VC++万国编码,无解
acmilan 2016-1-3Windows
Visual C++是一个很古老的编程平台,它最初来自DOS操作系统下的C/C++语言(如Turbo C),后来经过Windows 3.x、Windows NT、Windows 98的多次重新修补,最后不能适应现在日益发展的Unicode需求了。编码问题很多情况下需要自己解决。

一、控制台方面
VC运行库中,对控制台的Unicode支持是最差的。直到2002年的Visual C++ .NET这个版本,conio.h才支持宽字符函数,直到Visual C++ 2005,stdio.h才支持_O_WTEXT、_O_U8TEXT、_O_U16TEXT这三个模式,但是到Visual C++ 2010,这三个模式又临时性地抽风了,对于支持这么不稳定的特性,当然不能随便用。事实上,控制台的工作机制是这样的:
在Windows中,可以通过GetStdHandle(STD_INPUT/OUTPUT/ERROR_HANDLE)获取标准I/O句柄,句柄可能代表控制台,也可能代表文件或管道。获得句柄之后可以用三个方式读写:
ReadFile/WriteFile总是有效,仅支持char
ReadConsoleA/WriteConsoleA句柄被重定向后无效,仅支持char
ReadConsoleW/WriteConsoleW句柄被重定向后无效,支持wchar_t
运行库的标准I/O一般情况下只会使用ReadFile/WriteFile读写控制台,因此也仅支持char,如果使用wprintf/wscanf等宽字符函数的话,对于_O_BINARY和_O_TEXT模式,表现有两处不同:
_O_BINARY对于宽字符数据直接整体ReadFile/WriteFile,而控制台则始终认为ReadFile/WriteFile传输的是char,因此肯定会乱码。
_O_TEXT对于宽字符首先按照运行库默认的locale转码成ANSI,然后将\n转换为\r\n,最后按照ANSI传输出去,这样如果没有特殊字符的话,是不会乱码的,有特殊字符的话,则本次I/O会出错,根本不会有输出。
然而那个_O_WTEXT设定后,“正常情况下”它首先判断是控制台还是文件或管道(可用GetConsoleMode调用是否成功来判断),如果是控制台的话,直接调用ReadConsoleW/WriteConsoleW,如果是文件或管道的话,则判断文件原有编码,并进行正确的输出。但是在Visual C++ 2010中有两处作死,首先原来_O_WTEXT支持ANSI,但是在VC2010中它和_O_U16TEXT被设定为含义完全相同,其次它对于stdin它会直接调用ReadFile,这样的结果肯定是乱码。所以_O_WTEXT不能用。
在Visual C++ .NET中,conio.h给我们提供了_cgetws(_s)、_cputws、_cwscanf、_cwprintf几个可以直接调用ReadConsoleW/WriteConsoleW的方便函数,它们不能被重定向,理论上可以自行判断标准句柄属性再决定是用conio.h还是stdio.h,但是实际上只有_cputws和_cwprintf特性和stdio.h对应函数完全相同,_cgetws_s不会像_getws_s一样触发异常,参数也不一样,比_getws_s更先进,而_cwscanf则是由_getwche读入的,这个函数不像getwchar那样是按行读入,而是按字符读入的。
所以呢,与其这么麻烦,倒不如直接默认Windows控制台不支持Unicode,先设置好区域,然后由运行库负责char和wchar_t字符转换,适配Windows控制台支持宽字符太麻烦,还是算了吧:
只提供ANSI代码页支持:
setlocale(LC_ALL, "");
提供多种代码页支持的话(不支持UTF-8代码页65001,仅支持运行库所支持的代码页):
char loc[20] = "";
sprintf(loc, ".%u", GetConsoleCP());
setlocale(LC_ALL, loc) || setlocale(LC_ALL, "");

