今天让我总结以前前几天断断续续从网上学习的DLL用法。
首先呢?
首先呢DLL可以看做可以从其他地方拿过来用的容纳变量和函数的仓库,我们的仓库经历了“无库-静态链接库-动态链接库”。
首先我们看看静态链接库和动态链接库的区别。
静态链接库在和你程序编译的时候把它本身也加到到exe文件里面了,当然是指它本身被编译后的二进制码。
而动态链接库则不然,我们一般通过动态链接库的lib文件和程序一起编译,但并不把的lib文件加到文件里面。也就是说exe文件根本没有动态链接库代码本身,而是存储了动态链接库的一些简单信息(如函数入口地址等)。
所以我们会发现很多程序本身不大,却可以运行很多东西。想QQ一样。他们都是在需要的时候在加载相应DLL,也就是加载相应的代码咯。那么就可以避免程序本身太大带来的坏处。
说了这么多我们进入正题。
我们总是要循序渐进的来。
现看动态链接库的祖先静态链接库是如何使用的。
打开vs在VC的win32程序里面创建静态库。以下是vs2008的步骤
文件【文件】-【新建】-【项目】-【vc++】-【win32项目】(动态和静态链接库都采用这个步骤建立项目),起一个项目名字就可以创建win32项目了。
然后在win32项目的应用程序向导里点击:应用程序设置,然后勾选静态库,空项目。
然后分别在对应的文件夹创建test.h,test.cpp文件。以下是个简单的例子
/////////////////////////////////////////////////////////////////test.h///////////////////////////////////////////////////////////////
#ifndef _LIB_MY_H_
#define _LIB_MY_H_
extern "C" int Add(int,int);//extern "C":C语言编译,这里就牵扯到C语言编译和C++语言编译函数的区别了(后面会讲)。
#endif
/////////////////////////////////////////////////////////////////test.cpp///////////////////////////////////////////////////////////////
#include "test.h"
int Add(int a,int b)
{
return a+b;
}
然后编辑编译,不要运行(因为不是单独的程序),在项目文件夹里面右击:【生成】
就会在Debug文件夹里面生成test.lib
代码解释:
extern "C" int Add(int,int);//为了说明清楚,我下面采用很大篇幅讲解,这是理解动态链接库的关键。
其中extern "C"表明两层含义:
1:首先extern对任何函数变量的修饰意义都是指该函数在这个模块的链接属性为外部,所以其他模块可以使用,而且和extern连接的变量声明不是变量定义,说以extern语句不定义变量。上一句说模块可能有的同学难以理解,其实可以直接理解为cpp文件。也就是说该变量可以在其他cpp文件使用。注意了extern "C" int Add(int,int)这条语句出现的位置是头文件,所以这只是函数声明。真正的函数定义在test.cpp定义了。
我们都知道函数的可以有多个声明,但只能有一个定义。通过上面的头文件可以让其他cpp文件很容易使用它来声明其他cpp里面的函数和变量以便使用,这也是头文件发明出来的意思(让其他模块使用链接属性为外部的变量和函数更方便)。此项目中头文件是为其他项目使用lib时提供的一个声明。(lib里面有相应的运行代码。)
2:第二层含义就是这个关键的东东了,"C"。当让这个"C"不能单独使用,是要和extern一起使用的。这是为什么了?这个首先让我解释一下"C"这个东东的基本含义,意思是让编译器以C语言的方法编译函数。这是为什么呢?C语言方式编译函数和C++编译函数难道不一样吗?是的,确实不一样。如果你看过C++,那你肯定知道C++支持函数重载,重载的意思就是函数名可以相同,只要特征标量不同就可以了。特征标量主要指函数的参数列表和const限定符。简单的我们就是指函数参数列表。例如
void Add(int,int);
void Add(int,double);
这两个函数在C++是允许从在的,但是在C语言是不允许总在的。
因为C语言编译上述函数是得到类似这样的函数名:_Add。不同实现当然有差别了,但原理是一样的。两个函数都会得到这样的编译后的函数名。那当用户调用的时候,那就不知道调用哪个函数了!所以C语言不支持函数重载,但是C++支持啊。怎么支持的呢?C++将函数编译成和参数有关的函数名:_Add_int_int_,_Add_int_double_。不同实现名字有所差别。这样的话,那么C++用户调用Add(1,2);Add(1,1.2);C++编译器就能识别了。
所以说了这么多你明白了,用C++编译和用C语言编译得到的函数名是不一样的。将函数导出为C标准接口,可以供其他语言使用。所以编写dll所有函数而变量都以extern "C"打头。读者可能会经常看到如下形式简化所有函数,变量以extern "C"打头。如下:
extern "C"{
int extern _declspec(dllexport) a;
int _declspec(dllexport) Add(int,int);
}
其中的_declspec(dllexport)后面有讲解。现在只需知道即可。
如何使用呢?
静态库是这样使用的,我们需要头文件,后文件编译后产生的test.lib文件。将以如下方式在其他程序中使用,例如:
/////////////////////////////////////////////////////////////////exmaple.cpp///////////////////////////////////////////////////////////////
#include <iostream>
#include "test.h"
#pragma comment(lib,"test.lib")//test路径,我们将头文件和test.lib方法项目的工作目录,也就是Debug上一层目录
//此语句的含义是,将项目生成的目标文件和test.lib一起连接执行。不用此语句也可以
//在项目属性里面的【C++连接器】-【输入】-【依赖库】中写上test.lib。
int main()
{
std::cout<<Add(1,2);
std::cin.get();
}
如果想调试静态库,请与目标主程序一起调试,使用断点看静态库函数里面的逻辑错误。从中我们可以看出lib文件中实际上已经存放Add函数的实际目标代码。以后会拿lib和dll做比较。
静态库说完了,我们接着看动态链接库。
动态链接库:
动态链接库的好处我已经在上面说了。现在我们开始创建动态链接库。
VS2008的步骤创建:【文件】-【新建】-【项目】-【VC++】-【win32】-【win32项目】,
在应用程序设置中:选择【DLL】,【空项目】。
在对应文件夹里面创建dllTest.h,dllTest.cpp。代码如下:
///////////////////////////////////////////////////////////////dllTest.h///////////////////////////////////////////////////////////////
#ifndef _DLL_TEST_
#define _DLL_TEsT_
extern "C" _declspec(dllexport) int Add(int,int);
#endif
/////////////////////////////////////////////////////////////////dllTest.cpp///////////////////////////////////////////////////////////////
#include "dllTest.h"
int Add(int a,int b)
{
return a+b;
}
我们看一下新加的_declspec(dllexport),其中_declspec是一个单独的,是declare special的缩写,微软用于特殊目的的编译指示符,和dllexport一起构成dll中的接口导出。意思就是将这个函数接口暴露出去,也称导出去。
导出接口含还有种方法那就是用def文件。利用def文件(模块定义文件)来为外界提供接口。有了def文件我们就不需要头文件了。因为他们两个都是用来导出接口的。
让我们在项目中添加def文件,【右击项目】-【添加】-【新建项】-【模块定义文件】。
然后在def中会自动添加上
;dlltest的def文件
LIBRARY dllTest
然后我们在后面将导出接口:
EXPORTS
Add @ 1
以下源码:
////////////////////////////////////////////////////////////dllTest.def////////////////////////////////////////////////////////////
LIBRARY dllTest
EXPORTS
Add @ 1
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
首先我们来看一下几个语法
LIBRARY说明要def文件对应的dll
EXPORTS说明一下是接口导出
Add @ 1: 函数名 @ 1这条语句指明函数的导出序号为1。以后我们可以通过1得到该函数地址,但方法得对。
另外def文件中注释必须单独一行,不能与语句一起。以;表示注释开始。
以其中任意一种方式,编译就将生成dllTest.dll,dllTest.lib文件。
这里呢?又会经常对初学者产生迷惑,刚才我们创建静态链接库的时候编译之后产生的就是静态链接库,扩展名是.lib,这次创建动态链接库是也出现了一个dll,还出现了一个lib。是不是这个lib也是静态链接库啊。非也非也。MS经常整一些让初学者难受的事情。其实dll附带产生的lib根本不是静态链接库,它是的真名是:导入库。
现在正是时候讲解导入库和静态链接库的区别了:
不同点:
1:导入库是记录dll中各个函数地址信息的文件,里面没有任何可执行代码。而静态库(静态链接库的简称)里面包含了可执行代码,其实静态库里面是一个个的obj文件。也就是目标文件,只欠连接就可以执行。
2:程序用导入库是在程序实际运行中加载dll后,从导入库中读取函数地址信息,然后执行dll中的代码。而程序用静态库时,是连接的时候找到静态库里面的函数,把它copy一本,到自己的obj文件里面,然后以后运行的时候和静态库一点瓜葛都没有了。因为程序现在本身就有所需的响应的可执行代码。
相同点:它们的文件扩展名都是lib文件,而且程序使用的时候用法也一样。如果用编译器连接#pragma comment(lib,"dllTest.lib),如果更改项目依赖库也一样,【项目属性】-【连接器】-【输入】-【附加依赖项】。
所以呢,由上面所述:
我们可以知道一些动态链接库为什么会比静态链接库更有用?
因为动态链接库是独立的,而且是在运行时连接的,可以为其他所有进程使用。所以不必像用静态库那样从静态库里面拷代码,还增加自身附带。有了动态链接库,系统只需加载了dll之后,内存内所有程序就都可以访问它,并且执行了。这样极大的提高了代码可重用性。
如何使用呢:
我们只需要dll,和lib就可以了。
将dll,lib放到目标程序的工作目录。//其实程序搜索目录顺序是:包含EXE文件的目录,进程的当前工作目录,Windows系统目录,Windows目录,列在Path环境变量中的一系列目录。
dll的连接方式有两种一种是静态连接(也成为隐式连接),一种的动态连接(也成为显示连接)。不要误会静态连接以为和静态库联系在一起,静态链接只是动态链接库的一种连接方式。
现在我们介绍静态连接的方法
目标程序使用方法如下:
/////////////////////////////////////////////////////////////////exmaple.cpp///////////////////////////////////////////////////////////////
#include <iostream>
#pragma comment(lib,"dllTest.lib")
extern "C" _declspec(dllimport) int Add(int a,int b);//_declspec是declare special的缩写,微软的编译指示符。与dllimport一起
//使用说明这是从dll中导入的函数
int main()
{
std::cout<<Add(1,2);
std::cin.get();
}
为什么称为静态呢?因为是由编译器决定什么时候加载dll,编译器并不是一开始就加载dll,而是运行到相应函数代码的时候加载,卸载dll的时机也是和整个应用程序一起被卸载。
为什么又称为隐式呢?因为没有明确指明让程序连接dll,而是通过编译器自己解读出来的,如何解读,应该是通过lib和申明的函数原型的吧。(没查,但我猜想因该是)
动态链接的方法:
是由用户决定什么时候加载dll,什么时候卸载dll。
/////////////////////////////////////////////////////////////////exmaple.cpp///////////////////////////////////////////////////////////////
#include <windows.h>
#include <iostream>
typedef int(*Add)(int,int);
int main()
{
HINSTANCE h1;
h1=LoadLibrary("dllTest.dll");
pAdd Add=(pAdd)GetProcAddress(h1,"Add");
if(NULL != Add)
{
int a=Add(1,2);
}
FreeLibrary(h1);
}
动态调用主要牵扯到四个地方:
1:typedef int(*pAdd)(int,int);根据typedef的意义就是定义一个新的指针int(*)(int,int)的别名为pAdd
2:LoadLibrary是加载dll的windows API可以加载相应dll
3:GetProcAddress是加载对应模块,对应名称的函数。模块可以理解为一个cpp文件,一个dll文件
4:FreeLibrary是释放对应动态链接库。
从中我们可以看出动态链接不需要函数原型和lib导入库。直接由LoadLibrary函数解读出函数位置。
下面介绍一下变量的使用:
和函数用法也是一样的。同样分为静态连接和动态链接。
首先介绍静态链接:
/////////////////////////////////////////////////////////////////dllTest.h///////////////////////////////////////////////////////////////
#ifndef _DLL_TEST_
#define _DLL_TEsT_
extern "C" _declspec(dllexport) int Add(int,int);
extern "C" int _declspec(dllexprot) m_Global;
#endif
/////////////////////////////////////////////////////////////////dllTest.cpp///////////////////////////////////////////////////////////////
#include "dllTest.h"
int Add(int a,int b)
{
return a+b;
}
int m_Global;
然后在你的程序里面添加如下代码:
/////////////////////////////////////////////////////////////////exmaple.cpp///////////////////////////////////////////////////////////////
#include <windows.h>
#include <iostream>
#pragma comment(lib,"dllTest.lib")
extern "C" int _declspec(dllimport) Add(int,int);
extern _declspec(dllimport) int m_Global;
int main()
{
int a=Add(2,4);
std::cout<<a<<m_Global<<std::endl;
std::cin.get();
}
动态链接:
//exmaple.cpp
#include <windows.h>
#include <iostream>
int main()
{
HINSTANCE h1;
h1=::LoadLibrary(L"dllTest.dll");
int* pm_Global=(int*)::GetProcAddress(h1,"m_Global");
cout<<*pm_Global;
FreeLibrary(h1);
}
另外讲一个dll头文件编译技术:
#ifndef _DLL_TEST_
#define _DLL_TEST_
#ifdef _cplusplus //如果定义了C++编译器,但经我测试vs2008没有定义_cplusplus。所有这个地方有待商榷
extern "C"{
#endif
#ifndef _DLL_EXPORTS_
#define DLLAPI _declspec(dllexport)
#else
#define DLLAPI _declspec(dllimport)
#endif
DLLAPI int Add(int,int);
DLLAPI extern int m_Global;
#ifdef _cplusplus
}
#endif
#endif
这个头文件编译指令应该将很好的解决DLL中函数让别人使用的方法。说应该是因为_cplusplus现在还不确定。
所以通过这里我们可以看出来变量函数本质的表达都是一样的,通过GetPorcAddress得到的都是指针,无论是函数还是变量,
基本就这么多了。