在Visual Studio中使用C++創建和使用DLL


什么是DLL(動態鏈接庫)?

 

   DLL是一個包含可由多個程序同時使用的代碼和數據的庫。例如:在Windows操作系統中,Comdlg32 DLL執行與對話框有關的常見函數。因此,每個程序都可以使用該DLL中包含的功能來實現“打開”對話框。這有助於促進代碼重用和內存的有效使用。這篇文章的目的就是讓你一次性就能了解和掌握DLL。

為什么要使用DLL(動態鏈接庫)?

 

   代碼復用是提高軟件開發效率的重要途徑。一般而言,只要某部分代碼具有通用性,就可以將它構造成相對獨立的功能模塊並在之后的項目中重復使用。比較常見的例子是各種應用程序框架,它們都以源代碼的形式發布。由於這種復用是源代碼級別的,源代碼完全暴露給了程序員,因而稱之為“白盒復用”。白盒復用有以下三個缺點:

  1. 暴露源代碼,多份拷貝,造成存儲浪費;
  2. 容易與程序員的本地代碼發生命名沖突;
  3. 更新模塊功能比較困難,不利於問題的模塊化實現;

    為了彌補這些不足,就提出了“二進制級別”的代碼復用了。使用二進制級別的代碼復用一定程度上隱藏了源代碼,對於“黑盒復用”的途徑不只DLL一種,靜態鏈接庫,甚至更高級的COM組件都是。

使用DLL主要有以下優點:

  1. 使用較少的資源;當多個程序使用同一函數庫時,DLL可以減少在磁盤和物理內存中加載的代碼的重復量。這不僅可以大大影響在前台運行的程序,而且可以大大影響其它在Windows操作系統上運行的程序;
  2. 推廣模塊式體系結構;
  3. 簡化部署與安裝。

創建DLL   

    打開Visual Studio 2012,創建如下圖的工程:


輸入工程名字,單擊[OK];

單擊[Finish],工程創建完畢了。現在,我們就可以在工程中加入我們的代碼了。加入MyCode.h和MyCode.cpp兩個文件;在MyCode.h中輸入以下代碼:

 

[cpp]   view plain   copy 
 
 
 
 
  1. #ifndef _MYCODE_H_  
  2. #define _MYCODE_H_  
  3. #ifdef DLLDEMO1_EXPORTS  
  4. #define EXPORTS_DEMO _declspec( dllexport )  
  5. #else  
  6. #define EXPORTS_DEMO _declspec(dllimport)  
  7. #endif  
  8. extern "C" EXPORTS_DEMO int Add (int a , int b);  
  9. #endif  

 

在MyCode.cpp中輸入以下代碼:

 

[cpp]   view plain   copy 
 
   在CODE上查看代碼片 派生到我的代碼片
 
  1. #include "stdafx.h"  
  2. #include "MyCode.h"  
  3. int Add ( int a , int b )  
  4. {  
  5.        return ( a + b );  
  6. }  
編譯工程,就會生成DLLDemo1.dll文件。在代碼中,很多細節的地方,我稍后進行詳細的講解。

 

 

使用DLL

   當我們的程序需要使用DLL時,就需要去加載DLL,在程序中加載DLL有兩種方法,分別為加載時動態鏈接和運行時動態鏈接。

  1. 在加載時動態鏈接中,應用程序像調用本地函數一樣對導出的DLL函數進行顯示調用。要使用加載時動態鏈接,需要在編譯和鏈接應用程序時提供頭文件和導入庫文件(.lib)。當這樣做的時候,鏈接器將向系統提供加載DLL所需的信息,並在加載時解析導出的DLL函數的位置;
  2. 在運行時動態鏈接中,應用程序調用LoadLibrary函數或LoadLibraryEx函數以在運行時加載DLL。成功加載DLL后,可以使用GetProcAddress函數獲得要調用的導出的DLL函數的地址。在使用運行時動態鏈接時,不需要使用導入庫文件。

在實際編程時有兩種使用DLL的方法,那么到底應該使用那一種呢?在實際開發時,是基於以下幾點進行考慮的:

  1. 啟動性能如果應用程序的初始啟動性能很重要,則應使用運行時動態鏈接;
  2. 易用性在加載時動態鏈接中,導出的DLL函數類似於本地函數,我們可以方便地進行這些函數的調用;
  3. 應用程序邏輯在運行時動態鏈接中,應用程序可以分支,以便按照需要加載不同的模塊。

