成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

C語言進階:程序預(yù)處理

binta / 2179人閱讀

摘要:如的語句被稱為預(yù)處理指令,還有注釋文本的刪除,都在此階段完成替換。故宏在程序規(guī)模和執(zhí)行速度方面更勝一籌。宏替換發(fā)生在預(yù)編譯期間,故無法調(diào)試。宏可能由于運算符優(yōu)先級的問題,會導(dǎo)致程序出錯。

程序預(yù)處理

本章節(jié)研究的是,源代碼文件test.c是如何一步步得到一個可執(zhí)行程序test.exe的。在之前的學(xué)習(xí)中可知.c文件要先后經(jīng)過編譯鏈接成.exe文件再執(zhí)行。

程序的編譯鏈接運行如下圖所示。翻譯中編譯又包括預(yù)編譯、編譯、匯編。

編譯鏈接執(zhí)行三個步驟,都需要為其配置不同的環(huán)境。編譯和鏈接在翻譯環(huán)境中,而執(zhí)行在運行環(huán)境中發(fā)生。

  • 翻譯環(huán)境:在該環(huán)境中源代碼被轉(zhuǎn)換成可執(zhí)行的機器指令。
  • 執(zhí)行環(huán)境:用于實際執(zhí)行代碼。

程序的翻譯環(huán)境

翻譯階段的大致流程如下圖所示。

組成一個程序的每個.c源文件都會被編譯器編譯,分別生成對應(yīng)的.obj目標文件。多個目標文件以及引入的鏈接庫被鏈接器鏈接在一起,形成一個單一的.exe可執(zhí)行程序。

編譯器即是一個用于編譯代碼的工具,在vs環(huán)境下為cl.exe的可執(zhí)行程序。連接器則是用于鏈接所有目標文件的工具,在vs中為link.exe的可執(zhí)行程序,鏈接庫是標準中任何被該程序用到的函數(shù)。如圖:

而若想觀察翻譯代碼過程中的每一個流程的具體細節(jié),在集成開發(fā)環(huán)境vs中不便展示,當(dāng)然我們可以使用Linux環(huán)境下的gcc編譯器。

此次演示就采用加法函數(shù),分別存放在兩個文件test.cadd.c

//1. add.cint Add(int x, int y){	int sum = x + y;	return sum;}//2. test.c#include  //聲明函數(shù)extern int Add(int x, int y);int main(){	int a = 10;	int b = 20;	int ret = 0;	ret = Add(a, b);	printf("ret = %d/n", ret);	return 0;}
預(yù)編譯

Linux環(huán)境下編寫完test.c文件的代碼后,輸入gcc test.c -E可以將代碼預(yù)編譯的結(jié)果輸出到屏幕上。還可以用gcc test.c -E -o test.i是將結(jié)果輸出到文件test.i。

#include,#define,#pragma的語句被稱為預(yù)處理指令,還有注釋文本的刪除,都在此階段完成替換。

所有可以看出預(yù)編譯階段的動作都是文本操作

  1. #include頭文件的包含
  2. #define預(yù)處理符號的替換
  3. 刪除注釋

預(yù)編譯,顧名思義,是在編譯前刪減代碼中的不必要的與機器識別代碼無關(guān)的內(nèi)容。被稱為文本操作

編譯

對預(yù)編譯產(chǎn)生的文件test.i再編譯gcc test.i -S,會自動生成匯編代碼test.s。

故編譯階段是將C語言代碼轉(zhuǎn)化為匯編代碼,這是整體現(xiàn)象。實際上會發(fā)生這四個動作:

  1. 詞法分析,語法分析,語義分析

詞法分析,語法分析,語義分析都是編譯器識別語句的操作。重點是接下接下來的符號匯總。

  1. 符號匯總

符號匯總,是只對全局符號進行匯總,局部符號是不進行匯總的。目的是能夠?qū)⑺形募械拇a組合到一起成一個完整的程序。如add.c文件中的函數(shù)名Add,還有test.c文件中的Addmain。

匯編

gcc test.s -C將編譯結(jié)束產(chǎn)生的匯編代碼轉(zhuǎn)化成了二進制指令(機器指令)存入二進制文件test.o中。

