序言:本文主要讲解静态链接库和动态链接库的区别,以及怎么样编译和引用两种库,怎么样从DLL中导出函数和导出C++类。
一、静态链接库和动态链接库
1.静态链接库(.LIB):函数和数据被编译进一个二进制文件。发布时,只需要发布这个可执行文件,并不需要发布被使用的静态库。
2.动态库(.DLL):在使用动态库的时候,往往提供两个文件:一个引入库(.lib)文件和一个DLL(.dll)。虽然引入库和静态库的后缀名相同,但是差别很大。对于一个DLL来说,其引入库文件包含该DLL导出的函数和变量的符号名,而.dll文件包含DLL的实际的函数和数据。在使用动态链接库的情况下,在编译链接可执行文件时,只需要DLL的引入库文件,而在运行可执行程序时,需要加载所需要的DLL,发布产品时,需要发布调用的动态链接库。
二、静态链接库的创建和引用
a.创建静态链接库
打开VS2008,创建一个Win32控制台应用程序,选择静态链接库选项,不加预编译头文件。如下:
新建一个头文件add.h和源文件add.cpp
add.h
#ifndef ADD_H
#define ADD_H
int add(int a, int b);
#endif // ADD_H
add.cpp
#include "add.h"
int add(int a, int b) {
return a + b;
}
然后编译,在Debug目录下生成LIB1.lib
b.引用静态链接库
首先创建一个测试程序test,调用代码如下:
#include "../LIB1/add.h"
#include <stdio.h>
extern int add(int a, int b);
int main() {
int a = 2;
int b = 1;
printf("a=%d, b=%d\n", a,b);
printf("add: %d\n", add(a,b));
getchar();
return 0;
}
如果直接编译,会出现如下的错误,主要原因是找不到要引用的lib文件,下面讲解引用的方法
引用方法一:当有该lib项目的时候使用。右键单击测试项目,选择引用,弹出下面的框
单击确定后即可。
引用方法二:拖放的方法,该方法只针对第三方库。将要引用的lib文件复制到该项目的debug目录下,用鼠标将该lib文件拖放到资源文件夹下。
引用方法三:配置方法。对于带有配置文件的第三方库。将lib文件复制到debug目录下,设置项目的属性,找到链接的输入选项,在附加依赖项中添加要引用的库,这里为:.\debug\LIB1.lib
三、动态链接库的创建和引用
a.动态链接库的创建
导出函数
首先在VS2008中创建一个空的DLL1项目,编写两个函数,分别为加法和减法。
int add(int a,int b)
{
return a+b;
}
int subtract(int a,int b)
{
return a-b;
}
对上面的代码进行编译,在该项目的debug目录下生成DLL1.dll文件。
这时候外部的程序还不能对该DLL进行调用,主要是因为该DLL没有导出函数。这里使用Dumpbin命令来查看一个DLL有哪些导出函数。在命令行下输入 dumpbin命令,如下:
该命令在VS安装目录的bin文件夹下面
注明:如果无法执行该命令,则在该界面下执行VCVARS32.bat文件。每次启动新的命令需要运行该文件。
查看该DLL提供的导出函数:dumpbin -exports DLL1.dll
上面显示没有导出函数。从DLL导出函数需要在每一个将要被导出的函数前面添加标志符:_desclspec(dllexport)
修改后的函数:
_declspec(dllexport)int add(int a,int b)
{
return a+b;
}
_declspec(dllexport)int subtract(int a,int b)
{
return a-b;
}
编译之后,在debug目录下会生成两个文件,引入库DLL1.lib和DLL1.dll文件。然后重新查看该DLL的导出函数。
从上图可以看到多出了一些信息,ordinal表示导出函数的序号,hint表示提示码,RVA列出的地址值是导出函数在DLL模块中的位置,即通过该地址可以在DLL中找到它们;name列出了导出函数的名字。由于C++支持函数重载,对于重载的函数名是一样的,为了方便区别,在编译连接的时候,C++会按照自己的规则修改函数的名字,称为“名字编写”。
引用该DLL
首先为了让编译器知道这两个函数,需要用extern 声明,如下:
#include <stdio.h>
extern int add(int a, int b);
extern int subtract(int a,int b);
int main() {
int a = 2;
int b = 1;
printf("a=%d, b=%d\n", a,b);
printf("add: %d\n", add(a,b));
printf("subtract: %d\n", subtract(a,b));
getchar();
return 0;
}
编译出现如下错误:
这里产生三个错误是由于程序链接的时候产生的。为了解决这个问题,需要将DLL的引入库文件DLL1.lib文件复制到test工程目录下。并且设置附加依赖项.\debug\DLL1.lib。
重新编译,发现没有错误。然后运行该测试程序,出现如下错误:
主要原因是该测试程序找不到要加载的DLL1.dll文件,应该将该文件拷贝到test项目所在的目录下,再运行就不会发生错误。
注明:采用dumpbin -imports test.exe 可以查看该程序的输入信息以及其要加载的DLL信息。
采用Depends工具可以查看一个DLL或exe所依赖的动态链接库。
利用_declspec(dllimport)声明外部函数:上面采用的是extern来声明,如果调用的函数来自动态链接库,建议采用_declspec(dllimport),可以生成效率更高的代码。
_declspec(dllimport) int add(int a, int b);
_declspec(dllimport) int subtract(int a,int b);
如何让用户知道该DLL导出了哪些函数呢?在写DLL的时候,应该使用头文件,DLL1.h
#ifndef DLL1_H
#define DLL1_H
#ifdef BUILD_DLL
#define PORT_DLL __declspec(dllexport)
#else
#define PORT_DLL __declspec(dllimport)
#endif
int PORT_DLL add(int a, int b); int PORT_DLL subtract(int a,int b);
#endif // DLL1_H
源文件DLL1.CPP修改如下:
#define BUILD_DLL
#include"DLL1.h"
PORT_DLL int add(int a,int b)
{
return a+b;
}
PORT_DLL int subtract(int a,int b)
{
return a-b;
}
在调用的程序直接包含该头文件#include"DLL1.h"即可。
#include <stdio.h>
#include"DLL1.h"
int main() {
int a = 2;
int b = 1;
printf("a=%d, b=%d\n", a,b);
printf("add: %d\n", add(a,b));
printf("subtract: %d\n", subtract(a,b));
getchar();
return 0;
}
DLL中导出C++类:
首先在DLL1.h的头文件中定义一个类:
#ifndef DLL1_H
#define DLL1_H
#ifdef BUILD_DLL
#define PORT_DLL _declspec(dllexport)
#else
#define PORT_DLL _declspec(dllimport)
#endif
int PORT_DLL add(int a, int b);
int PORT_DLL subtract(int a,int b);
class PORT_DLL Point
{
public:
int Max(int a,int b);
};
#endif // DLL1_H
在DLL1.CPP文件中定义函数:
#define BUILD_DLL
#include"DLL1.h"
PORT_DLL int add(int a,int b)
{
return a+b;
}
PORT_DLL int subtract(int a,int b)
{
return a-b;
}
int Point::Max(int a, int b)
{
return a>=b?a:b;
}
编译之后,将DLL1.lib,DLL1.dll和Dll1.h拷贝到test项目下,然后进行调用:
#include <stdio.h>
#include "DLL1.h"
int main() {
int a = 2;
int b = 1;
printf("a=%d, b=%d\n", a,b);
printf("add: %d\n", add(a,b));
printf("subtract: %d\n", subtract(a,b));
Point pt;
printf("Point: %d\n",pt.Max(a,b));
getchar();
return 0;
}
导出一个类的某个函数:
class /*PORT_DLL*/ Point
{
public:
PORT_DLL int Max(int a,int b);
};
用dumpbin -exports DLL1.dll查看该DLLL导出的函数:
从上图可以看到导出了一个类Point, 以及该类中的Max函数,还有两个函数,但是 他们的函数名字已经发生改变。
名字改编问题:
C++编译器在生成DLL时,会对导出的函数进行名字修改,并且不同的编译器使用的改编规则不同。如果利用不同的编译器分别生成DLL和访问该DLL的客户端程序的话,则访问DLL的导出函数就会出现问题。如果用C++语言编写了一个DLL,那么用C语言编写的客户端访问该DLL的函数就会出现问题。因为后者使用函数原始名称来调用DLL中的函数,而C++编译器已经对该名称进行了改编,所以C语言编写的客户端程序就找不到所需DLL中的函数。
对于C++ 与C中的兼容,我们希望动态链接库在编译的时候,导出函数的名称不要发生变化,可以在定义导出函数时候,加上extern "C",C大写。如下:
#ifndef DLL1_H
#define DLL1_H
#ifdef BUILD_DLL
#define PORT_DLL extern"C" _declspec(dllexport)
#else
#define PORT_DLL extern"C" _declspec(dllimport)
#endif
int PORT_DLL add(int a, int b);
int PORT_DLL subtract(int a,int b);
class PORT_DLL Point
{
public:
int Max(int a,int b);
};
#endif // DLL1_H
编译,用dumpbin查看,发现名字没有发生改变。
extern "C"可以解决C++与C语言之间的相互调用的函数命名问题。但是有一个缺陷,就是不能导出一个类的成员函数。
如果导出函数的调用约定发生改变,即使使用了extern ”C“,该函数的名字仍然会发生改变。
下面采用标准调用约定,即在声明这些函数时添加_stdcall关键字;
#ifndef DLL1_H
#define DLL1_H
#ifdef BUILD_DLL
#define PORT_DLL extern"C" _declspec(dllexport)
#else
#define PORT_DLL extern "C" _declspec(dllimport)
#endif
PORT_DLL int _stdcall add(int a, int b);
PORT_DLL int _stdcall subtract(int a,int b);
/*class PORT_DLL Point
{
public:
int Max(int a,int b);
};*/
#endif // DLL1_H
源文件:
#define BUILD_DLL
#include"DLL1.h"
PORT_DLL int _stdcall add(int a,int b)
{
return a+b;
}
PORT_DLL int _stdcall subtract(int a,int b)
{
return a-b;
}
/*int Point::Max(int a, int b)
{
return a>=b?a:b;
}*/
编译然后查看:
从上图可以看到,add函数的名字变为_add@8,在add前面有一个下划线,后面跟一个@,接着是数字8,该数字表示add函数的参数所占的字节数,因为有两个int 参数。
通过上图可以发现,如果函数的调用约定发生了变化,即使在声明这些导出函数时使用了extern "C",它的名字仍然会发生变化。C语言和Delphi语言的调用约定不一样,后者使用pascal调用约定,即标准调用约定_stdcall.解决这种问题,可以定义一个模块定义文件(DEF)来解决名字改编问题。
使用DEF文件导出函数。首先创建一个新项目DLL2,在 DLL2.cpp中添加代码:
int add(int a,int b)
{
return a+b;
}
int subtract(int a,int b)
{
return a-b;
}
在该工程目录下,新建一个Dll2.DEF空文件,后缀名为def,然后通过下面方式加入到工程:项目——属性——连接器——输入——模块定义文件 中输入你所定义的def文件名
在Dll2.def加入如下的代码:
LIBRARY DLL2
EXPORTS 表明DLL将要导出的函数
add
subtract
如果想要导出的符号名和源文件中定义的函数名不一样,可以按照下面方法定义,比如要将add函数导出为myadd函数:
LIBRARY DLL2
EXPORTS
myadd=add
subtract
然后编译,用dumpbin工具查看导出函数,发现函数名字不变。
使用DEF文件来导出函数不需要使用_stdcall 和 _declspec(dllexport)