【总结】Visual C++对于Unicode支持的原生方案(更新)
acmilan2015/09/23软件综合 IP:四川
在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字符串,无法处理国际字符。

捕获.png

[修改于 8年8个月前 - 2015/09/25 14:16:42]

来自:计算机科学 / 软件综合
16
已屏蔽 原因:{{ notice.reason }}已屏蔽
{{notice.noticeContent}}
~~空空如也
acmilan 作者
8年8个月前 修改于 8年8个月前 IP:四川
791128
在printf、scanf、fprintf、fscanf、sprintf、sscanf等窄字符函数中使用宽字符,使用"%ls"作为限定符。
在wprintf、wscanf、fwprintf、fwscanf、swprintf、swscanf等宽字符函数中使用窄字符,使用L"%hs"作为限定符。
这两种情况也需要事先运行setlocale(LC_ALL, "");

对于偶尔需要打印宽字符的情况,C语言可以在setlocale(LC_ALL, "");之后使用printf的"%ls"限定符或wprintf打印,C++可以在XXXXXXXXbue(locale(""));之后使用wcout的<<操作符打印。这种方法遇到国际字符会被截断,无法正确处理切换代码页,不建议使用在正式程序中。
<code class="lang-cpp">setlocale(LC_ALL, ""); // #include <locale.h>
printf("%ls\n", L"我是宽字符串");
      
using namespace std;
wcout.imbue(locale("")); // #include <locale>
wcout << L"我是C++宽字符串" << endl;</locale></locale.h></code>

Visual C++ 2008中C++程序也可以使用locale::global(locale(""));初始化区域设定,但是在新版如Visual C++ 2015中似乎不再起作用。
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年8个月前 修改于 8年8个月前 IP:四川
791178
如果仅仅是为了学习C/C++,完全可以不用管Unicode支持的事情。
如果是为了编写成熟的Windows程序,正确处理Unicode可以提高程序的可靠性。
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年8个月前 修改于 8年8个月前 IP:四川
791220
如果在Windows的命令提示符下输入type命令显示文本文档的话,只有两种文本文档可以正常显示,一种是当前代码页的文档,一种是UTF-16 LE(低字节优先)文档。UTF-8是无法正常显示的(除非chcp切换到65001代码页并且更改为TrueType字体)。

实际上Windows的命令提示符只支持两种IO:当前代码页的窄字符IO和UTF-16 LE宽字符IO。

_O_TEXT、_O_U16TEXT、_O_WTEXT可以正常运行,但是_O_U8TEXT控制台输入stdin乱码。这一点MSDN并没有讲。

另外,", ccs=UNICODE"(即_O_WTEXT)对于无BOM的文件是以ANSI编码打开的(参见MSDN的_open/_wopen一节),而MSDN的fopen/_wfopen一节写的却是以UTF-16 LE打开,不正确。
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年8个月前 修改于 8年8个月前 IP:四川
792038
Windows中的区域代码页用于支持老的文本编码,其中ANSI代码页主要用于Windows程序(如记事本),而OEM代码页主要用于DOS/Console程序(如命令提示符)。新支持的文字将只有Unicode支持,不会分配ANSI/OEM代码页。
<code class="lang-text">Windows中的区域代码页列表:
            
ANSI代码页 OEM代码页 名称          说明
874                  泰文
932<>                日文
936<>*               简体中文
949<>                朝鲜文
950<>*               繁体中文
1250*      852       中欧
1251*      855,866   西里尔文      855塞尔维亚文和波斯尼亚文,866其它
1252*      437,850   西欧          437美国,850英国和西欧
1253       737       希腊文
1254*      857       土耳其文
1255       862       希伯来文
1256*      720       阿拉伯文
1257*      775       波罗的海文
1258                 越南文
                          
* 国际代码页,有多个国家或地区使用这些代码页
                 
<> 932、936、949、950为双字节代码页,控制台支持仅在特定系统区域下有效</code>

除了四种东亚语言代码页之外,常用的代码页有:英语和西欧1252-850、美国英语437、俄文1251-866。在Windows中,英文、法文、德文、西班牙文、葡萄牙文、意大利文、荷兰文、瑞典文、丹麦文、挪威文、冰岛文、芬兰文等常使用windows-1252,俄文常使用windows-1251。至于Linux中,则已通用UTF-8编码。OEM代码页的一大特点是带有制表符号,原因是为了在字符界面显示方框。但是Windows下不必用字符来显示方框,因此ANSI代码页并不包含制表符号。
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年8个月前 修改于 8年8个月前 IP:四川
792983
首先纠正一个错误,fopen和_wfopen的编码开关有ccs=UNICODE、ccs=UTF-8、ccs=UTF-16LE(不是ccs=UTF-16)。

再纠正一个错误,当ccs开关为ccs=UTF-8、ccs=UTF-16情况下只是无BOM时默认值不同而已,具体情况如下表:

Visual Studio 2005/2008:
ccs=UNICODE——(无BOM或新文件)ANSI——(EF BB BF)UTF-8——(FF FE)UTF-16LE
ccs=UTF-8——(无BOM或新文件)UTF-8——(EF BB BF)UTF-8——(FF FE)UTF-16LE
ccs=UTF-16LE——(无BOM或新文件)UTF-16LE——(EF BB BF)UTF-8——(FF FE)UTF-16LE

Visual Studio 2010以上的版本的MSDN中的说法:
ccs=UNICODE——(无BOM或新文件)UTF-16LE——(EF BB BF)UTF-8——(FF FE)UTF-16LE
ccs=UTF-8——(无BOM或新文件)UTF-8——(EF BB BF)UTF-8——(FF FE)UTF-16LE
ccs=UTF-16LE——(无BOM或新文件)UTF-16LE——(EF BB BF)UTF-8——(FF FE)UTF-16LE

实际上经我测试,Visual Studio 2010以上版本中,ccs=UNICODE时,默认还是ANSI,即对应关系并没有变化。

所有选项都能自动识别带BOM的文件,只是无BOM时的默认值各有不同。所以大家喜欢用什么用什么就行了。
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年8个月前 IP:四川
792984
IsTextUnicode这个函数只有在英文系统(非Unicode语言为英语,代码页1252)中才有用,在中文系统中根本没有任何用处。因为
GBK跟UTF-16冲码的太多了。无BOM的UTF-16文件在英文系统中打开正常,但是在中文系统中一打开必定乱码。

如果将文件按照"r+b"或"w+b"或"a+b"打开(_O_BINARY),C运行库将不会自动转换"\n"和"\r\n",也不会自动添加BOM。这个情况下使用fgets、fputs、fprintf、fgetws、fputws、fwprintf的作用就是将字符串原封不动地输入输出,fputs输出窄字符,而fputws输出宽字符。

如果在文件中按"r+b"或"w+b"或"a+b"输出宽字符,想在记事本中正常打开的话,需要手动在文件开始的地方添加BOM,手动输出BOM可以使用fputwc(L'\ufeff', f)。
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年8个月前 修改于 8年8个月前 IP:四川
792985
ccs=UNICODE并不能完全避免使用ANSI文本。如果想编写仅支持Unicode的程序,应该使用ccs=UTF-8或ccs=UTF-16LE。
它们的共同点,一是都会为程序自动加上BOM,二是在有BOM的情况下可以正常打开彼此的文件。

和在UNIX/Linux中不同,Windows中默认并不使用UTF-8字符集,不加BOM会因与ANSI文本冲码而出问题。

另外虽然记事本支持UTF-16BE,但是C运行库并不支持,可能是因为使用范围过小的原因。
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年8个月前 IP:四川
793057
在VC中将源代码以UTF-8/UTF-16保存的注意事项
一、除ANSI外,源码可保存为UTF-8、UTF-16LE、UTF-16BE三种编码,但是源码一定要带BOM,这样Visual C++才会识别出源代码是Unicode编码。新版gcc也支持带BOM的UTF-8源代码,因此不用担心移植性问题。

二、关于char[]窄字符常量的字符集问题

如果需要使用ANSI编码窄字符常量,则在其它语言的Windows下不一定能编译通过,一般需要在源代码中设置默认区域:
#pragma setlocale("chinese-simplified")

如果需要使用UTF-8编码char[]窄字符常量,则需要Visual C++ 2010 SP1以上的编译器,并设定执行字符集:
#pragma execution_character_set("utf-8")
这个选项有个bug,那就是不能正确处理转义字符,转义字符仍然被编码为ANSI。

如果使用Visual C++ 2015以上的编译器,则可以同时使用ANSI和UTF-8字符集的char[]窄字符常量:
char ansi_str[] = "我是ANSI字符串";
char utf8_str[] = u8"我是UTF-8字符串";
Visual Studio 2015完全解决了UTF-8字符串常量问题。

如果不想这么麻烦的话,可以使用宽字符wchar_t,而不是使用窄字符char储存中文。需要UTF-8的地方再将宽字符转换为UTF-8使用。
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年8个月前 修改于 8年7个月前 IP:四川
793060
C++原生支持UTF-8与UTF-16、UTF-32的转换
转码问题是由于Windows并没有UTF-8的locale导致的。实际上即使你使用_get_current_locale强行修改代码页也不行,根本原因因为VC运行库在转码时设置了UTF-8不支持的MB_PRECOMPOSED开关。因此想用wcstombs或mbstowcs转换UTF-16和UTF-8的尝试注定要失败。

要支持UTF-8,可以使用Visual C++ 2010 SP1以上提供的wstring_convert转码。不过要注意如果碰到非法字符的话,会触发异常,无法继续转换下去。使用try{程序代码}catch(...){},即catch括号里三个点这种形式可以捕捉异常,避免程序崩溃。

在Windows中(wchar_t是16位)最常用的方式:
// string(UTF-8) <-> wstring(UTF-16)
wstring_convert<codecvt_utf8_utf16<wchar_t>> cvt_utf8;

