從 DLL 導出 C++ 類別供其他模組使用,需解決編譯時導出/導入符號切換與名稱修飾問題。本文比較 __declspec(dllexport/dllimport) 與模組定義檔 (.def) 兩種主流做法,並提供最佳實務巨集模式。
// MyClass.h
class __declspec(dllexport) CMyClass { ... };
缺點:標頭檔同時被 DLL 專案與使用者專案包含。使用者專案編譯時同樣看到 dllexport,導致連結錯誤 (嘗試導出而非導入)。
標頭檔 (MyClass.h):
#ifdef MYDLL_EXPORTS
#define MYDLL_API __declspec(dllexport)
#else
#define MYDLL_API __declspec(dllimport)
#endif
class MYDLL_API CMyClass
{
public:
CMyClass();
void DoSomething();
// ...
};
DLL 專案設定:專案屬性 → C/C++ → Preprocessor → Preprocessor Definitions 加入 MYDLL_EXPORTS。
使用者專案:不定義 MYDLL_EXPORTS,自動變為 dllimport。
MyClass.def:
LIBRARY "MyDll"
EXPORTS
?DoSomething@CMyClass@@QAEXXZ ; 修飾名稱 (需依編譯器輸出)
??0CMyClass@@QAE@XZ ; 建構函數
??1CMyClass@@UAE@XZ ; 解構函數
缺點:
優點:完全隱藏導出細節於標頭檔外部,標頭檔無需 __declspec 汙染。
標頭檔使用巨集 MYDLL_API 宣告類別,.def 僅用於導出純 C 函數或特定進入點。類別導出完全依賴 __declspec 機制。
MyDll/ ├── MyDll.h // 巨集定義 + 類別宣告 ├── MyDll.cpp // 類別實作 ├── MyDll.def // (可選) 導出 C 風格函數 └── MyDll.vcxproj // 定義 MYDLL_EXPORTS
#pragma once
#ifdef MYDLL_EXPORTS
#define MYDLL_API __declspec(dllexport)
#else
#define MYDLL_API __declspec(dllimport)
#endif
class MYDLL_API CInDLL
{
public:
CInDLL();
~CInDLL();
void ProcessData(int nValue);
static int GetVersion(); // 靜態成員函數亦可導出
};
#include "MyDll.h"
#include
CInDLL::CInDLL() { std::cout << "CInDLL constructed\n"; }
CInDLL::~CInDLL() { std::cout << "CInDLL destroyed\n"; }
void CInDLL::ProcessData(int n) { std::cout << "Processing " << n << "\n"; }
int CInDLL::GetVersion() { return 100; }
// Client.cpp
#include "MyDll.h"
#pragma comment(lib, "MyDll.lib") // 連結匯入庫
int main()
{
CInDLL obj; // 建構函數自動從 DLL 載入
obj.ProcessData(42);
std::cout << "Version: " << CInDLL::GetVersion() << std::endl;
return 0;
}
| 議題 | 說明 |
|---|---|
| 名稱修飾 | C++ 類別成員函數導出依賴編譯器特定修飾名稱,僅限同編譯器版本互操作 (MSVC ↔ MSVC)。跨編譯器需用 C 風格介面。 |
| 類別佈局相容 | DLL 與使用者專案必須使用相同結構對齊、相同標頭檔版本,否則物件大小、vtable 位置不一致導致崩潰。 |
| CRT 版本一致 | DLL 與 EXE 必須使用相同 CRT 版本 (MD/MT/MDd/MTd),否則 new/delete、std::string 等跨模組記憶體管理會出錯。 |
| 匯入庫 | 建置 DLL 產生 .lib 匯入庫,使用者專案需連結此 .lib。 |
| 循環相依 | 避免 DLL 之間互相導出類別造成循環載入,改用介面類別 (純虛函數) 打破循環。 |