[C/C++]宽字符与控制台程序

在我刚开始学C/C++的时候,字符类型使用的都是char。接触Win32编程之后,养成了使用wchar_t的习惯,于是再写控制台程序的时候自然就使用wchar_t了。然而在控制台程序中使用宽字符会导致各种奇怪的问题,这些问题主要是在输出上。下面分享一下我在这方面的心得。

 

首先来看一下这段代码:

#include <stdio.h>

int main() {

    wprintf(L"%s", L"博客园");

    return 0;

}

 

wprintf用于输出宽字符类型的字符串,看上去似乎没有错误。但这段代码的输出却是三个问号。这是使用wprintf时最典型的问题。解决方法是加入对_wsetlocale的调用:

#include <stdio.h>

#include <locale.h>

 

int main() {

    _wsetlocale(LC_ALL, L"chs");

    wprintf(L"%s", L"博客园");

    return 0;

}

 

_wsetlocalesetlocale的宽字符版本,这两个函数的区别只在于返回值以及第二个参数使用的是否宽字符字符串,执行效果都是一样的。

 

要解释这段代码,首先要从控制台本身说起。凡是涉及到字符处理的地方都要用到字符集,而控制台是一个字符环境,因此控制台也需要使用字符集,它所使用的字符集叫做代码页,每一个代码页大致上对应一种自然语言,它定义了这种语言的字符如何与二进制代码相关联。例如,表示英语的代码页是437,表示简体中文的代码页是936。一个控制台窗口只能有一个活动代码页,所以不同语言的字符不能同时出现在一个控制台窗口中,除非这个字符是两者共有的,且有相同的二进制代码。可以通过chcp命令来改变当前控制台窗口所使用的代码页。

 

代码页实际上是一种多字节字符集,所以控制台本质上不支持Unicode。因此,如果直接向控制台输出宽字符,将不会得到正确的显示。必须先将宽字符转换成多字节字符,再进行输出。而wprintf函数在内部也的确是进行了这种转换,可以尝试一下在wprintf函数内单步执行,会看到执行过程最终到达wcstombs_s

 

问题出现在转换的过程上。转换函数必须知道将宽字符的二进制代码转换成哪种代码页字符的二进制代码,如果选择的代码页与控制台的活动代码页不相符,那么同样也不会正确显示。上面的第一段代码正是由于没有选择合适的代码页,导致输出错误。而在第二段代码中,通过将区域设置为中国,告诉转换函数将宽字符转换成936代码页的多字节字符,这与控制台的活动代码页一致,所以就可以正确输出了。

 

这里简单介绍一下_wsetlocale函数。该函数设置C运行库使用的区域文化。区域文化影响到数字、货币以及时间等数值的显示格式,当然还有代码页。第一个参数指示使用区域文化的哪个方面,取值可以是LC_COLLATELC_CTYPELC_MONETARYLC_NUMERICLC_TIME以及LC_ALL。例如,如果使用LC_NUMERIC,则C运行库输出数字的时候将使用指定区域文化的数字显示风格;如果使用LC_CTYPE,则只影响转换函数所选择的代码页。

 

第二个参数通过字符串指定区域文化。该字符串有一个固定的格式,详细情况可以参见MSDN文档。但一般情况下我们只需使用国家或地区的缩写即可,例如“chs”。如果使用空字符串“”,则表示根据当期操作系统的区域设置选择相应的代码页。所以如果操作系统选择的区域是“中文(中国)”,则也可以通过_wsetlocale(LC_ALL, “”)来设置正确的代码页。

 

C运行库默认使用一个名为“C”的区域文化,这是语言无关的,具有国际通用性,与其关联的代码页仅包含了ASCII中定义的字符。在程序启动的时候C运行库会以setlocale(LC_ALL, “C”)的方式调用setlocale,所以默认情况下wprintf不能正确输出含有中文的宽字符字符串。

 

 

C语言下对宽字符的输出处理就这样了。接下来看看C++对宽字符的输出处理。_wsetlocale只对C运行库有效,对coutwcout是没有影响的。对于coutwcout,应该使用其成员方法imbue

std::wcout.imbue(std::locale("chs", std::locale::all));

locale对象构造方法的两个参数与_wsetlocale函数参数的意义是一样的,只是位置调转了。

 

wprintf一样,wcout在输出宽字符字符串的时候,也是先将其转换成多字节字符字符串。不同的是,遇到代码页上不支持的字符的时候,wprint输出一个问号,而wcout无任何输出,同时将badbitfailbit置位,后续的输出全部都无效。个人认为wcout的处理方式欠妥,因为并不是所有场合都适合这样处理,还是wprintf的处理方式比较通用。

 

基于上面的讨论,我们在编写控制台程序时一定要非常小心地处理输入输出问题,确保程序的输出正确无误。

 (在本文第一次发表的内容中,建议编写控制台程序应该使用多字节字符集,而不要使用Unicode字符集。这是一个明显错误的建议,因此将这段内容删去了。)

 

 

 

最后对在网上看到的将char*字符串转换成wchar_t*字符串的方法发表一下看法。该方法的代码如下:

#include <iostream>

#include <sstream>

 

using namespace std;

 

int main() {

    wostringstream outStrStream;

    outStrStream << "博客园";

    wstring wstr = outStrStream.str();

    wcout << wstr << endl;

}

 

具体思路是:将char*类型的字符串输出到wostringstream对象中,再通过该对象的str方法获取转换后的字符串。这种方法作出了假设:wostringstream对象会自动将char*字符串转换成wchar_t*类型字符串。注意在这段代码中,没有调用wcout.imbu方法设置区域文化,但仍然能够正确输出中文。

 

编译、执行这段代码都没有问题,看上去似乎是正确的。但是如果试图获取转换后的字符串的长度就出问题了:

#include <iostream>

#include <sstream>

 

using namespace std;

 

int main() {

    wostringstream outStrStream;

    outStrStream << "博客园";

    wstring wstr = outStrStream.str();

    wcout << wstr.length() << endl;

}

 

这段程序将输出6,而不是3。除了长度之外,使用at方法获取到的字符也不是“博客园”中的一个。实际上,对该字符串进行操作的结果几乎都是不正确的。

 

为什么会出现这种情况呢?可以通过观察一下outStrStream对象内部的数据来寻找答案。下图是执行outStrStream << "博客园"之后的内存数据:

 

 

红色框内的便是outStrStream对象内的数据。再来看看宽字符与多字节字符的“博客园”字符串在内存中的实际数据:

#include <iostream>

#include <sstream>

 

using namespace std;

 

int main() {

    char* pStr = "博客园";

    wchar_t* pWStr = L"博客园";

}

 

 

上面的图是wchat_t*类型的,下面的图是char*类型的。通过这几幅图,可以看到outStrStream对象内的字符串仍然是多字节字符类型的字符串,只不过每个字节扩展成了两个字节。这根本不是宽字符类型的字符串,所以即使不调用wcout.imbue也能正确输出中文。

 

就写到这里吧。以上内容都是个人见解,如果存在错误疏漏请见谅。

posted on 2010-11-27 10:06  Zplutor  阅读(5968)  评论(0编辑  收藏  举报