DLL動態連接庫是程序複用的重要方式,DLL能夠導出函數,使函數被多個程序複用,DLL中的函數實現能夠被修改而無需從新編譯和鏈接使用該DLL的應用程序。做爲一名面向對象的程序員,但願DLL能夠導出類,以便在類的層次上實現複用。所幸的是,DLL確實也能夠導出類。html
然而事實卻沒這麼簡單,導出類的DLL在維護和修改時有不少地方必需很當心,增長成員變量、修改導出類的基類等操做均可能致使意想不到的後果,也許用戶更新了最新版本的DLL庫後,應用程序就不再能工做了。這就是著名的DLL Hell(DLL地獄)問題。
DLL地獄問題是怎麼產生的呢?看下面的例子,假設DLL有一個導出類ClassD1:程序員
class ClassD { public: int GetInt(); private: int m_i; }; int ClassD::GetInt() { return m_i; }
應用程序使用如今的代碼來使用這個類:安全
ClassD d;
printf(「%d」, d.GetInt());
程序進行正正常,沒有什麼問題。後來DLL須要升級,對ClassD進行了修改,增長了一個成員變量,以下:框架
class ClassD // 修改後 { public: int GetInt(); private: int m_i2; int m_i; };
把新的DLL編譯鏈接完成後,複製到應用程序目錄,這個倒楣的應用程序調用GetInt方法恐怕再也沒法得正確的值了。事實上它還算幸運的,若是GetInt的實現改爲以下這樣,那麼它立刻就要出錯退出了。函數
int ClassD::GetInt() // 修改後 { return m_i++; }
這樣的事情,稱它是個地獄(Hell)一點也不誇張。爲何會出錯呢?咱們要先從類實例的建立開始,看看使用一個類的工做過程。
首先,程序語句「ClassD d;」爲這個類申請一塊內存。這塊內存保存該類的全部成員變量,以及虛函數表。內存的大小由類的聲明決定,在應用程序編譯時就已經肯定。
而後,當調用「d.GetInt()」時,把申請的這一塊內存作爲this指針傳給 GetInt函數,GetInt函數從this指向的位置開始,加上m_i應有的偏移量,計算m_i所在的內存位置,並從該位置取數據返回。m_i相對 this的偏移量是由m_i在類中定義的位置決定的,定義在前的成員變量在內存中也更靠前。這個偏移量在DLL編譯時肯定。
當ClassD的定義改成修改後的狀態時,有些東西變了。
第一個變的是內存的大小。由於修改後的ClassD多了一個成員變量,因此內存也變大了。然而這一點應用程序並不知道。
第二個變的是m_i的偏移地址。由於在m_i以前定義了一個m_i2,m_i的實現偏移地址實際已經靠後了。因此d.GetInt()訪問的將是原來m_i後面的那個位置,而這個位置已經超出原來那塊內存的後部範圍了。
很顯然,在更換了DLL後,應用程序還按原來的大小申請了一塊內存,而它調用的方法卻訪問了比這塊內存更大的區域,出錯再在所不免。
一樣的情形還會發生在如下這些種狀況中:
1) 應用程序直接訪問類的公有變量,而該公有變量在新DLL中定義的位置發生了變化;
2) 應用程序調用類的一個虛函數,而新的類中,該虛函數的前面又增長了一個虛函數;
3) 新類的後面增長了成員變量,而且新類的成員函數將訪問、修改這些變量;
4) 修改了新類的基類,基類的大小發生了變化;
等等,總言而之,一不當心,你的程序就會掉進地獄。
經過對這些引發出錯的狀況進行分析,會發現其實只有三點變化會引發出錯,由於這三點是使用這個DLL的應用程序在編譯時就須要肯定的內容,它們分別是:
1) 類的大小;
2) 類成員的偏移地址;
3) 虛函數的順序。
要想作一個可升級的DLL,必需避免以上三個問題。因此如下三點用來使DLL遠離地獄。
1,不直接生成類的實例。對於類的大小,當咱們定義一個類的實例,或使用new語句生成一個實例時,內存的大小是在編譯時決定的。要使應用程序不依賴於類的大小,只有一個辦法:應用程序不生成類的實例,使用DLL中的函數來生成。把導出類的構造函數定義爲私有的(privated),在導出類中提供靜態(static)成員函數(如NewInstance())用來生成類的實例。由於 NewInstance()函數在新的DLL中會被從新編譯,因此總能返回大小正確的實例內存。
2,不直接訪問成員變量。應用程序直接訪問類的成員變量時會用到該變量的偏移地址。因此避免偏移地址依賴的辦法就是不要直接訪問成員變量。把全部的成員變量的訪問控制都定義爲保護型(protected)以上的級別,併爲須要訪問的成員變量定義Get或Set方法。Get或Set方法在編譯新DLL時會被從新編譯,因此總能訪問到正確的變量位置。
3,忘了虛函數吧,就算有也不要讓應用程序直接訪問它。由於類的構造函數已是私有 (privated)的了,因此應用程序也不會去繼承這個類,也不會實現本身的多態。若是導出類的父類中有虛函數,或設計須要(如類工場之類的框架),必定要把這些函數聲明爲保護的(protected)以上的級別,併爲應用程序從新設計調用該慮函數的成員函數。這一點也相似於對成員變量的處理。
若是導出的類能遵循以上三點,那麼之後對DLL的升級將能夠認爲是安全的。
若是對一個已經存在的導出類的DLL進行維護,一樣也要注意:不要改動全部的成員變量,包括導出類的父類,不管定義的順序仍是數量;不要動全部的虛函數,不管順序仍是數量。
總結起來,實際上是一句話:導出類的DLL不要導出除了函數之外的任何內容。聽起來是否是有點好笑呢!
事實上,建議你在發佈導出類的DLL的時候,從新定義一個類的聲明,這個聲明能夠無論原來的類裏的成員變量之類的,只把接口函數列在類的聲明裏,以下面的例子:post
class ClassInterface { privated: ClassInterface(); public: static ClassInterface * NewInstance(); int GetXXX(); void SetXXX(); void Function(); };
使用該DLL的應用程序用上面的定義做爲ClassInterface的頭文件,便不會有任何可能致使的安全問題。
DLL地獄問是歸根結底是由於DLL當初是做爲函數級共享庫設計的,並不能真正提供一個類所必需的信息。類層上的程序複用只有Java和C#生成的類文件才能作到。ui
參考連接:this