我想在visual studio2022中使用c语言制作dll应该怎么办,同时如果我写了另一个c语言程序想要使用这个dll中的函数应该怎么编写代码?
好的,在 Visual Studio 2022 中使用 C 语言创建和使用 DLL 的过程可以分为两大步。我会为你提供详细的步骤和代码示例。
第一部分:创建 DLL
步骤 1:创建新项目
- 打开 Visual Studio 2022。
- 点击“创建新项目”。
- 在搜索框中,输入
dynamic
,然后选择“动态链接库 (DLL)”模板。如果找不到,也可以选择“空项目”,然后手动配置。 - 点击“下一步”,为项目命名(例如
MyMathDLL
),选择位置,然后点击“创建”。
步骤 2:编写 DLL 代码
DLL 需要明确指定哪些函数是可供外部程序(称为“客户端”)使用的。我们通过 __declspec(dllexport)
关键字来实现。
在“解决方案资源管理器”中,右键点击“源文件” -> “添加” -> “新建项…”,创建一个名为 mymath.c
的文件。
将以下代码写入 mymath.c
:
// mymath.c - DLL 源文件#include <stdio.h>// 声明函数为导出函数
// __declspec(dllexport) 是关键,它告诉编译器这个函数需要被导出
__declspec(dllexport) int add(int a, int b) {return a + b;
}__declspec(dllexport) int subtract(int a, int b) {return a - b;
}// 一个只在DLL内部使用的函数(辅助函数),不被导出
void internal_helper_function() {printf("This is an internal function, not exported.\n");
}
步骤 3:创建头文件(可选但强烈推荐)
为了让使用你 DLL 的程序员知道有哪些函数可用以及它们的参数,最好创建一个头文件。
右键点击“头文件” -> “添加” -> “新建项…”,创建一个名为 mymath.h
的文件。
将以下代码写入 mymath.h
:
// mymath.h - DLL 的头文件// 为了防止头文件被多次包含时重复定义
#pragma once#ifdef __cplusplus
extern "C" { // 如果被C++程序使用,确保以C语言的方式编译和链接
#endif// 声明导出函数。// 注意:这里用的是 `__declspec(dllexport)`,因为当前在编译DLL本身。__declspec(dllexport) int add(int a, int b);__declspec(dllexport) int subtract(int a, int b);#ifdef __cplusplus
}
#endif
步骤 4:生成 DLL
- 在顶部菜单栏,确保解决方案配置是“Release”和“x64”(或者 x86,根据你的目标平台选择,建议初学用 x64)。
- 点击“生成” -> “生成解决方案”。
- 如果成功,你会在项目目录下的
x64/Release/
文件夹中找到两个重要文件:MyMathDLL.dll
:这就是动态链接库本身,包含了编译后的二进制代码。MyMathDLL.lib
:这是导入库,客户端程序在链接时需要这个文件。它包含了帮助程序找到DLL中函数的信息。
第二部分:创建客户端程序使用 DLL
现在,我们来创建另一个程序来调用刚刚生成的 DLL 中的函数。
方法一:隐式链接(最常用、最简单)
这种方法需要在编译时提供 .lib
文件,并在运行时需要 .dll
文件。
步骤 1:创建新的客户端项目
- 在当前的 Visual Studio 解决方案中:
- 右键点击“解决方案 ‘MyMathDLL’” -> “添加” -> “新建项目”。
- 选择“控制台应用”模板,命名为
TestDLL
,点击“下一步”创建。 - 确保新项目
TestDLL
也被设置为Release
和x64
(与 DLL 项目保持一致)。
步骤 2:配置客户端项目
我们需要告诉客户端项目在哪里可以找到 DLL 的头文件和导入库(.lib)。
-
添加头文件路径:
- 右键点击
TestDLL
项目 -> “属性”。 - 在左侧,找到“C/C++” -> “常规”。
- 在“附加包含目录”中,添加
mymath.h
所在的路径(即 DLL 项目的路径)。例如:..\MyMathDLL
- 右键点击
-
添加导入库(.lib)路径和文件名:
- 在属性页,找到“链接器” -> “常规”。
- 在“附加库目录”中,添加
MyMathDLL.lib
所在的路径(即x64/Release/
)。例如:..\MyMathDLL\x64\Release
- 找到“链接器” -> “输入”。
- 在“附加依赖项”中,添加
MyMathDLL.lib
。
步骤 3:编写客户端代码
在 TestDLL
项目的源文件(如 TestDLL.c
)中,写入以下代码:
// TestDLL.c - 客户端程序// 包含DLL的头文件,这样我们就知道add和subtract函数的原型
#include "mymath.h"
#include <stdio.h>int main() {int a = 10, b = 5;// 直接调用DLL中的函数,就像调用普通函数一样int sum = add(a, b);int difference = subtract(a, b);printf("a = %d, b = %d\n", a, b);printf("a + b = %d\n", sum);printf("a - b = %d\n", difference);return 0;
}
步骤 4:运行客户端程序
- 右键点击
TestDLL
项目 -> “设为启动项目”。 - 按
Ctrl + F5
运行。 - 重要: 确保
MyMathDLL.dll
文件在客户端程序(TestDLL.exe
)可以找到的地方。通常有几种方法:- 将
MyMathDLL.dll
复制到TestDLL.exe
所在的目录(即TestDLL\x64\Release\
)。 - 或者更简单:在
TestDLL
项目的“属性” -> “调试” -> “工作目录”中,设置为DLL所在的路径(..\MyMathDLL\x64\Release
)。
- 将
方法二:显式链接(更灵活,更复杂)
这种方法在运行时通过 Windows API(LoadLibrary
, GetProcAddress
)手动加载 DLL 和获取函数地址,不需要 .lib
和头文件(但你需要知道函数的原型)。
客户端代码示例(不需要配置链接器):
#include <windows.h>
#include <stdio.h>// 定义函数指针类型,必须与DLL中的函数原型完全匹配
typedef int(*ADD_FUNC)(int, int);
typedef int(*SUBTRACT_FUNC)(int, int);int main() {HINSTANCE hDll;ADD_FUNC add_func;SUBTRACT_FUNC subtract_func;int a = 10, b = 5;// 1. 加载DLLhDll = LoadLibrary(TEXT("MyMathDLL.dll"));if (hDll == NULL) {printf("Failed to load DLL!\n");return 1;}// 2. 获取函数地址add_func = (ADD_FUNC)GetProcAddress(hDll, "add");subtract_func = (SUBTRACT_FUNC)GetProcAddress(hDll, "subtract");if (add_func == NULL || subtract_func == NULL) {printf("Failed to get function address!\n");FreeLibrary(hDll);return 1;}// 3. 使用函数int sum = add_func(a, b);int difference = subtract_func(a, b);printf("a = %d, b = %d\n", a, b);printf("a + b = %d\n", sum);printf("a - b = %d\n", difference);// 4. 卸载DLLFreeLibrary(hDll);return 0;
}
使用这种方法,你只需要确保 MyMathDLL.dll
在程序可找到的路径(如同一目录)即可,不需要 .lib
文件和头文件(在编译时)。
总结
特性 | 隐式链接 | 显式链接 |
---|---|---|
易用性 | 高,像调用普通函数一样 | 低,需要手动处理加载和函数指针 |
所需文件 | 需要头文件 (.h) 和导入库 (.lib) 用于编译 | 只需要知道函数原型 |
灵活性 | 低,启动时如果找不到DLL则失败 | 高,可以在运行时决定加载哪个DLL,优雅处理错误 |
性能 | 稍快(函数调用直接) | 稍慢(需要通过指针调用) |
对于大多数情况,隐式链接是更直接和首选的方法。显式链接则在需要动态加载插件、处理不同版本DLL或需要更精细的错误控制时非常有用。
HINSTANCE是什么?
HINSTANCE
是 Windows API 中的一个核心数据类型,全称是 Handle to an Instance(实例句柄)。
本质是什么?
HINSTANCE
实际上是一个句柄(Handle),它本质上是一个数值(在32位系统中是32位,64位系统中是64位),用来唯一标识和引用一个加载到内存中的模块(如DLL、EXE)。
主要用途
在显式链接DLL时,HINSTANCE
用来:
-
代表已加载的DLL模块
HINSTANCE hDll = LoadLibrary(TEXT("MyMathDLL.dll")); // 此时 hDll 就是 MyMathDLL.dll 在内存中的"身份证"
-
作为参数传递给
GetProcAddress()
来获取函数地址ADD_FUNC add_func = (ADD_FUNC)GetProcAddress(hDll, "add"); // 告诉系统:在 hDll 这个模块中找名为 "add" 的函数
-
最后用于卸载DLL
FreeLibrary(hDll); // 通过句柄告诉系统卸载哪个DLL
底层细节
- 32位系统:
HINSTANCE
是void*
类型,占4字节 - 64位系统:
HINSTANCE
是void*
类型,占8字节 - 实际定义:
typedef HMODULE HINSTANCE;
(HMODULE
是模块句柄)
简单类比
你可以把 HINSTANCE
理解为:
- 文件操作中的文件句柄(就像
FILE*
) - 指针的指针:它不直接指向DLL代码,而是指向一个系统内部管理该DLL的数据结构
实际使用示例
#include <windows.h>
#include <stdio.h>typedef int(*ADD_FUNC)(int, int);int main() {// 1. 加载DLL,返回该DLL的实例句柄HINSTANCE hMyDll = LoadLibrary(TEXT("MyMathDLL.dll"));if (hMyDll == NULL) {printf("无法加载DLL! 错误代码: %d\n", GetLastError());return 1;}// 2. 使用句柄来获取函数地址ADD_FUNC add = (ADD_FUNC)GetProcAddress(hMyDll, "add");if (add == NULL) {printf("找不到函数! \n");FreeLibrary(hMyDll);return 1;}// 3. 使用获取到的函数int result = add(10, 5);printf("10 + 5 = %d\n", result);// 4. 卸载DLL,传入要卸载的DLL的句柄FreeLibrary(hMyDll);return 0;
}
总结
HINSTANCE
是 Windows 系统中用来标识和管理加载到内存中的模块(如DLL)的句柄,在显式链接DLL时起着关键作用,相当于你与操作系统之间关于该DLL的"通信凭证"。
客户端代码中,下面两行代码是什么意思:
ADD_FUNC add_func;
SUBTRACT_FUNC subtract_func;
这两行代码是函数指针变量的声明。
详细解释
1. 代码含义
ADD_FUNC add_func;
SUBTRACT_FUNC subtract_func;
- 声明了两个变量:
add_func
和subtract_func
- 变量类型:
ADD_FUNC
和SUBTRACT_FUNC
(之前用typedef
定义的类型) - 用途:这两个变量将用来存储从DLL中获取的函数地址
2. 类比理解
可以类比为普通的变量声明:
int number; // 声明一个整型变量
ADD_FUNC add_func; // 声明一个函数指针变量
普通变量声明 | 函数指针声明 | 含义 |
---|---|---|
int number; | ADD_FUNC add_func; | 声明一个变量 |
number 存储整数 | add_func 存储函数地址 | 变量存储的内容 |
number = 10; | add_func = GetProcAddress(...); | 给变量赋值 |
3. 在完整代码中的角色
// 1. 定义函数指针类型(蓝图)
typedef int(*ADD_FUNC)(int, int);
typedef int(*SUBTRACT_FUNC)(int, int);int main() {// 2. 声明函数指针变量(创建容器)ADD_FUNC add_func;SUBTRACT_FUNC subtract_func;// 3. 给变量赋值(获取真正的函数地址)add_func = (ADD_FUNC)GetProcAddress(hDll, "add");subtract_func = (SUBTRACT_FUNC)GetProcAddress(hDll, "subtract");// 4. 使用变量(通过指针调用函数)int result = add_func(10, 5);return 0;
}
4. 另一种写法:声明时直接赋值
你也可以在声明的同时赋值:
// 声明并立即赋值
ADD_FUNC add_func = (ADD_FUNC)GetProcAddress(hDll, "add");
SUBTRACT_FUNC subtract_func = (SUBTRACT_FUNC)GetProcAddress(hDll, "subtract");
5. 为什么要这样写?
这样设计的好处是:
- 类型安全:编译器会检查函数签名是否匹配
- 代码清晰:通过类型名就知道这个指针指向什么类型的函数
- 易于使用:可以像调用普通函数一样调用
add_func(10, 5)
总结
ADD_FUNC add_func;
这行代码的意思是:声明一个名为 add_func
的变量,这个变量的类型是 ADD_FUNC
(即指向一个接受两个int参数并返回int的函数的指针),用来存储从DLL中获取的 add
函数的地址。