發現每次寫技術博客時,都會在文章開頭處花費一番功夫
...從前,有一個程序員....他的名字叫magicsoar
為什么有時會出現aaa已在bbb中重定義的錯誤?
為什么有時會出現無法解析的外部符號?
為什么有的內聯函數的定義需要寫在頭文件中?
為什么對於模板,聲明和定義都要寫在一起?
讀完這篇博客,相信你會有一個初步的認識
注,我們現在談的編譯其實可以認為由4個環節組成,其中有編譯環節,鏈接環節, 我會盡量在上下文中指明說的總體的編譯,還是具體的編譯環節,望讀者周知
關於編譯過程詳解說明,可以參照我之前的一篇博客 C++編譯與鏈接(1)-編譯與鏈接過程
編譯單元
首先讓我們來認識一下編譯單元,什么是編譯單元呢?簡單來說一個cpp文件就是一個編譯單元。
在集成式的IDE中,我們往往點擊一下運行便可以了,編譯的所有工作都交給了IDE去處理,往往忽略了其中的內部流程
事實上編譯每個編譯單元(.cpp)時是相互獨立的,即每個cpp文件之間是不知道對方的存在的(不考慮#include “xxx.cpp" 這種奇葩的寫法)
編譯器會分別將每個編譯單元(.cpp)進行編譯,生成相應的obj文件
然后鏈接器會將所有的obj文件進行鏈接,生成最終可執行文件
內部鏈接與外部鏈接
那么什么內部鏈接和外部鏈接又是什么呢?
我們知道C++中聲明和定義是可以分開的
例如在vs中,我們可以一個函數聲明定義放在b.cpp中,在a.cpp只需再聲明一下這個函數,就可以在a.cpp中使用這個函數了
a.cpp
void show(); int main() { show(); return 0; }
b.cpp
#include <iostream> void show() { std::cout << "Hello" << std::endl; }
而通過之前的了解,我們知道每個編譯單元間是相互獨立不知道彼此的存在的
那么a.cpp又是如何知道show函數的定義的呢
其實在編譯一個編譯單元(.cpp)生成相應的obj文件過程中
編譯器會將分析這個編譯單元(.cpp)
將其所能提供給其他編譯單元(.cpp)使用的函數,變量定義記錄下來。
而將自己缺少的函數,變量的定義也記錄下來。
所以可以認為a.obj和b.obj記錄了以下的信息
然后在鏈接器連接的時候就會知道a.obj需要show函數定義,而b.obj中恰好提供了show函數的定義,通過鏈接,在最終的可執行文件中我們能看到show函數的運行
哪這些又和內部鏈接,外部鏈接有什么關系呢?
那些編譯單元(.cpp)中能向其他編譯單元(.cpp)展示,提供其定義,讓其他編譯單元(.cpp)使用的的函數,變量就是外部鏈接,例如全局變量
而那些編譯單元(.cpp)中不能向其他編譯單元(.cpp)展示,提供其定義的函數,變量就是內部鏈接,例如static函數,inline函數等
好了讓我們看下編譯單元,內部鏈接和外部鏈接比較正式的定義吧
編譯單元:當一個c或cpp文件在編譯時,預處理器首先遞歸包含頭文件,形成一個含有所有 必要信息的單個源文件,這個源文件就是一個編譯單元。
內部連接:如果一個名稱對編譯單元(.cpp)來說是局部的,在鏈接的時候其他的編譯單元無法鏈接到它且不會與其它編譯單元(.cpp)中的同樣的名稱相沖突。
外部連接:如果一個名稱對編譯單元(.cpp)來說不是局部的,而在鏈接的時候其他的編譯單元可以訪問它,也就是說它可以和別的編譯單元交互。
最后讓我們回到文章開頭處的那幾個問題吧
為什么有時會出現aaa已在bbb中重定義的錯誤?
答:你可能在不同的cpp中重復定義了一個具有外部鏈接的函數或變量,鏈接器在鏈接時找到了多個一樣的函數或變量定義
為什么有時會出現無法解析的外部符號?
答:你可能只提供了函數或變量的聲明,沒有提供其定義,或者聲明和定義的函數原型不一致,鏈接器沒有找到其定義在哪里,所以在鏈接環節出現了無法解析的外部符號的錯誤
為什么有的內聯函數的定義需要寫在頭文件中呢?
答:因為內鏈函數是內部鏈接的,如果你在b.cpp中定義這個函數,那么在a.cpp中即使有這個函數聲明,但由於內鏈函數是內部鏈接的,所以b.cpp不會提供其定義
所以在鏈接時a.obj無法找到這個函數的定義,便會出現無法解析的外部符號的錯誤
為什么對於模板,聲明和定義都要寫在一起呢?
答:我們假設我們有如下結構的代碼
b.h
#pragma once template<typename T> class A { public: A(const T &t); };
b.cpp
#include "b.h" #include <iostream> template<typename T> A<T>::A(const T &t) { std::cout << t << std::endl; }
a.cpp
#include "b.h" int main() { //A<int> a(5); return 0; }
那么a.cpp中注釋的那行代碼能否正常運行呢?答案是不能我們首先來分析一下編譯器在編譯a.cpp時,發現其缺少A<int>::a(const int& t)的定義而在編譯器編譯b.cpp時,由於每個編譯單元是獨立的,而模板只有被用到的時候才會被實例化,產生定義,b.cpp不知道a.cpp用了A<int>::a(const int& t),所以它不會提供A<int>::a(const int& t)的定義,編譯器不會有任何反應,這樣在鏈接時a.obj無法找到A<int>::a(const int& t)的定義,就會出現無法解析的外部符號的錯誤
宏是內部鏈接還是外部鏈接
答:都不是,宏在預處理環節時就被替換掉了,而內部鏈接與外部鏈接是針對編譯環節與鏈接環節而言的

