1. 程式人生 > >C++——重複編譯與重複定義

C++——重複編譯與重複定義

來源引用:

https://blog.csdn.net/Tsinting/article/details/62232518(非常清楚)

https://www.cnblogs.com/jdxn/p/6970228.html

http://www.cnblogs.com/xuepei/p/4027946.html

https://blog.csdn.net/u014557232/article/details/50354127

https://blog.csdn.net/qq_34809033/article/details/80652116(非常清楚)

 

為什麼要避免重複包含?


       1.在編譯c或c++程式時候,編譯器首先要對程式進行預處理,預處理其中一項工作便是將你源程式中#include的標頭檔案完整的展開,如果你在(同一個.cpp下)有意或無意的多次包含相同的標頭檔案,會導致編譯器在後面的編譯步驟多次編譯該標頭檔案,工程程式碼量小還好,工程量一大會使整個專案編譯速度變的緩慢,後期的維護修改變得困難。
       2.第一點講的標頭檔案重複包含的壞處其實還能忍,畢竟現在計算機運算能力已經不是早些時候那麼慢了。但是標頭檔案重複包含帶來的最大壞處是會使程式在編譯連結的時候崩潰(重複定義),這是我們無法容忍的。

//a.h  
#include<stdio.h>  
int A=1;  
  
  
//b.h  
#include "a.h"  
void f(){printf("%d",A);}  
  
//main.c  
#include<stdio.h>  
#include"a.h"  
#include"b.h"  
void main(){f();} 

 

如何解決?(兩種做法)

方法一:條件編譯:

//條件編譯
//test.h

#ifndef _TEST_H_
#define _TEST_H_                //一般是檔名的大寫
//檔名全大寫,前後加下劃線,並把檔名中的“.”也變成下劃線,
//………………………………
//………………………………
//………………………………
//………………………………
//………………………………

#endif

值得注意的是,#ifndef起到的效果是防止一個原始檔(上例指main.c)兩次包含同一個標頭檔案(上例指a.h),而不是防止兩個原始檔包含同一個標頭檔案。網上很多資料對這一細節的描述都是錯誤的。事實上,防止同一標頭檔案被兩個不同的原始檔包含這種要求本身就是不合理的,標頭檔案存在的價值就是被不同的原始檔包含。也就是說對於A.cpp和B.cpp兩個原始檔都包含了head.h的標頭檔案時,條件編譯不會起到作用(如下例——b.cpp和c.cpp分別包含了a.h,a.h被重複包含甚至定義)。這也就意味著如果標頭檔案中定義了類外的函式或者全域性變數,那麼當多個原始檔同時包含這一標頭檔案時,會發生重複定義的錯誤(如下例 int A =1)。

//a.h  
  
#include<stdio.h>  
#ifndef _A_H  
#define _A_H  
  
int A = 1;  
  
#endif;  
//b.h  
  
#include<stdio.h>  
#include "a.h"  
void f();  
  
//b.c  
  
#include"b.h"  
void f()  
{  
   printf("%d",A+1);  
}  
  
//c.h  
  
#include<stdio.h>  
#include "a.h"  
void fc();  
  
//c.c  
  
#include"c.h"  
void fc()  
{  
   printf("%d",A+2);  
}  
//main.c  
  
#include<stdio.h>  
#include "b.h"  
#include "c.h"  
void main()  
{  
    fb();  
    fc();  
}

條件編譯只防止“(同一個.cpp)重複編譯(導致的重複定義)”,而不是“重複定義
重複編譯可能造成重複定義,但重複定義的來源不只有重複編譯

從程式碼變成可執行的程式,需要兩個步驟
編譯和連結
編譯開始時,將所有#include標頭檔案的地方替換成該標頭檔案的程式碼
在編譯階段,編譯所有原始檔成為模組,各模組中的每個變數與函式都得到了屬於自己的空間
在連結階段,各個模組被組合到一起

上例為什麼會出錯呢?按照條件編譯,a.h並沒有重複包含,可是還是提示變數A重複定義了。
在這裡我們要注意一點,變數,函式,類,結構體的重複定義不僅會發生在源程式編譯的時候,在目標程式連結的時候同樣也有可能發生。我們知道c/c++編譯的基本單元是.c或.cpp檔案,各個基本單元的編譯是相互獨立的,#ifndef等條件編譯只能保證在一個基本單元(單獨的.c或.cpp檔案)中標頭檔案不會被重複編譯,但是無法保證兩個或者更多基本單元中相同的標頭檔案不會被重複編譯,不理解?沒關係,還是拿剛才的例子講:
gcc -c b.c -o b.o :b.c檔案被編譯成b.o檔案,在這個過程中,預處理階段編譯器還是會開啟a.h檔案,定義_A_H並將a.h包含進b.c中。
gcc -c c.c -o c.o:c.c檔案被編譯成c.o檔案,在這個過程中,請注意預處理階段,編譯器依舊開啟a.h檔案,此時的_A_H是否已被定義呢?前面提到不相關的.c檔案之間的編譯是相互獨立的,自然,b.c的編譯不會影響c.c的編譯過程,所以c.c中的_A_H不會受前面b.c中_A_H的影響,也就是c.c的_A_H是未定義的!!於是編譯器再次幹起了相同的活,定義_A_H,包含_A_H。
到此,我們有了b.o和c.o,編譯main.c後有了main.o,再將它們連結起來生成main時出現問題了:
編譯器在編譯.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的地址,產生了二義性,所以程式會出錯。

#ifndef能夠防止在編譯階段,(同一個.cpp中include的)一段程式碼被重複編譯,並且由此可以避免一個變數被重複定義
但它不能防止連結階段,各模組中都有叫某個名字的變數,於是報連結錯誤:變數重複定義

 

子問題:如何解決重複定義?

為了避免重複定義,一般標頭檔案中不會存放定義,只存放函式宣告和變數的宣告。但也有例外,類、inline函式和編譯時值已知的const物件可以在標頭檔案中定義,這是因為遵守“單一定義規則”(One-Definition Rule, ODR)。根據此規則, 如果對同一個類的兩個定義完全相同且出現在不同編譯單位,會被當作同一個定義。當包含類的標頭檔案分別被兩個不同的編譯單位(file1.cpp, file2.cpp)包含,滿足ODR規則,會被當作同一個定義,所以不會有衝突。此外,模板和inline函式也適用此規則。

解決方法即不僅用#ifndef組合防止重複編譯,而且將變數在原始檔中定義,只在標頭檔案裡放extern宣告
這樣各模組在編譯的時候,就知道“有這麼個變數,但它的空間不在我這裡”,連結的時候,這個變數雖然出現在所有包含這個標頭檔案的模組裡,但只有一個模組是它的真身所在

 

方法二:#pragma once

#pragma once這種方式,是微軟編譯器獨有的,也是後來才有的,所以知道的人並不是很多,用的人也不是很多,因為他不支援跨平臺。如果你想寫跨平臺的程式碼,最好使用條件編譯。如果想使用#pragma once,只需在標頭檔案開頭加上#pragma once即可。