C/C++ 的編譯和連接

C/C++文件

C/C++程序文件包括 .h .c .hpp .cpp,其中源文件(.c .cpp)是基本的編譯單元,頭文件(.h .hpp)不會被編譯器編譯。c++

C/C++項目構建(build)過程,分爲如下幾個步驟 預處理 → 編譯 → 連接。程序員

預編譯

預編譯的過程能夠理解爲編譯器(其實是預處理器,這裏統稱爲編譯器就能夠了)在正式編譯以前處理C/C++文件中的預處理命令,即#開頭的代碼。函數

經常使用的幾個預處理命令以下:優化

#include ......ui

#ifdef ...... #else......#endifspa

#define ......code

#pragma ......blog

舉個例子,下面是個很簡單的類定義:ip

MyClass.h作用域

#define DEFAULT_VALUE 0

class MyClass {
public:
    void Fun();
public:
    int value = DEFAULT_VALUE;
};

MyClass.cpp

#include "MyClass.h"

void MyClass::Fun() {
    // Do someting
    return;
}

預編譯完成後的樣子:

class MyClass {
public:
    void Fun();
public:
    int value = 0;
};

void MyClass::Fun() {
    // Do someting
    return;
}

能夠看到編譯器把.h文件替換到了.cpp文件中的#include 位置上,把DEFAULT_VALUE定義的值也替換到了相應的位置。

 

編譯

預編譯以後,編譯器會編譯每一個源文件(.c .cpp),若是編譯成功,會生成對應的目標文件,Linux爲.o文件,Windows平臺下爲.obj文件。

以Linux平臺爲例,上面的MyClass.cpp編譯完成後會生成MyClass.o文件

使用objdump能夠看到目標文件MyClass.o的內容

$$ objdump -x MyClass.o

......
0000000000000000 g     F .text  0000000000000015 _ZN7MyClass3FunEv
......

編譯器會把MyClass::Fun()的名字改爲_ZN7MyClass3FunEv,這個過程叫Mangle,因爲C++支持重載,覆蓋等特性,因此編譯器必須把函數用一個惟一的標識表示。這個字符串就是編譯器生成的惟一標識。

這裏還要單獨說一下頭文件,頭文件的既然不是編譯單元,那麼它的做用是什麼?

頭文件就是負責」聲明「,編譯器在編譯MyClass.cpp的時候,對於MyClass這個類以及Fun()這個成員函數,編譯器必須找到它的聲明,這個函數才能被正確編譯。

若是有其餘cpp須要使用MyClass這個類的時候,也須要它的的聲明。例如

main.cpp

#include "MyClass.h"

int main(int argc, char** argv) {
    MyClass tmp;
    tmp.Fun();
    return 0;
}

加上#include "MyClass.h" 編譯器在編譯main.cpp的時候才知道怎麼編譯MyClass這個類。MyClass.h裏聲明是不會真正被編譯到main.o中,.h文件中的內容在目標文件中只是以列表的形式存在,這個表在後面連接時會用到。

固然,頭文件不只能夠用來聲明,還能夠定義(定義全局變量,全局函數等),在頭文件中的定義要當心,可能會引發連接錯誤。

 

連接

連接就是將一堆目標文件加靜態庫文件裝配成可執行文件的過程。(或者是裝配成靜態/動態庫的過程)

上面兩個cpp分別被編譯成了MyClass.o, 和main.o,咱們要生成可執行程序的話,就必須通過連接的過程,把兩個目標文件合成一個可執行文件。

main.o中,main函數會構造MyClass, 而且調用Fun()函數,那麼main就根據MyClass.h生成的表,找到MyClass.o中的函數,這個就是連接器要作的工做。

 

常見錯誤

構建c/c++工程的時候,最多見的就是兩種錯誤:

-- 編譯錯誤,在編譯過程當中產生的錯誤,一般是語法錯誤,沒有聲明,重複聲明致使編譯目標文件錯誤

其中沒有聲明一般是因爲沒有#include相應的頭文件,或者頭文件缺乏相應的聲明。

而重複聲明一般是#include了相同的頭文件,好比 B.h 和 C.h 都包含 A.h,而後 main.h 包含了 B.h 和 C.h,這就致使A.h 在main中被包含了兩次。

