深入理解C++在.h頭文件中定義函數導致的multiple definition

問題:某個頭文件中聲明並定義了一個函數,然後在多個源碼文件中調用該函數,編譯鏈接時出現了該函數multiple definition問題,在頭文件中添加了 #ifndef 頭也不行,經過嘗試發現如果將該函數的聲明和定義分開到.h和.cpp文件之後問題消失,爲什麼不能將函數直接定義在.h文件中呢?

針對該問題,抽象出如下幾個問題:

1頭文件中只可放置函數聲明,不可放置函數定義嗎?

以下面的程序爲例:

// a.h

#ifndef __a_h__
#define __a_h__
void funcA(void);   // 聲明
void funcA(void)    // 定義
{}
#endif

// b.h

#ifndef __b_h__
#define __b_h__
void funcB(void);
#endif

// b.cpp

#include "b.h"
#include "a.h"
void funcB(void)
{
    funcA();
}

//c.h

#ifndef __c_h__
#define __c_h__
void funcC(void);
#endif

//c.cpp

#include "c.h"
#include "a.h"
void funcC(void)
{
    funcA();
}

//main.cpp

#include "b.h"
#include "c.h"
int main(int argc, char* argv[])
{
    funcB();
    funcC();
    return 0;
}

上述代碼編譯鏈接的時候編譯器(g++)會報如下錯誤:

c.o: In function `funcA()':
c.cpp:(.text+0x0): multiple definition of `funcA()'
b.o:b.cpp:(.text+0x0): first defined here
collect2: ld returned 1 exit status

爲什麼編譯器在鏈接的時候會抱怨“funcA()重複定義”?

其實本質問題就是funcA的定義被放在了a.h中,如果寫在a.cpp中,就不會有重複定義的問題。下面分析一下編譯過程都發生了什麼,這樣更容易從編譯器的角度理解此問題。

編譯器處理include指令很簡單粗暴,就是直接把頭文件中的內容包含進來。所以b.cpp、c.cpp和main.cpp代碼展開後可以簡化爲:

// b.cpp

void funcA(void);   // 聲明
void funcA(void)    // 定義
{}
void funcB(void);
void funcB(void)
{
    funcA();
}

// c.cpp

void funcA(void);   // 聲明
void funcA(void)    // 定義
{}
void funcC(void);
void funcC(void)
{
    funcA();
}

// main.cpp

void funcB(void);
void funcC(void);
int main(int argc, char* argv[])
{
    funcB();
    funcC();
    return 0;
}

編譯的時候,C++是採用獨立編譯,就是每個cpp單獨編譯成對應的.o文件,最後鏈接器再將多個.o文件鏈接成可執行程序。所以從編譯的時候,從各個cpp文件看,編譯沒有任何問題。但是能發現一個問題,b.o中聲明和定義了一次funcA(),c.o中也聲明和定義funcA(),這就是編譯器報重複定義的原因。有人可能會問,既然是從同一份文件include過來的函數funcA,那麼定義都是同一份,爲什麼編譯器不會智能的處理一下,讓鏈接時候不報錯呢?

其實編譯器鏈接的時候,並不知道b.cpp中定義的funcA與c.cpp中定義的funcA是同一個文件include過來的,它只會認爲如果有兩份定義,而且這兩份定義如果實現不同,那麼到底以哪個爲準呢?既然決定不了,那就乾脆報錯好了。

2爲什麼有些頭文件中直接把函數定義都寫進去了?

剛纔的分析,可以得出結論:頭文件中只做變量和函數的聲明,而不要定義,否則就會有重複定義的錯誤。但是有幾種情況是例外的。

內聯函數的定義
類(class)的定義
const 和 static 變量

以上幾種可以在頭文件中定義,下面逐個進行解釋。

內聯的目的就是在編譯期讓編譯器把使用函數的地方直接替換掉,而不是像普通函數一樣通過鏈接器把地址鏈接上。這種情況,如果定義沒有在頭文件的話,編譯器是無法進行函數替換的。所以C++規定,內聯函數可以在程序中定義多次,只要內聯函數定義在同一個cpp中只出現一次就行。
按照這個理論,上述a.h簡單修改一下就可以避免重複定義了。

// a.h

#ifndef __a_h__
#define __a_h__
inline void funcA(void);   // 內聯聲明
void funcA(void)    // 定義
{}
#endif

此外,類(class)的定義,可以放在頭文件中。

用類創建對象的時候,編譯器要知道對象如何佈局才能分配內存,因此類的定義需要在頭文件中。一般情況下,我們把類內成員函數的定義放在cpp文件中,但是如果直接在class中完成函數聲明+定義的話,這種函數會被編譯器當作inline的,因此滿足上面inline函數可以放在頭文件的規則。但是如果聲明和定義分開實現,但是都放在頭文件中,那就會報重複定義了!!

const 和 static 變量,可以放在頭文件中。

const對象默認是static的,而不是extern的,所以即使放在頭文件中聲明和定義。多個cpp引用同一個頭文件,互相也沒有感知,所以不會導致重複定義。

3模板函數/類中要求頭文件中必須包含定義才能進行模板實例化,這種定義放在頭文件的情況會不會有問題?

前面分析可知,頭文件中要麼只有函數聲明,要麼是含有inline函數的定義。但是模板的定義(包括非inline函數/成員函數)要求聲明和實現都必須放在頭文件中,難道沒有“重複定義”的問題???
答案當然是不會有問題(要不template早就被抱怨死了)。其實編譯器也考慮到會遇到類似的問題,在編譯器或連接器的某處已經有防止重定義的處理了。這裏參考stackflow中的答案:http://stackoverflow.com/questions/235616/multiple-definitions-of-a-function-template

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章