1. 程式人生 > >標頭檔案重複包含和變數重複定義

標頭檔案重複包含和變數重複定義

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

來看個例子:

  1. //a.h

  2. #include<stdio.h>

  3. int A=1;

  4. //b.h

  5. #include "a.h"

  6. void f(){printf("%d",A);}

  7. //main.c

  8. #include<stdio.h>

  9. #include"a.h"

  10. #include"b.h"

  11. void main(){f();}

此時輸入gcc -c main.c進行編譯,會提示A重複定義,程式崩潰:

然後輸入gcc -E main.c -o main.i看下預處理內容:

可以看到6015行和6021行重複出現int A=1;的定義,違背了一次定義的原則,所以會出錯。

瞭解了標頭檔案重複包含的壞處,那麼如何避免它呢?
通常有兩種做法:條件編譯和#pragma once
條件編譯就是通常的
#ifndef _XXX
#define _XXX
...
#endif
具體怎麼用,可以google。這裡介紹下#pragma once:#pragma once這種方式,是微軟編譯器獨有的,也是後來才有的,所以知道的人並不是很多,用的人也不是很多,因為他不支援跨平臺。如果你想寫跨平臺的程式碼,最好使用條件編譯。如果想使用#pragma once,只需在標頭檔案開頭加上#pragma once即可。
兩者的聯絡與區別:


 聯絡是都可以避免標頭檔案重複包含,區別主要在於兩者避免標頭檔案重複包含的實現方式上。
再看上面的例子:
暫不考慮b.h,將main.c中變為:

  1. #include<stdio.h>

  2. #include "a.h"

  3. #include "a.h"

  4. void main()

  5. {

  6. printf("%d",A);

  7. }

1.在a.h中加入條件編譯:

  1. //a.h

  2. #include<stdio.h>

  3. #ifndef _A_H

  4. #define _A_H

  5. int A = 1;

  6. #endif;

2.在a.h 中加入#pragma once:

  1. //a.h

  2. #pragma once

  3. #include<stdio.h>

  4. int A = 1;

考慮情況1條件編譯:

編譯main.c時,預處理階段遇到①,編譯器開啟a.h,發現_A_H未定義,於是將 #define到#endif之間的內容包含進main.c;當遇到②時,編譯器再次開啟a.h,發現_A_H已經定義,於是直接關閉a.h,a.h沒有再次包含進main.c,從而避免了重複包含。
考慮情況2#pragma once:
預處理階段遇到①時,開啟a.h,將#pragma once後面的內容包含進main.c中,關閉a.h。遇到②時,編譯器直接跳過該語句,執行後面的語句,從而避免重複包含。

講完了檔案的重複包含,讓我們來思考一個問題:如前所說,避免標頭檔案的重複包含可以有效地避免變數的重複定義,其實不光是變數的重複定義,也可以避免函式和類、結構體的重複定義。但是
避免標頭檔案的重複包含是否一定可以避免變數、函式、類、結構體的重複定義?
答案當然是否!
讓我們再看上面的例子:

  1. //a.h

  2. #include<stdio.h>

  3. #ifndef _A_H

  4. #define _A_H

  5. int A = 1;

  6. #endif;

  1. //b.h

  2. #include<stdio.h>

  3. #include "a.h"

  4. void f();

  5. //b.c

  6. #include"b.h"

  7. void f()

  8. {

  9. printf("%d",A+1);

  10. }

  11. //c.h

  12. #include<stdio.h>

  13. #include "a.h"

  14. void fc();

  15. //c.c

  16. #include"c.h"

  17. void fc()

  18. {

  19. printf("%d",A+2);

  20. }

  1. //main.c

  2. #include<stdio.h>

  3. #include "b.h"

  4. #include "c.h"

  5. void main()

  6. {

  7. fb();

  8. fc();

  9. }

然後分別編譯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檔案)中標頭檔案不會被重複編譯,但是無法保證兩個或者更多基本單元中相同的標頭檔案不會被重複編譯,不理解?沒關係,還是拿剛才的例子講:
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的地址,產生了二義性,所以程式會出錯。
(怎麼處理處理這個問題呢?弄 一個a.c,在a.c裡面做定義(int i = 3;),a.h裡面做宣告(extern int i;))
講了這麼多,那麼到底怎麼樣才能避免重複定義呢?
其實避免重複定義關鍵是要避免重複編譯,防止標頭檔案重複包含是有效避免重複編譯的方法,但是最好的方法還是記住那句話:標頭檔案儘量只有宣告,不要有定義。這麼做不僅僅可以減弱檔案間的編譯依存關係,減少編譯帶來的時間效能消耗,更重要的是可以防止重複定義現象的發生,防止程式崩潰。