摘要:如的語句被稱為預(yù)處理指令,還有注釋文本的刪除,都在此階段完成替換。故宏在程序規(guī)模和執(zhí)行速度方面更勝一籌。宏替換發(fā)生在預(yù)編譯期間,故無法調(diào)試。宏可能由于運(yùn)算符優(yōu)先級(jí)的問題,會(huì)導(dǎo)致程序出錯(cuò)。
本章節(jié)研究的是,源代碼文件
test.c
是如何一步步得到一個(gè)可執(zhí)行程序test.exe
的。在之前的學(xué)習(xí)中可知.c
文件要先后經(jīng)過編譯鏈接成.exe
文件再執(zhí)行。
程序的編譯鏈接運(yùn)行如下圖所示。翻譯中編譯又包括預(yù)編譯、編譯、匯編。
編譯鏈接執(zhí)行三個(gè)步驟,都需要為其配置不同的環(huán)境。編譯和鏈接在翻譯環(huán)境中,而執(zhí)行在運(yùn)行環(huán)境中發(fā)生。
- 翻譯環(huán)境:在該環(huán)境中源代碼被轉(zhuǎn)換成可執(zhí)行的機(jī)器指令。
- 執(zhí)行環(huán)境:用于實(shí)際執(zhí)行代碼。
翻譯階段的大致流程如下圖所示。
組成一個(gè)程序的每個(gè).c
源文件都會(huì)被編譯器編譯,分別生成對(duì)應(yīng)的.obj
目標(biāo)文件。多個(gè)目標(biāo)文件以及引入的鏈接庫被鏈接器鏈接在一起,形成一個(gè)單一的.exe
可執(zhí)行程序。
編譯器即是一個(gè)用于編譯代碼的工具,在vs環(huán)境下為
cl.exe
的可執(zhí)行程序。連接器則是用于鏈接所有目標(biāo)文件的工具,在vs中為link.exe
的可執(zhí)行程序,鏈接庫是標(biāo)準(zhǔn)中任何被該程序用到的函數(shù)。如圖:
而若想觀察翻譯代碼過程中的每一個(gè)流程的具體細(xì)節(jié),在集成開發(fā)環(huán)境
vs
中不便展示,當(dāng)然我們可以使用Linux
環(huán)境下的gcc
編譯器。
此次演示就采用加法函數(shù),分別存放在兩個(gè)文件test.c
和add.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;}
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ù)編譯階段的動(dòng)作都是文本操作:
#include
頭文件的包含#define
預(yù)處理符號(hào)的替換預(yù)編譯,顧名思義,是在編譯前刪減代碼中的不必要的與機(jī)器識(shí)別代碼無關(guān)的內(nèi)容。被稱為文本操作。
對(duì)預(yù)編譯產(chǎn)生的文件
test.i
再編譯gcc test.i -S
,會(huì)自動(dòng)生成匯編代碼test.s
。
故編譯階段是將C語言代碼轉(zhuǎn)化為匯編代碼,這是整體現(xiàn)象。實(shí)際上會(huì)發(fā)生這四個(gè)動(dòng)作:
詞法分析,語法分析,語義分析都是編譯器識(shí)別語句的操作。重點(diǎn)是接下接下來的符號(hào)匯總。
符號(hào)匯總,是只對(duì)全局符號(hào)進(jìn)行匯總,局部符號(hào)是不進(jìn)行匯總的。目的是能夠?qū)⑺形募械拇a組合到一起成一個(gè)完整的程序。如add.c
文件中的函數(shù)名Add
,還有test.c
文件中的Add
和main
。
gcc test.s -C
將編譯結(jié)束產(chǎn)生的匯編代碼轉(zhuǎn)化成了二進(jìn)制指令(機(jī)器指令)存入二進(jìn)制文件test.o
中。
匯編階段會(huì)形成符號(hào)表,因?yàn)闄C(jī)器在調(diào)用指令時(shí)需要知道其存放的位置,所謂符號(hào)表大概就是符號(hào)和其地址的集合。如圖,可以假設(shè):
鏈接將二進(jìn)制指令目標(biāo)文件
test.o
等,鏈接在一起形成可執(zhí)行程序test.out
。目標(biāo)文件test.o
是elf
格式文件,在Linux平臺(tái)下可以用readelf
翻譯并查看其內(nèi)容。
鏈接階段的動(dòng)作是:
所謂的鏈接,就是將對(duì)應(yīng)的段合并起來。
符號(hào)表的合并,是將各自的符號(hào)表合并到一起。如test.o
中的Add
的無效地址,需把add.o
中Add
的地址合并過去再重定位到變量的真實(shí)地址,才是有意義的。
從編譯期間的符號(hào)匯總,到匯編時(shí)的形成符號(hào)表,再到鏈接時(shí)的合并和重定位符號(hào)表,都是為了最后生成可執(zhí)行程序時(shí)能夠找到并鏈接各個(gè)文件中的符號(hào)。
程序首先載入內(nèi)存
有的機(jī)器上有操作系統(tǒng),這個(gè)動(dòng)作就是由操作系統(tǒng)完成,沒有的由手工完成。
執(zhí)行調(diào)用main
函數(shù)
創(chuàng)建函數(shù)棧幀
程序使用一個(gè)運(yùn)行時(shí)堆棧,存儲(chǔ)函數(shù)的局部變量和返回地址。
終止程序
可以正常也可以意外終止程序。
程序的執(zhí)行并不是本章的要點(diǎn),所以就大概介紹一下。
?
上面總體介紹了程序的編譯鏈接運(yùn)行,下面詳細(xì)的講解程序預(yù)處理時(shí)所發(fā)生的事情。
下面所列舉的是一些預(yù)定義符號(hào),之所以叫預(yù)定義,是因?yàn)橹辉陬A(yù)定義階段有效,而預(yù)編譯時(shí)就將其轉(zhuǎn)換為相應(yīng)的值。
//1.__FILE__ //代碼所在文件的文件名//2.__LINE__ //當(dāng)前代碼所在的行號(hào)//3.__DATE__ //文件被編譯的日期//4.__TIME__ //文件被編譯的時(shí)間//5.__STDC__ //當(dāng)前編譯器支持ANSI C,則值為1,否則未定義
使用場景,如圖所示:
當(dāng)然vs對(duì)C標(biāo)準(zhǔn)并不是完全支持的,所以最后一個(gè)在vs中無法顯示。
#define MAX 100int main() { int m = MAX; return 0;}
#define
定義的符號(hào)在預(yù)編譯期間會(huì)完成替換。如圖所示:
#define
定義標(biāo)識(shí)符時(shí),最好不要在最后加上;
若加上
;
,那么;
也就是標(biāo)識(shí)符內(nèi)容的一部分。這樣會(huì)在實(shí)際代碼中多出一個(gè)分號(hào),空語句。
#define
和typedef
的區(qū)別#define
和typedef
一個(gè)是定義標(biāo)識(shí)符,一個(gè)是定義類型,二者本身并無任何聯(lián)系。
#define INT inttypedef int int_t;
當(dāng)#define定義類型時(shí),除了語法形式不同外,
#define定義的INT
是個(gè)標(biāo)識(shí)符,在預(yù)處理階段就被替換成int
。typedef定義的int_t
本身編譯器認(rèn)定為類型,編譯到運(yùn)行都不會(huì)變。
#define
定義宏和標(biāo)識(shí)符常量的區(qū)別是宏有參數(shù)。將參數(shù)替換到文本中,這種實(shí)現(xiàn)被稱為宏。
//聲明形式#define Name(para1,...) stuff
參數(shù)列表需緊靠左邊宏名,不然會(huì)被解析為宏體的一部分。
宏形式類型于數(shù)學(xué)中的函數(shù) f ( x ) = x 2 f(x)=x^2 f(x)=x2 ,都是將參數(shù)帶入計(jì)算結(jié)果。如圖:
//1.#define SQUARE(x) x*xint main(){ int ret = SQUARE(5 + 1); printf("%d/n", ret); return 0;}
上述代碼,計(jì)算的結(jié)果并非36,而是11。因?yàn)樵谔鎿Q的過程中SQUARE(5+1)
替換成立5+1*5+1
,遂得11。
為避免參數(shù)為表達(dá)式時(shí)由運(yùn)算符優(yōu)先級(jí)差異而產(chǎn)生歧義,需要對(duì)宏體中的單項(xiàng)x
加(x)
。
//2.#define DOUBLE(x) (x)+(x)int main(){ int ret = 2 * DOUBLE(5); printf("%d/n", ret); return 0;}
上述代碼計(jì)算結(jié)果也不是我們想要的2*(5+5)=20
,而是2*5+5=15
。這次是宏名外的運(yùn)算符產(chǎn)生的歧義,故得出宏體整體還需加()
。
所以正確的寫法為
#define DOUBLE(x) ((x)+(x))
正確形式是:宏體中的單項(xiàng)參數(shù)和整個(gè)宏體都需要加上()
。
宏調(diào)用時(shí),首先檢查并替換參數(shù)和宏體中用#define定義的符號(hào)。
然后再將宏和參數(shù)的值替換過去。
掃描結(jié)果文本,若仍包含#define定義內(nèi)容,就重復(fù)上述處理。
#
和##
#
可以將參數(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)绾螌⒆詣?dòng)將字符串中的a,b,c替換而不用每次都修改字符串呢?
首先,C語言中兩個(gè)字符串放在一起會(huì)自動(dòng)視為一個(gè)字符串,如:
printf("Hello world/n");printf("Hello ""world/n");
當(dāng)然**#
的作用是將#
后面的參數(shù)轉(zhuǎn)化成對(duì)應(yīng)的字符串**,如果前后都是字符串,那么自動(dòng)拼接為一個(gè)字符串。
這樣上述需求我們就找到了解決方法。
#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)
會(huì)被替換成printf("The value of ""a"" is %d/n",a)
。
##
將位于其兩邊的符號(hào)合成一個(gè)符號(hào)。
#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;}
可見,拼接起來的不僅可以視為符號(hào),也可以視為數(shù)字,字符串等。個(gè)人認(rèn)為既然##
拼接行為是在預(yù)處理階段完成的,對(duì)于正在編譯的代碼來說##
合成的結(jié)果和代碼敲出來的是一樣的。
宏操作符
#
和##
只能在宏中使用。
宏的參數(shù)傳入一些帶有副作用的操作符,可能會(huì)導(dǎo)致一些未知的錯(cuò)誤。
a = 1;//1.b = a + 1;//b=2, a=1//2.b = a++;//b=2, a=2
如此,二者相比b雖然都是2,但后者a自增了1,這就是帶有副作用的表達(dá)式。
//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; }
因?yàn)槎际呛笾?code>++,所以
a++
,b++
的值還是20和10,當(dāng)然判斷之后a
,b
的值分別+1,整個(gè)表達(dá)式的值就是后面的a++
的值即21,然后a的值又+1,當(dāng)然后面b++的表達(dá)式不執(zhí)行。
可以看出,宏的參數(shù)是不計(jì)算,直接預(yù)編譯時(shí)整體替換后在編譯期間計(jì)算的。而函數(shù)傳參同樣因?yàn)楹笾?+,而傳的是a++
,b++
的值,傳完之后a,b分別+1。
宏常被用于執(zhí)行相對(duì)簡單的運(yùn)算,正如上面的例子。當(dāng)然函數(shù)同樣也能執(zhí)行這樣的任務(wù),如何選擇,請(qǐng)看下列二者優(yōu)劣的分析。
宏的優(yōu)勢:
宏的劣勢:
當(dāng)然宏可以做到函數(shù)做不到的事情,如宏的參數(shù)可以是類型。下列宏
offsetof
計(jì)算成員的偏移量的模擬實(shí)現(xiàn)。
#define offsetof(StructType, MemberName) (size_t)&(((StructType*)0)->MemberName)
分類 | 宏 | 函數(shù) |
---|---|---|
代碼長度 | 宏代碼插入后,程序長度可能大幅增加 | 函數(shù)代碼僅存一份,每次調(diào)用同一位置 |
執(zhí)行速度 | 簡單更快 | 棧幀的創(chuàng)建和銷毀的額外開銷 |
操作符優(yōu)先級(jí) | 周圍表達(dá)式中操作符優(yōu)先級(jí)可能會(huì)致錯(cuò),故要加全括號(hào) | 參數(shù)在調(diào)用處求值一次并傳遞表達(dá)式的值 |
參數(shù)副作用 | 直接替換后再對(duì)參數(shù)進(jìn)行處理,副作用的參數(shù)可能會(huì)致錯(cuò) | 參數(shù)在傳參處求值后再傳參處理數(shù)據(jù) |
參數(shù)類型 | 宏參數(shù)與類型無關(guān),在操作合法的情況下,適用于任意類型 | 函數(shù)參數(shù)受類型限制,參數(shù)類型不同需要不同的函數(shù) |
調(diào)試 | 宏無法調(diào)試 | 函數(shù)可以調(diào)試 |
遞歸 | 宏無法遞歸 | 函數(shù)可以遞歸 |
所以對(duì)于二者的好壞我們要辯證的看待。
宏與函數(shù)的使用方式很類似,語法無法將二者區(qū)分開來。故一般規(guī)定宏名字母全部大寫,而函數(shù)采用大小駝峰形式。
命名規(guī)范是約定俗成的東西,真正凸顯實(shí)力的是寫出效率高量少的代碼,而不是任性違背規(guī)范。
#undef
用于移除宏定義。故一般和#define
搭配使用。
#define MAX 100int main(){ int a = MAX; #undef MAX //int b = MAX;Err return 0;}
這樣可以使預(yù)定義符號(hào)MAX
在不同的代碼處,可以擁有不同的定義。先移除再重新定義即可。
命令行定義是指在啟動(dòng)編譯時(shí)對(duì)代碼文本中的符號(hào)進(jìn)行定義。
如上列代碼所示,數(shù)組大小SZ
未定義,我們可以在編譯該源文件時(shí)添上對(duì)SZ
的定義:gcc test.c -D SZ=10
根據(jù)不同的情況給變量賦不同的值。這使得對(duì)于同一段代碼編譯出不同結(jié)果時(shí),更加方便。
條件編譯指令使得讓某段代碼參與或不參與編譯的操作變得相對(duì)容易,類似于注釋代碼,達(dá)到選擇性編譯的效果。
常見的條件編譯指令如下,類似于if語句也有單分支多分支的情況:
//1.#if 常量表達(dá)式#endif//2.#if 常量表達(dá)式#elif 常量表達(dá)式#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í)行。注意條件只能是常量表達(dá)式,因?yàn)轭A(yù)編譯指令只在預(yù)處理階段中起作用,而變量是在運(yùn)行期間創(chuàng)建的。
還有更特殊化的條件編譯指令,多帶帶用于判斷符號(hào)是否被定義,如#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ī)定每一個(gè)條件編譯指令#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)其后條件滿足時(shí),執(zhí)行下面語句,#ifdef..
是其簡寫形式。#if !define..
代表當(dāng)其后條件不滿足時(shí),執(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ù)處理指令,用于包含代碼所需頭文件。一般有兩種形式:
#include
#include "filename"
二者查找策略不同,<>
首先在安裝目錄的鏈接庫目錄下查找,找不到則報(bào)錯(cuò)。""
首先在工程目錄下查找,如果找不到則去安裝目錄下查找。
庫文件也可以用
""
的方式包含,但這樣會(huì)降低效率,也不易區(qū)分。
頭文件一多容易出現(xiàn)重復(fù)包含,解決方案有兩種:
//1. 條件編譯指令#ifndef __TEST.H__#define __TEST.H__#endif//2. 預(yù)處理指令#pragma once
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/122567.html
摘要:程序預(yù)處理本章節(jié)研究的是,源代碼文件是如何一步步得到一個(gè)可執(zhí)行程序的。如的語句被稱為預(yù)處理指令,還有注釋文本的刪除,都在此階段完成替換。目的是能夠?qū)⑺形募械拇a組合到一起成一個(gè)完整的程序。終止程序可以正常也可以意外終止程序。 ...
摘要:學(xué)單片機(jī)多去官網(wǎng)上查資料,下載手冊(cè),像我入門的單片機(jī)經(jīng)常去官網(wǎng),還有學(xué)的系列板子,公司的官網(wǎng)的官方例程給的很詳細(xì),在英文視角閱讀對(duì)你大有益處。 目錄 1.C語言經(jīng)典 2.單片機(jī)系列 3.Python方面 4.嵌入式LWip協(xié)議 5.Android 6.C++經(jīng)典書籍 7.Linux開發(fā) ...
摘要:在符號(hào)位中,表示正,表示負(fù)。我們知道對(duì)于整型來說,內(nèi)存中存放的是該數(shù)的補(bǔ)碼。在計(jì)算機(jī)系統(tǒng)中,數(shù)值一律用補(bǔ)碼來表示和存儲(chǔ)。表示有效數(shù)字,。規(guī)定對(duì)于位的浮點(diǎn)數(shù),最高的位是 ...
目錄 ? ?一、數(shù)據(jù)類型介紹 二、類型的意義 三、類型的基本歸類 整型家族 浮點(diǎn)數(shù)家族 構(gòu)造類型(自定義類型) 指針類型 空類型 四、整形在內(nèi)存中的存儲(chǔ) 原碼、反碼、補(bǔ)碼 大小端字節(jié)序 為什么有大端和小端? 一道經(jīng)典筆試題 ?一、數(shù)據(jù)類型介紹 數(shù)據(jù)從大的方向分為兩類: 內(nèi)置類型自定義類型內(nèi)置類型我們前面已經(jīng)學(xué)習(xí)過,如下: char? ? ? ? ? ? //字符數(shù)據(jù)類型 short? ? ? ...
閱讀 2265·2023-04-25 23:15
閱讀 1916·2021-11-22 09:34
閱讀 1546·2021-11-15 11:39
閱讀 954·2021-11-15 11:37
閱讀 2152·2021-10-14 09:43
閱讀 3492·2021-09-27 13:59
閱讀 1505·2019-08-30 15:43
閱讀 3453·2019-08-30 15:43