標籤:des style blog color os 使用 io ar 檔案
在正式進入C的文法之前,有必要對其整體外觀和組成元素作一個瀏覽。這部分內容對大多數人是比較陌生的,但它們卻是C的起點和骨架。而這些內容涉及的背景或細節又可以展開為專門的課題,這裡也只是淺嘗則止,說明個大概即可。
1. C程式組成
任何一個程式都首先以源檔案(source file)的形式存在,它是一個普通的文字檔。C程式一般由一系列尾碼為.c和.h的檔案組成,前者包含了程式的執行內容,後者包含了各種聲明或定義。其實檔案名稱並不重要,這樣的尾碼名僅是約定俗成的習慣。但建議保持這樣的風格,一是為了看程式的人能一目瞭然,二是在整合式開發環境(IDE)裡它們已經成為C檔案的標識。
文字檔有許多字元組成,這些字元的編碼方法由編輯器決定。對C編譯器有意義的是它們所表示的字元而非編碼本身,預先處理開始前會將這些字元對應表成source character set(一般是UTF-8)。預先處理就是在該字元集下進行的,預先處理後還會將字元和字串映射成execution character set,它由目標平台決定,但一般和前者相同。
這兩種字元集都包含base character set,它就是我們正常使用英文和符號(編碼在兩個字元集中相同),字母是區分大小寫。新標準還支援extended character set,它可出現在兩種字元集中。比如在source字元集中可在多處使用unicode:identifier、char constant、string literal、headfile name、comment、preprocessing token。樣本如下(需編譯器支援或開啟開關),但不建議這樣的編碼風格。
// Define variable αwchar_t \u03B1 = L‘α‘;
C語言的編譯是以translation unit為單元的,它是預先處理後的.c檔案。各單元的編譯互不相干,連接器最終會把它們和庫一起整合成執行檔案。關於編譯、串連和調試,我打算另開課題,這裡不深入討論。以下是一個多檔案程式的常見錯誤,但在編譯串連時並不會報錯,因為無法跨unit檢查文法。運行時檔案2中會把數組a的元素當地址使用,出現錯誤。
// File 1int a[3];// File 2extern int *a; // should be a[]
C程式可能獨立運行(嵌入式),也可能運行在作業系統中。C規範對這兩種情況的要求稍微有點不同,分別叫freestanding implementation和hosted implementation。其中後者要求實現更多的庫,而且必須有一個main函數。而前者只需要少數必要的庫,程式入口不作規定(但建議也用main)。main函數可有以下兩種形式,對第二種形式,規範要求argv[0]為程式名,argv[argc] = NULL。
int main(void);int main(int argc, char* argv[]); // or char** argv
程式運行時,記憶體中除了常量區(代碼和字串)、資料區外還會有堆和棧區。一般棧底在高地址,向低地址增長。hosted程式的地址一般是邏輯地址,運行時由OS負責映射為物理地址。
2. 預先處理步驟
第0步,字元集映射。將來源程式文字檔的字元對應表為source字元集,甚至包括將分行符號的統一編碼。C還要求每行都以分行符號結束,如果檔案尾沒有換行,編譯器會warning(需開啟)。
第1步,trigraph sequance。為支援某些古老的鍵盤,C使用??x來轉義它們沒有的符號。所以請在代碼中避免使用??序列,字串中可使用\?轉義。下表是規範支援的trigraph sequance,不在該表中的不進行轉義。
??( |
??) |
??< |
??> |
??= |
??/ |
??‘ |
??! |
??- |
[ |
] |
{ |
} |
# |
\ |
^ |
| |
~ |
第2步,去除“\+斷行符號”。將該組合去除,不產生或消除空白。所以identifier中也可以被斷開,但它一般用於宏定義和字串換行(見示意代碼)。注意下一行的前置空白會被保留,所以不能為了格式對齊而添加空格。
#define INC(a) \{ a++; }char str[] = "this is a long string";
第3步,preprocessing token。將注釋換成一個空格,解析pp token和空白(white space)。空白包括空格、換行、tab等,換行被保留,其它空白的處理基於實現。注釋/**/可跨行,不可以嵌套。如果想臨時注釋掉一段代碼,最好用#if 0。注釋//作用到本行末,舊C不支援該用法。
// should use #if 0/*int a; /* declare var */*/
第4步,預先處理指令。展開宏,匯入包含檔案,執行預先處理指令,直至結束。預先處理指令(directive)以#開頭,僅包括當前行,#前後可以有空白。
第5步,字串處理。字元(串)映射為execution字元集,包括將逸出序列\x編碼。將相鄰字串拼接為一個字串,只添加一個結束符‘\0‘。
char str[] = "This is a " "long string";
第6步,C token。將pp token映射為C token,空白被丟棄。
3. Token 解析
程式設計語言在字元集的基礎上進行詞法(Lexical)、文法(Syntax)和語義(Semantic)的分析。C詞法分析就是將程式分解為token,這一步在預先處理階段完成。token一般不會被賦予太多的意義,只是根據序列特徵大致分類,編譯器根據這些特徵解析出一個個token。token解析採用貪婪原則(也稱最長原理),一個token的下一個字元與它不能再組成有意義的token。不滿足貪婪原則的分割,即使有意義,也是不被採用的。預先處理將token大致分為四類:identifier,data constant,string,punctual。
identifier即標識,它包括directive、keyword、object、function、tag、member、name、lable、macro等,簡單說就是用來表示某個東西的名字。identifier的詞法大家都熟悉,就是由字母、數字和‘_‘組成,但不由數字開頭。identifier不宜過長,因為有些編譯器會進行截取。另外在起名字時盡量迴避關鍵字還有__xxx__和_Axx_(大寫字母開頭),它們都預留給系統使用。
data constant就是各種常量,包括整形、浮點、字元等。它們有自己的格式,在下一章將有描述。headfile name和string literal以<>或“”作為邊界,其中可以含有空格。在其它場合,空白和符號往往是分割token的邊界。
punctual就是各種符號。它同樣也遵循貪婪原則,以最長的有意義符號串作為一個token,注意其中不能有空白。另外C還支援digraph逸出序列(下表),但和trigraph不同,它是在token解析時進行的。
<: |
:> |
<% |
%> |
%: |
%:%: |
[ |
] |
{ |
} |
# |
## |
需要強調的是,token解析是在預先處理階段完成的,而且除特殊情況外不重新解析。預先處理裡中的token到C編譯時間會做一些調整(字元轉義、丟棄空白等),但token的分割已經完成。也就是說token解析完成後,程式的組成單位就是token,而不是字元集了。由此可見,宏定義不光是簡單的字串替換,至少它還影響了token的解析。以下的例子能很好的說明本段的一些內容。
#define plus +a+++b; // (a++) + ba+ ++b; // a + (++b)a+ + +b; // illegala plus++b; // a + (++b)a+ =b; // illegala plus=b; // illegala/*p; // should be a/ *p
4. 預先處理指令4.1 宏
宏是預先處理中最複雜也是最強大的功能,這裡用單獨篇幅說明宏的使用。簡單來說,宏就是將宏identifier用其定義的token序列替代。替代的token序列的頭和尾的空白被去除,中間的空白可能被合并,包括宏參數也是這樣處理的,這與我們一直認識的“替代”還是有差別的。另外,宏只做替換,對常量運算式並不做計算。
#define r 1#define C (2*r*3.14) // (2 * 1 * 3.14), not 6.28#define mul(a, b) ( (a) * (b) )mul( 2 + 3 , 5 ); // ( (2 + 3) * (5) ), attention to the space
宏中有兩個可以改變token的操作符:#和##。#叫stringify operator,它可以將宏參數字串化。宏實參可能為token序列,其中可能有字串,這時將"和串中的\用\轉義(非串中的\不轉)。#僅作用於宏參數,不可用於一般token。
#define str #hi // not "hi"#define str(a) #astr(\a"hi\!"); // "\a\"hi\\!\""
##叫token pasting operator,用於合并token。它的左右操作對象可以是宏參數,也可以是一般token,它與操作對象間的空白會被去除。##甚至可以連用,串起更多的操作對象。#和##只在宏展開時起作用,如果宏結果中出現#或##,將不再起作用。
#define twoj # ## # // ###define fun(pre, post) pre##_f_##postfun(res, get)(); // res_f_get()
不帶參數的宏叫object-like macro,帶參數的叫function-like macro。函數宏的定義中,宏名與()之間不能有空白,參數可為空白。調用時宏名與()之間可有空白,而且新規範允許宏實參為空白。函數宏定義最好能讓使用者自由添加‘;‘,見do while語句。
#define add1 (a, b) (a+b)add1(1, 1); // wrong. (a, b) (a+b)(1, 1)#define add2(a, b) (a+b)add2 (1, 1); // ok, 1+1#define fun1() // ok#define fun2(a, b) add##a##bfun2(); // addfun2(1); // add1fun2(, 2); // add2
新規範中支援宏的變長參數,只需將末尾的參數用...表示即可。不同於C的變長參數,宏中不需要前置參數。在定義中用_VA_ARGS_代替實參,實參為token序列(包含‘,‘),頭尾沒有空白,中間可能有空白。
#define show(...) printf(#_VA_ARGS_)show( hi, there! ); // printf("hi, there!")#define fun(a, ...) a##_VA_ARGS_fun(1, 2, 3); // 12, 3fun(1); // 1
宏展開中最複雜的情況是宏嵌套,但其實只要弄清三點即可:(1)宏實參遇到#或##時,立即產生作用,不再繼續展開;(2)其它地方宏實參要先自行展開再帶入結果;(3)對結果中出現的曾經完整展開的宏或#、##,不作處理,其它宏則繼續展開。結合(1)(2),如果想先展開再做#或##,可以將#或##操作本身包在宏裡。以下代碼中,展開內層M(0)時外層M尚未完全展開,所以內層M(0)可展開。而f()展開為f後,f()不可再次展開。
#define one 1#define show(a) printf(#a" = %d\n", a)show(one); // printf("one = %d\n", 1)#define name edward#define str(s) #s#define show(a) printf(str(a))show(name); // printf("edward")#define M(x) x#define f() fM(M(0)); // 0f()(); // f()
宏展開是比較靠前執行的,#include和#if指令中都可以使用宏定義。由於整個檔案名稱(包括<>"")是一個token,宏也要定義完整的檔案名稱。宏不可以重定義,除非先#undef或定義完全一樣,這裡的一樣是指參數個數和token序列一樣,參數名可以不一樣。
#define name1 stdio#define name2 <stdio.h>#include <name1.h> // <name1.h>#include name2 // <stdio.h>#define add()#undef add#define add(a, b) a+b // ok#define add(x, y) x+y // ok#define add(x, y) x + y // illegal
系統提供了一些預定義宏,可以在程式中使用。以下是規範要求必須定義的宏,這些宏不可以#undef。有些不斷變化的宏(如__LINE__)其實是系統變數,規範要求每個函數開始都有一個隱藏定義static const char __FUNC__[] = file_name。
Macro |
Type |
Description |
__FILE__ |
"path\name" |
包含路徑,實際檔案 |
__LINE__ |
integer |
實際檔案 |
__DATE__ |
"Mmm dd yyyy" |
無值的位補0 |
__TIME__ |
"hh:mm:ss" |
無值的位補0 |
__STDC__ |
0 or 1 |
是否與規範相容 |
__STDC_VERSION__ |
yyyymmL |
所相容規範版本 |
__STDC_HOSTED__ |
0 or 1 |
是否有OS |
因為宏只是token替換,它隱含很多不利之處,有時要使用其它替代方法。用宏定義的整型無法在調試時顯示,可以用枚舉常量替代。宏定義的字串常量可能產生多份,可以用const string替代。函數宏無參數檢查且有副作用,可以用inline函數替代。
4.2 其它指令
#include指令將標頭檔包含在本unit中,後面跟標頭檔名。< >包含的檔案到庫目錄中尋找,編譯環境一般可以指定該目錄。" "包含的檔案先從目前的目錄下尋找,再到庫中尋找,從而可以先使用自訂的庫。為了消除重複包含,可以用宏(見樣本)或編譯器擴充語句。
為增加移植性和靈活性,預先處理支援conditional compiling。它一般以#if、#ifdef或#ifndef分支開頭,後面跟著0個或多個#elif分支,末尾最多一個#else分支,最後以#endif結束。條件運算式的結果要是整型,可以是整型常量、宏或defined運算子,不可使用字元(串)、浮點常量。defined是預先處理的唯一關鍵字,它有defined(M)和defined M兩種形式。
#line指令後跟行號和可選的檔案名稱,這個指令一般用於產生.c檔案的檔案中,使行號(檔案名稱)指向原始檔案,而非.c檔案。#error指令後跟任意token序列,這些token不能展開宏。預先處理遇到#error會掛起,並且顯示token序列。
#pragma指令由編譯器自訂行為,STDC開頭的指令預留給規範使用,目前已定義了一些功能開關。#pragma中不可以展開宏,新規範中使用關鍵字_Pragma("command")支援宏展開,它等價於#pragma command。
// headfile, only included once#ifndef HEAD.H#define HEAD.H// ...#endif#if defined X // the same as #ifdef X#elif defined(Y)// ...#elif#else#endif#line 100#line 100 "\test.c"#define name edward#error fail, name! // show fail, name!#define str(cmd) #cmd_Pragma(str(align(4))); // the same as #pragma align(4)
【C】 02 - 程式結構和預先處理