新建--win32项目--DLL--空项目,在源文件目录下新建一个cpp文件,随便编写两个简单函数,代码如下:
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
然后按 F7 生成,结果如下:
生成了一个 dll 文件,但是这个文件并没有声明函数导出,可以借助 dumpbin.exe 程序来查看 dll 文件是否有函数导出以及导出了哪些函数。
在 D:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin 目录下有一个 vcvars32.bat 批处理程序,用于暂时配置使用例如 dumpbin.exe 类似的程序,打开命令提示符 cmd,切换到上面新建的动态链接库项目的 Debug 目录下:
然后直接将 vcvars32.bat 拖拽到cmd中:
Enter 回车运行,没有任何其他显示,则运行正常:
然后输入 dumpbin,回车:
说明dumpbin.exe 程序可以正常运行,现在回车后继续,使用 dumpbin -exports *.dll 命令来查看 dll 文件的函数导出情况:
如上图,dll 文件在没有函数导出声明的情况下是没有任何函数导出的。需要加入如下代码:
_declspec(dllexport) int add(int a, int b)
{
return a + b;
}
_declspec(dllexport) int subtract(int a, int b)
{
return a - b;
}
这时重新生成后,如下:
多了一个引入库文件 lib 文件和一个 exp 文件(在使用动态库的时候没用,编译链接的中间文件)。
这时,再运行一下dumpbin 看看结果:
这样就说明 dll 文件可以成功的导出函数了,ordinal是函数的序号,hint是标识(然并卵),RVA是地址,name函数名加入和一些符号,因为C++中支持函数重载,为了区分相同名字的函数的用处。并且,不同的C++编译器对于这种名字的改编方式不一样。
现在我们编写客户端程序来调用刚刚编写的动态链接库文件,同样新建一个简单的控制台程序:
#include <iostream>
using namespace std;
extern int add(int a, int b);
extern int subtract(int a, int b);
int main()
{
cout << add(5, 3) << endl;
return 0;
}
因为 add 和 subtract 函数都是外部调用,因此用 extern 关键字来声明这两个函数,但是在生成的时候报出了 Link 错误:
之所以会这样是因为在编译的时候,因为提前对两个函数做出了声明,所以没有编译错误,但是在链接的时候,编译器无法找到这两个函数是在什么位置导出的。
现在我们将之前在 Dll-Test 项目中生成的 Dll-Test.lib 引入库文件拷贝到 Dll-Client 目录下,并且在Dll-Client 项目的属性设置中加入对 Dll-Test.lib 文件的引用:
再次生成,就没有报错了:
并且,我们也可以借助 dumpbin 工具来查看我们的 Dll-Client 程序使用了那些动态链接库,在cmd 中切换到Dll-Client 目录下:
我们可以看到,Dll-Client.exe 这个程序使用到了那些 dll 文件。
但是,当我们运行程序的时候,还是会弹出错误信息:
因为,我们的程序不知道去哪里寻找 Dll-Test.dll 这个动态链接库文件,VS只会在默认的路径下去寻找 dll 文件,于是我们再将 Dll-Test.dll 文件拷贝到 Dll-Client 目录下。这时便可以正常运行程序了:
我们也可以使用另外一种方式来对 add 和 subtract 这两个函数做出声明,来指出他们是从动态链接库中引入的:
#include <iostream>
using namespace std;
_declspec(dllimport) int add(int a, int b);
_declspec(dllimport) int subtract(int a, int b);
int main()
{
cout << add(5, 3) << endl;
return 0;
}
同样是可以得到正常的运行结果。对于调用的函数是动态链接库中的函数时,使用后者声明,会生成运行效率更高的程序。
但是一般情况下,我们编写好的动态链接库,可能会被其他客户端使用,因此我们在 Dll-Test 项目中制作一个头文件,这样头文件和源文件代码如下:
_declspec(dllimport) int add(int a, int b);
_declspec(dllimport) int subtract(int a, int b);
<pre name="code" class="cpp">#include <iostream>
#include "..\..\Dll-Test\Dll-Test\dll.h"//具体路径视情况而定
using namespace std;
int main()
{
cout << add(5, 3) << endl;
return 0;
}
同样也是可以正常运行的。但是这个头文件仅仅是给客户端程序所使用,我们可以再对头文件进行修改,使得它即可以给客户端使用,也可以给 动态链接库的源文件使用,Dll-Test 项目中的头文件和源文件如下(注意:头文件不参与编译,源文件才参与编译):
#ifdef DLL_API
#else
#define DLL_API _declspec(dllimport)
#endif
DLL_API int add(int a, int b);
DLL_API int subtract(int a, int b);
#define DLL_API _declspec(dllexport)
#include "dll.h"
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
dll 中设计一个类:
#ifdef DLL_API
#else
#define DLL_API _declspec(dllimport)
#endif
DLL_API int add(int a, int b);
DLL_API int subtract(int a, int b);
class DLL_API Point
{
public:
void output(int x, int y);
};
#define DLL_API _declspec(dllexport)
#include "dll.h"
#include "iostream"
using namespace std;
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
void Point::output(int x, int y)
{
cout << "x*y=" << x*y << endl;
}
客户端程序:
#include <iostream>
#include "..\..\Dll-Test\Dll-Test\dll.h"
using namespace std;
int main()
{
Point pt;
pt.output(4, 5);
cout << add(5, 3) << endl;
return 0;
}
关于 C++ 编译器的名字改编问题,如果不同的编译器,可能名字改编的方式不一样,这样dll库的函数给其他编译器环境使用可能就不行,我们希望在编写 dll 库函数的时候不做名字改编,可以这样修改代码:
#ifdef DLL_API
#else
#define DLL_API extern "C" _declspec(dllimport)
#endif
DLL_API int add(int a, int b);
DLL_API int subtract(int a, int b);
#define DLL_API extern "C" _declspec(dllexport)
#include "dll.h"
#include "iostream"
using namespace std;
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
这样生成的 dll 文件也可以用dumpbin 来查看一下导出函数的名字:
可以看到,导出的函数名字没有加入其他的字符。
还有一种情况就是,虽然现在将名字改编问题解决了,但是有些语言使用的函数调用约定不同,比如delphi 使用的是Pascal 调用约定,即_stdcall 调用约定,如果将dll中的函数强行修改为 _stdcall 调用约定,即使使用 extern “C” 也会使导出的函数名发生变换,下面对几种导出的函数名做出对比:
直接导出:
_declspec(dllexport) int add(int a, int b)
{
return a + b;
}
C编译器导出:
extern "C" _declspec(dllexport) int add(int a, int b)
{
return a + b;
}
_stdcall 调用约定导出:
extern "C" _declspec(dllexport) int _stdcall add(int a, int b)
{
return a + b;
}
所以,如果我们要让delphi 编译器使用C++编译器所导出的 dll 动态库函数,这种导出方法还是不能使用,我们必须使用模块定义的方法,即添加一个 .def 文件,来导出源文件中的函数,def 文件以及源文件的代码如下:
LIBRARY Dll-Test2
EXPORTS
add
需要注意的是,Dll-Test2这个文件名字,必须要跟dll文件的名字相同。
源文件:
int add(int a, int b)
{
return a + b;
}
添加def文件后,要在链接器中指明模块定义:
导出的函数名:
C编译器导出:
extern "C" int add(int a, int b)
{
return a + b;
}
_stdcall 标准库导出:
extern "C" int _stdcall add(int a, int b)
{
return a + b;
}
这样,即使使用 _stdcall 的调用约定也可以使导出的函数名不发生改变。
关于静态库和动态库的比较:
静态库:函数和数据被编译进一个二进制文件(通常扩展名为.lib)。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把他们和应用程序的其他模块组合起来,创建最终的可执行文件.exe文件。
动态库:在使用动态库的时候,往往提供两个文件:一个引入库和一个 dll。引入库包含被 dll 导出的函数和变量的符号名,dll 包含实际的函数和数据。在编译链接可执行文件时,只需要链接引入库,dll 中的函数代码和数据并不复制到可执行文件中,在运行的时候,再去加载 dll,访问 dll 中导出的函数。
而动态库的调用又分为两种:显示链接和隐式链接,之前介绍的都是隐式链接(需要.lib和.dll 两个文件),而显示链接只需要.dll 文件,接下来介绍显示链接。
我们将客户端 Dll-Client 项目中的依赖项去掉:
动态库的函数导出只要是采用无名字改编的方式都可以,客户端的源文件代码如下:
<pre name="code" class="cpp">#include <iostream>
#include <windows.h>
#include <tchar.h>
#include "..\..\Dll-Test\Dll-Test\dll.h"
using namespace std;
int main()
{
HINSTANCE hInst;
hInst = LoadLibrary(_T("Dll-Test.dll"));
typedef int (*ADDPROC)(int a, int b);
ADDPROC Add = (ADDPROC)GetProcAddress(hInst, "add");
if (!Add)
{
cout << "函数导出失败!" << endl;
FreeLibrary(hInst);
return 0;
}
cout << Add(5, 3) << endl;
FreeLibrary(hInst);
return 0;
}
这样我们在使用动态链接库的时候就不需要.lib文件了。另外,如果 dll 导出函数使用的是 _stdcall 标准函数调用约定,那么在客户端程序要做如下修改:
typedef int (_stdcall *ADDPROC)(int a, int b);