Qt中老有人问起汉字编码的问题,想整理一下,可想来想去还是应该从C语言开始。 整理整理自己的思路,有时会突然觉得自己理解的不少了,可是一旦准备写出来时,总会突然间变得没有头绪。
wchar_t 与 char 的一点区别:
- 就像名字暗示的:
- wchar_t (wide char type)是多字节,char是单字节(最多256个字符)
- wchar_t 中存放的是 Unicode,而 char 多字节字符完全没概念。
- 源文件内
- 使用 wchar_t 时,编译器负责将本地编码的文字转成Unicode
- 使用 char 时,编译器一般直接将本地编码的字节流存入char串中
- 输出时
- 只需知道客户端本地编码,即可将 wchar_t 从Unicode转成正确的编码流
- 而char串,则需要本地编码和char串中使用的编码一致。
严格说来C语言是没有字符串的,有的只是字符数组 char[],以及指针 char *(暂不考虑宽字符 wchar_t )。
首先,我们对 下面的a、b 是完全等价的 这一说法,应该是没有任何异议的。
#include <stdio.h>int main()
{
char a [] = "ABCD";
char b [] = "\x41\x42\x43\x44";
printf("a:%s b:%s", a, b);
return 0;
}
但是,当涉及到中文时,问题变得相当复杂了(为了能简单些,下面只限制在UTF-8和GBK范围内讨论这个问题)
2.1. "我是汉字"究竟是什么东东char 只有8位(最多256个字符),它不可能存储汉字的,能存储只是编码后的一个一个的字节
看看这个例子:这个字符串的长度究竟是多少呢?
#include <stdio.h>#include <strings.h>
int main()
{
char a [] = "我是汉字";
printf("length of a: %d\n", strlen(a));
return 0;
}
将该文件分别保存为GBK、带BOM的UTF8、不带BOM的UTF8 这3种个是,然后分别用MSVC中的cl 和 Mingw 中的 gcc 进行编译 看一下结果:
-
源文件编码
编译器
结果
GBK
cl
8
gcc
8
UTF8(带BOM)
cl
8
gcc
12
UTF8(不带BOM)
cl
12
gcc
12
我们都知道 这4个汉字对应的GBK和UTF-8编码分别是:
"\xce\xd2\xca\xc7\xba\xba\xd7\xd6""\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97"
也就是说:
-
源文件编码
编译器
"我是中文" 内存中编码
GBK
cl
GBK
gcc
GBK
UTF8(带BOM)
cl
GBK
gcc
UTF8
UTF8(不带BOM)
cl
UTF8
gcc
UTF8
这似乎是太乱了,不是么?其实很简单:
- 对 gcc 来说,源文件保存为gbk,就是gbk,保存为utf8就是utf8。没有任何特殊处理。
- 对 cl 来说,只要你的文件带BOM,就转码成gbk。不带BOM的不做任何特殊处理。
- cl
对cl编译器,源文件可以采用带BOM的utf8,带BOM的utf16等等编码。编译器都会自动转码成gbk!!。
也就是说,你保存成BOM的utf8或utf16,和保存成gbk都是一样的。
- gcc
前面的实验其实是mingw32-gcc得出的,其实linux下,带BOM的utf8文件编译器是不认的。
3. wchar_t前面说了 char 不懂中文,于是 C 语言弄了个可以懂中文的 wchar_t。说它懂中文,是因为它可以将一个汉字作为1个字符来处理,而不想char那样,当做2或3个单字节字符处理。但这个东西也不是很好用。
不过事实应该还算好吧,主流的编译器都将其实现为了Unicode,尽管Windows下用的16位的UCS2(UTF-16),而Linux下用的32位的UCS4(UTF-32)。我们就可以简单地认为 wchar_t 存放的就是 Unicode 。
现在是这么一个情况了:
- 我们的源代码保存时采用的是 UTF8 或 GBK,而 wchar_t 存放的是Unicode,这中间需要编译器来完成一个转换。于是,不同的编译器又各显神威了,唉。
- 无论我们将结果输出到终端(控制台)或写入到文件,都需要将 Unicode 转换成字节流(UTF8、GBK或其他),这个转换是由 locale 控制。
由于 wchar_t 存放的是Unicode,而源代码所用的编码可能各种各样,一个问题摆在眼前了,编译器对这各种编码都能正常工作吗?
在Windows下做个试验看看:
#include <stdio.h>#include <wchar.h>
#include <locale.h>
int main()
{
wchar_t a [] = L"我是汉字";
setlocale(LC_ALL, "");
wprintf(L"a:%ls", a);
return 0;
}
- 保存为GBK编码,用Mingw gcc和 VS cl 分别编译运行: cl 工作正常,gcc报错:非法的字节序
- 用记事本另存为UTF8格式,重新编译运行: cl 与 gcc 均正常
- 用16进制编辑器将UTF8文件的开头的3字节的BOM删除,重新编译运行: cl 乱码,gcc正常
列成表格:
-
源文件编码
编译器
"我是中文" 内存中编码
GBK
cl
成功转成Unicode
gcc
失败,编译不过
UTF8(带BOM)
cl
成功
gcc
成功
UTF8(不带BOM)
cl
失败,输出乱码
gcc
成功
其实很简单:
- 对 cl
只要源代码带BOM,直接转unicode。如果不带BOM,则将其作为gbk编码转换到unicode。
于是,当我们用不带BOM的utf8格式时,cl将其按照gbk解码,最后得到长度为6的字符串。
- 对 gcc
默认将所有的字符串都按照utf8解码。在上面,当源代码保存为gbk时,它按照utf8解码,解码出错。
3.3. gcc前面说的都是默认情况,其实gcc是支持其他编码的,只不过需要通过选项来指定。比如对gbk
gcc main.c -finput-charset=gbk -o main是没有问题的。(Mingw4.4有bug,linux下的gcc及tdm-gcc没有问题)
3.4. 转义字符同本文一开始一样,我们对下面的程序中的a与b的完全等价性是应该没有任何异议的:
#include <stdio.h>#include <wchar.h>
#include <locale.h>
int main()
{
wchar_t a [] = L"ABCD";
wchar_t b [] = L"\x41\x42\x43\x44";
setlocale(LC_ALL, "");
wprintf(L"a:%ls b:%ls", a, b);
return 0;
}
那么,汉字的情况的,能和前面char部分的提到的那样:a还等价于 b1 或 b2 中的一个么?
wchar_t a [] = L"我是汉字";wchar_t b1 [] = L"\xce\xd2\xca\xc7\xba\xba\xd7\xd6";
wchar_t b2 [] = L"\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97";
显然不行了,同L"\x41\x42\x43\x44"一样,现在b1是长度为8的宽字符串,b2是长度为12的宽字符串(每一个代表一个Unicode字符,同下)。
汉字该如何呢? 其实同char中指定ASCII码是一样的,只不过这次换成了Unicode(UTF16)编码:
wchar_t a [] = L"我是汉字";wchar_t b1 [] = L"\x6211\x662f\x6c49\x5b57";
wchar_t b2 [] = L"\u6211\u662f\u6c49\u5b57";
C 源文件内的中文(2)
当我们使用 wchar_t 时,就无法回避locale问题了。因为程序内是 unicode,输出时要输出传统的本地字符串,比如big5、gbk等。到底要如何选择呢?
setlocale(LC_ALL, "");使用"",也就是让系统帮我们做选择:
- Linux 下,获取locale的顺序是:环境变量 LC_ALL,每个单独的locale分类LC_*,最后是 LANG 变量
- windows下,受控制面板中“区域与语言选项”的设置影响。简体中文windows下代码页 936,即 GBK
locale 的具体格式是:
语言_国家地区.编码在前面的例子中,如果我们显式设置locale为简体中文的gbk编码,则Windows和linux下分别为:
setlocale(LC_ALL, "Chinese_People's Republic of China.936"); setlocale(LC_ALL, "zh_CN.GBK");感觉上linux下舒服多了。
5. printf我们都知道printf中的格式控制符是怎么回事,也了解用错会有什么结果,比如:
double a= 100;printf("%d", a);
printf 是根据格式控制符来解释内存中的内容的,所以,当我们用 char* 和 wchar_t * 时,格式控制就很重要了。不然一个 wchar_t* 直接作为 char * 来解释或翻过来解释,结果你可以想得到的,对吧。
格式控制符
cl
gcc
ls 或 lS
wchar_t
wchar_t
s
char
char
S
wchar_t
char
当我们使用 wprintf 时,gcc的行为不变,但 cl 行为就有点古怪了
格式控制符
cl
gcc
ls 或 lS
wchar_t
wchar_t
s
wchar_t
char
S
char
char
6. 参考