Visual C++编程,关于字符集的那些事,大总结
acmilan2015/12/16软件综合 IP:四川
Visual C++程序应该使用什么字符集,应该根据使用的需求来决定。

一、学习C/C++语言基础,编写可移植程序

如果是这个目的的话,使用ANSI C/C++函数和默认字符集就可以接受了,没必要使用其它字符集。因为对于大多数中国人来说,GBK字符集已经够用,并且默认字符集用char数组就可以表示,可以避免大多数麻烦的情况。(实际上,GBK当年是按“暂时代替Unicode”的字符集来设计的)

二、编写现代Windows程序

1.程序内码
程序内码应选择UTF-16,并使用wchar_t数组存储字符串。默认字符集(称为ANSI字符集)是为了兼容16位平台和Win9x平台存在的。现代Windows都是以UTF-16为内码的。在Windows中,使用老旧的默认字符集(GBK等)会频繁操作进程的堆内存用以转换字符串,降低程序效率。同时,不能在默认字符集表示的字符会变为问号,影响程序的稳定性。
使用UTF-16的好处显而易见,新加入的WinAPI、所有的COM组件、.NET和Java都使用UTF-16,使用UTF-16可以更方便地调用这些组件。UTF-16字符串处理起来也更加简单。

2.文件编码
保存文件时应根据需要选择ANSI、UTF-8、UTF-16。例如批处理文件和vbs文件必须以ANSI编码,INI配置文件可以用ANSI或UTF-16编码,而XML文件则通常以UTF-8编码。如果是你自己使用的文件,用什么编码都无所谓。要注意ANSI编码可能导致信息丢失,而UTF-8或UTF-16则不会。

3.头文件
在引用windows.h时,应该提前将预处理器符号UNICODE设为1,以避免误用ANSI函数导致编译错误:
#define UNICODE 1 // 强制使用Unicode,对于windows.h
#include <windows.h>
对于其它头文件(包括C运行库、MFC和ATL),应该将_UNICODE设为1,同时要保证_MBCS不被定义。在所有头文件之前可这样定义:
#undef _MBCS
#define UNICODE 1 // 强制使用Unicode,对于windows.h
#define _UNICODE 1 // 强制使用Unicode,对于所有其它头文件

4.入口点
传统的main和WinMain使用ANSI,新程序应该使用wmain和wWinMain。其中wmain的形式是
int wmain(int argc, wchar_t *argv[], wchar_t *envp[])
而wWinMain的形式是
int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, LPWSTR lpCmdLine, int nCmdShow);
对于MinGW等其它编译器,C++模式下可能对wmain和wWinMain无法正确识别而编译为带重载修饰的符号,导致链接器报错无法找到_wmain或wWinMain。始终在前面添加extern "C"前缀是好的习惯:
extern "C"
int wmain(int argc, wchar_t *argv[], wchar_t *envp[])
extern "C"
int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, LPWSTR lpCmdLine, int nCmdShow);

5.运行库的调整
在Visual C++运行库中,有不少ANSI标准没有提及的宽字符函数(如_wfopen等),大多数系统相关函数都以_w开头,可以在宽字符环境下使用,并且支持Unicode字符。Visual C++中所有的宽字符函数见我的另一个帖子:https://www.kechuang.org/t/78249
对于Visual C++运行库,初始状态下区域设定为L"C",在Windows下表示纯英文,系统在进行字符转换时,无法正确处理中文。因此必须在程序开始时手动读取系统的区域设定(需要包含locale.h):
_wsetlocale(LC_ALL, L"");

6.控制台程序
对于控制台程序,默认情况下是调用ReadFile/WriteFile或ReadConsoleA/WriteConsoleA进行输入输出的,依赖于控制台代码页。Visual C++ 2005以后,运行库支持使用Unicode模式的控制台,使用ReadConsoleW/WriteConsoleW进行输入输出,但需要自己设置模式(需要包含io.h和fcntl.h):
_setmode(_fileno(stdin), _O_WTEXT); // VC2015和VC2005均测试通过,VC2010SP1乱码
_setmode(_fileno(stdout), _O_WTEXT);
_setmode(_fileno(stderr), _O_WTEXT);
另一种可行的解决方案,是直接使用ReadConsole(W)和WriteConsole(W)进行输出,并使用swscanf和swprintf手动进行格式化。这种方法不受运行库版本的影响,但需要自己分配缓冲区。使用ReadConsole要注意三点,一是ReadConsole可以指定要读取的字符数,一次读取不完可以重复读取;二是读取后缓冲区是开放的,需要自己在buf[chrread]处添加L'\0'以终结字符串;三是这样读取的字符串回车符是L"\r\n"而不是L"\n"。

