posts - 29, comments - 16, trackbacks - 0, articles - 0
   :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

    使用C/C++语言开发软件的程序员经常碰到这样的问题:有时候是程序编译没有问题,但是链接的时候总是报告函数不存在(经典的LNK 2001错误),有时候是程序编译和链接都没有错误,但是只要调用库中的函数就会出现堆栈异常。这些现象通常是出现在CC++的代码混合使用的情况下或 在C++程序中使用第三方的库的情况下(不是用C++语言开发的),其实这都是函数调用约定(Calling Convention)和函数名修饰(Decorated Name)规则惹的祸。函数调用方式决定了函数参数入栈的顺序,是由调用者函数还是被调用函数负责清除栈中的参数等问题,而函数名修饰规则决定了编译器使 用何种名字修饰方式来区分不同的函数,如果函数之间的调用约定不匹配或者名字修饰不匹配就会产生以上的问题。本文分别对CC++这两种编程语言的函数调 用约定和函数名修饰规则进行详细的解释,比较了它们的异同之处,并举例说明了以上问题出现的原因。

函数调用约定(Calling Convention

    函数调用约定不仅决定了发生函数调用时函数参数的入栈顺序,还决定了是由调用者函数还是被调用函数负责清除栈中的参数,还原堆栈。函数调用约定有很多方 式,除了常见的__cdecl__fastcall__stdcall之外,C++的编译器还支持thiscall方式,不少C/C++编译器还支持 naked call方式。这么多函数调用约定常常令许多程序员很迷惑,到底它们是怎么回事,都是在什么情况下使用呢?下面就分别介绍这几种函数调用约定。


1.__cdecl

    编译器的命令行参数是/Gd__cdecl方式是C/C++编译器默认的函数调用约定,所有非C++成员函数和那些没有用__stdcall __fastcall声明的函数都默认是__cdecl方式,它使用C函数调用方式,函数参数按照从右向左的顺序入栈,函数调用者负责清除栈中的参数,由 于每次函数调用都要由编译器产生清除(还原)堆栈的代码,所以使用__cdecl方式编译的程序比使用__stdcall方式编译的程序要大很多,但是 __cdecl调用方式是由函数调用者负责清除栈中的函数参数,所以这种方式支持可变参数,比如printfwindowsAPI wsprintf就是__cdecl调用方式。对于C函数,__cdecl方式的名字修饰约定是在函数名称前添加一个下划线;对于C++函数,除非特别使 用extern "C"C++函数使用不同的名字修饰方式。


2.__fastcall

    编译器的命令行参数是/Gr__fastcall函数调用约定在可能的情况下使用寄存器传递参数,通常是前两个 DWORD类型的参数或较小的参数使用ECXEDX寄存器传递,其余参数按照从右向左的顺序入栈,被调用函数在返回之前负责清除栈中的参数。编译器使用 两个@修饰函数名字,后跟十进制数表示的函数参数列表大小,例如:@function_name@number。需要注意的是__fastcall函数调 用约定在不同的编译器上可能有不同的实现,比如16位的编译器和32位的编译器,另外,在使用内嵌汇编代码时,还要注意不能和编译器使用的寄存器有冲突。


3.__stdcall
 
   
编译器的命令行参数是/Gz__stdcallPascal程序的缺省调用方式,大多数WindowsAPI也是__stdcall调用约定。 __stdcall函数调用约定将函数参数从右向左入栈,除非使用指针或引用类型的参数,所有参数采用传值方式传递,由被调用函数负责清除栈中的参数。对 于C函数,__stdcall的名称修饰方式是在函数名字前添加下划线,在函数名字后添加@和函数参数的大小,例如:_functionname@number

4.thiscall

    thiscall只用在C++成员函数的调用,函数参数按照从右向左的顺序入栈,类实例的this指针通过ECX寄存器传递。需要注意的是thiscall不是C++的关键字,不能使用thiscall声明函数,它只能由编译器使用。

5.naked call

    采用前面几种函数调用约定的函数,编译器会在必要的时候自动在函数开始添加保存ESIEDIEBXEBP寄存器的代码,在退出函数时恢复这些寄存器 的内容,使用naked call方式声明的函数不会添加这样的代码,这也就是为什么称其为naked的原因吧。naked  call不是类型修饰符,故必须和_declspec共同使用。

    VC的编译环境默认是使用__cdecl调用约定,也可以在编译环境的Project Setting...菜单-》C/C++ =》Code  Generation项选择设置函数调用约定。也可以直接在函数声明前添加关键字__stdcall__cdecl__fastcall等单独确定函 数的调用方式。在Windows系统上开发软件常用到WINAPI宏,它可以根据编译设置翻译成适当的函数调用约定,在WIN32中,它被定义为 __stdcall 

 

函数名字修饰(Decorated Name)方式

    函数的名字修饰(Decorated Name)就是编译器在编译期间创建的一个字符串,用来指明函数的定义或原型。LINK程序或其他工具有时需要指定函数的名字修饰来定位函数的正确位置。 多数情况下程序员并不需要知道函数的名字修饰,LINK程序或其他工具会自动区分他们。当然,在某些情况下需要指定函数的名字修饰,例如在C++程序中, 为了让LINK程序或其他工具能够匹配到正确的函数名字,就必须为重载函数和一些特殊的函数(如构造函数和析构函数)指定名字装饰。另一种需要指定函数的 名字修饰的情况是在汇编程序中调用CC++的函数。如果函数名字,调用约定,返回值类型或函数参数有任何改变,原来的名字修饰就不再有效,必须指定新的 名字修饰。CC++程序的函数在内部使用不同的名字修饰方式,下面将分别介绍这两种方式。

1. C编译器的函数名修饰规则

    对于__stdcall调用约定,编译器和链接器会在输出函数名前加上一个下划线前缀,函数名后面加上一个“@”符号和其参数的字节数,例如_functionname@number__cdecl调用约定仅在输出函数名前加上一个下划线前缀,例如_functionname__fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,例如@functionname@number   
 
2. C++
编译器的函数名修饰规则

    C++的函数名修饰规则有些复杂,但是信息更充分,通过分析修饰名不仅能够知道函数的调用方式,返回值类型,参数个数甚至参数类型。不管 __cdecl__fastcall还是__stdcall调用方式,函数修饰都是以一个“?”开始,后面紧跟函数的名字,再后面是参数表的开始标识和 按照参数类型代号拼出的参数表。对于__stdcall方式,参数表的开始标识是“@@YG,对于__cdecl方式则是“@@YA,对于__fastcall方式则是“@@YI。参数表的拼写代号如下所示:
X--void   
D--char   
E--unsigned char   
F--short   
H--int   
I--unsigned int   
J--long   
K--unsigned long
DWORD
M--float   
N--double   
_N--bool
U--struct
....
指 针的方式有些特别,用PA表示指针,用PB表示const类型的指针。后面的代号表明指针类型,如果相同类型的指针连续出现,以0”代替,一个0”代 表一次重复。U表示结构类型,通常后跟结构体的类型名,用“@@”表示结构类型名的结束。函数的返回值不作特殊处理,它的描述方式和函数参数一样,紧跟着 参数表的开始标志,也就是说,函数参数表的第一项实际上是表示函数的返回值类型。参数表后以“@Z标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。下面举两个例子,假如有以下函数声明:

int Function1(char *var1,unsigned long);

其函数修饰名为“?Function1@@YGHPADK@Z,而对于函数声明:

void Function2();

其函数修饰名则为“?Function2@@YGXXZ

 

    对于C++的类成员函数(其调用方式是thiscall),函数的名字修饰与非成员的C++函数稍有不同,首先就是在函数名字和参数表之间插入以“@”字符引导的类名;其次是参数表的开始标识不同,公有(public)成员函数的标识是“@@QAE”,保护(protected)成员函数的标识是“@@IAE”,私有(private)成员函数的标识是“@@AAE,如果函数声明使用了const关键字,则相应的标识应分别为“@@QBE“@@IBE“@@ABE。如果参数类型是类实例的引用,则使用“AAV1”,对于const类型的引用,则使用“ABV1”。下面就以类CTest为例说明C++成员函数的名字修饰规则:

class CTest
{
......
private:
    void Function(int);
protected:
    void CopyInfo(const CTest &src);
public:
    long DrawText(HDC hdc, long pos, const TCHAR* text, RGBQUAD color, BYTE bUnder, bool bSet);
    long InsightClass(DWORD dwClass) const;
......
};

对于成员函数Function,其函数修饰名为“?Function@CTest@@AAEXH@Z,字符串“@@AAE表示这是一个私有函数。成员函数CopyInfo只有一个参数,是对类CTestconst引用参数,其函数修饰名为“?CopyInfo@CTest@@IAEXABV1@@Z DrawText是一个比较复杂的函数声明,不仅有字符串参数,还有结构体参数和HDC句柄参数,需要指出的是HDC实际上是一个HDC__结构类型的指 针,这个参数的表示就是“PAUHDC__@@”,其完整的函数修饰名为“?DrawText@CTest@@QAEJPAUHDC__@@JPBDUtagRGBQUAD@@E_N@ZInsightClass是一个共有的const函数,它的成员函数标识是“@@QBE,完整的修饰名就是“?InsightClass@CTest@@QBEJK@Z

无论是C函数名修饰方式还是C++函数名修饰方式均不改变输出函数名中的字符大小写,这和PASCAL调用约定不同,PASCAL约定输出的函数名无任何修饰且全部大写。

3.查看函数的名字修饰

    有两种方式可以检查你的程序中的函数的名字修饰:使用编译输出列表或使用Dumpbin工具。使用/FAc/FAs/FAcs命令行参数可以让编译器 输出函数或变量名字列表。使用dumpbin.exe /SYMBOLS命令也可以获得obj文件或lib文件中的函数或变量名字列表。此外,还可以使用 undname.exe 将修饰名转换为未修饰形式。

 

函数调用约定和名字修饰规则不匹配引起的常见问题

    函数调用时如果出现堆栈异常,十有八九是由于函数调用约定不匹配引起的。比如动态链接库a有以下导出函数:

long MakeFun(long lFun);

动态库生成的时候采用的函数调用约定是__stdcall,所以编译生成的a.dll中函数MakeFun的调用约 定是_stdcall,也就是函数调用时参数从右向左入栈,函数返回时自己还原堆栈。现在某个程序模块b要引用a中的MakeFunba一样使用 C++方式编译,只是b模块的函数调用方式是__cdecl,由于b包含了a提供的头文件中MakeFun函数声明,所以MakeFunb模块中被其它 调用MakeFun的函数认为是__cdecl调用方式,b模块中的这些函数在调用完MakeFun当然要帮着恢复堆栈啦,可是MakeFun已经在结束 时自己恢复了堆栈,b模块中的函数这样多此一举就引起了栈指针错误,从而引发堆栈异常。宏观上的现象就是函数调用没有问题(因为参数传递顺序是一样 的),MakeFun也完成了自己的功能,只是函数返回后引发错误。解决的方法也很简单,只要保证两个模块的在编译时设置相同的函数调用约定就行了。

 

    在了解了函数调用约定和函数的名修饰规则之后,再来看在C++程序中使用C语言编译的库时经常出现的LNK 2001错误就很简单了。还以上面例子的两个模块为例,这一次两个模块在编译的时候都采用__stdcall调用约定,但是a.dll使用C语言的语法编 译的(C语言方式),所以a.dll的载入库a.libMakeFun函数的名字修饰就是“_MakeFun@4b包含了a提供的头文件中MakeFun函数声明,但是由于b采用的是C++语言编译,所以MakeFunb模块中被按照C++的名字修饰规则命名为“?MakeFun@@YGJJ@Z,编译过程相安无事,链接程序时c++的链接器就到a.lib中去找“?MakeFun@@YGJJ@Z,但是a.lib中只有“_MakeFun@4,没有“?MakeFun@@YGJJ@Z,于是链接器就报告:

error LNK2001: unresolved external symbol ?MakeFun@@YGJJ@Z

解决的方法和简单,就是要让b模块知道这个函数是C语言编译的,extern "C"可以做到这一点。一个采用C语言编译的库应该考虑到使用这个库的程序可能是C++程序(使用C++编译器),所以在设计头文件时应该注意这一点。通常应该这样声明头文件:

#ifdef _cplusplus
extern "C" {
#endif

long MakeFun(long lFun);

#ifdef _cplusplus
}
#endif

这样C++的编译器就知道MakeFun的修饰名是“_MakeFun@4,就不会有链接错误了。

    许多人不明白,为什么我使用的编译器都是VC的编译器还会产生“error LNK2001”错误?其实,VC的编译器会根据源文件的扩展名选择编译方式,如果文件的扩展名是“.C”,编译器会采用C的语法编译,如果扩展名是 “.cpp”,编译器会使用C++的语法编译程序,所以,最好的方法就是使用extern "C"

 

posted @ 2009-02-15 10:43 王勇良 阅读(1080) | 评论 (0)编辑 收藏

1. ASCII码

我们知道,在计算机内部,所有的信息最终都表示为一个二进制的字符串。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出 256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从 0000000到11111111。

上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为ASCII码,一直沿用至今。

ASCII码一共规定了128个字符的编码,比如空格“SPACE”是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。

2、非ASCII编码

英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用ASCII码表示。 于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使 用的编码体系,可以表示最多256个符号。

但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码 中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0—127表示的符号是一样的,不一样的只是128—255的这一段。

至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。 比如,简体中文常见的编码方式是GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示256x256=65536个符号。

中文编码的问题需要专文讨论,这篇笔记不涉及。这里只指出,虽然都是用多个字节表示一个符号,但是GB类的汉字编码与后文的Unicode和UTF-8是毫无关系的。

3.Unicode

正如上一节所说,世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。

可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是Unicode,就像它的名字都表示的,这是一种所有符号的编码。

Unicode当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字“严”。具体的符号对应表,可以查询unicode.org,或者专门的汉字对应表

4. Unicode的问题

需要注意的是,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。

比如,汉字“严”的unicode是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。

这里就有两个严重的问题,第一个问题是,如何才能区别unicode和ascii?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号 呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必 然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。

它们造成的结果是:1)出现了unicode的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示unicode。2)unicode在很长一段时间内无法推广,直到互联网的出现。

