总觉得自己写过的程序没有做下记录和分析是件很可惜的事情,也许在别人看来是很简单的事情,但是如果没有自己深入研究和思考,并且分析整理的话,那编过的程序也仅仅是个程序,并没有太大的价值,所以想开始对自己写的程序开始记录,分析,这就作为自己编程的一个新的开始。
这次的程序是关于DLL的,其实一直想要掌握DLL的相关知识,但是无奈工作中没有涉及到这方面的程序编写。这次也是自己找个相关的程序练手,打算以这个为切入点了解DLL,好了开始编程了。
先把程序贴出来吧:DLL的头文件HDI.h(所有程序编译的环境是xp+vc6.0)
#ifndef _HDI_H
#define _HDI_H
#if defined DLLEXPORT
#define DLL_EXPORT extern"C"_declspec(dllexport)
#else
#define DLL_EXPORE extern"C"_declspec(dllimport)
#endif
DLL_EXPORT int fun(int a, int b);
#endif
然后是cpp文件
#define DLLEXPORT
#include "hdi.h"
int fun(int a,int b)
{
return a+b;
}
下面是测试程序的cpp
#include <iostream>
#include <windows.h>
#include "hdi.h"
#pragma comment(lib,"HID.lib")
//extern"C" int fun(int,int);
typedef int (*pFun)(int ,int);
int main()
{
// HMODULE hmodle = ::LoadLibrary("F:\\HID\\HID\\Debug\\HID.dll");
// HMODULE hmodle = ::LoadLibrary("HID.dll");
// if (hmodle == NULL)
// {
// std::cout<<"Load Library fail!"<<std::endl;
// }
// else
// {
// pFun addFun = (pFun)GetProcAddress(hmodle , "fun");
// int k = addFun(1,2);
// std::cout<<k<<std::endl;
// FreeLibrary(hmodle);
// }
std::cout<<fun(1,2)<<std::endl;
system("pause");
return 0;
};
针对这个程序做个简要的说明:它是一个普通的只导出函数的DLL,并没有导出变量和类,网上说导出类是不能动态加载,这里先不考虑。
首先来看看h文件,这里有一句define DLL_EXPORT extern"C"_declspec(dllexport) ,关键词extern"C"可以让导出的函数名格式符合C的标准,也就是只有函数名(这同时也意味着同名函数的重载将不可能,编译会出错),他的好处是其他编译器也能正确导入这个函数,否则他的函数名会按照c++的风格导出,会变成“@@fun_int_int “之类,这样只有c++的编译器才能正确导入该函数(虽然我的机器貌似只有加extern”C“才能正确导入)。
接着来看看关键字dllexport和下面的dllimport,dllexport表示这是一个导出函数,凡是要导出的函数必须加这个修饰,如果不用_declspec(dllexport)修饰,那么就要写def文件,这个我也没有去测试,觉得这种方式方便。dllimport函数说明这是一个导入函数,之所以这样写是在编写的dll的cpp文件中会预编译dllexport,而在应用dll的程序中就会预编译dllimport,可能这样说还是不清楚,这么说吧应用程序中也会用得到dll的头文件但是如果吧dll的头文件直接拿过来用而没有加这个预编译判断,那么程序用应用程序中也声明成dllexport显然是不正确的,所以加了这个判断就可以直接把dll头文件拿来用(至于哪里用到我下面写)。
最后只要用这个宏修饰函数,并在cpp文件中实现它就可以了。程序编写完成以后,编译通过后会在debug文件下生成dll文件和lib文件,网上有人说在debug环境下生成的dll只要函数参数有拷贝赋值函数或重载=,他就只能被debug版本的应用程序编译通过,这我没有发现。倒是我测试过以string作为参数,应用程序传的参数是字符串常量时编译通过,运行会报错,而传string变量就没有错,搞了半天没有头绪。string变量是采用应用计数和写时复制的技术,以变量为参数是没问题,但是以常量为参数就会在跳出dll函数时出错,我理解是string析构问题,但是dll有自己的数据段,像参数应该是堆栈段,是共用主程序的,理论上应该析构不会有什么问题。这里我给的解释是由于字符常量是在主函数中定义,在作为参数时他同时也会在主程序中new一个string,然后这个string被传到了dll中,dll函数结束时引用计数为0,他就析构但是他要访问主程序的内存,这不被允许所以出错,而传string变量时虽然dll函数也结束了,但引用计数不为0,他不会去释放主程序的内存空间。解决的方法就是在主程序中开辟的内存在主程序中析构,在dll中开辟的在dll中析构。
DLL的调用,网上都说了有两种一种是动态调用(也叫显示的调用),一种叫静态调用(也叫隐式调用):
动态调用:函数LoadLibrary(path),GetProcaddress(hmodule),头文件在<windows.h>,这里的path可以是dll文件的绝对路径,也可以是相对路径。绝对路径要注意windows里面用'\\'表示文件夹路径,相对路径要将dll文件放对位置,加载dll完成后有必要判断是否加载成功。导出函数要用函数指针获取,注意函数指针的声明,最后使用完后要记得释放动态库。动态加载真正体现了动态库的意义,他能有效的减小程序的代码体积,只有在用到时才调用。
静态调用:会用到dll生成时同时生成的lib文件,这个文件只有导出函数的信息,没有代码,在应用程序编译时会将它一起编进去。将lib放到工作目录下,dll放到exe同一个目录下编译。代码中要加入预编译指令#pragma comment(lib,'hid.ilb''),并且要申明导出函数,这里你就可以直接将dll的头文件引入,不需要自己申明函数了。
总结:这只是一个简单的讨论,并且稍微深入的研究了下,还有许多问题没有涉及,例如dll的入口函数有什么用,dll的debug版本和release有什么区别,还有延伸出去的com组件,导出变量和函数等等。