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