场景设定
- DLL 名称:
myutils.dll - DLL 提供的功能:
- 一个函数,接收两个整数,返回它们的和。
- 一个函数,接收一个字符串,将其打印到控制台。
- 调用程序: 一个控制台应用程序,它将加载
myutils.dll并调用其中的函数。
第一步:创建 DLL (使用 Visual Studio)
这里我们使用 Visual Studio 来创建 DLL 项目,如果你使用 MinGW (GCC) 或其他工具链,语法会略有不同,但核心概念是一样的。

(图片来源网络,侵删)
1. 创建 DLL 项目
- 打开 Visual Studio。
- 选择 "创建新项目"。
- 搜索并选择 "动态链接库 (DLL)" 项目模板。
- 给项目命名,
MyUtils,并点击 "创建"。
2. 编写 DLL 代码
Visual Studio 会为你生成一个 dllmain.c 文件,我们将其重命名为 myutils.c 并修改其内容,添加我们需要的函数。
myutils.h (头文件,可选但推荐)
这个头文件定义了 DLL 将要导出的函数,方便其他程序包含和使用。
// myutils.h #ifndef MYUTILS_H #define MYUTILS_H // 使用 __declspec(dllexport) 来告诉编译器这个函数要从 DLL 中导出 #ifdef MYUTILS_EXPORTS // 在 DLL 项目中定义这个宏 #define MYUTILS_API __declspec(dllexport) #else // 在调用 DLL 的程序中不定义这个宏 #define MYUTILS_API __declspec(dllimport) #endif // 导出函数声明 MYUTILS_API int Add(int a, int b); MYUTILS_API void PrintMessage(const char* message); #endif // MYUTILS_H
myutils.c (DLL 源文件)

(图片来源网络,侵删)
// myutils.c
#include "myutils.h"
#include <stdio.h>
// Add 函数的实现
int Add(int a, int b) {
return a + b;
}
// PrintMessage 函数的实现
void PrintMessage(const char* message) {
printf("Message from DLL: %s\n", message);
}
// DLL 的入口点 (可选,但通常需要)
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
3. 编译 DLL
按 F7 或 "生成" -> "生成解决方案",编译成功后,你会在项目的 x64/Debug (或 x64/Release) 目录下找到 MyUtils.dll 和 MyUtils.lib 文件。
.dll文件是动态链接库本身,运行时需要。.lib文件是导入库链接器,用于编译阶段,帮助链接器找到函数地址。
第二步:创建调用 DLL 的 C 程序
现在我们创建一个新的 C 控制台台项目来调用我们刚刚生成的 DLL。
1. 创建新项目
- 在 Visual Studio 中,"创建新项目"。
- 选择 "控制台应用" 项目模板。
- 命名,
CallMyUtils,并点击 "创建"。
2. 编写调用代码
main.c
// main.c
#include <stdio.h>
#include <windows.h> // 必须包含,用于 LoadLibrary, GetProcAddress, FreeLibrary 等
// 为了方便,我们可以复制 DLL 的头文件过来,或者设置项目包含目录
// 这里我们直接复制过来,但去掉 MYUTILS_EXPORTS 的定义
// 在调用方,MYUTILS_API 会被定义为 __declspec(dllimport)
// 复制 myutils.h 的内容,但移除 #ifdef MYUTILS_EXPORTS ... #endif 部分
// 简化版,只包含函数声明
#ifdef __cplusplus
extern "C" { // 确保使用 C 方式链接,避免 C++ 的名称修饰
#endif
// 在调用方,我们告诉编译器这些函数是从外部 DLL 导入的
__declspec(dllimport) int Add(int a, int b);
__declspec(dllimport) void PrintMessage(const char* message);
#ifdef __cplusplus
}
#endif
int main() {
// 1. 加载 DLL
// 使用 LoadLibrary 函数,需要传入 DLL 的文件名。
// DLL 不在系统路径或当前工作目录下,需要提供完整路径。
HMODULE hDll = LoadLibrary(L"MyUtils.dll"); // 注意:Windows API 通常使用宽字符 L"..."
if (hDll == NULL) {
printf("Failed to load DLL. Error: %lu\n", GetLastError());
return 1;
}
printf("DLL loaded successfully.\n");
// 2. 获取函数地址
// 使用 GetProcAddress 函数,通过函数名获取其在内存中的地址。
// 我们使用 typedef 来定义函数指针,使代码更清晰、更安全。
typedef int (*AddFuncPtr)(int, int);
typedef void (*PrintMessageFuncPtr)(const char*);
AddFuncPtr pAdd = (AddFuncPtr)GetProcAddress(hDll, "Add");
PrintMessageFuncPtr pPrintMessage = (PrintMessageFuncPtr)GetProcAddress(hDll, "PrintMessage");
// 检查是否成功获取函数地址
if (pAdd == NULL || pPrintMessage == NULL) {
printf("Failed to get function address. Error: %lu\n", GetLastError());
FreeLibrary(hDll); // 记得释放资源
return 1;
}
printf("Function addresses obtained.\n");
// 3. 调用函数
int result = pAdd(5, 7);
printf("Result from Add(5, 7): %d\n", result);
pPrintMessage("Hello from the calling program!");
// 4. 释放 DLL
// 当不再需要使用 DLL 时,必须调用 FreeLibrary 来释放它。
FreeLibrary(hDll);
printf("DLL unloaded.\n");
return 0;
}
3. 配置调用项目
这是最关键的一步,否则链接器会找不到函数。

