C++类库开发详解

前言:这是一篇总结性的文章,需要有一点C++和dll基本知识的基础,在网上查阅了很多资料感觉没有一篇详细、具体、全面的dll开发介绍,我这是根据最近项目和网上资料整理出来的,并附带实例的一个总结性的文章(由于篇幅较长故不附带源码解释)。另外,个人愚昧地认为以后C++的开发会更多地面向库的开发,所以学会库的开发必不可少。

 

1、 静态链接库和动态链接库

1.   静态链接库(LIB)只用在程序开发期间使用,而动态链接库(DLL)在执行期间使用。

2.   静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。

3.   静态链接库,浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。如果静态库libname.lib更新了,所有使用它的应用程序都需要重新编译、发布给用户(对于用户来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。

4.   动态库则实现了增量更新,进程间可以共享动态库,节约了资源,当程序第一次调用动态库时,系统将该库加载到内存中,当另一个程序也调用这个库时,系统不再加载,而是将状态+1,当某个程序退出或释放该库时,状态则-1,直到当系统中没有程序调用该库时,系统自动将其清理并释放内存。

 

2、 知识点

1.   在VS中创建的类库有2种类型,一种是直接选择VC++类库(是使用微软版本的C++创建的类库),一种是Win32项目或Win32控制台程序,然后选择对应的类库类型,也就是ANSI标准的C++类库,一般我们用这种方式创建的类库,而它又分三种:Non-MFC DLL(非MFC动态库)、MFC Regular DLL(MFC规则DLL)、MFC ExtensionDLL(MFC扩展DLL)。

2.   非MFC动态库不采用MFC类库结构,其导出函数为标准的C接口,能被非MFC或MFC编写的应用程序所调用;MFC规则DLL包含一个继承自CWinApp的类,但其无消息循环;MFC扩展DLL采用MFC的动态链接版本创建,它只能被用MFC类库所编写的应用程序所调用。

3.   当Windows要执行一个使用了动态链接库的程序而需要加载该链接库时,动态链接库文件必须储存在含有该.EXE程序的目录下、目前的目录下、Windows系统目录下、Windows目录下,或者是在通过MS-DOS环境中的PATH可以存取到的目录下(Windows会按顺序搜索这些目录)。

4.   动态链接库模块可能有其它扩展名(如.EXE或.FON),但标准扩展名是.DLL。只有带.DLL扩展名的动态链接库才能被Windows自动加载。如果文件有其它扩展名,则程序必须另外使用LoadLibrary或者LoadLibraryEx函数加载该模块。

5.   DLL 的编制与具体的编程语言及编译器无关。只要遵循约定的DLL接口规范和调用方式,用各种语言编写的DLL都可以相互调用。

 

3、 关键字

1.   当建立一个DLL时,它应该包含处理字符和字符串的Unicode和非Unicode版的所有函数,比如实现ANSI版和宽字符版。

#ifdef UNICODE
#define TextW  //定义宽字符版的函数
#else
#define TextA  //定义ANSI版的函数
#endif

 

2.  __declspec(dllexport),该关键字位于类/函数的声明和定义中,表示该类/函数为DLL的导出类/函数。而DLL内的类/函数分两种,一种是DLL导出类/函数供外部程序调用,一种是DLL内部函数供DLL自己调用。

3.  __declspec(dllimport),该关键字说明类/函数为导入函数,与__declspec(dllexport)匹配对应,为了在应用程序中使用其声明的类/函数。

 

4.  extern"C",是为了解决导出函数名的问题,因为C++编译器,为实现函数重载,在编译生成的汇编代码中要对函数名进行一些处理,而用 extern "C"声明的函数将使用函数名作符号名,这时C的处理方式。因此,只有非成员函数才能被声明为extern "C",并且不能被重载。但是,冠以extern "C"限定符后,并不意味着函数中无法使用C++代码了,相反,它仍然是一个完全的C++函数,可以使用任何C++特性和各种类型的参数。但这只解决了C/C++之间调用的问题,更可靠的方法是定义一个.def文件。

5.  __cplusplus,是cpp中自定义宏,定义了这个宏表示它是一段cpp的代码。并且这是一个C++编译器保留的宏定义,意味着C++编译器认为这个宏已经定义了。

如果函数这样定义:extern”C” int add(int a, int b);由于C编译器不能识别extern”C”指令,那么C调用C++程序就会出现问题,这时候__cplusplus就起作用了。如下:

#ifdef __cplusplus
extern "C" {
#endif

//....声明代码

#ifdef __cplusplus
}
#endif


6.  #pragma comment( lib , "..//debug//LIBProject.lib" )的意思是指本文件(应用程序)生成的.obj文件应与LIBProject.lib一起连接。

7.  __stdcall,它是Standard Call的缩写,是C的标准函数调用方式:所有参数从右到左依次入栈,如果调用的是类成员的话,最后一个入栈的是this指针。堆栈中的参数由被调用的函数在返回后清除,函数在编译的时候就必须严格控制参数生成,否则返回后会出错。

8.  __fastcall,是编译器指定的快速调用方式。由于使用堆栈传递比较费时,因此__fastcall通常规定将前N(一般2个,不同的编译器规定使用的寄存器个数不同)个参数由CPU寄存器传递,其余的还是用内存的堆栈传递。返回方式和__stdcall相当。由于其涉及到编译器决定参数传递方式,故不能作为跨编译器的接口。

 

9.  __cdecl,它是C Declaration的缩写,表示C语言默认的函数调用方式:所有的参数从右到左依次入栈,参数由调用者清除(手动清除,调用者一般指编译器)。特点在于可以使用不定个数的参数。

 

如果通过VC++编写的DLL欲被其他语言编写的程序调用,应将函数的调用方式声明为__stdcall方式,而C/C++缺省的调用方式却为__cdecl(默认调用方式)。__stdcall与__cdecl的区别在于生成函数名最终符号的方式不同。若采用C编译方式(在C++中将函数声明为extern "C"),__stdcall约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数,形如_functionname@number;而__cdecl约定仅在输出函数名前面加下划线,形如_functionname。

注意,声明函数形式:extern “C” int __stdcall add(int x, int y);,应用程序中定义函数指针为:typedef int(__stdcall *lpAddFun)(int, int);

 

4、 静态库(lib/a)

由于静态库是跟随着应用程序一起编译连接到源文件(exe)的,所以代码写法基本没有特殊的地方,需要注意的是extern"C"来确定是否使用C的方式编译,然后就是利用#ifndef来管理一类型的类/函数的调用。

值得注意的是在应用程序中使用静态库时的声明#include"MathTool.h"、#pragma comment(lib,"LIBProject.lib"):直接将.h和.lib文件放在当前目录似乎是不行的(我测试时不行的),两种方法:1.使用相对路径或绝对路径包含.h和.lib文件。2.通过”属性”--”C/C++” --“常规”--”附加包含目录”,来指定.h的文件目录。通过”属性”--”链接器” -- “常规”--”附加库目录”,来添加.lib目录。这种方法动态库也适用。

如果不想用#pragmacomment来指定lib,还可以在”属性”--”链接器” -- “常规”--”附加依赖库”来指定库。也可以不设置库目录和依赖库名,而是直接“属性”--“链接器”--”命令行”,输入静态库的完整路径即可。但一般不推荐。

示例见:LIBProject。

 

5、 动态库(dll/so)

1.  动态库的lib文件和静态库的lib文件

静态库对应的lib文件叫静态库,动态库对应的lib文件叫导入库。实际上静态库本身就包含了实际执行代码、符号表等等,而对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息。

 

2.  声明导出函数(介绍.def)

DLL导出函数的声明有两种方式:一种是在声明中加__declspec(dllexport); 一种是采用模块定义(.def) 文件声明。.def文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息。

.def文件的规则为:

(1)LIBRARY语句说明.def文件相应的DLL;

 (2)EXPORTS语句后列出要导出函数的名称。可以在.def文件中的导出函数名后加@n,表示要导出函数的序号为n(在进行函数调用时,这个序号将发挥其作用);

(3).def 文件中的注释由每个注释行开始处的分号(;) 指定,且注释不能与语句共享一行。

GetInstance = GetInstance这样可以指定了DLL的函数导出后的名称仍然不变。

注:使用了.def文件记得在编译器:”属性”--”链接器”--”输入”--”模块定义文件”中设置改def文件。

如果使用__declspec(dllexport)的方式声明导出,在C++中使用当然没有问题,但是当其他语言(C#、VB)调用时,就会出现找不到函数名的情况,是因为C++中为实现重载机制将函数名改变了,这时我们为了能够让其他语言正确的调用,就要使用extern “C”+ __declspec(dllexport)+__stdcall的方式。而使用def文件这些都可以不要,这样更方便,但是用到类的地方还是要用第一种方式。

 

3.  调用方式

DLL的调用方式也有两种:一种是动态调用;一种是静态调用。

动态调用:它完全由编程者用 API 函数加载和卸载 DLL,程序员可以决定 DLL文件何时加载或不加载,显式链接在运行时决定加载哪个 DLL文件。由LoadLibrary->GetProcAddress->FreeLibrary系统API提供的三位一体“DLL加载-DLL函数地址获取-DLL释放”方式。

动态调用注意的地方,在函数前面加extern “C”或定义def文件。显式调用类库中的class是很危险和繁琐的,因此能隐式不显式,能静态不动态

静态调用: 它的特点是由编译系统完成对DLL的加载和应用程序结束时 DLL 的卸载。静态调用方式的当调用某DLL的应用程序结束时,若系统中还有其它程序使用该 DLL,则Windows对DLL的应用记录减1,直到所有使用该DLL的程序都结束时才释放它。当程序员通过静态链接方式编译生成应用程序时,应用程序中调用的与.lib文件中导出符号相匹配的函数符号将进入到生成的EXE 文件中,.lib文件中所包含的与之对应的DLL文件的文件名也被编译器存储在 EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows将根据这些信息发现并加载DLL,然后通过符号名实现对DLL 函数的动态链接。这样,EXE将能直接通过函数名调用DLL的输出函数,就像调用程序内部的其他函数一样。静态调用方式简单实用,但不如动态调用方式灵活。

静态调用需要两步:

1.  告诉编译器与DLL相对应的.lib文件所在的路径及文件名, #pragma comment(lib,"dllTest.lib")。程序员在建立一个DLL文件时,连接器会自动为其生成一个对应的.lib文件,该文件包含了DLL 导出函数的符号名及序号(并不含有实际的代码)。在应用程序里,.lib文件将作为DLL的替代文件参与编译。具体可以这样做,将.h、.lib、.dll文件拷贝到客户端程序当前目录下,然后在程序中#include<*.h> + #pragmacomment(lib,"dllTest.lib");或者在客户端程序的工程属性里面增加对该lib文件的引入。

2.  声明导入函数, 用__declspec(dllimport)说明为导入函数。有时是不用声明的,在类库的编写过程中定义好了。

 

4.  DllMain函数

Windows在加载DLL的时候,需要一个入口函数,就如同控制台或DOS程序需要main函数、WIN32程序需要WinMain函数一样。当程序中没有写DllMain函数时,系统会从其它运行库中引入一个不做任何操作的缺省DllMain函数版本,并不是DLL可以放弃DllMain函数。

根据编写规范,Windows必须查找并执行DLL里的DllMain函数作为加载DLL的依据,它使得DLL得以保留在内存里。这个函数并不属于导出函数,而是DLL的内部函数。这意味着不能直接在应用程序中引用DllMain函数,DllMain是自动被调用的。

DllMain函数在DLL被加载和卸载时被调用,在单个线程启动和终止时,DLLMain函数也被调用,ul_reason_for_call指明了被调用的原因。

 

BOOL APIENTRY DllMain( HMODULEhModule,
                      DWORD ul_reason_for_call,
                      LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

解析:

APIENTRY被定义为__stdcall,它意味着这个函数以标准Pascal的方式进行调用,也就是WINAPI方式;

ul_reason_for_call的四个参数DLL_PROCESS_ATTACH表示进程调用;DLL_THREAD_ATTACH表示线程调用;DLL_THREAD_DETACH线程释放;DLL_PROCESS_DETACH线程释放。

lpReserved是一个保留字,基本没有什么作用,无需了解。

进程中的每个DLL模块被全局唯一的32字节的HINSTANCE句柄标识,只有在特定的进程内部有效,句柄代表了DLL模块在进程虚拟空间中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,这两种类型可以替换使用,这就是函数参数hModule的来历。

GetProcAddress( hDll, MAKEINTRESOURCE ( 1 ) )要注意,它直接通过.def文件中为add函数指定的顺序号访问add函数,具体体现在MAKEINTRESOURCE ( 1 ),MAKEINTRESOURCE是一个通过序号获取函数名的宏,定义为(节选自winuser.h):

#defineMAKEINTRESOURCEA(i) (LPSTR)((DWORD)((WORD)(i)))
#defineMAKEINTRESOURCEW(i) (LPWSTR)((DWORD)((WORD)(i)))
#ifdef UNICODE
#defineMAKEINTRESOURCE MAKEINTRESOURCEW
#else
#defineMAKEINTRESOURCE MAKEINTRESOURCEA


5.  DLL导出变量

DLL定义的全局变量可以被调用进程访问;DLL也可以访问调用进程的全局数据。步骤:

1.  库的头文件.h中声明,extern int dllGlobalVar;

2.  在.cpp文件中声明使用,声明:int dllGlobalVar;使用:dllGlobalVar = 100;

3.  .def文件中导出,”GlobalVar_a@[n]”的方式,有人说是dllGlobalVar CONSTANT(已过时)/ dllGlobalVar DATA这个方式,但是我用的不行。

4.  在应用程序中引用DLL中定义的全局变量,方式有两种:一种方法(使用def声明的变量):首先声明extern int dllGlobalVar;然后int a = *(int*)dllGlobalVar。 特别要注意的是用externint dllGlobalVar声明所导入的并不是DLL中全局变量本身,而是其地址。另一种方法:externint _declspec(dllimport) dllGlobalVar; 通过_declspec(dllimport)方式导入的就是DLL中全局变量本身而不再是其地址了。建议第二种方式,第一种方式我测试中遇到一些很奇怪的问题。希望,有人根据我的测试,帮我解决下。

示例见:DLLProjec

 

6.  DLL导出类

导出类,我们一般使用静态调用,这样我们可以将整个类导出,或者只将类中的指定成员导出。如果使用动态调用类,我们一般使用一个全局函数来返回一个类的对象。

最后,学习到这里,最大的领悟就是,类库的编写,主要理解extern”C”、_declspec(dllexport)、_declspec(dllimport)、__stdcall、__cdecl这几个基本关键字,类库是提供给别人用的,我需要做的就是指定将哪些类或函数指定为用户使用的。还有就是使用类库,知道静态调用和动态调用的方法。

示例见:DLLProjec

 附源码链接:http://download.csdn.net/detail/z702143700/8738467

后记:本文很长,也是我花费了很多个工作日学习实践的成果,如果有错误或意见希望能给我留言,结合示例来看会更加好些,如果可以直接看懂示例,那么长的文字也就可以随便浏览了。接下来,我会介绍其他语言调用C++类库常用的方法(C#调用C++的库)。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。