5.UTF-8

互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种unicode的实现方式。其他实现方式还包括UTF-16和UTF-32,不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一。

UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。

UTF-8的编码规则很简单,只有二条:

1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。

2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。

下表总结了编码规则,字母x表示可用编码的位。

Unicode符号范围 | UTF-8编码方式
(十六进制) | (二进制)
--------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

下面,还是以汉字“严”为例,演示如何实现UTF-8编码。

已知“严”的unicode是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此“严”的UTF-8编码需要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严”的UTF-8编码是 “11100100 10111000 10100101”,转换成十六进制就是E4B8A5。

6. Unicode与UTF-8之间的转换

通过上一节的例子,可以看到“严”的Unicode码是4E25,UTF-8编码是E4B8A5,两者是不一样的。它们之间的转换可以通过程序实现。

在Windows平台下,有一个最简单的转化方法,就是使用内置的记事本小程序Notepad.exe。打开文件后,点击“文件”菜单中的“另存为”命令,会跳出一个对话框,在最底部有一个“编码”的下拉条。

bg2007102801.jpg

里面有四个选项:ANSI,Unicode,Unicode big endian 和 UTF-8。

1)ANSI是默认的编码方式。对于英文文件是ASCII编码,对于简体中文文件是GB2312编码(只针对Windows简体中文版,如果是繁体中文版会采用Big5码)。

