C# 与 C++ DLL 的联合调试
创建简单的本机 DLL
为 DLL 项目创建文件:
-
打开 Visual Studio 并创建项目。
按 Esc 关闭开始窗口。 键入 Ctrl+Q 以打开搜索框,键入"空项目",然后选择"空项目"(C++)。 在出现的"配置新项目"对话框中,键入名称(如 Mixed_Mode_Debugging),并单击"创建"。
如果未看到 空项目 项目模板,请转到 工具 >获取工具和功能...,这将打开 Visual Studio 安装程序。 Visual Studio 安装程序将启动。 选择"使用 C++ 的桌面开发"工作负载,然后选择"修改"按钮。
Visual Studio 创建项目。
-
在 解决方案资源管理器 中,选择 源文件 ,然后选择 项目 >添加新项 。 或者,右键单击 源文件 并选择"添加 >新项 。如果未看到所有项模板,请选择 显示所有模板。
-
在 "新建项 "对话框中,选择 C++文件(.cpp) 。 在"名称"字段中键入"Mixed_Mode.cpp",然后选择"添加"。Visual Studio 将新的C++文件添加到 解决方案资源管理器。
-
将以下代码复制到 Mixed_Mode.cpp:
cpp#include "Mixed_Mode.h" -
在 解决方案资源管理器 中,选择 头文件 ,然后选择 项目 >添加新项 。 或者,右击 头文件 ,并选择 添加 >新项 。如果未看到所有项模板,请选择 显示所有模板。
-
在"新项"对话框中,选择"头文件(.h)"。 在 名称 字段中键入 Mixed_Mode.h ,然后选择 添加 。Visual Studio 将新的头文件添加到 解决方案资源管理器。
-
将以下代码复制到 Mixed_Mode.h:
cpp#ifndef MIXED_MODE_MULTIPLY_HPP #define MIXED_MODE_MULTIPLY_HPP extern "C" { __declspec(dllexport) int __stdcall mixed_mode_multiply(int a, int b) { return a * b; } } #endif -
选择"文件""全部保存",或按 Ctrl>ShiftS 进行保存。++
配置和生成 DLL 项目:
-
在 Visual Studio 工具栏中,选择 调试 配置,x86 或 x64 平台。 如果调用应用为 .NET Core(始终以 64 位模式运行),请选择 x64 作为平台。
-
在 解决方案资源管理器 中,选择 Mixed_Mode_Debugging 项目节点,然后选择 属性 图标,或右键单击项目节点并选择 属性。
-
在"属性 "窗格顶部,确保 配置 设置为 Active(Debug) ,平台 与在工具栏中设置的内容相同:x64 ,或 win32 x86 平台。
如果将平台从 x86 切换到 x64,则必须重新配置新平台的属性。
-
在左窗格中 配置属性 下,选择 "链接器 >高级 ",然后在 "无入口点" 旁边的下拉列表中,选择 "无"。 如果必须将其更改为"否",请选择"应用"。
-
在 配置属性 下,选择 常规 ,然后在 配置类型 旁边的下拉列表中,选择 动态库(.dll)。 选择"应用",然后选择"确定"。

