2009年8月15日 星期六

GCC (4.4.1)之C Preprocessor part I


C Preprocessor系列,其實就是CPP manual的讀書心得。

initial processing 在初始化的處理上,CPP主要有四個任務:
  1. 讀取檔案到記憶體中。
  2. 如果有啟動trigraphs,那麼會做trigraph轉換,如??/轉換成\,??-轉成~等等。
  3. 合併連續行成一行,就是每行後面有\當結尾的,會將該行和下一行合併成一行。
  4. 將註解的部分用一個空白取代。
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,這些指令是固定的,無法讓使用者自訂新的。這些指令主要有四種用途:
  1. include of header file(比如#include)。
  2. Macro expansion(比如將#define展開)。
  3. Conditional Compilation(比如#if,決定哪些要被編譯,哪些不被編譯)。
  4. Line control(我不是很清楚,只知道#line num下一行就會由num當行數重新計算,也可以簡寫成# num)。
  5. Diagnostic(比如#error)。


Header files Header files主要分成兩類:
  1. System header files:提供OS部分的interface,<xx.h>。
  2. 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有兩種變形:
  1. #include <file>:for system header files,就是會尋找系統路徑。
  2. #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



3 則留言:

  1. http://c-faq.com/decl/auto.html
    What's the auto keyword good for?

    The auto keyword is useless in the C language. It is there because before the C language there existed a B language in which that keyword was necessary for declaring local variables. (B was developed into NB, which became C).

    https://stackoverflow.com/questions/2192547/where-is-the-c-auto-keyword-used

    回覆刪除
  2. http://c-faq.com/decl/extern.html
    What does extern mean in a function declaration?

    extern is significant only with data declarations. In function declarations, it can be used as a stylistic hint to indicate that the function's definition is probably in another source file, but there is no formal difference between
    extern int f();
    and int f();

    回覆刪除
  3. http://c-faq.com/decl/typedefvsdefine.html
    What's the difference between using a typedef or a #define for a user-defined type?

    In general, typedefs are preferred, in part because they can correctly encode pointer types. For example, consider these declarations:
    typedef char *String_t;
    #define String_d char *
    String_t s1, s2;
    String_d s3, s4;

    s1, s2, and s3 are all declared as char *, but s4 is declared as a char, which is probably not the intention.

    回覆刪除

熱門文章