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。
200字以内,仅用于支线交流,主线讨论请采用回复功能。