Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 的一种实现

/* assert.h standard header */
#undef assert	/* remove existing definition */

#ifdef NDEBUG
	#define assert(test)	((void)0)
#else		/* NDEBUG not defined */
	void _Assert(char *);
	/* macros */
	#define _STR(x) _VAL(x)
	#define _VAL(x) #x
	#define assert(test)	((test) ? (void)0 \
		: _Assert(__FILE__ ":" _STR(__LINE__) " " #test))
#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 函数是我们自己定义的,在另一个源文件中:

/* xassert.c _Assert function */
#include <stdio.h>
#include <stdlib.h>

void _Assert(char *mesg)
{		/* print assertion message and abort */
	fputs(mesg, stderr);
	fputs(" -- assertion failed\n", stderr);
	abort();
}

注意,在头文件 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 放在同一个目录下。

/* main.c */
#include "assert.h"

int main(void)
{
	assert(2>3);
	return 0;
}

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

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

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

例 21.4. 特殊标识符__func__

#include <stdio.h>

void myfunc(void)
{
	printf("%s\n", __func__);
}

int main(void)
{
	myfunc();
	printf("%s\n", __func__);
	return 0;
}
$ gcc main.c
$ ./a.out
myfunc
main