[修改于 7年7个月前 - 2017/04/24 22:58:38]
引用 金星凌日:我认为可以参考一下VC/MinGW的TCHAR.H,虽然这个头文件是做Win9x移植的,但POSIX与Win9x都是窄字符,因此有相似性。可以自己用宏写个重定向方案TCHAR_MSVC_POSIX.H。这种做法的好处是完全原生化。不足是有平台差异性,如UTF-16最多两个字符组合,UTF-8可达4个字符组合。一些显著的平台差异,如路径的组成,open/fopen函数的打开方式等,不能完全满足要求,还要做其它的处理。
请教:如何编写(在字符编码问题上)足够可靠的可移植C++程序?
已经过去了很长一段时间,感觉顶楼的有些地方值得重新考虑,这里重新总结一下。
这里将不会讨论任何与MFC有关的内容。因为MFC也需要熟练的Win32基础才能正确使用,现在来看,掌握Win32远比掌握MFC要重要得多。
XXXXXXXXXXXXXXXXXXXXXXXXXX/en-us/library/windows/desktop/dd374083(v=vs.85).aspx
C/C++在Visual C++中的字符串编码
源代码可以保存为ANSI格式、Unicode格式(含BOM)、Unicode big endian格式(含BOM)、UTF-8(含BOM)。
char[]字符串字面量"xxx"所使用的编码,是编译时系统当前ANSI代码页的编码。简体中文系统中就是GBK。
wchar_t[]字符串字面量L"xxx"所使用的编码,是UTF-16编码。
C/C++运行库使用的编码,是运行时系统当前ANSI代码页的编码。
虽然C/C++运行库也支持wchar_t[],但是仅推荐使用少数功能,因为C90Amendment1大多数功能是在Unicode还没有普及的时候发明的,因此大多数功能实现得不可靠。建议使用WinAPI实现需要wchar_t[]的功能。
WinAPI头文件windows.h
Windows系统调用是由Win32 API(WinAPI)进行的。Win32 API的最初版本是1993年的Windows NT,它设计了两套WinAPI:
后者(Unicode版本)是Windows NT原生的,前者是Windows NT通过自动转换字符串模拟的。
使用WinAPI需要引入头文件windows.h。
如果在引入windows.h之前定义了UNICODE宏(即选择“使用Unicode字符集”),则会将没有结尾的WinAPI映射到-W结尾的WinAPI上;如果没有定义这个宏(即选择“使用多字节字符集”或“未设置”),则会映射到-A结尾的WinAPI上。
为了表示通用的WinAPI字符类型,头文件定义了一个类型TCHAR,当定义了UNICODE宏,TCHAR它会映射到wchar_t,否则会映射到char;为了重定向字面量,定义了一个宏TEXT(x),当定义了UNICODE宏,TEXT("xxx")会映射到L"xxx",否则会映射到"xxx"。
引入tchar.h后,可以使用_TCHAR类型,以及更简短的_T("xxx")宏,定义_UNICODE宏以后,会映射到L"xxx",否则会映射到"xxx"。
为了windows.h与tchar.h的一致性,定义UNICODE宏的同时需要定义带下划线的_UNICODE宏,以同时使用宽字符映射,没有定义UNICODE宏的情况下同时定义_MBCS宏,以支持GBK等双字节字符串处理。
对字符串进行转换
**字节字符串与Unicode宽字符串之间转换,比较可靠的方法是使用WideCharToMultiByte和MultiByteToWideChar。**它的优点是稳定性强,并且支持ANSI、OEM、UTF-7、UTF-8等多种字符集,用对应的代码页表示。
如果在某些地方因为预处理宏冲突等问题不能引入windows.h,可以使用如下函数定义:
<code class="language-cpp">__declspec(dllimport) int __stdcall MultiByteToWideChar( unsigned int codepage, unsigned long flags, const char *srcstr, int srcsize, wchar_t *dststr, int dstsize); __declspec(dllimport) int __stdcall WideCharToMultiByte( unsigned int codepage, unsigned long flags, const wchar_t *srcstr, int srcsize, char *dststr, int dstsize, const char *defchr, int *useddefchr); </code>
示例代码:
<code class="language-cpp">// 日常科普:char和wchar_t转换 // wchar_t[]转char[] wchar_t src_wstr[] = L"convert from wchar_t[] to char[]\r\n"; // 短字符串 char dst_str_short[1024] = ""; WideCharToMultiByte(CP_ACP, 0, src_wstr, -1, dst_str_short, 1024, NULL, NULL); OutputDebugStringA(dst_str_short); // 使用char[] // 长字符串 char *dst_str_long = NULL; do // 多步可能失败的操作,用do-while(0)-break { int dst_str_cchsize = WideCharToMultiByte(CP_ACP, 0, src_wstr, -1, NULL, 0, NULL, NULL); if (dst_str_cchsize <= 0) break; dst_str_long="(char" *)heapalloc(getprocessheap(), heap_zero_memory, dst_str_cchsize * sizeof (char)); if (dst_str_long="=" null) widechartomultibyte(cp_acp, 0, src_wstr, -1, dst_str_long, dst_str_cchsize, null, null); 下面是使用char[]的代码 outputdebugstringa(dst_str_long); }while(0); !="NULL)" do-while(0)-break之后只有清理代码 { heapfree(getprocessheap(), dst_str_long); } char[]转wchar_t[] char src_str[]="from char[] to wchar_t[]\r\n" ; 短字符串 wchar_t dst_wstr_short[1024]="L"";" multibytetowidechar(cp_acp, src_str, dst_wstr_short, 1024); outputdebugstringw(dst_wstr_short); 使用wchar_t[] 长字符串 *dst_wstr_long="NULL;" do 多步可能失败的操作,用do-while(0)-break int dst_wstr_cchsize="MultiByteToWideChar(CP_ACP," 0); (dst_wstr_cchsize <="0)" dst_wstr_long="(wchar_t" (wchar_t)); (dst_wstr_long="=" dst_wstr_long, dst_wstr_cchsize); 下面是使用wchar_t[]的代码 outputdebugstringw(dst_wstr_long); dst_wstr_long); code></=></code>
文本的处理
ANSI文本:建议使用系统的CharNextExA、CharPrevExA或_mbsinc、_mbsdec函数处理,而不要使用自己编写的算法。
UTF-8文本:建议直接转换为Unicode(宽字符)处理。如果要手工处理UTF-8文本,一定要在处理之前把char *指针转换为unsigned char *指针,不然由于char通常是有符号的,往往会导致编写的多字节算法无效。
宽字符文本:一般可以直接使用C运行库的wcs系列函数处理。如果需要获得单个码点的值,或者数码点数,需要使用IS_SURROGATE_PAIR判断UTF-16代理对。如果需要数复杂文字的组合字符数,需要使用CharNextW或CharPrevW函数,或者使用Uniscribe复杂文字分析技术。
引入tchar.h后,可以使用_tcslen、_tcscpy、_tcscat、_tcstol、_tcstoi64、_tcstod等函数,它们随_UNICODE宏和_MBCS宏是否定义而被映射到strxxx、wcsxxx、_mbsxxx函数。其中_tcstod(strtod、wcstod)函数是比较重要的,它可以将字符串转换为浮点数,这个功能很难用WinAPI实现。
引入strsafe.h后,可以使用StringCbXxxxx和StringCchXxxxx系列,前者要求缓冲区的字节长度,后者要求缓冲区的字符长度,它们实际上是包装了运行库函数的缓冲区安全化内联函数。同时,它们和WinAPI的规则是相同的,也使用UNICODE宏以及-W和-A结尾。其中StringCbPrintf和StringCchPrintf函数是比较重要的,它们可以将浮点数转换为字符串,这个功能很难用WinAPI实现。
命令参数
Visual C++支持宽字符入口点,通过它们可以获取获取宽字符命令参数。
<code class="language-cpp">int wmain(int argc, wchar_t *argv[]); int __stdcall wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, wchar_t *szCmdLine, int nShowCmd); </code>
引入tchar.h后,可以使用_tmain和_tWinMain宏表示入口点。
<code class="language-cpp">int _tmain(int argc, _TCHAR *argv[]); int __stdcall _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, _TCHAR *szCmdLine, int nShowCmd); </code>
使用这个入口点以后,只会初始化_wenviron全局变量,environ全局变量并没有被自动初始化,不过,只需调用一次getenv("")即可修复这个问题。
除此之外,使用GetCommandLineW可以在任何地方获取Unicode宽字符命令参数。GetCommandLineW返回的是固定地址,不需要释放。使用GetCommandLineA可以获取相应的多字节版本命令参数。
使用CommandLineToArgvW可以获取Unicode宽字符版本的argv。CommandLineToArgvW使用LocalAlloc分配了一个内存块,使用完需要使用LocalFree释放。它只能在Windows 2000以上操作系统使用。
<code class="language-cpp">#include <windows.h> int __stdcall wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, wchar_t *szCmdLine, int nShowCmd) { // 主程序:获取Unicode宽字符版本的命令参数 int argcW = 0; wchar_t **argvW = CommandLineToArgvW(GetCommandLineW(), &argcW); if (argvW) { for (int i = 0; i < argcW; i++) { MessageBox(NULL, argvW[i], L"命令参数", MB_OK); } LocalFree(argvW); } return 0; } </windows.h></code>
文件I/O
一般来说,使用stdio.h就足够了,可使用fopen、_wfopen、_tfopen打开文件,fclose关闭文件。读写使用fread、fwrite或fgets、fputs等其它函数。
可以考虑使用open、_wopen、_topen、sopen、_wsopen、_tsopen打开文件,使用_read和_write读写文件。如果需要转换\n与\r\n换行符,使用_O_TEXT,否则使用_O_BINARY。
如果需要临时禁止转换换行符,使用_setmode(_fileno(stdxxx), _O_BINARY);。使用_setmode(_fileno(stdxxx), _O_TEXT);恢复转换换行符。在VC++2015之前并不支持freopen(NULL, "xxx");这种做法。
如果需要更高的性能和灵活性,可以使用CreateFile打开文件,使用CloseHandle关闭文件。使用ReadFile和WriteFile读写文件。
要注意的是,stdio.h读写文件,对于普通文件和管道或设备,具有一致的行为,而后两种方法,则对文件和管道或设备具有不一致的行为,更加贴近底层一些。
不要尝试使用", ccs=[UNICODE|UTF-16|UTF-8]"、O[W|U8|U16]TEXT,它是VC++2005时期微软的中二产物,手工转换虽然麻烦一点,但可靠性高。
标准I/O和控制台I/O
I/O编程实际上分为两个方向:面向一般设备(普通文件、管道、conin$/conout$等)、面向控制台(conin$/conout$)。
面向一般设备(普通文件、管道、conin$/conout$等)
直接使用stdio.h中的printf/scanf/fgets/puts/getchar/putchar等函数即可,并且为了和其它部分的C/C++程序统一编码,一般假定它们是ANSI。
不要和attrib/tree/more这些MS-DOS工具链统一编码。微软为了避免用户产生乱码的困惑,因此它们被设计为使用OEM代码页,是仿古程序(仿MS-DOS程序)。但是实际上仿古程序的编程是很复杂的,一般人不需要也没必要知道。
不建议直接使用_read/_write或ReadFile/WriteFile,因为普通文件、管道、conin$/conout$的I/O特性各不相同,如果要同时支持这些东西,相当于重新写了一遍stdio.h的函数。
面向控制台(conin$/conout$)
使用GetStdHandle(x)函数可以直接获取标准输入、标准输出、标准错误的Win32文件句柄,参数x可以是STD_INPUT_HANDLE、STD_OUTPUT_HANDLE、STD_ERROR_HANDLE。它们也可以通过(HANDLE)_get_osfhandle(_fileno(stdxxx))获取,如果你要与标准库同步的话。
除此之外,还可以使用CreateFile自行打开conin$/conout$。一定要使用如下标准形式。
<code>HANDLE hconin = CreateFile(TEXT("conin$"), GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); HANDLE hconout = CreateFile(TEXT("conout$"), GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); </code>
可以使用ReadConsole/WriteConsole,其中ReadConsoleA/WriteConsoleA可以支持控制台当前代码页(可以通过GetConsoleCP()/GetConsoleOutputCP()获得),而ReadConsoleW/WriteConsoleW支持Unicode。可以支持。使用这些函数要注意几点:
如何看待奇葩且难以改变的Win32编码体系?
为什么Windows的编码设定这么奇葩?ANSI字符集被淘汰了吗?ANSI字符集是为了Windows 95/98/Me而生的吗?在936之外,需要兼容932、949、950、1251、1252等其它ANSI代码页的系统吗?文本文件的编码情况如何?控制台的编码情况如何?文本文件的编码情况如何?
Windows并不是可以随意重编译的开源生态,而是二进制兼容的闭源生态。ANSI的WinAPI,编码无法随意升级,只能停留在上个世纪90年代的水平。Unicode的WinAPI,编码也不能改为UTF-8或UTF-32,只能使用UTF-16。
ANSI字符集并没有被淘汰,仍然是必需的。在编程中,我们一般认为ANSI和Unicode都够用了,使用Unicode更好,使用ANSI也是可以接受的。至于在用户名上使用emoji会发生什么困扰,是用户自己作死的事情。
在基于Windows NT的Windows操作系统中,ANSI字符集和Unicode字符集在Win32编程中地位是相同的。ANSI字符集是为了便于从(当年的)其它平台迁移而生的,远早于Windows 95/98/Me的出现,所以ANSI字符集不是为了Windows 95/98/Me而存在的。
如果程序要求GBK字符集,则没有必要兼容这些代码页,因为即使兼容了这些代码页,中文也会变成一堆问号。如果程序只要求ASCII字符集,可以考虑支持其它代码页。
在Windows中,文本文件分为ANSI、Unicode、Unicode big endian、UTF-8文本文件,其中后三者分别要以\xFF\xFE、\xFE\xFF、\xEF\xBB\xBF这些BOM序列开头,但是也经常遇到不包含BOM的UTF-8。通常的做法是,不带BOM的文件默认为ANSI,带有BOM的支持Unicode、UTF-8两种即可。可以放弃支持ANSI转而支持不带BOM的UTF-8,但是一定要同时支持带有BOM的UTF-8,不然肯定会对用户产生困扰。
控制台的编码情况比较复杂。控制台交互使用的是Unicode,但是控制台程序的文本流I/O(管道或重定向文件)并没有统一的编码标准,实际使用的可能是GetConsoleCP()和GetConsoleOutputCP()代码页的编码,但也有可能是固定为ANSI或OEM或437代码页的编码,甚至可能是UTF-8编码。对于控制台程序的文本流I/O,一般可以认为是代码页不确定的ASCII文本,应该提供选项让用户选择可读的编码。
时段 | 个数 |
---|---|
{{f.startingTime}}点 - {{f.endTime}}点 | {{f.fileCount}} |
200字以内,仅用于支线交流,主线讨论请采用回复功能。