在Windows中C/C++默认使用ANSI字符串,ANSI字符串默认是一种窄字符串,可以用char保存,这对于学习C/C++语言的原理比较方便。但是Windows是一个基于Unicode的操作系统,在正式程序中使用ANSI字符串,会造成程序的不稳定因素,如本文附图所示。
WinAPI原生支持基于UTF-16的wchar_t宽字符,但是C/C++运行库默认并未被配置使用宽字符,想在C/C++运行库使用宽字符需要一些技巧。本文所述技巧并不使用任何WinAPI或ATL等外部库。这里主要讲C/C++中可以改成宽字符的三个地方,控制台函数、文本文件、命令行参数。
一、启用控制台函数的Unicode宽字符I/O模式(需使用wscanf、wprintf等宽字符函数)
Windows的控制台支持两种I/O模式:当前代码页的窄字符I/O模式,以及UTF-16 LE宽字符I/O模式。
C/C++标准库默认的I/O模式是窄字符I/O模式,切换代码页会乱码。开启控制台UTF-16 LE宽字符I/O模式的方法是使用_setmode。将stdin/stdout/stderr的读写模式设为_O_U16TEXT,可以开启stdin/stdout/stderr的UTF-16 LE宽字符I/O模式,可以在不同代码页下正常工作。设为_O_TEXT可以恢复窄字符I/O模式。
一旦开启UTF-16 LE宽字符I/O模式,便无法再使用printf、scanf、cin、cout等窄字符I/O流(_getws_s和_putws等下划线函数也不支持),只能使用fgetws、fputws、wscanf、wprintf、wcin、wcout等宽字符I/O流。
<code class="lang-cpp">#include <stdio.h>
#include <io.h> // _setmode
#include <fcntl.h> // _O_U16TEXT
// C++包含文件
#include <iostream>
int main()
{
_setmode(_fileno(stdin), _O_U16TEXT); // 设置控制台为宽字符I/O模式
_setmode(_fileno(stdout), _O_U16TEXT);
_setmode(_fileno(stderr), _O_U16TEXT);
wchar_t buf2[200] = L"";
fgetws(buf2, 200, stdin);
wprintf(L"你输入了:%s", buf2); // 只能使用宽字符函数
// C++
using namespace std;
wcout << L"C++宽字符测试" << endl;
return 0;
}</iostream></fcntl.h></io.h></stdio.h></code>
_setmode其它相关的取值:
_O_U8TEXT 表示使用UTF-8编码的宽字符I/O模式,它在stdin上无法正常工作。
_O_WTEXT 表示通过检测BOM自动切换编码的宽字符I/O模式,它在stdin/stdout/stderr作用和_O_U16TEXT一样。
二、改变C标准库的默认区域映射
C标准库依赖区域映射来支持ANSI字符串和宽字符串的转换。不正确的区域设置会导致转换ANSI字符串时字符乱码或中断,因此如果程序使用了ANSI字符串,程序启动时必须要正确设置区域映射。
在C标准库中,程序启动时默认为“C”区域映射(ISO-8859-1),这个映射仅支持英语和西欧字符。要支持本地ANSI字符,需要程序启动时调用setlocale(LC_ALL, "");向setlocale传递一个空字符串,以读取系统默认的ANSI区域映射。
正确设置区域映射后,如果需要在wprintf、swprintf、fwprintf中使用ANSI字符串,可以使用L"%hs"限定符,函数会将它转换为宽字符。
<code class="lang-cpp">#include <stdio.h>
#include <io.h> // _setmode
#include <fcntl.h> // _O_U16TEXT
int main()
{
_setmode(_fileno(stdin), _O_U16TEXT); // 设置控制台为宽字符I/O模式
_setmode(_fileno(stdout), _O_U16TEXT);
_setmode(_fileno(stderr), _O_U16TEXT);
wprintf(L"setlocale之前,窄字符测试:%hs\n", "我是ANSI窄字符"); // 乱码
setlocale(LC_ALL, ""); // 读取系统区域设定
wprintf(L"setlocale之后,窄字符测试:%hs\n", "我是ANSI窄字符"); // 正常
return 0;
}</fcntl.h></io.h></stdio.h></code>
另外,正确设置区域映射后,虽然控制台已经可以输出宽字符中的中文,但是chcp切换代码页后也会乱码,遇到国际字符(如朝鲜文、越南文、泰文等)仍然会导致输出中断。这是因为这时控制台还是窄字符模式,只是C运行库的locale变为中文了而已。开启控制台Unicode宽字符输出的正确的方法是使用第一节所述的_setmode方法。
三、启用文本文件的Unicode(UTF-8/UTF-16)支持(需使用fgetws、fwprintf等宽字符函数)
读取文件时自动识别文件编码,写入时总是使用UTF-8:
<code class="lang-cpp">#include <stdio.h>
#include <locale.h> // setlocale
#include <io.h> // _setmode
#include <fcntl.h> // _O_U16TEXT
int main()
{
_setmode(_fileno(stdin), _O_U16TEXT); // 设置控制台为宽字符I/O模式
_setmode(_fileno(stdout), _O_U16TEXT);
_setmode(_fileno(stderr), _O_U16TEXT);
setlocale(LC_ALL, ""); // 读取系统区域设定,方便读取ANSI文档
wchar_t buf[200] = L"";
FILE *f = _wfopen(L"abc.txt", L"w+, ccs=UTF-8"); // 始终使用UTF-8保存(也可以使用UTF-16)
fwprintf(f, L"我是Unicode测试\n"); // 注意:使用宽字符函数
fclose(f);
f = _wfopen(L"abc.txt", L"r+, ccs=UNICODE"); // 通过BOM自动识别UTF-16、UTF-8、ANSI文档
fgetws(buf, 200, f);
fclose(f);
fputws(buf, stdout); // 显示读取的字符
return 0;
}</fcntl.h></io.h></locale.h></stdio.h></code>
fopen和_wfopen的第二个参数可以附加一个可选项", ccs=<编码>",编码可以选择以下几种:
ccs=UNICODE 通过检测BOM自动识别UTF-16、UTF-8、ANSI文本(对应_setmode的_O_WTEXT)
ccs=UTF-16 始终按UTF-16打开和写入(对应_setmode的_O_U16TEXT)
ccs=UTF-8 始终按UTF-8打开和写入(对应_setmode的_O_U8TEXT)
为了支持文件名中的国际字符,可以使用_wfopen函数。
C++的wfstream封装紧密,未找到原生支持上述特性的解决方案。
四、命令行参数使用宽字符
这个最简单,改用wmain作为入口点即可。代码如下:
<code class="lang-cpp">int wmain(int argc, wchar_t *argv[])
{
return 0;
}</code>
Visual C++还支持第三个参数(环境变量),是可选的:
<code class="lang-cpp">int wmain(int argc, wchar_t *argv[], wchar_t *envp[])
{
return 0;
}</code>
五、使用ANSI字符串的安全性规则
由于ANSI字符串不能表示国际字符,因此在程序中最好不要使用ANSI字符串。必须使用ANSI字符串的话,要遵循这几个规则:
1. 允许将ANSI字符串转换为宽字符串(如使用L"%hs"限定符),允许读取ANSI文本文件。
2. 尽量避免将宽字符串转换为ANSI字符串(如使用"%ls"限定符),避免写入ANSI文本文件,以防信息丢失。
3. 尽量将ANSI字符串转换为宽字符串再处理,因为ANSI字符串变长且尾字节有冲突,不好处理。
将宽字符转换为ANSI字符串的正确方法是使用WideCharToMultiByte或使用ATL/MFC强制转换为(CStringA)或(CW2A),处理ANSI字符串的正确方法是使用_mbsstr或CStringA的成员函数,这超出了本文的范畴,不再讨论,有兴趣的话可以看我发的相关帖子或MSDN Library的相关章节。
附图:Windows的CHM查看器hh.exe使用ANSI字符串,无法处理国际字符。
200字以内,仅用于支线交流,主线讨论请采用回复功能。