-
在 解决方案资源管理器 中选择项目,然后选择 生成 >生成解决方案 ,按 F7 ,或右键单击项目并选择 生成。
该项目应顺利生成,没有错误。
创建一个简单的托管应用来调用 DLL
-
打开 Visual Studio 并创建新项目。
按 Esc 关闭开始窗口。 键入 Ctrl + Q 打开搜索框,键入"控制台",然后选择适用于 .NET 或 .NET Framework 的 C# 控制台应用。
然后,键入名称(如 Mixed_Mode_Calling_App ),单击 "下一步" 或 "创建",选择可用的选项。
对于 .NET Core 或 .NET 5+,请选择建议的目标框架或 .NET 10,然后选择" 创建"。
如果未看到正确的项目模板,请转到 工具 >获取工具和功能... ,这将打开 Visual Studio 安装程序。 根据先决条件选择正确的 .NET 工作负荷,然后选择 修改。
注意
还可以将新的托管项目添加到现有C++解决方案。 我们正在新解决方案中创建项目,以使混合模式调试任务更加困难。
Visual Studio 创建空项目,并将其显示在解决方案资源管理器 中。
-
将 Program.cs 中的所有代码替换为以下代码:
C#
csusing System; using System.Runtime.InteropServices; namespace Mixed_Mode_Calling_App { public class Program { // Replace the file path shown here with the // file path on your computer. For .NET Core, the typical (default) path // for a 64-bit DLL might look like this: // C:\Users\username\source\repos\Mixed_Mode_Debugging\x64\Debug\Mixed_Mode_Debugging.dll // Here, we show a typical path for a DLL targeting the **x86** option. [DllImport(@"C:\Users\username\source\repos\Mixed_Mode_Debugging\Debug\Mixed_Mode_Debugging.dll", EntryPoint = "mixed_mode_multiply", CallingConvention = CallingConvention.StdCall)] public static extern int Multiply(int x, int y); public static void Main(string[] args) { int result = Multiply(7, 7); Console.WriteLine("The answer is {0}", result); Console.ReadKey(); } } } -
在新代码中,将
[DllImport]中的文件路径替换为刚创建的 Mixed_Mode_Debugging.dll 的文件路径。 有关提示,请参阅代码注释。 确保替换 username 占位符。 -
选择"文件 >"以保存"Program.cs" ,或按Ctrl +S 保存文件。
配置混合模式调试
-
在 解决方案资源管理器 中,选择 Mixed_Mode_Calling_App 项目节点,然后选择 属性 图标,或右键单击项目节点并选择 属性。
-
启用本机代码调试:右键 C# 项目 > 属性 > 调试 > 打开启动配置文件 UI(或直接在"调试"页)> 勾选 "启用本机代码调试" 。
-
在属性中启用本机代码调试。
.NET 代码
在左窗格中选择 调试 ,选择 打开调试启动配置文件 UI ,然后选择 启用本机代码调试 复选框,然后关闭属性页以保存更改。


在左侧菜单中,选择"调试"。 然后,在 调试器引擎 部分中,选择 启用本机代码调试 属性,然后关闭属性页以保存更改。
-
如果要从 .NET Framework 应用定位 x64 DLL,请将平台目标从"任何 CPU"更改为 x64。 为此,可能需要从"调试"工具栏的解决方案平台下拉列表中选择 Configuration Manager 。 然后,如果无法直接切换到 x64,请创建面向 x64 的新 配置。
设置断点并开始调试
-
在 C# 项目中,打开 Program.cs。 在下列代码行中设置断点,方法是点击最左侧边缘并选择该行再按 F9,或右键单击该行并选择"断点""插入断点">。
csint result = Multiply(7, 7);在设置断点的左边距中会出现一个红色圆圈。
-
按 F5 ,选择 Visual Studio 工具栏中的绿色箭头,或者选择 调试 >启动调试 来开始调试。
调试器会在设置的断点上暂停。 黄色箭头指示调试器当前暂停的位置。
单步执行和单步跳出本机代码
-
托管应用中的调试暂停时按 F11,或选择"调试""单步执行">。
"Mixed_Mode.h"本机头文件打开,在调试器暂停位置看到黄色箭头。

-
现在,可以设置并命中断点以及检查本机代码或托管代码中的变量。
-
将鼠标悬停在源代码中的变量上以查看其值。
-
在"自动"和"局部变量"窗口查看变量和变量值。
-
在调试器中暂停时,还可以使用"监视"窗口和"调用堆栈"窗口。
-
-
再按 F11,将调试器推进一行。
-
按 ShiftF11 或选择"调试"+"单步跳出",在托管应用中继续执行并再次暂停。>
-
按 F5 或选择绿色箭头以继续调试应用。
Windows C/C# 使用 DLL 的 4 种主流方式完整讲解
一、隐式链接(静态加载,编译时绑定.lib)
原理
编译时链接器读取 DLL 配套的导入库 .lib,程序启动时操作系统自动加载对应 DLL,无需手动调用加载 API。
适用场景
- 自有 DLL 工程,同一解决方案开发;
- DLL 随程序一同发布,不会动态切换不同版本 dll;
- C++、C#(仅 C++ 支持原生隐式链接,C# 无.lib 隐式链接,只能 DllImport)。
C++ 完整使用步骤
- DLL 工程导出函数,生成
xxx.dll+xxx.lib; - 调用方项目配置:
- 附加包含目录:DLL 头文件目录;
- 附加库目录:lib 文件所在文件夹;
- 附加依赖项:
xxx.lib;
- 代码引入头文件直接调用:
cpp
#include "Calculator.h"
int main()
{
Calculator* calc = CreateCalculator();
DeleteCalculator(calc);
return 0;
}
- 发布时必须把
xxx.dll和 exe 放在同一目录,否则启动直接报错找不到模块。
优缺点
✅ 优点:代码简洁,无需手动加载释放,启动即用; ❌ 缺点:
- 缺少 dll 程序直接无法启动;
- 不能运行时切换 dll 版本;
- C# 不支持该方式。
二、显式动态加载(LoadLibrary / FreeLibrary,运行时手动加载)
原理
程序运行后,代码主动调用 Win32 API 加载 DLL,通过函数名获取导出地址,用完手动释放。
适用场景
- 可选插件架构,不存在对应 dll 时程序也能正常运行;
- 运行时切换不同版本 DLL;
- 只使用 DLL 内少量函数,不想全局链接 lib;
- C++、C#(P/Invoke 调用 LoadLibrary)通用。
C++ 标准示例
cpp
#include <Windows.h>
typedef Calculator* (__cdecl *pfnCreateCalc)();
typedef void (__cdecl *pfnDeleteCalc)(Calculator*);
void TestDll()
{
// 加载DLL
HMODULE hDll = LoadLibraryW(L"Calculator.dll");
if (hDll == NULL) return;
// 获取导出函数地址
pfnCreateCalc CreateCalc = (pfnCreateCalc)GetProcAddress(hDll, "CreateCalculator");
pfnDeleteCalc DeleteCalc = (pfnDeleteCalc)GetProcAddress(hDll, "DeleteCalculator");
if (CreateCalc && DeleteCalc)
{
Calculator* p = CreateCalc();
DeleteCalc(p);
}
// 释放DLL模块
FreeLibrary(hDll);
}
C# 动态加载封装示例
cs
using System.Runtime.InteropServices;
public class DllLoader
{
[DllImport("kernel32.dll")]
public static extern IntPtr LoadLibrary(string dllPath);
[DllImport("kernel32.dll")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32.dll")]
public static extern bool FreeLibrary(IntPtr hModule);
delegate IntPtr CreateCalc();
delegate void DeleteCalc(IntPtr ptr);
static void Test()
{
IntPtr hDll = LoadLibrary("Calculator.dll");
IntPtr pCreate = GetProcAddress(hDll, "CreateCalculator");
CreateCalc create = Marshal.GetDelegateForFunctionPointer<CreateCalc>(pCreate);
IntPtr calc = create();
// ...业务逻辑
FreeLibrary(hDll);
}
}
优缺点
✅ 优点:
- 无 dll 时可做降级逻辑,程序不会崩溃;
- 运行时指定 dll 路径、切换版本;
- 无需.lib 导入库,只需要 dll; ❌ 缺点:代码繁琐,需要手动管理模块句柄与函数指针。
三、C# 专用:DllImport 特性加载(托管 P/Invoke)
原理
C# 独有的托管调用原生 DLL 方式,本质底层是封装后的动态加载,无需手动LoadLibrary,第一次调用函数时自动加载 dll。
适用场景
C# WinForms/WPF/ 控制台调用原生 C++ DLL(你当前计算器项目场景)。
代码示例
cs
using System.Runtime.InteropServices;
class Program
{
// 声明DLL导出函数,指定调用约定匹配C++ __cdecl
[DllImport("Calculator.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr CreateCalculator();
[DllImport("Calculator.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void DeleteCalculator(IntPtr calcPtr);
static void Main()
{
IntPtr calc = CreateCalculator();
DeleteCalculator(calc);
}
}
关键约束
- dll 必须放在 exe 同级目录、系统 system32 目录;
- 调用约定必须和 C++ 完全匹配(
__cdecl/__stdcall); - 无法直接传递 C++ 类对象,只能用指针交互。
优缺点
✅ 优点:语法简洁,C# 官方标准调用方式,混合调试友好; ❌ 缺点:dll 路径搜索规则固定,不能动态指定 dll 完整路径;缺失 dll 启动后调用函数才报错。
四、C++/CLI 托管混合 DLL(托管 + 原生互通)
原理
创建CLR C++ 项目,编译生成混合模式 dll,同时包含原生 C++ 代码与.NET 托管接口,C# 直接引用该 dll,像调用 C# 类一样使用原生逻辑,无需 DllImport。
适用场景
大量 C++ 逻辑需要高频给 C# 调用,不想手动写 DllImport 指针转换。
示例逻辑
- C++/CLI 封装原生 Calculator 类,输出托管包装类;
- C# 项目直接添加引用该混合 dll,new 包装类即可使用;
cpp
// C++/CLI 封装代码
public ref class CalcWrapper
{
private:
Calculator* m_native;
public:
CalcWrapper()
{
m_native = CreateCalculator();
}
~CalcWrapper()
{
DeleteCalculator(m_native);
}
int Add(int a, int b)
{
return m_native->Add(a,b);
}
};
C# 调用:
cs
CalcWrapper calc = new CalcWrapper();
int res = calc.Add(1,2);
优缺点
✅ 优点:托管原生无缝互通,不用处理函数指针、调用约定; ❌ 缺点:
- 依赖.NET 运行时;
- 生成的 dll 体积更大;
- 不同 VS 版本、平台目标容易出现兼容性问题。
四种方式对比汇总表
| 使用方式 | 支持语言 | 是否需要.lib | 能否动态切换 DLL | 开发复杂度 | 典型场景 |
|---|---|---|---|---|---|
| 隐式链接 (lib) | C++ | 需要 | 否 | 低 | 固定配套 dll,内部项目 |
| LoadLibrary 显式加载 | C++/C# | 不需要 | 是 | 高 | 插件、可选组件 |
| C# DllImport | C# | 不需要 | 否 | 低 | C# 调用固定原生 DLL(你的计算器) |
| C++/CLI 混合 DLL | C+++C# 互通 | 不需要 | 否 | 中 | 大量原生逻辑给 C# 调用 |
补充开发建议(适配你当前 C#+C++ 计算器项目)
- 日常调试开发优先使用 DllImport,配合后期生成事件自动复制 dll+pdb,支持混合调试;
- 如果后续要做插件功能,改用
LoadLibrary动态加载; - 不推荐隐式链接,C# 无法使用;
- 大量复杂类交互可考虑 C++/CLI 包装层,省去指针转换。
ib 与 dll 完整区别详解(分静态库、导入库两种 lib)
一、先分清两种完全不同的 .lib
很多人混淆,lib 分两类,作用天差地别:
- 静态库 Static Library(静态 lib):代码 / 数据完整打包进 lib,编译时直接复制到 exe
- 导入库 Import Library(DLL 配套 lib):仅记录 DLL 导出函数地址索引,不包含实现,配合 dll 隐式链接
二、核心概念
1. DLL(Dynamic Link Library 动态链接库)
Windows 动态链接文件,运行时加载
- 内部包含完整函数实现、资源、DllMain 入口;
- exe 运行后才载入内存,多个程序可共用同一份 dll;
- 单独文件,发布必须随 exe 一起分发。
2. 静态 lib(Static Lib)
编译期静态打包库
- 存放完整函数二进制代码、全局数据;
- 链接时把用到的代码直接复制进 EXE;
- 最终 exe 独立运行,不需要附带 lib 文件。
3. DLL 配套导入 lib(Import lib)
编译 DLL 时 VS 自动生成的小型 lib 文件
- 没有任何函数实现代码,只存导出函数名、地址偏移;
- 仅给编译器做 "索引表",用来隐式链接 DLL;
- 运行完全不需要这个 lib,只需要 dll。
三、核心对比表
| 对比维度 | 静态 .lib(静态库) | 导入 .lib(DLL 配套) | .dll 动态库 |
|---|---|---|---|
| 内部内容 | 完整机器码、全局变量、函数实现 | 仅导出函数符号、地址索引表,无实现 | 完整函数实现、资源、DllMain |
| 链接时机 | 编译链接阶段 | 编译链接阶段 | 程序运行时加载 |
| 运行依赖 | 不需要任何外部文件,exe 独立 | 运行不需要 lib,只需要 dll | 程序启动 / 调用时必须存在 dll |
| 内存占用 | 每个 exe 单独拷贝一份代码,多开重复占用 | 代码在 dll,多程序共享同一份内存 | 多进程共享 dll 内存,节省资源 |
| 更新方式 | 必须重新编译所有依赖 exe | 只替换 dll 即可,不用重编 exe | 直接替换 dll,程序无需重新编译 |
| 代码共享 | 不支持,多程序重复加载 | 依托 dll 实现共享 | 天然支持多进程内存共享 |
| 崩溃影响 | 代码内置 exe,崩溃等同于主程序 | 代码在 dll,dll 出错只影响加载它的进程 | dll 内部异常、崩溃会导致调用进程崩溃 |
| C# 可用性 | 无法直接使用 | C# 不支持隐式链接,完全不用 | C# 通过 DllImport/LoadLibrary 调用 |
四、优缺点拆解
静态 lib 优缺点
✅ 优点:
- 发布简单,只需要 exe,不用附带一堆库文件;
- 无 DLL 路径缺失、版本冲突问题;
- 编译时即可发现所有符号缺失错误。
❌ 缺点:
- exe 体积大,所有库代码全部打包进去;
- 多程序使用同一库会重复占用内存;
- 库升级必须重新编译所有使用它的程序;
- 无法实现插件动态加载。
DLL + 导入 lib 组合优缺点
✅ 优点:
- exe 体积更小,代码剥离到 dll;
- 多个程序共用一个 dll,节省内存;
- 单独更新 dll,主程序无需重新编译;
- 支持运行时动态加载 / 卸载,实现插件系统;
- 可单独对 dll 加密、加壳保护核心算法。
❌ 缺点:
- 发布必须同步分发 dll,丢失 dll 程序直接报错;
- 容易出现 dll 版本冲突(DLL Hell);
- 运行时错误(dll 缺失、函数导出变更)只能启动后才能发现;
- 跨模块堆内存操作易崩溃(new 在 dll、delete 在 exe)。
五、关键场景区分
场景 1:只给 C++ 小程序用,不想分发额外文件
选静态 lib,打包进 exe,单文件发布。
场景 2:多程序共用一套工具库、需要单独更新算法
选 DLL + 导入 lib,隐式链接开发,发布只带 dll。
场景 3:C# WinForms/WPF 调用 C++ 原生代码(你当前计算器项目)
只用 DLL,导入 lib 完全用不上;C# 通过DllImport或LoadLibrary加载 dll。
场景 4:插件架构,程序无 dll 也能正常运行
只用 DLL,运行时LoadLibrary动态加载,不需要导入 lib。
六、高频易错点澄清
- 混淆两种 lib
- 静态 lib:有完整代码,脱离库也能运行;
- DLL 导入 lib:只是一张 "对照表",删掉 lib 不影响程序运行,只影响编译链接。
- 修改 dll 后不用重编 exe 只要导出函数名、参数不变,直接替换 dll 即可生效,这是 DLL 最大优势;静态库必须全部重编。
- 内存崩溃经典坑 DLL 内
new创建对象,EXE 中直接delete会崩溃;因为两个模块独立 CRT 堆管理器,必须配套导出DeleteCalculator在 DLL 内部释放。 - 编译产物区分
- 新建「静态库项目」→ 输出静态
.lib; - 新建「动态链接库 (DLL) 项目」→ 输出
.dll+ 配套导入.lib。
- 新建「静态库项目」→ 输出静态