C++面向对象程序设计——24.dll(动态链接库)的创建和使用

一、 核心概念:什么是DLL,我们为什么用它?

DLL (Dynamic Link Library),即动态链接库,是包含了可由多个程序同时使用的代码和数据的库。把它想象成一个“功能工具箱”,任何需要这些功能的程序都可以随时调用。

DLL的主要优势 (笔试可能考):

  1. 代码复用与模块化:将通用的功能(如数学计算、文件操作)封装在DLL中,多个应用程序可以共享,避免重复编写代码。
  2. 节省内存:当多个程序使用同一个DLL时,该DLL在内存中通常只会加载一份,所有程序共享这一个实例。这与静态库不同,静态库会把代码完整地复制到每个程序中,造成内存冗余。
  3. 易于维护与升级:如果DLL中的功能需要更新或修复Bug,理论上只需替换这个DLL文件即可,而不需要重新编译所有使用它的应用程序(前提是接口保持不变)。

第二部分:C风格DLL的创建与使用 (基础必会)

这是最基本、兼容性最好的DLL形式。它只导出独立的C函数。

1. 如何创建C风格DLL

目标:创建一个名为 mydll.dll 的库,它导出两个函数:int add(int n)float value(float v1, float v2)

步骤1:创建项目
在Code::Blocks等IDE中,新建一个项目,选择模板为 Dynamic Link Library (动态链接库)

步骤2:编写头文件 mydll.h
这个头文件既要给DLL自己用(导出函数),也要给调用它的程序用(导入函数)。

  • __declspec(dllexport):告诉编译器,这个函数要从DLL中“导出”,给别人用。
  • __declspec(dllimport):告诉编译器,这个函数是从外部DLL中“导入”的。
  • extern "C":这是C++项目中的关键。它告诉C++编译器,请用C语言的方式来处理这些函数名,不要进行C++特有的“名称修串”(Name Mangling),否则外部程序将找不到原始的函数名。
// mydll.h

#ifndef MYDLL_H_INCLUDED
#define MYDLL_H_INCLUDED

// 通过宏定义来区分是DLL项目自身编译,还是被外部调用
#ifdef BUILDING_MYDLL
#define MYDLL_API __declspec(dllexport)
#else
#define MYDLL_API __declspec(dllimport)
#endif

// 使用 extern "C" 来保证函数名在DLL中不被改变
#ifdef __cplusplus
extern "C" {
#endif

MYDLL_API int add(int n);
MYDLL_API float value(float v1, float v2);

#ifdef __cplusplus
}
#endif

#endif // MYDLL_H_INCLUDED

步骤3:编写源文件 mydll.cpp
在这里实现函数的具体功能。

// mydll.cpp

// 定义这个宏,使得 MYDLL_API 展开为 __declspec(dllexport)
#define BUILDING_MYDLL

#include "mydll.h"

int add(int n) {
    return n + 100;
}

float value(float v1, float v2) {
    return v1 * v2;
}

步骤4:编译项目
编译成功后,你会在项目的输出目录(通常是 bin/Debugbin/Release)下得到三个非常重要的文件:

  • mydll.dll:动态链接库文件,程序运行时需要它。
  • libmydll.a (或在Visual Studio下是 mydll.lib):静态导入库,在“直接调用”时需要它来帮助链接器找到函数。
  • mydll.h:头文件,给调用方项目使用。

2. 如何使用C风格DLL

使用DLL主要有两种方式:直接调用间接调用

方式一:直接调用 (隐式链接/静态加载)

思路:在编译时就将程序和DLL“关联”起来,调用DLL函数就像调用普通本地函数一样简单。

步骤:

  1. 新建一个应用程序项目 (例如,控制台应用 UseDll.exe)。
  2. 配置项目
    • mydll.h 复制到你的项目目录中,并在 main.cpp#include 它。
    • 在项目的 Linker settings (链接器设置) 中,添加 libmydll.a (或 .lib) 文件。这是为了在编译链接阶段,让编译器知道 addvalue 函数的地址信息在DLL里。
  3. 编写代码 main.cpp:直接像调用普通函数一样调用DLL中的函数。
