函數實現不放在標頭檔的原因,及何時可以放標頭檔的情況

來源:互聯網
上載者:User

原文:http://hi.baidu.com/pope123/blog/item/344407d5512953d450da4b6c.html

1 、引子

這篇文章的題目念起來比較怪,不好意思,我是參照《愛在西元前》這樣的句式構造來的,希望讀者能喜歡。原本計劃寫成《標頭檔裡的類實現》,可是想了想還有函數,如果寫成《標頭檔裡的類和函數的實現》,又太長,所以就這樣了。

在平常的 C/C++ 開發中,幾乎所有的人都已經習慣了把類和函數分離放置,一個 .h 的標頭檔裡放聲明,對應的 .c 或者 .cpp 中放實現。從開始接觸,到熟練使用,幾乎已經形成了下意識的流程。在 Symbian OS 下編程,則更是如此,再小的類也會分成兩個檔案,幾乎沒有人想去改變。

儘管這樣的做法無可厚非,而且在不少情況下是相對合理甚至必須的,但我還是要給大家介紹一下把實現全部放置到標頭檔中的方式,給出可供大家使用的另一個選擇。同時針對這一做法,也順便說一下其優缺點以及需要注意的情況。

我是一個很喜歡簡潔的人,多年以來甚至養成了這樣的癖好,如果一個功能是能夠用一條語句實現的,那就不要用兩條語句。在我看來,如果給別人提供一份可以複用的代碼的話,最優雅的狀態莫過於僅僅提供一個標頭檔就全部搞定。

之所以不太喜歡引入源檔案,最重要的原因是源檔案往往會帶來工程檔案的變化;而且,在使用過程中也會增加一些額外的操作,例如,在一個組織良好的工程裡,標頭檔和源檔案很有可能是位於不同的目錄,這樣就會多帶來一次檔案複製操作。

2 、本文

2.1 顧慮

我遇到有不少人不使用標頭檔來包含實現,往往是出於以下幾種顧慮:

1、              暴露了實現細節

2、              標頭檔被包含到不同的源檔案中,會導致連結衝突

3、              標頭檔被包含到不同的源檔案中,會導致有多份實現被編譯出來,增大可執行體的體積

如果有顧慮 1 ,那很顯然應該在第一時間拋棄完全在標頭檔中實現的念頭。不過我遇到的情形裡,通常後兩種顧慮佔據了絕對的比例。而這種顧慮,通常是由於對 C/C++ 沒有足夠的瞭解導致的。

有顧慮 2 的,經常會是一些有 C 語言開發經驗的程式員。他們所擔心的也往往是出現的全域函數的情況。例如有以下標頭檔 c_function.h (清晰起見,防衛宏之類的代碼沒有列出):

int integer_add(const int a, const int b)

{

         return a + b;

}

如果在同一工程中,有 a.c (或者是 .cpp )和 b.c 兩個(或兩個以上)源檔案包含了此標頭檔,則在連結時期就會發生衝突,因為在兩個源檔案編譯得到的目標檔案中都有一份 integer_add 的函數實現,導致連結器不知道對於調用了此函數的調用者,應該使用哪一個副本。

2.2 著手

解決的辦法有兩個,各自為兩個關鍵字,一個是 inline ,另一個是 static 。使用這兩個關鍵字的任意一個來修飾 integer_add 函數,都會消除上述的衝突問題,然而本質卻大不相同。

如果使用 inline ,則意味著編譯器會在調用此函數的地方把函數的目標代碼直接插入,而不是放置一個真正的函數調用,實際作用就是這個函數事實上已經不再存在,而是像宏一樣被就地展開了。使用 inline 的副作用,首先在於毋庸置疑地,代碼的體積變大了;其次則是,這個關鍵字嚴格算起來並不是 C 語言的關鍵字,使用它多少會帶來一些移植性方面的風險,儘管主流的 C 語言編譯器都可以支援 inline 。對於 GCC , inline 功能關鍵字就是 inline 本身,而對於微軟的編譯器,應該是 __inline (注意有兩個前置底線)。

而且,根據慣例, inline 通常都是對編譯器的某種暗示而非強制要求,編譯器有權力在你不知情的情況下把它實現為非 inline 的狀態(可能的原因有,函數太大或者複雜度過高)。這樣的後果是什麼,不好意思,我沒有測試過。

如果是使用 static ,那麼至少結果是可預料的。所有包含此標頭檔的源檔案中都會存在此函數的一份副本。雖然代碼也有一定程度的膨脹,但好就好在互相不衝突,因為 static 關鍵字保證了該函數的可見度為單個源檔案之內

以上的討論雖然看起來主要聚焦在 C 語言上,但由於 C++ 是 C 語言的超集,並且在這些方面並沒有做太多的修改,因此討論結果同樣也適用於 C++ 。

2.3 繼續

對於 C 語言來講,上面的改進幾乎已經走到了盡頭,沒有繼續發展的餘地。然而對於 C++ 則不同,我們還可以進一步把它做得更漂亮。

首先,我們做以下的改動:

class Integer

{

public:

         int add(int a, int b)

         {

                   return a + b;

         }

};

這樣的形式,幾乎連 C++ 的初學者都能看出來,確實不會再發生連結衝突的問題了。不過也有一個問題,我們如果要計算兩個整數的和的話,需要這樣寫:

Integer op;

op.add(i, j);

而這顯然不是一種可接受的狀態,之前很簡單的一條函數語句的調用,現在卻必須定義一個類的對象執行個體。於是我們再次求助於 static ( inline 是不適用的,因為它不能去掉定義對象執行個體這一步,而且事實上,把實現寫到類定義之內的函數預設就是 inline 的)。現在,類就像這個樣子:

class Integer

{

public:

         static int add(int a, int b)

         {

                   return a + b;

         }

};

調用方式也相應地簡化為:

Integer::add(i, j);

尤其需要注意的就是這裡, C++ 類中的 static 函數和全域 static 函數的行為是有差異的,它編譯之後僅產生一份實現代碼,並不會由於被多個源檔案包含而產生多份副本 

這距離我們的終極目標已經不遠了(我們的終極目標是: add(i, j) 就可以搞定)。於是我們再次高舉起宏這杆大旗,在標頭檔裡添加以下定義:

#define integer_add         Integer::add

(後註:突然想到,似乎定義 const 函數指標也可以達到相同的目的)

上面解決的其實僅僅是 C++ 中全域函數的標頭檔複用問題,那麼類呢?類的情況要複雜一些。如果是 static 方法,那麼正好是和上述我們對全域函數的變通實現是一致的;如果是 inline 的方法(不管有沒有 inline 關鍵字),則其狀態幾乎理論上等同於前面所述的 inline 全域函數的情況。那麼還有最後的一種情況, virtual函數。對於 virtual 函數,我們等到的是一個好訊息:它總是產生一份代碼(甚至你顯式使用 inline 關鍵字修飾) 。這裡面有個玄機: virtual 函數的地址會被寫到類的 v-table 裡,是要能夠在運行期被調用的(其核心在於,其調用者以及調用時機在編譯時間是不明確的),所以絕對不能產生為全部就地展開的形式。以此可以做一個推論:所有會被求址的成員函數,都會產生一份函數實體,而不能單純地去符合內聯的修飾關鍵字。

3 、後記

當然,把實現全部放在標頭檔中並不是萬金油,不是放之四海而皆準的準則,正如本文開頭所說,這僅僅是一種選擇,只不過你之前沒有想到過可以這麼做,而現在知道了。它最適合的場合是一些規模較小的工具類的實現。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.