首页 > 后端开发 > C++ > 正文

如何在C++中创建一个动态链接库_C++动态链接库(DLL)创建与使用

裘德小鎮的故事
发布: 2025-09-22 16:34:01
原创
1043人浏览过
创建C++动态链接库需使用__declspec(dllexport)标记导出函数,并通过头文件和宏定义区分导出与导入,编译生成.dll和.lib文件;使用时可通过隐式链接(自动加载)或显式链接(运行时动态加载)调用DLL功能,解决模块化、代码复用与内存效率问题。

如何在c++中创建一个动态链接库_c++动态链接库(dll)创建与使用

在C++中创建一个动态链接库(DLL)的核心在于,你需要明确地告诉编译器哪些函数或类是打算被外部程序调用的。这通常通过特定的关键字实现,例如在Windows平台上是

__declspec(dllexport)
登录后复制
。然后,将这些带有导出标记的代码编译成一个特殊的二进制文件(.dll),同时通常会生成一个对应的导入库(.lib),供其他程序在链接时使用。

解决方案

要创建一个C++动态链接库,我们通常会经历以下几个步骤。这不仅仅是技术操作,更是一种模块化思维的体现,把你的功能封装起来,让其他项目能够方便地复用。

首先,你需要定义你的DLL接口。这通常在一个头文件(例如

MyDLL.h
登录后复制
)中完成。在这个头文件中,你需要使用
__declspec(dllexport)
登录后复制
来标记那些你希望从DLL中导出的函数、类或变量。同时,为了让使用DLL的客户端程序能够正确导入这些符号,它们需要使用
__declspec(dllimport)
登录后复制
。为了避免在DLL内部编译时也使用
dllimport
登录后复制
,我们通常会定义一个宏来区分。

MyDLL.h
登录后复制
示例:

立即学习C++免费学习笔记(深入)”;

#ifdef MYDLL_EXPORTS
#define MYDLL_API __declspec(dllexport)
#else
#define MYDLL_API __declspec(dllimport)
#endif

// 确保C++编译器不会对这些函数进行名称修饰
// 这对于C语言或其他语言调用DLL函数尤其重要
extern "C" {
    MYDLL_API int Add(int a, int b);
    MYDLL_API void PrintMessage();
}

// 导出C++类,需要注意名称修饰问题
class MYDLL_API MyClass {
public:
    MyClass();
    void Greet();
    int Multiply(int a, int b);
};
登录后复制

接下来,在你的源文件(例如

MyDLL.cpp
登录后复制
)中实现这些函数和类。在编译这个源文件时,你需要定义
MYDLL_EXPORTS
登录后复制
这个宏,这样
MYDLL_API
登录后复制
就会被扩展为
__declspec(dllexport)
登录后复制

MyDLL.cpp
登录后复制
示例:

#define MYDLL_EXPORTS // 编译DLL时定义此宏

#include "MyDLL.h"
#include <iostream>

extern "C" {
    int Add(int a, int b) {
        return a + b;
    }

    void PrintMessage() {
        std::cout << "Hello from MyDLL!" << std::endl;
    }
}

MyClass::MyClass() {
    std::cout << "MyClass instance created in DLL." << std::endl;
}

void MyClass::Greet() {
    std::cout << "Greetings from MyClass in DLL!" << std::endl;
}

int MyClass::Multiply(int a, int b) {
    return a * b;
}
登录后复制

最后一步是编译。 如果你使用 MSVC (Visual Studio)

  1. 创建一个“动态链接库(DLL)”项目。
  2. MyDLL.h
    登录后复制
    MyDLL.cpp
    登录后复制
    添加到项目中。
  3. 编译项目,它会生成
    MyDLL.dll
    登录后复制
    MyDLL.lib
    登录后复制

如果你使用 MinGW/GCC: 你可以使用命令行编译。

g++ -c MyDLL.cpp -o MyDLL.o -D MYDLL_EXPORTS # 编译源文件
g++ -shared -o MyDLL.dll MyDLL.o -Wl,--out-implib,MyDLL.lib # 链接生成DLL和导入库
登录后复制

这里的

-D MYDLL_EXPORTS
登录后复制
就是定义了那个宏。
-shared
登录后复制
告诉GCC生成一个共享库(DLL)。
-Wl,--out-implib,MyDLL.lib
登录后复制
是链接器选项,用于生成导入库。

至此,你的DLL就创建完成了。接下来的挑战就是如何让其他应用程序正确地使用它。

为什么我们要用动态链接库?(以及它到底解决了什么痛点)

说实话,我最初接触DLL的时候,觉得它把事情搞得有点复杂,多了一个文件,多了一些编译步骤。但随着项目规模的增长,我才真正体会到它的价值。最核心的,DLL解决了软件开发的“模块化”和“资源共享”两大痛点。

