Windows端的C函数setlocale、printf与wprintf打印中文字符谜局小解
Windows端的C函数setlocale、printf与wprintf打印中文字符谜局小解
背景:区域(locale)设置
在现代操作系统中,为了适配各国用户的文化传统,包括语言字符集、时间日期和货币表达方式等方面的差异,都存在locale设置。用户在安装操作系统时便会选择自己所在国家/地区,作为系统的默认区域设置。在中国,Windows系统的区域设置便是:Chinese (Simplified)_China.936
,包括“936代码页+GB2312字符集+年月日时分日期顺序+货币单位为¥”。顺带一提,在Linux系统上,默认的区域设置是“C.UTF-8”,使用Unicode字符集和UTF-8编码。
对于Windows开发者来说,在Visual Studio中编译好的程序默认的区域设置(“C”,仅支持ASCII字符和编码)和操作系统并不一致,因此在控制台I/O中文字符时通常需要进行区域设置。特别是使用wchar_t
类型的字符时,需要调用wprintf
函数打印字符串,这种场景下调用setlocale函数是必需的。
现象观察:调用setlocale前后打印中文字符串效果
我们这里在讨论仅Windows端的状况
在Windows平台上,标准库函数printf用于输出常规字符串(char*),标准库函数wprintf用于输出宽字符串(wchar*)。有时,开发者想在控制台输出中文信息,而又苦于在在不同代码页的控制台中难以统一正常输出。我们以默认的936代码页和65001代码页为例,进行实验。测试代码如下:
#include<stdio.h>
#include<stdlib.h>int main()
{printf("1.中文测试printf\n");wprintf(L"2.中文测试wprintf\n");setlocale(LC_CTYPE, "");/// Windows中文平台默认为 GBKprintf("3.中文测试printf\n");wprintf(L"4.中文测试wprintf\n");return 0;
}
将编译生成的exe在两个代码页的控制台运行,根据实验观察,打印含中文的字符串时有以下两个现象:
- 调用
setlocale
前:
printf
可以正常打印含中文的字符串。将默认代码页936切换到代码页65001(utf-8编码)时,不能正常打印。wprintf
不能正常打印含中文的字符串。如果字符串首个字符就是中文字符,整个字符串都不会被输出;如果首个字符是ASCII字符,则可以正常输出、直到遇到中文字符。将默认代码页936切换到代码页65001(utf-8编码)时,不能正常打印。
- 调用
setlocale(LC_TYPE,"");
后(即将程序进程的区域设置为系统默认值,例如Windows中文系统的locale是Chinese (Simplified)_China.936
):
printf
依然可以正常打印含中文的字符串。将默认代码页936切换到代码页65001(utf-8编码)时,可以正常打印。wprintf
也可以正常打印含中文的字符串。将默认代码页936切换到代码页65001(utf-8编码)时,可以正常打印。
这是为什么?
现象解释
1. 为何调用setlocale后wprintf能正常输出中文字符?
printf
、wprintf
打印字符串时,都会调用common_vfprintf
函数,并且在完成字符串格式化后,最终都调用_write_nolock
函数将多字节编码的字符串输出到控制台。而_write_nolock
函数会首先检测当前进程是否设置了locale:
- wprintf按wchar类型处理每个中文字符,首先调用
wctomb_s
将其转换为多字节字符编码;再根据是否调用了setlocale,对转换结果作进一步处理,最终将字节流输出到标准流。而wctomb_s
函数需要设置正确的locale,默认的区域设置"C"不支持ASCII以外的字符,该函数无法正常转换。因此,未设置locale时无法正常输出中文内容。 - 如果设置了locale,
wctomb_s
函数可以正常工作,将每个Unicode编码的中文字符(L’')转为多字节编码,不会报错。
2. 为何调用setlocale之前printf就能正常输出中文字符?
未调用setlocale时,字符串的默认编码是多字节编码(GBK),对于printf函数而言,用户传入的char*指针指向的内容相当于原样传递给最终的WriteFile函数,输出代码页是936(GBK编码)时可以正常显示中文字符,而65001代码页就不行。因为65001代码页假定传入的字节流是UTF-8编码的。
3. 为何调用setlocale之后wprintf/printf在不同代码页都能正常输出中文字符?
设置了locale
后,printf
和wprintf
都将字符串的多字节编码(ANSI)字节流先转为Unicode编码,再获取控制台的代码页信息,随后调用WideCharToMultiByte
,将Unicode编码的字符串转换到输出终端对应代码页的多字节编码,将字节流输出到标准流。
因此,只要是支持中文的控制台代码页,调用setlocale
之后wprintf/printf
都可以正常输出中文字符。
总结
wprintf
对于输入的字符串一般用wctomb_s
转换成多字节编码,若未调用setlocale则直接将转换后的多字节编码字节流输出到标准流。
调用了setlocale之后,wprintf
会再额外转换一次,将多字节编码的字节流先转为Unicode编码(UTF-16 LE),然后再根据输出控制台代码页转为多字节编码,将字节流输出到标准流。这次额外的转换正是printf和wprintf在不同代码页控制台能正确输出中文字符的根源。
参考
微软代码页标识符