C++生成与调用dll文件初探
为什么要用dll文件
动态数据库(Dynamic Link Library, DLL)文件使程序的封装调用变得方便,项目的协同工作也能够更加独立化。仅需定义好接口,在任何需要使用该模块功能的程序都可直接引用。常与dll文件一同提及的是lib文件。lib文件分为两类,静态链接库中的lib文件实际包含了所需封装模块的代码,并将在编译时将代码加入程序一同编译;动态链接库中的lib文件则只提供一个索引功能,就像函数的声明,功能实现则有dll文件提供。这就意味着,使用dll文件至少能带来以下优势:
- 降低程序的耦合度,增加模块的复用效率;
- 模块的更新仅需替换dll文件,不必对程序进行重新编译;
- dll文件有利于程序代码的隐藏,调用者无法得知模块动能的具体实现。
dll文件的创建
笔者使用Visual Studio 2017开发环境为例,简单介绍下最简单的函数的导出。
首先在VS中新建一个动态链接库(dll)项目,可以看到项目默认生成了pch.h等4个文件。作为最简单的示例,可以先删除它们,暂时不用,自带的这几个文件应该是微软出于优化等目的所用。
首先建立头文件,用于声明所要导出的接口。
// myDll.h #ifdef MY_DLL_API #else #define MY_DLL_API extern "C" _declspec(dllimport) // extern "C" 以C的方式导出,保证了接口名称不变 #endif MY_DLL_API double add(double a, double b); MY_DLL_API double minus(double a, double b);
在建立cpp文件编写函数的实现
// myDll.cpp #include "myDll.h" double add(double a, double b) { return add2(a) + b; } double minus(double a, double b) { return a - b; }
生成即可在项目路径的对应文件夹(Debug或Release)下找到所需的lib和dll文件。使用dll查看工具即可查看dll文件的接口。
也可以使用vcvars32.bat,打开命令提示符转至vcvars32.bat路径下,并以此输入以下代码(当然也可以直接使用VS的开发人员命令提示符,这样就可以直接转至dll所在路径用dumpbin命令了)
cd C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build vcvars32.bat dumpbin -exports “D:\code\VS\20220724_testDll\myDll\Release\myDll.dll”
可以看到所导出的接口名称都是正确的。若没有明确声明extern "C",则由于C和C++间对函数编译转换的不同,会改变函数名,主要可能是考虑到C++函数重载的问题。
也可以使用模块定义文件(.def)控制导出。
dll文件的调用
调用dll文件分为显式调用和隐式调用,其中前者可以不用lib文件,而后者需要lib文件。笔者在此先简要介绍下隐式调用。
新建一个控制台工程,并在导入lib文件。cpp文件代码如下
// main.cpp #include <iostream> #pragma comment(lib, "myDll.lib") // 可以在项目中包含lib,此时便无需将lib和dll文件拷贝至项目路径下,也无需手动关联lib extern "C" _declspec(dllimport) double add (double a , double b); extern "C" _declspec(dllimport) double minus(double a, double b); // 要有extern "C" 和原先定义的一致,否则会报错 int main() { double x1, x2; std::cout << "x1 = "; std::cin >> x1; std::cout << "x2 = "; std::cin >> x2; std::cout << "*******************" << std::endl; std::cout << "x1 + x2 = " << add(x1, x2) << std::endl; std::cout << "x1 - x2 = " << minus(x1, x2) << std::endl; std::cout << "*******************" << std::endl; system("pause"); }
运行结果为
找到程序的运行文件所在,更改dll文件可以直接更改对应的模块功能。如原先的add函数更改为
double add2(double a) { return a * a; } double add(double a, double b) { return add2(a) + b; }
生成的新dll文件直接替换,运行main.exe,可以看到结果已经改变。
类的封装
可以看到,上文为了保证函数名不变,使用了C的编译方法,然而当需要封装类时就出问题了。有人认为应避免直接导出类,不过如果确实出于各种需要,如需要保证数据结构整体性,也应当要使用的。
使用以下代码生成dll
// Cal.h #include <iostream> #ifdef MY_DLL_API #else #define MY_DLL_API _declspec(dllimport) // extern "C" 以C的方式导出,保证了接口名称不变 #endif class MY_DLL_API Cal { public: Cal(); ~Cal(); void SetPara(unsigned int _n,double *_x); double Add(); double Minus(); private: double *x; unsigned int n; };
// Cal.cpp #include "Cal.h" Cal::Cal() { x = nullptr; n = 0; std::cout << "creat Cal class" << std::endl; } Cal::~Cal() { std::cout << "distory Cal class" << std::endl; } void Cal::SetPara(unsigned int _n, double *_x) { if (n != _n) { if (x) delete x; x = new double[_n]; n = _n; } for (int ii = 0; ii < n; ii++) { *(x + ii) = _x[ii]; } } double Cal::Add() { double result = 0; for (int ii = 0; ii < n; ii++) { result += x[ii]; } return result; } double Cal::Minus() { double result = 0; for (int ii = 0; ii < n; ii++) { result -= x[ii]; } return result; }
在调用时有一些些不同,除了lib文件和dll文件,还需要使用h文件。由于C++特性丰富,接口需要根据头文件明确。
#include <iostream> #include "Cal.h" // class MY_DLL_API Cal; // 该句在h文件中已声明,因此无需 int main() { Cal t; double x[] = { 1, 3, 5 }; t.SetPara(3, x); std::cout << "add: " << t.Add() << std::endl; std::cout << "minus: " << t.Minus() << std::endl; std::cout << "*******************" << std::endl; double y[] = { 1, 2}; t.SetPara(2, y); std::cout << "add: " << t.Add() << std::endl; std::cout << "minus: " << t.Minus() << std::endl; std::cout << "*******************" << std::endl; system("pause"); return 0; }
然而,这样的封装和调用需要暴露出私有成员,这有时是开发者不愿意的,因此需要采取一定手段来隐藏私有成员。常用的方法可以有工厂模式和Pimpl风格,其中工厂模式又有简单工厂、工厂方法、抽象工厂等诸多类别,此外也有网友提出了一些自己的方法,都是值得借鉴的。此处笔者介绍一个基于简单工厂模式的封装。
首先定义一个工厂类作为基类,这是一个可以给其他使用者看到的源代码,其中仅定义了需要开放的接口,并将接口定义为虚函数。此外又定义了一个函数以获取对象地址。值得注意的是,基类的析构函数使用了虚析构函数,这是因为对象实际是由子类实例化的,然而返回给用户的还是基类类型,若不使用虚析构函数则销毁对象时仅调用基类的析构函数,反之才能调用子类的析构函数,正确释放内存空间。
// Cal_public.h #pragma once #ifdef MY_DLL_API #else #define MY_DLL_API _declspec(dllimport) #endif class Cal_base { public: virtual ~Cal_base() {}; // 虚析构函数使得:在删除指向子类对象的基类指针时调用子类析构,防止内存泄露 virtual void SetPara(unsigned int _n, double *_x) = 0; virtual double Add() = 0; virtual double Minus() = 0; }; // 也可以单独定义一个类以获取接口 //class MY_DLL_API Cal_factor //{ //public: // Cal_base * Get_Cal(); //}; // 使用函数获取接口 MY_DLL_API Cal_base * Get_Cal();
Cal.h文件也要做出相应的改变:
// Cal.h #pragma once #include <iostream> #include "Cal_public.h" class Cal : public Cal_base { // ... }; Cal_base * Get_Cal() { return new Cal(); }
这样就只需将编译所得的lib文件、dll文件并Cal_public.h一块给使用者即可。此时dll的调用变为:
// main.cpp #include <iostream> #include "Cal_public.h" int main() { //Cal_factor tt; //Cal_base *t = tt.Get_Cal(); Cal_base *t = Get_Cal(); double x[] = { 1, 3, 5 }; // ...以下类似... delete t; system("pause"); return 0; }
此处显式地销毁了t,能够对比在工厂类中析构函数是否为虚函数时的不同。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)