+++++++++++++++++++++++++++++++++++++++++++++
++++++++++++ 动态库的创建及使用 +++++++++++
+++++++++++++++++++++++++++++++++++++++++++++
1. 创建一个简单的非MFC的DLL:
用动态链接库实现一个同样功能的add函数
l 创建一个DLL的项目:
文件—》新建—》项目—》win32控制台—》输入名字“dllTest”—》Dll—》空项目—》完成。
l 在建立的工程中添加lib.h及lib.cpp文件,源代码如下:
lib.h
/* 文件名:lib.h */
#ifndef LIB_H
#define LIB_H
//对函数add的声明前面添加了__declspec(dllexport)语句。这个语句的含义是声明函数add为DLL的导出函数
extern "C" int __declspec(dllexport)add(int x, int y);
#endif
lib.cpp
/* 文件名:lib.cpp */
#include "lib.h"
#include <iostream>
using namespace std;
int add(int x, int y)
{
std::cout<<"动态库中......两个数之积:是吗???"<<(x*y)<<std::endl;
return x * y;
}
l 编译成功后
你在..//Debue里面能找到下面的文件,则证明动态库生成成功。
l 动态调用方式————创建同一个解决方案下的应用项目dllCall
加上下面的代码,编译运行。
Main.cpp
#include <stdio.h>
#include <tchar.h> //和_T有关,且是Unicode
#include <windows.h>
typedef int(*lpAddFun)(int, int); //宏定义函数指针类型,定义了一个与add函数接受参数类型和返回值均相同的函数指针类型
int main(int argc, char *argv[])
{
HINSTANCE hDll; //DLL句柄
lpAddFun addFun; //函数指针,定义了lpAddFun的实例addFun
//通过Win32 Api函数LoadLibrary动态加载了DLL模块并将DLL模块句柄赋给了hDll
//动态库所在的路径,如果有误则会报错。上一级目录下面的Debug文件夹下面的,一个dllTest.dll文件
hDll = LoadLibraryW(_T("..//Debug//dllTest.dll"));
if (hDll != NULL)
{
//Win32 Api函数GetProcAddress得到了所加载DLL模块中函数add的地址并赋给了addFun
addFun = (lpAddFun)GetProcAddress(hDll, "add");
if (addFun != NULL)
{ //应用程序使用动态库中的函数
int result = addFun(2, 3);
printf("%d", result);
}
//应用工程使用完DLL后,在函数main中通过Win32 Api函数FreeLibrary释放了已经加载的DLL模块。
FreeLibrary(hDll);
}
return 0;
}
能得到你要的结果。你可以在上面的 int result = addFun(2, 3);中设置断点,然后F11单步调试,你可以进入到原动态库里面的语句中。
通过这个简单的例子,我们获知DLL定义和调用的一般概念:
(1)DLL中需以某种特定的方式声明导出函数(或变量、类);
(2)应用工程需以某种特定的方式调用DLL的导出函数(或变量、类)。
2.声明导出函数
在函数声明中加上__declspec(dllexport),上面已经举例说明;另外一种方式是采用模块定义(.def) 文件声明,.def文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息。
l 在dllTest中添加lib.def的文件
l 在lib.def中加下面的内容:
LIBRARY dllTest
EXPORTS
add @ 1
l .def文件的规则:
(1)LIBRARY语句说明.def文件相应的DLL;
(2)EXPORTS语句后列出要导出函数的名称。可以在.def文件中的导出函数名后加@n,表示要导出函数的序号为n(在进行函数调用时,这个序号将发挥其作用);
(3).def 文件中的注释由每个注释行开始处的分号 (;) 指定,且注释不能与语句共享一行。
由此可以看出,例子中lib.def文件的含义为生成名为“dllTest”的动态链接库,导出其中的add函数,并指定add函数的序号为1。
3.Dll的调用方式:
在1中已经介绍了动态调用方式,现在我们来认识静态调用方式:
l 创建一个新的控制台项目StaticCallDll,并把下面代码拷贝到
StaticMain.cpp中
#pragma comment(lib,"dllTest.lib")
#include <iostream>
using namespace std;
//.lib文件中仅仅是关于其对应DLL文件中函数的重定位信息
extern "C" __declspec(dllimport) int add(int x,int y);
using namespace std;
void main(int argc, char* argv[])
{
int result = add(2,3);
std::cout<<"应用程序中两个数的之和:"<<result<<std::endl;
}
l 将编译dllTest工程所生成的.lib和.dll文件拷入dllCall工程所在的路径。
当然你也可以在
StaticCallDll的属性—》链接—》常用—》增加Lib路径中设置新的路径。并在链接—》输入—》dllTest.lib这个名字的Lib
l 总结:
(1)告诉编译器与DLL相对应的.lib文件所在的路径及文件名,#pragma comment(lib,"dllTest.lib")就是起这个作用。
程序员在建立一个DLL文件时,连接器会自动为其生成一个对应的.lib文件,该文件包含了DLL 导出函数的符号名及序号(并不含有实际的代码)。在应用程序里,.lib文件将作为DLL的替代文件参与编译。
(2)声明导入函数,extern "C" __declspec(dllimport) add(int x,int y)语句中的__declspec(dllimport)发挥这个作用。
静态调用方式不再需要使用系统API来加载、卸载DLL以及获取DLL中导出函数的地址。这是因为,当程序员通过静态链接方式编译生成应用程序时,应用程序中调用的与.lib文件中导出符号相匹配的函数符号将进入到生成的EXE 文件中,.lib文件中所包含的与之对应的DLL文件的文件名也被编译器存储在 EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows将根据这些信息发现并加载DLL,然后通过符号名实现对DLL 函数的动态链接。这样,EXE将能直接通过函数名调用DLL的输出函数,就象调用程序内部的其他函数一样。
1. 导出变量:
DLL定义的全局变量可以被调用进程访问;DLL也可以访问调用进程的全局数据,我们来看看在应用工程中引用DLL中变量的例子。
l 在原来的动态库项目中修改如下:
lib.h
/* 文件名:lib.h */
#ifndef LIB_H
#define LIB_H
//对函数add的声明前面添加了__declspec(dllexport)语句。这个语句的含义是声明函数add为DLL的导出函数
extern "C" int __declspec(dllexport)add(int x, int y);
//导出一个全局变量
extern int dllGlobalVar;
#endif
lib.cpp
#include "lib.h"
#include "windows.h"
#include "stdio.h"
#include <iostream>
using namespace std;
// lib.cpp : DLLMain函数的例子
//如果程序员没有为DLL模块编写一个DLLMain函数,系统会从其它运行库中引入一个不做任何操作的
//缺省DLLMain函数版本。在单个线程启动和终止时,DLLMain函数也被调用。正如由dwReason参数所
//表明的那样。
int dllGlobalVar; //全局变量声明
BOOL APIENTRY DllMain( HANDLE hModule,
/*
进程中的每个DLL模块被全局唯一的字节的HINSTANCE句柄标识进程自己还有一个HINSTANCE句柄。
所有这些模块句柄都只有在特定的进程内部有效,它们代表了DLL或EXE模块在进程虚拟空间中的起始
地址。在Win32中,HINSTANCE和HMODULE的值是相同的,这两种类型可以替换使用。进程模块句柄几乎
总是等于x400000,而DLL模块的加载地址的缺省句柄是x10000000。如果程序同时使用了几个DLL模
块,每一个都会有不同的HINSTANCE值。这是因为在创建DLL文件时指定了不同的基地址,或者是因为
加载程序对DLL代码进行了重定位。
*/
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
printf("/nprocess attach of dll/n");
dllGlobalVar = 100; //在dll被加载时,赋全局变量为
break;
case DLL_THREAD_ATTACH:
printf("/nthread attach of dll/n");
break;
case DLL_THREAD_DETACH:
printf("/nthread detach of dll/n");
break;
case DLL_PROCESS_DETACH:
printf("/nprocess detach of dll/n");
break;
}
return TRUE;
}
int add(int x, int y)
{
std::cout<<"动态库中......两个数之积:是吗???"<<(x*y)<<std::endl;
return x * y;
}
lib.def
LIBRARY "dllTest"
EXPORTS
add @ 1
dllGlobalVar CONSTANT
;或dllGlobalVar DATA
从lib.h和lib.cpp中可以看出,全局变量在DLL中的定义和使用方法与一般的程序设计是一样的。若要导出某全局变量,我们需要在.def文件的EXPORTS后添加:
变量名 CONSTANT //过时的方法
或
变量名 DATA //VC++提示的新方法
在StaticCallDll项目中的StaticMain.cpp里面修改如下
#pragma comment(lib,"dllTest.lib")
#include <iostream>
using namespace std;
//.lib文件中仅仅是关于其对应DLL文件中函数的重定位信息
extern "C" __declspec(dllimport) int add(int x,int y);
extern int dllGlobalVar; //声明所导入的并不是DLL中全局变量本身,而是其地址
//extern int _declspec(dllimport) dllGlobalVar; //用_declspec(dllimport)导入
//通过_declspec(dllimport)方式导入的就是DLL中全局变量本身而不再是其地址了
using namespace std;
void main(int argc, char* argv[])
{
int result = add(2,3);
std::cout<<"应用程序中两个数的之和:"<<result<<std::endl;
应用程序必须通过强制指针转换来使用DLL中的全局变量。这一点,从*(int*)dllGlobalVar可以看出
printf("%d ", *(int*)dllGlobalVar);
*(int*)dllGlobalVar = 1;
printf("%d ", *(int*)dllGlobalVar);
}
l 应用工程中引用DLL中全局变量的一个更好方法:
#include <stdio.h>
#pragma comment(lib,"dllTest.lib")
extern int _declspec(dllimport) dllGlobalVar; //用_declspec(dllimport)导入
int main(int argc, char *argv[])
{
printf("%d ", dllGlobalVar);
dllGlobalVar = 1; //这里就可以直接使用, 无须进行强制指针转换
printf("%d ", dllGlobalVar);
return 0;
}
通过_declspec(dllimport)方式导入的就是DLL中全局变量本身而不再是其地址了,笔者建议在一切可能的情况下都使用这种方式。
2. 导出类:
DLL中定义的类可以在应用工程中使用。
下面的例子里,我们在DLL中定义了point和circle两个类,并在应用工程中引用了它们。
l 添加代码:
point.h
#ifndef _POINT_H
#define _POINT_H
#ifdef DLL_FILE
class _declspec(dllexport) point //导出类point
#else
class _declspec(dllimport) point //导入类point
#endif
{
public:
float y;
float x;
point();
point(float x_coordinate, float y_coordinate);
};
#endif
point.cpp
#ifndef DLL_FILE
#define DLL_FILE
#endif
#include "point.h"
//类point的缺省构造函数
point::point()
{
x = 0.0;
y = 0.0;
}
//类point的构造函数
point::point(float x_coordinate, float y_coordinate)
{
x = x_coordinate;
y = y_coordinate;
}
circle.h
#ifndef CIRCLE_H
#define CIRCLE_H
#include "point.h"
#ifdef DLL_FILE
class _declspec(dllexport)circle //导出类circle
#else
class _declspec(dllimport)circle //导入类circle
#endif
{
public:
void SetCentre(const point );
void SetRadius(float r);
float GetGirth();
float GetArea();
circle();
private:
float radius;
point centre;
};
#endif
circle.cpp
#ifndef DLL_FILE
#define DLL_FILE
#endif
#include "circle.h"
#define PI 3.1415926
//circle类的构造函数
circle::circle()
{
centre = point(0, 0);
radius = 0;
}
//得到圆的面积
float circle::GetArea()
{
return PI *radius * radius;
}
//得到圆的周长
float circle::GetGirth()
{
return 2 *PI * radius;
}
//设置圆心坐标
void circle::SetCentre(const point centrePoint)
{
centre = centrePoint;
}
//设置圆的半径
void circle::SetRadius(float r)
{
radius = r;
}
l 编译生成后,在应用程序中用下面的代码,编译再运行。
#pragma comment(lib,"dllTest.lib");
//#include "windows.h"
#include "stdio.h"
#include "circle.h" //包含类声明头文件
int main(int argc, char *argv[])
{
circle c;
point p(2.0, 2.0);
c.SetCentre(p);
c.SetRadius(1.0);
printf("area:%f girth:%f", c.GetArea(), c.GetGirth());
return 0;
}
从上述源代码可以看出,由于在DLL的类实现代码中定义了宏DLL_FILE,故在DLL的实现中所包含的类声明实际上为
class _declspec(dllexport) point //导出类point
{
…
}
和
class _declspec(dllexport) circle //导出类circle
{
….
}
而在应用工程中没有定义DLL_FILE,故其包含point.h和circle.h后引入的类声明为:
class _declspec(dllimport) point //导入类point
{
…
}
和
class _declspec(dllimport) circle //导入类circle
{
…
}