前言
字符串的表现形式各异,象TCHAR,std::string,BSTR等等,有时还会见到怪怪的用_tcs起头的宏。这个指南的目的就是说明各种字符串类型及其用途,并说明如何在必要时进行类型的相互转换。
在指南的第一部分,介绍三种字符编码格式。理解编码的工作原理是致为重要的。即使你已经知道字符串是一个字符的数组这样的概念,也请阅读本文,它会让你明白各种字符串类之间的关系。
指南的第二部分,将阐述各个字符串类,什么时候使用哪种字符串类,及其相互转换。
字符串基础 - ASCII, DBCS, Unicode
所有的字符串类都起源于C语言的字符串,而C语言字符串则是字符的数组。首先了解一下字符类型。有三种编码方式和三种字符类型。
第一种编码方式是单字节字符集,称之为SBCS,它的所有字符都只有一个字节的长度。ASCII码就是SBCS。SBCS字符串由一个零字节结尾。
第二种编码方式是多字节字符集,称之为MBCS,它包含的字符中有单字节长的字符,也有多字节长的字符。Windows用到的MBCS只有二种字符类型,单字节字符和双字节字符。因此Windows中用得最多的字符是双字节字符集,即DBCS,通常用它来代替MBCS。
在DBCS编码中,用一些保留值来指明该字符属于双字节字符。例如,Shift-JIS(通用日语)编码中,值0x81-0x9F 和 0xE0-0xFC 的意思是:“这是一个双字节字符,下一个字节是这个字符的一部分”。这样的值通常称为前导字节(lead byte),总是大于0x7F。前导字节后面是跟随字节(trail byte)。DBCS的跟随字节可以是任何非零值。与SBCS一样,DBCS字符串也由一个零字节结尾。
第三种编码方式是Unicode。Unicode编码标准中的所有字符都是双字节长。有时也将Unicode称为宽字符集(wide characters),因为它的字符比单字节字符更宽(使用更多内存)。注意,Unicode不是MBCS - 区别在于MBCS编码中的字符长度是不同的。Unicode字符串用二个零字节字符结尾(一个宽字符的零值编码)。
单字节字符集是拉丁字母,重音文字,用ASCII标准定义,用于DOS操作系统。双字节字符集用于东亚和中东语言。Unicode用于COM和Windows NT内部。
读者都很熟悉单字节字符集,它的数据类型是char。双字节字符集也使用char数据类型(双字节字符集中的许多古怪处之一)。Unicode字符集用wchar_t数据类型。Unicode字符串用L前缀起头,如:
wchar_t wch = L'1'; // 2 个字节, 0x0031
wchar_t* wsz = L"Hello"; // 12 个字节, 6 个宽字符
字符串的存储
单字节字符串顺序存放各个字符,并用零字节表示字符串结尾。例如,字符串"Bob"的存储格式为:
Unicode编码中,L"Bob"的存储格式为:
用0x0000 (Unicode的零编码)结束字符串。
DBCS 看上去有点象SBCS。以后我们会看到在串处理和指针使用上是有微妙差别的。字符串"日本语" (nihongo) 的存储格式如下(用LB和TB分别表示前导字节和跟随字节):
注意,"ni"的值不是WORD值0xFA93。值93和FA顺序组合编码为字符"ni"。(在高位优先CPU中,存放顺序正如上所述)。
字符串处理函数
C语言字符串处理函数,如strcpy(), sprintf(), atol()等只能用于单字节字符串。在标准库中有只用于Unicode字符串的函数,如wcscpy(), swprintf(), _wtol()。
微软在C运行库(CRT)中加入了对DBCS字符串的支持。对应于strxxx()函数,DBCS使用_mbsxxx()函数。在处理DBCS字符串(如日语,中文,或其它DBCS)时,就要用_mbsxxx()函数。这些函数也能用于处理SBCS字符串(因为DBCS字符串可能就只含有单字节字符)。
现在用一个示例来说明字符串处理函数的不同。如有Unicode字符串L"Bob":
x86 CPU的排列顺序是低位优先(little-endian)的,值0x0042的存储顺序为42 00。这时如用strlen()函数求字符串的长度就发生问题。函数找到第一个字节42,然后是00,意味着字符串结尾,于是返回1。反之,用wcslen()函数求"Bob"的长度更糟糕。wcslen()首先找到0x6F42,然后是0x0062,以后就在内存缓冲内不断地寻找00 00直至发生一般性保护错(GPF)。
strxxx()及其对应的_mbsxxx()究竟是如何运作的?二者之间的不同是非常重要的,直接影响到正确遍历DBCS字符串的方法。下面先介绍字符串遍历,然后再回来讨论strxxx()和 _mbsxxx()。
字符串遍历
我们中的大多数人都是从SBCS成长过来的,都习惯于用指针的 ++ 和 -- 操作符来遍历字符串,有时也使用数组来处理字符串中的字符。这二种方法对于SBCS 和 Unicode 字符串的操作都是正确无误的,因为二者的字符都是等长的,编译器能够的正确返回我们寻求的字符位置。
但对于DBCS字符串就不能这样了。用指针访问DBCS字符串有二个原则,打破这二个原则就会造成错误。
1. 不可使用 ++ 算子,除非每次都检查是否为前导字节。
2. 绝不可使用 -- 算子来向后遍历。
先说明原则2,因为很容易找到一个非人为的示例。假设,有一个配制文件,程序启动时要从安装路径读取该文件,如:C:\Program Files\MyCoolApp\config.bin。文件本身是正常的。
假设用以下代码来配制文件名:
bool GetConfigFileName ( char* pszName, size_t nBuffSize )
{
char szConfigFilename[MAX_PATH];
// 这里从注册表读取文件的安装路径,假设一切正常。
// 如果路径末尾没有反斜线,就加上反斜线。
// 首先,用指针指向结尾零:
char* pLastChar = strchr ( szConfigFilename, '\0' );
// 然后向后退一个字符:
pLastChar--;
if ( *pLastChar != '\\' )
strcat ( szConfigFilename, "\\" );
// 加上文件名:
strcat ( szConfigFilename, "config.bin" );
// 如果字符串长度足够,返回文件名:
if ( strlen ( szConfigFilename ) >= nBuffSize )
return false;
else
{
strcpy ( pszName, szConfigFilename );
return true;
}
}
这段代码的保护性是很强的,但用到DBCS字符串还是会出错。假如文件的安装路径用日语表达:C:\ヨウユソ,该字符串的内存表达为:
这时用上面的GetConfigFileName()函数来检查文件路径末尾是否含有反斜线就会出错,得到错误的文件名。
错在哪里?注意上面的二个十六进制值0x5C(蓝色)。前面的0x5C是字符"\",后面则是字符值83 5C,代表字符"ソ"。可是函数把它误认为反斜线了。
正确的方法是用DBCS函数将指针指向恰当的字符位置,如下所示:
bool FixedGetConfigFileName ( char* pszName, size_t nBuffSize )
{
char szConfigFilename[MAX_PATH];
// 这里从注册表读取文件的安装路径,假设一切正常。
// 如果路径末尾没有反斜线,就加上反斜线。
// 首先,用指针指向结尾零:
char* pLastChar = _mbschr ( szConfigFilename, '\0' );
// 然后向后退一个双字节字符:
pLastChar = CharPrev ( szConfigFilename, pLastChar );
if ( *pLastChar != '\\' )
_mbscat ( szConfigFilename, "\\" );
// 加上文件名:
_mbscat ( szConfigFilename, "config.bin" );
// 如果字符串长度足够,返回文件名:
if ( _mbslen ( szInstallDir ) >= nBuffSize )
return false;
else
{
_mbscpy ( pszName, szConfigFilename );
return true;
}
}
这个改进的函数用CharPrev() API 函数将指针pLastChar向后移动一个字符。如果字符串末尾的字符是双字节字符,就向后移动2个字节。这时返回的结果是正确的,因为不会将字符误判为反斜线。
现在可以想像到第一原则了。例如,要遍历字符串寻找字符":",如果不使用CharNext()函数而使用++算子,当跟随字节值恰好也是":"时就会出错。
与原则2相关的是数组下标的使用:
2a. 绝不可在字符串数组中使用递减下标。
出错原因与原则2相同。例如,设置指针pLastChar为:
char* pLastChar = &szConfigFilename [strlen(szConfigFilename) - 1];
结果与原则2的出错一样。下标减1就是指针向后移动一个字节,不符原则2。
再谈strxxx() 与_mbsxxx()
现在可以清楚为什么要用 _mbsxxx() 函数了。strxxx() 函数不认识DBCS字符而 _mbsxxx()认识。如果调用strrchr("C:\\", '\\')函数可能会出错,但 _mbsrchr()认识双字节字符,所以能返回指向最后出现反斜线字符的指针位置。
最后提一下strxxx() 和 _mbsxxx() 函数族中的字符串长度测量函数,它们都返回字符串的字节数。如果字符串含有3个双字节字符,_mbslen()将返回6。而Unicode的函数返回的是wchar_ts的数量,如wcslen(L"Bob") 返回3
C++字符串完全指南 - Win32字符编码(二)
翻译:连波
15/11/2002
URL: http://www.zdnet.com.cn/developer/tech/story/0,2000081602,39098306,00.htm
Win32 API中的MBCS 和 Unicode
API的二个字符集
也许你没有注意到,Win32的API和消息中的字符串处理函数有二种,一种为MCBS字符串,另一种为Unicode字符串。例如,Win32中没有SetWindowText()这样的接口,而是用SetWindowTextA()和 SetWindowTextW()函数。后缀A (表示ANSI)指明是MBCS函数,后缀W(表示宽字符)指明是Unicode函数。
编写Windows程序时,可以选择用MBCS或Unicode API接口函数。用VC AppWizards向导时,如果不修改预处理器设置,缺省使用的是MBCS函数。但是在API接口中没有SetWindowText()函数,该如何调用呢?实际上,在winuser.h头文件中做了以下定义:
BOOL WINAPI SetWindowTextA ( HWND hWnd, LPCSTR lpString );
BOOL WINAPI SetWindowTextW ( HWND hWnd, LPCWSTR lpString );
#ifdef UNICODE
#define SetWindowText SetWindowTextW
#else
#define SetWindowText SetWindowTextA
#endif
编写MBCS应用时,不必定义UNICODE,预处理为:
#define SetWindowText SetWindowTextA
然后将SetWindowText()处理为真正的API接口函数SetWindowTextA() (如果愿意的话,可以直接调用SetWindowTextA() 或SetWindowTextW()函数,不过很少有此需要)。
如果要将缺省应用接口改为Unicode,就到预处理设置的预处理标记中去掉 _MBCS标记,加入UNICODE 和 _UNICODE (二个标记都要加入,不同的头文件使用不同的标记)。不过,这时要处理普通字符串反而会遇到问题。如有代码:
HWND hwnd = GetSomeWindowHandle();
char szNewText[] = "we love Bob!";
SetWindowText ( hwnd, szNewText );
编译器将"SetWindowText"置换为"SetWindowTextW"后,代码变为:
HWND hwnd = GetSomeWindowHandle();
char szNewText[] = "we love Bob!";
SetWindowTextW ( hwnd, szNewText );
看出问题了吧,这里用一个Unicode字符串处理函数来处理单字节字符串。
第一种解决办法是使用宏定义:
HWND hwnd = GetSomeWindowHandle();
#ifdef UNICODE
wchar_t szNewText[] = L"we love Bob!";
#else
char szNewText[] = "we love Bob!";
#endif
SetWindowText ( hwnd, szNewText );
要对每一个字符串都做这样的宏定义显然是令人头痛的。所以用TCHAR来解决这个问题:
TCHAR的救火角色
TCHAR 是一种字符类型,适用于MBCS 和 Unicode二种编码。程序中也不必到处使用宏定义。
TCHAR的宏定义如下:
#ifdef UNICODE
typedef wchar_t TCHAR;
#else
typedef char TCHAR;
#endif
所以,TCHAR中在MBCS程序中是char类型,在Unicode中是 wchar_t 类型。
对于Unicode字符串,还有个 _T() 宏,用于解决 L 前缀:
#ifdef UNICODE
#define _T(x) L##x
#else
#define _T(x) x
#endif
## 是预处理算子,将二个变量粘贴在一起。不管什么时候都对字符串用 _T 宏处理,这样就可以在Unicode编码中给字符串加上L前缀,如:
TCHAR szNewText[] = _T("we love Bob!");
SetWindowTextA/W 函数族中还有其它隐藏的宏可以用来代替strxxx() 和 _mbsxxx() 字符串函数。例如,可以用 _tcsrchr 宏取代strrchr(),_mbsrchr(),或 wcsrchr()函数。_tcsrchr 根据编码标记为_MBCS 或 UNICODE,将右式函数做相应的扩展处理。宏定义方法类似于SetWindowText。
不止strxxx()函数族中有TCHAR宏定义,其它一些函数中也有。例如,_stprintf (取代sprintf()和swprintf()),和 _tfopen (取代fopen() 和 _wfopen())。MSDN的全部宏定义在"Generic-Text Routine Mappings"栏目下。
String 和 TCHAR 类型定义
Win32 API 文件中列出的函数名都是通用名(如"SetWindowText"),所有的字符串都按照TCHAR类型处理。(只有XP除外,XP只使用Unicode类型)。下面是MSDN给出的常用类型定义:
类型 | MBCS 编码中的意义 | Unicode 编码中的意义 |
|
|
|
| zero-terminated string of char (char | zero-terminated string of char (char |
| constant zero-terminated string of char (constchar | constant zero-terminated string of char (constchar |
| zero-terminated Unicode string ( | zero-terminated Unicode string ( |
| constant zero-terminated Unicode string (const | constant zero-terminated Unicode string (const |
| char |
|
| zero-terminated string of | zero-terminated string of |
| constant zero-terminated string of | constant zero-terminated string of |
何时使用TCHAR 和Unicode
可能会有疑问:“为什么要用Unicode?我一直用的都是普通字符串。”
在三种情况下要用到Unicode:
-
程序只运行于Windows NT。
- 处理的字符串长于MAX_PATH定义的字符数。
- 程序用于Windows XP中的新接口,那里没有A/W版本之分。
大部分Unicode API不可用于Windows 9x。所以如果程序要在Windows 9x上运行的话,要强制使用MBCS API (微软推出一个可运行于Windows 9x的新库,叫做Microsoft Layer for Unicode。但我没有试用过,无法说明它的好坏)。相反,NT内部全部使用Unicode编码,使用Unicode API可以加速程序运行。每当将字符串处理为MBCS API时,操作系统都会将字符串转换为Unicode并调用相应的Unicode API 函数。对于返回的字符串,操作系统要做同样的转换。尽管这些转换经过了高度优化,模块尽可能地压缩到最小,但毕竟会影响到程序的运行速度。
NT允许使用超长文件名(长于MAX_PATH 定义的260),但只限于Unicode API使用。Unicode API的另外一个优点是程序能够自动处理输入的文字语言。用户可以混合输入英文,中文和日文作为文件名。不必使用其它代码来处理,都按照Unicode编码方式处理。
最后,作为Windows 9x的结局,微软似乎抛弃了MBCS API。例如,SetWindowTheme() 接口函数的二个参数只支持Unicode编码。使用Unicode编码省却了MBCS与Unicode之间的转换过程。
如果程序中还没有使用到Unicode编码,要坚持使用TCHAR和相应的宏。这样不但可以长期保持程序中DBCS编码的安全性,也利于将来扩展使用到Unicode编码。那时只要改变预处理中的设置即可!
C++字符串完全指南(2) - 各种字符串类(一)
翻译:连波
19/11/2002
URL: http://www.zdnet.com.cn/developer/tech/story/0,2000081602,39098621,00.htm
前言
C语言的字符串容易出错,难以管理,并且往往是黑客到处寻找的目标。于是,出现了许多字符串包装类。可惜,人们并不很清楚什么情况下该用哪个类,也不清楚如何将C语言字符串转换到包装类。
本文涉及到Win32 API,MFC,STL,WTL和Visual C++运行库中使用到的所有的字符串类型。说明各个类的用法,如何构造对象,如何进行类转换等等。Nish为本文提供了Visual C++ 7的managed string 类的用法。
阅读本文之前,应完全理解本指南第一部分中阐述的字符类型和编码。
字符串类的首要原则:
不要随便使用类型强制转换,除非转换的类型是明确由文档规定的。
之所以撰写字符串指南这二篇文章,是因为常有人问到如何将X类型的字符串转换到Z类型。提问者使用了强制类型转换(cast),但不知道为什么不能转换成功。各种各样的字符串类型,特别是BSTR,在任何场合都不是三言二语可以讲清的。因此,我以为这些提问者是想让强制类型转换来处理一切。
除非明确规定了转换算子,不要将任何其它类型数据强制转换为string。一个字符串不能用强制类型转换到string类。例如:
void SomeFunc ( LPCWSTR widestr );
main()
{
SomeFunc ( (LPCWSTR) "C:\\foo.txt" ); // 错!
}
这段代码100%错误。它可以通过编译,因为类型强制转换超越了编译器的类型检验。但是,能够通过编译,并不证明代码是正确的。
下面,我将指出什么时候用类型强制转换是合理的。
C语言字符串与类型定义
如指南的第一部分所述,Windows API定义了TCHAR术语。它可用于MBCS或Unicode编码字符,取决于预处理设置为_MBCS 或 _UNICODE标记。关于TCHAR的详细说明请阅指南的第一部分。为便于叙述,下面给出字符类型定义:
Type | Meaning |
| Unicode character ( |
| MBCS or Unicode character, depending on preprocessor settings |
| string of char (char |
| constant string of char (constchar |
| string of |
| constant string of |
| string of |
| constant string of |
另外还有一个字符类型OLECHAR。这是一种对象链接与嵌入的数据类型(比如嵌入Word文档)。这个类型通常定义为wchar_t。如果将预处理设置定义为OLE2ANSI,OLECHAR将被定义为char类型。现在已经不再定义OLE2ANSI(它只在MFC 3以前版本中使用),所以我将OLECHAR作为Unicode字符处理。
下面是与OLECHAR相关的类型定义:
Type | Meaning |
| Unicode character ( |
| string of |
| constant string of |
还有以下二个宏让相同的代码能够适用于MBCS和Unicode编码:
Type | Meaning |
| Prepends |
| Prepends |
宏_T有几种形式,功能都相同。如: -- TEXT, _TEXT, __TEXT, 和 __T这四种宏的功能相同。
COM中的字符串 - BSTR 与 VARIANT
许多COM接口使用BSTR声明字符串。BSTR有一些缺陷,所以我在这里让它独立成章。
BSTR是Pascal类型字符串(字符串长度值显式地与数据存放在一起)和C类型字符串(字符串长度必须通过寻找到结尾零字符来计算)的混合型字符串。BSTR属于Unicode字符串,字符串中预置了字符串长度值,并且用一个零字符来结尾。下面是一个"Bob"的BSTR字符串:
注意,字符串长度值是一个DWORD类型值,给出字符串的字节长度,但不包括结尾零。在上例,"Bob"含有3个Unicode字符(不计结尾零),6个字节长。因为明确给出了字符串长度,所以当BSTR数据在不同的处理器和计算机之间传送时,COM库能够知道应该传送的数据量。
附带说一下,BSTR可以包含任何数据块,不单是字符。它甚至可以包容内嵌零字符数据。这些不在本文讨论范围。
C++中的BSTR变量其实就是指向字符串首字符的指针。BSTR是这样定义的:
typedef OLECHAR* BSTR;
这个定义很糟糕,因为事实上BSTR与Unicode字符串不一样。有了这个类型定义,就越过了类型检查,可以混合使用LPOLESTR和BSTR。向一个需要LPCOLESTR (或 LPCWSTR)类型数据的函数传递BSTR数据是安全的,反之则不然。所以要清楚了解函数所需的字符串类型,并向函数传递正确类型的字符串。
要知道为什么向一个需要BSTR类型数据的函数传递LPCWSTR类型数据是不安全的,就别忘了BSTR必须在字符串开头的四个字节保留字符串长度值。但LPCWSTR字符串中没有这个值。当其它的处理过程(如Word)要寻找BSTR的长度值时就会找到一堆垃圾或堆栈中的其它数据或其它随机数据。这就导致方法失效,当长度值太大时将导致崩溃。
许多应用接口都使用BSTR,但都用到二个最重要的函数来构造和析构BSTR。就是SysAllocString()和SysFreeString()函数。SysAllocString()将Unicode字符串拷贝到BSTR,SysFreeString()释放BSTR。示例如下:
BSTR bstr = NULL;
bstr = SysAllocString ( L"Hi Bob!" );
if ( NULL == bstr )
// 内存溢出
// 这里使用bstr
SysFreeString ( bstr );
当然,各种BSTR包装类都会小心地管理内存。
自动接口中的另一个数据类型是VARIANT。它用于在无类型语言,诸如JScript,VBScript,以及Visual Basic,之间传递数据。VARIANT可以包容许多不用类型的数据,如long和IDispatch*。如果VARIANT包含一个字符串,这个字符串是BSTR类型。在下文的VARIANT包装类中我还会谈及更多的VARIANT。
C++字符串完全指南(2) - 各种字符串类- CRT类
翻译:连波
20/11/2002
URL: http://www.zdnet.com.cn/developer/tech/story/0,2000081602,39098682,00.htm
_bstr_t
字符串包装类
我已经说明了字符串的各种类型,现在讨论包装类。对于每个包装类,我都会说明它的对象构造过程和如何转换成C类型字符串指针。应用接口的调用,或构造另一个不同类型的字符串类,大多都要用到C类型指针。本文不涉及类的其它操作,如排序和比较等。
再强调一下,在完全了解转换结果之前不要随意使用强制类型转换。
CRT类
_bstr_t
_bstr_t 是BSTR的完全包装类。实际上,它隐含了BSTR。它提供多种构造函数,能够处理隐含的C类型字符串。但它本身却不提供BSTR的处理机制,所以不能作为COM方法的输出参数[out]。如果要用到BSTR* 类型数据,用ATL的CComBSTR类更为方便。
_bstr_t 数据可以传递给需要BSTR数据的函数,但必须满足以下三个条件:
首先,_bstr_t 具有能够转换为wchar_t*类型数据的函数。
其次,根据BSTR定义,使得wchar_t* 和BSTR对于编译器来说是相同的。
第三,_bstr_t内部保留的指向内存数据块的指针 wchar_t* 要遵循BSTR格式。
满足这些条件,即使没有相应的BSTR转换文档,_bstr_t 也能正常工作。示例如下:
// 构造 _bstr_t bs1 = "char string"; // 从LPCSTR构造 _bstr_t bs2 = L"wide char string"; // 从LPCWSTR构造 _bstr_t bs3 = bs1; // 拷贝另一个 _bstr_t _variant_t v = "Bob"; _bstr_t bs4 = v; // 从一个含有字符串的 _variant_t 构造 // 数据萃取 LPCSTR psz1 = bs1; // 自动转换到MBCS字符串 LPCSTR psz2 = (LPCSTR) bs1; // cast OK, 同上 LPCWSTR pwsz1 = bs1; // 返回内部的Unicode字符串 LPCWSTR pwsz2 = (LPCWSTR) bs1; // cast OK, 同上 BSTR bstr = bs1.copy(); // 拷贝bs1, 返回BSTR // ... SysFreeString ( bstr );
注意,_bstr_t 也可以转换为char* 和 wchar_t*。这是个设计问题。虽然char* 和 wchar_t*不是常量指针,但不能用于修改字符串,因为可能会打破内部BSTR结构。
_variant_t
_variant_t
_variant_t 是VARIANT的完全包装类。它提供多种构造函数和数据转换函数。本文仅讨论与字符串有关的操作。
// 构造 _variant_t v1 = "char string"; // 从LPCSTR 构造 _variant_t v2 = L"wide char string"; // 从LPCWSTR 构造 _bstr_t bs1 = "Bob"; _variant_t v3 = bs1; // 拷贝一个 _bstr_t 对象 // 数据萃取 _bstr_t bs2 = v1; // 从VARIANT中提取BSTR _bstr_t bs3 = (_bstr_t) v1; // cast OK, 同上
注意,_variant_t 方法在转换失败时会抛出异常,所以要准备用catch 捕捉_com_error异常。
另外要注意 _variant_t 不能直接转换成MBCS字符串。要建立一个过渡的_bstr_t 变量,用其它提供转换Unicode到MBCS的类函数,或ATL转换宏来转换。
与_bstr_t 不同,_variant_t 数据可以作为参数直接传送给COM方法。_variant_t 继承了VARIANT类型,所以在需要使用VARIANT的地方使用_variant_t 是C++语言规则允许的。
C++字符串完全指南(2) - STL和ATL类
翻译:连波
21/11/2002
URL: http://www.zdnet.com.cn/developer/tech/story/0,2000081602,39098845,00.htm
STL类
STL类
STL只有一个字符串类,即basic_string。basic_string管理一个零结尾的字符数组。字符类型由模板参数决定。通常,basic_string被处理为不透明对象。可以获得一个只读指针来访问缓冲区,但写操作都是由basic_string的成员函数进行的。
basic_string预定义了二个特例:string,含有char类型字符;which,含有wchar_t类型字符。没有内建的TCHAR特例,可用下面的代码实现:
// 特例化 typedef basic_string tstring; // TCHAR字符串 // 构造 string str = "char string"; // 从LPCSTR构造 wstring wstr = L"wide char string"; // 从LPCWSTR构造 tstring tstr = _T("TCHAR string"); // 从LPCTSTR构造 // 数据萃取 LPCSTR psz = str.c_str(); // 指向str缓冲区的只读指针 LPCWSTR pwsz = wstr.c_str(); // 指向wstr缓冲区的只读指针 LPCTSTR ptsz = tstr.c_str(); // 指向tstr缓冲区的只读指针
与_bstr_t 不同,basic_string不能在字符集之间进行转换。但是如果一个构造函数接受相应的字符类型,可以将由c_str()返回的指针传递给这个构造函数。例如:
// 从basic_string构造_bstr_t _bstr_t bs1 = str.c_str(); // 从LPCSTR构造 _bstr_t _bstr_t bs2 = wstr.c_str(); // 从LPCWSTR构造 _bstr_t
ATL类
CComBSTR
CComBSTR 是ATL的BSTR包装类。某些情况下比_bstr_t 更有用。最主要的是,CComBSTR允许操作隐含BSTR。就是说,传递一个CComBSTR对象给COM方法时,CComBSTR对象会自动管理BSTR内存。例如,要调用下面的接口函数:
// 简单接口 struct IStuff : public IUnknown { // 略去COM程序... STDMETHOD(SetText)(BSTR bsText); STDMETHOD(GetText)(BSTR* pbsText); };
CComBSTR 有一个BSTR操作方法,能将BSTR直接传递给SetText()。还有一个引用操作(operator &)方法,返回BSTR*,将BSTR*传递给需要它的有关函数。
CComBSTR bs1; CComBSTR bs2 = "new text"; pStuff->GetText ( &bs1 ); // ok, 取得内部BSTR地址 pStuff->SetText ( bs2 ); // ok, 调用BSTR转换 pStuff->SetText ( (BSTR) bs2 ); // cast ok, 同上
CComVariant
CComBSTR有类似于 _bstr_t 的构造函数。但没有内建MBCS字符串的转换函数。可以调用ATL宏进行转换。
// 构造 CComBSTR bs1 = "char string"; // 从LPCSTR构造 CComBSTR bs2 = L"wide char string"; // 从LPCWSTR构造 CComBSTR bs3 = bs1; // 拷贝CComBSTR CComBSTR bs4; bs4.LoadString ( IDS_SOME_STR ); // 从字符串表加载 // 数据萃取 BSTR bstr1 = bs1; // 返回内部BSTR,但不可修改! BSTR bstr2 = (BSTR) bs1; // cast ok, 同上 BSTR bstr3 = bs1.Copy(); // 拷贝bs1, 返回BSTR BSTR bstr4; bstr4 = bs1.Detach(); // bs1不再管理它的BSTR // ... SysFreeString ( bstr3 ); SysFreeString ( bstr4 );
上面的最后一个示例用到了Detach()方法。该方法调用后,CComBSTR对象就不再管理它的BSTR或其相应内存。所以bstr4就必须调用SysFreeString()。
最后讨论一下引用操作符(operator &)。它的超越使得有些STL集合(如list)不能直接使用CComBSTR。在集合上使用引用操作返回指向包容类的指针。但是在CComBSTR上使用引用操作,返回的是BSTR*,不是CComBSTR*。不过可以用ATL的CAdapt类来解决这个问题。例如,要建立一个CComBSTR的队列,可以声明为:
std::list< CAdapt> bstr_list;
CAdapt 提供集合所需的操作,是隐含于代码的。这时使用bstr_list 就象在操作一个CComBSTR队列。
CComVariant
CComVariant 是VARIANT的包装类。但与 _variant_t 不同,它的VARIANT不是隐含的,可以直接操作类里的VARIANT成员。CComVariant 提供多种构造函数和多类型操作。这里只介绍与字符串有关的操作。
// 构造 CComVariant v1 = "char string"; // 从LPCSTR构造 CComVariant v2 = L"wide char string"; // 从LPCWSTR构造 CComBSTR bs1 = "BSTR bob"; CComVariant v3 = (BSTR) bs1; // 从BSTR拷贝 // 数据萃取 CComBSTR bs2 = v1.bstrVal; // 从VARIANT提取BSTR
跟_variant_t 不同,CComVariant没有不同VARIANT类型之间的转换操作。必须直接操作VARIANT成员,并确定该VARIANT的类型无误。调用ChangeType()方法可将CComVariant数据转换为BSTR。
CComVariant v4 = ... // 从某种类型初始化 v4
CComBSTR bs3;
if ( SUCCEEDED( v4.ChangeType ( VT_BSTR ) ))
bs3 = v4.bstrVal;
跟 _variant_t 一样,CComVariant不能直接转换为MBCS字符串。要建立一个过渡的_bstr_t 变量,用其它提供转换Unicode到MBCS的类函数,或ATL转换宏来转换。
ATL转换宏
ATL转换宏
ATL的字符串转换宏可以方便地转换不同编码的字符,用在函数中很有效。宏按照[source type]2[new type] 或 [source type]2C[new type]格式命名。后者转换为一个常量指针 (名字内含"C")。类型缩写如下:
A:MBCS字符串,char* (A for ANSI)
W:Unicode字符串,wchar_t* (W for wide)
T:TCHAR字符串,TCHAR*
OLE:OLECHAR字符串,OLECHAR* (实际等于W)
BSTR:BSTR (只用于目的类型)
例如,W2A() 将Unicode字符串转换为MBCS字符串,T2CW()将TCHAR字符串转换为Unicode字符串常量。
要使用宏转换,程序中要包含atlconv.h头文件。可以在非ATL程序中使用宏转换,因为头文件不依赖其它的ATL,也不需要 _Module全局变量。如在函数中使用转换宏,在函数起始处先写上USES_CONVERSION宏。它表明某些局部变量由宏控制使用。
转换得到的结果字符串,只要不是BSTR,都存储在堆栈中。如果要在函数外使用这些字符串,就要将这些字符串拷贝到其它的字符串类。如果结果是BSTR,内存不会自动释放,因此必须将返回值分配给一个BSTR变量或BSTR的包装类,以避免内存泄露。
下面是若干宏转换示例:
// 带有字符串的函数: void Foo ( LPCWSTR wstr ); void Bar ( BSTR bstr ); // 返回字符串的函数: void Baz ( BSTR* pbstr ); #include main() { using std::string; USES_CONVERSION; // 声明局部变量由宏控制使用 // 示例1:送一个MBCS字符串到Foo() LPCSTR psz1 = "Bob"; string str1 = "Bob"; Foo ( A2CW(psz1) ); Foo ( A2CW(str1.c_str()) ); // 示例2:将MBCS字符串和Unicode字符串送到Bar() LPCSTR psz2 = "Bob"; LPCWSTR wsz = L"Bob"; BSTR bs1; CComBSTR bs2; bs1 = A2BSTR(psz2); // 创建 BSTR bs2.Attach ( W2BSTR(wsz) ); // 同上,分配到CComBSTR Bar ( bs1 ); Bar ( bs2 ); SysFreeString ( bs1 ); // 释放bs1 // 不必释放bs2,由CComBSTR释放。 // 示例3:转换由Baz()返回的BSTR BSTR bs3 = NULL; string str2; Baz ( &bs3 ); // Baz() 填充bs3内容 str2 = W2CA(bs3); // 转换为MBCS字符串 SysFreeString ( bs3 ); // 释放bs3 }
可以看到,向一个需要某种类型参数的函数传递另一种类型的参数,用宏转换是非常方便的。
C++字符串完全指南(2) - MFC类
翻译:连波
22/11/2002
URL: http://www.zdnet.com.cn/developer/tech/story/0,2000081602,39098983,00.htm
MFC类
MFC类
CString
MFC的CString含有TCHAR,它的实际字符类型取决于预处理标记的设置。通常,CString象STL字符串一样是不透明对象,只能用CString的方法来修改。CString比STL字符串更优越的是它的构造函数接受MBCS和Unicode字符串。并且可以转换为LPCTSTR,因此可以向接受LPCTSTR的函数直接传递CString对象,不必调用c_str()方法。
// 构造 CString s1 = "char string"; // 从LPCSTR构造 CString s2 = L"wide char string"; // 从LPCWSTR构造 CString s3 ( ' ', 100 ); // 预分配100字节,填充空格 CString s4 = "New window text"; // 可以在LPCTSTR处使用CString: SetWindowText ( hwndSomeWindow, s4 ); // 或者,显式地做强制类型转换: SetWindowText ( hwndSomeWindow, (LPCTSTR) s4 );
也可以从字符串表加载字符串。CString通过LoadString()来构造对象。用Format()方法可有选择地从字符串表读取一定格式的字符串。
// 从字符串表构造/加载 CString s5 ( (LPCTSTR) IDS_SOME_STR ); // 从字符串表加载 CString s6, s7; // 从字符串表加载 s6.LoadString ( IDS_SOME_STR ); // 从字符串表加载打印格式的字符串 s7.Format ( IDS_SOME_FORMAT, "bob", nSomeStuff, ... );
第一个构造函数看上去有点怪,但它的确是文档标定的字符串加载方式。
注意,CString只允许一种强制类型转换,即强制转换为LPCTSTR。强制转换为LPTSTR (非常量指针)是错误的。按照老习惯,将CString强制转换为LPTSTR只能伤害自己。有时在程序中没有发现出错,那只是碰巧。转换到非常量指针的正确方法是调用GetBuffer()方法。
下面以往队列加入元素为例说明如何正确地使用CString:
CString str = _T("new text"); LVITEM item = {0}; item.mask = LVIF_TEXT; item.iItem = 1; item.pszText = (LPTSTR)(LPCTSTR) str; // 错! item.pszText = str.GetBuffer(0); // 正确 ListView_SetItem ( &item ); str.ReleaseBuffer(); // 将队列返回给str
pszText成员是LPTSTR,一个非常量指针,因此要用str的GetBuffer()。GetBuffer()的参数是CString分配的最小缓冲区。如果要分配一个1K的TCHAR,调用GetBuffer(1024)。参数为0,只返回指向字符串的指针。
上面示例的出错语句可以通过编译,甚至可以正常工作,如果恰好就是这个类型。但这不证明语法正确。进行非常量的强制类型转换,打破了面向对象的封装原则,并逾越了CString的内部操作。如果你习惯进行这样的强制类型转换,终会遇到出错,可你未必知道错在何处,因为你到处都在做这样的转换,而代码也都能运行。
知道为什么人们总在抱怨有缺陷的软件吗?不正确的代码就臭虫的滋生地。然道你愿意编写明知有错的代码让臭虫有机可乘?还是花些时间学习CString的正确用法让你的代码能够100%的正确吧。
CString还有二个函数能够从CString中得到BSTR,并在必要时转换成Unicode。那就是AllocSysString()和SetSysString()。除了SetSysString()使用BSTR*参数外,二者一样。
// 转换成BSTR CString s5 = "Bob!"; BSTR bs1 = NULL, bs2 = NULL; bs1 = s5.AllocSysString(); s5.SetSysString ( &bs2 ); // ... SysFreeString ( bs1 ); SysFreeString ( bs2 );
COleVariant 与CComVariant 非常相似。COleVariant 继承于VARIANT,可以传递给需要VARIANT的函数。但又与CComVariant 不同,COleVariant 只有一个LPCTSTR的构造函数,不提供单独的LPCSTR和LPCWSTR的构造函数。在大多情况下,没有问题,因为总是愿意把字符串处理为LPCTSTR。但你必须知道这点。COleVariant 也有接受CString的构造函数。
// 构造 CString s1 = _T("tchar string"); COleVariant v1 = _T("Bob"); // 从LPCTSTR构造 COleVariant v2 = s1; // 从CString拷贝
对于CComVariant,必须直接处理VARIANT成员,用ChangeType()方法在必要时将其转换为字符串。但是,COleVariant::ChangeType() 在转换失败时会抛出异常,而不是返回HRESULT的出错码。
// 数据萃取 COleVariant v3 = ...; // 从某种类型构造v3 BSTR bs = NULL; try { v3.ChangeType ( VT_BSTR ); bs = v3.bstrVal; } catch ( COleException* e ) { // 出错,无法转换 } SysFreeString ( bs );
WTL类
WTL类
CString
WTL的CString与MFC的CString的行为完全相同,参阅上面关于MFC CString的说明即可。
CLR 及 VC 7 类
System::String 是.NET的字符串类。在其内部,String对象是一个不变的字符序列。任何操作String对象的String方法都返回一个新的String对象,因为原有的String对象要保持不变。String类有一个特性,当多个String都指向同一组字符集时,它们其实是指向同一个对象。Managed Extensions C++ 的字符串有一个新的前缀S,用来表明是一个managed string字符串。
// 构造 String* ms = S"This is a nice managed string";
可以用unmanaged string字符串来构造String对象,但不如用managed string构造String对象有效。原因是所有相同的具有S前缀的字符串都指向同一个对象,而unmanaged string没有这个特点。下面的例子可以说明得更清楚些:
String* ms1 = S"this is nice"; String* ms2 = S"this is nice"; String* ms3 = L"this is nice"; Console::WriteLine ( ms1 == ms2 ); // 输出true Console::WriteLine ( ms1 == ms3); // 输出false
要与没有S前缀的字符串做比较,用String::CompareTo()方法来实现,如:
Console::WriteLine ( ms1->CompareTo(ms2) ); Console::WriteLine ( ms1->CompareTo(ms3) );
二者都输出0,说明字符串相等。
在String和MFC 7的CString之间转换很容易。CString可以转换为LPCTSTR,String有接受char* 和 wchar_t* 的二种构造函数。因此可以直接把CString传递给String的构造函数:
CString s1 ( "hello world" ); String* s2 ( s1 ); // 从CString拷贝
反向转换的方法也类似:
String* s1 = S"Three cats"; CString s2 ( s1 );
可能有点迷惑。从VS.NET开始,CString有一个接受String对象的构造函数,所以是正确的。
CStringT ( System::String* pString );
为了加速操作,有时可以用基础字符串(underlying string):
String* s1 = S"Three cats"; Console::WriteLine ( s1 ); const __wchar_t __pin* pstr = PtrToStringChars(s1); for ( int i = 0; i < wcslen(pstr); i++ ) (*const_cast<__wchar_t*>(pstr+i))++; Console::WriteLine ( s1 );
PtrToStringChars() 返回指向基础字符串的 const __wchar_t* 指针,可以防止在操作字符串时,垃圾收集器去除该字符串。
C++字符串完全指南(2) - 总结
翻译:连波
23/11/2002
URL: http://www.zdnet.com.cn/developer/tech/story/0,2000081602,39099061,00.htm
字符串类的打印格式函数
对字符串包装类使用printf()或其它类似功能的函数时要特别小心。包括sprintf()函数及其变种,以及TRACE 和ATLTRACE 宏。它们的参数都不做类型检验,一定要给它们传递C语言字符串,而不是整个string对象。
例如,要向ATLTRACE()传递一个_bstr_t 里的字符串,必须显式用(LPCSTR)或 (LPCWSTR)进行强制类型转换:
_bstr_t bs = L"Bob!"; ATLTRACE("The string is: %s in line %d\n", (LPCSTR) bs, nLine);
如果忘了用强制类型转换,直接把整个 _bstr_t 对象传递给ATLTRACE,跟踪消息将输出无意义的东西,因为_bstr_t 变量内的所有数据都进栈了。
所有类的总结
常用的字符串类之间的转换方法是:将源字符串转换为C类型字符串指针,然后将该指针传递给目标类的构造函数。下面列出将字符串转换为C类型指针的方法,以及哪些类的构造函数接受C类型指针。
Class | string | convert to char | convert to constchar | convert to | convert to const | convert to | construct from char | construct from |
|
| yes, cast1 | yes, cast | yes, cast1 | yes, cast | yes2 | yes | yes |
|
| no | no | no | cast to | cast to | yes | yes |
| MBCS | no | yes, | no | no | no | yes | no |
| Unicode | no | no | no | yes, | no | no | yes |
|
| no | no | no | yes, cast | yes, cast | yes | yes |
|
| no | no | no | yes4 | yes4 | yes | yes |
|
| no6 | in MBCS | no6 | in Unicode | no5 | yes | yes |
|
| no | no | no | yes4 | yes4 | in MBCS builds | in Unicode builds |
附注:
-
虽然 _bstr_t 可以转换为非常量指针,但对内部缓冲区的修改可能导致内存溢出,或在释放BSTR时导致内存泄露。
- bstr_t 的BSTR内含 wchar_t* 变量,所以可将const wchar_t* 转换到BSTR。但这个用法将来可能会改变,使用时要小心。
- 如果转换到BSTR失败,将抛出异常。
- 用ChangeType()处理VARIANT的bstrVal。在MFC,转换失败将抛出异常。
- 虽然没有BSTR的转换函数,但AllocSysString()可返回一个新的BSTR。
- 用GetBuffer()方法可临时得到一个非常量TCHAR指针。
本文引用通告地址: http://blog.csdn.net/venuszhou/services/trackbacks/299754.aspx
- 作者: largedong 2005年10月19日, 星期三 14:35 回复(0) | 引用(0) 加入博采
状态栏弹出窗口
OnCreate中添加
ShowWindow( SW_HIDE );
ModifyStyleEx(WS_EX_APPWINDOW,WS_EX_TOOLWINDOW);
UpdateWindow ();
//以上代码 为了不在任务栏显示
AnimateWindow( 1000, AW_VER_NEGATIVE );//滑动
OnClose中添加
AnimateWindow( 1000, AW_HIDE|AW_VER_NEGATIVE );
起一个定时器 让窗口自杀
找出托盘区的坐标
BOOL CALLBACK FindTrayWnd(HWND hwnd, LPARAM lParam)
{
TCHAR szClassName[256];
GetClassName(hwnd, szClassName, 255);
// 比较窗口类名
if (_tcscmp(szClassName, _T("TrayNotifyWnd")) == 0)
{
CRect *pRect = (CRect*) lParam;
::GetWindowRect(hwnd, pRect);
return TRUE;
}
// 当找到时钟窗口时表示可以结束了
if (_tcscmp(szClassName, _T("TrayClockWClass")) == 0)
{
CRect *pRect = (CRect*) lParam;
CRect rectClock;
::GetWindowRect(hwnd, rectClock);
pRect->right = rectClock.left;
return FALSE;
}
return TRUE;
}
CRect GetTrayWndRect()
{
CRect rect(0,0,0,0);
// 查找托盘窗口
CWnd* pWnd = FindWindow(_T("Shell_TrayWnd"), NULL);
if (pWnd)
{
//通过列举子窗口来进行查找
EnumChildWindows(pWnd->m_hWnd, FindTrayWnd, (LPARAM)&rect);
pWnd->GetWindowRect(rect);
//rect 为托盘区矩形
}
return rect;
动画窗口的实现(AnimateWindow函数)-
要实现这种动画窗口的编程效果,主要用到Windows API中的AnimateWindow函数,通过在窗口的创建或消毁过程中运用该函数,来实现开启和关闭程序时达到所希望的动画窗口效果。AnimateWindow函数所提供的动画效果十分丰富,我们可以在自己的程序中选择各种不同的动画效果,增强程序的趣味性。为使读者对AnimateWindow函数有一个基本了解,我们先对该函数做一个简单介绍:
函数原型:BOOL AnimateWindow(HWND hWnd,DWORD dwTime,DWORD dwFlags)。
函数功能:该函数能在显示与隐藏窗口时产生两种特殊类型的动画效果:滚动动画和滑动动画。
参数含义:
hWnd:指定产生动画的窗口的句柄。
dwTime:指明动画持续的时间(以微秒计),完成一个动画的标准时间为200微秒。
dwFags:指定动画类型。这个参数可以是一个或多个下列标志的组合。标志描述:
AW_SLIDE:使用滑动类型。缺省则为滚动动画类型。当使用AW_CENTER标志时,这个标志就被忽略。
AW_ACTIVATE:激活窗口。在使用了AW_HIDE标志后不能使用这个标志。
AW_BLEND:实现淡出效果。只有当hWnd为顶层窗口的时候才可以使用此标志。
AW_HIDE:隐藏窗口,缺省则显示窗口。
AW_CENTER:若使用了AW_HIDE标志,则使窗口向内重叠,即收缩窗口;若未使用AW_HIDE标志,则使窗口向外扩展,即展开窗口。
AW_HOR_POSITIVE:自左向右显示窗口。该标志可以在滚动动画和滑动动画中使用。当使用AW_CENTER标志时,该标志将被忽略。
AW_VER_POSITIVE:自顶向下显示窗口。该标志可以在滚动动画和滑动动画中使用。当使用AW_CENTER标志时,该标志将被忽略。
AW_VER_NEGATIVE:自下向上显示窗口。该标志可以在滚动动画和滑动动画中使用。当使用AW_CENTER标志时,该标志将被忽略。
返回值:如果函数成功,返回值为非零;如果函数失败,返回值为零。在下列情况下函数将失败:
窗口使用了窗口边界;窗口已经可见仍要显示窗口;窗口已经隐藏仍要隐藏窗口。
三、动画窗口的实现
下面就以一个简单的单文档程序为例,说明如何在VC中使用AnimateWindow函数来实现打开和关闭程序时的动画效果。基于多文档与对话框的程序所用方法类似,本文就不一一介绍。笔者所使用的开发环境为:
1.WindowsXP,Visual C++6。
1、建立一个MFC AppWizard(exe)应用工程Animate。
在MFC AppWizard向导的第一步中选择Single document,再点击按键Finish->OK完成工程建立。
2、在CMainFrame::OnCreate函数中增加黑体加粗部分语句。
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{ ……
m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
EnableDocking(CBRS_ALIGN_ANY);
DockControlBar(&m_wndToolBar);
AnimateWindow(GetSafeHwnd(),1000,AW_CENTER);
return 0;
}
3、使用MFC ClassWizard增加消息处理函数
使用ClassWizard在CMainFrame类中增加WM_CLOSE消息处理函数,并增加以下语句。
void CMainFrame::OnClose()
{// TODO: Add your message handler code here and/or call default
AnimateWindow(GetSafeHwnd(),1000,AW_HIDE|AW_CENTER);
CFrameWnd::OnClose();
}
2.WindowsXP,vc.net2003
创建步骤相同
四、编译时出现的问题
在实现动画窗口的程序时,笔者发现如果直接在VC中使用AnimateWindow函数,在编译时会报告出错。以上述程序为例,在编译时系统会报告:
在环境1,2下都会出现相同的问题
通过错误提示可以看出是编译系统认为AnimateWindow函数和AW_HIDE、AW_CENTER两个参数没有定义。因该函数是定义在Winuser.h头文件中的,于时,笔者显示地在程序中定义了对该头文件的包含,编译时却仍然出现相同的错误。为什么在VC中编译不能通过呢?通过查阅MSDN笔者发现在MSDN中明确提到WindowsNT5.0和Windows98以上版本均支持该函数。通过笔者的研究发现,问题出在定义AnimateWindow函数的头文件Winuser.h中,在VC安装目录下进入include子目录,用EDIT打开Winuser.h文件,按F3键查找AnimateWindow,可以发现有两处定义,一处是定义该函数中使用到的参数;另一处是该函数原型的定义。在这两处定义中均出现了对Windows版本的条件判断,#if (WINVER >= 0X500)……,原来问题出在这里,我们目前所使用的各种Windows主版本号均为5点零以下,所以在VC中编译上述程序时,编译系统自然将AnimateWindow函数排除在外。因此为了在我们的程序中使用该函数,就得对其头文件进行一些小小的修改,即将#if (WINVER >= 0X500)改为#if (WINVER >= 0X400),请注意两处出现该函数定义的部分都要进行修改。
修改好头文件后,编译即可通过。
在环境1下,(1)添加头文件#include"winuser.h"
(2)如上所说的改变版本.
问题就可以得到解决
在环境2下,我同样添加了头文件,并且同样修改了版本,可是问题并没有得到解决,还会同样的错,仍报错
'AnimateWindow' : undeclared identifier //错误编号C3861
'AW_HIDE' : undeclared identifier //错误编号C2065
'AW_CENTER' : undeclared identifier
真服了,不知道怎么样解决了
五、小结
实际上在AnimateWindow函数中包含了多种动画效果,我们只须在程序中增加几条语句,就能使得程序的开启和关闭就得生动有趣。
另外,通过本例我们可以发现,尽管在MSDN中明确指出在Windows98中支持AnimateWindow函数,但是在实际编程时却发现该函数在头文件中的定义却将版本控制在了5.0以上,笔者虽然不知道原因何在,但本文对此问题的解决无疑是一种比较新颖的作法,希望能够对广大读者的工作起到抛砖引玉的效果。
另:2005.9.3在我看到PRM的评论后,重新做了一遍实验,却发现在环境一和环境二下都没有碰到任何问题。也邪门,想当初调试这个函数是屡屡受挫。确实在vc++6中windows.h的定义版本为(#define WINVER 0x0400),而在vc.net2003中(#define WINVER 0x0501)。所以两个编译环境默认版本为不一样的。当然可以采用PRM的方法,可是并不是每一个人都会碰到这样的错误,譬如我刚才重做了一遍,什么错误都没有。万一哪天又用到这个函数,碰到这个问题可以到网上多搜几种方法,看看别人又什么高招!
了解CObject 和 CRuntimeClass
转载请保留作者署名
CObject和CRuntimeClass是MFC中两个非常重要的类/结构,绝大部分MFC类都是以CObject做为基类, CRuntimeClass结构同CObject密不可分,了解它们对于深入理解MFC具有重要意义。
一、CRuntimeClass结构
要理解CObject,我们先来看一下CRuntimeClass这个在MFC中至关重要的一个结构。
每个从CObject中派生的类都有有一个CRuntimeClass对象同它关联以完成在运行时得到类实例的信息或者是它的基类。 在afx.h中它的定义如下:
struct CRuntimeClass
{
// Attributes
LPCSTR m_lpszClassName; //类名,一般是指包含CRuntimeClass对象的类的名称
int m_nObjectSize; //包含CRuntimeClass对象的类sizeof的大小,不包括它分配的内存
UINT m_wSchema; // schema number of the loaded class
CObject* (PASCAL* m_pfnCreateObject)(); // NULL => abstract class 指向一个建立实例的构造函数
#ifdef _AFXDLL// Call this when using MFC in a shared DLL
CRuntimeClass* (PASCAL* m_pfnGetBaseClass)();
#else
CRuntimeClass* m_pBaseClass;
#endif
//以上m_pBaseClass的指针(函数)是MFC运行时确定类层次的关键,它一个简单的单向链表
// Operations
CObject* CreateObject(); //这个函数给予CObject 派生类运行时动态建立的能力
BOOL IsDerivedFrom(const CRuntimeClass* pBaseClass) const;
//这个函数使用 m_pBaseClass或 m_pfnGetBaseClass遍历整个类层次确定是否pBaseClass指向的类是基类,
//使用它可以判断某类是否是从pBaseClass指向的类在派生来。
// Implementation
void Store(CArchive& ar) const;
static CRuntimeClass* PASCAL Load(CArchive& ar, UINT* pwSchemaNum);
// CRuntimeClass objects linked together in simple list
CRuntimeClass* m_pNextClass; // linked list of registered classes
};
二、CObject类
CObject是MFC类的大多数类的基类,主要是通过它实现:
(1)、运行类信息;(2)、序列化;(3)、对象诊断输出;(4)、同集合类相兼容;
(1)、运行时类信息:
注意:要想使用CRuntimeClass结构得到运行时类信息,你必须在你的类中包括DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC、 DECLARE_DYNCREATE/IMPLEMENT_DYNCREATE或DECLARE_SERIAL/IMPLEMENT_SERIAL。但你的类必须是从CObject派生的才能使用这些宏, 因为通过DECLARE_DYNAMIC将定义一个实现如下的函数:
CRuntimeClass* PASCAL B::_GetBaseClass()
{
return RUNTIME_CLASS(base_name);
}
其中的RUNTIME_CLASS是这样定义的
#define RUNTIME_CLASS( class_name )
(CRuntimeClass *)(&class_name::class##class_name);
即得到类中的CRuntimeClass对象指针,显而易见,如果没有基类你用IMPLEMENT_DYNAMIC时将得到一个编译错误。 除非你象CObject一样不用DECLARE_DYNAMIC而定义和实现了这些函数,CObject中的GetBaseClass只是简单的返回NULL。 实际的DECLARE_DYNAMIC在afx.h中声明如下:
#define DECLARE_DYNAMIC(class_name)
protected:
static CRuntimeClass* PASCAL _GetBaseClass();
public:
static const AFX_DATA CRuntimeClass class##class_name;
virtual CRuntimeClass* GetRuntimeClass() const;
IMPLEMENT_DYNAMIC在afx.h中定义如下:
#define IMPLEMENT_DYNAMIC(class_name, base_class_name)
IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, 0xFFFF, NULL)
#define IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema, pfnNew)
CRuntimeClass* PASCAL class_name::_GetBaseClass()
{ return RUNTIME_CLASS(base_class_name); }
AFX_COMDAT const AFX_DATADEF CRuntimeClass class_name::class##class_name = {
#class_name, sizeof(class class_name), wSchema, pfnNew,
&class_name::_GetBaseClass, NULL };
CRuntimeClass* class_name::GetRuntimeClass() const
{ return RUNTIME_CLASS(class_name); }
其中的CRuntimeClass* GetRuntimeClass() const;被定义为虚函数,以完成在类层次上的重载。 这也是MFC利用多态实现运行时动态类信息的方法。
另外两个DECLARE_DYNCREATE和DECLARE_SERIAL类似。只不过它们多定义和实现了一些函数,对于使用DECLARE_DYNCREATE 要注意的是类必须要有一个无参数的缺省构造函数,因为在DECLARE_DYNCREATE中定义了一个CreateObject函数 用以在动态的建立对象,它只是一条简单的return new class_name。
我们先来看一下序列化:
,CObject实现这些功能绝大部分是通过它里面的CRuntimeClass对象classObject实现的,
CObject不支持多重继承,即表示以CObject为基类的类层次中只能有一个CObject基类。
之所以会这样,就是因为CRuntimeClass对象的成员m_pBaseClass的关系。因为它只是一个单链表。
以下是它在afx.h中的定义:
/
// class CObject is the root of all compliant objects
#ifdef _AFXDLL
class CObject
#else
class AFX_NOVTABLE CObject
#endif
{
public:
// Object model (types, destruction, allocation)
virtual CRuntimeClass* GetRuntimeClass() const;
// 上面的函数的实现只是很简单的return RUNTIME_CLASS(classObject);
virtual ~CObject(); // virtual destructors are necessary
// Diagnostic allocations
void* PASCAL operator new(size_t nSize);
void* PASCAL operator new(size_t, void* p);
void PASCAL operator delete(void* p);
#if _MSC_VER >= 1200
void PASCAL operator delete(void* p, void* pPlace);
#endif
#if defined(_DEBUG) && !defined(_AFX_NO_DEBUG_CRT)
// for file name/line number tracking using DEBUG_NEW
void* PASCAL operator new(size_t nSize, LPCSTR lpszFileName, int nLine);
#if _MSC_VER >= 1200
void PASCAL operator delete(void *p, LPCSTR lpszFileName, int nLine);
#endif
#endif
// Disable the copy constructor and assignment by default so you will get
// compiler errors instead of unexpected behaviour if you pass objects
// by value or assign objects.
protected:
CObject();
private:
CObject(const CObject& objectSrc); // no implementation
void operator=(const CObject& objectSrc); // no implementation
// Attributes
public:
BOOL IsSerializable() const; // 对对象进行序列化
BOOL IsKindOf(const CRuntimeClass* pClass) const; //判是否是
// Overridables
virtual void Serialize(CArchive& ar);
#if defined(_DEBUG) || defined(_AFXDLL)
// Diagnostic Support
virtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
#endif
// Implementation
public:
static const AFX_DATA CRuntimeClass classCObject;
#ifdef _AFXDLL
static CRuntimeClass* PASCAL _GetBaseClass();
#endif
};
如果你在你的类的实现和定义中使用可选宏的其中一个,你必须了解从CObject派生的好处。
第一级的宏是DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC,它允许你在运行时处理类名和类层次中的位置,允许你做有意义的诊断Dump。
第二级的宏是DECLARE_SERIAL/IMPLEMENT_SERIAL,它包括第一级宏所有的功能,允许你进行对象的序列化。
要想彻底了解序列化,不得不了解Archive类,我们将在下回详细论述这个类。希望感兴趣的朋友同我联系共同进步。
关于const的使用方法【转载】
1、什么是const?
常类型是指使用类型修饰符const说明的类型,常类型的变量或对象的值是不能被更新
的。(当然,我们可以偷梁换柱进行更新:)
2、为什么引入const?
const 推出的初始目的,正是为了取代预编译指令,消除它的缺点,同时继承它的优点
。
3、cons有什么主要的作用?
(1)可以定义const常量,具有不可变性。
例如:
const int Max=100;
int Array[Max];
(2)便于进行类型检查,使编译器对处理内容有更多了解,消除了一些隐患。
例如:
void f(const int i) { .........}
编译器就会知道i是一个常量,不允许修改;
(3)可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改。
同宏定义一样,可以做到不变则已,一变都变!如(1)中,如果想修改Max的内
容,只需要:const int Max=you want;即可!
(4)可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。
还是上面的例子,如果在函数体内修改了i,编译器就会报错;
例如:
void f(const int i) { i=10;//error! }
(5) 为函数重载提供了一个参考。
class A
{
......
void f(int i) {......} //一个函数
void f(int i) const {......} //上一个函数的重载
......
};
(6) 可以节省空间,避免不必要的内存分配。
例如:
#define PI 3.14159 //常量宏
const doulbe Pi=3.14159; //此时并未将Pi放入ROM中
......
double i=Pi; //此时为Pi分配内存,以后不再分配!
double I=PI; //编译期间进行宏替换,分配内存
double j=Pi; //没有内存分配
double J=PI; //再进行宏替换,又一次分配内存!
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#def
ine一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#d
efine定义的常量在内存中有若干个拷贝。
(7) 提高了效率。
编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,
这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
3、如何使用const?
(1)修饰一般常量
一般常量是指简单类型的常量。这种常量在定义时,修饰符const可以用在类型说
明符前,也可以用在类型说明符后。
例如:
int const x=2; 或 const int x=2;
(2)修饰常数组
定义或说明一个常数组可采用如下格式:
int const a[5]={1, 2, 3, 4, 5};
const int a[5]={1, 2, 3, 4, 5};
(3)修饰常对象
常对象是指对象常量,定义格式如下:
class A;
const A a;
A const a;
定义常对象时,同样要进行初始化,并且该对象不能再被更新,修饰符const可以
放在类名后面,也可以放在类名前面。
(4)修饰常指针
const int *A; //const修饰指向的对象,A可变,A指向的对象不可变
int const *A; //const修饰指向的对象,A可变,A指向的对象不可变
int *const A; //const修饰指针A, A不可变,A指向的对象可变
const int *const A; //指针A和A指向的对象都不可变
(5)修饰常引用
使用const修饰符也可以说明引用,被说明的引用为常引用,该引用所引用的对
象不能被更新。其定义格式如下:
const double & v;
(6)修饰函数的常参数
const修饰符也可以修饰函数的传递参数,格式如下:
void Fun(const int Var);
告诉编译器Var在函数体中的无法改变,从而防止了使用者的一些无意的或错误的
修改。
(7)修饰函数的返回值:
const修饰符也可以修饰函数的返回值,是返回值不可被改变,格式如下:
const int Fun1();
const MyClass Fun2();
(8)修饰类的成员函数:
const修饰符也可以修饰类的成员函数,格式如下:
class ClassName
{
public:
int Fun() const;
.....
};
这样,在调用函数Fun时就不能修改类里面的数据
(9)在另一连接文件中引用const常量
extern const int i; //正确的引用
extern const int j=10; //错误!常量不可以被再次赋值
另外,还要注意,常量必须初始化!
例如:
const int i=5;
4、几点值得讨论的地方:
(1)const究竟意味着什么?
说了这么多,你认为const意味着什么?一种修饰符?接口抽象?一种新类型?
也许都是,在Stroustup最初引入这个关键字时,只是为对象放入ROM做出了一种
可能,对于const对象,C++既允许对其进行静态初始化,也允许对他进行动态初始化。理
想的const对象应该在其构造函数完成之前都是可写的,在析够函数执行开始后也都是可写
的,换句话说,const对象具有从构造函数完成到析够函数执行之前的不变性,如果违反了
这条规则,结果都是未定义的!虽然我们把const放入ROM中,但这并不能够保证const的任
何形式的堕落,我们后面会给出具体的办法。无论const对象被放入ROM中,还是通过存储
保护机制加以保护,都只能保证,对于用户而言这个对象没有改变。换句话说,废料收集
器(我们以后会详细讨论,这就一笔带过)或数据库系统对一个const的修改怎没有任何问
题。
(2)位元const V.S. 抽象const?
对于关键字const的解释有好几种方式,最常见的就是位元const 和 抽象const。
下面我们看一个例子:
class A
{
public:
......
A f(const A& a);
......
};
如果采用抽象const进行解释,那就是f函数不会去改变所引用对象的抽象值,如
果采用位元const进行解释,那就成了f函数不会去改变所引用对象的任何位元。
我们可以看到位元解释正是c++对const问题的定义,const成员函数不被允许修
改它所在对象的任何一个数据成员。
为什么这样呢?因为使用位元const有2个好处:
最大的好处是可以很容易地检测到违反位元const规定的事件:编译器只用去寻
找有没有对数据成员的赋值就可以了。另外,如果我们采用了位元const,那么,对于一些
比较简单的const对象,我们就可以把它安全的放入ROM中,对于一些程序而言,这无疑是
一个很重要的优化方式。(关于优化处理,我们到时候专门进行讨论)
当然,位元const也有缺点,要不然,抽象const也就没有产生的必要了。
首先,位元const的抽象性比抽象const的级别更低!实际上,大家都知道,一个
库接口的抽象性级别越低,使用这个库就越困难。
其次,使用位元const的库接口会暴露库的一些实现细节,而这往往会带来一些
负面效应。所以,在库接口和程序实现细节上,我们都应该采用抽象const。
有时,我们可能希望对const做出一些其它的解释,那么,就要注意了,目前,
大多数对const的解释都是类型不安全的,这里我们就不举例子了,你可以自己考虑一下,
总之,我们尽量避免对const的重新解释。
(3)放在类内部的常量有什么限制?
看看下面这个例子:
class A
{
private:
const int c3 = 7; // ???
static int c4 = 7; // ???
static const float c5 = 7; // ???
......
};
你认为上面的3句对吗?呵呵,都不对!使用这种类内部的初始化语法的时候,
常量必须是被一个常量表达式初始化的整型或枚举类型,而且必须是static和const形式。
这显然是一个很严重的限制!
那么,我们的标准委员会为什么做这样的规定呢?一般来说,类在一个头文件中
被声明,而头文件被包含到许多互相调用的单元去。但是,为了避免复杂的编译器规则,
C++要求每一个对象只有一个单独的定义。如果C++允许在类内部定义一个和对象一样占据
内存的实体的话,这种规则就被破坏了。
(4)如何初始化类内部的常量?
一种方法就是static 和 const 并用,在内部初始化,如上面的例子;
另一个很常见的方法就是初始化列表:
class A
{
public:
A(int i=0):test(i) {}
private:
const int i;
};
还有一种方式就是在外部初始化,例如:
class A
{
public:
A() {}
private:
static const int i; //注意必须是静态的!
};
const int A::i=3;
(5)常量与数组的组合有什么特殊吗?
我们给出下面的代码:
const int size[3]={10,20,50};
int array[size[2]];
有什么问题吗?对了,编译通不过!为什么呢?
const可以用于集合,但编译器不能把一个集合存放在它的符号表里,所以必
须分配内存。在这种情况下,const意味着“不能改变的一块存储”。然而,其值在编译时
不能被使用,因为编译器在编译时不需要知道存储的内容。自然,作为数组的大小就不行
了:)
你再看看下面的例子:
class A
{
public:
A(int i=0):test[2]({1,2}) {} //你认为行吗?
private:
const int test[2];
};
vc6下编译通不过,为什么呢?
关于这个问题,前两天,njboy问我是怎么回事?我反问他:“你认为呢?”他
想了想,给出了一下解释,大家可以看看:我们知道编译器堆初始化列表的操作是在构造
函数之内,显式调用可用代码之前,初始化的次序依据数据声明的次序。初始化时机应该
没有什么问题,那么就只有是编译器对数组做了什么手脚!其实做什么手脚,我也不知道
,我只好对他进行猜测:编译器搜索到test发现是一个非静态的数组,于是,为他分配内
存空间,这里需要注意了,它应该是一下分配完,并非先分配test[0],然后利用初始化列
表初始化,再分配test[1],这就导致数组的初始化实际上是赋值!然而,常量不允许赋值
,所以无法通过。
呵呵,看了这一段冠冕堂皇的话,真让我笑死了!njboy别怪我揭你短呀:)我对
此的解释是这样的:C++标准有一个规定,不允许无序对象在类内部初始化,数组显然是一
个无序的,所以这样的初始化是错误的!对于他,只能在类的外部进行初始化,如果想让
它通过,只需要声明为静态的,然后初始化。
这里我们看到,常量与数组的组合没有什么特殊!一切都是数组惹的祸!
(6)this指针是不是const类型的?
this指针是一个很重要的概念,那该如何理解她呢?也许这个话题太大了,那我
们缩小一些:this指针是个什么类型的?这要看具体情况:如果在非const成员函数中,t
his指针只是一个类类型的;如果在const成员函数中,this指针是一个const类类型的;如
果在volatile成员函数中,this指针就是一个volatile类类型的。
(7)const到底是不是一个重载的参考对象?
先看一下下面的例子:
class A
{
......
void f(int i) {......} //一个函数
void f(int i) const {......} //上一个函数的重载
......
};
上面是重载是没有问题的了,那么下面的呢?
class A
{
......
void f(int i) {......} //一个函数
void f(const int i) {......} //?????
......
};
这个是错误的,编译通不过。那么是不是说明内部参数的const不予重载呢?再
看下面的例子:
class A
{
......
void f(int& ) {......} //一个函数
void f(const int& ) {......} //?????
......
};
这个程序是正确的,看来上面的结论是错误的。为什么会这样呢?这要涉及到接
口的透明度问题。按值传递时,对用户而言,这是透明的,用户不知道函数对形参做了什
么手脚,在这种情况下进行重载是没有意义的,所以规定不能重载!当指针或引用被引入
时,用户就会对函数的操作有了一定的了解,不再是透明的了,这时重载是有意义的,所
以规定可以重载。
(8)什么情况下为const分配内存?
以下是我想到的可能情况,当然,有的编译器进行了优化,可能不分配内存。
A、作为非静态的类成员时;
B、用于集合时;
C、被取地址时;
D、在main函数体内部通过函数来获得值时;
E、const的 class或struct有用户定义的构造函数、析构函数或基类时;。
F、当const的长度比计算机字长还长时;
G、参数中的const;
H、使用了extern时。
不知道还有没有其他情况,欢迎高手指点:)
(9)临时变量到底是不是常量?
很多情况下,编译器必须建立临时对象。像其他任何对象一样,它们需要存储空
间而且必须被构造和删除。区别是我们从来看不到编译器负责决定它们的去留以及它们存
在的细节。对于C++标准草案而言:临时对象自动地成为常量。因为我们通常接触不到临时
对象,不能使用与之相关的信息,所以告诉临时对象做一些改变有可能会出错。当然,这
与编译器有关,例如:vc6、vc7都对此作了扩展,所以,用临时对象做左值,编译器并没
有报错。当他们遇到流库时,也会引发一系列的问题,限于篇幅所限,我们在讨论临时对
象时详细讨论。
(10)与static搭配会不会有问题?
假设有一个类:
class A
{
public:
......
static void f() const { ......}
......
};
我们发现编译器会报错,因为在这种情况下static不能够与const共存!
为什么会出现这种问题呢?我们知道static修饰的函数使其被各个对象以及派生
类的对象共享,然而const却使得所有引用它的对象内部的数据不能被修改,换句话说,c
onst的意义在基类不知道的情况下被扩展了,为了防止这类事情的发生,干脆就定义stat
ic不能够与const共存(只是上面的情况下)!
我们再进一步想一下如果不这样定义会不会出现什么情况呢?(换句话说,有什
么情况使得这种扩展产生异常呢?)
由于static函数只能调用静态对象与函数,并且不能够与virtual共存(virtua
l为动态,而static为静态,两者内部机制冲突),所以不可能由于虚拟机制产生问题。
下面我们再看一种情况:
class A
{
public:
static void f() { cout<<"A"<<endl; p="" }="" <="">
static void g() { f(); }
};
class B:public A
{
public:
static void f() { cout<<"B"<<endl; p="" }="" <="">
};
由于静态函数独立于类体外所以B也可以调用g(),但是也由于静态函数依次初
始化,所以就注定了g()所调用的f()只能为基类A的,永远都不会是B的!(这里,有两个f
()并不会发生冲突,因为编译器利用了name mangling机制),所以这种情况下也不会发生
意外!
那么究竟在那种情况下使得static不能够与const共存呢?或者说根本就没有
,而是标准委员会为了将来扩展避免发生意外而作的这项规定?希望高手指点一下:)
(11)如何修改常量?
有时候我们却不得不对类内的数据进行修改,但是我们的接口却被声明了cons
t,那该怎么处理呢?我对这个问题的看法如下:
1)标准用法:mutable
class A
{
public:
A(int i=0):test(i) { }
void SetValue(int i)const { test=i; }
private:
mutable int test; //这里处理!
};
2)强制转换:static_cast
class A
{
public:
A(int i=0):test(i) { }
void SetValue(int i)const
{ static_cast (test)=i; }//这里处理!
private:
int test;
};
3)灵活的指针:int*
class A
{
public:
A(int i=0):test(i) { }
void SetValue(int i)const
{ *test=i; }
private:
int* test; //这里处理!
};
4)未定义的处理
class A
{
public:
A(int i=0):test(i) { }
void SetValue(int i)const
{ int *p=(int*)&test; *p=i; }//这里处理!
private:
int test;
};
注意,这里虽然说可以这样修改,但结果是未定义的,避免使用!
5)内部处理:this指针
class A
{
public:
A(int i=0):test(i) { }
void SetValue(int i)const
{ ((A*)this)->test=i; }//这里处理!
private:
int test;
};
6)最另类的处理:空间布局
class A
{
public:
A(int i=0):test(i),c('a') { }
private:
char c;
const int test;
};
int main()
{
A a(3);
A* pa=&a;
char* p=(char*)pa;
int* pi=(int*)(p+4);//利用边缘调整
*pi=5; //此处改变了test的值!
return 0;
}
虽然我给出了6中方法,但是我只是想说明如何更改,但出了第一种用法之外,另
外5种用法,我们并不提倡,不要因为我这么写了,你就这么用,否则,我真是要误人子弟
了:)
(12)最后我们来讨论一下常量对象的动态创建。
既然编译器可以动态初始化常量,就自然可以动态创建,例如:
const int* pi=new const int(10);
这里要注意2点:
1)const对象必须被初始化!所以(10)是不能够少的。
2)new返回的指针必须是const类型的。
那么我们可不可以动态创建一个数组呢?
答案是否定的,因为new内置类型的数组,不能被初始化。
这里我们忽视了数组是类类型的,同样对于类内部数组初始化我们也做出了这
样的忽视,因为这涉及到数组的问题,我们以后再讨论。
好了,就写到这吧,太累了,休息一下:)
五 使用const 的一些建议
1 要大胆的使用const,这将给你带来无尽的益处,但前提是你必须搞清楚原委;
2 要避免最一般的赋值操作错误,如将const 变量赋值,具体可见思考题;
3 在参数中使用const 应该使用引用或指针,而不是一般的对象实例,原因同上;
4 const 在成员函数中的三种用法(参数、返回值、函数)要很好的使用;
5 不要轻易的将函数的返回值类型定为const;
6 除了重载操作符外一般不要将返回值类型定为对某个对象的const 引用;