二、文件方面
大家知道在Windows中,fopen可以使用二进制模式和文本模式(VC2005提供的Unicode模式已经在前一节被否决了)。作为一个原则,可移植程序一般使用文本模式,而Windows程序一般使用二进制模式。
文本模式仅支持ANSI,且转换换行符,在使用宽字符时自动按locale转换为char,如果你想编写支持UTF-8的软件,需要使用MultiByteToWideChar和WideCharToMultiByte手动转换。
然而这个换行符转换、宽字符转换,在Windows编程中实际上有点麻烦,所以Windows程序一般使用二进制模式。使用二进制模式要注意,不要混用宽字符和ANSI窄字符函数,否则会导致混乱,也不要使用"\n"或L"\n"作为换行符,要使用"\r\n"或L"\r\n"。同时,写入UTF-16LE文本文件时,第一个字符一定是"\uFEFF"也就是BOM。读取文本文件时,应该先fgetwc一下,如果是L'\uFEFF'就按UTF-16LE读取,否则就ungetwc回去这个字符,再改用ANSI读取。读取之后如果有ANSI版本的函数直接用上就行了,不然可以使用mbstowcs和wcstombs转换(注意setlocale)。如果想要支持UTF-8的话和文本模式一样,也需要通过MBTWC和WCTMB转换。
对于文件名,可以使用宽字符函数,如_wfopen以支持Unicode路径,然而很多可移植程序只会使用通用的fopen,这个函数只能由SetFileApisToANSI和SetFileApisToOEM切换编码,微软在这里作了一个死,当初还没有UTF-8,微软认为除了ANSI就是OEM,不需要第三种编码了,所以只给了一个AreFileApisANSI作为判断,TRUE就是ANSI,FALSE就是OEM,这样再想加上SetFileApisToUTF8已经不可能了。

三、WinAPI方面
WinAPI现在是普遍支持UTF-16LE的,所以应该没啥问题,唯一要注意的是,追加一个ANSI文件会导致数据损坏,所以在比如读写INI时,建议将INI先转换为UTF-16LE。

[修改于 4 年前 - 2016-01-07 04:07:30]

来自 Windows
2016-1-3 23:31:20
acmilan(作者)
1楼
。。。

[修改于 4 年前 - 2016-01-05 22:01:28]

折叠评论
加载评论中,请稍候...
折叠评论
2016-1-4 00:14:57
acmilan(作者)
2楼
WinAPI读写管道和文件,方式只有一个,那就是ReadFile和WriteFile。如果程序对控制台可交互性要求不高,但是对可移植性和清晰性要求很高,那就不要盲目去支持Unicode控制台了——可以认为Windows控制台不支持Unicode,需要调用WinAPI时再转码为Unicode。

[修改于 4 年前 - 2016-01-05 22:01:48]

折叠评论
加载评论中,请稍候...
折叠评论
acmilan(作者)
3楼
欧洲或中东文字Windows中,默认控制台代码页不是ANSI代码页(如英文Windows中,控制台默认437/850,但ANSI代码页为1252),这样的话部分窄字符函数可能乱码,最好的方法是直接重置控制台代码页为ANSI。对于中日韩越泰等东亚文字Windows用户,控制台默认代码页就是ANSI代码页(如简体中文都是936,繁体中文都是950),不必理会这个问题。
SetConsoleCP(GetACP());
SetConsoleOutputCP(GetACP());
折叠评论
加载评论中,请稍候...
折叠评论
acmilan(作者)
4楼
以前以为Windows程序可以完全Unicode化,不过现在发现想多了。。。就连Windows本身都没有完全Unicode化,自己的程序又为什么要这么做呢。。。

[修改于 4 年前 - 2016-01-05 22:04:25]