下面,我將分別使用兩種方法調用DLL動態鏈接庫。

加載時動態鏈接:

[cpp]   view plain   copy 
 
   在CODE上查看代碼片 派生到我的代碼片
 
  1. #include <windows.h>  
  2. #include <iostream>  
  3. //#include "..\\DLLDemo1\\MyCode.h"  
  4. using namespace std;  
  5. #pragma comment(lib, "..\\debug\\DLLDemo1.lib")  
  6. extern "C" _declspec(dllimport) int Add(int a, int b);  
  7. int main(int argc, char *argv[])  
  8. {  
  9.       cout<<Add(2, 3)<<endl;  
  10.       return 0;  
  11. }  
運行時動態鏈接:

 

[cpp]   view plain   copy 
 
   在CODE上查看代碼片 派生到我的代碼片
 
  1. #include <windows.h>  
  2. #include <iostream>  
  3. using namespace std;  
  4. typedef int (*AddFunc)(int a, int b);  
  5. int main(int argc, char *argv[])  
  6. {  
  7.       HMODULE hDll = LoadLibrary("DLLDemo1.dll");  
  8.       if (hDll != NULL)  
  9.       {  
  10.             AddFunc add = (AddFunc)GetProcAddress(hDll, "Add");  
  11.             if (add != NULL)  
  12.             {  
  13.                   cout<<add(2, 3)<<endl;  
  14.             }  
  15.             FreeLibrary(hDll);  
  16.       }  
  17. }  

上述代碼都在DLLDemo1工程中。(工程下載)。

DllMain函數

   Windows在加載DLL時,需要一個入口函數,就像控制台程序需要main函數一樣。有的時候,DLL並沒有提供DllMain函數,應用程序也能成功引用DLL,這是因為Windows在找不到DllMain的時候,系統會從其它運行庫中引入一個不做任何操作的默認DllMain函數版本,並不意味着DLL可以拋棄DllMain函數。

   根據編寫規范,Windows必須查找並執行DLL里的DllMain函數作為加載DLL的依據,它使得DLL得以保留在內存里。這個函數並不屬於導出函數,而是DLL的內部函數,這就說明不能在客戶端直接調用DllMain函數,DllMain函數是自動被調用的。

   DllMain函數在DLL被加載和卸載時被調用,在單個線程啟動和終止時,DllMain函數也被調用。參數ul_reason_for_call指明了調用DllMain的原因,有以下四種情況:

  DLL_PROCESS_ATTACH:當一個DLL被首次載入進程地址空間時,系統會調用該DLL的DllMain函數,傳遞的ul_reason_for_call參數值為DLL_PROCESS_ATTACH。這種情況只有首次映射DLL時才發生;

  DLL_THREAD_ATTACH:該通知告訴所有的DLL執行線程的初始化。當進程創建一個新的線程時,系統會查看進程地址空間中所有的DLL文件映射,之后用DLL_THREAD_ATTACH來調用DLL中的DllMain函數。要注意的是,系統不會為進程的主線程使用值DLL_THREAD_ATTACH來調用DLL中的DllMain函數;

   DLL_PROCESS_DETACH:當DLL從進程的地址空間解除映射時,參數ul_reason_for_call參數值為DLL_PROCESS_DETACH。當DLL處理DLL_PROCESS_DETACH時,DLL應該處理與進程相關的清理操作。如果進程的終結是因為系統中有某個線程調用了TerminateProcess來終結的,那么系統就不會用DLL_PROCESS_DETACH來調用DLL中的DllMain函數來執行進程的清理工作。這樣就會造成數據丟失;

   DLL_THREAD_DETACH:該通知告訴所有的DLL執行線程的清理工作。注意的是如果線程的終結是使用TerminateThread來完成的,那么系統將不會使用值DLL_THREAD_DETACH來執行線程的清理工作,這也就是說可能會造成數據丟失,所以不要使用TerminateThread來終結線程。以上所有講解在工程DLLMainDemo(工程下載)都有體現。

函數導出方式

  在DLL的創建過程中,我使用的是_declspec( dllexport )方式導出函數的,其實還有另一種導出函數的方式,那就是使用導出文件(.def)。你可以在DLL工程中,添加一個Module-Definition File(.def)文件。.def文件為鏈接器提供了有關被鏈接器程序的導出、屬性及其它方面的信息。

對於上面的例子,.def可以是這樣的:

[cpp]   view plain   copy 
 
 
 
 
  1. LIBRARY     "DLLDemo2"  
  2. EXPORTS  
  3. Add @ 1 ;Export the Add function  

 

Module-Definition File(.def)文件的格式如下:

  1. LIBRARY語句說明.def文件對應的DLL;
  2. EXPORTS語句后列出要導出函數的名稱。可以在.def文件中的導出函數名后加@n,表示要導出函數的序號為n(在進行函數調用時,這個序號有一定的作用)。

使用def文件,生成了DLL,客戶端調用代碼如下:

[cpp]   view plain   copy 
 
   在CODE上查看代碼片 派生到我的代碼片
 
  1. #include <windows.h>  
  2. #include <iostream>  
  3. using namespace std;  
  4. typedef int (*AddFunc)(int a, int b);  
  5. int main(int argc, char *argv[])  
  6. {  
  7.       HMODULE hDll = LoadLibrary("DLLDemo2.dll");  
  8.       if (hDll != NULL)  
  9.       {  
  10.             AddFunc add = (AddFunc)GetProcAddress(hDll, MAKEINTRESOURCE(1));  
  11.             if (add != NULL)  
  12.             {  
  13.                   cout<<add(2, 3)<<endl;  
  14.             }  
  15.             FreeLibrary(hDll);  
  16.       }  
  17. }  
