C++动态库封装及调用

一直对动态库的封装理解不是很透彻,虽然之前写过一个Demo,不过并没有真正的理解。所以写下来,帮助自己理解下。

1、一个程序从源文件编译生成可执行文件的步骤:

预编译 -->  编译 -->  汇编 --> 链接

(1)预编译,即预处理,主要处理在源代码文件中以“#”开始的预编译指令,如宏展开、处理条件编译指令、处理#include指令等。

(2)编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。

(3)汇编是将汇编代码转变成二进制文件。

(4)链接将二进制文件链接成一个可执行的命令,主要是把分散的数据和代码收集并合成一个单一的可加载并可执行的的文件。链接可以发生在代码静态编译、程序被加载时以及程序执行时。链接过程的主要工作是符号解析和重定位。

2、库 

库是一组目标文件的包,就是一些最常用的代码编译成目标文件后打包存放。而最常见的库就是运行时库(Runtime Library),如C运行库CRT.

库一般分为两种:静态库(.a 、.lib)动态库(.so 、.dll )所谓静态、动态是指链接过程。 

3、静态库与动态库

区别:

(1)lib是编译时用到的,dll是运行时用到的。如果要完成源代码的编译,只需要lib;如果要使动态链接的程序运行起来,只需要dll。
(2)如果有dll文件,那么lib一般是一些索引信息,记录了dll中函数的入口和位置,dll中是函数的具体内容;如果只有lib文件,那么这个lib文件是静态编译出来的,索引和实现都在其中。使用静态编译的lib文件,在运行程序时不需要再挂动态库,缺点是导致应用程序比较大,而且失去了动态库的灵活性,发布新版本时要发布新的应用程序才行。
(3)动态链接的情况下,有两个文件:一个是LIB文件,一个是DLL文件。LIB包含被DLL导出的函数名称和位置,DLL包含实际的函数和数据,应用程序使用LIB文件链接到DLL文件。在应用程序的可执行文件中,存放的不是被调用的函数代码,而是DLL中相应函数代码的地址,从而节省了内存资源。DLL和LIB文件必须随应用程序一起发行,否则应用程序会产生错误。如果不想用lib文件或者没有lib文件,可以用WIN32 API函数LoadLibrary、GetProcAddress装载。

------这里主要讲动态库的优点特性。--------

静态库:函数和数据被编译进一个二进制文件(通常扩展名为.LIB)。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件(.EXE文件)。
在使用动态库的时候,往往提供两个文件:一个引入库和一个DLL。引入库包含被DLL导出的函数和变量的符号名,DLL包含实际的函数和数据。在编译链接可执行文件时,只需要链接引入库,DLL中的函数代码和数据并不复制到可执行文件中,在运行的时候,再去加载DLL,访问DLL中导出的函数。

静态库有两个重大缺点:

1)空间浪费

2)静态链接对程序的更新、部署和发布会带来很多麻烦。一旦程序中有任何模块更新,整个程序就要重新链接,发布给用户。

动态链接的基本思想:把程序按照模块拆分成各个相对独立的部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是想静态链接一样把所有的程序模块都链接成一个单独的可执行文件。

特点:

1)代码共享,所有引用该动态库的可执行目标文件共享一份相同的代码与数据。

2)程序升级方便,应用程序不需要重新链接新版本的动态库来升级,理论上只要简单地将旧的目标文件覆盖掉。

3)在运行时可以动态地选择加载各种应用程序模块

下面重点介绍Windows下动态链接库DLL.

DLL即动态链接库(Dynamic-Link Libaray)的缩写,相当于Linux下的共享对象。Windows系统中大量采用了DLL机制,甚至内核的结构很大程度依赖与DLL机制。Windows下的DLL文件和EXE文件实际上是一个概念,都是PE格式的二进制文件。一般的动态库程序有lib文件和dll文件,lib文件是编译时期连接到应用程序中的,而dll文件是运行时才会被调用的。

为了更好的理解DLL,首先介绍一下导出和导入的概念。

(1)导出与导入

在ELF(Linux下动态库的格式),共享库中所有的全局函数和变量在默认情况下都可以被其他模块使用,即ELF默认导出所有的全局符号。DLL不同,需要显式地“告诉”编译器需要导出某个符号,否则编译器默认所有的符号都不导出。

程序使用DLL的过程其实是引用DLL中导出函数和符号的过程,即导入过程。对于从其他DLL导入的符号,需要使用“__declspec(dllimport)”显式声明某个符号为导入符号。在ELF中,使用外部符号时,不需要额外声明该符号是从其他共享对象导入的。

指定符号的导入导出一般有如下两种方法:

1)MSVC编译器提供了一系列C/C++的扩展来指定符号的导入导出,即__declspec属性关键字

__declspec(dllexport) 表示该符号是从本DLL导出的符号

__declspec(dllimport) 表示该符号是从别的DLL中导入的

2)使用“.def”文件来声明导入到导出符号,详细参考《程序员的自我修养--链接、装载与库》。

应用程序使用DLL可以采用两种方式:一种是隐式链接(调用),另一种是显式链接。在使用DLL之前首先要知道DLL中函数的结构信息
4、DLL创建

C++动态库封装及调用_第1张图片

C++动态库封装及调用_第2张图片

下面是头文件内容:创建工程时有默认的导出函数,这里将其删除掉重新写的。