解決這個問題的方法是能夠在全部.h文件的第一行加上

#pragma once

或者,使用#ifndef ... #define ... #endif 語句塊

#ifndef NEWCLASS_H
#define NEWCLASS_H

......

#endif /* NEWCLASS_H */

 

-- 連接錯誤,常見的錯誤也是兩種,沒有定義和重複定義,和上面的沒有聲明,重複聲明相似。(這裏定義指的就是函數實現)

  • 先討論沒有定義(undefined reference to xxx)

一般是由於函數有聲明,並且被使用了,可是沒有被定義。好比上面MyClass.cpp中,若是Fun()沒有被實現的話,MyClass.cpp和main.cpp編譯時都不會報錯,可是連接時會報告找不到Fun()。

固然,若是Fun()沒被main.cpp調用的話,即便不實現它,整個構建過程也不會出錯,由於連接器根本不會去找這個函數的定義。

  • 而後是重複定義(multiple definition)

指的一份相同的定義在兩個目標文件中都存在,連接的時候連接器不知道時用哪一個了。這種問題一般因爲全局函數,和全局變量定義在了頭文件中。致使多個目標文件包含相同的全局函數和全局變量的定義。

解決方法就是在頭文件中聲明,定義放到cpp文件中,或者爲定義加上const 或 static這樣的修飾符,連接時會對這些帶有const和修飾符的變量特殊處理的。

const只適用於定義常量變量,static定義的是靜態全局變量,只在當前cpp有效,因此連接它也不會被別的目標文件連接,就不會有重複定義的問題了。

總之在頭文件中定義變量和函數要特別主意,可能會致使連接錯誤。

固然也不是全部定義都不能放到頭文件中,好比剛纔說的const常量,static全局變量就是例外,還有內聯函數,能夠定義在.h文件中,由於內聯函數會被拷貝到每一個目標文件中,也不會參與連接的過程。

還有模板類必須放在頭文件中定義,這個下面會討論這個。

 

關於模板,靜態成員變量

模板類模板函數必須聲明和定義在頭文件中,緣由是什麼,舉個例子,假設MyClass是模板類

MyClass.h

template <typename T>
class MyClass {
public:
    void Fun();
public:
    T value;
};

MyClass.cpp

#include "MyClass.h"

template <typename T>
void MyClass<T>::Fun() {
    // Do someting
    return;
}

main.cpp

int main(int argc, char** argv) {
    MyClass<int> tmp;
    tmp.Fun();
    return 0;
}

編譯的時候沒有問題,可是連接時會報錯,main.cpp找不到MyClass<int>::Fun(),以下圖

MyClass雖然定義了Fun函數,可是MyClass.o中存在MyClass<T>::Fun(),而根據MyClass.h文件,main.o中須要找到MyClass<int>::Fun()的定義

結果連接器哪都找不到,只好報錯了。(實際上經過objdump查看MyClass.o,編譯器都沒有生成MyClass<T>::Fun(),由於編譯器認爲這個函數沒人使用,就直接優化掉了)

若是非得在cpp中定義模板類的成員函數呢,有一種方法就是要顯示的在cpp文件中聲明,好比

MyClass.cpp

#include "MyClass.h"

template <typename T>
void MyClass<T>::Fun() {
    // Do someting
    return;
}

template void MyClass<int>::Fun();

加上下面這行就不會有問題了,可是缺點就是開發MyClass的程序員無從知道其餘類是怎麼使用這個模板的,不可能把全部可能的模板參數全都一一的列在這裏。

因此模板類的定義仍是要寫在.h文件中,

那麼若是main.cpp使用到了MyClass<int>, 另一個cpp也使用到了MyClass<int>,會不會產生重複代碼致使重複定義呢,不會,編譯器會處理好模板類的。

 

下面是靜態成員變量,爲何靜態成員變量的定義要放在cpp裏,(模板類的靜態成員變量除外)

靜態成員變量和靜態全局成員變量不一樣。

靜態成員變量的做用域能夠是整個工程,而靜態全局變量的做用域只是當前的cpp。因此靜態成員變量定義在.h中就會發生重定義錯誤。

相關文章
相關標籤/搜索