爲什麼有些庫的頭文件只提供類的預先聲明,而無類的定義

1.問題的引出

在程序設計領域,庫可以說是一切程序的基礎。當今的程序幾乎沒有一個是從0實現的,或多或少都是建立在已有實現功能模塊的基礎上,這些可以被程序員使用具有一定功能的代碼模塊就叫做庫。

庫的使用方式可粗略分爲兩種,一是源碼級別的使用,二是二進制級別使用。

對於第一種,庫的源碼對於使用者完全開放,用戶不僅可以使用,而且可以理解庫的實現原理,甚至可以修改庫來擴充功能,總之,源碼之前,了無祕密。這種方式的缺點是對於庫開發廠商,很難保守自己的知識產權,更別說商業祕密;再就是每次編譯程序都需要重新編譯庫這需要更多的編譯連接時間;最後由於人人可以修改,容易導致版本混亂,不便於維繫穩定的接口。目前開放源代碼的庫也有不少,如Qt,GlibC,GTK等等,但大多數不是讓用戶直接使用源代碼,而是提供二進制庫的同時附帶源代碼供用戶參考。

對於第二種,庫只對用戶開放有限的說明,目的是讓用戶可以使用庫,但卻不知道庫的具體實現。這種庫的使用方式,典型的就是提供給用戶頭文件和.lib二進制文件。頭文件通常包含數據結構聲明或定義和函數的原型聲明以及常量宏定義,而庫文件則是實際的數據結構和函數功能實現。(注:對於動態鏈接的庫,只是把鏈接放到了運行階段,原理類似)。這種方式下,用戶只知道有限的使用信息,對於庫的實現對用戶是不可見的。這就起到了隱藏實現的作用,對於保護庫廠商的知識產權有着良好的支持。畢竟通過逆向工程來分析庫的實現是很難的事情。

本文關注的就是能通過第二種使用方式使用的庫的實現原理,重點是如何對用戶隱藏更多的信息,而不影響其使用。

2.需要隱藏的信息

庫對於用戶來說就是(包含數據結構定義和函數聲明的)頭文件 +(帶導出符號的)二進制實現文件。

所以發生隱藏的地方也就只有兩處:頭文件,二進制文件。

對於需要隱藏的內容主要是:數據結構的定義,函數的名稱(或聲明),函數實現。

所謂的隱藏,指的是源代碼中符號名,數據結構和算法實現的隱藏。

(1)對於函數的實現。都已經編譯成二進制,所以其源代碼已經自動隱藏了,在庫中再也找不到實現這些運算的源代碼的痕跡了。在頭文件中更不可能出現函數的實現代碼。

(2)對於函數的名字與原型。二進制庫中不存在原型的聲明,只有在導出表中存在函數名符號的可能。頭文件中可能有函數的聲明。這裏有個依賴的問題。那就是二進制文件導出符號表中存在函數的名稱是頭文件存在函數原型聲明的前提。正常做法是導出表中存在的函數名中只有一部分在頭文件中進行了聲明,未被聲明的部分,主要用於庫廠商自己開發使用,用戶無法直接使用。這在WindowsAPI庫中普遍存在,微軟也因此遭到衆多人的譴責,因爲微軟可以使用比其他人更多的庫函數。Vega的庫也是一樣,儘管庫的導出符號表中包含了大量的函數,包括C++成員函數,C函數,然而其提供給用戶的頭文件中並沒有包含某些C函數的聲明和大部分C++類的定義(注意:C++成員函數不能單獨聲明,必須通過整個類的定義聲明)。因此,用戶只能使用有限的C函數進行開發,而不能使用C++的接口進行開發。VegaPrime顯然顯得更加開放,它給用戶提供了大量的C++類接口使用,更加暴露了其庫的數據結構類型。

如果頭文件中聲明瞭函數,而庫導出表中沒有響應的符號,則此函數根本無法使用,因爲在連接時會發生找不到函數的錯誤。

(3)對於數據結構的定義。數據結構在C中主要指結構體,共同體等,在C++中主要是類。是否把這些數據結構的定義開放給用戶,取決於庫廠商。如果允許用戶直接對數據結構進行操作,比如爲其成員賦值,調用其成員函數,則必須把其定義開放給用戶。因爲編譯器在進行此類操作的編譯時,必須要知道數據結構的內存佈局,而內存佈局只有通過其定義得知。這方面典型的庫是MFC,MFC允許用戶直接使用其類建立對象,並調用其成員函數。因此其提供給用戶的頭文件中存在大量的類的定義。

然而如果不允許用戶直接對數據結構進行操作,而是提供一組對數據結構進行操作的函數,就可以對用戶隱藏數據結構的定義。在提供給用戶的頭文件中不需要包含數據結構的定義,只需要提供預先聲明就行,如: class ClibClass; 或 struct STUDENT; 在操作這些數據結構的函數的參數中,不能出現數據結構的類型,因爲如果出現的話,編譯器需要根據數據結構的內存佈局進行壓棧操作,取而代之,應該使用數據結構的指針類型作爲參數類型。因爲編譯器對指針參數在壓棧操作時,無論什麼類型的指針都具有相同的字節數(如4字節)。至於指針類型的解釋,都放在函數實現中完成了。原則上可以在函數聲明中聲明指針參數時,可以以任何類型。因爲在函數實現中對它進行類型轉換就行了。但是對於C++卻不行,原因是C++編譯後的函數名字與參數類型有關,如果函數聲明和函數定義時使用不同類型的指針類型,那麼兩者編譯後的函數名是不相同的,所以也就在鏈接時找不到函數名。對於C語言,編譯後的函數名與參數類型無關,因此函數聲明中的參數類型可以與實現時參數類型不一致。也就是說在預先聲明結構體時,可以採取任何名字,如頭文件:

struct SanyName;

extern FuntionLib(struct SanyName * p);

而實現文件:

Struct Student

{

       Int age;

       Double score;

}

FuntionLib(struct Student *p) //指針類型自動轉換

{

       p->age = 20;

       p->score = 88.5;

}

這樣做不僅可以隱藏數據結構的實現,而且連數據結構的真實名字也隱藏了。當然這樣做沒什麼必要。

3.vega頭文件爲啥只有class vgWindow;而無vgWindow的定義?

相信使用過vega庫的讀者,都有過類似的疑惑:vg.h頭文件中只有類似class vgWindow;的預先類聲明,卻不見vgWindow類的定義。讀者也就不知道vgWindow的成員有什麼。相信現在你已經明白了,因爲使用vega庫根部不需要(或者不想)讓用戶知道類的內部結構,由於類是C++裏的概念,所以這裏預先聲明的類的名稱應該與vega庫中的一致,這裏class vgWindow;的唯一作用也就是使得編譯器編譯函數時使得函數的修飾名字與庫的導出符號一致。

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