讀完需要明白的問題:
(1) 如何避免程序報重復定義錯誤?
(2)在頭文件中可以定義些什么?
(3)什么是內部鏈接與外部鏈接,為什么不在頭文件中定義具有外部鏈接的實體?
(4) 為什么類的定義放在.h文件中。而類的實現放在同名的cpp文件中?
在c或c++中,頭文件重復包含問題是程序員必須避免的問題,也是很容易犯錯的問題 .
(1)為什么要避免頭文件重復包含呢?
1.在編譯c或c++程序時候,編譯器首先要對程序進行預處理,預處理其中一項工作便是將源程序中 #include的頭文件完整的展開,如果多次包含相同的頭文件,會導致編譯器在后面的編譯步驟多次編譯該頭文件,工程代碼量小還好,工程量一大會使整個項目編譯速度變的緩慢,后期的維護修改變得困難。
2.第一點講的頭文件重復包含的壞處其實還能忍,但是頭文件重復包含帶來的最大壞處是會使程序在編譯鏈接的時候崩潰
1 //a.h 2 #include<stdio.h> 3 int A=1; 4 5 6 //b.h 7 #include "a.h" 8 void f(){printf("%d",A);} 9 10 //main.c 11 #include<stdio.h> 12 #include"a.h" 13 #include"b.h" 14 void main(){f();}
此時輸入gcc -c main.c進行編譯,會提示A重復定義,程序崩潰:
然后輸入gcc -E main.c -o main.i看下預處理內容:
可以看到6015行和6021行重復出現int A=1;的定義,違背了一次定義的原則,所以會出錯。
(2)那么如何避免它呢?
通常有兩種做法:條件編譯和#pragma once
1. 使用宏定義避免重復引入
其中,_NAME_H 是宏的名稱。需要注意的是,這里設置的宏名必須是獨一無二的,不要和項目中其他宏的名稱相同。
當程序中第一次 #include 該文件時,由於 _NAME_H 尚未定義,所以會定義 _NAME_H 並執行“頭文件內容”部分的代碼;當發生多次 #include 時,因為前面已經定義了 _NAME_H,所以不會再重復執行“頭文件內容”部分的代碼
2. 使用#pragma once避免重復引入
除了前面第一種最常用的方式之外,還可以使用 #pragma one 指令,將其附加到指定文件的最開頭位置,則該文件就只會被 #include 一次
注意:#pragma once 只能作用於某個具體的文件,而無法向 #ifndef 那樣僅作用於指定的一段代碼
3. 兩種方式得區別
#ifndef 是通過定義獨一無二的宏來避免重復引入的,這意味着每次引入頭文件都要進行識別,所以效率不高。但考慮到 C 和 C++ 都支持宏定義,所以項目中使用 #ifndef 規避可能出現的“頭文件重復引入”問題,不會影響項目的可移植性。
和 ifndef 相比,#pragma once 不涉及宏定義,當編譯器遇到它時就會立刻知道當前文件只引入一次,所以效率很高。但值得一提的是,並不是每個版本的編譯器都能識別 #pragma once 指令,一些較老版本的編譯器就不支持該指令(執行時會發出警告,但編譯會繼續進行),即 #pragma once 指令的兼容性不是很好。
(3)避免頭文件的重復包含是否一定可以避免變量、函數、類、結構體的重復定義?
答案當然是否!
繼續上面的例子:
1 //a.h 2 #include<stdio.h> 3 #ifndef _A_H 4 #define _A_H 5 6 int A = 1; 7 8 #endif;
//b.h #include<stdio.h> #include "a.h" void f(); //b.c #include"b.h" void f() { printf("%d",A+1); }
1 //c.h 2 3 #include<stdio.h> 4 #include "a.h" 5 void fc(); 6 7 //c.c 8 9 #include"c.h" 10 void fc() 11 { 12 printf("%d",A+2); 13 }
//main.c #include<stdio.h> #include "b.h" #include "c.h" void main() { fb(); fc(); }
然后分別編譯gcc -c b.c -o b.o和gcc -c main.c -o main.o,並未提示任何錯誤。
但是當生成可執行文件時候gcc b.o main.o -o main,編譯器提示出錯:
為什么會出錯呢?按照條件編譯,a.h並沒有重復包含,可是還是提示變量A重復定義了。
在這里我們要注意一點,變量,函數,類,結構體的重復定義不僅會發生在源程序編譯的時候,在目標程序鏈接的時候同樣也有可能發生。我們知道c/c++編譯的基本單元是.c或.cpp文件,各個基本單元的編譯是相互獨立的,#ifndef等條件編譯的作用域僅在單個文件中,因此只能保證在一個基本單元(單獨的.c或.cpp文件)中頭文件不會被重復編譯,但是無法保證兩個或者更多基本單元中相同的頭文件不會被重復編譯
【出錯本質:編譯器在編譯.c或.cpp文件時,有個很重要的步驟,就是給這些文件中含有的已經定義了的變量分配內存空間,在a.h中A就是已經定義的變量,由於b.c和c.c獨立,所以A相當於定義了兩次,分配了兩個不同的內存空間。在main.o鏈接b.o和c.o的時候,由於main函數調用了fb和fc函數,這兩個函數又調用了A這個變量,對於main函數來說,A變量應該是唯一的,應該有唯一的內存空間,但是fb和fc中的A被分配了不同的內存,內存地址也就不同,main函數無法判斷那個才是A的地址,產生了二義性,所以程序會出錯】
那么到底怎么樣才能避免重復定義呢?
其實避免重復定義關鍵是要避免重復編譯,防止頭文件重復包含是有效避免重復編譯的方法,但是最好的方法是記住:頭文件盡量只有聲明,不要有定義
(4)聲明與定義
“聲明”:只是聲明某個符號(變量或函數)的存在,即告訴編譯器,這個符號是在其他文件中定義的,我這里先用着,你鏈接的時候再到別的地方去找找看它到底是什么吧。
“定義”:則是要按C++語法完整地定義一個符號(變量或者函數),告訴編譯器在此處分配存儲空間建立變量和函數。
頭文件的作用:就是被其他的.cpp包含進去的, 本身並不參與編譯。但實際上,它們的內容卻在多個.cpp文件中得到了編譯。通過"定義只能有一次”的規則,很容易可以得出:頭文件中應該只放變量和函數的聲明,而不能放它們的定義。因為一個頭文件的內容實際上是會被引 入到多個不同的.cpp文件中的,並且它們都會被編譯。放聲明當然沒事,如果放了定義,那么也就相當於在多個.cpp文件中出現了對同一個符號(變量或函數)的定義,因此就會報“重復定義的錯誤”。
總結:聲明是將一個名稱引入程序;定義提供了一個實體(類型、變量、對象、函數)在程序中的唯一描述
所以:一個符號,在整個程序中可以被聲明多次,但只允許被定義一次
大多數情況下,聲明與定義是相同的,但是有少些情況下,聲明並非定義。
聲明:
1 #ifndef _DEMO_H_ 2 #define _DEMO_H_ 3 4 void declaration(int a,int b); //聲明一個全局函數; 5 extern int number; //聲明一個全局變量; 6 class test{ 7 ...; 8 static int a; //類內聲明一個靜態類數據成員; 9 void func(int ,int); //類內聲明一個成員函數 10 int b; //類內聲明一個普通數據成員 11 ...}; 12 13 class A; //類的聲明; 14 tepedef int INT; //typedef聲明;
15 class B{ friend func(class *B); //類內友元函數聲明 16 } 17 #endif
定義:
1) 定義了一個靜態類數據成員; int test::a = 4 或者 static int a=4;
2) 定義了一個non-inline成員函數; void declaration(int a,int b){return (a<b?a:b)}
(5)C/C++編譯過程
編譯一個.cpp文件時, 經過處理、編譯、匯編和鏈接 4 個步驟,生成一個可執行程序:
gcc編譯過程
1. 預處理:將(include)的文件插入原文件中、將宏定義展開、根據條件編譯命令選擇要使用的代碼
2. 編譯: 將預處理得到的源代碼文件,進行“翻譯轉換”, 生成匯編代碼(編譯階段要檢查代碼的規范性、是否有語法錯誤,如函數、變量是否被聲明等)
3. 匯編: 將匯編代碼翻譯成了機器碼,表現為二進制文件
4. 鏈接: 將匯編生成的.o文件及其他函數庫文件鏈接起來,生成能在特定平台上運行的可執行程序(在鏈接程序時,鏈接器會在所有的目標文件中找尋函數的實現。如果找不到,那到就會報鏈接錯誤碼(LinkerError))
(6)內部鏈接與外部鏈接
1. 內部鏈接:內部鏈接意味着對符號名的訪問僅限於當前編譯單元。即:對於任何其他編譯單元都是不可見的,在鏈接的時候不會與其它編譯單元中同樣的名稱相沖突,則這個符號具有內部鏈接。
具體有:
1)靜態(static)全局變量的定義、靜態自由函數的定義、靜態友元函數的定義
2)類的聲明與定義
3)內聯函數定義
4)Union共同體/結構體/枚舉類型定義
5)const常量定義
6)各種聲明
C++又補充規定,extern const聯合修飾時,extern將壓制const的內部鏈接屬性。
舉例:
1 static int x; //靜態全局變量定義
2 static void func(){...};//靜態自由函數定義 3 //靜態友元函數函數定義 4 class A{...}; //類定義
class A; //類聲明 5 inline void func(){...};//內聯函數定義 6 Union AA{...}; //Union共同體定義 7 const int y; //const常量定義 8 enum Boolean{No, Yes}; //枚舉類型定義
9 extern int z; //全局變量聲明
用內部鏈接定義的一個重要的例子就是類的定義。類的定義如下。因此,它不能夠在同一作用域的編譯單元內重復定義。如果需要在其他編譯單元使用,類必須被定義在頭文件且被其他文件包含。僅僅在其他文件中使用class Point;聲明是不行的,原因就是類的定義是內部鏈接,不會在目標文件導出符號。也就不會被其他單元解析它們的未定義符號
class Point{ int d_x; //內部鏈接 int d_y; public: Point(int x,int y):d_x(x),d_y(y){} //內部鏈接 int x() const{return d_x;} //內部鏈接 int y() const{return d_y;} //內部鏈接 };
因此:具有內部鏈接的符號無法作用於當前文件外部,要讓其影響程序的其他部分,可以將其放在.h文件中。此時在所有包含此.h文件的源文件都有自己的定義且互不影響。
2. 外部鏈接:外部鏈接意味着這個定義不局限於單個的編譯單元。在.o文件中,具有外部鏈接的定義產生外部符號,這些外部符號可以被所有其他編譯單元訪問,用來解析其他編譯單元中未定義的符號,即:一個名稱在鏈接時可以和其他編譯單元交互,那么這個名稱就具有外部鏈接。
因此:因此它們在整個程序中必須是唯一的,否則將會導致重復定義
具體有:
1.類的非內聯函數(包括成員函數和靜態成員函數)的定義
2.類的靜態成員變量的定義
3.名字空間或全局非靜態的自由函數,非靜態變量,非友元函數的定義
舉例:
class A { static int a; //類的靜態成員聲明,內部鏈接
void fun(); //類的非內聯成員函數聲明,內部鏈接
static void fun2(); //類的非內聯靜態成員函數聲明,內部鏈接
void fun2(){...}; //類內實現函數定義,若為內聯則為內部鏈接,若為非內聯則為外部鏈接
} int A::a = 1; //類的靜態成員定義,外部鏈接
void A::fun(){...}; //類的非內聯成員函數定義,外部鏈接
static void A::fun2(){...}; //類的非內聯靜態成員函數定義,外部鏈接
namespace A{...} //名字空間定義,外部鏈接
void fun3(){...}; //全局非靜態自由函數定義,外部鏈接
int b; //全局非靜態變量,外部鏈接
有一些名字定義所表示的實體擁有外部鏈接,這樣就意味着他可以跨越編譯單元去進行代碼的鏈接。
所以,擁有外部鏈接的實體如果放在頭文件中並且被多個.cpp文件包含,可能就會出現鏈接沖突錯誤,因為每個包含這個擁有外部鏈接實體的.cpp都會分配空間,當多個編譯單元鏈接的時候,連接器就會面對多個相同的名字,無法正常鏈接到正確的對象。
因此:由於cpp文件中存儲的是成員函數的實現,而成員函數具有外部鏈接特性,會在目標文件產生符號。在此文件中此符號是定義過的。其他調用此成員函數的目標文件也會產生一個未定的符號。兩目標文件連接后此符號就被解析
判斷一個符號是內部鏈接還是外部鏈接的一個很好的方法就是看該符號是否被寫入.o文件,由於聲明只對當前編譯單元有用,因此聲明並不將任何東西寫入.o文件
(7)總結:頭文件中應該寫什么
1 //test.h 2 #ifndef TEST_H 3 #define TEST_H 4 int a; //外部鏈接,不能在頭文件中定義(是聲明也是定義) 5 extern int b=10; //外部鏈接,不能在頭文件中定義(是定義)
extern int bb; //聲明,內部鏈接,可以定在頭文件中(是聲明)
6 const int c=2; //內部鏈接,可以定在頭文件中但應該避免 7 static int d=3; //內部鏈接,可以定在頭文件中但應該避免 8 static void func(){} //同上 9 void func2(){} //同a 10 void func3(); //可以,僅僅是聲明。並不會導致符號名被寫入目標文件。 11 class A //可以,類定義,內部鏈接 12 { 13 public: 14 static int e; //可以,聲明,內部鏈接 15 int f; //同上 16 void func4(); //同上 17 }; 18 int A::e=10; //不可以,外部鏈接,符號名會寫入目標文件 19 void A:func4()//不可以,外部鏈接 20 { 21 //...... 22 } 23 #endif
聲明本身不會影響到.o文件的內容,在源文件中每一個聲明都只是命名一個外部符號,使當前的編譯單元在需要的時候可以訪問相應的外部定義
函數調用會導致一個未定義的符號被寫入到.o文件。如果func在該文件中沒有被使用,那么不會被寫入到.o文件。而有對此函數的調用,就會將此符號寫入目標文件,此后該.o文件與定義此符號的.o文件被連接在一起,前面未定義的符號被解析。
宏是內部鏈接還是外部鏈接
答:都不是,宏在預處理環節時就被替換掉了,而內部鏈接與外部鏈接是針對編譯環節與鏈接環節而言的
(8)總結
非模板類型 | 模板類型 | |
.h | 全局變量聲明(帶extern字符) 全局自由函數聲明 內聯函數聲明與定義(inline) static/const變量聲明與定義(聲明可以,定義不建議) typedef的聲明 namespace的定義 |
帶inline限定符的全局模板函數的聲明與定義 |
類的定義與聲明 類屬性與方法的聲明(類內) 類內函數定義(相當與inline) 帶static/const限定符的數據成員初始化(類內) 帶inline限定符的類成員函數定義(類外) |
模板類的定義 模板類成員的聲明與定義(定義可以放在類內或類外,類外不需要寫inline) |
|
.cpp | 全局變量定義及初始化 全局自由函數的定義 |
無 |
類成員函數的定義 帶static類成員屬性的初始化 |
最后再給出一個C++編程建議,慎重考慮在頭文件中定義有外部鏈接的實體:
1. 如果頭文件是像int a=1;這樣的定義,被包含在多個.cpp文件后肯定會報出鏈接錯誤。
2. 如果是static int a = 2;這樣的定義就會在所有包含他的.cpp文件中生成一個副本,如果被大量源文件include的話,就會占據大量的空間,造成內存浪費。
總之:頭文件盡量只有聲明,不要有定義
參考:
https://blog.csdn.net/iteye_21199/article/details/82438044?utm_medium=distribute.pc_relevant.none-task-blog-OPENSEARCH-4.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-4.control
附:
(1)在.h中和.cpp中include頭文件有什么區別
1 在 .h 里面 include 的好處是: 2 如果很多.c,.cpp文件,都包含一批頭文件, 3 如果復制很容易遺漏 4 如果輸入,很容易出錯 5 6 如果全部在一個.h, include 那么每個.c,.cpp文件只需要一個#include 語句 7 這樣不僅輸入量減少, 8 而且代碼也美觀多了 9 代碼也主次分明了 10 畢竟,.c.cpp, 里面 11 要實現的函數,才是主要代碼 12 13 2)主要缺陷, 14 可能會包含完全不需要的頭文件, 15 增加編譯工作量
例子:
(1)如果你在a.h頭文件中include了“stdio.h”,“iostream”,……一大堆
那么你的a.cpp源文件只要include你的a.h,就相當於include了“stdio.h”,“iostream”,……一大堆
但是當其他文件include你的a.h的同時也就包含了“stdio.h”,“iostream”,……一大堆
如果a.cpp包含了頭文件a.h,a.h包含了頭文件b.h,b.c也包含了b.h,那么當b.h發生改變時,a.c和b.c都會重新編譯,增加編譯工作量
所以:
1. 如果你需要讓其他文件也include一大堆,那么寫在a.h中就可以;
2. 如果只有a.cpp需要include一大堆,那么建議在a.cpp中include一大堆
3. 有時只需要前置聲明就可以滿足使用變量或者函數的需求,建議不要包含頭文件
(2)頭文件中可 以寫內聯函數(inline)的定義
內聯函數之所有具有內部鏈接,因為編譯器在可能的時候,會將所有對函數的調用替換為函數體,不將任何符號寫入.o文件
因為inline函數是需要編譯器在遇到它的地方根據它的定義把它內聯展開的,而並非是普通函數那樣可以先聲明再鏈 接的(內聯函數不會鏈接),所以編譯器就需要在編譯時看到內聯函數的完整定義才行。如果內聯函數像普通函數一樣只能定義一次的話,這事兒就難辦了。因為在 一個文件中還好,我可以把內聯函數的定義寫在最開始,這樣可以保證后面使用的時候都可以見到定義;但是,如果我在其他的文件中還使用到了這個函數那怎么辦 呢?這幾乎沒什么太好的解決辦法,因此C++規定,內聯函數可以在程序中定義多次,只要內聯函數在一個.cpp文件中只出現一次,並且在所有的.cpp文 件中,這個內聯函數的定義是一樣的,就能通過編譯。那么顯然,把內聯函數的定義放進一個頭文件中是非常明智的做法。
(3)頭文件中可以寫類 (class)的定義
因為在程序中創建一個類的對象時,編譯器只有在這個類的定義完全可見的情況下,才能知道這個類的對象應該如何布局,所以,關於類的 定義的要求,跟內聯函數是基本一樣的。所以把類的定義放進頭文件,在使用到這個類的.cpp文件中去包含這個頭文件,是一個很好的做法。在這里,值得一提 的是,類的定義中包含着數據成員和函數成員。數據成員是要等到具體的對象被創建時才會被定義(分配空間),但函數成員卻是需要在一開始就被定義的,這也就 是我們通常所說的類的實現。一般,我們的做法是,把類的定義放在頭文件中,而把函數成員的實現代碼放在一個.cpp文件中。這是可以的,也是很好的辦法。 不過,還有另一種辦法。那就是直接把函數成員的實現代碼也寫進類定義里面。在C++的類中,如果函數成員在類的定義體中被定義,那么編譯器會視這個函數為 內聯的。因此,把函數成員的定義寫進類定義體,一起放進頭文件中,是合法的。注意一下,如果把函數成員的定義寫在類定義的頭文件中,而沒有寫進類定義中, 這是不合法的,因為這個函數成員此時就不是內聯的了。一旦頭文件被兩個或兩個以上的.cpp文件包含,這個函數成員就被重定義了