4. 其它预处理特性

#pragma预处理指示供编译器实现一些非标准的特性,C标准没有规定#pragma后面应该写什么以及起什么作用,由编译器自己规定。有的编译器用#pragma定义一些特殊功能寄存器名,有的编译器用#pragma定位链接地址,本书不做深入讨论。如果编译器在代码中碰到不认识的#pragma指示则忽略它,例如gcc#pragma指示都是#pragma GCC ...这种形式,用别的编译器编译则忽略这些指示。

C标准规定了几个特殊的宏,在不同的地方使用可以自动展开成不同的值,常用的有__FILE____LINE____FILE__展开为当前源文件的文件名,是一个字符串,__LINE__展开为当前代码行的行号,是一个整数。这两个宏在源代码中不同的位置使用会自动取不同的值,显然不是用#define能定义得出来的,它们是编译器内建的特殊的宏。在打印调试信息时打印这两个宏可以给开发者非常有用的提示,例如在第 6 节 “折半查找”我们看到assert函数打印的错误信息就有__FILE____LINE__的值。现在我们自己实现这个assert函数,以理解它的原理。这个实现出自[Standard C Library]

例 21.3. assert.h的一种实现

  1. /* assert.h standard header */
  2. #undef assert /* remove existing definition */
  3.  
  4. #ifdef NDEBUG
  5. #define assert(test) ((void)0)
  6. #else /* NDEBUG not defined */
  7. void _Assert(char *);
  8. /* macros */
  9. #define _STR(x) _VAL(x)
  10. #define _VAL(x) #x
  11. #define assert(test) ((test) ? (void)0 \
  12. : _Assert(__FILE__ ":" _STR(__LINE__) " " #test))
  13. #endif

通过这个例子可以全面复习本章所讲的知识。C标准规定assert应该实现为宏定义而不是一个真正的函数,并且assert(test)这个表达式的值应该是void类型的。首先用#undef assert确保取消前面对assert的定义,然后分两种情况:如果定义了NDEBUG,那么assert(test)直接定义成一个void类型的值,什么也不做;如果没有定义NDEBUG,则要判断测试条件test是否成立,如果条件成立就什么也不做,如果不成立则调用_Assert函数。假设在main.c文件的第33行调用assert(is_sorted()),那么__FILE__是字符串"main.c"__LINE__是整数33#test是字符串"is_sorted()"。注意_STR(__LINE__)的展开过程:首先展开成_VAL(33),然后进一步展开成字符串"33"。这样,最后_Assert调用的形式是_Assert("main.c" ":" "33" " " "is_sorted()"),传给_Assert函数的字符串是"main.c:33 is_sorted()"_Assert函数是我们自己定义的,在另一个源文件中:

  1. /* xassert.c _Assert function */
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4.  
  5. void _Assert(char *mesg)
  6. { /* print assertion message and abort */
  7. fputs(mesg, stderr);
  8. fputs(" -- assertion failed\n", stderr);
  9. abort();
  10. }

注意,在头文件assert.h中自己定义的内部使用的标识符都以_线开头,例如_STR_VAL_Assert,因为我们在模拟C标准库的实现,在第 3 节 “变量”讲过,以_线开头的标识符通常由编译器和C语言库使用,在/usr/include下的头文件中你可以看到大量_线开头的标识符。另外一个问题,为什么我们不直接在assert的宏定义中调用fputsabort呢?因为调用这两个函数需要包含stdio.hstdlib.h,C标准库的头文件应该是相互独立的,一个程序只要包含assert.h就应该能使用assert,而不应该再依赖于别的头文件。_Assert中的fputs向标准错误输出打印错误信息,abort异常终止当前进程,这些函数以后再详细讨论。

现在测试一下我们的assert实现,把assert.hxassert.c和测试代码main.c放在同一个目录下。

  1. /* main.c */
  2. #include "assert.h"
  3.  
  4. int main(void)
  5. {
  6. assert(2>3);
  7. return 0;
  8. }

注意#include "assert.h"要用"引号而不要用<>括号,以保证包含的是我们自己写的assert.h而非C标准库的头文件。然后编译运行:

  1. $ gcc main.c xassert.c
  2. $ ./a.out
  3. main.c:6 2>3 -- assertion failed
  4. Aborted

在打印调试信息时除了文件名和行号之外还可以打印出当前函数名,C99引入一个特殊的标识符__func__支持这一功能。这个标识符应该是一个变量名而不是宏定义,不属于预处理的范畴,但它的作用和__FILE____LINE__类似,所以放在一起讲。例如:

例 21.4. 特殊标识符__func__

  1. #include <stdio.h>
  2.  
  3. void myfunc(void)
  4. {
  5. printf("%s\n", __func__);
  6. }
  7.  
  8. int main(void)
  9. {
  10. myfunc();
  11. printf("%s\n", __func__);
  12. return 0;
  13. }
  1. $ gcc main.c
  2. $ ./a.out
  3. myfunc
  4. main