CString 操作指南
通过阅读本文你可以学习如何有效地使用 CString。
CString 是一种很有用的数据类型。它们很大程度上简化了MFC中的许多操
作,使得MFC在做字符串操作的时候方便了很多。不管怎样,使用 CString有很
多特殊的技巧,特别是对于纯 C背景下走出来的程序员来说有点难以学习。这篇
文章就来讨论这些技巧。
使用 CString可以让你对字符串的操作更加直截了当。这篇文章不是 CString
的完全手册,但囊括了大部分常见基本问题。这篇文章包括以下内容:
1.CString 对象的连接
2.格式化字符串(包括 int 型转化为 CString )
3.CString 型转化成 int 型
4.CString 型和 char* 类型的相互转化
char* 转化成 CString
CString 转化成 char* 之一:使用 LPCTSTR强制转化
CString 转化成 char* 之二:使用 CString对象的 GetBuffer方法
CString 转化成 char* 之三: 和控件的接口
5.CString 型转化成 BSTR 型;
6.BSTR 型转化成 CString 型;
7.VARIANT 型转化成 CString 型;
8.载入字符串表资源;
9.CString 和临时对象;
10.CString 的效率;
11.
总结
初级经济法重点总结下载党员个人总结TXt高中句型全总结.doc高中句型全总结.doc理论力学知识点总结pdf
下面我分别讨论。
1. CString 对象的连接
能体现出 CString 类型方便性特点的一个方面就字符串的连接,使用
CString 类型,你能很方便地连接两个字符串,正如下面的例子:
1 CString gray("Gray");
2 CString cat("Cat");
3 CString graycat = gray + cat;
要比用下面的方法好得多:
1 char gray[] = "Gray";
2 char cat[] = "Cat";
3 char * graycat = malloc(strlen(gray) + strlen(cat) + 1);
4 strcpy(graycat, gray);
5 strcat(graycat, cat);
2. 格式化字符串
与其用 sprintf() 函数或 wsprintf() 函数来格式化一个字符串,还不如用
CString 对象的 Format()方法:
1 CString s;
2 s.Format(_T("The total is %d"), total);
用这种方法的好处是你不用担心用来存放格式化后数据的缓冲区是否足够
大,这些工作由 CString类替你完成。
格式化是一种把其它不是字符串类型的数据转化为 CString 类型的最常用技
巧,比如,把一个整数转化成 CString类型,可用如下方法:
1 CString s;
2 s.Format(_T("%d"), total);
我总是对我的字符串使用_T()宏,这是为了让我的代码至少有 Unicode 的意
识,当然,关于 Unicode的话题不在这篇文章的讨论范围。_T()宏在 8位字符环
境下是如下定义的:
1 #define _T(x) x // 非 Unicode版本(non-Unicode version)
而在 Unicode环境下是如下定义的:
1 #define _T(x) L##x // Unicode版本(Unicode version)
所以在 Unicode环境下,它的效果就相当于:
1 s.Format(L"%d", total);
如果你认为你的程序可能在 Unicode 的环境下运行,那么开始在意用
Unicode 编码。比如说,不要用 sizeof() 操作符来获得字符串的长度,因为在
Unicode环境下就会有 2倍的误差。我们可以用一些方法来隐藏 Unicode的一些
细节,比如在我需要获得字符长度的时候,我会用一个叫做 DIM 的宏,这个宏
是在我的 dim.h文件中定义的,我会在我写的所有程序中都包含这个文件:
1 #define DIM(x) ( sizeof((x)) / sizeof((x)[0]) )
这个宏不仅可以用来解决 Unicode的字符串长度的问题,也可以用在编译时
定义的表格上,它可以获得表格的项数,如下:
1 clss Whatever { ... };
2 Whatever data[] = {
3 { ... },
4 ...
5 { ... },
6 };
7 for(int i = 0; i < DIM(data); i++) // 扫描表格寻找匹配项。
这里要提醒你的就是一定要注意那些在参数中需要真实字节数的API函数调
用,如果你传递字符个数给它,它将不能正常工作。如下:
1 TCHAR data[20];
2 lstrcpyn(data, longstring, sizeof(data) - 1); // WRONG!
3 lstrcpyn(data, longstring, DIM(data) - 1); // RIGHT
4 WriteFile(f, data, DIM(data), &bytesWritten, NULL); // WRONG!
5 WriteFile(f, data, sizeof(data), &bytesWritten, NULL); // RIGHT
造成以上原因是因为 lstrcpyn需要一个字符个数作为参数,但是WriteFile却
需要字节数作为参数。
同样需要注意的是有时候需要写出数据的所有内容。如果你仅仅只想写出数
据的真实长度,你可能会认为你应该这样做:
1 WriteFile(f, data, lstrlen(data), &bytesWritten, NULL); // WRONG
但是在 Unicode环境下,它不会正常工作。正确的做法应该是这样:
1 WriteFile(f, data, lstrlen(data) * sizeof(TCHAR), &bytesWritten, NULL); //
RIGHT
因为WriteFile需要的是一个以字节为单位的长度。(可能有些人会想“在非
Unicode的环境下运行这行代码,就意味着总是在做一个多余的乘 1操作,这样
不会降低程序的效率吗?”这种想法是多余的,你必须要了解编译器实际上做了
什么,没有哪一个 C 或 C++编译器会把这种无聊的乘 1 操作留在代码中。在
Unicode环境下运行的时候,你也不必担心那个乘 2操作会降低程序的效率,记
住,这只是一个左移一位的操作而已,编译器也很乐意为你做这种替换。)
使用_T宏并不是意味着你已经创建了一个 Unicode的程序,你只是创建了一
个有 Unicode意识的程序而已。如果你在默认的 8-bit模式下编译你的程序的话,
得到的将是一个普通的8-bit的应用程序(这里的8-bit指的只是8位的字符编码,
并不是指 8 位的计算机系统);当你在 Unicode 环境下编译你的程序时,你才会
得到一个 Unicode的程序。记住,CString 在 Unicode 环境下,里面包含的可都
是 16位的字符哦。
3. CString 型转化成 int 型
把 CString 类型的数据转化成整数类型最简单的方法就是使用
标准
excel标准偏差excel标准偏差函数exl标准差函数国标检验抽样标准表免费下载红头文件格式标准下载
的字符
串到整数转换例程。
虽然通常你怀疑使用_atoi()函数是一个好的选择,它也很少会是一个正确的
选择。如果你准备使用 Unicode 字符,你应该用_ttoi(),它在 ANSI 编码系统中
被编译成_atoi(),而在 Unicode 编码系统中编译成_wtoi()。你也可以考虑使用
_tcstoul()或者_tcstol(),它们都能把字符串转化成任意进制的长整数(如二进制、
八进制、十进制或十六进制),不同点在于前者转化后的数据是无符号的
(unsigned),而后者相反。看下面的例子:
1 CString hex = _T("FAB");
2 CString decimal = _T("4011");
3 ASSERT(_tcstoul(hex, 0, 16) == _ttoi(decimal));
4. CString 型和 char* 类型的相互转化
这是初学者使用 CString 时最常见的问题。有了 C++ 的帮助,很多问题你
不需要深入的去考虑它,直接拿来用就行了,但是如果你不能深入了解它的运行
机制,又会有很多问题让你迷惑,特别是有些看起来没有问题的代码,却偏偏不
能正常工作。
比如,你会奇怪为什么不能写向下面这样的代码呢:
1 CString graycat = "Gray" + "Cat";
或者这样:
1 CString graycat("Gray" + "Cat");
事实上,编译器将抱怨上面的这些尝试。为什么呢?因为针对 CString 和
LPCTSTR数据类型的各种各样的组合,“ +” 运算符 被定义成一个重载操作符。
而不是两个 LPCTSTR 数据类型,它是底层数据类型。你不能对基本数据(如 int、
char 或者 char*)类型重载 C++ 的运算符。你可以象下面这样做:
1 CString graycat = CString("Gray") + CString("Cat");
或者这样:
1 CString graycat = CString("Gray") + "Cat";
研究一番就会发现:“ +”总是使用在至少有一个 CString 对象和一个
LPCSTR 的场合。
注意,编写有 Unicode 意识的代码总是一件好事,比如:
1 CString graycat = CString(_T("Gray")) + _T("Cat");
这将使得你的代码可以直接移植。
char* 转化为 CString
现在你有一个 char* 类型的数据,或者说一个字符串。怎么样创建 CString
1 char * p = "This is a test";
或者象下面这样更具有 Unicode 意识:
1 TCHAR * p = _T("This is a test")
或
1 LPTSTR p = _T("This is a test");
你可以使用下面任意一种写法:
1 CString s = "This is a test"; // 8-bit only
2 CString s = _T("This is a test"); // Unicode-aware
3 CString s("This is a test"); // 8-bit only
4 CString s(_T("This is a test")); // Unicode-aware
5 CString s = p;
6 CString s(p);
用这些方法可以轻松将常量字符串或指针转换成 CString。需要注意的是,
字符的赋值总是被拷贝到 CString 对象中去的,所以你可以象下面这样操作:
1 TCHAR * p = _T("Gray");
2 CString s(p);
3 p = _T("Cat");
4 s += p;
结果字符串肯定是“GrayCat”。
CString 类还有几个其它的构造函数,但是这里我们不考虑它,如果你有兴
趣可以自己查看相关文档。
事实上,CString 类的构造函数比我展示的要复杂,比如:
1 CString s = "This is a test";
这是很草率的编码,但是实际上它在 Unicode 环境下能编译通过。它在运
行时调用构造函数的 MultiByteToWideChar 操作将 8 位字符串转换成 16 位字
符串。不管怎样,如果 char * 指针是网络上传输的 8 位数据,这种转换是很有
用的。
CString 转化成 char* 之一:强制类型转换为 LPCTSTR;
这是一种略微硬性的转换,有关“正确”的做法,人们在认识上还存在许多
混乱,正确的使用方法有很多,但错误的使用方法可能与正确的使用方法一样多。
我们首先要了解 CString 是一种很特殊的 C++ 对象,它里面包含了三个值:
一个指向某个数据缓冲区的指针、一个是该缓冲中有效的字符记数以及一个缓冲
区长度。 有效字符数的大小可以是从 0到该缓冲最大长度值减 1之间的任何数
(因为字符串结尾有一个 NULL字符)。字符记数和缓冲区长度被巧妙隐藏。
除非你做一些特殊的操作,否则你不可能知道给 CString 对象分配的缓冲区
的长度。这样,即使你获得了该 0缓冲的地址,你也无法更改其中的内容,不能
截短字符串,也 绝对没有办法加长它的内容,否则第一时间就会看到溢出。
LPCTSTR 操作符(或者更明确地说就是 TCHAR * 操作符)在 CString 类中
被重载了,该操作符的定义是返回缓冲区的地址,因此,如果你需要一个指向
CString 的 字符串指针的话,可以这样做:
1 CString s("GrayCat");
2 LPCTSTR p = s;
它可以正确地运行。这是由 C语言的强制类型转化规则实现的。当需要强制
类型转化时,C++规测容许这种选择。比如,你可以将(浮点数)定义为将某个
复数 (有一对浮点数)进行强制类型转换后只返回该复数的第一个浮点数(也
就是其实部)。可以象下面这样:
1 Complex c(1.2f, 4.8f);
2 float realpart = c;
如果(float)操作符定义正确的话,那么实部的的值应该是 1.2。
这种强制转化适合所有这种情况,例如,任何带有 LPCTSTR 类型参数的函
数都会强制执行这种转换。 于是,你可能有这样一个函数(也许在某个你买来
的 DLL中):
1 BOOL DoSomethingCool(LPCTSTR s);
你象下面这样调用它:
1 CString file("c:\\myfiles\\coolstuff")
2 BOOL result = DoSomethingCool(file);
它能正确运行。因为 DoSomethingCool 函数已经说明了需要一个 LPCTSTR
类型的参数,因此 LPCTSTR 被应用于该参数,在 MFC 中就是返回的串地址。
如果你要格式化字符串怎么办呢?
1 CString graycat("GrayCat");
2 CString s;
3 s.Format("Mew! I love %s", graycat);
注意由于在可变参数列表中的值(在函数说明中是以“...”表示的)并没有
隐含一个强制类型转换操作符。你会得到什么结果呢?
一个令人惊讶的结果,我们得到的实际结果串是:
"Mew! I love GrayCat"。
因为 MFC 的设计者们在设计 CString 数据类型时非常小心, CString 类型
表达式求值后指向了字符串,所以这里看不到任何象 Format 或 sprintf 中的强
制类型转换,你仍然可以得到正确的行为。描述 CString 的附加数据实际上在
CString 名义地址之后。
有一件事情你是不能做的,那就是修改字符串。比如,你可能会尝试用“,”
代替“.”(不要做这样的,如果你在乎国际化问题,你应该使用十进制转换的
National Language Support 特性,),下面是个简单的例子:
1 CString v("1.00"); // 货币金额,两位小数
2 LPCTSTR p = v;
3 p[lstrlen(p) - 3] = '','';
这时编译器会报错,因为你赋值了一个常量串。如果你做如下尝试,编译器
也会错:
1 strcat(p, "each");
因为 strcat 的第一个参数应该是 LPTSTR 类型的数据,而你却给了一个
LPCTSTR。
不要试图钻这个错误消息的牛角尖,这只会使你自己陷入麻烦!
原因是缓冲有一个计数,它是不可存取的(它位于 CString 地址之下的一个
隐藏区域),如果你改变这个串,缓冲中的字符计数不会反映所做的修改。此外,
如果字符串长度恰好是该字符串物理限制的长度(梢后还会讲到这个问题),那
么扩展该字符串将改写缓冲以外的任何数据,那是你无权进行写操作的内存(不
对吗?),你会毁换坏不属于你的内存。这是应用程序真正的死亡处方。
CString 转化成 char* 之二:使用 CString 对象的 GetBuffer 方法;
如果你需要修改 CString 中的内容,它有一个特殊的方法可以使用,那就是
GetBuffer,它的作用是返回一个可写的缓冲指针。 如果你只是打算修改字符或
者截短字符串,你完全可以这样做:
1 CString s(_T("File.ext"));
2 LPTSTR p = s.GetBuffer();
3 LPTSTR dot = strchr(p, ''.''); // OK, should have used s.Find...
4 if(p != NULL)
5 *p = _T(''\0'');
6 s.ReleaseBuffer();
这是 GetBuffer 的第一种用法,也是最简单的一种,不用给它传递参数,它
使用默认值 0,意思是:“给我这个字符串的指针,我保证不加长它”。当你调用
ReleaseBuffer 时,字符串的实际长度会被重新计算,然后存入 CString 对象中。
必须强调一点,在 GetBuffer 和 ReleaseBuffer 之间这个范围,一定不能使
用你要操作的这个缓冲的 CString 对象的任何方法。因为 ReleaseBuffer 被调用
之前,该 CString 对象的完整性得不到保障。研究以下代码:
1 CString s(...);
2
3 LPTSTR p = s.GetBuffer();
4
5 //... 这个指针 p 发生了很多事情
6
7 int n = s.GetLength(); // 很糟 D!!!!! 有可能给出错误的答案!!!
8
9 s.TrimRight(); // 很糟!!!!! 不能保证能正常工作!!!!
10 s.ReleaseBuffer(); // 现在应该 OK
11
12 int m = s.GetLength(); // 这个结果可以保证是正确的。
13
14 s.TrimRight(); // 将正常工作。
假设你想增加字符串的长度,你首先要知道这个字符串可能会有多长,好比
是声明字符串数组的时候用:
1 char buffer[1024];
表示 1024 个字符空间足以让你做任何想做得事情。在 CString 中与之意义
相等的表示法:
1 LPTSTR p = s.GetBuffer(1024);
调用这个函数后,你不仅获得了字符串缓冲区的指针,而且同时还获得了长
度至少为 1024 个字符的空间(注意,我说的是“字符”,而不是“字节”,因为
CString 是以隐含方式感知 Unicode 的)。
同时,还应该注意的是,如果你有一个常量串指针,这个串本身的值被存储
在只读内存中,如果试图存储它,即使你已经调用了 GetBuffer ,并获得一个只
读内存的指针,存入操作会失败,并
报告
软件系统测试报告下载sgs报告如何下载关于路面塌陷情况报告535n,sgs报告怎么下载竣工报告下载
存取错误。我没有在 CString 上证明这
一点,但我看到过大把的 C 程序员经常犯这个错误。
C 程序员有一个通病是分配一个固定长度的缓冲,对它进行 sprintf 操作,
然后将它赋值给一个 CString:
1 char buffer[256];
2 sprintf(buffer, "%......", args, ...); // ... 部分省略许多细节
3 CString s = buffer;
虽然更好的形式可以这么做:
1 CString s;
2 s.Format(_T("%...."), args, ...);
如果你的字符串长度万一超过 256 个字符的时候,不会破坏堆栈。
另外一个常见的错误是:既然固定大小的内存不工作,那么就采用动态分配
字节,这种做法弊端更大:
3 int len = lstrlen(parm1) + 13 lstrlen(parm2) + 10 + 100;
4
5 char * buffer = new char[len];
6
7 sprintf(buffer, "%s is equal to %s, valid data", parm1, parm2);
8
9 CString s = buffer;
10
11 ......
12
13 delete [] buffer;
它可以能被简单地写成:
1 CString s;
2
3 s.Format(_T("%s is equal to %s, valid data"), parm1, parm2);
需要注意 sprintf 例子都不是 Unicode 就绪的,尽管你可以使用 tsprintf 以
及用 _T() 来包围格式化字符串,但是基本 思路仍然是在走弯路,这这样很容易
出错。
CString to char * 之三:和控件的接口;
我们经常需要把一个 CString 的值传递给一个控件,比如,CTreeCtrl。MFC
为我们提供了很多便利来重载这个操作,但是 在大多数情况下,你使用“原始”
形式的更新,因此需要将墨某个串指针存储到 TVINSERTITEMSTRUCT 结构的
TVITEM 成员中。如下:
1 TVINSERTITEMSTRUCT tvi;
2 CString s;
3 // ... 为 s赋一些值。
4 tvi.item.pszText = s; // Compiler yells at you here
5 // ... 填写 tvi的其他域
6 HTREEITEM ti = c_MyTree.InsertItem(&tvi);
为什么编译器会报错呢?明明看起来很完美的用法啊!但是事实上如果你看
看 TVITEM 结构的定义你就会明白,在 TVITEM 结构中 pszText 成员的声明如下:
1 LPTSTR pszText;
2 int cchTextMax;
因此,赋值不是赋给一个 LPCTSTR 类型的变量,而且编译器无法知道如何
将赋值语句右边强制转换成 LPCTSTR。好吧,你说,那我就改成这样:
1 tvi.item.pszText = (LPCTSTR)s; //编译器依然会报错。
编译器之所以依然报错是因为你试图把一个 LPCTSTR 类型的变量赋值给一
个 LPTSTR 类型的变量,这种操作在 C或 C++中是被禁止的。你不能用这种方法
来滥用常量指针与非常量指针概念,否则,会扰乱编译器的优化机制,使之不知
如何优化你的程序。比如,如果你这么做:
1 const int i = ...;
2 //... do lots of stuff
3 ... = a[i]; // usage 1
4 // ... lots more stuff
5 ... = a[i]; // usage 2
那么,编译器会以为既然 i 是 const ,所以 usage1 和 usage2 的值是相同
的,并且它甚至能事先计算好 usage1 处的 a[i] 的地址,然后保留着在后面的
usage2 处使用,而不是重新计算。如果你按如下方式写的话:
1 const int i = ...;
2 int * p = &i;
3 //... do lots of stuff
4 ... = a[i]; // usage 1
5 // ... lots more stuff
6 (*p)++; // mess over compiler''s assumption
7 // ... and other stuff
8 ... = a[i]; // usage 2
编译器将认为 i 是常量,从而 a[i] 的位置也是常量,这样间接地破坏了先
前的假设。因此,你的程序将会在 debug 编译模式(没有优化)和 release 编
译模式(完全优化)中反映出不同的行为,这种情况可不好,所以当你试图把指
向 i 的指针赋值给一个 可修改的引用时,会被编译器诊断为这是一种伪造。这
就是为什么(LPCTSTR)强制类型转化不起作用的原因。
为什么不把该成员声明成 LPCTSTR 类型呢?因为这个结构被用于读写控件。
当你向控件写数据时,文本指针实际上被当成 LPCTSTR,而当你从控件读数据 时,
你必须有一个可写的字符串。这个结构无法区分它是用来读还是用来写。
因此,你会常常在我的代码中看到如下的用法:
1 tvi.item.pszText = (LPTSTR)(LPCTSTR)s;
它把 CString 强制类型转化成 LPCTSTR,也就是说先获得改字符串的地址,
然后再强制类型转化成 LPTSTR,以便可以对之进行赋值操作。 注意这只有在使
用 Set 或 Insert 之类的方法才有效!如果你试图获取数据,则不能这么做。
如果你打算获取存储在控件中的数据,则方法稍有不同,例如,对某个
CTreeCtrl 使用 GetItem 方法,我想获取项目的文本。我知道这些 文本的长度不
会超过 MY_LIMIT,因此我可以这样写:
1 TVITEM tvi;
2 // ... assorted initialization of other fields of tvi
3 tvi.pszText = s.GetBuffer(MY_LIMIT);
4 tvi.cchTextMax = MY_LIMIT;
5 c_MyTree.GetItem(&tvi);
6 s.ReleaseBuffer();
可以看出来,其实上面的代码对所有类型的 Set 方法都适用,但是并不需
要这么做,因为所有的类 Set 方法(包括 Insert方法)不会改变字符串的内容。
但是当你需要写 CString 对象时,必须保证缓冲是可写的,这正是 GetBuffer 所
做的事情。再次强调: 一旦做了一次 GetBuffer 调用,那么在调用 ReleaseBuffer
之前不要对这个 CString 对象做任何操作。
5. CString 型转化成 BSTR 型
当我们使用 ActiveX 控件编程时,经常需要用到将某个值表示成 BSTR 类型。
BSTR 是一种记数字符串,Intel平台上的宽字符串(Unicode),并且 可以包含嵌
入的 NULL 字符。
你可以调用 CString 对象的 AllocSysString 方法将 CString 转化成 BSTR:
1 CString s;
2 s = ... ; // whatever
3 BSTR b = s.AllocSysString();
现在指针 b 指向的就是一个新分配的 BSTR 对象,该对象是 CString 的一
个拷贝,包含终结 NULL 字符。现在你可以将它传递给任何需要 BSTR 的接口。
通常,BSTR 由接收它的组件来释放,如果你需要自己释放 BSTR 的话,可以这
么做:
1 ::SysFreeString(b);
对于如何表示传递给 ActiveX 控件的字符串,在微软内部曾一度争论不休,
最后 Visual Basic 的人占了上风,BSTR(“Basic String”的首字母缩写)就是这场
争论的结果。
6. BSTR 型转化成 CString 型
由于 BSTR 是记数 Unicode 字符串,你可以用标准转换方法来创建 8 位
的 CString。实际上,这是 CString 内建的功能。在 CString 中 有特殊的构造函
数可以把 ANSI 转化成 Unicode,也可以把 Unicode 转化成 ANSI。你同样可以
从 VARIANT 类型的变量中获得 BSTR 类型的字符串,VARIANT 类型是 由各种
COM 和 Automation (自动化)调用返回的类型。
例如,在一个 ANSI程序中:
1 BSTR b;
2 b = ...; // whatever
3 CString s(b == NULL ? L"" : b)
对于单个的 BSTR 串来说,这种用法可以工作得很好,这是因为 CString 有
一个特殊的构造函数以 LPCWSTR(BSTR正是这种类型) 为参数,并将它转化成
ANSI 类型。专门检查是必须的,因为 BSTR 可能为空值,而 CString 的构造函
数对于 NULL 值情况考虑的不是很周到,(感谢 Brian Ross 指出这一点!)。这种
用法也只能处理包含 NUL 终结字符的单字符串;如果要转化含有多个 NULL 字
符 串,你得额外做一些工作才行。在 CString 中内嵌的 NULL 字符通常表现不
尽如人意,应该尽量避免。
根据 C/C++ 规则,如果你有一个 LPWSTR,那么它别无选择,只能和
LPCWSTR 参数匹配。
在 Unicode 模式下,它的构造函数是:
1 CString::CString(LPCTSTR);
正如上面所表示的,在 ANSI 模式下,它有一个特殊的构造函数:
1 CString::CString(LPCWSTR);
它会调用一个内部的函数将 Unicode 字符串转换成 ANSI 字符串。(在
Unicode模式下,有一个专门的构造函数,该函数有一个参数是 LPCSTR类型――
一个 8 位 ANSI 字符串 指针,该函数将它加宽为 Unicode 的字符串!)再次强
调:一定要检查 BSTR 的值是否为 NULL。
另外还有一个问题,正如上文提到的:BSTRs 可以含有多个内嵌的 NULL 字
符,但是 CString 的构造函数只能处理某个串中单个 NULL 字符。 也就是说,
如果串中含有嵌入的 NUL字节,CString 将会计算出错误的串长度。你必须自己
处理它。如果你看看 strcore.cpp 中的构造函数,你会发现 它们都调用了 lstrlen,
也就是计算字符串的长度。
注意从 Unicode 到 ANSI 的转换使用带专门参数的 ::WideCharToMultiByte,
如果你不想使用这种默认的转换方式,则必须编写自己的转化代码。
如果你在 UNICODE 模式下编译代码,你可以简单地写成:
1 CString convert(BSTR b)
2 {
3 if(b == NULL)
4 return CString(_T(""));
5 CString s(b); // in UNICODE mode
6 return s;
7 }
如果是 ANSI 模式,则需要更复杂的过程来转换。注意这个代码使用
与 ::WideCharToMultiByte 相同的参数值。所以你 只能在想要改变这些参数进行
转换时使用该技术。例如,指定不同的默认字符,不同的标志集等。
8 CString convert(BSTR b)
9 {
10 CString s;
11 if(b == NULL)
12 return s; // empty for NULL BSTR
13 #ifdef UNICODE
14 s = b;
15 #else
16 LPSTR p = s.GetBuffer(SysStringLen(b) + 1);
17 ::WideCharToMultiByte(CP_ACP, // ANSI Code Page
18 0, // no flags
19 b, // source widechar string
20 -1, // assume NUL-terminated
21 p, // target buffer
22 SysStringLen(b)+1, // target buffer length
23 NULL, // use system default char
24 NULL); // don''t care if default used
25 s.ReleaseBuffer();
26 #endif
27 return s;
28 }
我并不担心如果 BSTR 包含没有映射到 8 位字符集的 Unicode 字符时会
发生什么,因为我指定了::WideCharToMultiByte 的最后两个参数为 NULL。这就
是你可能需要改变的地方。
7. VARIANT 型转化成 CString 型
事实上,我从来没有这么做过,因为我没有用 COM/OLE/ActiveX 编写过程
序。但是我在 microsoft.public.vc.mfc 新闻组上看到了 Robert Quirk 的一篇帖子
谈到了这种转化,我觉得把他的文章包含在我的文章里是不太好的做法,所以在
这里多做一些解释和演示。如果和他的文章有相孛的地方可能是我的疏忽。
VARIANT 类型经常用来给 COM 对象传递参数,或者接收从 COM 对象返
回的值。你也能自己编写返回 VARIANT 类型的方法,函数返回什么类型 依赖可
能(并且常常)方法的输入参数(比如,在自动化操作中,依赖与你调用哪个方
法。IDispatch::Invoke 可能返回(通过其一个参数)一个 包含有 BYTE、WORD、
float、double、date、BSTR 等等 VARIANT 类型的结果,(详见 MSDN 上的
VARIANT 结构的定义)。在下面的例子中,假设 类型是一个 BSTR的变体,也就
是说在串中的值是通过 bsrtVal 来引用,其优点是在 ANSI 应用中,有一个构造
函数会把 LPCWCHAR 引用的值转换为一个 CString(见 BSTR-to-CString 部分)。
在 Unicode 模式中,将成为标准的 CString 构造函数,参见对缺
省::WideCharToMultiByte 转换的告诫,以及你觉得是否可以接受(大多数情况下,
你会满意的)。
1 VARIANT vaData;
2 vaData = m_com.YourMethodHere();
3 ASSERT(vaData.vt == VT_BSTR);
4 CString strData(vaData.bstrVal);
你还可以根据 vt 域的不同来建立更通用的转换例程。为此你可能会考虑:
1 CString VariantToString(VARIANT * va)
2 {
3 CString s;
4 switch(va->vt)
5 { /* vt */
6 case VT_BSTR:
7 return CString(vaData->bstrVal);
8 case VT_BSTR | VT_BYREF:
9 return CString(*vaData->pbstrVal);
10 case VT_I4:
11 s.Format(_T("%d"), va->lVal);
12 return s;
13 case VT_I4 | VT_BYREF:
14 s.Format(_T("%d"), *va->plVal);
15 case VT_R8:
16 s.Format(_T("%f"), va->dblVal);
17 return s;
18 ... 剩下的类型转换由读者自己完成
19 default:
20 ASSERT(FALSE); // unknown VARIANT type (this ASSERT is optional)
21 return CString("");
22 } /* vt */
23 }
8. 载入字符串表资源
如果你想创建一个容易进行语言版本移植的应用程序,你就不能在你的源代
码中直接包含本土语言字符串 (下面这些例子我用的语言都是英语,因为我的
本土语是英语),比如下面这种写法就很糟:
1 CString s = "There is an error";
你应该把你所有特定语言的字符串单独摆放(调试信息、在发布版本中不出
现的信息除外)。这意味着向下面这样写比较好:
1 s.Format(_T("%d - %s"), code, text);
在你的程序中,文字字符串不是语言敏感的。不管怎样,你必须很小心,不
要使用下面这样的串:
1 // fmt is "Error in %s file %s"
2 // readorwrite is "reading" or "writing"
3 s.Format(fmt, readorwrite, filename);
这是我的切身体会。在我的第一个国际化的应用程序中我犯了这个错误,尽
管我懂德语,知道在德语的语法中动词放在句子的最后面,我们的德国方面的发
行人还是苦苦的抱怨他们不得不提取那些不可思议的德语错误提示信息然后重
新格式化以让它们能正常工作。比较好的办法(也是我现在使用的办法)是使用
两个字符串,一个用 于读,一个用于写,在使用时加载合适的版本,使得它们
对字符串参数是非敏感的。也就是说加载整个格式,而不是加载串“reading”,
“writing”:
1 // fmt is "Error in reading file %s"
2 // "Error in writing file %s"
3 s.Format(fmt, filename);
一定要注意,如果你有好几个地方需要替换,你一定要保证替换后句子的结
构不会出现问题,比如在英语中,可以是主语-宾语,主语-谓语,动词-宾语的结
构等等。
在这里,我们并不讨论 FormatMessage,其实它比 sprintf/Format 还要有优
势,但是不太容易和 CString 结合使用。解决这种问题的办法就是我们按照参数
出现在参数表中的位置给参数取名字,这样在你输出的时候就不会把他们的位置
排错了。
接下来我们讨论我们这些独立的字符串放在什么地方。我们可以把字符串的
值放入资源文件中的一个称为 STRINGTABLE 的段中。过程如下:首先使用 Visual
Studio 的资源编辑器创建一个字符串,然后给每一个字符串取一个 ID,一般我
们给它取名字都以 IDS_开头。所以如果你有一个信息,你可以创建一个字符串
资源然后取名为 IDS_READING_FILE,另外一个就取名为 IDS_WRITING_FILE。它
们以下面的形式出现在你的 .rc 文件中:
1 STRINGTABLE
2 IDS_READING_FILE "Reading file %s"
3 IDS_WRITING_FILE "Writing file %s"
4 END
注意:这些资源都以 Unicode 的格式保存,不管你是在什么环境下编译。
他们在 Win9x 系统上也是以 Unicode 的形式存在,虽然 Win9x 不能真正处理
Unicode。
然后你可以这样使用这些资源:
// 在使用资源串表之前,程序是这样写的:
1 CString fmt;
2 if(...)
3 fmt = "Reading file %s";
4 else
5 fmt = "Writing file %s";
6 ...
7 // much later
8 CString s;
9 s.Format(fmt, filename);
// 使用资源串表之后,程序这样写:
1 CString fmt;
2 if(...)
3 fmt.LoadString(IDS_READING_FILE);
4 else
5 fmt.LoadString(DS_WRITING_FILE);
6 ...
7 // much later
8 CString s;
9 s.Format(fmt, filename);
现在,你的代码可以移植到任何语言中去。LoadString 方法需要一个字符串
资源的 ID 作为参数,然后它从 STRINGTABLE 中取出它对应的字符串,赋值给
CString 对象。 CString 对象的构造函数还有一个更加聪明的特征可以简化
STRINGTABLE 的使用。这个用法在 CString::CString 的文档中没有指出,但是在
构造函数的示例程序中使用了。(为什么这个特性没有成为正式文档的一部分,
而是放在了一个例子中,我记不得了!)――【译者注:从这句话看,作者可能
是 CString的设计者。其实前面还有一句类似的话。说他没有对使用 GetBuffer(0)
获得的指针指向的地址是否可读做有效性检查 】。这个特征就是:如果你将一个
字符串资源的 ID 强制类型转换为 LPCTSTR,将会隐含调用 LoadString。因此,
下面两个构造字符串的例子具有相同的效果,而且其 ASSERT 在 debug模式下不
会被触发:
1 CString s;
2 s.LoadString(IDS_WHATEVER);
3 CString t( (LPCTSTR)IDS_WHATEVER );
4 ASSERT(s == t);//不会被触发,说明 s和 t是相同的。
现在,你可能会想:这怎么可能工作呢?我们怎么能把 STRINGTABLE ID 转
化成一个指针呢?很简单:所有的字符串 ID 都在 1~65535这个范围内,也就是
说,它所有的高位都是 0,而我们在程序中所使用的指针是不可能小于 65535的,
因为程序的低 64K 内存永远也不可能存在的,如果你试图访问 0x00000000 到
0x0000FFFF之间的内存,将会引发一个内存越界错误。所以说 1~65535的值不可
能是一个内存地址,所以我们可以用这些