C/C++檔案
C/C++程式檔案包括 .h .c .hpp .cpp,其中源檔案(.c .cpp)是基本的編譯單元,頭檔案(.h .hpp)不會被編譯器編譯,
C/C++專案構建(build)程序,分為以下幾個步驟 預處理 → 編譯 → 鏈接,
預編譯
預編譯的程序可以理解為編譯器(實際上是前處理器,這里統稱為編譯器就可以了)在正式編譯之前處理C/C++檔案中的預處理命令,即#開頭的代碼,
常用的幾個預處理命令如下:
#include ......
#ifdef ...... #else......#endif
#define ......
#pragma ......
舉個例子,下面是個很簡單的類定義:
MyClass.h
#define DEFAULT_VALUE 0 class MyClass { public: void Fun(); public: int value =https://www.cnblogs.com/macomfan/p/ 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中就會發生重定義錯誤,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/88003.html
標籤:C++
下一篇:2019.11.11&12題解
