頭文件重複包含和變量重複定義

原文鏈接:https://blog.csdn.net/u014557232/article/details/50354127

---------------------
版權聲明:本文爲CSDN博主「printfnothing」的原創文章,遵循CC 4.0 by-sa版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/u014557232/article/details/50354127

在c或c++中,頭文件重複包含問題是程序員必須避免的問題,也是很多新手容易犯錯的問題。
爲什麼要避免頭文件重複包含呢?
       1.我們知道在編譯c或c++程序時候,編譯器首先要對程序進行預處理,預處理其中一項工作便是將你源程序中#include的頭文件完整的展開,如果你有意或無意的多次包含相同的頭文件,會導致編譯器在後面的編譯步驟多次編譯該頭文件,工程代碼量小還好,工程量一大會使整個項目編譯速度變的緩慢,後期的維護修改變得困難。
       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();}

此時輸入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中變爲:

   

    #include<stdio.h>
    #include "a.h"
    #include "a.h"
     
    void main()
    {
       printf("%d",A);
    }

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

   

    //a.h
     
    #include<stdio.h>
    #ifndef _A_H
    #define _A_H
     
    int A = 1;
     
    #endif;

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

   

    //a.h
     
    #pragma once
    #include<stdio.h>
    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。遇到②時,編譯器直接跳過該語句,執行後面的語句,從而避免重複包含。

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

   

     //a.h
     
    #include<stdio.h>
    #ifndef _A_H
    #define _A_H
     
    int A = 1;
     
    #endif;

   

     //b.h
     
    #include<stdio.h>
    #include "a.h"
    void fb();
      //b.c
     
    #include"b.h"
    void fb()
    {
       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();
    }

然後分別編譯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的地址,產生了二義性,所以程序會出錯。

講了這麼多,那麼到底怎麼樣才能避免重複定義呢?
其實避免重複定義關鍵是要避免重複編譯,防止頭文件重複包含是有效避免重複編譯的方法,但是最好的方法還是記住那句話:頭文件儘量只有聲明,不要有定義。這麼做不僅僅可以減弱文件間的編譯依存關係,減少編譯帶來的時間性能消耗,更重要的是可以防止重複定義現象的發生,防止程序崩潰
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章