程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 函數實現不放在頭文件的原因,及何時可以放頭文件的情況

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

編輯:C++入門知識

1 、引子
       在平常的 C/C++ 開發中,幾乎所有的人都已經習慣了把類和函數分離放置,一個 .h 的頭文件裡放聲明,對應的 .c 或者 .cpp 中放實現。從開始接觸,到熟練使用,幾乎已經形成了下意識的流程。盡管這樣的做法無可厚非,而且在不少情況下是相對合理甚至必須的,但我還是要給大家介紹一下把實現全部放置到頭文件中的方式,給出可供大家使用的另一個選擇。同時針對這一做法,也順便說一下其優缺點以及需要注意的情況。
       我是一個很喜歡簡潔的人,多年以來甚至養成了這樣的癖好,如果一個功能是能夠用一條語句實現的,那就不要用兩條語句。在我看來,如果給別人提供一份可以復用的代碼的話,最優雅的狀態莫過於僅僅提供一個頭文件就全部搞定。之所以不太喜歡引入源文件,最重要的原因是源文件往往會帶來工程文件的變化;而且,在使用過程中也會增加一些額外的操作,例如,在一個組織良好的工程裡,頭文件和源文件很有可能是位於不同的目錄,這樣就會多帶來一次文件復制操作。

2 、正文
     2.1 顧慮
         我遇到有不少人不使用頭文件來包含實現,往往是出於以下幾種顧慮:
         1、 暴露了實現細節
         2、 頭文件被包含到不同的源文件中,會導致鏈接沖突
         3、 頭文件被包含到不同的源文件中,會導致有多份實現被編譯出來,增大可執行體的體積
       如果有顧慮 1 ,那很顯然應該在第一時間拋棄完全在頭文件中實現的念頭。不過我遇到的情形裡,通常後兩種顧慮占據了絕對的比例。而這種顧慮,通常是由於對 C/C++ 沒有足夠的了解導致的。
      有顧慮 2 的,經常會是一些有 C 語言開發經驗的程序員。他們所擔心的也往往是出現的全局函數的情況。例如有以下頭文件 c_function.h (清晰起見,防衛宏之類的代碼沒有列出):
[cpp]
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++ 則不同,我們還可以進一步把它做得更漂亮。
首先,我們做以下的改動:
[cpp] 
class Integer 

public: 
         int add(int a, int b) 
         { 
                   return a + b; 
         } 
}; 
       這樣的形式,幾乎連 C++ 的初學者都能看出來,確實不會再發生鏈接沖突的問題了。不過也有一個問題,我們如果要計算兩個整數的和的話,需要這樣寫:
       Integer op;
       op.add(i, j);
       而這顯然不是一種可接受的狀態,之前很簡單的一條函數語句的調用,現在卻必須定義一個類的對象實例。於是我們再次求助於 static ( inline 是不適用的,因為它不能去掉定義對象實例這一步,而且事實上,把實現寫到類定義之內的函數缺省就是 inline 的)。現在,類就像這個樣子:
[cpp]
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 、後記
      當然,把實現全部放在頭文件中並不是萬金油,不是放之四海而皆准的准則,正如本文開頭所說,這僅僅是一種選擇,只不過你之前沒有想到過可以這麼做,而現在知道了。它最適合的場合是一些規模較小的工具類的實現。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved