还有一种方法可以显式导出类成员函数,就是采用虚函数表的方法。 COM 就是这样做的。当在类中声明一组虚函数的时候, 编译器会创建一个虚函数表,将各虚函数的地址按声明的顺序放入其中。当一个类对象被创建时,它的前四个字节是一个指针,指向这个虚函数表。所以,修改以上的代码:
//CTry.h
…
virtual void print() ; // 将这个成员函数声明为虚函数
TRY_API CTry* createObject() ; // 声明一个全局函数,创建一个 Ctry 对象的实例
//CTry.cpp
CTry *createObject()
{
return new CTry() ;
}
在测试工程中,显式导出该全局函数,并通过它调用类成员函数。
typedef CTry* (*Fn_FunType)() ;
Fn_FunType pFun=(Fn_FunType)::GetProcAddress(hInstance ,"?createObject@@YAPAVCTry@@XZ") ;
CTry * pTry = pFun() ;
pTry ->print() ;
delete pTry ;
这个方法虽然同样能达到显式导出类成员函数的目的。但是上面的方法有个缺点,就是在DLL中new 出来的对象,却要放到客户端释放。因为客户端并不知道DLL中的实现方法,很可能会不知道释放这个对象,从而造成内存泄露。<<Effiective C++>>中明文规定禁止这样做。
现在要找一个方法可以把回收内存的任务交 DLL 自己来处理。有没有这样一个方法呢?当然有,它就是引用计数。引用计数是一个简单的垃圾回收机制,它的原理很简单,组件内部维护着一个引用计数的数值,当用户从组件取得一个接口时,该数值 +1 ,当用户使用完这个接口,并释放该接口时,该数值 -1 ,当该数值为 0 时,组件将自己从内存中删除。下面是采用了引用计数的 DLL 实现方法。
上面的方法中,用户不能通过构造函数来实例化一个对象,必须通过 createObject 接口才能实例化对象,当用户使用完这个对象时必须调用 removeRef() 接口。代码如下:
当然上面只是我为了方便测试而写的一个简单的例子,还不完善。引用计数还有很多细节和注意事项。具体请看《more effective c++》第29项。
关于名字修饰约定
前面代码我都是通过编译器中的修饰名来调用相应的函数的,修饰名是编译器在编译函数定义或者原型时生成的字符串。比如 "?createObject@@YAPAVCTry@@XZ" 就是 CTry::createObject 的修饰名。
名字修饰约定随调用约定和编译种类 (C 或 C++) 的不同而变化。函数名修饰约定随编译种类和调用约定的不同而不同,下面分别说明。
A 、 C 编译时函数名修饰约定规则:
__stdcall 调用约定在输出函数名前加上一个下划线前缀,后面加上一个 "@" 符号和其参数的字节数,格式为 _functionname@number 。
__cdecl 调用约定仅在输出函数名前加上一个下划线前缀,格式为 _functionname 。
__fastcall 调用约定在输出函数名前加上一个 "@" 符号,后面也是一个 "@" 符号和其参数的字节数,格式为 @functionname@number 。
它们均不改变输出函数名中的字符大小写,这和 PASCAL 调用约定不同, PASCAL 约定输出的函数名无任何修饰且全部大写。
B 、 C++ 编译时函数名修饰约定规则:
__stdcall 调用约定:
1 、以 "?" 标识函数名的开始,后跟函数名;
2 、函数名后面以 "@@YG" 标识参数表的开始,后跟参数表;
3 、参数表以代号表示:
X--void ,
D--char ,
E--unsigned char ,
F--short ,
H--int ,
I--unsigned int ,
J--long ,
K--unsigned long ,
M--float ,
N--double ,
_N--bool ,
....
PA-- 表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以 "0" 代替,一个 "0" 代表一次重复;
4 、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型 , 指针标识在其所指数据类型前;
5 、参数表后以 "@Z" 标识整个名字的结束,如果该函数无参数,则以 "Z" 标识结束。
其格式为 "?functionname@@YG*****@Z" 或 "?functionname@@YG*XZ" ,例如
int Test1 ( char *var1,unsigned long ) ----- “ ?Test1@@YGHPADK@Z ”
void Test2 () ----- “ ?Test2@@YGXXZ ”
__cdecl 调用约定:
规则同上面的 _stdcall 调用约定,只是参数表的开始标识由上面的 "@@YG" 变为 "@@YA" 。
__fastcall 调用约定:
规则同上面的 _stdcall 调用约定,只是参数表的开始标识由上面的 "@@YG" 变为 "@@YI" 。 VC++ 对函数的省缺声明是 "__cedcl", 将只能被 C/C++ 调用 .
通常我们希望我们的 DLL 中的导出函数名能够更易被识别(用户使用才会更方便),也就是说 DLL 应该编译出无修饰的 C 函数名,而不复杂的修饰名。所以当使用 C++ 文件来创建 DLL 时应该使用 extern “c” 来修饰导出函数和变量。实际上 VS 编译器已经为我们准备了一个宏 EXTERN_C 用来代替 extern “c” 。修改以上的代码如下:
EXTERN_C TRY_API int nTry;
EXTERN_C TRY_API int fnTry(void);
生成 DLL ,用 depens 工具查看,他们已经变成了如下模样:
这样在客户端就可以通过函数名去调用它们。但是 EXTERN_C 宏却没办法修饰类成员函数,
如果像全局函数那样修饰类成员函数,编译无法通过。用 .DEF 文件可以解决这个问题。
关于 .def 文件
模块定义 (.def) 文件是包含一个或多个描述 DLL 各种属性的 Module 语句的文本文件。
def 文件包含下列模块定义语句:
1. 文件中的第一个语句必须是 LIBRARY 语句。此语句将 .def 文件标识为属于 DLL 。 LIBRARY 语句的后面是 DLL 的名称。链接器将此名称放到 DLL 的导入库中。
2. EXPORTS 语句列出被导出函数的名字;将要输出的函数修饰名罗列在 EXPORTS 之下,这个名字必须与定义函数的名字完全一致,如此就得到一个没有任何修饰的函数名了。
3. 可以使用 DESCRIPTION 语句描述 DLL 的用途 ( 此句可选 ) ;
4. ";" 对一行进行注释 ( 可选 ) 。
创建一 .DEF 文件,命名为 export.def ,用来修饰 CTry::print 函数,如下:
LIBRARY "Try"
EXPORTS
print = ?print@CTry@@UAEXXZ PRIVATE
在 DLL 工程属性 -> 链接器 -> 输入 -> 模块定义文件中将 export.def 添加进去。这样就可以直接通过函数名来显式调用类成员函数了。下面是 DLL 在 depens 工具中显示的情况。
参考资料:
《 windows 核心编程》
《 programming windows 》
《微软 DLL 专题》