在Visual Studio中用C++語言建立DLL動態連結程式庫圖文教程_C 語言

來源:互聯網
上載者:User

什麼是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中輸入以下代碼:

複製代碼 代碼如下:

#ifndef _MYCODE_H_
#define _MYCODE_H_
#ifdef DLLDEMO1_EXPORTS
#define EXPORTS_DEMO _declspec( dllexport )
#else
#define EXPORTS_DEMO _declspec(dllimport)
#endif
extern "C" EXPORTS_DEMO int Add (int a , int b);
#endif

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

複製代碼 代碼如下:

#include "stdafx.h"
#include "MyCode.h"
int Add ( int a , int b )
{
       return ( a + b );
}

編譯工程,就會產生DLLDemo1.dll檔案。在代碼中,很多細節的地方,我稍後進行詳細的講解(工程下載)。

使用DLL

當我們的程式需要使用DLL時,就需要去載入DLL,在程式中載入DLL有兩種方法,分別為載入時動態連結和運行時動態連結。

1.在載入時動態連結中,應用程式像調用本地函數一樣對匯出的DLL函數進行顯示調用。要使用載入時動態連結,需要在編譯和連結應用程式時提供標頭檔和匯入庫檔案(.lib)。當這樣做的時候,連結器將向系統提供載入DLL所需的資訊,並在載入時解析匯出的DLL函數的位置;

2.在運行時動態連結中,應用程式調用LoadLibrary函數或LoadLibraryEx函數以在運行時載入DLL。成功載入DLL後,可以使用GetProcAddress函數獲得要調用的匯出的DLL函數的地址。在使用運行時動態連結時,不需要使用匯入庫檔案。

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

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

下面,我將分別使用兩種方法調用DLL動態連結程式庫。

載入時動態連結:

複製代碼 代碼如下:

#include <windows.h>
#include <iostream>
//#include "..\\DLLDemo1\\MyCode.h"
using namespace std;
#pragma comment(lib, "..\\debug\\DLLDemo1.lib")
extern "C" _declspec(dllimport) int Add(int a, int b);
int main(int argc, char *argv[])
{
      cout<<Add(2, 3)<<endl;
      return 0;
}

運行時動態連結:

複製代碼 代碼如下:

#include <windows.h>
#include <iostream>
using namespace std;
typedef int (*AddFunc)(int a, int b);
int main(int argc, char *argv[])
{
      HMODULE hDll = LoadLibrary("DLLDemo1.dll");
      if (hDll != NULL)
      {
            AddFunc add = (AddFunc)GetProcAddress(hDll, "Add");
            if (add != NULL)
            {
                  cout<<add(2, 3)<<endl;
            }
            FreeLibrary(hDll);
      }
}

上述代碼都在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可以是這樣的:

複製代碼 代碼如下:

LIBRARY     "DLLDemo2"
EXPORTS
Add @ 1 ;Export the Add function

Module-Definition File(.def)檔案的格式如下:

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

使用def檔案,產生了DLL,用戶端調用代碼如下:

複製代碼 代碼如下:

#include <windows.h>
#include <iostream>
using namespace std;
typedef int (*AddFunc)(int a, int b);
int main(int argc, char *argv[])
{
      HMODULE hDll = LoadLibrary("DLLDemo2.dll");
      if (hDll != NULL)
      {
            AddFunc add = (AddFunc)GetProcAddress(hDll, MAKEINTRESOURCE(1));
            if (add != NULL)
            {
                  cout<<add(2, 3)<<endl;
            }
            FreeLibrary(hDll);
      }
}

可以看到,在調用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”對該變數做”外部變數聲明”,表示該變數是一個已經定義的外部變數。有了這個聲明,就可以從聲明處起合理地使用該變數了,例如:

複製代碼 代碼如下:

/*
** FileName     : Extern Demo
** Author       : Jelly Young
** Date         : 2013/11/18
** Description  : More information
*/
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
      extern int a;
      cout<<a<<endl;
}
int a = 100;

2.在多檔案的程式中,如果多個檔案都要使用同一個外部變數,不能在各個檔案中各定義一個外部變數,否則會出現“重複定義”的錯誤。正確的做法是在任意一個檔案中定義外部變數,其它檔案用extern對變數做“外部變數聲明”。在編譯和連結時,系統會知道該變數是一個已經在別處定義的外部變數,並把另一檔案中外部變數的範圍擴充到本檔案,這樣在本檔案就可以合法地使用該外部變數了。寫過MFC程式的人都知道,在在CXXXApp類的標頭檔中,就使用extern聲明了一個該類的變數,而該變數的實際定義是在CXXXApp類的實現檔案中完成的;

3.外部函數,在定義函數時,如果在最左端加關鍵字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函數的名字添加了很多的東西,當使用這種方式匯出時,用戶端調用時,代碼就是下面這樣:

複製代碼 代碼如下:

#include <windows.h>
#include <iostream>
using namespace std;
typedef int (*AddFunc)(int a, int b);
int main(int argc, char *argv[])
{
     HMODULE hDll = LoadLibrary("DLLDemo1.dll");
     if (hDll != NULL)
     {
          AddFunc add = (AddFunc)GetProcAddress(hDll, "?Add@@YAHHH@Z");
          if (add != NULL)
          {
               cout<<add(2, 3)<<endl;
          }
          FreeLibrary(hDll);
     }
}

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

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

DLL匯出變數

DLL定義的全域變數可以被調用進程訪問;DLL也可以訪問調用進程的全域資料。(工程下載)

DLL匯出類

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

總結

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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.