天天看點

C++ 頭檔案的包含

 作為初學者,經常會對對c/c++頭檔案的包含很糾結。這兩天和實驗室的小夥伴們花了點時間研究了一下,在這裡做個總結。

1) 首先,頭檔案不是必不可少的,可以用直接寫聲明來取代。

對于小規模項目,隻需要一個源檔案,所有聲明和定義都放在這一個源檔案裡面即可,是以不需要頭檔案。但是要遵循一個原則,即先聲明後調用。是以應把所有聲明(如對全局變量和函數的聲明)都放在主函數之前。但是函數的定義允許放在主函數之後。

對于含有兩個或兩個以上源檔案的項目,也不一定需要頭檔案。例如有一個項目,其中含有兩個源檔案,a.cpp和m.cpp。a.cpp裡定義了一個函數void f(), 而m.cpp需要調用該函數,這時應該怎麼辦呢?其實很簡單,還是遵循先聲明後調用的原則,隻要在m.cpp主函數前加入一個聲明void f();即可(這裡要注意函數的聲明是預設extern的,如果是變量聲明,必須在聲明前加上extern字樣,以告之定義在另一檔案處)。編譯器會先編譯成a.obj和m.obj兩個目标檔案,然後連接配接器根據m.obj中的聲明接口去a.cpp找定義,連接配接生成可執行檔案。是以說,頭檔案是可以完全不要的。如果非要用頭檔案(假設命名為a.h),其實就是把m.cpp裡的聲明void f();轉移到該頭檔案中,然後在m.cpp裡面添加#inlcude "a.h"。這個效果和不用頭檔案是一模一樣的,即達到在m.cpp裡先聲明後調用的目的。

2)其次,頭檔案的存在,一是為了少打字,二是為了封裝。

一,少打字。如上所述頭檔案的作用和聲明的功能是一樣的。但是我們可以把經常用到的聲明放在一個頭檔案裡,做成一個聲明的集合,友善一次性調用多個聲明。這些聲明的定義可以位于不同的源檔案,比如有些聲明的定義位于标準庫,而有些聲明的定義位于自己寫的源檔案。從這裡也可以看出頭檔案的命名理論上是任意的,它可以和任意源檔案都不重名(不包含擴充名)。并非有一個頭檔案就必須有一個對應的源檔案。并且如果某個聲明的函數或變量并未被真正調用,是不要求存在對應的定義的。

二,實作封裝。我們經常會把某些具有共性的函數定義放在同一個源檔案裡(如果是面向對象程式設計,就可以把這些函數做成一個類),并且該源檔案具有一般性,會被其他幾個不同的源檔案同時調用,或者在以後的人生中可能還會被再次調用。此時我們就可以把該源檔案裡的所有函數聲明封裝到一個頭檔案裡,這樣就不用每次調用的時候都去檢視一下源檔案裡定義了哪些函數,不用每次都寫一遍聲明。這時頭檔案的命名習慣上和源檔案重名,便于了解,但是不做強求。

以下兩種情況,頭檔案是沒有價值的。1,頭檔案裡隻聲明了一個函數。此時寫一遍聲明和寫一遍包含頭檔案所做的功差不多,那麼也就沒有必要再弄個頭檔案了。2,如果一個源檔案不具有任何一般性,這輩子估計就隻在一個源檔案裡調用一次,以後打死也不用了,那麼頭檔案對于該源檔案也是沒有價值的,因為把聲明放在頭檔案裡和直接放在調用該源檔案的另一源檔案裡所做的功是一樣的,都是一輩子隻寫一遍。當然,人還是給自己留條後路比較好,别把自己往死裡逼,還是建議都使用頭檔案。

3)如果用了頭檔案就要注意避免重複定義。聲明可以重複,但是定義不可以。

對于變量,什麼是聲明什麼是定義,c和c++的标準不同。對于c,隻要不指派,就認為是聲明,是以一個檔案裡面同一作用域可以出現多個int i,但是隻能有一個int i=5。但是對于c++,int i既是聲明也是定義,是以隻能出現一次int i,隻有加了extern字樣的才是單純的聲明。

重複定義有兩種情況,一種是同一源檔案内的重定義,另一種是不同源檔案之間的重定義。前者會在編譯階段報錯,後者會在連接配接階段報錯。這裡插播一段編譯器和連接配接器的工作過程,雖然前面已經提過。首先編譯器會把所有cpp源檔案編譯成一個個對應的.obj目标檔案(是以cpp擴充名不可任意改動),如果發現某一源檔案裡同一辨別符(同一作用域内)出現了兩次或多次定義(同一源檔案内的重定義),則宣告編譯失敗。如果每個源檔案都不存在重定義,則編譯成功。但編譯成功并不代表連接配接成功。接着連接配接器把所有obj檔案連接配接成一個可執行exe檔案,如果發現某兩個obj檔案裡各自對同一辨別符進行了一次定義(不同源檔案之間的重定義),則宣告連接配接失敗,反之則成功。

那麼在包含頭檔案時怎麼避免這兩種重定義呢?對于第一種情況,如果一個頭檔案被同一個源檔案包含了兩次或兩次以上就會有重定義的風險。例如,b.h包含了a.h,m.cpp既包含了a.h又包含了b.h,這就等于m.cpp包含了a.h兩次,那麼頭檔案a.h裡如果出現了定義,會導緻m.cpp源檔案内部的重定義,無法編譯成m.obj檔案。這種同一源檔案内的重定義有三種方式避免,一是直接去掉b.h裡包含a.h的指令,這樣m.cpp便隻包含了a.h一次;二是把a.h裡的定義移到cpp檔案裡,隻留聲明;三是加入條件編譯語句(如#ifndef, #define, #endif, 或者#pragma once),強制隻編譯一次。對于第二種情況(不同源檔案之間的重定義),來看一個例子,a.cpp包含了a.h,m.cpp也包含了a.h,即a.cpp和m.cpp包含了同一個頭檔案,這裡不存在同一源檔案内部的重定義,是以編譯可以通過,得到兩個目标檔案a.obj和m.obj。但是如果a.h裡有定義,會導緻在連接配接階段發現a.obj和m.obj裡出現重定義,連接配接失敗。解決方法是把a.h裡的定義移到cpp裡去,隻留聲明。

這裡有三個特殊情況要注意,一是常量的聲明和定義,二是内聯函數的聲明和定義,三是類的聲明和定義。

一,常量的聲明和定義。常量的聲明也是以extern開頭,但不能指派,如extern const double pi。常量的定義是const double pi=3.14。同一源檔案裡不允許出現兩次定義,否則編譯失敗,避免方法如上所述。但是不同源檔案之間允許重定義,這和一般的變量很不同。c++标準之是以如此規定,其中一個原因是為了友善數組的定義。假如我們把數組的大小作為參數放在頭檔案裡,那麼該參數必須是const,而且頭檔案裡必須有該參數的定義(指派)。如果頭檔案裡隻放聲明,而把定義放在另一源檔案裡,那麼在編譯階段會找不到數組的大小,無法通過編譯。是以,頭檔案裡必須允許常量定義的存在。作為妥協,還得允許不同源檔案都能包含該頭檔案。

二,内聯函數的聲明和定義。内聯函數可以在頭檔案裡定義,并且不導緻重定義。這是因為内聯函數的内容在編譯時會在源程式中原地擴充,而非在運作時調用,也就是說在目标檔案裡内聯函數其實并不存在,是以也就無所謂重定義了。

三,類的聲明和定義。類是一種資料類型(抽象資料類型),它和函數一樣,不調用(确切的說是聲明類對象)就不會占據記憶體(隻占代碼區記憶體,不占其他三區記憶體)。習慣上會把類的聲明和定義都放在頭檔案裡(這是必須的),而把成員函數的定義(實作)放在源檔案裡(但是允許在類定義裡直接定義小規模成員函數,并将其預設為内聯函數)。是以對類頭檔案進行包含時也要注意避免同一源檔案内的重定義。對小規模項目,可以盡量隻包含一次,對于大項目,特别是多人合作項目,就要加入條件編譯語句,避免他人重複包含導緻的重定義。但是,和常量一樣,允許不同源檔案都能對同一聲明(并定義)類的頭檔案進行包含,并不導緻重定義。這也是因為在編譯階段,編譯器必須知道類的定義,否則無法編譯。關于類的聲明有一個有趣的現象請參考[2]。

如有了解錯誤的地方,歡迎指正。

參考資料:

[1] C++程式設計教程,錢能主編

[2] http://blog.csdn.net/eclipser1987/article/details/7516968

 

c++

繼續閱讀