// main.cpp

#include <iostream>
#include "mydll.h" // 包含DLL的头文件

int main() {
    int result_add = add(88); // 直接调用
    float result_val = value(2.5, 1.2); // 直接调用

    std::cout << "add(88) = " << result_add << std::endl;
    std::cout << "value(2.5, 1.2) = " << result_val << std::endl;

    return 0;
}
  1. 部署运行:编译生成 UseDll.exe 后,必须将 mydll.dll 文件拷贝到与 UseDll.exe 相同的目录下,然后才能成功运行。

优点:编码简单,调用直观。
缺点:程序启动时必须找到DLL,否则无法运行。

方式二:间接调用 (显式链接/动态加载)

思路:程序在运行时才去加载DLL并获取函数地址,更加灵活。

步骤:

  1. 新建一个应用程序项目。这次不需要配置链接器,也不需要 .lib 文件。
  2. 编写代码 main.cpp
    • 使用Windows API函数 LoadLibrary() 来加载DLL。
    • 使用 GetProcAddress() 来获取DLL中函数的地址。
    • 使用 FreeLibrary() 在用完后释放DLL。
    • 为了方便调用,通常会用 typedef 定义一个和DLL中函数签名完全一致的函数指针类型。
// main.cpp

#include <iostream>
#include <windows.h> // 必须包含此头文件

// 定义函数指针类型,必须与DLL中函数的原型完全一致
typedef int (*FuncType_add)(int);
typedef float (*FuncType_value)(float, float);

int main() {
    // 1. 加载DLL
    HINSTANCE hDll = LoadLibrary("mydll.dll");
    if (hDll == NULL) {
        std::cout << "Error: Failed to load mydll.dll" << std::endl;
        return 1;
    }

    // 2. 获取函数地址
    FuncType_add add_func = (FuncType_add)GetProcAddress(hDll, "add");
    FuncType_value value_func = (FuncType_value)GetProcAddress(hDll, "value");

    if (add_func == NULL || value_func == NULL) {
        std::cout << "Error: Failed to get function address" << std::endl;
        FreeLibrary(hDll); // 别忘了释放
        return 1;
    }

    // 3. 通过函数指针调用函数
    int result_add = add_func(88);
    float result_val = value_func(2.5, 1.2);

    std::cout << "add(88) = " << result_add << std::endl;
    std::cout << "value(2.5, 1.2) = " << result_val << std::endl;

    // 4. 释放DLL
    FreeLibrary(hDll);

    return 0;
}
  1. 部署运行:同样,将 mydll.dll 文件拷贝到与 UseDll.exe 相同的目录下。

优点:非常灵活,程序可以在运行时根据条件决定是否加载、加载哪个DLL,常用于实现插件(Plug-in)系统
缺点:编码相对复杂,需要手动管理DLL的加载和释放。


第三部分:C++风格DLL (进阶核心)

直接在DLL中导出整个C++类是可行的,但存在严重问题,因此有一种更推荐的、基于接口的模式。

1. 不推荐的方法:直接导出C++类

做法:在类的定义前直接加上 __declspec(dllexport)

// mydll.h - 不推荐的方式
class MYDLL_API Cat {
public:
    void setAge(int age);
    int getAge() const;
private:
    int m_age; // 私有成员
};

使用:和直接调用C函数类似,链接 .lib 文件,#include 头文件,然后直接创建 Cat 对象。

致命缺点 (笔试高频考点)

  1. 编译器和版本强绑定:DLL和调用它的EXE必须使用完全相同的编译器和相同版本进行编译,否则会因为C++ ABI(应用二进制接口)不兼容导致程序崩溃。
  2. 实现细节暴露:类的所有私有成员(如 int m_age;)都定义在头文件中,完全暴露给了调用者,破坏了类的封装性。
  3. 依赖地狱:如果Cat类继承了其他类,或者其成员变量是另一个自定义类,那么这些相关的类也必须全部导出,非常麻烦。

结论在实际项目中,应极力避免使用这种方法。

2. 推荐的方法:基于“纯虚接口 + 工厂函数” (机试核心)

