[前言:使用__FILE__和__LINE__來定位錯誤已經屢見不鮮,然而其中一些道理又有幾個人仔細探究過。本文參考了Curtis Krauskopf的一篇名為
Using __FILE__ and __LINE__ to Report Errors的文章,希望達到解惑之效。]
問題:當運行時錯誤產生時,我怎樣才能得到包含C++文件名和行號的字符串信息?
回答:在C++中的__FILE__預編譯指示器包含了被編譯的文件名,而__LINE__則包含了源代碼的行號。__FILE__和__LINE__的前后都包含了兩個下劃線,讓我們仔細看看__FILE__所包含的每個字符:
_ _ F I L E _ _
下面展示了在控制臺程序中如果顯示文件名和代碼行號。
#include <stdio.h>
int main(int , char**)
{
printf("This fake error is in %s on line %d\n", __FILE__, __LINE__);
return 0;
}
輸出結果:
This fake error is in c:\temp\test.cpp on line 5
讓我們更上一層樓
我想通過一個通用函數error()來報告錯誤,以使當某個錯誤發生時我能設置斷點以及隔離錯誤處理(例如,在屏幕上打印錯誤信息或者寫入日志)。因此,函數的原型應該是這樣的吧:
void error(const char *file, const unsigned long line, const char *msg);
調用方式如下:
error(__FILE__, __LINE__, "my error message");
預處理魔法
這里有三個問題需要解決:
- __FILE__和__LINE__在每次調用error時作為參數傳入。
- __FILE和__LINE__前后的下劃線很容易被遺忘,從而導致編譯錯誤。
- __LINE__是一個整數,這無疑增加了error函數的復雜度。我絕不想直接使用整型的__LINE__,而通常都是將轉換為字符串打印到屏幕或寫入日志文件。
__FILE__和__LINE__應該被自動處理,而非每次作為參數傳遞給error,這樣會讓error的使用者感覺更爽些,它的形式可能是下面這樣:
error(AT, "my error message");
我希望上面的宏AT展開為:"c:\temp\test.cpp:5"。而新的error函數則變成:
void error(const char *location, const char *msg);
因為Borland C++ Builder編譯器能夠自動合并相鄰的字符串,因此我把AT寫成下面這樣:
#define AT __FILE__ ":" __LINE__
然而它卻罷工了,因為__LINE__被擴展成了整數而非字符串,所以宏展開后變成:
"c:\temp\test.cpp" ":" 5
這是一個無效的字符串,因為末尾有一個未被雙引號包含的整數。
怎么辦?別著急,一個特殊的預編譯指示器“#”能夠幫我們將一個變量轉換成字符串。所以重新定義宏:
#define AT __FILE__ ":" #__LINE__
嘿嘿,這樣總行了吧。別高興得太早,這樣也是不行的。因為編譯器會抱怨#是個無效字符。其實,問題是#預編譯指示器只有這樣使用才會
被正確識別:
#define symbol(X) #X
因此,我把代碼改為:
#define STRINGIFY(x) #x
#define AT __FILE__ ":" STRINGIFY(__LINE__)
然而,奇怪的結果產生了,__LINE__居然被作為了輸出的一部分:
c:\temp\test.cpp:__LINE__: fake error
解決方法是再用一個宏來包裝STRINGIFY():
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
#define AT __FILE__ ":" TOSTRING(__LINE__)
OK,我們用下面的代碼來試試:
#include <stdio.h>
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
#define AT __FILE__ ":" TOSTRING(__LINE__)
void error(const char *location, const char *msg)
{
printf("Error at %s: %s\n", location, msg);
}
int main(int , char**)
{
error(AT, "fake error");
return 0;
}
輸出結果:
Error at c:\temp\test\test.cpp:11: fake error
Visual Studio下的實踐
在《Windows核心編程》中,Jeffrey Richter提供了下面的宏在編譯期輸出有用信息:
#define chSTR2(x) #x
#define chSTR(x) chSTR2(x)
#define chMSG(desc) message(__FILE__ "(" chSTR(__LINE__) "):" #desc)
message是一個預編譯指令,上面宏的使用方法是:
#pragma chMSG(Fix this later)
結論
- 預編譯指示器__FILE__和__LINE__能夠提供有用的信息。
- __LINE__的處理方式遠比我們想象的要復雜。
- __FILE__被處理成字符串,給我們帶來了不少方便。