2)Unicode编码指的是UCS-2编码方式,即直接用两个字节存入字符的Unicode码。这个选项用的little endian格式。

3)Unicode big endian编码与上一个选项相对应。我在下一节会解释little endian和big endian的涵义。

4)UTF-8编码,也就是上一节谈到的编码方法。

选择完”编码方式“后,点击”保存“按钮,文件的编码方式就立刻转换好了。

7. Little endian和Big endian

上一节已经提到,Unicode码可以采用UCS-2格式直接存储。以汉字”严“为例,Unicode码是4E25,需要用两个字节存储,一个字节 是4E,另一个字节是25。存储的时候,4E在前,25在后,就是Big endian方式;25在前,4E在后,就是Little endian方式。

这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big- Endian)敲开还是从小头(Little-Endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。

因此,第一个字节在前,就是”大头方式“(Big endian),第二个字节在前就是”小头方式“(Little endian)。

那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?

Unicode规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格“(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。这正好是两个字节,而且FF比FE大1。

如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。

8. 实例

下面,举一个实例。

打开”记事本“程序Notepad.exe,新建一个文本文件,内容就是一个”严“字,依次采用ANSI,Unicode,Unicode big endian 和 UTF-8编码方式保存。

然后,用文本编辑软件UltraEdit中的”十六进制功能“,观察该文件的内部编码方式。

1)ANSI:文件的编码就是两个字节“D1 CF”,这正是“严”的GB2312编码,这也暗示GB2312是采用大头方式存储的。

2)Unicode:编码是四个字节“FF FE 25 4E”,其中“FF FE”表明是小头方式存储,真正的编码是4E25。