匯編階段會形成符號表,因為機器在調(diào)用指令時需要知道其存放的位置,所謂符號表大概就是符號和其地址的集合。如圖,可以假設(shè):

鏈接

鏈接將二進制指令目標文件test.o等,鏈接在一起形成可執(zhí)行程序test.out。目標文件test.oelf格式文件,在Linux平臺下可以用readelf翻譯并查看其內(nèi)容。

鏈接階段的動作是:

  1. 合并段表

所謂的鏈接,就是將對應(yīng)的段合并起來。

  1. 符號表的合并和重定位

符號表的合并,是將各自的符號表合并到一起。如test.o中的Add的無效地址,需把add.oAdd的地址合并過去再重定位到變量的真實地址,才是有意義的。

從編譯期間的符號匯總,到匯編時的形成符號表,再到鏈接時的合并和重定位符號表,都是為了最后生成可執(zhí)行程序時能夠找到并鏈接各個文件中的符號。

程序的執(zhí)行環(huán)境

  1. 程序首先載入內(nèi)存

    有的機器上有操作系統(tǒng),這個動作就是由操作系統(tǒng)完成,沒有的由手工完成。

  2. 執(zhí)行調(diào)用main函數(shù)

  3. 創(chuàng)建函數(shù)棧幀

    程序使用一個運行時堆棧,存儲函數(shù)的局部變量和返回地址。

  4. 終止程序

    可以正常也可以意外終止程序。

程序的執(zhí)行并不是本章的要點,所以就大概介紹一下。

?

程序的預(yù)處理

上面總體介紹了程序的編譯鏈接運行,下面詳細的講解程序預(yù)處理時所發(fā)生的事情。

預(yù)定義符號

下面所列舉的是一些預(yù)定義符號,之所以叫預(yù)定義,是因為只在預(yù)定義階段有效,而預(yù)編譯時就將其轉(zhuǎn)換為相應(yīng)的值。

//1.__FILE__ //代碼所在文件的文件名//2.__LINE__ //當(dāng)前代碼所在的行號//3.__DATE__ //文件被編譯的日期//4.__TIME__ //文件被編譯的時間//5.__STDC__ //當(dāng)前編譯器支持ANSI C,則值為1,否則未定義

使用場景,如圖所示:

當(dāng)然vs對C標準并不是完全支持的,所以最后一個在vs中無法顯示。

#define

#define 定義符號
#define MAX 100int main() {	int m = MAX;	return 0;}

#define定義的符號在預(yù)編譯期間會完成替換。如圖所示:

注意
  • #define定義標識符時,最好不要在最后加上;

若加上;,那么;也就是標識符內(nèi)容的一部分。這樣會在實際代碼中多出一個分號,空語句。

  • 當(dāng)定義類型時,#definetypedef的區(qū)別

#definetypedef一個是定義標識符,一個是定義類型,二者本身并無任何聯(lián)系。

#define INT inttypedef int int_t;

當(dāng)#define定義類型時,除了語法形式不同外,

#define定義的INT是個標識符,在預(yù)處理階段就被替換成int。typedef定義的int_t本身編譯器認定為類型,編譯到運行都不會變。

#define 定義的宏

#define定義宏和標識符常量的區(qū)別是宏有參數(shù)。將參數(shù)替換到文本中,這種實現(xiàn)被稱為宏。

//聲明形式#define Name(para1,...) stuff

參數(shù)列表需緊靠左邊宏名,不然會被解析為宏體的一部分。

宏形式類型于數(shù)學(xué)中的函數(shù) f ( x ) = x 2 f(x)=x^2 f(x)=x2 ,都是將參數(shù)帶入計算結(jié)果。如圖:

錯誤形式
//1.#define SQUARE(x) x*xint main(){	int ret = SQUARE(5 + 1);	printf("%d/n", ret);    return 0;}

上述代碼,計算的結(jié)果并非36,而是11。因為在替換的過程中SQUARE(5+1)替換成立5+1*5+1,遂得11。

為避免參數(shù)為表達式時由運算符優(yōu)先級差異而產(chǎn)生歧義,需要對宏體中的單項x(x)。

//2.#define DOUBLE(x) (x)+(x)int main(){	int ret = 2 * DOUBLE(5);	printf("%d/n", ret);	return 0;}