想象一下,你有一个庞大的应用程序,里面有几十个甚至上百个模块。如果所有代码都编译成一个巨大的可执行文件,每次修改哪怕一个很小的功能,你都得重新编译整个程序,这效率简直是灾难。DLL就像是把这些模块拆分成独立的“积木”,每个积木可以独立开发、独立编译、独立更新。我个人觉得最舒服的,就是那种项目变得可插拔的感觉。比如,我可以为我的应用程序开发一个插件系统,每个插件就是一个DLL,用户可以根据需要加载或卸载,而不需要我重新发布整个应用程序。这极大地提升了软件的灵活性和可维护性。

再者,资源共享也是一个大头。多个应用程序如果都使用了同一套底层功能(比如一个图形渲染库或者一个数据库访问模块),如果这些功能都静态链接到每个应用程序里,那么每个应用程序运行时都会在内存中加载一份相同的代码副本。而使用DLL,这份代码只需要在内存中加载一次,所有使用它的程序都可以共享。这在系统资源有限的情况下,能显著减少内存占用,提升系统整体效率。虽然现代计算机内存普遍较大,但这依然是一个优雅的解决方案,尤其是在部署和分发时,可以减少应用程序的体积。

创建DLL时,有哪些“坑”是新手常踩的?

创建DLL这事儿,看起来直截了当,但实际操作起来,总有些小细节能把人搞得头大。我刚开始的时候,就没少在这些“坑”里打滚。

最大的一个问题,我觉得就是导出符号问题。你明明写了函数,也编译了DLL,结果调用方就是“找不到符号”。这多半是忘记了

__declspec(dllexport)
登录后复制
,或者在头文件里没有正确区分
dllexport
登录后复制
dllimport
登录后复制
。编译器并不会自动导出所有函数,你得明确告诉它哪些是“公共接口”。还有就是C++的名称修饰(Name Mangling),这是个隐形杀手。C++为了支持函数重载和命名空间,编译器会把函数名和参数类型编码成一个内部名称。比如
int Add(int a, int b)
登录后复制
可能被修饰成
?Add@@YAHHH@Z
登录后复制
(在MSVC上)。如果你不使用
extern "C"
登录后复制
,外部程序,特别是那些非C++编写的程序,就很难找到正确的函数入口点。所以,对于打算被C语言或其他语言调用的函数,务必加上
extern "C"
登录后复制
,强制编译器使用C语言的链接约定,避免名称修饰。

LuckyCola工具库
LuckyCola工具库

LuckyCola工具库是您工作学习的智能助手,提供一系列AI驱动的工具,旨在为您的生活带来便利与高效。

LuckyCola工具库 19
查看详情 LuckyCola工具库

另一个常见的陷阱是调用约定不匹配。在Windows上,常见的有

__cdecl
登录后复制
__stdcall
登录后复制
__cdecl
登录后复制
是C/C++的默认调用约定,调用者负责清理
__stdcall
登录后复制
则由被调用者清理栈,常用于Windows API。如果DLL导出的函数是
__stdcall
登录后复制
,而你的调用代码期望的是
__cdecl
登录后复制
,那么栈就会出现问题,轻则程序崩溃,重则出现难以追踪的内存错误。

再来就是运行时库不一致。如果你用Visual Studio开发,Debug版本和Release版本,或者使用MT(静态链接运行时库)和MD(动态链接运行时库)的DLL和调用程序,它们的运行时库版本必须一致。否则,当你尝试在DLL和主程序之间传递STL对象(如

std::string
登录后复制
std::vector
登录后复制
)时,就可能出现内存分配和释放不匹配的问题,导致程序崩溃或数据损坏。这不是编译器错误,而是运行时环境的冲突,排查起来非常麻烦。

最后,别忘了DLL Hell。虽然现在Windows系统在这方面做得更好了,但不同版本的DLL文件可能会相互覆盖,导致某些程序崩溃。这提醒我们,部署DLL时要特别小心,尽量使用独立的应用程序目录,或者确保版本兼容性。

如何在C++应用程序中正确加载和使用DLL?

创建好DLL之后,下一步就是如何在你的C++应用程序中“消费”它。这主要有两种方式:隐式链接和显式链接。我通常会根据项目需求来决定用哪种方式,各有优劣。

1. 隐式链接(Implicit Linking)

这是最简单、最常用的方式,感觉就像在用一个普通的静态库一样。在编译你的应用程序时,你需要告诉链接器去链接DLL对应的导入库(

.lib
登录后复制
文件)。当应用程序启动时,操作系统会自动加载DLL文件到内存中。

