c++动态库生成与调用

一、生成动态库(含头文件、不含头文件)

以生成dllTest.dll为例(工程名为dllTest、 头文件名为dllTest.h、 源文件名为dllTest.cpp)

1.1 不含头文件的动态库

我们生成的动态库想要被别人调用,那么一定要将想要被调用的函数导出,使用_declspec(dllexport)进行导出。
//dllTest.cpp
_declspec(dllexport) int add(int a, int b)
{
	return a+b;
}

_declspec(dllexport) int sub(int a, int b)
{
	return a-b;
}

编译之后,在工程目录的Debug目录下我们可以看到以下的几个文件,分别是
得到上述的文件之后,我们就可以进行调用该动态链接库了。其中,dll文件是包含了函数具体实现的可执行文件;lib文件是导入库文件,主要包含的是函数名和符号名。我们可以用vs提供的dumpbin工具查看生成的动态库中导出的函数以及名字。我的vs安装在C:\Program Files (x86)\Microsoft Visual Studio 10.0,进入该文件夹后点击VC进入该目录,可以看到一个vcvarsall.bat的文件。依次点击Microsoft Visual Studio 2010 -> visual studio tools ->Visual Studio Prompt 2010,如下图所示:
即可打开一个vs的命令框,将vcvarsall.bat文件拖拽进入该命令框中,将得到下图所示的内容:

然后我们将得到的dllTest.dll文件放入到VC文件夹中,输入dumpbin -exports dllTest.dll即可看到该动态库导出的函数以及经过c++编译器修饰后的函数名字。

椭圆所示的即为导出的函数以及其经过修饰后的名字。(因为c++要支持函数重载以及命名空间等,因此需要将函数进行修饰)。

1.2 动态库调用(隐式连接、动态连接)

1.2.1 隐式连接

首先建立一个工程,配置工程属性,具体的步骤如下:
(1)新建一个win32控制台应用程序,工程名dllCall;
(2)配置工程属性,添加对动态库dllTest.lib的引用:
a.  工程项目右键->属性->链接器->gengeral->附加库目录,在其中添加导入库dllTest.lib所在的文件目录,如下图:

b.工程项目右键->属性->链接器->输入->附加依赖项,在其中添加导入库dllTest.lib,如下图:


(3)此时,我们就可以在我们的工程中对该动态库进行调用了。

调用函数dllCal.cppl如下:
<pre name="code" class="cpp">// dllCall.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
using namespace std;

extern int add(int, int );                             //告诉编译器,add函数是在该源文件外部定义的函数
_declspec(dllimport) int sub(int, int);//告诉编译器,sub函数是从动态库导入的函数
//这两种方式都可以正常的调用,但是下面的相对来说加载的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 5, b = 3;
	cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
	cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
	return 0;
}


 
 (4)编写好之后,编译、链接都能通过,但是运行的时候会出错,因为调用函数不知道int add(int, int ) 以及int sub(int, int)的可执行文件(dllTest.dll)在哪,因此当调用该函数时,就找不到具体的执行的代码,因而就会报错。此时,我们只需将dllTest.dll放入到调用函数的.exe文件所在的目录中即可,结果如下图: 
上述的两种形式都能正常的调用。

1.2.2 动态调用

上述的隐式调用需要我们在工程属性中配置一些动态库导入库(dllTest.lib)的目录以及名称,会很麻烦,有时候我们也会忘记配置这些属性或者当动态库较多的时候有遗漏,都会导致函数链接的时候出现unresolve external symbol的错误。而且,动态调用还有一个优点就是,什么时候需要调用动态库的函数的时候什么时候加载该动态库,这样就不必在程序运行开始时加载所需的所有的动态库,这样也能加快启动的速度。此外,我们仅仅只需要一个dllTest.dll文件即可。
(1)我们首先删掉工程属性中 “附加库目录”以及“附加依赖项”中我们输入的内容。
(2)修改相应的调用代码,如下:
// dllCall.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include<Windows.h>
using namespace std;

//extern int add(int, int );                             //告诉编译器,add函数是在该源文件外部定义的函数
//_declspec(dllimport) int sub(int, int);//告诉编译器,sub函数是从动态库导入的函数
//这两种方式都可以正常的调用,但是下面的相对来说加载的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 5, b = 3;
	HINSTANCE hInst = LoadLibraryA("dllTest.dll");
	typedef int (*pFun)(int, int);//定义一个函数指针类型pAdd
	pFun add = (pFun)GetProcAddress(hInst,"?add@@YAHHH@Z");
	cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
	pFun sub = (pFun)GetProcAddress(hInst,"?sub@@YAHHH@Z");
	cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
	return 0;
}
(3)程序运行正常,结果同上面的结果。