上述代碼計算結(jié)果也不是我們想要的2*(5+5)=20,而是2*5+5=15。這次是宏名外的運算符產(chǎn)生的歧義,故得出宏體整體還需加()。

所以正確的寫法為

#define DOUBLE(x) ((x)+(x))

正確形式是:宏體中的單項參數(shù)和整個宏體都需要加上()

#define 的替換規(guī)則
  1. 宏調(diào)用時,首先檢查并替換參數(shù)和宏體中用#define定義的符號。

  2. 然后再將宏和參數(shù)的值替換過去。

  3. 掃描結(jié)果文本,若仍包含#define定義內(nèi)容,就重復(fù)上述處理。

注意
  • 宏參數(shù)和宏體中允許出現(xiàn)其他#define定義的宏或標識符。但宏不允許遞歸。
  • 預(yù)處理器搜索#define定義符號時,字符串常量中的內(nèi)容不被搜索。
宏操作符 ###

#可以將參數(shù)插入字符串中。

int a = 10;printf("The value of a is %d/n", a);int b = 10;printf("The value of b is %d/n", b);int c = 10;printf("The value of c is %d/n", c);

如這樣的代碼,我們?nèi)绾螌⒆詣訉⒆址械腶,b,c替換而不用每次都修改字符串呢?

首先,C語言中兩個字符串放在一起會自動視為一個字符串,如:

printf("Hello world/n");printf("Hello ""world/n");

當(dāng)然**#的作用是將#后面的參數(shù)轉(zhuǎn)化成對應(yīng)的字符串**,如果前后都是字符串,那么自動拼接為一個字符串。

這樣上述需求我們就找到了解決方法。

#define PRINT(n) printf("The value of "#n" is %d/n",n);int main(){    int a = 10;    PRINT(a);    int b = 20;    PRINT(b);}

首先傳參將n替換為a,故#a被轉(zhuǎn)化為字符串"a"PRINT(a)會被替換成printf("The value of ""a"" is %d/n",a)。

##將位于其兩邊的符號合成一個符號。

#define CAT(X,Y) X##Yint main(){	int class102 = 100;		printf("%d/n", CAT(class,102));//100    printf("%d/n", CAT(1, 0));//10		CAT(class, 102) = 200;	printf("%d/n", CAT(class, 102));//200	return 0;}

可見,拼接起來的不僅可以視為符號,也可以視為數(shù)字,字符串等。個人認為既然##拼接行為是在預(yù)處理階段完成的,對于正在編譯的代碼來說##合成的結(jié)果和代碼敲出來的是一樣的。

宏操作符###只能在宏中使用。

帶副作用的宏參數(shù)

宏的參數(shù)傳入一些帶有副作用的操作符,可能會導(dǎo)致一些未知的錯誤。

a = 1;//1.b = a + 1;//b=2, a=1//2.b = a++;//b=2, a=2

如此,二者相比b雖然都是2,但后者a自增了1,這就是帶有副作用的表達式。

//1. 宏#define MAX(X,Y) ((X)>(Y)?(X):(Y))//2. 函數(shù)int Max(int x, int y) {    return x>y?x:y;}int main() {	int a = 20;	int b = 10;	int m1 = MAX(a++, b++);    int m2 = Max(a++, b++);	return 0; }

因為都是后置++,所以a++,b++的值還是20和10,當(dāng)然判斷之后a,b的值分別+1,整個表達式的值就是后面的a++的值即21,然后a的值又+1,當(dāng)然后面b++的表達式不執(zhí)行。

可以看出,宏的參數(shù)是不計算,直接預(yù)編譯時整體替換后在編譯期間計算的。而函數(shù)傳參同樣因為后置++,而傳的是a++,b++的值,傳完之后a,b分別+1。

宏和函數(shù)的對比

宏常被用于執(zhí)行相對簡單的運算,正如上面的例子。當(dāng)然函數(shù)同樣也能執(zhí)行這樣的任務(wù),如何選擇,請看下列二者優(yōu)劣的分析。

宏的優(yōu)勢:

  1. 使用函數(shù)要建立棧幀,銷毀棧幀,一系列的準備工作比實際任務(wù)大得多。故宏在程序規(guī)模和執(zhí)行速度方面更勝一籌。
  2. 函數(shù)參數(shù)必須聲明類型,且只能適用一種類型,而宏無類型檢查,只要滿足運算的類型都可以作參數(shù)。