// 下列 ifdef 块是创建使从 DLL 导出更简单的
// 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 MYDLL_EXPORTS
// 符号编译的。在使用此 DLL 的
// 任何其他项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将
// MYDLL_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的
// 符号视为是被导出的。
#ifdef MYDLL_EXPORTS
#define MYDLL_API __declspec(dllexport)
#else
#define MYDLL_API __declspec(dllimport)
#endif

extern "C" MYDLL_API double seekArea(int r, int h);

这里将seek函数声明为导出函数;

当定义了符号MYDLL_EXPORTS,MYDLL_API被设置为 __declspec(dllexport)修饰符,This modifier enables the function to be exported by the DLL so that it can be used by other applications。若未定义则TMYDLL_API被设置为__declspec(dllimport)This modifier enables the compiler to optimize the importing of the function from the DLL for use in other applications。当DLL项目生成时,MYDLL_EXPORTS默认是定义的,所以默认设置的是__declspec(dllexport) 修饰符。 

对应源文件的内容:

// myDLL.cpp : 定义 DLL 应用程序的导出函数。
//封装圆柱体的体积

#include "stdafx.h"
#include "stdio.h"
#include "myDLL.h"

void show(){
	printf("Call the library function.\n");
	printf("***************************\n");
}
double area(int r){
	return 3.14*r*r;
}

MYDLL_API double seekArea(int r, int h){
	show();
	double under = 3.14*r*r;
	double v = under*h;
	return v;
}

然后编译就会生成对应的dll文件,同时也会生成对应的lib文件。 
注意 :a.DLL中导出函数的声明有两种方式:在函数声明中加上__declspec(dllexport);采用模块定义(.def)文件声明。详见: http://www.cnblogs.com/enterBeijingThreetimes/archive/2010/08/04/1792099.html  
  b.对于C文件创建dll时或者想使用C编译器创建dll时,建议使用 extern “C” 标志,参见 extern "C"的简单解析。

C++动态库封装及调用_第3张图片

5.DLL的隐式调用

隐式链接采用静态加载的方式,比较简单,需要.h、.lib、.dll三件套。新建“控制台应用程序”或“空项目”。配置如下: 
项目->属性->配置属性->VC++ 目录-> 在“包含目录”里添加头文件testdll.h所在的目录 
项目->属性->配置属性->VC++ 目录-> 在“库目录”里添加头文件testdll.lib所在的目录 
项目->属性->配置属性->链接器->输入-> 在“附加依赖项”里添加“testdll.lib”(若有多个 lib 则以空格隔开) 。 //你也可以在项目属性中设置库的链接,#pragma comment(lib, "DLLSample.lib")


库文件头文件等目录设置,本文将库文件及头文件拷贝到工程目录下DLL文件夹下

添加LIB依赖项


#调用的源程序#

// callmyDLL.cpp : 定义控制台应用程序的入口点。
//包含头头文件,函数声明


#include "stdafx.h"
#include "stdlib.h"
#include "myDLL.h"
extern "C" _declspec(dllimport)  double seekArea(int r, int h);


int _tmain(int argc, _TCHAR* argv[])
{
	int r = 1, h = 5;
	double area = seekArea(r, h);
	printf("Area is:%f\n", area);
	system("pause");
	return 0;
}


运行时最后一步:将动态库文件拷贝到可执行文件目录下,否则会出现如下错误。


6、DLL显示调用

对于显示连接,即动态加载我们需要调用LoadLibrary
在MSDN中:HMODULE WINAPI LoadLibrary(
  __in  LPCTSTR lpFileName
);
它的功能是映射一个可执行模块到调用进程的地址空间。由此我们知道显示调用就是函数指针来调用函数。

Steps:
1、声明头文件,说明我想用windows32方法来加载和卸载DLL
2、然后用typedef定义一个指针函数类型.typedef  void(*fun) //这个指针类型,要和你调用的函数类型和参数保持一致
3、定一个句柄实例,用来取DLL的实例地址。HINSTANCE hdll;
格式为hdll=LoadLibrary(“DLL地址”);这里字符串类型是LPSTR,当是unicode字符集的时候会不行,
因此要在配置-属性-常规里面把默认字符集“unicode”改成支持多字符扩展即可。
4、取的地址要判断,返回的句柄是否为空,如果为无效句柄,那么要释放加载DLL所占用的内存。
5、定义一个函数指针,用来获取你要用的函数地址。
  然后通过GetProcAdress来获取函数的地址,参数是DLL的句柄和你要调用的函数名:比如:FUN=(fun)GetProcAdress(hdll,"sum");
  这里也要判断要函数指针是否为空,如果没取到要求的函数,那么要释放句柄。
6、然后通过函数指针来调用函数。
7、调用结束后,就释放句柄FreeLibrary(hdll);

直接上代码,一一一一一目了然

// callDLLSee.cpp : 定义控制台应用程序的入口点。
//通过调用windowsAPI 来加载和卸载DLL
#include "stdafx.h"
#include "Windows.h"
typedef double(*Dllfun)(int , int);

int _tmain(int argc, _TCHAR* argv[])
{
	Dllfun funName;
	HINSTANCE hdll;
	//put DLL under the Debug path 
	//use   _T 设置为宽字符
	hdll = LoadLibrary( _T("myDLL.dll"));
	if (hdll == NULL)
	{
		FreeLibrary(hdll);
	}
	funName = (Dllfun)GetProcAddress(hdll, "seekArea");
	if (funName == NULL)
	{
		FreeLibrary(hdll);
	}
	int r = 1, h = 10;
	double area = funName(r, h);
	printf("area = %f\n", area);

	FreeLibrary(hdll);
	return 0;
}


你可能感兴趣的:(C/C++之路)