可以看到,在調用GetProcAddress函數時,傳入的第二個參數是MAKEINTRESOURCE(1),這里面的1就是def文件中對應函數的序號。( 工程下載

extern “C”

為什么要使用extern “C”呢?C++之父在設計C++時,考慮到當時已經存在了大量的C代碼,為了支持原來的C代碼和已經寫好的C庫,需要在C++中盡可能的支持C,而extern “C”就是其中的一個策略。在聲明函數時,注意到我也使用了extern “C”,這里要詳細的說說extern “C”。

extern “C”包含兩層含義,首先是它修飾的目標是”extern”的;其次,被它修飾的目標才是”C”的。先來說說extern;在C/C++中,extern用來表明函數和變量作用范圍(可見性)的關鍵字,這個關鍵字告訴編譯器,它申明的函數和變量可以在本模塊或其它模塊中使用。extern的作用總結起來就是以下幾點:

  1. 在一個文件內,如果外部變量不在文件的開頭定義,其有效范圍只限定在從定義開始到文件的結束處。如果在定義前需要引用該變量,則要在引用之前用關鍵字”extern”對該變量做”外部變量聲明”,表示該變量是一個已經定義的外部變量。有了這個聲明,就可以從聲明處起合理地使用該變量了,例如:
[cpp]   view plain   copy 
 
   在CODE上查看代碼片 派生到我的代碼片
 
  1. /* 
  2. ** FileName     : Extern Demo 
  3. ** Author       : Jelly Young 
  4. ** Date         : 2013/11/18 
  5. ** Description  : More information, please go to http://www.jellythink.com 
  6. */  
  7. #include <iostream>  
  8. using namespace std;  
  9. int main(int argc, char *argv[])  
  10. {  
  11.       extern int a;  
  12.       cout<<a<<endl;  
  13. }  
  14. int a = 100;  
  1. 在多文件的程序中,如果多個文件都要使用同一個外部變量,不能在各個文件中各定義一個外部變量,否則會出現“重復定義”的錯誤。正確的做法是在任意一個文件中定義外部變量,其它文件用extern對變量做“外部變量聲明”。在編譯和鏈接時,系統會知道該變量是一個已經在別處定義的外部變量,並把另一文件中外部變量的作用域擴展到本文件,這樣在本文件就可以合法地使用該外部變量了。寫過MFC程序的人都知道,在在CXXXApp類的頭文件中,就使用extern聲明了一個該類的變量,而該變量的實際定義是在CXXXApp類的實現文件中完成的;
  2. 外部函數,在定義函數時,如果在最左端加關鍵字extern,表示此函數是外部函數。C語言規定,如果在定義時省略extern,則隱含為外部函數。而內部函數必須在前面加static關鍵字。在需要調用此函數的文件中,用extern對函數作聲明,表明該函數是在其它文件中定義的外部函數。

   接着說”C”的含義。我們都知道C++通過函數參數的不同類型支持重載機制,編譯器根據參數為每個重載函數產生不同的內部標識符;但是,如果遇到了C++程序要調用已經被編譯后的C函數,那該怎么辦呢?比如上面的int Add ( int a , int b )函數。該函數被C編譯器后在庫中的名字為_Add,而C++編譯器則會生成像_Add_int_int之類的名字用來支持函數重載和類型安全。由於編譯后的名字不同,C++程序不能直接調用C函數,所以C++提供了一個C連接交換指定符號extern “C”來解決這個問題;所以,在上面的DLL中,Add函數的聲明格式為:extern “C” EXPORTS_DEMO int Add (int a , int b)。這樣就告訴了C++編譯器,函數Add是個C連接的函數,應該到庫中找名字_Add,而不是找_Add_int_int。當我們將上面DLL中的”C”去掉,編譯生成新的DLL,使用Dependency Walker工具查看該DLL,如圖:


請注意導出方式為C++,而且導出的Add函數的名字添加了很多的東西,當使用這種方式導出時,客戶端調用時,代碼就是下面這樣:
 
         
[cpp]   view plain   copy 
 
   在CODE上查看代碼片 派生到我的代碼片
 
  1. #include <windows.h>  
  2. #include <iostream>  
  3. using namespace std;  
  4. typedef int (*AddFunc)(int a, int b);  
  5. int main(int argc, char *argv[])  
  6. {  
  7.      HMODULE hDll = LoadLibrary("DLLDemo1.dll");  
  8.      if (hDll != NULL)  
  9.      {  
  10.           AddFunc add = (AddFunc)GetProcAddress(hDll, "?Add@@YAHHH@Z");  
  11.           if (add != NULL)  
  12.           {  
  13.                cout<<add(2, 3)<<endl;  
  14.           }  
  15.           FreeLibrary(hDll);  
  16.      }  
  17. }  

請注意GetProcAddress函數的第二個參數,該參數名就是導出的函數名,在編碼時,寫這樣一個名字是不是很奇怪啊。當我們使用extern “C”方式導出時,截圖如下:

 

注意導出方式為C,而且函數名現在就是普通的Add了。我們再使用GetProcAddress時,就可以直接指定Add了,而不用再加那一長串奇怪的名字了。

 

DLL導出變量

DLL定義的全局變量可以被調用進程訪問;DLL也可以訪問調用進程的全局數據。

 

 

DLL導出類

 

DLL中定義的類,也可以被導出。詳細工程代碼,請參見(工程下載

總結

對DLL的講解就到此結束,由於MFC在現在的環境下使用較少,此處不予講解,如果以后做項目遇到了MFC的DLL相關知識,我再做總結。最后,希望大家給我的博客提出一些中肯的建議。

本文版權歸 果凍說所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,否則保留追究法律責任的權利。如果這篇文章對你有幫助,你可以 請我喝杯咖啡。» 本文鏈接: http://www.jellythink.com/archives/111» 訂閱本站: http://www.jellythink.com/feed
» 轉載請注明來源: 果凍想 » <a rel="bookmark" title="在Visual Studio中使用C++創建和使用DLL" 《在Visual Studio中使用C++創建和使用DLL》

******************************************************************************************************

參考上面的內容,自己在VS2010開發環境上測試了一遍,測試步驟如下:

1.將所需要的函數封裝成DLL.

   首先創建DLL工程項目,命名為DllDemo,如下圖:      
然后創建頭文件(MyCode.h)和.cpp文件(MyCode.cpp),並分別添加代碼:
MyCode.h頭文件:
[cpp]   view plain   copy 
 
 
 
 
  1. #ifndef _MYCODE_H_  
  2. #define _MYCODE_H_  
  3. #ifdef DLLDEMO1_EXPORTS  
  4. #define EXPORTS_DEMO _declspec( dllexport )  
  5. #else  
  6. #define EXPORTS_DEMO _declspec(dllimport)  
  7. #endif  
  8. extern "C" EXPORTS_DEMO int Add (int a , int b);  
  9. #endif  
MyCode.cpp文件:
[cpp]   view plain   copy 
 
 
 
 
  1. #include "MyCode.h"  
  2. int Add ( int a , int b )  
  3. {  
  4.        return ( a + b );  
  5. }  
編譯工程,就會在Debug文件下生成DllDemo.dll文件。

2.加載時動態鏈接方式調用DLL.

     首先創建控制台應用程序,命名為DllTest,如下圖所示:
``
 然后添加代碼:
[cpp]   view plain   copy 
 
 
 
 
  1. // DllTest.cpp : 定義控制台應用程序的入口點。  
  2. #include "stdafx.h"  
  3. #include <iostream>  
  4. //#include "..\\DLLDemo1\\MyCode.h"  
  5. using namespace std;  
  6. #pragma comment(lib, "..\\debug\\DllDemo.lib")         //***********************************************************************問題1  
  7. extern "C" _declspec(dllimport) int Add(int a, int b);  
  8. int _tmain(int argc, _TCHAR* argv[])  
  9. {  
  10.       cout<<Add(2, 3)<<endl;  
  11.       while(1);//程序運行到這,方便看運行結果  
  12.           return 0;  
  13. }  
運行結果如下圖:
注意:導入庫文件的目錄必須在本工程的目錄下,也就是說要把生成的dll和lib文件都要拷貝到該工程的目錄下,因為不再該目錄下,盡管修改了路徑,仍然提示找不到DllDemo.dll,不知道為什么?

3.運行時動態鏈接方式調用DLL.

  和第二步一樣,創建控制台應用程序,命名為DllTest1,添加代碼如下:
[cpp]   view plain   copy 
 
 
 
 
  1. // DllTest1.cpp : 定義控制台應用程序的入口點。  
  2. //  
  3. #include "stdafx.h"  
  4. #include <iostream>  
  5. #include <windows.h>  
  6. using namespace std;  
  7. typedef int (*AddFunc)(int a, int b);  
  8.   
  9. int _tmain(int argc, _TCHAR* argv[])  
  10. {  
  11.   
  12.      HMODULE hDll = LoadLibrary(_T("DllDemo.dll"));  
  13.       if (hDll != NULL)  
  14.       {  
  15.             AddFunc add = (AddFunc)GetProcAddress(hDll, "Add");  
  16.             if (add != NULL)  
  17.             {  
  18.                   cout<<add(2, 3)<<endl;  
  19.             }  
  20.             FreeLibrary(hDll);  
  21.       }  
  22.       while(1);  
  23. }  
 
          
 運行結果如下圖: 
         
 

4.以.def文件(模塊定義文件)方式導出函數(非_declspec(dllexport)方式導出函數):

 首先創建DLL工程項目,命名為DllDemo,如下圖:   
然后創建頭文件(MyCode.h)和.cpp文件(MyCode.cpp),並分別添加代碼:
MyCode.h頭文件:
[cpp]   view plain   copy 
 
 
 
 
  1. #ifndef _MYCODE_H_  
  2. #define _MYCODE_H_  
  3. extern "C" int Add (int a , int b);      
  4. #endif  
MyCode.cpp文件:
[cpp]   view plain   copy 
 
 
 
 
  1. #include "MyCode.h"  
  2. int Add ( int a , int b )  
  3. {  
  4.        return ( a + b );  
  5. }  
然后添加模塊定義文件(.def文件):


添加代碼:
[cpp]   view plain   copy 
 
 
 
 
  1. LIBRARY  "DllDemo"   //這里的字符串名和工程名要一致  
  2. EXPORTS  
  3. Add @1;Export the Add function  
編譯工程,即刻生成DllDemo.dll文件。
使用def文件,生成了DLL,客戶端調用代碼如下:
[cpp]   view plain   copy 
 
 
 
 
  1. // DllTest2.cpp : 定義控制台應用程序的入口點。  
  2. //  
  3. #include "stdafx.h"  
  4. #include <windows.h>  
  5. #include <iostream>  
  6. using namespace std;  
  7. typedef int (*AddFunc)(int a, int b);  
  8. int _tmain(int argc, _TCHAR* argv[])  
  9. {  
  10.       HMODULE hDll = LoadLibrary("DllDemo.dll");  
  11.       if (hDll != NULL)  
  12.       {  
  13.             AddFunc add = (AddFunc)GetProcAddress(hDll, MAKEINTRESOURCE(1));  
  14.             if (add != NULL)  
  15.             {  
  16.                   cout<<add(2, 3)<<endl;  
  17.             }  
  18.             FreeLibrary(hDll);  
  19.       }  
  20.       while(1);  
  21. }  
工程代碼下載:

遇到的問題:

1.庫導入的時候目錄的問題。對應文中的問題1,后面有解釋。
2.字符集的問題(是Unicode字符集還是多字節集),兩種方案,一種修改字符集為多字節集,二是將字符串前面加  _T(""),文中問題2
3.不知道怎么通過模塊定義文件方式生成DLL,通過看參考博客的代碼找到了答案,主要修改頭文件,和添加模塊定義文件。
4.模塊定義文件中的庫文件名應和工程名一致。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM