CString操纵指南
副标题#e#
通过阅读本文你可以进修如何有效地利用CString。
CString 是一种很有用的数据范例。它们很洪流平上简化了MFC中的很多操纵,使得MFC在做字符串操纵的时候利便了许多。不管奈何,利用CString有许多非凡的能力,出格是对付纯C配景下走出来的措施员来说有点难以进修。这篇文章就来接头这些能力。
利用CString可以让你对字符串的操纵越发直截了当。这篇文章不是CString的完全手册,但席卷了大部门常见根基问题。
下面我别离接头。
1、CString 工具的毗连
能浮现出 CString 范例利便性特点的一个方面就字符串的毗连,利用 CString 范例,你能很利便地毗连两个字符串,正如下面的例子:
CString gray("Gray");
CString cat("Cat");
CString graycat = gray + cat;
要比用下面的要领好得多:
char gray[] = "Gray";
char cat[] = "Cat";
char * graycat = malloc(strlen(gray) + strlen(cat) + 1);
strcpy(graycat, gray);
strcat(graycat, cat);
2、名目化字符串
与其用 sprintf() 函数或 wsprintf() 函数来名目化一个字符串,还不如用 CString 工具的Format()要领:
CString s;
s.Format(_T("The total is %d"), total);
用这种要领的长处是你不消担忧用来存放名目化后数据的缓冲区是否足够大,这些事情由CString类替你完成。
名目化是一种把其它不是字符串范例的数据转化为CString范例的最常用能力,好比,把一个整数转化成CString范例,可用如下要领:
CString s;
s.Format(_T("%d"), total);
我老是对我的字符串利用_T()宏,这是为了让我的代码至少有Unicode的意识,虽然,关于Unicode的话题不在这篇文章的接头范畴。_T()宏在8位字符情况下是如下界说的:
#define _T(x) x // 非Unicode版本(non-Unicode version)
而在Unicode情况下是如下界说的:
#define _T(x) L##x // Unicode版本(Unicode version)
所以在Unicode情况下,它的结果就相当于:
s.Format(L"%d", total);
假如你认为你的措施大概在Unicode的情况下运行,那么开始在意用 Unicode 编码。好比说,不要用 sizeof() 操纵符来得到字符串的长度,因为在Unicode情况下就会有2倍的误差。我们可以用一些要领来埋没Unicode的一些细节,好比在我需要得到字符长度的时候,我会用一个叫做DIM的宏,这个宏是在我的dim.h文件中界说的,我会在我写的所有措施中都包括这个文件:
#define DIM(x) ( sizeof((x)) / sizeof((x)[0]) )
这个宏不只可以用来办理Unicode的字符串长度的问题,也可以用在编译时界说的表格上,它可以得到表格的项数,如下:
class Whatever { ... };
Whatever data[] = {
{ ... },
...
{ ... },
};
for(int i = 0; i < DIM(data); i++) // 扫描表格寻找匹配项。
这里要提醒你的就是必然要留意那些在参数中需要真实字节数的API函数挪用,假如你通报字符个数给它,它将不能正常事情。如下:
TCHAR data[20];
lstrcpyn(data, longstring, sizeof(data) - 1); // WRONG!
lstrcpyn(data, longstring, DIM(data) - 1); // RIGHT
WriteFile(f, data, DIM(data), &bytesWritten, NULL); // WRONG!
WriteFile(f, data, sizeof(data), &bytesWritten, NULL); // RIGHT
造成以上原因是因为lstrcpyn需要一个字符个数作为参数,可是WriteFile却需要字节数作为参数。
同样需要留意的是有时候需要写出数据的所有内容。假如你仅仅只想写出数据的真实长度,你大概会认为你应该这样做:
WriteFile(f, data, lstrlen(data), &bytesWritten, NULL); // WRONG
可是在Unicode情况下,它不会正常事情。正确的做法应该是这样:
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位的字符哦。
#p#副标题#e#
3、CString 型转化成 int 型
把 CString 范例的数据转化成整数范例最简朴的要领就是利用尺度的字符串到整数转换例程。
#p#分页标题#e#
固然凡是你猜疑利用_atoi()函数是一个好的选择,它也很少会是一个正确的选择。假如你筹备利用 Unicode 字符,你应该用_ttoi(),它在 ANSI 编码系统中被编译成_atoi(),而在 Unicode 编码系统中编译成_wtoi()。你也可以思量利用_tcstoul()可能_tcstol(),它们都能把字符串转化成任意进制的长整数(如二进制、八进制、十进制或十六进制),差异点在于前者转化后的数据是无标记的(unsigned),尔后者相反。看下面的例子:
CString hex = _T("FAB");
CString decimal = _T("4011");
ASSERT(_tcstoul(hex, 0, 16) == _ttoi(decimal));
4、CString 型和 char* 范例的彼此转化
这是初学者利用 CString 时最常见的问题。有了 C++ 的辅佐,许多问题你不需要深入的去思量它,直接拿来用就行了,可是假如你不能深入相识它的运行机制,又会有许多问题让你疑惑,出格是有些看起来没有问题的代码,却偏偏不能正常事情。
好比,你会奇怪为什么不能写向下面这样的代码呢:
CString graycat = "Gray" + "Cat";
可能这样:
CString graycat("Gray" + "Cat");
事实上,编译器将诉苦上面的这些实验。为什么呢?因为针对CString 和 LPCTSTR数据范例的各类百般的组合,“ +” 运算符 被界说成一个重载操纵符。而不是两个 LPCTSTR 数据范例,它是底层数据范例。你不能对根基数据(如 int、char 可能 char*)范例重载 C++ 的运算符。你可以象下面这样做:
CString graycat = CString("Gray") + CString("Cat");
可能这样:
CString graycat = CString("Gray") + "Cat";
研究一番就会发明:“ +”老是利用在至少有一个 CString 工具和一个 LPCSTR 的场所。
留意,编写有 Unicode 意识的代码老是一件功德,好比:
CString graycat = CString(_T("Gray")) + _T("Cat");
这将使得你的代码可以直接移植。
char* 转化为 CString
此刻你有一个 char* 范例的数据,可能说一个字符串。怎么样建设 CString 工具呢?这里有一些例子:
char * p = "This is a test";
可能象下面这样更具有 Unicode 意识:
TCHAR * p = _T("This is a test")
或
LPTSTR p = _T("This is a test");
你可以利用下面任意一种写法:
CString s = "This is a test"; // 8-bit only
CString s = _T("This is a test"); // Unicode-aware
CString s("This is a test"); // 8-bit only
CString s(_T("This is a test")); // Unicode-aware
CString s = p;
CString s(p);
用这些要领可以轻松将常量字符串或指针转换成 CString。需要留意的是,字符的赋值老是被拷贝到 CString 工具中去的,所以你可以象下面这样操纵:
TCHAR * p = _T("Gray");
CString s(p);
p = _T("Cat");
s += p;
功效字符串必定是“GrayCat”。
CString 类尚有几个其它的结构函数,可是这里我们不思量它,假如你有乐趣可以本身查察相关文档。
事实上,CString 类的结构函数比我展示的要巨大,好比:
CString s = "This is a test";
这是很纰漏的编码,可是实际上它在 Unicode 情况下能编译通过。它在运行时挪用结构函数的 MultiByteToWideChar 操纵将 8 位字符串转换成 16 位字符串。不管奈何,假如 char * 指针是网络上传输的 8 位数据,这种转换是很有用的。
CString 转化成 char* 之一:强制范例转换为 LPCTSTR;
这是一种略微硬性的转换,有关“正确”的做法,人们在认识上还存在很多杂乱,正确的利用要领有许多,但错误的利用要领大概与正确的利用要领一样多。
我们首先要相识 CString 是一种很非凡的 C++ 工具,它内里包括了三个值:一个指向某个数据缓冲区的指针、一个是该缓冲中有效的字符记数以及一个缓冲区长度。 有效字符数的巨细可以是从0到该缓冲最大长度值减1之间的任何数(因为字符串末了有一个NULL字符)。字符记数缓和冲区长度被巧妙埋没。
#p#分页标题#e#
除非你做一些非凡的操纵,不然你不行能知道给CString工具分派的缓冲区的长度。这样,纵然你得到了该0缓冲的地点,你也无法变动个中的内容,不能截短字符串,也 绝对没有步伐加长它的内容,不然第一时间就会看到溢出。
LPCTSTR 操纵符(可能更明晰地说就是 TCHAR * 操纵符)在 CString 类中被重载了,该操纵符的界说是返回缓冲区的地点,因此,假如你需要一个指向 CString 的 字符串指针的话,可以这样做:
CString s("GrayCat");
LPCTSTR p = s;
它可以正确地运行。这是由C语言的强制范例转化法则实现的。当需要强制范例转化时,C++规测容许这种选择。好比,你可以将(浮点数)界说为将某个复数 (有一对浮点数)举办强制范例转换后只返回该复数的第一个浮点数(也就是其实部)。可以象下面这样:
Complex c(1.2f, 4.8f);
float realpart = c;
假如(float)操纵符界说正确的话,那么实部的的值应该是1.2。
这种强制转化适合所有这种环境,譬喻,任何带有 LPCTSTR 范例参数的函数城市强制执行这种转换。 于是,你大概有这样一个函数(也许在某个你买来的DLL中):
BOOL DoSomethingCool(LPCTSTR s);
你象下面这样挪用它:
CString file("c:\\myfiles\\coolstuff")
BOOL result = DoSomethingCool(file);
它能正确运行。因为 DoSomethingCool 函数已经说明白需要一个 LPCTSTR 范例的参数,因此 LPCTSTR 被应用于该参数,在 MFC 中就是返回的串地点。
假如你要名目化字符串怎么办呢?
CString graycat("GrayCat");
CString s;
s.Format("Mew! I love %s", graycat);
留意由于在可变参数列表中的值(在函数说明中是以“…”暗示的)并没有隐含一个强制范例转换操纵符。你会获得什么功效呢?
一个令人惊奇的功效,我们获得的实际功效串是:
"Mew! I love GrayCat"。
因为 MFC 的设计者们在设计 CString 数据范例时很是小心, CString 范例表达式求值后指向了字符串,所以这里看不到任何象 Format 或 sprintf 中的强制范例转换,你仍然可以获得正确的行为。描写 CString 的附加数据实际上在 CString 名义地点之后。
有一件工作你是不能做的,那就是修改字符串。好比,你大概会实验用“,”取代“.”(不要做这样的,假如你在乎国际化问题,你应该利用十进制转换的 National Language Support 特性,),下面是个简朴的例子:
CString v("1.00"); // 钱币金额,两位小数
LPCTSTR p = v;
p[lstrlen(p) - 3] = '','';
这时编译器会报错,因为你赋值了一个常量串。假如你做如下实验,编译器也会错:
strcat(p, "each");
因为 strcat 的第一个参数应该是 LPTSTR 范例的数据,而你却给了一个 LPCTSTR。
不要试图钻这个错误动静的牛角尖,这只会使你本身陷入贫苦!
原因是缓冲有一个计数,它是不行存取的(它位于 CString 地点之下的一个埋没区域),假如你改变这个串,缓冲中的字符计数不会反应所做的修改。另外,假如字符串长度刚好是该字符串物理限制的长度(梢后还会讲到这个问题),那么扩展该字符串将改写缓冲以外的任何数据,那是你无权举办写操纵的内存(差池吗?),你会毁换坏不属于你的内存。这是应用措施真正的灭亡处方。
CString转化成char* 之二:利用 CString 工具的 GetBuffer 要领;
假如你需要修改 CString 中的内容,它有一个非凡的要领可以利用,那就是 GetBuffer,它的浸染是返回一个可写的缓冲指针。 假如你只是规划修改字符可能截短字符串,你完全可以这样做:
CString s(_T("File.ext"));
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);
#p#分页标题#e#
挪用这个函数后,你不只得到了字符串缓冲区的指针,并且同时还得到了长度至少为 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, "%s is equal to %s, valid data", parm1, parm2);
CString s = buffer;
......
delete [] buffer;
它可以能被简朴地写成:
CString s;
s.Format(_T("%s 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[i]; // usage 1
// ... lots more stuff
... = a[i]; // usage 2
那么,编译器会觉得既然 i 是 const ,所以 usage1和usage2的值是沟通的,而且它甚至能事先计较好 usage1 处的 a[i] 的地点,然后保存着在后头的 usage2 处利用,而不是从头计较。假如你按如下方法写的话:
const int i = ...;
int * p = &i;
//... do lots of stuff
... = a[i]; // usage 1
// ... lots more stuff
(*p)++; // mess over compiler''s assumption
// ... and other stuff
... = a[i]; // usage 2
编译器将认为 i 是常量,从而 a[i] 的位置也是常量,这样间接地粉碎了先前的假设。因此,你的措施将会在 debug 编译模式(没有优化)和 release 编译模式(完全优化)中反应出差异的行为,这种环境可欠好,所以当你试图把指向 i 的指针赋值给一个 可修改的引用时,会被编译器诊断为这是一种伪造。这就是为什么(LPCTSTR)强制范例转化不起浸染的原因。
#p#分页标题#e#
为什么不把该成员声明成 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(“Basic 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 字符。 也就是说,假如串中含有嵌入的 NUL字节,CString 将管帐算堕落误的串长度。你必需本身处理惩罚它。假如你看看 strcore.cpp 中的结构函数,你会发明 它们都挪用了lstrlen,也就是计较字符串的长度。
留意从 Unicode 到 ANSI 的转换利用带专门参数的 ::WideCharToMultiByte,假如你不想利用这种默认的转换方法,则必需编写本身的转化代码。
假如你在 UNICODE 模式下编译代码,你可以简朴地写成:
#p#分页标题#e#
CString convert(BSTR b)
{
if(b == NULL)
return CString(_T(""));
CString s(b); // in UNICODE mode
return s;
}
假如是 ANSI 模式,则需要更巨大的进程来转换。留意这个代码利用与 ::WideCharToMultiByte 沟通的参数值。所以你 只能在想要改变这些参数举办转换时利用该技能。譬喻,指定差异的默认字符,差异的符号集等。
CString convert(BSTR b)
{
CString s;
if(b == NULL)
return s; // empty for NULL BSTR
#ifdef UNICODE
s = b;
#else
LPSTR p = s.GetBuffer(SysStringLen(b) + 1);
::WideCharToMultiByte(CP_ACP, // ANSI Code Page
0, // no flags
b, // source widechar string
-1, // assume NUL-terminated
p, // target buffer
SysStringLen(b)+1, // target buffer length
NULL, // use system default char
NULL); // don''t care if default used
s.ReleaseBuffer();
#endif
return s;
}
我并不担忧假如 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 转换的申饬,以及你以为是否可以接管(大大都环境下,你会满足的)。
VARIANT vaData;
vaData = m_com.YourMethodHere();
ASSERT(vaData.vt == VT_BSTR);
CString strData(vaData.bstrVal);
你还可以按照 vt 域的差异来成立更通用的转换例程。为此你大概会思量:
CString VariantToString(VARIANT * va)
{
CString s;
switch(va->vt)
{ /* vt */
case VT_BSTR:
return CString(vaData->bstrVal);
case VT_BSTR | VT_BYREF:
return CString(*vaData->pbstrVal);
case VT_I4:
s.Format(_T("%d"), va->lVal);
return s;
case VT_I4 | VT_BYREF:
s.Format(_T("%d"), *va->plVal);
case VT_R8:
s.Format(_T("%f"), va->dblVal);
return s;
... 剩下的范例转换由读者本身完成
default:
ASSERT(FALSE); // unknown VARIANT type (this ASSERT is optional)
return CString("");
} /* vt */
}
8、载入字符串表资源
假如你想建设一个容易举办语言版本移植的应用措施,你就不能在你的源代码中直接包括本土语言字符串 (下面这些例子我用的语言都是英语,因为我的本土语是英语),好比下面这种写法就很糟:
CString s = "There is an error";
你应该把你所有特定语言的字符串单独摆放(调试信息、在宣布版本中不呈现的信息除外)。这意味着向下面这样写较量好:
s.Format(_T("%d - %s"), code, text);
在你的措施中,文字字符串不是语言敏感的。不管奈何,你必需很小心,不要利用下面这样的串:
// fmt is "Error in %s file %s"
// readorwrite is "reading" or "writing"
s.Format(fmt, readorwrite, filename);
#p#分页标题#e#
这是我的切身体会。在我的第一个国际化的应用措施中我犯了这个错误,尽量我懂德语,知道在德语的语法中动词放在句子的最后头,我们的德国方面的刊行人照旧苦苦的诉苦他们不得不提取那些不行思议的德语错误提示信息然后从头名目化以让它们能正常事情。较量好的步伐(也是我此刻利用的步伐)是利用两个字符串,一个用 于读,一个用于写,在利用时加载符合的版本,使得它们对字符串参数长短敏感的。也就是说加载整个名目,而不是加载串“reading”,“writing”:
// fmt is "Error in reading file %s"
// "Error in writing file %s"
s.Format(fmt, filename);
必然要留意,假如你有好几个处所需要替换,你必然要担保替换后句子的布局不会呈现问题,好比在英语中,可以是主语-宾语,主语-谓语,动词-宾语的布局等等。
在这里,我们并不接头 FormatMessage,其实它比 sprintf/Format 还要有优势,可是不太容易和CString 团结利用。办理这种问题的步伐就是我们凭据参数呈此刻参数表中的位置给参数取名字,这样在你输出的时候就不会把他们的位置排错了。
接下来我们接头我们这些独立的字符串放在什么处所。我们可以把字符串的值放入资源文件中的一个称为 STRINGTABLE 的段中。进程如下:首先利用 Visual Studio 的资源编辑器建设一个字符串,然后给每一个字符串取一个ID,一般我们给它取名字都以 IDS_开头。所以假如你有一个信息,你可以建设一个字符串资源然后取名为 IDS_READING_FILE,别的一个就取名为 IDS_WRITING_FILE。它们以下面的形式呈此刻你的 .rc 文件中:
STRINGTABLE
IDS_READING_FILE "Reading file %s"
IDS_WRITING_FILE "Writing file %s"
END
留意:这些资源都以 Unicode 的名目生存,不管你是在什么情况下编译。他们在Win9x系统上也是以Unicode 的形式存在,固然 Win9x 不能真正处理惩罚 Unicode。
然后你可以这样利用这些资源:
// 在利用资源串表之前,措施是这样写的:
CString fmt;
if(...)
fmt = "Reading file %s";
else
fmt = "Writing file %s";
...
// much later
CString s;
s.Format(fmt, filename);
// 利用资源串表之后,措施这样写:
CString fmt;
if(...)
fmt.LoadString(IDS_READING_FILE);
else
fmt.LoadString(DS_WRITING_FILE);
...
// much later
CString s;
s.Format(fmt, filename);
此刻,你的代码可以移植到任何语言中去。LoadString 要领需要一个字符串资源的 ID 作为参数,然后它从 STRINGTABLE 中取出它对应的字符串,赋值给 CString 工具。 CString 工具的结构函数尚有一个越发智慧的特征可以简化 STRINGTABLE 的利用。这个用法在 CString::CString 的文档中没有指出,可是在 结构函数的示例措施中利用了。(为什么这个特性没有成为正式文档的一部门,而是放在了一个例子中,我记不得了!)——【译者注:从这句话看,作者大概是CString的设计者。其实前面尚有一句雷同的话。说他没有对利用GetBuffer(0)得到的指针指向的地点是否可读做有效性查抄 】。这个特征就是:假如你将一个字符串资源的ID强制范例转换为 LPCTSTR,将会隐含挪用 LoadString。因此,下面两个结构字符串的例子具有沟通的结果,并且其 ASSERT 在debug模式下不会被触发:
CString s;
s.LoadString(IDS_WHATEVER);
CString t( (LPCTSTR)IDS_WHATEVER );
ASSERT(s == t);//不会被触发,说明s和t是沟通的。
此刻,你大概会想:这怎么大概事情呢?我们怎么能把 STRINGTABLE ID 转化成一个指针呢?很简朴:所有的字符串 ID 都在1~65535这个范畴内,也就是说,它所有的高位都是0,而我们在措施中所利用的指针是不行能小于65535的,因为措施的低 64K 内存永远也不行能存在的,假如你试图会见0x00000000到0x0000FFFF之间的内存,将会激发一个内存越界错误。所以说1~65535的值不行能是一个内存地点,所以我们可以用这些值来作为字符串资源的ID。
#p#分页标题#e#
我倾向于利用 MAKEINTRESOURCE 宏显式地做这种转换。我认为这样可以让代码越发易于阅读。这是个只适合在 MFC 中利用的尺度宏。你要记着,大大都的要领即可以接管一个 UINT 型的参数,也可以接管一个 LPCTSTR 型的参数,这是依赖 C++ 的重载成果做到的。C++重载函数带来的 漏洞就是造成所有的强制范例转化都需要显示声明。同样,你也可以给许多种布局只通报一个资源名。
CString s;
s.LoadString(IDS_WHATEVER);
CString t( MAKEINTRESOURCE(IDS_WHATEVER));
ASSERT(s == t);
汇报你吧:我不只只是在这里宣扬,事实上我也是这么做的。在我的代码中,你险些不行能找到一个字符串,虽然,那些只是偶尔在调试中呈现的可能和语言无关的字符串除外。
9、CString 和姑且工具
这是呈此刻 microsoft.public.vc.mfc 新闻组中的一个小问题,我简朴的提一下,这个问题是有个措施员需要往注册表中写入一个字符串,他写道:
我试着用 RegSetValueEx() 配置一个注册表键的值,可是它的功效老是令我狐疑。当我用char[]声明一个变量时它能正常事情,可是当我用 CString 的时候,老是获得一些垃圾:"ÝÝÝÝ…ÝÝÝÝÝÝ"为了确认是不是我的 CString 数据出了问题,我试着用 GetBuffer,然后强制转化成 char*,LPCSTR。GetBuffer 返回的值是正确的,可是当我把它赋值给 char* 时,它就酿成垃圾了。以下是我的措施段:
char* szName = GetName().GetBuffer(20);
RegSetValueEx(hKey, "Name", 0, REG_SZ,
(CONST BYTE *) szName,
strlen (szName + 1));
这个 Name 字符串的长度小于 20,所以我不认为是 GetBuffer 的参数的问题。
真让人狐疑,请帮帮我。
亲爱的 Frustrated,
你犯了一个相当微妙的错误,智慧反被智慧误,正确的代码应该象下面这样:
CString Name = GetName();
RegSetValueEx(hKey, _T("Name"), 0, REG_SZ,
(CONST BYTE *) (LPCTSTR)Name,
(Name.GetLength() + 1) * sizeof(TCHAR));
为什么我写的代码能行而你写的就有问题呢?主要是因为当你挪用 GetName 时返回的 CString 工具是一个姑且工具。拜见:《C++ Reference manual》§12.2
在一些情况中,编译器有须要建设一个姑且工具,这样引入姑且工具是依赖于实现的。假如编译器引入的这个姑且工具所属的类有结构函数的话,编译器要确保这个类的结构函数被挪用。同样的,假如这个类声明有析构函数的话,也要担保这个姑且工具的析构函数被挪用。
编译器必需担保这个姑且工具被销毁了。被销毁简直切所在依赖于实现…..这个析构函数必需在退出建设该姑且工具的范畴之前被挪用。
大部门的编译器是这样设计的:在姑且工具被建设的代码的下一个执行步调处隐含挪用这个姑且工具的析构函数,实现起来,一般都是在下一个分号处。因此,这个 CString 工具在 GetBuffer 挪用之后就被析构了(顺便提一句,你没有来由给 GetBuffer 函数通报一个参数,并且没有利用ReleaseBuffer 也是差池的)。所以 GetBuffer 原来返回的是指向这个姑且工具中字符串的地点的指针,可是当这个姑且工具被析构后,这块内存就被释放了。然后 MFC 的调试内存分派器会从头为这块内存全部填上 0xDD,显示出来恰好就是“Ý”标记。在这个时候你向注册表中写数据,字符串的内容虽然全被粉碎了。
我们不该应当即把这个姑且工具转化成 char* 范例,应该先把它生存到一个 CString 工具中,这意味着把姑且工具复制了一份,所以当姑且的 CString 工具被析构了之后,这个 CString 工具中的值依然生存着。这个时候再向注册表中写数据就没有问题了。
另外,我的代码是具有 Unicode 意识的。谁人操纵注册表的函数需要一个字节巨细,利用lstrlen(Name+1) 获得的实际功效对付 Unicode 字符来说比 ANSI 字符要小一半,并且它也不能从这个字符串的第二个字符起开始计较,也许你的本意是 lstrlen(Name) + 1(OK,我认可,我也犯了同样的错误!)。岂论如何,在 Unicode 模式下,所有的字符都是2个字节巨细,我们需要处理惩罚这个问题。微软的文档令人惊奇地对此保持沉默:REG_SZ 的值毕竟是以字节计较照旧以字符计较呢?我们假设它指的是以字节为单元计较,你需要对你的代码做一些修改来计较这个字符串所含有的字节巨细。
10、CString 的效率
CString 的一个问题是它确实掩藏了一些低效率的对象。从别的一个方面讲,它也确实可以被实现得越发高效,你大概会说下面的代码:
CString s = SomeCString1;
s += SomeCString2;
s += SomeCString3;
s += ",";
s += SomeCString4;
比起下面的代码来,效率要低多了:
char s[1024];
lstrcpy(s, SomeString1);
lstrcat(s, SomeString2);
lstrcat(s, SomeString 3);
lstrcat(s, ",");
lstrcat(s, SomeString4);
#p#分页标题#e#
总之,你大概会想,首先,它为 SomeCString1 分派一块内存,然后把 SomeCString1 复制到内里,然后发明它要做一个毗连,则从头分派一块新的足够大的内存,大到可以或许放下当前的字符串加上SomeCString2,把内容复制到这块内存 ,然后把 SomeCString2 毗连到后头,然后释放第一块内存,并把指针从头指向新内存。然后为每个字符串反复这个进程。把这 4 个字符串通接起来效率多低啊。事实上,在许多环境下基础就不需要复制源字符串(在 += 操纵符左边的字符串)。
在 VC++6.0 中,Release 模式下,所有的 CString 中的缓存都是按预界说量子分派的。所谓量子,即确定为 64、128、256 可能 512 字节。这意味着除非字符串很是长,毗连字符串的操纵实际上就是 strcat 颠末优化后的版本(因为它知道当地的字符串应该在什么处所竣事,所以不需要寻找字符串的末了;只需要把内存中的数据拷贝到指定的处所即可)加上从头计较字符串的长度。所以它的执行效率和纯 C 的代码是一样的,可是它更容易写、更容易维护和更容易领略。
假如你照旧不能确定毕竟产生了奈何的进程,请看看 CString 的源代码,strcore.cpp,在你 vc98的安装目次的 mfc\src 子目次中。看看 ConcatInPlace 要领,它被在所有的 += 操纵符中挪用。
啊哈!莫非 CString 真的这么"高效"吗?好比,假如我建设
CString cat("Mew!");
然后我并不是获得了一个高效的、精简的5个字节巨细的缓冲区(4个字符加一个竣事字符),系统将给我分派64个字节,而个中59个字节都被挥霍了。
假如你也是这么想的话,那么就请筹备好接管再教诲吧。大概在某个处所某小我私家给你讲过只管利用少的空间是件好工作。不错,这种说法简直正确,可是他忽略了事实中一个很重要的方面。
假如你编写的是运行在16K EPROMs下的嵌入式措施的话,你有来由只管少利用空间,在这种情况下,它能使你的措施更结实。可是在 500MHz, 256MB的呆板上写 Windows 措施,假如你照旧这么做,它只会比你认为的“低效”的代码运行得更糟。
举例来说。字符串的巨细被认为是影响效率的首要因素,使字符串尽大概小可以提高效率,反之则低落效率,这是各人一贯的想法。可是这种想法是差池的,准确的内存分派的效果要在措施运行了好几个小时后才气浮现得出来,当时,措施的堆中将布满小片的内存,它们太小以至于不能用来做任何事,可是他们增加了你措施的内存用量,增加了内存页面互换的次数,当页面互换的次数增加到系统可以或许忍受的上限,系统则会为你的措施分派更多的页面,直到你的措施占用了所有的可用内存。由此可见,固然内存碎片是抉择效率的次要因素,但正是这些因素实际节制了系统的行为,最终,它损害了系统的靠得住性,这是令人无法接管的。
记着,在 debug 模式下,内存往往是准确分派的,这是为了更好的排错。
假设你的应用措施凡是需要持续事情好几个月。好比,我常打开 VC++,Word,PowerPoint,Frontpage,Outlook Express,Forté Agent,Internet Explorer和其它的一些措施,并且凡是不封锁它们。我曾经夜以继日地持续用 PowerPoint 事情了好几天(反之,假如你不幸不得不利用像 Adobe FrameMaker 这样的措施的话,你将会体会到靠得住性的重要;这个措施时机天天都要瓦解4~6次,每次都是因为用完了所有的空间并填满我所有的互换页面)。所以准确内存分派是不行取的,它会危及到系统的靠得住性,并引起应用措施瓦解。
按量子的倍数为字符串分派内存,内存分派器就可以接纳用过的内存块,凡是这些接纳的内存块顿时就可以被其它的 CString 工具从头用到,这样就可以担保碎片最少。分派器的成果增强了,应用措施用到的内存就能尽大概保持最小,这样的措施就可以运行几个礼拜或几个月而不呈现问题。
#p#分页标题#e#
题外话:许多年以前,我们在 CMU 写一个交互式系统的时候,一些对内存分派器的研究显示出它往往发生许多内存碎片。Jim Mitchell,此刻他在 Sun Microsystems 事情,当时侯他缔造了一种内存分派器,它保存了一个内存分派状况的运行时统计表,这种技能和其时的主流分派器所用的技能都差异,且较为领先。当一个内存块需要被支解得比某一个值小的话,他并不支解它,因此可以制止发生太多小到什么事都干不了的内存碎片。事实上他在内存分派器中利用了一个浮动指针,他认为:与其让指令做长时间的存取内存操纵,还不如简朴的忽略那些太小的内存块而只做一些浮动指针的操纵。(His observation was that the long-term saving in instructions by not having to ignore unusable small storage chunks far and away exceeded the additional cost of doing a few floating point operations on an allocation operation.)他是对的。
永远不要认为所谓的“最优化”是成立在每一行代码都高速且节减内存的基本上的,事实上,高速且节减内存应该是在一个应用措施的整体程度上思量的。在软件的整体程度上,只利用最小内存的字符串分派计策大概是最糟糕的一种要领。
假如你认为优化是你在每一行代码上做的那些尽力的话,你应该想一想:在每一行代码中做的优化很少能真正起浸染。你可以看我的另一篇关于优化问题的文章《Your Worst Enemy for some thought-provoking ideas》。
记着,+= 运算符只是一种特例,假如你写成下面这样:
CString s = SomeCString1 + SomeCString2 + SomeCString3 + "," + SomeCString4;
则每一个 + 的应用会造成一个新的字符串被建设和一次复制操纵。
总结
以上是利用 CString 的一些能力。我天天写措施的时候城市用到这些。CString 并不是一种很难利用的类,可是 MFC 没有很明明的指出这些特征,需要你本身去摸索、去发明。