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