3)Unicode big endian:编码是四个字节“FE FF 4E 25”,其中“FE FF”表明是大头方式存储。

4)UTF-8:编码是六个字节“EF BB BF E4 B8 A5”,前三个字节“EF BB BF”表示这是UTF-8编码,后三个“E4B8A5”就是“严”的具体编码,它的存储顺序与编码顺序是一致的。

9. 延伸阅读

* The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets(关于字符集的最基本知识)

* 谈谈Unicode编码

* RFC3629:UTF-8, a transformation format of ISO 10646(如果实现UTF-8的规定)

posted @ 2009-02-15 10:38 王勇良 阅读(808) | 评论 (0)编辑 收藏

如果你用C++来编写COM,那么你将必不可少的使用这三个类型。使用这三种wrapper class毫无疑问会简化我们的编程,使得使用SAFEARRAY, VARIANT和BSTR简单。但是,使用这三个类型依然需要小心,因为使用不当的话,就会造成内存泄漏,或效率降低。

1. 如果拷贝两个BSTR
假如我们一个BSTR,这个时候我希望复制一份BSTR,并丢弃之前的BSTR。通常我们会这么写:
CComBSTR StringToBSTR(const string & sVal)
{
     CComBSTR bstrValue 
= sVal.data();
     
return bstrValue;
}

