【C】 02 - 程式結構和預先處理

來源:互聯網
上載者:User

標籤: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 - 程式結構和預先處理

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.