在wchar_t是32位环境中最好这样写:
// string(UTF-8) <-> wstring(UTF-32)
wstring_convert<codecvt_utf8<wchar_t>> cvt_utf8;

如果要可移植的话,建议改用平台无关的char16_t和u16string,而不是使用wchar_t和wstring:
// string(UTF-8) <-> u16string(UTF-16)
wstring_convert<codecvt_utf8_utf16<char16_t>, char16_t> cvt16_utf8_utf16;

除此之外,C++标准库还支持基于UTF-32的char32_t和u32string:
// string(UTF-8) <-> u32string
wstring_convert<codecvt_utf8<char32_t>, char32_t> cvt32_utf8;
// string(UTF-16BE字节流) <-> u32string
wstring_convert<codecvt_utf16<char32_t>, char32_t> cvt32_utf16le;
// string(UTF-16LE字节流) <-> u32string
wstring_convert<codecvt_utf16<char32_t, 0x10ffff, little_endian>, char32_t> cvt32_utf16le;
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年8个月前 IP:四川
793063
遇到无效字符就让程序崩溃是C标准库一贯的风格。。。
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年7个月前 修改于 8年7个月前 IP:四川
794374
这些带宽字符串的函数是Visual C++专用的,对C++编译器不可移植。因此如果你准备换用其它编译器(如MinGW或Intel C++),建议还是使用纯WinAPI编写Windows程序。

_wfopen可以改用CreateFileW,fclose可以改用CloseHandle,读写文本文件可自己使用ReadFile和WriteFile进行处理。

如果纯文本文件,还是UTF-16LE with BOM最好,除非你想编写文本编辑器,否则不要在自己的程序里纠结编码问题。

如果要兼容网络(指定编码或UTF-8)、老格式(如使用ANSI的老软件)、UNIX(UTF-8)、单片机(通常使用437/850/866/932/936/950等OEM代码页)等应用,就需要使用MultiByteToWideChar和WideCharToMultiByte转换。

至于控制台,则可以使用ReadConsoleW和WriteConsoleW实现Unicode输入输出。ReadFile和WriteFile只能使用控制台代码页,不支持Unicode。(英文系统下控制台要想支持中文,可以使用第三方控制台模拟器,如ConEmu)
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年7个月前 IP:四川
795100
如果想要让Windows将一个文本文件(如INI文件)当作Unicode文本文件,它的前两个字节必须为FF FE(UTF-16 LE BOM),一般只有手动加入这两个字节才行。
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年7个月前 IP:四川
796045
如果想要移植到unix,那么使用tchar.h是必要的,虽然gcc不支持宽字符msvcrt函数,但是,你可以通过自己重新编写tchar.h来将这些函数重定向到窄字符版本。
如果想要移植到其它编译器(如mingw)但是不准备移植到unix,那么使用winapi会更好一些。

#ifdef _MSC_VER
#include <tchar.h>
#else
#include <tchar_posix.h> // 程序自己提供此头文件,它把tchar.h中的宏重定向到窄字符版本
#endif
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年6个月前 修改于 8年6个月前 IP:四川
796438
UTF-16LE文件也可以不加BOM,如果不需要让用户编辑的话。末尾的奇数字节截掉就是了。
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
tomcatchen1982
7年3个月前 IP:重庆
830347
大神!膜拜!!!!需要认真反复看几遍。越看越透彻
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论

想参与大家的讨论?现在就 登录 或者 注册

所属专业
上级专业
同级专业
acmilan
进士 学者 笔友
文章
461
回复
2934
学术分
4
2009/05/30注册,5年3个月前活动
暂无简介
主体类型:个人
所属领域:无
认证方式:邮箱
IP归属地:未同步
文件下载
加载中...
{{errorInfo}}
{{downloadWarning}}
你在 {{downloadTime}} 下载过当前文件。
文件名称:{{resource.defaultFile.name}}
下载次数:{{resource.hits}}
上传用户:{{uploader.username}}
所需积分:{{costScores}},{{holdScores}}下载当前附件免费{{description}}
积分不足,去充值
文件已丢失

当前账号的附件下载数量限制如下:
时段 个数
{{f.startingTime}}点 - {{f.endTime}}点 {{f.fileCount}}
视频暂不能访问,请登录试试
仅供内部学术交流或培训使用,请先保存到本地。本内容不代表科创观点,未经原作者同意,请勿转载。
音频暂不能访问,请登录试试
支持的图片格式:jpg, jpeg, png
插入公式
评论控制
加载中...
文号:{{pid}}
投诉或举报
加载中...
{{tip}}
请选择违规类型:
{{reason.type}}

空空如也

加载中...
详情
详情
推送到专栏从专栏移除
设为匿名取消匿名
查看作者
回复
只看作者
加入收藏取消收藏
收藏
取消收藏
折叠回复
置顶取消置顶
评学术分
鼓励
设为精选取消精选
管理提醒
编辑
通过审核
评论控制
退修或删除
历史版本
违规记录
投诉或举报
加入黑名单移除黑名单
查看IP
{{format('YYYY/MM/DD HH:mm:ss', toc)}}