有维护性问题。不方便修改库中实现,修改后所有使用静态库的都要重新链接一遍
因为静态库相当于copy源码,所以如果a.exe 和b.exe 都用的同一套静态库,这两个exe中含有大量重复代码。
先把程序运行起来,再将库链接进来——动态链接
exe在可执行程序中没有相应库的代码,当软件运行起来再把库代码拷到内存。
所以使用动态库的软件不需要重新链接,修改动态库即可修复库的问题(即头疼医头,不用把整个身体链接一遍)。Windows中动态链接库通常为.dll 文件
缺点是启动慢,如果缺库则软件无法正常运行,静态库则相反
1.dll 创建
生成的工程中 dllmain 并不是必须的,可以删掉。不影响使用
dllmain 主要是 dll 被加载时立即执行的一段代码,用于初始化和监听是否被加载或卸载而已。
dll 主要还是作为库,提供函数给人调用的作用。
这里依然把其他多余的全删了,只创建两个, MyDll.h 和 MyDll.cpp

先来最简单的
MyDll.h :
#pragma once _declspec(dllexport) int Plus(int x, int y); _declspec(dllexport) int Sub(int x, int y); _declspec(dllexport) int Mul(int x, int y); _declspec(dllexport) int Div(int x, int y);
讯享网
_declspec(dllexport) 关键字 声明函数为导出函数,声明将函数相关放进后面说到的导出表中,作为导出函数给他人使用
MyDll.cpp:
讯享网#include "MyDll.h" int Plus(int x, int y) { return x + y; } int Sub(int x, int y) { return x - y; } int Mul(int x, int y) { return x * y; } int Div(int x, int y) { return x / y; }
同样点击平时的编译运行按钮,提示无法运行(本来就无法单独运行),但已成功生成.dll 文件,去到 Debug 或 Release 目录下即可看到,注意同样还有个.lib文件,我们后面会用到
2.dll 调用
分为动态使用(显式调用)和静态使用(隐式调用)
前者可以在调用者代码中任意位置自定义的加载进内存,即如果调用者没运行到那段代码,那 dll 就一直不被加载进内存,可以动态的使用。而这种调用方式需要显式的在代码中写出加载 dll 的代码。
后者就是通过编译选项和设置,使得被调程序只要运行就立即加载 dll 进内存,无法动态灵活的使用。因为调用者程序中没有显式的加载 dll 的代码,因而也叫隐式调用。
2.1 动态使用(显式调用)
同样新建一个带main的控制台项目工程。只需将dll 拷贝进目录下。
#include<stdio.h> #include<windows.h> int main() { // dll显式调用的测试 * 这里只需将相应dll拷贝到目录下即可,无需拷贝.h,.lib 与 调资源文件 //关于无法将参数1从"const char *"转换为"LPCWSTR" 的错误,见 项目->属性->常规->字符集 调整为 未设置 或 多字节 //如果还是编译不过 查看项目->属性->C++->预处理器->预处理定义中是否有UNICODE //定义函数指针 typedef int(* lpPlus)(int, int);//这里声明的函数参数类型需与.h中一致 lpPlus myPlus; //动态加载dll到内存中 HMODULE hMod = LoadLibrary("MyDll.dll"); if (hMod == nullptr) { printf("load dll error!"); system("pause"); return 0; } myPlus = (lpPlus)GetProcAddress(hMod, "?Plus@@YAHHH@Z"); //这里的函数名使用Dependency Walker查看,必须正确 if (myPlus == nullptr) { printf("load func error!"); system("pause"); return 0; } //调用函数 int a = myPlus(10, 2); printf("%d", a); return 0; }
动态调用主要通过 LoadLibrary 获取 加载dll 并获取其地址,后通过 GetProcAddress 通过 dll 句柄和函数名或者函数序号(后面会说)获取真正的函数地址,得到地址,套上和该函数格式一模一样的函数指针。即可成功调用。
首先,编译器通过名称粉碎(Name-Mangling)来达到作用域控制和函数重载的目的。所以,C++中的所有函数在编译后,编译器都会加上返回类型、调用约定、参数类型等来唯一标识一个函数。是编译器的内部规则,所以不具有普适性。所以放入导入表中,这个函数名就变了,这里变成?Plus@@YAHHH@Z,可以用 Dependency Walker 或 CFF Explorer 来查看。显示调用中通过GetProcAddress获取函数地址,参数的函数名必须和这个名称粉碎后的名字相同。由此这样的显示调用非常麻烦,后面有说不用名称粉碎的方法。
因为导出表中不止含有函数名,所以 GetProcAddress 也可以通过函数序号获取到函数地址。
2.2 静态使用(隐式调用)
还记得前面在生存dll 时还自动生成了个.lib 吗,这个lib是动态lib,里面没有实现代码,只有函数名、序号和dll名,同样的lib里的函数名依然是被名称粉碎过的,把这个 .lib 也拷贝到调用者目录下。
然后再把之前写的头文件拷贝到目录下。
然后上代码
讯享网#include<stdio.h> #include<windows.h> // dll隐式调用的测试 * #include "MyDll.h" //将.h文件复制到目录下则可用该条语句,include了则不需要再声明dll中的函数 #pragma comment(lib,"MyDll.lib") //这里就是lib没有错,不是.dll 在项目的"资源文件"中添加相应lib与dll则不用这一条 int main() { // dll隐式调用的测试 * printf("%d", Plus(2,3)); return 0; }
同样,#pragma comment(lib,"MyDll.lib") 这一条的有无参考上一章lib中使用/不使用资源文件的调用。
注意静态使用(隐式调用)只能导出名字的函数使用,序号的不能使用
运行后可顺利显示结果,证明调用成功。