2.含头文件的动态库

我们一般用编写一个动态库,同时也会提供一个头文件方便调用者使用,因为调用者一般情况下很难知道我们编写的动态库具体导出的函数。而且,我们不想想上面的函数调用一样,还要自己写_declspec(dllimport) int add(int, int)或者extern int add(int, int);这是我们需要向调用者提供一个头文件完成这项工作,是调用变得更加方便。

头文件:
//dllTest.h
#ifdef DLLTEST_API
#else 
#define DLLTEST_API _declspec(dllimport) 
#endif

DLLTEST_API int add(int, int);
DLLTEST_API int sub(int, int);
</pre><pre name="code" class="cpp"><span style="font-family: Arial, Helvetica, sans-serif;">	</span><span style="font-family: Arial, Helvetica, sans-serif;">源文件:</span>
<pre name="code" class="cpp">//dllTest.cpp
#define DLLTEST_API _declspec(dllexport)
#include "dllTest.h"

DLLTEST_API int add(int a, int b)
{
	return a+b;
}

DLLTEST_API int sub(int a, int b)
{
	return a-b;
}


 
 

2.1 函数调用(隐式链接、动态链接)

2.1.1 隐式链接

(1) 新建工程,配置工程属性(添加导入库目录、导入库),具体的参照上面的隐式调用。此外,还要将动态库头文件所在的目录加入到属性中:  在  c\c++  ->  gengeral  ->  additional include Directories  加入dllTest.h所在的目录。  如下图:
(2)修改调用函数,具体的代码如下:
// dllCall.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include "dllTest.h"
using namespace std;

//extern int add(int, int );                             //告诉编译器,add函数是在该源文件外部定义的函数
//_declspec(dllimport) int sub(int, int);//告诉编译器,sub函数是从动态库导入的函数
//这两种方式都可以正常的调用,但是下面的相对来说加载的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 5, b = 3;
	cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
	cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
	return 0;
}
(3)编译、链接、运行正常,结果如上面。此外,我们也可以不用在属性->链接器->输入->附加库目录中添加dllTest.lib, 通过在调用函数中添加#pragma comment(lib,"dllTest.lib")即可。具体代码为:
// dllCall.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include "dllTest.h"
#pragma comment(lib,"dllTest.lib")
using namespace std;

//extern int add(int, int );                             //告诉编译器,add函数是在该源文件外部定义的函数
//_declspec(dllimport) int sub(int, int);//告诉编译器,sub函数是从动态库导入的函数
//这两种方式都可以正常的调用,但是下面的相对来说加载的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 5, b = 3;
	cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
	cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
	return 0;
}

2.1.2 动态链接

动态链接同上面的一样。

3 不改变名字的导出库

但是我们发现,通过动态链接时,那个函数的名字经过c++编译器修饰后很复杂,刚开始并不熟悉修饰的规则(后面讲修饰规则)。因此,如果我们需要先用dumpbin工具查询该动态库导出的函数及其名字,才方便调用。那么,怎么才能使导出的函数名字不发生变化呢,同我们定义的函数名一样,这样我们在动态调用时更加方便。具体的方法有两种:一是采用extern "C";二是用模块定义文件.def。

3.1 extern "C"

3.1.1 默认调用方式_cedel

以含头文件的动态库为例进行修改,修改后的头文件和源文件如下:
//dllTest.h
#ifdef DLLTEST_API
#else 
#define DLLTEST_API extern "C" _declspec(dllimport) 
#endif

DLLTEST_API int add(int, int);
DLLTEST_API int sub(int, int);


//dllTest.cpp
#define DLLTEST_API extern "C" _declspec(dllexport)
#include "dllTest.h"

DLLTEST_API int add(int a, int b)
{
	return a+b;
}

DLLTEST_API int sub(int a, int b)
{
	return a-b;
}

进行编译得到相应的.dll、.lib文件,通过dumpbin工具,我们可以查看导出函数的名字,如下:

红色部分为 没有加extern "C" 导出的函数名,它经过了c++编译器的修饰。
绿色部分为添加了extern "C" 导出的函数名,它的意思是告诉编译器,以C的方式导出函数。

3.1.2 更改调用方式 _stdcall 