int main()
{
     CComBSTR vValue 
= StringToBSTR("value");

     
return 0;
}

当然,上面这个程序没有任何问题,不会有任何内存泄漏的可能。但是,你有没有上面代码里都发生了什么了?
答案很简单,在函数StringToBSTR里面,讲bstrValue返回的时候,会调用CComBSTR::Copy(),在Copy()里面将会调用
 ::SysAllocStringByteLen()
这个函数。而后在给vValue赋值的时候,又 会调用一次
::SysAllocString()
显而易见,开销很大。

那么,我们将怎么改进这段代码了?
BSTR StringToBSTR(const string & sVal)
{
     CComBSTR bstrValue 
= sVal.data();
     
return bstrValue.Detach();
}

int main()
{
     CComBSTR vValue.Attach(StringToBSTR(
"value"));

     
return 0;
}
这样,通过CComBSTR::Detach(),我们将BSTR返回回来,通过CComBSTR::Attach(),我们将BSTR指针存储起来。这样,就减小了两次开销,大大提高了效率,也不会造成内存效率。

2. 如何使用CComSafeArray
使 用CComSafeArray的一个最大的好处,就是它会自动释放元素是VARIANT和BSTR。也就是说,如果你的类型是VARIANT,它会自动调 用::VariantClear()。如果你的类型是BSTR,他会自动调用::SysStringFree()方法。但是使用它的时候,同样要小心。
2.1 成对使用::SafeArrayAccessData()和::SafeArrayUnaccessData()
我们有时候会这样使用CComSafeArray的元素:
void DoSomething()
{
     CComSafeArray
<double> pSafeArray(3);
     
double * pVal = NULL;
     ::SafeArrayAccessData(pSafeArray
.m_psa, (void**)&pVal);

     
//handle the elements through the pVal;
}
因为::SafeArrayAccessData 方法会在SFAEARRAY上给lock加1. 如果上面程序显示调用CComSafeArray::Destroy()函数,你检查它返回来的HRESULT的时候,应该是下面的值:
        hr    0x8002000d 内存已锁定。     HRESULT