宏的劣勢:

  1. 每次調(diào)用宏時,都會將宏的代碼替換到調(diào)用處。若宏代碼量大,可能會大幅增加代碼長度。
  2. 宏替換發(fā)生在預(yù)編譯期間,故無法調(diào)試。
  3. 宏的類型無關(guān)性,也會導(dǎo)致其不夠嚴謹。
  4. 宏可能由于運算符優(yōu)先級的問題,會導(dǎo)致程序出錯。

當(dāng)然宏可以做到函數(shù)做不到的事情,如宏的參數(shù)可以是類型。下列宏offsetof計算成員的偏移量的模擬實現(xiàn)。

#define offsetof(StructType, MemberName) (size_t)&(((StructType*)0)->MemberName)
分類函數(shù)
代碼長度宏代碼插入后,程序長度可能大幅增加函數(shù)代碼僅存一份,每次調(diào)用同一位置
執(zhí)行速度簡單更快棧幀的創(chuàng)建和銷毀的額外開銷
操作符優(yōu)先級周圍表達式中操作符優(yōu)先級可能會致錯,故要加全括號參數(shù)在調(diào)用處求值一次并傳遞表達式的值
參數(shù)副作用直接替換后再對參數(shù)進行處理,副作用的參數(shù)可能會致錯參數(shù)在傳參處求值后再傳參處理數(shù)據(jù)
參數(shù)類型宏參數(shù)與類型無關(guān),在操作合法的情況下,適用于任意類型函數(shù)參數(shù)受類型限制,參數(shù)類型不同需要不同的函數(shù)
調(diào)試無法調(diào)試函數(shù)可以調(diào)試
遞歸無法遞歸函數(shù)可以遞歸

所以對于二者的好壞我們要辯證的看待。

命名規(guī)范

宏與函數(shù)的使用方式很類似,語法無法將二者區(qū)分開來。故一般規(guī)定宏名字母全部大寫,而函數(shù)采用大小駝峰形式。

命名規(guī)范是約定俗成的東西,真正凸顯實力的是寫出效率高量少的代碼,而不是任性違背規(guī)范。

#undef

#undef用于移除宏定義。故一般和#define搭配使用。

#define MAX 100int main(){	int a = MAX;	#undef MAX    //int b = MAX;Err	    return 0;}

這樣可以使預(yù)定義符號MAX在不同的代碼處,可以擁有不同的定義。先移除再重新定義即可。

命令行定義

命令行定義是指在啟動編譯時對代碼文本中的符號進行定義。

如上列代碼所示,數(shù)組大小SZ未定義,我們可以在編譯該源文件時添上對SZ的定義:gcc test.c -D SZ=10

根據(jù)不同的情況給變量賦不同的值。這使得對于同一段代碼編譯出不同結(jié)果時,更加方便。

條件編譯

條件編譯指令使得讓某段代碼參與或不參與編譯的操作變得相對容易,類似于注釋代碼,達到選擇性編譯的效果。

常見編譯指令

常見的條件編譯指令如下,類似于if語句也有單分支多分支的情況:

//1.#if 常量表達式#endif//2.#if 常量表達式#elif 常量表達式#else#endif

#if,#elif,#else類似于if語句結(jié)構(gòu),#endif用于結(jié)束條件編譯。

//單分支int main() {#if 1	printf("haha/n");#endif#if 0 	printf("hehe/n");#endif	return 0;}//多分支int main() {#if 1==2	printf("hehe/n");#elif 2==3	printf("haha/n");#else	printf(".../n");#endif 	return 0;}

滿足條件則執(zhí)行,不滿足條件則不執(zhí)行。注意條件只能是常量表達式,因為預(yù)編譯指令只在預(yù)處理階段中起作用,而變量是在運行期間創(chuàng)建的。

還有更特殊化的條件編譯指令,多帶帶用于判斷符號是否被定義,如#if defined,#if !defined等。

//3.1#if defined (symbol)#endif//3.2#ifdef symbol#endif//4.1#if !defined(symbol)#endif//4.2#ifndef symbol#endif

語法規(guī)定每一個條件編譯指令#if...都要搭配上#endif使用。