但是如果改变了调用方式,通过这种方式进行导出,函数的名字依旧会改变,我们采用_stdcall调用方式,也就是

WINAPI,后面讲为什么window API 函数都采用该种调用方式

(1)编写_stdcall调用方式的动态库(以含头文件方式为例)
相应的头文件和源文件如下:
//dllTest.h
#ifdef DLLTEST_API
#else 
#define DLLTEST_API extern "C" _declspec(dllimport) 
#endif

DLLTEST_API int _stdcall add(int, int);
DLLTEST_API int _stdcall sub(int, int);

//dllTest.cpp
#define DLLTEST_API extern "C" _declspec(dllexport)
#include "dllTest.h"

DLLTEST_API int _stdcall add(int a, int b)
{
	return a+b;
}

DLLTEST_API int _stdcall sub(int a, int b)
{
	return a-b;
}
编译后利用dumpbin工具查看导出函数的名字如下:

红色的为采用c\c++默认调用方式导出的函数名字;
绿色的为采用_stdcall 调用方式导出的函数名字。至于为什么会是这样的名字在后面的名字修饰规则中进行说明。我们发现他的名字还是改变了。

3.2 模块定义文件.def

3.2.1 默认调用方式(_cedel)动态库

(1)新建一个Win32程序,选择一个动态库程序,勾选空工程。
(2)修改相应的代码(源文件、模块定义文件),具体的如下:
源文件:
//dllTest.cpp
 int  add(int a, int b)
{
	return a+b;
}

int  sub(int a, int b)
{
	return a-b;
}

模块定义文件:
LIBRARY dllTest

EXPORTS
add
sub
其中,LIBRARY 后面要与生成的动态库的名称相同,EXPORTS下面写需要导出的函数(add、sub),它会自动与你的源文件中的函数进行匹配。也可以用 add1 = add、 sub1 = sub 这样的方式来改变导出函数的名字。
(3)通过dumpbin工具查看动态库的导出函数,如下图(分别是没改名字以及改名字后的):




3.2.2 采用_stdcall,也就是WINAPI调用方式生成动态库

(1) 源文件
//dllTest.cpp
 int  _stdcall add(int a, int b)
{
	return a+b;
}

int  _stdcall sub(int a, int b)
{
	return a-b;
}

(2)模块定义文件
LIBRARY dllTest

EXPORTS
add
sub

(3)利用dumpbin查询动态库导出的函数


3.3 动态库调用

3.3.1 动态调用

动态调用同上,只是变了一个地方,如下:
<span style="white-space:pre">	</span>pFun add = (pFun)GetProcAddress(hInst,"add");
<span style="white-space:pre">	</span>pFun sub = (pFun)GetProcAddress(hInst,"sub");
也可以使用每个函数前面的编号,如add函数的编号为1 、sub函数的编号为2。只需要将上述函数的第二个参数改为
MAKEINTATOM(1)

3.3.2 隐式调用

<1> 动态库与函数库都采用_cedel调用方式
(1) 配置工程属性,加入附加库目录;(附加依赖项使用 #pragma comment(lib,"testdll.lib"代替)
(2)调用函数如下:
// dllCall.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#pragma comment(lib,"testdll.lib")
using namespace std;


_declspec(dllimport) int add(int, int);
_declspec(dllimport) int sub(int, int);
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 5, b = 3;
	cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
	cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
	return 0;
}
(3)编译、链接正常,结果正确。

<2>动态库函数采用_stdcall调用方式,调用方采用默认的调用方式(_cedel)
(1) 新建工程,配置工程属性,加入附加库目录; (附加依赖项使用#pragma comment(lib,"testdll.lib"代替)
(2)编写调用函数,如下:
// dllCall.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#pragma comment(lib,"testdll.lib")
using namespace std;


_declspec(dllimport) int add(int, int);
_declspec(dllimport) int sub(int, int);
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 5, b = 3;
	cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
	cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
	return 0;
}
(3)编译、链接正常,运行出错。


这是因为_stdcall是被调用的函数自己清理栈空间,而_cedel则是调用者清理栈。上述的调用方式会使栈得到两次清理,使得函数的返回地址、ebp寄存器的值被更改从而导致失败。通过观察其生成的汇编语言就可以看到(下面的主要的,并不是全部):

main函数在调用add函数之后清理的栈,而在add函数中,可以清楚的看到,add函数在运行完之后自己清理的栈。


因此,导致栈数据出现错误。

c\c++编译器修饰规则和函数调用中栈的变化后面有时间在写!!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值