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