7.文本文件读写
打开文件一定要使用_wfopen或_wopen,以支持Unicode路径。_wfopen和_wopen的第二个参数可以指定读取方式,并且有对应关系:
L"xxb" _O_BINARY 不转换换行符,原样输入输出【GUI/WinAPI程序建议使用】
L"xx" _O_TEXT 自动转换换行符,总是按ANSI读写(需设置区域以正确转换中文)
L"xx, ccs=UNICODE" _O_WTEXT 自动转换换行符,编码按BOM决定(支持UTF-8和UTF-16LE,下同),无BOM按ANSI处理(需设置区域以正确转换中文)
L"xx, ccs=UTF-8" _O_U8TEXT 自动转换换行符,编码按BOM决定,无BOM按UTF-8处理
L"xx, ccs=UTF-16LE" _O_U16TEXT 自动转换换行符,编码按BOM决定,无BOM按UTF-16LE处理
如果是GUI/WinAPI程序(包括使用WinAPI手动读写控制台的程序),应总是使用L"xxb"和_O_BINARY读写方式。这样的话,不会自动转换换行符(以避免与WinAPI交互时产生问题),使用fwprintf输出的是UTF-16LE,使用fprintf输出的则是ANSI。在文件的头部要手动添加BOM(\uFEFF)不然记事本中会乱码。
如果是使用C/C++标准库读取输入的控制台程序,可以在最后四种读写方式中选择一个使用。后三种读写方式不能使用fprintf等窄字符函数(会报错,主要是出于自动转换编码的考虑),使用fwprintf等宽字符函数可以正常读写。

8.字符串之间的转换
转换字符串的最简单方法是使用运行库函数:
第一种方法,使用snprintf(buf, bufsize, "%ls", srcstr);可以将宽字符转换为窄字符,使用swprintf和L"%hs"则可以转换为宽字符。(需设置区域以正确转换中文)
第二种方法,使用wcstombs和mbstowcs。(需设置区域以正确转换中文)
以上两种方法不支持UTF-8。
第三种方法,对于ANSI可以使用wstring_convert<codecvt<wchar_t, char, mbstate_t>>转换(需设置区域),对于UTF-8可以使用wstring_convert<codecvt_utf8_utf16<wchar_t>>转换(需新版编译器)。
通过标准库进行字符串转换有一个弊端,那就是遇到无效字符程序会崩溃。如果你不介意的话当然可以使用,更好的方法是使用WinAPI进行转换。
在Visual C++中,用WinAPI转换字符串,我们可以有五种方法可以选择:
第一种方法,如果某个WinAPI有ANSI形式(如SetWindowTextA),直接使用这个形式即可。此方法不支持UTF-8。
第二种方法,直接使用MultiByteToWideChar和WideCharToMultiByte进行转换。MBTWC有6个参数,WCTMB有8个参数,实际用到的只有第1,3,5,6四个参数,用法如下:
int reqbufsz = MultiByteToWideChar(cp, 0, src, -1, 0, 0); // 获取所需缓冲区大小
MultiByteToWideChar(cp, 0, src, -1, buf, bufsz); // 进行转换
int reqbufsz = WideCharToMultiByte(cp, 0, src, -1, 0, 0, 0, 0); // 获取所需缓冲区大小
WideCharToMultiByte(cp, 0, src, -1, buf, bufsz, 0, 0); // 进行转换
第三种方法,通过_bstr_t这种字符串类型进行转换。_bstr_t以SysAllocString函数UTF-16保存字符串,可以随意从char*或wchar_t*初始化,也可以随意用作char*或wchar_t*类型,它会自动进行转换。此方法不支持UTF-8。
第四种方法,如果安装有ATL或正在使用MFC的话,可以使用CStringA和CStringW构造函数进行转换(需要包含atlbase.h和atlstr.h或使用MFC工程)。此方法不支持UTF-8。
第五种方法,如果安装有ATL或正在使用MFC的话,可以使用CW2A和CA2W构造函数进行转换(需要包含atlbase.h或使用MFC工程),第二个参数可以自定义代码页,支持UTF-8。

