C语言#line、#error和#pragma命令,以及_pragma运算符

本文将介绍几种预处理命令及其使用方法,其中包括 #line 命令、#error 命令和 #pragma 命令。此外,还讲述了_Pragma 运算符的相关知识。

定义行号

编译器会在警告消息、错误消息与调试信息中包含代码所在的行号与所在的源文件名,并提供给调试工具。你可以在源代码中利用 #line 命令改变编译器默认指定的文件名与行号信息。#line 命令的语法如下:
#line line_number ["filename"]

#line 命令的下一行行号会指定为 line_number。如果该命令也包含可选字符串字面量"filename",那么该编译器会把该字符串名称作为当前源文件名。

line_number 必须是大于 0 的十进制常量。如下例所示:
#line 1200 "primary.c"

包含 #line 命令的那一行代码,也可以包含其他宏。如果包含其他宏,预处理器会先展开所有宏,然后再执行 #line 命令。但要确保在宏展开后,#line 命令是正确的。

程序可以利用标准预定义宏 __LINE__ 和 __FILE__ 来访问当前的行号和文件名设置
printf( "This message was printed by line %d in the file %s.\n",
        __LINE__, __FILE__ );

#line 命令通常用在将 C 源代码作为输出的程序上。通过将对应的输入文件行号放置在 #line 命令内,程序可以让 C 编译器的错误消息指向源文件中相应的行。

生成错误消息

无论是否有实际错误,#error 命令都会让预处理器发出错误消息。它的语法如下:
#error [text]

如果上述命令存在可选项 text,则 text 就会被包含在预处理器的错误消息中。然后,编译器会终止处理源代码,并结束执行,仿佛遇到了严重错误。text可以是任意预处理器记号序列。如果 text 中有其他宏,它们都不会被展开。最好在这里使用字符串字面量,以避免标点符号字符(如单引号)的影响。

下面的例子测试标准宏 __STDC__ 是否已经被定义。如果没有,则生成一个错误消息:
#ifndef __STDC__
  #error "This compiler does not conform to the ANSI C standard."
#endif

#pragma 命令

#pragma 命令是向编译器提供额外信息的标准方法,其格式如下:
#pragma [tokens]

如果 #pragma 之后的第一个标记(token)是 STDC,那么该命令就是一个标准 pragma。否则,该 #pragma 命令的作用取决于实现版本。为了保障代码的可移植性,应该尽量少使用 #pragma 命令。

如果预处理器支持所指定的标记,就会执行这些标记所代表的动作,或者把该信息传递给编译器。如果预处理器不支持所指定的标记,就忽略该 #pragma 命令。

例如,最新版本的 GNU C 编译器和微软 Visual C 编译器都支持 #pragma pack(n),它使得编译器让结构成员对齐到特定的字节边界。下面的例子使用 pack(1)指示每个结构成员必须对齐到字节边界:
#if defined( __GNUC__ ) || defined( _MSC_VER )
  #pragma pack(1)                             // 对齐字节,没有填充
#endif

单字节对齐方式可以确保结构成员之间不会有间隙。pack 的参数 n 通常是 2 的幂(但幂值较小)。例如,pack(2)把结构成员对齐到偶数地址,而 pack(4)把结构成员对齐到 4 为倍数的地址。pack()没有参数,它指示对齐方式设置为实现版本的默认值。

C99 新增下面三个标准的 pragma:
#pragma STDC FP_CONTRACT on_off_switch
#pragma STDC FENV_ACCESS on_off_switch
#pragma STDC CX_LIMITED_RANGE on_off_switch
on_off_switch 的值必须是 ON、OFF 或 DEFAULT。

_Pragma 运算符

无法通过宏展开创建一个 #pragma 命令(或任何其他命令)。当碰到需要这么做的情况时,C99 新增了一个预处理运算符 _Pragma,它可以与宏配合使用。它的语法如下:
_Pragma (string_literal )

_Pragma 运算符的工作方式是这样的。首先,该 string_literal 操作数会被“解字符串化”(destringized),或被转换为预处理器记号序列,该过程如下:删除字符串前后的双引号;使用"替代 \";使用 \ 替代 \\。然后,预处理器会翻译前述结果序列的记号,类似于 #pragma 命令。

下面这一行代码定义了一个名为 STR 的宏,借此可以使用 _Pragma 运算符重写 #pragma 命令:
#define STR(s)   #s             // 这个#是“字符串化”运算符

有了上述定义,下面两行代码是等同的:
#pragma tokens
_Pragma ( STR(tokens) )

下面的例子是在宏中使用 _Pragma 运算符:
#define ALIGNMENT(n) _Pragma( STR(pack(n)) )
ALIGNMENT(2)

宏替换会把调用宏 ALIGNMENT(2)的过程重写为如下形式:
_Pragma( "pack(2)" )

预处理器接着处理这行代码,实现方式与使用下面的命令一样:
#pragma pack(2)