C++編譯連結的那些小事 .
最近,有同事向我多次問及C++關於編譯連結方面的問題,包括如下:
1:什麼樣的函式以及變數可以定義在標頭檔案中
2:extern "C"的作用
3:防止重複包含的巨集的作用
4:函式之間是怎麼連結起來的
我認為,這些問題不難,書上基本上都有,但要是沒有真正思考過,就憑死記硬背,也就是隻能“嘴上說說”而已,遇到問題還真棘手,所以我覺得有必要說一下。
C/C++的編譯連結過程
其實,“編譯”這個詞大多數時候,我們指的是由一堆.h,.c,.cpp檔案生成連結庫或者可執行檔案的過程。但是拿C/C++來說,其實這是很模糊的,由一堆C/C++檔案生成應用程式包括預處理---編譯檔案---連結(寫的比較粗糙,不影響本文論述)。
首先,要明白什麼是編譯單元,一個編譯單元可以認為是一個.c或者.cpp檔案,每一個編譯單元首先會經過預處理得到一個臨時的編譯單元,這裡稱為tmp.cpp,預處理會把.c或者.cpp直接或者間接包含的其它檔案(不只侷限於.h檔案,只要是#include即可)的內容替換進來,並展開巨集呼叫等。
下面首先看一個例子:
a.h
#ifndef A_H_
#define A_H_
static int a = 1;
void fun();
#endif
a.cpp
#include "a.h"
static void hello_world()
{
}
只有a.h和a.cpp這兩個檔案,及其簡單。首先通過g++的-E引數得到a.cpp預處理之後的內容
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" 1
static int a = 1;
void fun();
# 2 "a.cpp" 2
static void hello_world()
{
}
tmp.cpp就是隻經過預處理得到的檔案,這個檔案才是編譯器能夠真正看到的檔案。這個過程就是預處理。
其中#define A_H_的作用是防止重複包含a.h這個標頭檔案,很多人都知道這一點,但是再仔細問,我見過大多數人都說不清楚。
這種巨集是為了防止一個編譯單元(cpp檔案)重複包含同一個標頭檔案。它在預處理階段起作用,前處理器發現a.cpp內已經定義過A_H_這個巨集的話,在a.cpp中再次發現#include "a.h"的時候就不會把a.h的內容替換進a.cpp了。
編譯器看到tmp.cpp的時候,會編譯成一個obj檔案,最後由連結器對這一個對obj檔案進行連結,從而得到可執行程式。
編譯錯誤和連線錯誤
編譯錯誤指的是一個cpp編譯單元在編譯時發生的錯誤,這種錯誤一般都是語法錯誤,拼寫錯誤,引數不匹配等。
以main.cpp為例(只有一個main函式)
int main()
{
hello_world();
}
編譯(加-c參數列示只編譯不連結)
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函式未宣告,把void hello_world();這條語句加到main函式前面,再次編譯
coderchen@coderchen:~/c++$ g++ -c -o main.o main.cpp
coderchen@coderchen:~/c++$
編譯成功,雖然我們呼叫了hello_world函式,卻沒有定義這個函式。好,接下來,我們把這個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這個函式找不到。這個例子很簡單,基本上可以區分出編譯錯誤和連結錯誤。我們再新增一個hello_world.cpp
void hello_world()
{
}
編譯
coderchen@coderchen:~/c++$ g++ -c -o hello_world.o hello_world.cpp
連結
coderchen@coderchen:~/c++之所以$ g++ -o main main.o hello_world.o
ok,我們的main程式已經生成了,我們經歷了預處理---編譯---連結的過程。
有的人說為什麼不需要寫一個hello_world.h的標頭檔案,宣告hello_world函式,然後再讓main.cpp包含hello_world.h呢?這樣寫自然是標準的做法,不過預處理過後,和我們現在寫的一樣的,預處理會把hello_world.h的內容替換到main.cpp中。
問題:在連結的時候,main.o怎麼知道hello_world函式定義在hello_world.o中呢?
答案:main.o不知道hello_world函式定義在那個obj檔案中,每個obj檔案都有一個匯出符號表,對於這個例子,hello_world.o的匯出符號表中有hello_world這個函式,而main.o需要用到這個函式,可以想象就像幾個插槽一樣。連結器通過掃描obj檔案發現這個函式定義在hello_world.o中,然後就可以連結了。
問題:為什麼函式不能定義在標頭檔案中?
這個問題是不恰當的,因為用inline和static修飾的函式可以定義在標頭檔案中,而inline修飾的函式必須定義在標頭檔案中。
如果函式定義在標頭檔案中,並且有多個cpp檔案都包含了這個標頭檔案的話,那麼這些cpp檔案生成的obj檔案的匯出符號表中都有這個標頭檔案中定義的函式,單檔案編譯的時候是不會出錯的,但是連結的時候就會報錯。連結器發現了多個函式實體,但卻無法確定應該使用哪一個。這是一個連結錯誤。
inline修飾的函式,通常都不會存在函式實體,即便編譯器沒有對其內聯,那麼obj檔案也不會匯出inline函式,所以連結不會出錯。
static修飾的函式,只能由定義它的編譯單元呼叫,也不會匯出。如果標頭檔案中頂一個static修飾的函式,就相當於多個obj檔案中都頂一個了一個一模一樣的函式,大家各用各的,互補干擾。
問題:什麼樣的變數可以定義在標頭檔案中?
其實變數於函式很類似,由static或const修飾的變數可以定義在標頭檔案中。
static修飾的變數於static修飾的函式一樣,道理同上。
const修飾的變數預設是不會進入匯出符號表的,相當於每個obj中都定義了一個一模一樣的const變數,各用各的。而const可以再用extern修飾,如果用extern const修飾的變數定義在標頭檔案中,那麼就會出現連結錯誤,原因就是“想一想extern是幹嘛的”
問題:extern "C"是幹嘛的?
如果有人回答“相容C和C++”,我只能說“這是一個正確答案,但我不知道你是否真的知道”。
首先要知道C不支援過載,C++支援過載,C++為了支援過載,引入了函式重新命名的機制,就像下面這樣:
int hello_world(type1 param);
int hello_world(type2 param);
通常第一個函式會被編譯成hello_world_type1這樣子,第二個函式會被編譯成hello_world_type2這樣子。不管是定義的地方還是呼叫的地方,都會把函式改成同樣的名字,所以連結器可以正確的找到函式實體。
而我們寫C++程式的時候,通常會引入由c編寫的庫(gcc編譯的c檔案),而c不支援過載,自然不會對函式重新命名。而我們在C++中呼叫的地方很可能會重新命名,這就造成了呼叫的地方(C++編譯)和定義的地方(C編譯)函式名不一致的情況,這也是一種連結錯誤。
所以我們經常會看到在C++中用extern "C" { #include "some_c.h" }這種程式碼。這就是告訴c++編譯器,some_c.h中的函式要按照c的方式編譯,不要重新命名,這樣在連結的時候就ok了。
相關文章
- C++應用程式在Windows下的編譯、連結(四)動態連結C++Windows編譯
- G++編譯連結的那些事!G++的特殊使用方法[常用]編譯
- C/C++預處理、編譯、連結過程【Z】C++編譯
- 編譯連結過程編譯
- 編譯、連結學習筆記(一)簡述編譯連結過程編譯筆記
- 程式的編譯和連結原理分析編譯
- (轉)編譯和連結的區別編譯
- 關於程式的編譯和連結編譯
- GCC編譯過程(預處理->編譯->彙編->連結)GC編譯
- 編譯、彙編、連結、載入、顯示編譯
- GCC編譯和連結過程GC編譯
- 談談 React 那些小事React
- C++應用程式在Windows下的編譯、連結:第三部分 靜態連結(一)C++Windows編譯
- C++應用程式在Windows下的編譯、連結:第三部分 靜態連結(二)C++Windows編譯
- Java程式碼的編譯與反編譯那些事兒Java編譯
- 又拍雲 API 使用的那些小事API
- 關於rails效能的那些小事兒AI
- gcc 從語言編譯全過程 預處理---->編譯---->彙編----->連結GC編譯
- C語言的編譯連結執行過程C語言編譯
- 程式設計師的自我修養-編譯連結程式設計師編譯
- Android 編譯打包的那些疑問Android編譯
- jad反編譯工具的那些事編譯
- Cookie&Session,登入的那些小事兒~CookieSession
- 使用pybind11為Python編寫C++擴充套件(一)配置篇:Build(編譯和連結)PythonC++套件UI編譯
- c++的連結器C++
- C++應用程式在Windows下的編譯、連結:第一部分 概述C++Windows編譯
- protobuf 的交叉編譯使用(C++)編譯C++
- 編譯C++ 程式的過程編譯C++
- iOS開發你不知道的事-編譯&連結iOS編譯
- Ubuntu中編譯連結Opencv應用的簡便方式Ubuntu編譯OpenCV
- windows 下c++編譯WindowsC++編譯
- C++ 編譯過程C++編譯
- C++應用程式在Windows下的編譯、連結:第二部分COFF/PE檔案結構C++Windows編譯
- CMake 進行多專案中dll的編譯和連結編譯
- 編譯 TensorFlow 的 C/C++ 介面編譯C++
- C語言編譯和連結過程簡介C語言編譯
- linux下C/C++編譯時系統搜尋 include 和 連結庫 檔案路徑的指定LinuxC++編譯
- C++編譯器優化C++編譯優化