3.其他变式与拓展答疑
前面提到可以更方面的使用显示调用,无需打开其他工具查看名称粉碎后的结果了
方法一可以通过加入 extern "C" 关键字,意为使用C语言的格式,那么就没有C++的名称粉碎规则了。(但注意,C语言仍然有自己的名称粉碎规则)如下格式声明和定义的函数,在 dll 导出表中名称和原名一致。
声明:
extern "C" _declspec(dllexport) int Plus(int x, int y);
实现:
讯享网extern "C" int Plus(int x, int y) { return x + y; }
那么这样生成的 dll ,函数名和原名一致。但如果调用规则变化为__stdcall。即实现为:
extern "C" int __stdcall Plus(int x, int y) { return x + y; }
声明为
讯享网extern "C" _declspec(dllexport) int __stdcall Plus(int x, int y);
则生成的 dll 函数名依然会被在C语言规则下的名称粉碎弄乱名称。使用工具查看导出函数名,一样是被粉碎过的。
方法二可以通过添加 def 文件,在源文件->添加->新建项->搜索def,即可添加def文件。
或者在文件中直接弄个空文件改成.def,然后在添加到项目的源文件中 也可以。
但第二种方法添加到项目的源文件后,需要在如下项目属性的这里,填入添加的def的文件名才能正常使用

def 文件中写入下列文本:(开头的 LIBRARY 可有可无,这里没写)
EXPORTS Plus @12 Sub @15 NONAME Mul @13 Div @16
即可无视各类名称粉碎规则,输出的导出函数名必然是表上的名字,后面的@12为规定函数名为序号, NONAME 为无名字导出,这个函数就只能以序号方式调用,另外写入Plus=AA 可以更换导出函数名为AA。具体更多字段可自行查阅def文本规则。
由于静态使用(隐式调用)只能以导出名字的函数使用,所以设置为NONAME,就无法静态使用该函数了。
通过上面可以知道,C++的文件通过声明 extern "C" 即可生成双方都能调用的库:C++使用仍然要加上extern "C",C语言直接使用即可。而C生成的库,C可以直接调用,C++需加上extern "C",但C++生成的库若不加该关键字,则只能C++使用。所以一般做成C的库,两者都能通用。但 extern "C" 只在C++有,C是没有这个用法的。所以为了通用,给使用者的头文件可以写成以下形式:
讯享网#ifdef __cplusplus extern "C" { #endif void fun1(); void fun2(); extern int g_num; #ifdef __cplusplus } #endif
如果需要在上述框架下加入 全局变量,也可以,但要在变量前加上extern关键字,就像上面的g_num; 全局变量的用法和函数一致,本质都只是一个地址,并且也会受到名称粉碎的影响。
还有一点,如果需要再导出C++类。由于头文件写的类是含有__declspec(dllexport)的,而这是个导出类,放到调用者那调用会报错,因此需要在调用者的.cpp中拷贝一份.h中的类定义并把__declspec(dllexport) 改成 __declspec(dllimport) 。但这样会报重定义错误。因此需要在头文件中定义宏:
#ifdef _USRDLL #define CLASS_DEF_declspec(dllexport) #else #define CLASS_DEF _declspec(dllimport) #endif class CLASS_DEF Ctest { … }
这样两边就都能用了,dll 编译时头文件中的这个看作导出类,但.h头文件到了dll使用者那里又变成了导入
另外,由于.lib文件中只记录了.dll的名字,但并没有记录dll的路径,而是通过一个默认的搜索规则,先搜索当前路径,再搜索系统目录,再搜索环境变量的。所以存在 dll 劫持的安全问题。即dll 可以按照这个规则被人为替换掉。而里面所有函数都可能会被动手脚。
※※※※
都说有了 def 就不用extern "C" 了,两种方法都只改变了函数名,使用起来应该是一致的。
但注意到一个问题,dll 导出函数使用 extern "C" 与 使用 def 文件而不使用 extern "C" 相比。两种方法在 dll 中的导出函数名一致,extern "C" 在调用者处使用extern "C" 声明就能正常使用。
但是DEF文件弄的导出函数,C++隐式调用起来声明extern "C" 会报链接错误(可见C使用者也会报错,实验证明的确这样)。证明这两者还是有区别的。
是因为 DLL文件里的函数名是 def 文件指定的,但是它对应的 lib 文件则不是。既 lib 文件中的函数名仍然被名称粉碎了!
所以 无extern "C" 且用 def 文件弄出的动态库,无法给C语言调用者 静态使用。因为C语言认不出 lib 中被粉碎的名字。(C++使用者在声明中不加 extern "C" 即可)
但因为只涉及到 lib 文件,所以C语言使用者仍然可以通过动态使用(显式调用)的方式使用 dll 的

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/75308.html