这是现代C++中跨模块传递对象的标准做法,它稳定、安全且灵活。

思路

  • 我们不导出具体的实现类 Cat
  • 我们定义一个只包含纯虚函数的接口类 ICat,并在DLL中导出它。
  • 我们导出一个C风格的工厂函数 createCat(),这个函数负责在DLL内部创建 Cat 的实例,并返回一个指向接口 ICat 的指针。
  • 调用方通过这个接口指针来操作对象,完全不知道具体实现类的存在。

步骤1:创建推荐风格的DLL

  • 修改头文件 mydll.h
    • 定义接口类 ICat,它只有纯虚函数和一个虚析构函数。
    • 声明一个用 extern "C" 修饰的工厂函数。
// mydll.h - 推荐的方式

#ifndef MYDLL_H_INCLUDED
#define MYDLL_H_INCLUDED

#include <memory> // 为了使用智能指针 std::shared_ptr

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

// 1. 定义接口类
class ICat {
public:
    virtual ~ICat() {} // 接口类必须有虚析构函数!
    virtual void setAge(int age) = 0;
    virtual int getAge() const = 0;
    virtual void passYears(int years) = 0;
};

// 2. 声明C风格的工厂函数,返回一个指向接口的智能指针
#ifdef __cplusplus
extern "C" {
#endif

// 使用智能指针可以自动管理内存,防止内存泄漏
MYDLL_API std::shared_ptr<ICat> createCat();

#ifdef __cplusplus
}
#endif

#endif // MYDLL_H_INCLUDED
  • 创建内部实现类 (cat.h, cat.cpp) - 这部分不给调用者
    • cat.h:定义一个具体的 Cat 类,它继承 ICat 并实现所有接口。
    • cat.cpp:编写 Cat 类的具体实现。
// cat.h (DLL内部使用,不公开)
#include "mydll.h"
class Cat : public ICat {
public:
    virtual ~Cat() override {}
    void setAge(int age) override;
    int getAge() const override;
    void passYears(int years) override;
private:
    int m_age = 0;
};
  • 实现工厂函数 (在 mydll.cpp 中)
    • 在这个函数里,我们 new 一个具体的 Cat 对象,然后把它包装在智能指针里返回。
// mydll.cpp
#define BUILDING_MYDLL
#include "mydll.h"
#include "cat.h" // 包含内部实现

std::shared_ptr<ICat> createCat() {
    return std::make_shared<Cat>();
}

步骤2:使用推荐风格的DLL

  • 配置项目:与“直接调用”方式相同,需要链接 libmydll.a (或 .lib)。
  • 编写代码 main.cpp
    • #include "mydll.h",但这次头文件里只有接口和工厂函数。
    • 调用工厂函数 createCat() 获取一个指向对象的接口指针。
    • 通过这个接口指针调用所有方法。
// main.cpp
#include <iostream>
#include "mydll.h" // 只需包含这个对外接口头文件

int main() {
    // 1. 调用工厂函数获取对象实例
    std::shared_ptr<ICat> my_cat = createCat();

    // 2. 通过接口指针操作对象
    my_cat->setAge(5);
    std::cout << "Cat's age is: " << my_cat->getAge() << std::endl; // 输出 5

    my_cat->passYears(3);
    std::cout << "After 3 years, cat's age is: " << my_cat->getAge() << std::endl; // 输出 8

    // 3. 不需要手动释放内存,shared_ptr会在离开作用域时自动处理
    return 0;
}

优点 (笔试高频考点)

  1. 完美封装,隐藏实现:调用者只知道 ICat 接口,完全不知道内部的 Cat 类是如何实现的。
  2. 二进制兼容性好:由于导出的是C函数(工厂函数),并且通过虚函数表(v-table)来调用成员函数,这是一种非常稳定的跨模块调用方式,基本摆脱了编译器版本的束缚。
  3. 维护性极佳:只要接口 ICat 和工厂函数不变,你可以随意修改DLL内部 Cat 类的实现,甚至换一个完全不同的类,而调用程序完全不需要改动,也不需要重新编译。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱看烟花的码农

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值