程式環境和預處理
老規矩筆記在gitee自取~:程式環境和預處理筆記
??歡迎喜歡學習C/C++的朋友互關一起努力!!??
文章目錄
- 程式環境和預處理
- 一、程式的環境
- 二、預處理符號
- 三、預處理指令#define
- 1. 定義識別符號常量
- 2. 定義宏
- 3. #define和tpyedef區別
- 4. 替換規則
- 5. \# 和 \##
- 1. \#
- 2. \##
- 6. 宏與函式的優缺點
- 四、\#undef 移除宏定義
- 五、命令列定義
- 六、條件編譯
- 1. 簡單條件編譯
- 2. 判斷是否定義
- 3. 多個分支的條件編譯
- 4. 嵌套指令
- 七、頭檔案包含方式
- 1. 本地檔案包含
- 2. 庫函式包含
- 3. 檔案嵌套包含
一、程式的環境
在ANSI C(國際標準c語言)的任何一種實作中,存在兩個不同的環境,
??翻譯環境:在這個環境中源代碼被轉換為可執行的機器指令
??執行環境:它用于切實執行代碼
下圖詳解(VS底下的編譯器是cl.exe,連接器是link.exe)

-
每個檔案
(.c檔案)都會經過編譯器處理,變成目標檔案(.obj) -
頭檔案(.h)的包含在預編譯完成,生成檔案(.i)#include/#define/#pragma等,是預編譯口令
注釋也會在預編譯階段洗掉掉
-
編譯階段完成,將c語言代碼轉化成匯編代碼,生成
匯編代碼檔案(.s)??語法分析
??詞法分析
??語意分析
??符號匯總(只匯總全域符號) -
匯編代碼進行匯編操作,將匯編代碼轉化成二進制代碼,生成符號表并且生成
目標檔案(型別linux/vs .o/.obj)此檔案可以在debug目錄(運行程式之后生成)下找到
檔案中資料的存盤格式為
elf格式readelf指令可以查看
-
鏈接庫包含庫函式檔案 -
所有的目標檔案+鏈接庫通過聯結器編譯成可執行檔案(.exe) -
鏈接各檔案中操作:
??合并段表
??符號表的合并和符號表的重定位(相同的地方合并)
更深層次理解
??預處理:相當于根據預處理指令組裝新的C/C++程式,
經過預處理,會產生一個
沒有頭檔案(都已經被展開了)、宏定義(都已經替換了),沒有條件編譯指令(該屏蔽的都屏蔽掉了),沒有特殊符號的輸出檔案,這個檔案的含義同原本的檔案無異,只是內容上有所不同,
??編譯:將預處理完的檔案逐一進行一系列
詞法分析、語法分析、語意分析及優化后,產生相應的匯編代碼檔案,編譯是針對單個檔案編譯的,只校驗本檔案的語法是否有問題,不負責尋找物體,
??鏈接:通過聯結器將一個個目標檔案(或許還會有庫檔案)鏈接在一起生成一個完整的可執行程式, 鏈接程式的主要作業就是將
有關的目標檔案彼此相連接,也就是將在一個檔案中參考的符號同該符號在另外一個檔案中的定義連接起來,使得所有的這些目標檔案成為一個能夠被作業系統裝入執行的統一整體,
在此程序中會發現被呼叫的函式未被定義,需要注意的是,鏈接階段只會鏈接呼叫了的函式/全域變數,如果存在一個不存在物體的宣告(函式宣告、全域變數的外部宣告),但沒有被呼叫,依然是可以正常編譯執行的,
二、預處理符號
以下符號在預處理階段處理
符號都是語言內置,無需包含檔案
__FILE__ //進行編譯的源檔案名
__LINE__ //檔案當前的行號
__DATE__ //檔案被編譯的日期
__TIME__ //檔案被編譯的時間
__STDC__ //如果編譯器遵循ANSI C,其值為1,否則未定義
注意: __ STDC __ 在VS下不支持ANSI C所以未定義;而在Linux環境下,gcc對ANSI C支持
三、預處理指令#define
1. 定義識別符號常量
#define MAX 100
int main()
{
printf("%d", MAX);
return 0;
}
在預編譯階段,MAX已經變成了100
注意:
-
??識別符號常量后
不要加;,容易出現問題 -
??當前處理器/編譯器中
搜索不到#define定義的符號
2. 定義宏
與函式不同,宏是把引數替換到文本中
#define ADD(x, y) ((x) + (y))
int main()
{
int a = 2;
int b = 3;
ADD(2, 3);//計算2+3
return 0;
}
注意:
-
🐳引數串列的
左括號必須與宏名稱緊鄰, -
🐳如果兩者之間有任何
空白存在,引數串列就會被解釋為后面式子的一部分, -
🐳數值運算式進行求值的宏定義都應在
外面加上括號,避免在使用宏時由于引數中的運算子或鄰近運算子產生作用導致計算錯誤 -
🐳宏引數和#define定義中可以出現其他#define定義的變數,但是對于宏,
不能出現遞回 -
🐳注意++和–等有副作用的符號
當宏引數在宏的定義中出現超過一次的時候,如果引數帶有副作用,那么你在使用這個宏的時候就可能出現危險,導致不可預測的后果,
#define MAX(a, b) ( (a) > (b) ? (a) : (b) ) int main() { int x = 5; int y = 8; int z = MAX(x++, y++); printf("x=%d y=%d z=%d\n", x, y, z); return 0; }以這個三目運算子為例,引數a和b分別都出現了2次,三目運算子的意思是結果為真,回傳a,結果為假,回傳b
判斷陳述句:a++ > b++ ?
這里
比較的時候a和b是5和8比較完a和b是6和9回傳結果:回傳b的值,z被賦值為9
宏結束后:回傳b++結束后,y為10
最后結果