如果你不仔细检查,那么将造成CComSafeArray没有释放。
2.2 从CComSafeArray转为成CComVariant
有时候我们使用CComVariant包装SAFEARRY。你会这样写代码:
void DoSomething()
{
     CComSafeArray
<double> pSafeArray(3);
    
     
//fill the safearray

     CComVariant v 
= pSafeArray.Detach();
}
你可能会任务CComVariant会存储pSafeArray的指针。可惜,你错了。
CComVariant会调用::SafeArrayCopy 来完成赋值操作。而你的pSafeArray已经调用了Detach()操作,那么它里面的SAFEARRAY就变成了孤儿,没有人去释放它了。
那么你应该怎么写了?
你可以这么写:
void DoSomething()
{
     CComSafeArray
<double> pSafeArray(3);
    
     
//fill the safearray

     CComVariant v 
= pSafeArray.m_psa;
}
这样,CComVariant会调用::SafeArrayCopy 来完成复制操作,而CComSafeArray也会保证在析构的时候释放里面的SAFEARRAY。

使用上面三个wrapper类,确实可以很方便我们编程,也能避免很多memory leak。但是,使用他们同样要小心,不然,同样会造成性能损失,或者,更糟糕的,内存泄漏。

posted @ 2009-02-15 10:37 王勇良 阅读(2547) | 评论 (0)编辑 收藏

一 static 产生背景

引出原因:函数内部定义的变量,在程序执行到它的定义处时,编译器为它在栈上分配空间,大家知道,函数

在栈上分配的空间在此函数执行结束时会释放掉,这样就产生了一个问题: 如果想将函数中此变量的值保存至

下一次调用时,如何实现?

最容易想到的方法是定义一个全局的变量,但定义为一个全局变量有许多缺点,最明显的缺点是破坏了此变量

的访问范围(使得在此函数中定义的变量,不仅仅受此函数控制)。

       类的静态成员也是这个道理。


解决方案:因此C++ 中引入了static,用它来修饰变量,它能够指示编译

器将此变量在程序的静态存储区分配空间保存,这样即实现了目的,又使得此变量的存取范围不变。


2) 具体作用

Static作用分析总结:static总是使得变量或对象的存储形式变成静态存储,连接方式变成内部连接,对于局

部变量(已经是内部连接了),它仅改变其存储方式;对于全局变量(已经是静态存储了),它仅改变其连接

类型。(1 连接方式:成为内部连接;2 存储形式:存放在静态全局存储区)
二 const 产生背景

a) C++有一个类型严格的编译系统,这使得C++程序的错误在编译阶段即可发现许多,从而使得出错率大为减少

,因此,也成为了C++与C相比,有着突出优点的一个方面。

b) C中很常见的预处理指令 #define VariableName VariableValue 可以很方便地进行值替代,这种值替代至

少在三个方面优点突出:

一是避免了意义模糊的数字出现,使得程序语义流畅清晰,如下例:

  #define USER_NUM_MAX 107 这样就避免了直接使用107带来的困惑。

二是可以很方便地进行参数的调整与修改,如上例,当人数由107变为201时,改动此处即可;

三是提高了程序的执行效率,由于使用了预编译器进行值替代,并不需要为这些常量分配存储空间,所以执行

的效率较高。

然而,预处理语句虽然有以上的许多优点,但它有个比较致命的缺点,即,预处理语
句仅仅只是简单值替代,缺乏类型的检测机制。这样预处理语句就不能享受C++严
格类型检查的好处,从而可能成为引发一系列错误的隐患。


Const 推出的初始目的,正是为了取代预编译指令,消除它的缺点,同时

继承它的优点。

现在它的形式变成了:

Const DataType VariableName = VariableValue ;

2) 具体作用

1.const 用于指针的两种情况分析:

 
int const *A;  //A可变,*A不可变

 
int *const A;  //A不可变,*A可变

 分析:const 是一个左结合的类型修饰符,它与其左侧的类型修饰符和为一个

类型修饰符,所以,int const 限定 *A,不限定A。int *const 限定A,不限定*A。


2.const 限定函数的传递值参数:

 
void Fun(const int Var);
     分析:上述写法限定参数在函数体中不可被改变。


3.const 限定函数的值型返回值:

const int Fun1();
const MyClass Fun2();
     分析:上述写法限定函数的返回值不可被更新,当函数返回内部的类型时(如Fun1),已经是一个数值,