#define MAX 100int main() {//1.定義#if defined (MAX)	printf("haha/n");#endif#ifdef MAX	printf("hehe/n");#endif//2.未定義#if !defined (MAX)	printf("dada/n");#endif#ifndef MAX	printf("titi/n");#endif	return 0;}
  • #if define..代表當(dāng)其后條件滿足時,執(zhí)行下面語句,#ifdef..是其簡寫形式。
  • #if !define..代表當(dāng)其后條件不滿足時,執(zhí)行下面語句,#ifndef..是其簡寫形式。
嵌套指令
#define SBL 100#define OPTION 100int main() {#if defined (SBL1)    #ifdef OPTION1		option1();	#endif	#ifdef OPTION2		option2();	#endif#elif defined (SBL2)    #ifdef OPTION3		option3();	#endif	#ifdef OPTION4		option4();	#endif#endif	return 0;}

同樣條件編譯指令也是預(yù)處理指令,預(yù)處理后自然將不滿足條件的內(nèi)容刪去。

文件包含

#include..也是預(yù)處理指令,用于包含代碼所需頭文件。一般有兩種形式:

  1. #include

  2. #include "filename"

二者查找策略不同,<>首先在安裝目錄的鏈接庫目錄下查找,找不到則報錯。""首先在工程目錄下查找,如果找不到則去安裝目錄下查找。

庫文件也可以用""的方式包含,但這樣會降低效率,也不易區(qū)分。

頭文件一多容易出現(xiàn)重復(fù)包含,解決方案有兩種:

//1. 條件編譯指令#ifndef __TEST.H__#define __TEST.H__#endif//2. 預(yù)處理指令#pragma once

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/122567.html

相關(guān)文章

  • C語言進階程序預(yù)處理

    摘要:程序預(yù)處理本章節(jié)研究的是,源代碼文件是如何一步步得到一個可執(zhí)行程序的。如的語句被稱為預(yù)處理指令,還有注釋文本的刪除,都在此階段完成替換。目的是能夠?qū)⑺形募械拇a組合到一起成一個完整的程序。終止程序可以正常也可以意外終止程序。 ...

    gxyz 評論0 收藏0
  • 只看不敲,神也學(xué)不好C---------計算機經(jīng)典書籍經(jīng)驗分享

    摘要:學(xué)單片機多去官網(wǎng)上查資料,下載手冊,像我入門的單片機經(jīng)常去官網(wǎng),還有學(xué)的系列板子,公司的官網(wǎng)的官方例程給的很詳細,在英文視角閱讀對你大有益處。 目錄 1.C語言經(jīng)典 2.單片機系列 3.Python方面 4.嵌入式LWip協(xié)議 5.Android 6.C++經(jīng)典書籍 7.Linux開發(fā) ...

    FleyX 評論0 收藏0
  • C語言進階第一問:數(shù)據(jù)在內(nèi)存中是如何存儲的?(手把手帶你深度剖析數(shù)據(jù)在內(nèi)卒中的存儲,超全解析,碼住不

    摘要:在符號位中,表示正,表示負。我們知道對于整型來說,內(nèi)存中存放的是該數(shù)的補碼。在計算機系統(tǒng)中,數(shù)值一律用補碼來表示和存儲。表示有效數(shù)字,。規(guī)定對于位的浮點數(shù),最高的位是 ...

    ghnor 評論0 收藏0
  • C語言進階】??數(shù)據(jù)類型&amp;&amp;整型在內(nèi)存中的存儲

    目錄 ? ?一、數(shù)據(jù)類型介紹 二、類型的意義 三、類型的基本歸類 整型家族 浮點數(shù)家族 構(gòu)造類型(自定義類型) 指針類型 空類型 四、整形在內(nèi)存中的存儲 原碼、反碼、補碼 大小端字節(jié)序 為什么有大端和小端? 一道經(jīng)典筆試題 ?一、數(shù)據(jù)類型介紹 數(shù)據(jù)從大的方向分為兩類: 內(nèi)置類型自定義類型內(nèi)置類型我們前面已經(jīng)學(xué)習(xí)過,如下: char? ? ? ? ? ? //字符數(shù)據(jù)類型 short? ? ? ...

    Xufc 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<