C Preprocessor系列,其實就是CPP manual的讀書心得。
initial processing
在初始化的處理上,CPP主要有四個任務:
- 讀取檔案到記憶體中。
- 如果有啟動trigraphs,那麼會做trigraph轉換,如??/轉換成\,??-轉成~等等。
- 合併連續行成一行,就是每行後面有\當結尾的,會將該行和下一行合併成一行。
- 將註解的部分用一個空白取代。
Tokenization
CPP使用空白區分token,如果沒有token之間沒有空白,那麼CPP會使用greedy法則,也就是盡可能的找最長字串的token,比如a++++b會被解釋成a++ ++ +b,而不是a++ + ++b,而token之間預設也是一個空白,比如:
#define foo() bar
foo()baz
會被解釋成bar baz,不管#define中的foo()和bar有幾個空白。
brook@debian:~$ echo -e "#define foo() bar\nfoo()baz"|gcc -E -
# 1 "<stdin>"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "<stdin>"
bar baz
The preprocessing language
所謂的preprocessing language,就是以#開頭的那些preprocessing directive,這些指令是固定的,無法讓使用者自訂新的。這些指令主要有四種用途:
- include of header file(比如#include)。
- Macro expansion(比如將#define展開)。
- Conditional Compilation(比如#if,決定哪些要被編譯,哪些不被編譯)。
- Line control(我不是很清楚,只知道#line num下一行就會由num當行數重新計算,也可以簡寫成# num)。
- Diagnostic(比如#error)。
Header files
Header files主要分成兩類:
- System header files:提供OS部分的interface,<xx.h>。
- Your own header file:提供source files的interface,"xx.h"。
傳統上,C的header files都使用.h當結尾,當您在#include
時,其實就是將xx.h複製到該行上,並利用Line control,重新計算後面的行數,比如:
brook@desktop:~$ cat a.h
#ifndef A_H
#define A_H
int hello(char *str);
#endif
brook@desktop:~$ cat a.c
#include
#include "a.h"
int main(int argc, char * argv[])
{
printf("hello world\n", __LINE__);
return 0;
}
brook@desktop:~$ cpp -I./ a.c
... 略 ...
# 2 "a.c" 2
# 1 "a.h" 1
int hello(char *str);
# 3 "a.c" 2
int main(int argc, char * argv[])
{
printf("hello world\n", 6);
return 0;
}
看到把a.h中的int hello(char *str)複製到source file中了嗎?隨後又重新計算行數# 3,而header files並沒有規定只能放啥東西,不過通常就是放置一些宣告。
include syntax
#include有兩種變形:
- #include <file>:for system header files,就是會尋找系統路徑。
- #include "file":for your own header files,會先尋找目前目錄,找不到就依循的search path尋找。
注意:有些OS會使用'\'當pathname separator,比如M$,但是還是請使用'/',因為GCC一律都使用'/'當pathname separator。
search path
一般在UNIX底下,system header files的search path為
/usr/local/include
libdir/gcc/target/version/include
/usr/target/include
/usr/include
透過GCC實際觀察一下吧
brook@desktop:~$ gcc -I./ -v a.c
... 略 ...
ignoring nonexistent directory "/usr/local/include/i486-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../i486-linux-gnu/include"
ignoring nonexistent directory "/usr/include/i486-linux-gnu"
#include "..." search starts here:
#include <...> search starts here:
./
/usr/local/include
/usr/lib/gcc/i486-linux-gnu/4.3.2/include
/usr/lib/gcc/i486-linux-gnu/4.3.2/include-fixed
/usr/include
End of search list.
... 略 ...
您可以透過-I新增search path,新增的search path會優先被尋找,但是如果新增的search path已經在預設的(系統的)search path中,該search path會被忽略,避免影響預設的search path順序。有興趣可以再研究一下-I-和-iquote。
Once-Only headers
一個header files很可能被include兩次,進而造成重複define,引起compile error,標準做法應該是在header file中,使用conditional compilation防止被引入兩次,如:
#ifndef XX_H
#define XX_H
the entire file
#endif /* !XX_H */
XX_H我們稱為controlling macro或guard macro。macro name不應該使用'_'開頭的命名方式,而在system header files中應該使用'__'開頭,避免與user program造成衝突。
computed include
有時候我們會需要根據不同的configure引入不同的header files,比如:
#if SYSTEM_1
#include "system1.h"
#elif SYSTEM_2
#include "system2.h"
#elif
... 略 ...
#endif
像這樣需要經過計算然後才引入的行為就稱為computed include,你可以發現很就會被許多的#elif淹沒,你可以使用簡單的方式替代,比如:
#define SYSTEM1_H <stdio.h>
#define SYSTEM2_H "system3.h"
... 略 ...
#include SYSTEM1_H
#include SYSTEM2_H
甚至SYSTEM_H可以由Makefile傳遞進來。
System Headers是用來宣告OS和runtime libraries的interface,system header所產生的warning除了#warning會顯示出來,其他的都會被抑制住。一般而言,當GCC在編譯時就已經設定哪些目錄會被當成system header存放的目錄了,不過我們還是可以透過-isystem和#pragma GCC system_header兩種方式,將一般的header file當成system header file。
- -isystem後面接的目錄會被當成system header,用法上和-I一樣。
- #pragma GCC system_header這個指令告訴GCC把這個header file當成system header。
brook@ubuntu:~$ cat syshdr.c
#include <stdio.h>
#include "syshdr1.h"
#include "syshdr2.h"
#include "syshdr3.h"
int main(int argc, char *argv[])
{
syshdr1();
syshdr2();
syshdr3();
return 0;
}
brook@ubuntu:~$ cat syshdr/syshdr1.h
#ifndef SYSHDR1_H
#define SYSHDR1_H
int syshdr1(void) { printf("%s\n", __FUNCTION__); }
#endif
brook@ubuntu:~$ cat syshdr2.h
#ifndef SYSHDR2_H
#define SYSHDR2_H
#pragma GCC system_header
int syshdr2(void) { printf("%s\n", __FUNCTION__); }
#endif
brook@ubuntu:~$ cat syshdr3.h
#ifndef SYSHDR3_H
#define SYSHDR3_H
int syshdr3(void) { printf("%s\n", __FUNCTION__); }
#endif
brook@ubuntu:~$ gcc -Wall -isystem syshdr syshdr.c -o my_syshdr
syshdr.c: In function ‘syshdr3’:
syshdr3.h:3: warning: control reaches end of non-void function