当然不可被赋值更新,所以,此时const无意义,最好去掉,以免困惑。当函数返回自定义的类型时(如Fun2)

,这个类型仍然包含可以被赋值的变量成员,所以,此时有意义。


4. 传递与返回地址: 此种情况最为常见,由地址变量的特点可知,适当使用const,意义昭然。


5. const 限定类的成员函数:

class ClassName {
 public:
  int Fun() const;
 .....
}
  注意:采用此种const 后置的形式是一种规定,亦为了不引起混淆。在此函数的声明中和定义中均要使用

const,因为const已经成为类型信息的一部分。

获得能力:可以操作常量对象。

失去能力:不能修改类的数据成员,不能在函数中调用其他不是const的函数。

三 inline 产生背景

inline这个关键字的引入原因和const十分相似,inline 关键字用来定义一个类的内联函数,引入它的主要原

因是用它替代C中

表达式形式的宏定义。

表达式形式的宏定义一例:

   #define ExpressionName(Var1,Var2) (Var1+Var2)*(Var1-Var2)
       这种表达式形式宏形式与作用跟函数类似,但它使用预编译器,没有堆栈,使用上比函数高效。但它只

是预编译器上符号表的简单替换,不能进行参数有效性检测及使用C++类的成员访问控制。

inline 推出的目的,也正是为了取代这种表达式形式的宏定义,它消除了它的缺点,同时又很好地继承了它的

优点。inline代码放入预编译器符号表中,高效;它是个真正的函数,调用时有严格的参数检测;它也可作为

类的成员函数。


2) 具体作用

直接在class类定义中定义各函数成员,系统将他们作为内联函数处理; 成员函数是内联函数,意味着:每个

对象都有该函数一份独立的拷贝。
在类外,如果使用关键字inline定义函数成员,则系统也会作为内联函数处理;

C关键字
#define 宏名
要替换的代码

宏定义,保存在预编译器的符号表中,执行高效;作为一种简单的符号替换,不进行其中参数有效性的检测


typedef
已有类型
新类型

别名,
常用于创建平台无关类型, typedef 在编译时被解释,因此让编译器来应付超越预处理器能力的文本替换。

posted @ 2009-02-15 10:32 王勇良 阅读(293) | 评论 (0)编辑 收藏

C 风格(C-style)强制转型如下:
(T) exdivssion // cast exdivssion to be of type T
函数风格(Function-style)强制转型使用这样的语法:
T(exdivssion) // cast exdivssion to be of type T

这两种形式之间没有本质上的不同,它纯粹就是一个把括号放在哪的问题。我把这两种形式称为旧风格(old-style)的强制转型。


使用标准C++的类型转换符:static_cast、dynamic_cast、reinterdivt_cast、和const_cast。
1. static_cast
用法:static_cast < type-id > ( exdivssion )
该运算符把exdivssion转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:
①用于类层次结构中基类和子类之间指针或引用的转换。
  进行上行转换(把子类的指针或引用转换成基类表示)是安全的;
  进行下行转换(把基类指针或引用转换成子类表示)时,由于没有动态类型检查,所以是不安全的。
②用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
③把空指针转换成目标类型的空指针。
④把任何类型的表达式转换成void类型。
注意:static_cast不能转换掉exdivssion的const、volitale、或者__unaligned属性。

2. dynamic_cast
用法:dynamic_cast < type-id > ( exdivssion )
该运算符把exdivssion转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void *;
如果type-id是类指针类型,那么exdivssion也必须是一个指针,如果type-id是一个引用,那么exdivssion也必须是一个引用。
dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。
在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;
在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。
class B{
public:
int m_iNum;
virtual void foo();
};
class D:public B{
public:
char *m_szName[100];
};
void func(B *pb){
D *pd1 = static_cast(pb);
D *pd2 = dynamic_cast(pb);
}
在上面的代码段中,如果pb指向一个D类型的对象,pd1和pd2是一样的,并且对这两个指针执行D类型的任何操作都是安全的;
但是,如果pb指向的是一个B类型的对象,那么pd1将是一个指向该对象的指针,对它进行D类型的操作将是不安全的(如访问m_szName),
而pd2将是一个空指针。
另外要注意:B要有虚函数,否则会编译出错;static_cast则没有这个限制。
这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表(
关于虚函数表的概念,详细可见)中,只有定义了虚函数的类才有虚函数表,
没有定义虚函数的类是没有虚函数表的。
另外,dynamic_cast还支持交叉转换(cross cast)。如下代码所示。
class A{
public:
int m_iNum;
virtual void f(){}
};
class B:public A{
};
class D:public A{
};
void foo(){
B *pb = new B;
pb->m_iNum = 100;
D *pd1 = static_cast(pb); //compile error
D *pd2 = dynamic_cast(pb); //pd2 is NULL
delete pb;
}
在函数foo中,使用static_cast进行转换是不被允许的,将在编译时出错;而使用 dynamic_cast的转换则是允许的,结果是空指针。

3. reindivter_cast
用法:reindivter_cast (exdivssion)
type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。
它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,
在把该整数转换成原类型的指针,还可以得到原先的指针值)。
该运算符的用法比较多。
4. const_cast
用法:const_cast (exdivssion)
该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和exdivssion的类型是一样的。
常量指针被转化成非常量指针,并且仍然指向原来的对象;
常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。
Voiatile和const类试。举如下一例:
class B{
public:
int m_iNum;
}
void foo(){
const B b1;
b1.m_iNum = 100; //comile error
B b2 = const_cast(b1);
b2. m_iNum = 200; //fine
}
上面的代码编译时会报错,因为b1是一个常量对象,不能对它进行改变;
使用const_cast把它转换成一个常量对象,就可以对它的数据成员任意改变。注意:b1和b2是两个不同的对象。

== ===========================================
== dynamic_cast .vs. static_cast
== ===========================================

class B { ... };
class D : public B { ... };

void f(B* pb)
{
D* pd1 = dynamic_cast(pb);
D* pd2 = static_cast(pb);
}


If pb really points to an object of type D, then pd1 and pd2 will get the same value. They will also get the same value if pb == 0.


If pb points to an object of type B and not to the complete D class, then dynamic_cast will know enough to return zero. However, static_cast relies on the programmer’s assertion that pb points to an object of type D and simply returns a pointer to that supposed D object.


即dynamic_cast可用于继承体系中的向下转型,即将基类指针转换为派生类指针,比static_cast更严格更安全。dynamic_cast在执行效率上比static_cast要差一些,但static_cast在更宽上范围内可以完成映射,这种不加限制的映射伴随着不安全性。static_cast覆盖的变换类型除类层次的静态导航以外,还包括无映射变换、窄化变换(这种变换会导致对象切片,丢失信息)、用VOID*的强制变换、隐式类型变换等...


== ===========================================
== static_cast .vs. reinterdivt_cast
== ================================================


reinterdivt_cast是为了映射到一个完全不同类型的意思,这个关键词在我们需要把类型映射回原有类型时用到它。我们映射到的类型仅仅是为了故弄玄虚和其他目的,这是所有映射中最危险的。(这句话是C++编程思想中的原话)
static_cast 和 reinterdivt_cast 操作符修改了操作数类型。它们不是互逆的; static_cast 在编译时使用类型信息执行转换,在转换执行必要的检测(诸如指针越界计算, 类型检查). 其操作数相对是安全的。另一方面;reinterdivt_cast 仅仅是重新解释了给出的对象的比特模型而没有进行二进制转换, 例子如下:


int n=9; double d=static_cast < double > (n);


上面的例子中, 我们将一个变量从 int 转换到 double。 这些类型的二进制表达式是不同的。 要将整数 9 转换到 双精度整数 9,static_cast 需要正确地为双精度整数 d 补足比特位。其结果为 9.0。而reinterdivt_cast 的行为却不同:


int n=9;
double d=reinterdivt_cast (n);

这次, 结果有所不同. 在进行计算以后, d 包含无用值. 这是因为 reinterdivt_cast 仅仅是复制 n 的比特位到 d, 没有进行必要的分析.


因此, 你需要谨慎使用 reinterdivt_cast. 


posted @ 2009-02-15 10:30 王勇良 阅读(288) | 评论 (0)编辑 收藏

仅列出标题
共6页: 1 2 3 4 5 6