CString v(\// 货币金额,两位小数 LPCTSTR p = v; p[lstrlen(p) - 3] = ,;
这时编译器会报错,因为你赋值了一个常量串。如果你做如下尝试,编译器也会错:
strcat(p, \
因为 strcat 的第一个参数应该是 LPTSTR 类型的数据,而你却给了一个 LPCTSTR。
不要试图钻这个错误消息的牛角尖,这只会使你自己陷入麻烦!
原因是缓冲有一个计数,它是不可存取的(它位于 CString 地址之下的一个隐藏区域),如果你改变这个串,缓冲中的字符计数不会反映所做的修改。此外,如果字符串长度恰好是该字符串物理限制的长度(梢后还会讲到这个问题),那么扩展该字符串将改写缓冲以外的任何数据,那是你无权进行写操作的内存(不对吗?),你会毁换坏不属于你的内存。这是应用程序真正的死亡处方。
CString转化成char* 之二:使用 CString 对象的 GetBuffer 方法;
如果你需要修改 CString 中的内容,它有一个特殊的方法可以使用,那就是 GetBuffer,它的作用是返回一个可写的缓冲指针。 如果你只是打算修改字符或者截短字符串,你完全可以这样做: CString s(_T(\ LPTSTR p = s.GetBuffer();
LPTSTR dot = strchr(p, .); // OK, should have used s.Find... if(p != NULL) *p = _T(\\0); s.ReleaseBuffer();
这是 GetBuffer 的第一种用法,也是最简单的一种,不用给它传递参数,它使用默认值 0,意思是:\给我这个字符串的指针,我保证不加长它\。当你调用 ReleaseBuffer 时,字符串的实际长度会被重新计算,然后存入 CString 对象中。 必须强调一点,在 GetBuffer 和 ReleaseBuffer 之间这个范围,一定不能使用你要操作的这个缓冲的 CString 对象的任何方法。因为 ReleaseBuffer 被调用之前,该 CString 对象的完整性得不到保障。研究以下代码: CString s(...);
LPTSTR p = s.GetBuffer(); //... 这个指针 p 发生了很多事情
int n = s.GetLength(); // 很糟D!!!!! 有可能给出错误的答案!!! s.TrimRight(); // 很糟!!!!! 不能保证能正常工作!!!! s.ReleaseBuffer(); // 现在应该 OK
int m = s.GetLength(); // 这个结果可以保证是正确的。 s.TrimRight(); // 将正常工作。
假设你想增加字符串的长度,你首先要知道这个字符串可能会有多长,好比是声明字符串数组的时候用: char buffer[1024];
表示 1024 个字符空间足以让你做任何想做得事情。在 CString 中与之意义相等的表示法:
LPTSTR p = s.GetBuffer(1024);
调用这个函数后,你不仅获得了字符串缓冲区的指针,而且同时还获得了长度至少为 1024 个字符的空间(注意,我说的是\字符\,而不是\字节\,因为 CString 是以隐含方式感知 Unicode 的)。
同时,还应该注意的是,如果你有一个常量串指针,这个串本身的值被存储在只读内存中,如果试图存储它,即使你已经调用了 GetBuffer ,并获得一个只读内存的指针,存入操作会失败,并报告存取错误。我没有在 CString 上证明这一点,但我看到过大把的 C 程序员经常犯这个错误。
C 程序员有一个通病是分配一个固定长度的缓冲,对它进行 sprintf 操作,然后将它赋值给一个 CString: char buffer[256];
sprintf(buffer, \args, ...); // ... 部分省略许多细节 CString s = buffer;
虽然更好的形式可以这么做: CString s;
s.Format(_T(\args, ...);
如果你的字符串长度万一超过 256 个字符的时候,不会破坏堆栈。
另外一个常见的错误是:既然固定大小的内存不工作,那么就采用动态分配字节,这种做法弊端更大:
int len = lstrlen(parm1) + 13 lstrlen(parm2) + 10 + 100; char * buffer = new char[len];
sprintf(buffer, \is equal to %s, valid data\parm1, parm2); CString s = buffer; ......
delete [] buffer;
它可以能被简单地写成: CString s;
s.Format(_T(\is equal to %s, valid data\parm1, parm2);
需要注意 sprintf 例子都不是 Unicode 就绪的,尽管你可以使用 tsprintf 以及用 _T() 来包围格式化字符串,但是基本 思路仍然是在走弯路,这这样很容易出错。 CString to char * 之三:和控件的接口;
我们经常需要把一个 CString 的值传递给一个控件,比如,CTreeCtrl。MFC为我们提供了很多便利来重载这个操作,但是 在大多数情况下,你使用\原始\形式的
更新,因此需要将墨某个串指针存储到 TVINSERTITEMSTRUCT 结构的 TVITEM 成员中。如下:
TVINSERTITEMSTRUCT tvi; CString s;
// ... 为s赋一些值。
tvi.item.pszText = s; // Compiler yells at you here // ... 填写tvi的其他域
HTREEITEM ti = c_MyTree.InsertItem(&tvi);
为什么编译器会报错呢?明明看起来很完美的用法啊!但是事实上如果你看看 TVITEM 结构的定义你就会明白,在 TVITEM 结构中 pszText 成员的声明如下: LPTSTR pszText; int cchTextMax;
因此,赋值不是赋给一个 LPCTSTR 类型的变量,而且编译器无法知道如何将赋值语句右边强制转换成 LPCTSTR。好吧,你说,那我就改成这样: tvi.item.pszText = (LPCTSTR)s; //编译器依然会报错。
编译器之所以依然报错是因为你试图把一个 LPCTSTR 类型的变量赋值给一个 LPTSTR 类型的变量,这种操作在C或C++中是被禁止的。你不能用这种方法 来滥用常量指针与非常量指针概念,否则,会扰乱编译器的优化机制,使之不知如何优化你的程序。比如,如果你这么做: const int i = ...; //... do lots of stuff ... = a; // usage 1 // ... lots more stuff ... = a; // usage 2
那么,编译器会以为既然 i 是 const ,所以 usage1和usage2的值是相同的,并且它甚至能事先计算好 usage1 处的 a 的地址,然后保留着在后面的 usage2 处使用,而不是重新计算。如果你按如下方式写的话: const int i = ...; int * p = &i; //... do lots of stuff ... = a; // usage 1 // ... lots more stuff
(*p)++; // mess over compilers assumption // ... and other stuff ... = a; // usage 2
编译器将认为 i 是常量,从而 a 的位置也是常量,这样间接地破坏了先前的假设。因此,你的程序将会在 debug 编译模式(没有优化)和 release 编译模式(完全优化)中反映出不同的行为,这种情况可不好,所以当你试图把指向 i 的指针赋
值给一个 可修改的引用时,会被编译器诊断为这是一种伪造。这就是为什么(LPCTSTR)强制类型转化不起作用的原因。
为什么不把该成员声明成 LPCTSTR 类型呢?因为这个结构被用于读写控件。当你向控件写数据时,文本指针实际上被当成 LPCTSTR,而当你从控件读数据 时,你必须有一个可写的字符串。这个结构无法区分它是用来读还是用来写。 因此,你会常常在我的代码中看到如下的用法: tvi.item.pszText = (LPTSTR)(LPCTSTR)s;
它把 CString 强制类型转化成 LPCTSTR,也就是说先获得改字符串的地址,然后再强制类型转化成 LPTSTR,以便可以对之进行赋值操作。 注意这只有在使用 Set 或 Insert 之类的方法才有效!如果你试图获取数据,则不能这么做。 如果你打算获取存储在控件中的数据,则方法稍有不同,例如,对某个 CTreeCtrl 使用 GetItem 方法,我想获取项目的文本。我知道这些 文本的长度不会超过 MY_LIMIT,因此我可以这样写: TVITEM tvi;
// ... assorted initialization of other fields of tvi tvi.pszText = s.GetBuffer(MY_LIMIT); tvi.cchTextMax = MY_LIMIT; c_MyTree.GetItem(&tvi); s.ReleaseBuffer();
可以看出来,其实上面的代码对所有类型的 Set 方法都适用,但是并不需要这么做,因为所有的类 Set 方法(包括 Insert方法)不会改变字符串的内容。但是当你需要写 CString 对象时,必须保证缓冲是可写的,这正是 GetBuffer 所做的事情。再次强调: 一旦做了一次 GetBuffer 调用,那么在调用 ReleaseBuffer 之前不要对这个 CString 对象做任何操作。 [编辑本段]
5、CString 型转化成 BSTR 型
当我们使用 ActiveX 控件编程时,经常需要用到将某个值表示成 BSTR 类型。BSTR 是一种记数字符串,Intel平台上的宽字符串(Unicode),并且 可以包含嵌入的 NULL 字符。
你可以调用 CString 对象的 AllocSysString 方法将 CString 转化成 BSTR: CString s;
s = ... ; // whatever
BSTR b = s.AllocSysString();
现在指针 b 指向的就是一个新分配的 BSTR 对象,该对象是 CString 的一个拷贝,包含终结 NULL字符。现在你可以将它传递给任何需要 BSTR 的接口。通常,
BSTR 由接收它的组件来释放,如果你需要自己释放 BSTR 的话,可以这么做: ::SysFreeString(b);
对于如何表示传递给 ActiveX 控件的字符串,在微软内部曾一度争论不休,最后 Visual Basic 的人占了上风,BSTR(\String\的首字母缩写)就是这场争论的结果。 [编辑本段]
6、BSTR 型转化成 CString 型
由于 BSTR 是记数 Unicode 字符串,你可以用标准转换方法来创建 8 位的 CString。实际上,这是 CString 内建的功能。在 CString 中 有特殊的构造函数可以把 ANSI 转化成 Unicode,也可以把Unicode 转化成 ANSI。你同样可以从 VARIANT 类型的变量中获得 BSTR 类型的字符串,VARIANT 类型是 由各种 COM 和 Automation (自动化)调用返回的类型。 例如,在一个ANSI程序中: BSTR b;
b = ...; // whatever
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 模式下,它的构造函数是: CString::CString(LPCTSTR);
正如上面所表示的,在 ANSI 模式下,它有一个特殊的构造函数: CString::CString(LPCWSTR);
它会调用一个内部的函数将 Unicode 字符串转换成 ANSI 字符串。(在Unicode模式下,有一个专门的构造函数,该函数有一个参数是LPCSTR类型——一个8位 ANSI 字符串 指针,该函数将它加宽为 Unicode 的字符串!)再次强调:一定要检查 BSTR 的值是否为 NULL。
另外还有一个问题,正如上文提到的:BSTRs可以含有多个内嵌的NULL字符,但是 CString 的构造函数只能处理某个串中单个 NULL 字符。 也就是说,如果串