前言
感謝老鐵們的陪伴和支持,初始C語言專欄在本章內(nèi)容也是要結(jié)束了,這創(chuàng)作一路下來也是很不容易,如果大家對 Java 后端開發(fā)感興趣,歡迎各位老鐵來我的Java專欄!當(dāng)然了,我也會更新幾章C語言實現(xiàn)簡單的數(shù)據(jù)結(jié)構(gòu)!不過由于我是Java 技術(shù)棧的,所以如果以后有機會學(xué)習(xí)C++的話,我會重新回來把C語言進階專欄更完的!
Java專欄:JavaSE
C語言數(shù)據(jù)結(jié)構(gòu)專欄:C語言進階
個人主頁:熵減玩家
個人格言:為祖國添磚加瓦(Java)~~
編譯與鏈接
翻譯環(huán)境和運行環(huán)境
? ? ? ?在源文件到程序運行結(jié)果的過程中,經(jīng)過了兩種環(huán)境,在翻譯環(huán)境中形成可執(zhí)行的機器指令(二進制指令),在運行環(huán)境中生成可執(zhí)行的程序(.exe文件)然后輸出結(jié)果。
翻譯環(huán)境
? ? ? ?在翻譯環(huán)境中有兩個過程,一個是編譯(而編譯又可以分解成:預(yù)處理(有些書也叫預(yù)編譯)、編譯、匯編三個過程。),另一個是鏈接。
預(yù)編譯我會在下面詳細介紹~
?個C語言的項目中可能有多個 .c 文件?起構(gòu)建,那多個 .c ?件如何生成可執(zhí)行程序呢?
? 多個.c文件單獨經(jīng)過編譯器,編譯處理生成對應(yīng)的目標(biāo)?件。
? 注:在Windows環(huán)境下的?標(biāo)?件的后綴是 .obj ,Linux環(huán)境下?標(biāo)?件的后綴是 .o
? 多個?標(biāo)文件和鏈接庫?起經(jīng)過鏈接器處理生成最終的可執(zhí)行程序。
? 鏈接庫是指運行時庫(它是支持程序運行的基本函數(shù)集合)或者第三方庫。
如果再詳細一點的話,回分為下面的步驟:
以gcc為例:
拆分編譯三個過程
? ? ? ?這里提到的編譯是翻譯環(huán)境里的大的編譯,所以其被拆分成三個過程分別是:預(yù)處理,編譯,匯編。
預(yù)處理(預(yù)編譯)
? ? ? ?這里作簡單的介紹,下面又更加詳細的預(yù)處理詳解~~
預(yù)處理階段主要處理那些源?件中#開始的預(yù)編譯指令。比如:#include,#define,處理的規(guī)則如下:
? 將所有的 #define 刪除,并展開所有的宏定義。
? 處理所有的條件編譯指令,如: #if、#ifdef、#elif、#else、#endif 。
? 處理#include 預(yù)編譯指令,將包含的頭?件的內(nèi)容插?到該預(yù)編譯指令的位置。這個過程是遞歸進行的,也就是說被包含的頭文件也可能包含其他?件。
? 刪除所有的注釋
? 添加行號和文件名標(biāo)識,方便后續(xù)編譯器生成成調(diào)試信息等。
? 或保留所有的#pragma的編譯器指令,編譯器后續(xù)會使?。
經(jīng)過預(yù)處理后的.i文件中不再包含宏定義,因為宏已經(jīng)被展開。并且包含的頭文件都被插入到.i?件中。所以當(dāng)我們?法知道宏定義或者頭文件是否包含正確的時候,可以查看預(yù)處理后的.i文件來確認。
編譯
這里的編譯又被分成三個小過程,詞法分析、語法分析、語義分析及優(yōu)化
詞法分析
將源代碼程序被輸?掃描器,掃描器的任務(wù)就是簡單的進行詞法分析,把代碼中的字符分割成?系列的記號(關(guān)鍵字、標(biāo)識符、字?量、特殊字符等)
array[index]=(index+4)*(2+6)
上?程序進行詞法分析后得到了16個記號:
語法分析
接下來語法分析器,將對掃描產(chǎn)?的記號進?語法分析,從?產(chǎn)?語法樹。這些語法樹是以表達式為節(jié)點的樹。
語義分析
由語義分析器來完成語義分析,即對表達式的語法層?分析。編譯器所能做的分析是語義的靜態(tài)分析。靜態(tài)語義分析通常包括聲明和類型的匹配,類型的轉(zhuǎn)換等。這個階段會報告錯誤的語法信息。
匯編
匯編器是將匯編代碼轉(zhuǎn)變成機器可執(zhí)行的指令,每?個匯編語句幾乎都對應(yīng)?條機器指令。就是根據(jù)匯編指令和機器指令的對照表??的進行翻譯,也不做指令優(yōu)化。
鏈接
鏈接是?個復(fù)雜的過程,鏈接的時候需要把?堆文件鏈接在?起才生成可執(zhí)行程序。
鏈接過程主要包括:地址和空間分配,符號決議和重定位等這些步驟。
鏈接解決的是?個項?中多文件、多模塊之間互相調(diào)用的問題。
例如:在一個工程中,鏈接就是把這個工程中所有的頭文件,源文件全部連接在一起,形成一個可執(zhí)行的程序。
我們已經(jīng)知道,每個源文件都是單獨經(jīng)過編譯器處理生成對應(yīng)的目標(biāo)文件。
test.c 經(jīng)過編譯器處理生成 test.o
add.c 經(jīng)過編譯器處理生成 add.o
我們在 test.c 的?件中使用了 add.c 文件中的 Add 函數(shù)和 g_val 變量。
我們在 test.c ?件中每?次使用 Add 函數(shù)和 g_val 的時候必須確切的知道 Add 和 g_val 的地址,但是由于每個文件是單獨編譯的,在編譯器編譯 test.c 的時候并不知道 Add 函數(shù)和 g_val變量的地址,所以暫時把調(diào)用 Add 的指令的。目標(biāo)地址和 g_val 的地址擱置。等待最后鏈接的時候由鏈接器根據(jù)引?的符號 Add 在其他模塊中查找 Add 函數(shù)的地址,然后將 test.c 中所有引?到Add 的指令重新修正,讓他們的目標(biāo)地址為真正的 Add 函數(shù)的地址,對于全局變量 g_val 也是類似的方法來修正地址。這個地址修正的過程也被叫做:重定位。
所以當(dāng)出現(xiàn)函數(shù)未定義的報錯信息時,就是在連接的時候出錯的!??!
如果大家還想更加深入了解上面的過程,可以去閱讀《程序員的自我修養(yǎng)》這本書!
運行環(huán)境
- 程序必須載?內(nèi)存中。在有操作系統(tǒng)的環(huán)境中:?般這個由操作系統(tǒng)完成。在獨立的環(huán)境中,程序的載?必須由手工安排,也可能是通過可執(zhí)行代碼置?只讀內(nèi)存來完成。
- 程序的執(zhí)行便開始。接著便調(diào)用main函數(shù)。
- 開始執(zhí)行程序代碼。這個時候程序?qū)⑹??個運行時堆棧(stack),存儲函數(shù)的局部變量和返回地址。程序同時也可以使?靜態(tài)(static)內(nèi)存,存儲于靜態(tài)內(nèi)存中的變量在程序的整個執(zhí)行過程?直保留他們的值。
- 終?程序。正常終止main函數(shù);也有可能是意外終止。
預(yù)處理詳解
? ? ? ?這里我們就來詳細解剖預(yù)處理~~
預(yù)定義符號
C語言設(shè)置了?些預(yù)定義符號,可以直接使用,預(yù)定義符號也是在預(yù)處理期間處理的。
__FILE__ //進?編譯的源?件
__LINE__ //?件當(dāng)前的?號
__DATE__ //?件被編譯的?期
__TIME__ //?件被編譯的時間
__STDC__ //如果編譯器遵循ANSI C,其值為1,否則未定義
我們來使用一下:
#define 定義常量
#define 定義常量,需要在后面寫個標(biāo)識符,后面再跟一個常量即可。
#define M 100
#define 定義宏
宏也可以類比成函數(shù),在#define 后面跟一個標(biāo)識符,在標(biāo)識符后面緊跟(參數(shù)),一定要進更,否則會被編譯器認為這時一個#define 定義常量?。?!
#define SUM(x,y) x+y
要注意了,宏是不允許出現(xiàn)遞歸的?。?!
續(xù)航符 \
當(dāng)#define 定義的宏或者參數(shù)很長時,為了更好的閱讀,我們會把它拆分成好幾行,為了不發(fā)生編譯錯誤,我們需要加上續(xù)航符 \
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
#define替換的規(guī)則
所有的#define 定義的常數(shù)或者宏都會被替換成你定義的值或式子。
注意,只會替換,不會參與任何計算?。?!
帶有副作用的宏參數(shù)
? ? ? ?由于我們知道#define 只會完成替換,并不會參與計算,所以你傳進去的值可能不是你的預(yù)期結(jié)果。
舉個栗子:
#define SQUARE(x) x*x
當(dāng)你傳入1+3(即SQUARE(1+3)),程序替換后會變成這樣:1+3*1+3 ;算出來的結(jié)果就是7,而不是16,所以宏在使用的時候會用一定的副作用。
如何避免:
加多點括號,不要吝嗇你的括號!?。?/p>
修改之后的宏定義:
#define SQUARE(x) ((x) * (x))
這樣定義之后,你傳進去的1+3 替換后就變成了:((1+3)*(1+3))
宏與函數(shù)的對比
1.宏的代碼量更少,在完成一些小型計算的時候,會比函數(shù)更快,因為函數(shù)需要調(diào)用函數(shù),進行計算,然后返回結(jié)果,相比之下,宏會有優(yōu)勢一些。
舉個例子:比較兩個值的大小
函數(shù):
int max(int x, int y)
{
return (x > y ? x : y);
}
宏:
#define MAX(x,y) ((x)>(y)?(x):(y))
2.宏不會進行參數(shù)類型檢查,而函數(shù)會,就也體現(xiàn)了宏對參數(shù)類型是不會限制的,只要傳值就可以了,在上面的例子就體現(xiàn)到這一點,上面的例子中max函數(shù)需要傳入兩個整型的值,而宏卻不用。
由于宏對參數(shù)類型沒有檢查,我們在malloc 開辟空間的時候其實也可以用宏來寫:
#define MALLOC(n,type) (type*)malloc(n*sizeof(type))
由于宏沒有參數(shù)類型檢查,所以不夠嚴(yán)謹(jǐn),即有優(yōu)點,也有缺點。
3.由于宏是直接完成替換,所以如果宏出錯了,我們不能通過調(diào)試來檢查出宏的錯誤,但是函數(shù)可以。
4.宏可能會帶來運算符優(yōu)先級的問題,導(dǎo)致程容易出現(xiàn)錯。這就是我們上面提到宏參數(shù)代有的副作用。
5.宏是不能完成遞歸的,宏只會完成替換,并不會進行遞歸,但是函數(shù)可以遞歸?。?!
#和##
引入一個東西:
字符串打印的時候,如果中間出現(xiàn)相互配對的" " ;程序會自動合并他們,刪除中間的一切空格,有了這個知識之后,我們往下看:
#運算符將宏的?個參數(shù)轉(zhuǎn)換為字符串字面量。它僅允許出現(xiàn)在帶參數(shù)的宏的替換列表中。
就是你不想讓這個參數(shù)被替換成值,而是想要讓它就是原本的字符,可以這樣使用 #
#define PRINT(n,format) printf("the value of "#n" is "format"",n)
替換之后:printf(“the value of “a” is “”%d”"",10)
消掉中間的" " ; 變成printf(“the value of a is %d”,10)
##可以把位于它兩邊的符號合成?個符號,它允許宏定義從分離的?本?段創(chuàng)建標(biāo)識符。 ## 被稱為記號粘合
舉個例子,如果你想使用宏來創(chuàng)建int_max,float_max,double_max時,我們需要在_max前面的標(biāo)簽修改一下,為了不被認為是一個與_max組成的一個字符,我們可以使用 ## 來標(biāo)記:
#include <stdio.h>
#define type_max(type) \
type type##_max(type x,type y) \
{ \
return ((x)>(y)?(x):(y)); \
}
type_max(int);
type_max(double);
int main()
{
int ret = int_max(3, 5);
double ret2 = double_max(5.1, 6.5);
printf("%d\n", ret);
printf("%lf\n", ret2);
return 0;
}
命名約定
?般來講函數(shù)的宏的使用語法很相似。所以語言本身沒法幫我們區(qū)分二者。
那我們平時的?個習(xí)慣是:
把宏名全部大寫
函數(shù)名不要全部大寫
不建議#define 定義的常量或者宏后面加上分號; 替換也會把分號替換過去的,所以會很容易出錯!
#undef
這條指令?于移除?個宏定義,使用的時候記得跟你要移除的宏名。
#include <stdio.h>
#define M 10
int main()
{
printf("%d\n", M);
#undef M
printf("%d\n", M);
return 0;
}
命令行定義
許多C 的編譯器提供了?種能力,允許在命令行中定義符號。?于啟動編譯過程。
例如:當(dāng)我們根據(jù)同?個源?件要編譯出?個程序的不同版本的時候,這個特性有點?處。(假定某個程序中聲明了?個某個長度的數(shù)組,如果機器內(nèi)存有限,我們需要?個很小的數(shù)組,但是另外?個機器內(nèi)存大些,我們需要?個數(shù)組能夠大些。)
條件編譯
1
#if 常量表達式
//...
#endif
這個只有當(dāng)if 后面的表達式為真,后面的代碼才會被編譯,并且需要使用#endif 來確定你要控制的代碼段。
#include <stdio.h>
int main()
{
#if 0
printf("hello world!\n");
#endif
2. 多個分支的條件編譯
#if 常量表達式
//...
#elif 常量表達式
//...
#else
//...
#endif
這個和if 、else if、else 是類似的,只會進去一個。
3.判斷是否被定義
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
他們會自動匹配好,不需要使用大括號?。?!
頭文件的包含
在一個大型的工程中,每個人負責(zé)相應(yīng)的部分,最后所有人編寫的頭文件和源文件會鏈接到一起,所以難免會出現(xiàn)頭文件被多次包含的情況,如果不做處理,有多少頭文件就會被編譯多少,這樣會影響到程序的運行效率!
所以我們會使用下面的條件編譯指令來避免頭文件被多次包含:
例如:
#ifndef __ADD_H__(這里的__是由兩個_ 組成的?。?!下面也是)
#define __ADD_H__
//頭?件的內(nèi)容
#endif //__ADD_H__
當(dāng)然了,還有一個更加簡介的條件編譯指令,專門用在頭文上,寫在頭文件的第一行!
#program once
其他預(yù)處理指令
除了上面提到的條件編譯指令外,還有其他的一些條件編譯指令:
#error
#pragma
#line
...
#pragram pack() 在結(jié)構(gòu)體詳解中提到過!
這里不做詳細解說,大家有興趣的可以自己學(xué)習(xí)?。?!文章來源:http://www.zghlxwxcb.cn/news/detail-853130.html
《初始C語言》專欄到此完結(jié)撒花!??!文章來源地址http://www.zghlxwxcb.cn/news/detail-853130.html
到了這里,關(guān)于初始C語言最后一章《編譯、鏈接與預(yù)處理詳解》的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!