動態鏈接庫(dll)
Windows下有靜態鏈接(lib)庫和動態鏈接庫(dll)兩種共享代碼的方式。
本文將介紹dll的應用場景,以及在vs2019平台下的生成和使用。
今天的筆記內容說的是平時經常能看見的,運行 VS 項目的時候老在下方加載的 .dll 。包括一小部分的理論和超大部分的實操。
[What] dll是什么
動態鏈接庫(Dynamic Link Library)又稱為“應用程序擴展”,在windows系統中,大多數應用程序並非僅有一個可執行文件exe,同時也包含一些相對獨立(模塊化)的dll文件。dll中存放函數代碼實現,exe中存放dll中相應函數代碼的地址,而且dll中的代碼可以被多個exe調用而在內存中僅保留一份拷貝,從而節省了內存空間。
[How] 如何生成dll
步驟<1>:創建新項目

步驟<2>:配置新項目
輸入“項目名稱”,然后選擇工程“位置”,“解決方案名稱”與“項目名稱”相同,是自動生成的,如果沒有特殊需求建議不要修改,不要勾選“將解決方案和項目放在同一目錄中”,最后點擊“創建”按鈕。

步驟<3>:導出DLL
vs官方文檔中提供了兩種方式可以導出dll中的函數:
- 關鍵字
__declspec(dllexport):操作簡單,但通用性較差。可見,vs創建dll項目時默認使用了該方式 - 模塊定義文件(.def):通用性(指給其他語言eg. Java、C#調用)好,但操作相對復雜
使用關鍵字__declspec(dllexport)
(1)首先新創建頭文件“CreateDll.h”,它的作用是用來聲明需要導出的函數接口。
#ifdef MYDLL_EXPORTS #define MYDLL_API __declspec(dllexport) #else #define MYDLL_API __declspec(dllimport) #endif //導出類 class MYDLL_API Rectangle { public: double getarea(double w, double h); void print(); }; //導出函數 extern"C" MYDLL_API int __stdcall mysum(int a, int b);
(2)然后我們需要在‘CreateDll.cpp’中實現在‘CreateDll.h’中被聲明的函數,代碼如下:
#include "pch.h" #include "CreateDll.h" #include<iostream> double Rectangle::getarea(double w, double h) { return w * h; } void Rectangle::print() { std::cout << "已被打印"; } int __stdcall mysum(int a, int b) { return a + b; }
(3)點擊重新生成解決方案,即在debug目錄下生成MyDll.lib和MyDll.dll

代碼分析:
- __declspec(dllexport)此修飾符告訴編譯器和鏈接器被它修飾的函數或變量需要從DLL導出,以供其他應用程序使用;
與其相對的還有一句代碼是__declspec(dllimport),此修飾符的作用是告訴編譯器和鏈接器被它修飾的函數或變量需要從DLL導入
- extern "C"的作用是告訴編譯器將被它修飾的代碼按C語言的方式進行編譯
這是由於C語言沒有重載,不會改變函數名。而C++中有重載,在編譯過程中會根據返回值和參數修改函數名。
- __stdcall定義導出函數入口點調用約定為_stdcall
C編譯器的函數名修飾規則:
- __stdcall調用約定,編譯器和鏈接器會在輸出函數名前加上一個下划線前綴,函數名后面加上一個“@”符號和其參數的字節數,例如 _functionname@number。
- __cdecl調用約定僅在輸出函數名前加上一個下划線前綴,例如_functionname。
- __fastcall調用約定在輸出函數名前加上一個“@”符號,后面也是一個“@”符號和其參數的字節數,例如@functionname@number
模塊定義文件(.def)
(1)新建.def文件

VS會自動添加.def文件為鏈接器輸入:

(2)實現一個dll函數

(3)編寫.def文件如下

[How] 如何調用dll
新建一個控制台應用,在其中調用上述生成的dll。
調用dll有兩種鏈接方式:隱式鏈接和顯式鏈接,無論哪種方式都要求將dll和exe放在同一目錄下。
隱式鏈接
- 隱式鏈接需要三個文件:.h文件、.lib文件 和 .dll文件。
- 對於.h文件: 屬性頁->C/C++->附加包含目錄 添加路徑並引用。(或者直接引用絕對路徑)
- 對於.lib文件(有兩種添加方法)
- 屬性頁->鏈接器->常規->附加庫目錄( 添加.lib文件路徑); 屬性頁->鏈接器->輸入->附加依賴項 (添加.lib文件名)
- 直接用#pragma comment(lib,"MyDll.lib) (需要將該lib文件放到與exe同目錄下)
在配置好文件后編寫代碼,調用dll:
#include"CreateDll.h" #include<iostream> #pragma comment(lib,"MyDll.lib") int main() { Rectangle rect; std::cout << "矩形面積:" << rect.getarea(3, 2)<<std::endl; rect.print(); std::cout << "二數相加" << mysum(3, 2); return 0; }

顯式鏈接
- 顯式鏈接只需要一個文件:.dll文件。
- 所謂顯式鏈接,就是直接調用WIN32 API函數
LoadLibrary、GetProcAddress和FreeLibrary顯式地裝載、卸載dll。
顯式鏈接整體思路:
- 聲明頭文件<windows.h>,說明我想用windows32方法來加載和卸載DLL
- 然后用typedef定義一個指針函數類型(這個指針類型,要和你調用的函數類型和參數保持一致)
- 定義一個句柄實例,用來取DLL的實例地址。(HMODULE hdll;)
- 加載目標DLL,即 LoadLibrary()函數,將DLL加載到句柄實例,若成功則返回該DLL模塊的句柄,否則返回NULL
- 獲得導出函數的地址,即GetProcAddress()函數,成功時返回函數地址,否則返回NULL
- 調用導出函數
- 卸載dll
#include<iostream> #include<Windows.h> typedef int(*Pmysum)(int a, int b);//定義一個指針函數類型 int main() { HMODULE Hdll = LoadLibrary(L"MyDll.dll");//獲取dll地址 if (Hdll!=NULL) { Pmysum mysunm = (Pmysum)GetProcAddress(Hdll, "mysum");//獲取dll中的函數地址 if (mysunm !=NULL) { std::cout << "調用兩變量相加函數:"<<mysunm(3, 2); } } FreeLibrary(Hdll);//卸載dll return 0; }

這也暴露出顯式鏈接的一個弊端:要求開發人員必須清楚地知道調用函數的導出名稱和傳參格式。extern "C"和def文件相當於給函數重命名,如果想調用默認c++方式導出的函數,就要用那一長串修飾后的函數名。
實例:用顯式調用dll中的類
一,創建dll庫

2️⃣在Interface.h
#ifdef INTERFACE_EXPORTS #define INTERFACE_API __declspec(dllexport) #else #define INTERFACE_API __declspec(dllimport) #endif #pragma once class Interface { public: virtual void ShowMsg() = 0; // 將調用方需要調用的成員函數聲明成純虛函數 virtual ~Interface() {};// 抽象類的虛析構函數 }; extern "C" INTERFACE_API Interface * Export(void); //外部接口
3️⃣Interface.cpp( 通過導出外部接口向調用方提供指向派生類Test對象的基類指針)
#include "pch.h" #include "Interface.h" #include"Test.h" // 通過導出函數形式向調用方提供指向派生類對象的基類指針 Interface* Export(void) { return (Interface*)new Test(); }
4️⃣將真正要調用的類Test聲明成抽象類 Interface 的派生類
Test.h
#pragma once #include "Interface.h" #include <string> class Test:public Interface { public: Test(); virtual ~Test(); virtual void ShowMsg(void);//重寫虛函數 private: std::string s; };
Test.cpp
#include "pch.h" #include "Test.h" #include<iostream> Test::Test() { s = "hello form dll"; } Test::~Test() { std::cout << "destroy"; } void Test::ShowMsg() { std::cout << s << std::endl; }
二,顯式調用dll
創建一個空項目testdll,將生成的Mydll.dll和Interface.h放入testdll的目錄下

在testdll項目中新建rundll.cpp。動態調用dll
#include <Windows.h> #include"Interface.h" // 包含抽象類從而使用接口 #include<iostream> using pExport = Interface * (*)(void); // 定義指向導出函數的指針類型 int main() { HINSTANCE hDll = LoadLibrary(L"Mydll.dll");// 加載DLL庫文件,DLL名稱和路徑用自己的 if (hDll !=NULL) { pExport Get = (pExport)GetProcAddress(hDll, "Export");// 將指針指向函數首地址 if (Get == NULL) { std::cout << "load address fail \n"; return -1; } Interface* t = Get();// 調用導出函數獲得抽象類指針 t->ShowMsg();// 通過該指針調用類成員函數 delete t; // 釋放DLL中生成的對象 FreeLibrary(hDll); //釋放庫句柄 } system("pause"); return 0; }

此時需要注意兩點:
- 我們需要把Interface.h放在UseDLL工程目錄下
- 如果編譯時出現:無法將參數 1 從“const char [14]”轉換為“LPCWSTR”的錯誤,則我們需要點擊項目屬性,常規-》字符集-》改為“未設置”即可
實際上整個項目的方法是Interface完成了接口的設置,而具體的實現在test中進行,真正使用了類的抽象性和多態性,封閉性。
