C 语言中一些不常用的指令

目录汇总:C 语言零基础入门教程

在这个部分的 C 语言零基础入门教程 的最后,我们将简要地了解一下 #error 指令、#line 指令和 #pragma 指令。与前面讨论过的指令相比,这些指令更专业,使用频率也低得多。

一、#error 指令

#error 指令有如下格式:

1
[#error指令] #error 消息

其中,消息是任意的记号序列。如果预处理器遇到 #error 指令,它会显示一条包含消息的出错消息。对于不同的编译器,出错消息的具体形式也可能会不一样。格式可能类似:

1
Error directive: 消息

或者

1
#error 消息

遇到 #error 指令预示着程序中出现了严重的错误,有些编译器会立即终止编译,不再检查其他错误。

#error 指令通常与条件编译指令一起用于检测正常编译过程中不应出现的情况。例如,假定我们需要确保一个程序无法在一台 int 类型不能存储小于 100 000 的数的机器上编译。允许的最大 int 值用 INT_MAX 宏表示,所以我们需要做的就是当 INT_MAX 宏小于 100 000 时调用 #error 指令:

1
2
3
#if INT_MAX < 100000
#error int type is too small
#endif

如果试图在一台以 16 位存储整数的机器上编译这个程序,将产生一条出错消息:

1
Error directive: int type is too small

#error 指令通常会出现在 #if-#elif-#else 序列中的 #else 部分:

1
2
3
4
5
6
7
8
9
#if defined(WIN32)
...
#elif defined(MAC_OS)
...
#elif defined(LINUX)
...
#else
#error No operating system specified
#endif

二、#line 指令

#line 指令用来改变程序行的编号方法。(正如你所期望的那样,行的编号通常是按 1, 2, 3, …来进行的。)我们也可以使用这条指令使编译器认为它正在从一个有不同名字的文件中读取程序。

#line 指令有两种形式。第一种形式只指定行号:

1
[#line指令(形式1] #line n

n 必须是 1~32 767(C99 中是 2 147 483 647)范围内的整数。这条指令导致程序中后续的行被编号为 n、n+1、n+2 等。

#line 指令的第二种形式同时指定行号和文件名:

1
[#line指令(形式2] #line n "文件"

指令后面的行会被认为来自文件,行号由 n 开始。n 和文件字符串的值可以用宏指定。

#line 指令的一种作用是改变 __LINE__ 宏(可能还有 __FILE__ 宏)的值。更重要的是,大多数编译器会使用来自 #line 指令的信息生成出错消息。例如,假设下列指令出现在文件 foo.c 的开头:

1
#line 10 "bar.c"

现在,假设编译器在 foo.c 的第 5 行发现一个错误。出错消息会指向 bar.c 的第 13 行,而不是 foo.c 的第 5 行。(为什么是第 13 行呢?这是因为指令占据了 foo.c 的第 1 行,因此对 foo.c 的重新编号从第 2 行开始,并将这一行作为 bar.c 的第 10 行。)

乍一看,#line 指令使人迷惑。为什么要使出错消息指向另一行,甚至是另一个文件呢?这样不是会使程序变得难以调试吗?

实际上,程序员并不经常使用 #line 指令。它主要用于那些产生 C 代码作为输出的程序。最著名的程序之一是 yacc(Yet Another Compiler-Compiler),它是一个用于自动生成编译器的一部分的 UNIX 工具(yacc 的 GNU 版本称为 bison)。在使用 yacc 之前,程序员需要准备一个包含 yacc 所需要的信息以及 C 代码段的文件。通过这个文件,yacc 生成一个 C 程序 y.tab.c,并合并程序员提供的代码。程序员接着按照正常方法编译 y.tab.c。通过在 y.tab.c 中插入 #line 指令,yacc 会使编译器认为代码来自原始文件,也就是程序员写的那个文件。于是,任何编译 y.tab.c 时产生的出错消息会指向原始文件中的行,而不是 y.tab.c 中的行。其最终结果是,调试变得更容易,因为出错消息都指向程序员编写的文件,而不是由 yacc 生成的(那个更复杂的)文件。

三、#pragma 指令

#pragma 指令为要求编译器执行某些特殊操作提供了一种方法。这条指令对非常大的程序或需要使用特定编译器的特殊功能的程序非常有用。

#pragma 指令有如下形式:

1
[#pragma指令] #pragma 记号

其中,记号是任意记号。#pragma 指令可以很简单(只跟着一个记号),也可以很复杂:

1
#pragma data(heap_size => 1000, stack_size => 2000)

#pragma 指令中出现的命令集在不同的编译器上是不一样的。你必须通过查阅你所使用的编译器的文档来了解可以使用哪些命令,以及这些命令的功能。顺便提一下,如果 #pragma 指令包含了无法识别的命令,预处理器必须忽略这些 #pragma 指令,不允许给出出错消息。

C89 中没有标准的编译提示(pragma),它们都是在实现中定义的。C99 有 3 个标准的编译提示,都使用 STDC 作为 #pragma 之后的第一个记号。这些编译提示是 FP_CONTRACTCX_LIMITED_RANGEFENV_ACCESS

四、_Pragma 运算符

C99 引入了与 #pragma 指令一起使用的 _Pragma 运算符。_Pragma 表达式可以具有如下形式:

1
[_Pragma表达式] _Pragma (字面串)

遇到该表达式时,预处理器通过移除字符串两端的双引号,并分别用字符 "\ 代替转义序列 \"\\ 来实现对字面串(C99 标准中的术语)的“去串化”。表达式的结果是一系列的记号,这些记号被当作 pragma 指令中的记号。例如:

1
_Pragma("data(heap_size => 1000, stack_size => 2000)")

1
#pragma data(heap_size => 1000, stack_size => 2000)

是一样的。

_Pragma 运算符使我们摆脱了预处理器的局限性:预处理指令不能产生其他指令。因为 _Pragma 是运算符而不是指令,所以可以出现在宏定义中。这使得我们能够在 #pragma 指令后面进行宏的扩展。

现在来看一个 GCC 手册中的例子。下面的宏使用了 _Pragma 运算符:

1
#define DO_PRAGMA(x) _Pragma(#x)

宏调用如下:

1
DO_PRAGMA(GCC dependency "parse.y")

扩展后的结果:

1
#pragma GCC dependency "parse.y"

这是 GCC 支持的一种编译提示。[如果指定的文件(本例中是 parse.y)比当前文件(正被编译的文件)还要新,会给出警告消息。]需要注意的是,DO_PRAGMA 调用的参数是一系列的记号。DO_PRAGMA 定义中的 # 运算符会导致这些记号被串化为 "GCC dependency \"parse.y\"",这个字符串随后作为参数传递给 _Pragma 运算符,该运算符对其进行去串化操作,从而得到包含原始记号的 #pragma 指令。

请参阅

(完)

comments powered by Disqus