3. #define和tpyedef區別
#define INT int*
typedef int* INT_T;
int main()
{
INT a, b;//這里是int* a; int b;
INT_T c, d;//這里是 int* a; int* b
}
-
🌼所以這里define僅僅是
符號的替換,而typedef起作用效果的物件,卻是之后的所有變數 -
🌼🌼如果這里的
INT只是int* 效果也是一樣,所以typedef的優點也在于此 -
🌼🌼🌼并且提醒大家以后定義變數盡量
一行一個變數以防出錯
4. 替換規則
在定義符號和宏的時候,需要注意:
- 🐚在呼叫宏時,查看引數否
包含任何由#define定義的符號,如果有將先被替換 - 🐚對于符號,替換文本被插入到程式中
原來文本的位置 - 🐚對于宏,引數名被他們的
值所替換 - 🐚最后,再次對結果檔案進行掃描,看看它是否包含任何由#define定義的符號,如果是,就
重復上述處理程序,
5. # 和 ##
1. #
將一個宏引數變成對應的字串
直接上一串代碼體驗:
我們想要實作printf的一個函式
int a = 1;
int b = 2;
printf("a的值是%d", a);
printf("b的值是%d", b);
//以上代碼用函式實作
void print(int x)
{
printf("x的值是%d", x);
}
但是列印出來是:x的值是1,x的值是2
實作不了我們想要的:a的值是1,b的值是2
而define加上#就有奇效:
#define PRINT(x) printf(""#x"的值為%d", x)
int main()
{
int a = 2;
PRINT(a);
return 0;
}
結果是:

原理是什么樣的?
🌱🌱🌱因為字串有相鄰自動連接功能
printf("hi""hello");