折叠评论
加载评论中,请稍候...
折叠评论
acmilan(作者)
5楼
CMD中对管道和重定向的支持简直扯淡,可以这样测试一下:
1.用cmd/u打开一个Unicode的控制台
2.用echo abcdefg>abc.txt向abc.txt输出文本
3.然后type abc.txt看看,是不是乱码了。。。
CMD在使用>>追加的时候也并不能通过BOM自动识别编码,因此总是会乱码。。。
1.echo abcdefg>>一个UTF16文本.txt
2.type 一个UTF16文本.txt
所以CMD对Unicode的支持仅限于显示层面,重定向以后根本就是扯淡。。。

PowerShell的做法就更扯淡了:
1.内部命令用|管道传给外部程序的是ANSI
2.外部程序用|管道传给内部命令的内容,可以根据BOM识别是ANSI、UTF-8、UTF-16LE、UTF-16BE编码。
3.外部程序被>或>>重定向后,实际上是重定向到了PowerShell托管的管道,由PowerShell统一接收信息,识别编码,并转为UTF-16LE。

也就是说,对于管道和重定向支持Unicode,微软的实现也很混乱。。。

[修改于 4 年前 - 2016-01-05 22:10:41]

折叠评论
加载评论中,请稍候...
折叠评论
acmilan(作者)
6楼
。。。

[修改于 4 年前 - 2016-01-05 22:04:44]

折叠评论
加载评论中,请稍候...
折叠评论
acmilan(作者)
7楼
。。。好吧,既然是无解了,那就按照控制台函数不支持Unicode使用Visual C++吧。。。

[修改于 4 年前 - 2016-01-05 22:17:16]

折叠评论
加载评论中,请稍候...
折叠评论
2016-1-6 11:28:08
2016-1-6 11:28:08
8楼
= =所以现在考虑学C#了。。。貌似好多程序运行效率上不比C++差。。。不知道微软的翻译器怎么翻译的这么高效率。。。
折叠评论
加载评论中,请稍候...
折叠评论
2016-1-7 06:12:53
acmilan(作者)
9楼
引用 无名神棍:
= =所以现在考虑学C#了。。。貌似好多程序运行效率上不比C++差。。。不知道微软的翻译器怎么翻译的这么高效率。。。
累觉不爱,在win中封装最薄的msvcrt已然烂泥扶不上墙,已转向posix平台=_=我想很多win98时期的vc大牛如今已转投他处,或许就是这个原因吧,然而很多新人还是义无反顾地想入坑,就像以前我一样。可能vc最适合的平台只是dos,win3.x和win9x而已。也难怪微软到了winnt时代要转向.net,只有将char彻底变为16位,才能符合winnt平台最根本的编程需求。

[修改于 4 年前 - 2016-01-07 08:06:40]

折叠评论
加载评论中,请稍候...
折叠评论
10楼
引用 acmilan:
累觉不爱,在win中封装最薄的msvcrt已然烂泥扶不上墙,已转向posix平台=_=我想很多win98时期的vc大牛如今已转投他处,或许就是这个原因吧,然而很多新人还是义无反顾地想入坑,就像以前我一样。可能vc最适合的平台只是dos,wi...
C我现在就是在单片机用了233。。。电脑上我还是老老实实学一学C#吧。。。就是偶尔建个小网页我在考虑我是继续看JS还是转投ASP.NET门下
折叠评论
加载评论中,请稍候...
折叠评论
11楼
然而好多底层的应用比如hook神马的,还是只能用vc,不然基本无解
折叠评论
加载评论中,请稍候...
折叠评论
acmilan(作者)
12楼
引用 金坷居士:
然而好多底层的应用比如hook神马的,还是只能用vc,不然基本无解
这时候用的根本不是vc了吧,汇编也能做→_→
折叠评论
加载评论中,请稍候...
折叠评论

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

ID:{{user.uid}}
{{user.username}}
{{user.info.certsName}}
{{user.description}}
{{format("YYYY/MM/DD", user.toc)}}注册,{{fromNow(user.tlv)}}活动
{{submitted?"":"投诉"}}
请选择违规类型:
{{reason.description}}
支持的图片格式:jpg, jpeg, png