步骤:

  1. 包含头文件: 在你的应用程序代码中包含DLL的头文件(
    MyDLL.h
    登录后复制
    )。确保头文件中的
    MYDLL_API
    登录后复制
    宏被正确地扩展为
    __declspec(dllimport)
    登录后复制
    (即不要定义
    MYDLL_EXPORTS
    登录后复制
    )。
  2. 链接导入库: 在你的项目设置中,将DLL生成的
    .lib
    登录后复制
    文件添加到链接器的输入中。
    • MSVC: 在项目属性 -> 链接器 -> 输入 -> 附加依赖项中添加
      MyDLL.lib
      登录后复制
    • GCC/MinGW: 在编译命令中加入
      -L<lib_path> -lMyDLL
      登录后复制
    • 或者,在代码中使用
      #pragma comment(lib, "MyDLL.lib")
      登录后复制
      (仅限MSVC)。
  3. 部署DLL: 确保
    MyDLL.dll
    登录后复制
    文件在应用程序的可执行文件同目录下,或者在系统的PATH环境变量指定的路径中,以便操作系统能够找到并加载它。

示例代码:

// 应用程序代码 (MyApp.cpp)
#include "MyDLL.h" // 此时MYDLL_API 会被定义为 __declspec(dllimport)
#include <iostream>

#pragma comment(lib, "MyDLL.lib") // 告诉MSVC链接MyDLL.lib

int main() {
    // 调用DLL导出的C函数
    int result = Add(5, 3);
    std::cout << "Add(5, 3) = " << result << std::endl;
    PrintMessage();

    // 使用DLL导出的C++类
    MyClass myObj;
    myObj.Greet();
    int product = myObj.Multiply(4, 2);
    std::cout << "Multiply(4, 2) = " << product << std::endl;

    return 0;
}
登录后复制

优点: 使用起来非常方便,就像调用本地函数一样自然。 缺点: 应用程序在启动时就强依赖于DLL。如果DLL文件不存在或版本不匹配,应用程序将无法启动。

2. 显式链接(Explicit Linking)

这种方式更为灵活,你可以在运行时动态地加载DLL,并在不需要时卸载它。这对于实现插件系统或者处理可选功能模块非常有用。

步骤:

  1. 加载DLL: 使用
    LoadLibrary
    登录后复制
    (Windows) 或
    dlopen
    登录后复制
    (Linux/macOS) 函数加载DLL。
  2. 获取函数地址: 使用
    GetProcAddress
    登录后复制
    (Windows) 或
    dlsym
    登录后复制
    (Linux/macOS) 函数获取你想要调用的函数或类的入口点。这需要你将函数指针声明为正确的类型。
  3. 调用函数: 通过获取到的函数指针进行调用。
  4. 卸载DLL: 当不再需要DLL时,使用
    FreeLibrary
    登录后复制
    (Windows) 或
    dlclose
    登录后复制
    (Linux/macOS) 卸载它。

示例代码 (Windows):

// 应用程序代码 (MyApp_Explicit.cpp)
#include <windows.h> // For LoadLibrary, GetProcAddress, FreeLibrary
#include <iostream>

// 定义函数指针类型,匹配DLL中导出的函数签名
typedef int (*AddFunc)(int, int);
typedef void (*PrintMessageFunc)();
// 对于C++类,显式链接会更复杂,通常会导出工厂函数来创建对象
// 比如:typedef MyClass* (*CreateMyClassFunc)();
//       typedef void (*DestroyMyClassFunc)(MyClass*);


int main() {
    HMODULE hDLL = LoadLibrary(TEXT("MyDLL.dll")); // 加载DLL

    if (hDLL != NULL) {
        // 获取Add函数的地址
        AddFunc pAdd = (AddFunc)GetProcAddress(hDLL, "Add"); // 注意:这里是原始的函数名,因为我们使用了extern "C"

        // 获取PrintMessage函数的地址
        PrintMessageFunc pPrintMessage = (PrintMessageFunc)GetProcAddress(hDLL, "PrintMessage");

        if (pAdd != NULL && pPrintMessage != NULL) {
            int result = pAdd(10, 20);
            std::cout << "Explicit Link: Add(10, 20) = " << result << std::endl;
            pPrintMessage();
        } else {
            std::cerr << "Error: Could not find function in DLL." << std::endl;
        }

        FreeLibrary(hDLL); // 卸载DLL
    } else {
        std::cerr << "Error: Could not load MyDLL.dll. GetLastError(): " << GetLastError() << std::endl;
    }

    return 0;
}
登录后复制

优点: 极高的灵活性,可以动态加载/卸载DLL,实现插件、热更新等功能。应用程序启动时不会强依赖DLL,即使DLL不存在也能正常启动(只是相关功能不可用)。 缺点: 代码相对复杂,需要手动管理DLL的生命周期,并且对于C++类和其成员函数的导出和调用,处理起来会更加繁琐(通常需要导出工厂函数和销毁函数)。

选择哪种方式,真的得看你的具体场景。如果是一个核心功能,DLL是应用程序不可或缺的一部分,隐式链接会更方便。如果是一个可插拔的模块,或者你需要在运行时决定是否加载某个功能,那么显式链接就是你的不二之选。

以上就是如何在C++中创建一个动态链接库_C++动态链接库(DLL)创建与使用的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号