而#相當于將宏引數x左右加上""
使得x變成一個符號,這就是將宏引數變成對應的字串
實際上,上面的""#x"的值為x" 實則是 " " 、 ''x" 、 "的值為%d ",連接起來
2. ##
將兩邊的符號合成一個符號
#define PRINT(a, b) a##b
int main()
{
int a = 1;
int b = 0;
int ab = 10;
printf("%d", PRINT(a, b));//相當于列印ab
return 0;
}
🌾🌾🌾注意:這樣的連接必須產生一個
合法的識別符號,否則其結果就是未定義不必過于深究,只要理解其作用就行
6. 宏與函式的優缺點
| 比較內容 | #define定義宏 | 函式 |
|---|---|---|
| 代碼長度 | 每次使用時,宏代碼都會被插入到程式中,除了非常小的宏之外,程式的長度會大幅度增長 | 函式代碼只出現于一個地方;每次使用這個函式時,都呼叫那個地方的同一份代碼 |
| 執行速度 | 更快 | 存在函式的呼叫和回傳的額外開銷,所以相對慢一些 |
| 運算子優先級 | 宏引數的求值是在所有周圍運算式的背景關系環境里,除非加上括號,否則鄰近運算子的優先級可能會產生不可預料的后果,所以建議宏在書寫的時候多些括號 | 函式引數只在函式呼叫的時候求值一次,它的結果值傳遞給函式,運算式的求值結果更容易預測 |
| 帶有副作用的引數 | 引數可能被替換到宏體中的多個位置,所以帶有副 作用的引數求值可能會產生不可預料的結果 | 函式引數只在傳參的時候求值一 次,結果更容易控制 |
| 引數型別 | 宏的引數與型別無關,只要對引數的操作是合法 的,它就可以使用于任何引數型別 | 函式的引數是與型別有關的,如 果引數的型別不同,就需要不同的函式,即使他們執行的任務是 不同的 |
| 除錯 | 宏是不方便除錯的 | 函式是可以逐陳述句除錯的 |
| 遞回 | 宏是不能遞回的 | 函式是可以遞回的 |
命名約定:
- 🎍宏命名全部大寫
- 🎍函式名可以不全大寫
四、#undef 移除宏定義
#undef MAX
//如果現存的一個名字需要被重新定義,那么它的舊名字首先要被移除
五、命令列定義
有一些編譯器提供了一種能力,允許在命令列中定義符號
比如在linux環境下,gcc編譯器,使用指令,在預處理階段給
變數重新賦值????假定:某個程式中宣告了一個某個長度的陣列,如果機器記憶體有限,我們需要一個很小的陣列,但是另外一個機器記憶體大,我們需要一個陣列能夠大一點
六、條件編譯
滿足條件,代碼參與編譯,不滿足就不參與
1. 簡單條件編譯
int main()
{
int a = 1;
#if 0//這里寫運算式或者符號(常量)
//#if a 這里是錯誤的,因為在預編譯程序a還沒創建
//a創建的程序在運行的程序
printf("%d", a);//什么都不列印
#endif
return 0;
}
2. 判斷是否定義
對于宏
//定義了
#if defined(symbol)
#ifdef symbol
//沒有定義
#if !defined(symbol)
#ifndef symbol
//兩種寫法均可
代碼感受:
#define MAX 0
int main()
{
int a = 1;
#if defined(MAX)//測驗定義了沒有,與值無關
printf("%d", a);
#endif
#if max//根據MAX的值判斷
printf("%d", MAX);
#endif
return 0;
}
運行結果:
3. 多個分支的條件編譯
#if 常量運算式
//...
#elif 常量運算式
//...
#else
//...
#endif
4. 嵌套指令
#if defined(MAX)
#ifdef OP1
ADD1();
#endif
#ifdef OP2
ADD2();
#endif
#elif defined(MIN)
#ifdef OP2
DEL2();
#endif
#endif
用法有點類似if/else 陳述句
七、頭檔案包含方式
1. 本地檔案包含
#include "test.c"
🎓先在源檔案所在目錄下查找,如果該頭檔案未找到,編譯器就像查找庫函式頭檔案一樣在標準位置查找頭檔案
🎓如果找不到就提示編譯錯誤
2. 庫函式包含
#include <stdio.h>
🎓查找頭檔案直接去標準路徑下去查找,如果找不到就提示編譯錯誤
3. 檔案嵌套包含
可能存在一種,頭檔案在無意識中包含了兩次,意味著預編譯期間,會拷貝兩份頭檔案內容
為了防止上述情況形成
我們用條件編譯解決這個問題
#ifndef TSD
#define TSD
#include <stdio.h>
//頭檔案的內容
#endif
或者每個檔案開頭寫
#pragma once
第二種方式最簡單
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/310572.html
標籤:其他
上一篇:六大區別 (多載與重寫、順序表和鏈表、Comparable和Comparator、抽象類和介面、super和this、ArrayList和LinkedList)

