涵蓋:導出函數/變量/類、.def 文件、動態/靜態加載、__stdcall 約定、DllMain 生命週期
New Project → Win32 Dynamic-Link Library → 名稱 SimpleDLL → Application Settings → DLL, Empty Project。
__declspec(dllexport)(推薦,直觀)
// SimpleDLL.h
#ifndef SIMPLEDLL_H
#define SIMPLEDLL_H
#ifdef SIMPLEDLL_EXPORTS
#define DLL_API __declspec(dllexport)
#else
#define DLL_API __declspec(dllimport)
#endif
extern "C" DLL_API int Add(int a, int b); // C 鏈接 + 導出/導入宏
#endif
// SimpleDLL.cpp
#include "SimpleDLL.h"
int Add(int a, int b) { return a + b; }
專案屬性 → C/C++ → Preprocessor → Preprocessor Definitions 添加 SIMPLEDLL_EXPORTS。
.def(無裝飾名,精確控制序號)
; SimpleDLL.def
LIBRARY "SimpleDLL"
EXPORTS
Add @1 ; 序號 1
Sub @2 NONAME ; 僅按序號導出,無名稱
; 變量導出
g_Counter DATA
屬性 → Linker → Input → Module Definition File 填 SimpleDLL.def。
#include
#include
typedef int (__stdcall *PFN_ADD)(int, int); // 必須匹配調用約定
int main() {
HMODULE hDll = LoadLibrary(_T("SimpleDLL.dll"));
if (!hDll) { printf("LoadLibrary 失敗: %d\n", GetLastError()); return 1; }
// 方式 A:按名稱獲取
PFN_ADD pAdd = (PFN_ADD)GetProcAddress(hDll, "Add");
// 方式 B:按序號獲取(需 .def 指定 @n)
// PFN_ADD pAdd = (PFN_ADD)GetProcAddress(hDll, MAKEINTRESOURCEA(1));
if (pAdd) printf("2+3=%d\n", pAdd(2,3));
else printf("GetProcAddress 失敗: %d\n", GetLastError());
FreeLibrary(hDll);
return 0;
}
#pragma comment(lib, "SimpleDLL.lib") // 鏈接導入庫
extern "C" __declspec(dllimport) int Add(int, int); // 聲明導入
int main() {
printf("%d\n", Add(2,3)); // 直接調用,像本地函數
return 0;
}
原理:.lib 為導入庫,僅含導出符號重定位信息與 DLL 文件名。鏈接器將 DLL 文件名寫入 EXE 導入表,載入器啟動時自動加載 DLL 並解析地址。
DllMain:DLL 進程/線程附加分離入口
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason, LPVOID lpReserved) {
switch (ul_reason) {
case DLL_PROCESS_ATTACH:
// 進程加載 DLL:初始化全局資源、禁用線程附加通知優化
DisableThreadLibraryCalls(hModule);
printf("[DLL] Process Attach\n");
break;
case DLL_PROCESS_DETACH:
// 進程卸載 DLL:清理資源
printf("[DLL] Process Detach\n");
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
// 線程創建/退出時調用(若未 DisableThreadLibraryCalls)
break;
}
return TRUE; // FALSE 會導致加載失敗
}
驗證調用時機:動態加載時,輸出順序為 Process Attach → 業務調用 → Process Detach。
__stdcall 調用約定:跨語言互操作關鍵| 約定 | 參數入棧 | 棧清理 | 修飾名稱 | 適用場景 |
|---|---|---|---|---|
__cdecl (默認) | 右→左 | 調用者 | _FuncName | C/C++ 內部、可變參數 |
__stdcall (WINAPI) | 右→左 | 被調用者 (ret n) | _FuncName@n | Win32 API、COM、跨語言 |
__fastcall | 前 2 個進 ECX/EDX | 被調用者 | @FuncName@n | 高性能內部調用 |
錯誤示範:DLL 導出 __stdcall,EXE 用 __cdecl 函數指針調用 → 運行時棧破壞,彈出「Run-Time Check Failure #0」。
// 正確:雙方統一
// DLL.h
extern "C" __declspec(dllexport) int __stdcall Add(int, int);
// EXE
typedef int (__stdcall *PFN_ADD)(int, int); // 注意 __stdcall
// DLL.h
extern int g_Counter; // 聲明
// DLL.cpp
int g_Counter = 0; // 定義
// .def 中導出: g_Counter DATA
// EXE 使用(靜態加載)
#pragma comment(lib, "SimpleDLL.lib")
extern __declspec(dllimport) int g_Counter; // 直接用變量名
int main() {
printf("%d\n", g_Counter); // 讀
g_Counter = 100; // 寫,同步到 DLL 數據段
return 0;
}
注意:若用 extern int g_Counter 無 dllimport,得到的是地址,需 *(int*)&g_Counter 訪問,極易出錯。強烈建議統一使用 __declspec(dllimport)。
// Point.h
#ifndef POINT_H
#define POINT_H
#ifdef SIMPLEDLL_EXPORTS
#define DLL_CLASS __declspec(dllexport)
#else
#define DLL_CLASS __declspec(dllimport)
#endif
class DLL_CLASS Point {
public:
float x, y;
Point(float _x=0, float _y=0) : x(_x), y(_y) {}
float Distance() const;
};
#endif
// Point.cpp
#include "Point.h"
float Point::Distance() const { return sqrt(x*x + y*y); }
應用程序 #include "Point.h" + #pragma comment(lib,...) 即可直接構建對象、調用成員函數,完全透明化。
DLL_API 宏切換 dllexport/dllimport。extern "C" __stdcall,避免 C++ 名稱修飾與調用約定坑。__declspec(dllimport)。.def 文件精確控制導出序號,支援按序號獲取。DllMain 中只做極簡初始化/清理,嚴禁調用 LoadLibrary/同步操作/複雜邏輯。Release 版 DLL + .lib + .h + .def(可選) + 版本資源。