C++编译链接的那些小事
本文轉(zhuǎn)載,尊重原創(chuàng)!受益良多!點(diǎn)擊打開(kāi)鏈接
最近,有同事向我多次問(wèn)及C++關(guān)于編譯鏈接方面的問(wèn)題,包括如下:
1:什么樣的函數(shù)以及變量可以定義在頭文件中
2:extern "C"的作用
3:防止重復(fù)包含的宏的作用
4:函數(shù)之間是怎么鏈接起來(lái)的
我認(rèn)為,這些問(wèn)題不難,書(shū)上基本上都有,但要是沒(méi)有真正思考過(guò),就憑死記硬背,也就是只能“嘴上說(shuō)說(shuō)”而已,遇到問(wèn)題還真棘手,所以我覺(jué)得有必要說(shuō)一下。
C/C++的編譯鏈接過(guò)程
其實(shí),“編譯”這個(gè)詞大多數(shù)時(shí)候,我們指的是由一堆.h,.c,.cpp文件生成鏈接庫(kù)或者可執(zhí)行文件的過(guò)程。但是拿C/C++來(lái)說(shuō),其實(shí)這是很模糊的,由一堆C/C++文件生成應(yīng)用程序包括預(yù)處理---編譯文件---鏈接(寫(xiě)的比較粗糙,不影響本文論述)。
首先,要明白什么是編譯單元,一個(gè)編譯單元可以認(rèn)為是一個(gè).c或者.cpp文件,每一個(gè)編譯單元首先會(huì)經(jīng)過(guò)預(yù)處理得到一個(gè)臨時(shí)的編譯單元,這里稱為tmp.cpp,預(yù)處理會(huì)把.c或者.cpp直接或者間接包含的其它文件(不只局限于.h文件,只要是#include即可)的內(nèi)容替換進(jìn)來(lái),并展開(kāi)宏調(diào)用等。
下面首先看一個(gè)例子:
a.h
#ifndef A_H_ #define A_H_ static int a = 1; void fun(); #endifa.cpp
#include "a.h"static void hello_world() { }只有a.h和a.cpp這兩個(gè)文件,及其簡(jiǎn)單。首先通過(guò)g++的-E參數(shù)得到a.cpp預(yù)處理之后的內(nèi)容
coderchen@coderchen:~/c++$ g++ -E a.cpp > tmp.cpp查看tmp.cpp
# 1 "a.cpp" # 1 "<built-in>" # 1 "<command-line>" # 1 "a.cpp" # 1 "a.h" 1static int a = 1; void fun(); # 2 "a.cpp" 2static void hello_world() { }tmp.cpp就是只經(jīng)過(guò)預(yù)處理得到的文件,這個(gè)文件才是編譯器能夠真正看到的文件。這個(gè)過(guò)程就是 預(yù)處理。其中#define A_H_的作用是防止重復(fù)包含a.h這個(gè)頭文件,很多人都知道這一點(diǎn),但是再仔細(xì)問(wèn),我見(jiàn)過(guò)大多數(shù)人都說(shuō)不清楚。
這種宏是為了防止一個(gè)編譯單元(cpp文件)重復(fù)包含同一個(gè)頭文件。它在預(yù)處理階段起作用,預(yù)處理器發(fā)現(xiàn)a.cpp內(nèi)已經(jīng)定義過(guò)A_H_這個(gè)宏的話,在a.cpp中再次發(fā)現(xiàn)#include "a.h"的時(shí)候就不會(huì)把a(bǔ).h的內(nèi)容替換進(jìn)a.cpp了。
編譯器看到tmp.cpp的時(shí)候,會(huì)編譯成一個(gè)obj文件,最后由鏈接器對(duì)這一個(gè)對(duì)obj文件進(jìn)行鏈接,從而得到可執(zhí)行程序。
編譯錯(cuò)誤和連接錯(cuò)誤
編譯錯(cuò)誤指的是一個(gè)cpp編譯單元在編譯時(shí)發(fā)生的錯(cuò)誤,這種錯(cuò)誤一般都是語(yǔ)法錯(cuò)誤,拼寫(xiě)錯(cuò)誤,參數(shù)不匹配等。
以main.cpp為例(只有一個(gè)main函數(shù))
int main() { hello_world(); }編譯(加-c參數(shù)表示只編譯不鏈接)
coderchen@coderchen:~/c++$ g++ -c -o main.o main.cpp main.cpp: In function ‘int main()’: main.cpp:4: error: ‘hello_world’ was not declared in this scope這種錯(cuò)誤就是編譯,原因是hello_world函數(shù)未聲明,把void hello_world();這條語(yǔ)句加到main函數(shù)前面,再次編譯
coderchen@coderchen:~/c++$ g++ -c -o main.o main.cpp coderchen@coderchen:~/c++$編譯成功,雖然我們調(diào)用了hello_world函數(shù),卻沒(méi)有定義這個(gè)函數(shù)。好,接下來(lái),我們把這個(gè)main.o文件鏈接下,
coderchen@coderchen:~/c++$ g++ -o main main.o main.o: In function `main': main.cpp:(.text+0x7): undefined reference to `hello_world()' collect2: ld returned 1 exit status看到了吧,鏈接器ld報(bào)出了鏈接錯(cuò)誤,原因是hello_world這個(gè)函數(shù)找不到。這個(gè)例子很簡(jiǎn)單,基本上可以區(qū)分出編譯錯(cuò)誤和鏈接錯(cuò)誤。我們?cè)偬砑右粋€(gè)hello_world.cpp
void hello_world() { }編譯
鏈接
coderchen@coderchen:~/c++之所以$ g++ -o main main.o hello_world.ook,我們的main程序已經(jīng)生成了,我們經(jīng)歷了預(yù)處理---編譯---鏈接的過(guò)程。有的人說(shuō)為什么不需要寫(xiě)一個(gè)hello_world.h的頭文件,聲明hello_world函數(shù),然后再讓main.cpp包含hello_world.h呢?這樣寫(xiě)自然是標(biāo)準(zhǔn)的做法,不過(guò)預(yù)處理過(guò)后,和我們現(xiàn)在寫(xiě)的一樣的,預(yù)處理會(huì)把hello_world.h的內(nèi)容替換到main.cpp中。
問(wèn)題:在鏈接的時(shí)候,main.o怎么知道hello_world函數(shù)定義在hello_world.o中呢?
答案:main.o不知道hello_world函數(shù)定義在那個(gè)obj文件中,每個(gè)obj文件都有一個(gè)導(dǎo)出符號(hào)表,對(duì)于這個(gè)例子,hello_world.o的導(dǎo)出符號(hào)表中有hello_world這個(gè)函數(shù),而main.o需要用到這個(gè)函數(shù),可以想象就像幾個(gè)插槽一樣。鏈接器通過(guò)掃描obj文件發(fā)現(xiàn)這個(gè)函數(shù)定義在hello_world.o中,然后就可以鏈接了。
問(wèn)題:為什么函數(shù)不能定義在頭文件中?
這個(gè)問(wèn)題是不恰當(dāng)?shù)?#xff0c;因?yàn)橛胕nline和static修飾的函數(shù)可以定義在頭文件中,而inline修飾的函數(shù)必須定義在頭文件中。
如果函數(shù)定義在頭文件中,并且有多個(gè)cpp文件都包含了這個(gè)頭文件的話,那么這些cpp文件生成的obj文件的導(dǎo)出符號(hào)表中都有這個(gè)頭文件中定義的函數(shù),單文件編譯的時(shí)候是不會(huì)出錯(cuò)的,但是鏈接的時(shí)候就會(huì)報(bào)錯(cuò)。鏈接器發(fā)現(xiàn)了多個(gè)函數(shù)實(shí)體,但卻無(wú)法確定應(yīng)該使用哪一個(gè)。這是一個(gè)鏈接錯(cuò)誤。
inline修飾的函數(shù),通常都不會(huì)存在函數(shù)實(shí)體,即便編譯器沒(méi)有對(duì)其內(nèi)聯(lián),那么obj文件也不會(huì)導(dǎo)出inline函數(shù),所以鏈接不會(huì)出錯(cuò)。
static修飾的函數(shù),只能由定義它的編譯單元調(diào)用,也不會(huì)導(dǎo)出。如果頭文件中頂一個(gè)static修飾的函數(shù),就相當(dāng)于多個(gè)obj文件中都頂一個(gè)了一個(gè)一模一樣的函數(shù),大家各用各的,互補(bǔ)干擾。
問(wèn)題:什么樣的變量可以定義在頭文件中?
其實(shí)變量于函數(shù)很類似,由static或const修飾的變量可以定義在頭文件中。
static修飾的變量于static修飾的函數(shù)一樣,道理同上。
const修飾的變量默認(rèn)是不會(huì)進(jìn)入導(dǎo)出符號(hào)表的,相當(dāng)于每個(gè)obj中都定義了一個(gè)一模一樣的const變量,各用各的。而const可以再用extern修飾,如果用extern const修飾的變量定義在頭文件中,那么就會(huì)出現(xiàn)鏈接錯(cuò)誤,原因就是“想一想extern是干嘛的”
問(wèn)題:extern "C"是干嘛的?
如果有人回答“兼容C和C++”,我只能說(shuō)“這是一個(gè)正確答案,但我不知道你是否真的知道”。
首先要知道C不支持重載,C++支持重載,C++為了支持重載,引入了函數(shù)重命名的機(jī)制,就像下面這樣:
通常第一個(gè)函數(shù)會(huì)被編譯成hello_world_type1這樣子,第二個(gè)函數(shù)會(huì)被編譯成hello_world_type2這樣子。 不管是定義的地方還是調(diào)用的地方,都會(huì)把函數(shù)改成同樣的名字,所以鏈接器可以正確的找到函數(shù)實(shí)體。
而我們寫(xiě)C++程序的時(shí)候,通常會(huì)引入由c編寫(xiě)的庫(kù)(gcc編譯的c文件),而c不支持重載,自然不會(huì)對(duì)函數(shù)重命名。而我們?cè)贑++中調(diào)用的地方很可能會(huì)重命名,這就造成了調(diào)用的地方(C++編譯)和定義的地方(C編譯)函數(shù)名不一致的情況,這也是一種鏈接錯(cuò)誤。
所以我們經(jīng)常會(huì)看到在C++中用extern "C" { #include "some_c.h" }這種代碼。這就是告訴c++編譯器,some_c.h中的函數(shù)要按照c的方式編譯,不要重命名,這樣在鏈接的時(shí)候就ok了。
總結(jié)
以上是生活随笔為你收集整理的C++编译链接的那些小事的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Opencv载入图片并显示的问题
- 下一篇: 颜色空间那些事儿