9.与.NET Framework的互操作
Visual C++支持与.NET Framework的System::String^的互操作,但是不能直接将System::String^转换为wchar_t*。需要使用msclr/marshal.h msclr/marshal_cppstd.h msclr/marshal_windows.h msclr/marshal_atl.h等头文件中的marshal_as<T>(位于msclr::interop命名空间)函数进行封装和解封装处理。
marshal_as<T>可以将System::String^与CString、CComBSTR、_bstr_t、std::wstring等自释放的C++对象进行自由转换,但是对于wchar_t*、BSTR等需要手动释放的指针来说,则需要先创建一个marshal_context作为容器,并使用它的成员函数marshal_context::marshal_as<T>进行转换。

[修改于 7年0个月前 - 2017/04/24 22:58:38]

来自:计算机科学 / 软件综合
6
已屏蔽 原因:{{ notice.reason }}已屏蔽
{{notice.noticeContent}}
~~空空如也
acmilan 作者
8年4个月前 IP:四川
801021
ReadConsole的系统缓冲区好像是64KB,实际可缓冲字符数是31367,所以单次读取的话,用户缓冲区分配32768个wchar_t就够了。。。
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
金星凌日
8年4个月前 IP:陕西
801080
请教:如何编写(在字符编码问题上)足够可靠的可移植C++程序?
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年4个月前 修改于 8年4个月前 IP:四川
801274
引用 金星凌日:
请教:如何编写(在字符编码问题上)足够可靠的可移植C++程序?
我认为可以参考一下VC/MinGW的TCHAR.H,虽然这个头文件是做Win9x移植的,但POSIX与Win9x都是窄字符,因此有相似性。可以自己用宏写个重定向方案TCHAR_MSVC_POSIX.H。这种做法的好处是完全原生化。不足是有平台差异性,如UTF-16最多两个字符组合,UTF-8可达4个字符组合。一些显著的平台差异,如路径的组成,open/fopen函数的打开方式等,不能完全满足要求,还要做其它的处理。
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年4个月前 修改于 7年7个月前 IP:四川
801819
。。。。。。
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年4个月前 修改于 6年8个月前 IP:四川
801820

。。。。。。。。。。

引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
7年10个月前 修改于 6年7个月前 IP:四川
822030

已经过去了很长一段时间,感觉顶楼的有些地方值得重新考虑,这里重新总结一下。

这里将不会讨论任何与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:

  • ANSI版本——以-A结尾,使用char[]作为字符串类型,使用ANSI字符集
  • Unicode版本——以-W结尾,使用wchar_t[]作为字符串类型,使用Unicode字符集

后者(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等多种字符集,用对应的代码页表示。

  • ANSI代码页:0(CP_ACP)(Windows代码页)
  • OEM代码页:1(CP_OEMCP)(DOS代码页)
  • UTF-7代码页:65000(CP_UTF7)
  • UTF-8代码页:65001(CP_UTF8)

如果在某些地方因为预处理宏冲突等问题不能引入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等其它函数。

  • 如果想要按照\n读取换行符,则应使用文本模式"r/w/a[+]"。
  • 如果想要按照\r\n读取换行符,则应使用二进制模式"r/w/a[+]b"。

可以考虑使用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。可以支持。使用这些函数要注意几点:

  • 请求字符数不要低于256字符,因为行缓冲最小是256字符(包含\r\n)。
  • 请求字符数不要高于4096字符,C运行库的默认缓冲区大小,因为太大的请求字符数在WinXP/2003/Vista/7上容易RPC失败,4096字符已经能够满足大多数应用需要了。Win8+因为改成NtDeviceIoControlFile了,因此已经没有这个问题,但应该考虑兼容性问题。
  • ReadConsoleA缓冲区尽量大一个字节,因为双字节代码页(如936)中这玩意的实现有bug,容易缓冲区溢出。
  • UTF-8代码页65001虽然可以通过chcp切换过去,但是ReadConsoleA会出问题,因为它只是为了单字节代码页(如437)和双字节代码页(如936)设计的。

如何看待奇葩且难以改变的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文本,应该提供选项让用户选择可读的编码。

引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论

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

所属专业
所属分类
上级专业
同级专业
acmilan
进士 学者 笔友
文章
461
回复
2934
学术分
4
2009/05/30注册,5年2个月前活动
暂无简介
主体类型:个人
所属领域:无
认证方式:邮箱
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)}}