c++中创建与调用dll
好处想必不用说了,所谓的黑盒复用,实现模块化的同时避免源代码暴露等。可以将某一通用功能做成模块,方便复用,同时软件更新时如果只更新了几个模块,可以更换dll即可,无需完整地更新,便于程序拓展。以下教程基于VS2015,其他的版本类似。文章参考链接
1.dll的创建
-
创建项目。在vs中新建win32应用程序,名称为任意给定,如MyDll之类的,概述点击下一步,应用程序设置中的应用程序类型选择DLL(D),完成。
-
添加头文件。在项目-添加新项-**Visaul C++**中选择头文件,设置文件名如MyDll.h。
-
在头文件中添加如下代码:
#ifdef MYDLL_EXPORTS #define MYDLL_API __declspec(dllexport)//注意decl前面是两个下划线 #else #define MYDLL_API __declspec(dllimport) #endif namespace MyDllSpace { //导出类 class MyDllClass { private: double a; public: //导出的函数 MYDLL_API MyDllClass(); MYDLL_API MyDllClass(double a_); MYDLL_API double multiply(double b); MYDLL_API void display(); MYDLL_API static void conbine(MyDllClass m1, MyDllClass m2); }; }
注意: 因为该头文件既被dll的源文件包含,又被使用方包含,所以开头使用条件编译——当被dll源文件包含时(#ifdef MYDLL_EXPORTS,因为vs生成的DLL项目默认都定义了
DLL名称_EXPORTS
这个宏),导出的函数前要加__declspec(dllexport)
宏;如果是被使用方包含,则导出函数前要加__declspec(dllimport)
宏 -
在源文件中添加如下代码:
#include "MyDll.h" #include <iostream> MyDllSpace::MyDllClass::MyDllClass() { a = 0; } MyDllSpace::MyDllClass::MyDllClass(double a_) { a = a_; } MYDLL_API double MyDllSpace::MyDllClass::multiply(double b) { return a*b; } MYDLL_API void MyDllSpace::MyDllClass::display() { std::cout << "a=" << a << std::endl; } MYDLL_API void MyDllSpace::MyDllClass::conbine(MyDllClass m1, MyDllClass m2) { std::cout << "(" << m1.a << "," << m2.a << ")" << std::endl; }
注意: 这里的函数是通过在头文件对函数快速操作在源文件里自动生成的定义(当然函数体还是得自己写),自己码的话可以用命名空间MyDllSpace把这些定义括起来,这样不必每次都用命名空间限定是哪一个空间里的函数定义了
-
点击菜单栏的生成-生成解决方案,即创建dll完成,在项目的Debug目录下可以看到生成的dll和lib文件。
2.dll的使用
-
点击文件-新建-项目,选择win32控制台应用程序,名称任意设,如UseMyDll,确定,下一步,在应用程序设置中将附加选项中的预编译头多选框去除选中,完成。
-
将上一步生成的dll、lib、.h头文件复制到项目目录中,在解决方案资源管理器中添加现有项将其加入项目。
-
在UseMyDll.cpp文件中添加如下代码:
#include <iostream> #include "MyDll.h"//加入模块的头文件 using namespace std; int main() { double a = 8; MyDllSpace::MyDllClass c(4);//创建模块里的类实例 MyDllSpace::MyDllClass c2; c.display();//使用实例方法 cout << "8*4=" << c.multiply(a) << endl; MyDllSpace::MyDllClass::conbine(c, c2);//使用模块里的类的静态方法 system("pause");//暂停以便能看到输出 return 0; }
输出如下:
注意:
- 如果新建项目时选择将项目添加到解决方案,即在dll解决方案下新建方案,则可以添加dll的引用——直接右击解决方案资源管理器里的引用,添加引用,将之前创建的dll添加引用(这也是官方教程里的用法)——而不用将dll放到当前文件夹下,并添加到项目里。但是因为我们假设的是更一般的情况,复用的代码要求对方非得在自己项目下不太现实。
- 头文件也不是必须添加到当前项目里,只是为了方便找到。也可以在项目-属性-C/C++-常规下的附加包含目录里添加头文件所在目录,甚至可以把头文件放在系统包含目录里,只要包含头文件时能找到就可以。另外.lib文件也不是必须加到项目下,也可以通过添加.lib所在文件夹为库目录,然后将.lib文件添加为附加依赖项。
3.仅使用dll(显式链接)
上述方法为使用自己写的dll,但是如果只有别人给的一个dll不含其它任何信息怎么使用?步骤1,2中DLL调用方式为隐式链接(调用),另外有一种显式链接,不需要头文件和lib文件。显式链接可以在程序执行过程中随时加载DLL也可以随时卸载,更具有灵活性,适合解释型语言,当然使用也要更麻烦些。VS在VC\bin目录下提供了一个dumpbin.exe程序来查看DLL文件中的函数结构。参考链接。
-
如步骤1创建一个dll项目
头文件代码如下:
//FuncOnlyDll.h #ifdef FUNCONLYDLL_EXPORTS #define FUNCONLYDLL_API __declspec(dllexport) #else #define FUNCONLYDLL_API __declspec(dllimport) #endif namespace Funcs { FUNCONLYDLL_API double minValue(double a, double b); FUNCONLYDLL_API double maxValue(double a, double b); FUNCONLYDLL_API double add(double a, double b); FUNCONLYDLL_API double subtract(double a, double b); }
源代码如下:
//FuncOnlyDll.cpp #include "FuncOnlyDll.h" FUNCONLYDLL_API double Funcs::minValue(double a, double b) { return a < b ? a : b; } FUNCONLYDLL_API double Funcs::maxValue(double a, double b) { return a > b ? a : b; } FUNCONLYDLL_API double Funcs::add(double a, double b) { return a + b; } FUNCONLYDLL_API double Funcs::subtract(double a, double b) { return a - b; }
点击菜单栏中生成-生成解决方案,打开项目目录生成dll所在的文件夹,使用dumpbin.exe查看下dll中函数结构,如下图:
可以看到有四个函数,但是显示的就像乱码,顶多只能看出每个函数叫啥,这样对于使用的用户来说是不友好的,因为看一堆乱码是无法知道这些函数是干啥的,有哪些参数和返回值,这样是无法使用的,我们可以在菜单栏项目-添加新项-Visaul C++-代码选择模块定义文件(.def),设置文件名,在添加的def文件中给出如下代码:
LIBRARY EXPORTS [email protected]@@[email protected] [email protected]@@[email protected] [email protected]@@[email protected] [email protected]@@[email protected]
其中后面的乱码对应之前dump出来的dll函数名乱码,写这个def文件相当于把乱码重新定义名字,让它更好识别,这里我定义的规则是
返回类型_函数名_参数列表类型
,当然你可以定义自己的规则,这样使用者一看就知道这个函数干啥的,有啥参数和返回值,再dump一下dll看看:
发现后面四个函数输出了我们想要的结果,但是前面仍然有四个乱码函数(其实是同样四个函数),可以将头文件中的*#define FUNCONLYDLL_API __declspec(dllexport)改成#define FUNCONLYDLL_API*这样就不会输出前面的乱码(但是这样会使得dll使用隐式链接出错):
至此,dll创建完毕。
-
新建一个项目使用创建的dll
显式链接是不用配置项目属性的,直接将dll文件放在项目目录下,或是其他能查找到的目录比如系统path路径之类的,代码如下:
#include <iostream> //为了使用LoadLibrary和FreeLibrary等函数以及LPCWSTR和LPCSTR类型加入的头文件 #include <Windows.h> using namespace std; //定义要加载的各种函数指针,通过dumpbin查看dll函数中函数签名从而得知,或者查看API typedef double(*pMin)(double, double); typedef double(*pMax)(double, double); typedef double(*pAdd)(double, double); typedef double(*pSub)(double, double); int main() { LPCWSTR dllName = TEXT("FuncOnlyDll.dll"); LPCSTR funName1 = "double_minValue_double_double"; LPCSTR funName2 = "double_maxValue_double_double"; LPCSTR funName3 = "double_add_double_double"; LPCSTR funName4 = "double_subtract_double_double"; HMODULE hDll = LoadLibrary(dllName);//加载dll if (hDll != NULL) { pMin fp1 = (pMin)(GetProcAddress(hDll, funName1));//加载函数 if (fp1 != NULL) { cout << "min value of 5 and 6:" << fp1(5, 6) << endl; } else { cout << "函数min加载失败" << endl; } pMax fp2 = (pMax)(GetProcAddress(hDll, funName2)); if (fp2 != NULL) { cout << "max value of 5 and 6:" << fp2(5, 6) << endl; } else { cout << "函数max加载失败" << endl; } pAdd fp3 = (pAdd)(GetProcAddress(hDll, funName3)); if (fp3 != NULL) { cout << "sum value of 5 and 6:" << fp3(5, 6) << endl; } else { cout << "函数add加载失败" << endl; } pSub fp4 = (pSub)(GetProcAddress(hDll, funName4)); if (fp2 != NULL) { cout << "difference of 5 and 6:" << fp4(5, 6) << endl; } else { cout << "函数sub加载失败" << endl; } FreeLibrary(hDll);//卸载dll,一般用完就卸载,避免占用内存 } else { cout << "加载dll文件失败" << endl; } system("pause"); return 0; }
从上述使用方法可以看出,因为我们之前创建dll文件的时候贴心地把导出的函数名设置了下,才可以在dumpbin中查看到比较有意义的函数名信息,所以如果第三方给的dll没有这么做,那么你就根据dump出来的函数名乱码猜吧,祝你好运。
-
显式链接的不足
显然,如果只给了dll没给接口(头文件或者相应的API文档),而且dump出来函数名是乱码,是没法复用dll的。另外dll的显式链接是不支持导出类和变量的,至少没人建议这么做。
4.一点小的建议
经过看完网上各种教程强烈建议如果dll复用的话一定使用.h,.lib,.dll三件套,即隐式链接方式
其他任何解决办法都不好使,因为dll隐藏了文件中的函数结构,不知道函数结构是没法用的
即使通过dumbin.exe也只能探知到一些看起来像乱码的函数名,是无法了解这个函数具体干啥的
除非已经知道了接口,比如隐式链接,使用了dll对应源码的头文件,显然是暴露接口的一种方式