(图片来源网络,侵删)
-
设置包含目录:
- 在
CallMyUtils项目上右键 -> "属性"。 - 配置选择 "所有配置"。
- 左侧导航到 "C/C++" -> "常规" -> "附加包含目录"。
- 点击下拉箭头,选择 "编辑..."。
- 添加
MyUtils项目的头文件所在目录的路径,如果MyUtils项目在同一解决方案下,路径可能是..\MyUtils。
- 在
-
设置库目录:
- 在属性页中,导航到 "链接器" -> "常规" -> "附加库目录"。
- 添加
MyUtils项目生成的.lib文件所在的目录路径。..\MyUtils\x64\Debug。
-
添加依赖项:
- 在属性页中,导航到 "链接器" -> "输入" -> "附加依赖项"。
- 点击下拉箭头,选择 "编辑..."。
- 添加
MyUtils.lib,链接器会使用这个.lib文件在编译时解析函数符号。
4. 编译和运行
- 确保
CallMyUtils项目是启动项目(在解决方案资源管理器中右键点击CallMyUtils-> "设为启动项目")。 - 重要: 将
MyUtils.dll复制到CallMyUtils的输出目录(x64/Debug),因为LoadLibrary默认只在当前目录和系统路径中查找 DLL。 - 按
F5启动调试。
预期输出:
DLL loaded successfully.
Function addresses obtained.
Result from Add(5, 7): 12
Message from DLL: Hello from the calling program!
DLL unloaded.
-
导出函数 (DLL侧):
- 使用
__declspec(dllexport)关键字修饰需要被外部程序调用的函数。 - 通常配合宏(如
#ifdef MYUTILS_EXPORTS)来区分 DLL 源文件和调用方源文件。
- 使用
-
导入函数 (调用方侧):
- 使用
__declspec(dllimport)关键字声明来自 DLL 的函数。 - 包含 DLL 的头文件。
- 正确配置项目的 "附加包含目录" 和 "附加依赖项"。
- 使用
-
运行时调用:
LoadLibrary(L"DllName.dll"): 加载 DLL 到内存,返回一个模块句柄HMODULE。GetProcAddress(HMODULE, "FunctionName"): 根据函数名获取其在内存中的地址,返回一个FARPROC类型的指针,需要强制转换为正确的函数指针类型。- 通过函数指针调用函数。
FreeLibrary(HMODULE): 卸载 DLL,释放资源。
高级主题和注意事项
- C++ 名称修饰 (Name Mangling): 如果用 C++ 编写 DLL,函数名会被编译器修改(名称修饰),导致
GetProcAddress找不到,解决方案是在 C++ 函数声明上使用extern "C",如extern "C" __declspec(dllexport) int Add(...);。 DLL_PROCESS_ATTACH和DLL_PROCESS_DETACH: 在DllMain中,你可以响应 DLL 的加载和卸载事件,进行初始化和清理工作。- 错误处理: 始终检查
LoadLibrary和GetProcAddress的返回值,并使用GetLastError()获取详细的错误信息。 - 静态链接 vs. 动态链接: 上面描述的是“隐式链接”,通过
.lib文件在链接时解析地址,还有一种“显式链接”,就是我们上面代码中使用的LoadLibrary/GetProcAddress方式,这种方式更灵活,可以在运行时决定是否加载 DLL。
