静态链接库(Lib)和动态链接库(DLL)

            序言:本文主要讲解静态链接库和动态链接库的区别,以及怎么样编译和引用两种库,怎么样从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)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值