今天看到 C 语言委员会的提案 N1875,顿时有种「卧槽」的感觉,因为它的标题是:Adding classes to C。这要是通过了,那可真就变成名副其实的 C with Classes 了呀~
纵观提案全文,主要从 C++ 中吸收「类」的概念和用法,但是没有虚函数之类的东西。如果算上单独的「访问限制符」、「单一继承」提案,一个 C 语言的类,很可能类似于:
class Car: public Vehicle
{
public: // 这是单独的另一个提案引入的
// 构造函数
initCar() {
initVehicle(); // 需要显式调用父类构造函数
countWheels_ = 4;
}
// 构造函数
initCar(int speedMax, int countWheels) {
initVehicle(speedMax); // 需要显式调用父类构造函数
countWheels_ = countWheels;
}
// 析构函数
deleteCar() {
deleteVehicle(); // 需要显式调用父类析构函数
}
int getWheelsCount() const { // 也有 const 哟
return this->countWheels_; // 也有 this 指针
}
private:
int countWheels_;
};
使用时,构造写法有点奇怪,也没说构造失败会怎样,析构也需要手动显式调用:
Car car; // 相当于 initCar()
Car tank.initCar(80, 16);
tank.deleteCar(); // 析构函数需要显式调用
car.deleteCar();
从目前的样子看,这样的「类」更类似于语法糖。当然,这只是个提案而已,会不会最终被批准,还得拭目以待。
以上。
C 语言中的字符串
抽象数据类型
C 语言贴近机器模型,所以并不提供字符串数据类型。但是,字符串却在 C 语言中作为 ADT 存在着:使用字符指针代表字符串,同时提供了 string.h
作为字符串的操作库,而在实际存储时则使用字符数组。
虽说单引号引起的字符是该字符对应的一个整型常量,但是 'A'
和 65 没有任何关系。因为 C 语言并没有要求每个实现都用 ASCII 字符集作为执行字符集。
事实上,C 语言标准要求每个 C 语言实现都要定义两个字符集:代码字符集、执行字符集。每个字符集又都由基础字符和扩展字符构成。扩展字符可有可无,完全由该实现所支持的区域设置决定;基础字符只划定了字符,字符对应的码位留由实现决定。
字符与 char
类型
字符集顾名思义就是字符的集合,字符集除了划定所包含的字符外,还将其所包含的字符按照一定顺序编号,这样的编号称作码位。著名的 ASCII 参照已有电报报文设计,分别用数字 0 到 127 作为英文大小写字母、阿拉伯数字、标点符号等的码位。由于出现早、使用广,后辈字符集、字符编码几乎都与之兼容为荣。
然而 ASCII 本身并不完美,尽管名字叫「美国信息交换标准代码」,但是却连英语中的一些诸如 naïve、café、élite 的外来词都无法表示,欧洲各国那些稀奇古怪的字母就更别提了。所幸,我们很快来到了一个字节 8 位的年代,char
能够容纳 0 到 255 一共 256 个值,于是各国纷纷物尽其用,将一个字节中 ASCII 并没有用到的 128 到 255 对应为自己国家所需要用到字符。这些五花八门的对应规则便是后来的代码页(code page),同样的值可以在不同的代码页中表示不同的字符。
到目前为止,char
类型还算是名副其实的字符类型。然而,东亚文化圈广泛使用的汉字成千上万,少说也需要上千个码位,一个字节所能提供的这区区 256 个码位实在是杯水车薪。
计算机科学里的任何问题都可以通过增加一个中间层来解决。
既然码位不得不超过 255,一个字节放不下,那么将以前的「码位→字节」变成「码位→编码→字节序列」如何?
举个栗子:Unicode 字符集中,字符「汉」的码位是 27721(十六进制 6C49),一个字节显然容不下,我们可以通过 UTF-8 编码将其变成字节序列。
- 通过字符集找到对应码位:汉→
U+6C49
;
- 根据 UTF-8 编码规则,介于
U+0800
和 U+FFFF
之间的码位需要按照 1110xxxx 10xxxxxx 10xxxxxx
的形式编码为三个字节;
6C49
的二进制形式是 0110 1100 0100 1001
,代入上面的模板得到 11100110 10110001 10001001
;
- 得到最终的字节序列
E6 B1 89
。
看上去不错,但这样一来,char
这个「字符」类型就有点名不副实了:一个 char
可能只是一个字符的一部分,只有两个或者更多的 char
拼起来才能够表示一个字符。更为麻烦的是,相关的标准库也变得不再可靠了:类似 strlen()
这种 too simple, sometimes naive 的方法顿时从返回字符串的字符数降级成了返回字符串的字节数。
printf("%zu\n", strlen("\xE6\xB1\x89")); // 输出 3;但实际上只有一个字:汉
多字节字符串
为了解决 strlen()
的这种尴尬,C 语言中引入了「多字节字符串」(multibyte string)的概念,一个字符可以对应多个字节;而原先的字符串则对应称为「字节字符串」(byte string),一个字符只能对应一个字节。
char const *multibyteString = "Hello 世界"; // 和普通字符串没什么区别
多字节字符串依旧是 char *
,所以这两者的区别其实只是抽象概念上的,何况严格来说,字节字符串也是一种特殊的多字节字符串。
不同的编码每个字符占用的字节数可能不同,即便是同一种编码,不同的字符也可能占用不同的字节数。所以多字节字符串面临的主要问题便是「下一个字符到底占几个字节」。C 语言引入了 mblen()
函数来回答这个问题:
int mblen(char const *s, size_t n);
其中 s
是一个多字节字符串,而 n
表示至多检查多少个字节;这个函数会根据当前区域设置(Locale)的 LC_CTYPE
分类中所指定的编码来解析这个多字节字符串,返回该字符串的字一个字符占几个字节。
char const *multibyte= "\xE6\xB1\x89"; // 「汉」的 UTF-8 编码
printf("%d\n", mblen(multibyte , 5)); // 输出 -1;因为默认区域设置为 C,无法识别这样的序列。
setlocale(LC_CTYPE, "en_US.UTF-8"); // 非 Windows 平台用,Windows 平台不支持 UTF-8 的区域设置
printf("%d\n", mblen(multibyte, 5)); // 输出 3;因为当前区域设置为 UTF-8 编码。
宽字符 wchar_t
多字节字符串的引入终于算是「一定程度上解决了问题」,但这种做法实在繁琐。于是,C 语言同时引入了宽字符类型 wchar_t
和配套支持库 wchar.h
,试图一劳永逸地解决这个问题。
wchar_t const *wideString = L"Hello World"; // 使用前缀 L
与 char
的一个字节不同,wchar_t
类型本身可以占多个字节,保证永远与字符一一对应,省下了编码的步骤。如此就可以通过 wchar.h
中提供的 wcslen()
函数正确计算 wchar_t *
字符串中的字符数了。
多字节字符串和宽字符串的转换
与 mblen()
类似,C 语言提供了两组共四个函数,根据当前的区域设置进行多字节字符(串)和宽字符(串)的转换。
wctomb()
将宽字符转换为多字节字符
mbtowc()
将多字节字符转换为宽字符
wcstombs()
将宽字符串转换为多字节字符串
mbstowcs()
将多字节字符串转换为宽字符串
误区
看到这里,你是否想过这个能够保证与字符一一对应的 wchar_t
类型到底需要占几个字节呢?没错,不一定占多少,但总之至少一个字节就对了!
事实上,关于 wchar_t
的准确要求是:能够容纳下所支持的区域设置中,最大的扩展字符集中的每一个码位。
an integer type whose range of values can represent distinct codes for all
members of the largest extended character set specified among the supported locales.
也就是说,如果有一个只支持 ASCII 字符集的奇葩的 C 语言实现存在,那么在这个实现下,wchar_t
完全可以只占 8 位。
如此,我们经常听到的「宽字符和 Unicode 有关」的说法也就不攻自破了。
C11 中引入的定长字符类型
时过境迁,转眼到了 2011 年。正如 C99 中在 stdint.h
中加入了 uint32_t
之类的定长整数类型一样,C11 也引入了两种定长字符类型以满足 Unicode 的需要:
char16_t
占 16 位,2 字节,可以保存 UTF-16 字符。
char32_t
占 32 位,4 字节,可以保存 UTF-32 字符。
同时,继 string.h
、wchar.h
后,又进入了 uchar.h
作为这两种字符类型的支持库。
char16_t *utf16String = u"Hello UTF-16"; // 使用前缀 u
char32_t *utf32String = U"Hello UTF-32"; // 使用前缀 U
C++ 中的字符串
C++ 极尽所能兼容 C 语言,陆续支持了和 C 语言相同的那些 char
、wchar_t
、char16_t
和 char32_t
类型,这里就不再重复了。
至于 std::string
,用 C++ 不可能没听说过,它封装了常用的字符串操作。但你是否知道 std::wstring
的存在呢?
如果有心,你会发现 std::string
实际上是对 std::basic_string<T>
模板的特化,形式类似于:
typedef std::basic_string<char> std::string;
typedef std::basic_string<wchar_t> std::wstring;
至于 C++11 中,由于 char16_t
和 char32_t
的引入,也对应增加了 std::u16string
和 std::u32string
的特化版本。
std::basic_string::data()
在 C++11 以来的改变
std::basic_string<T>
其实只是对 T
字符数组的封装,如果需要使用 C 风格的字符串 T *
,可以通过 c_str()
方法获取。
而如果说 c_str()
方法表示「获取该类对应的 C 字符串表示」的语义,那么 data()
方法则表示「获取该类实际保存的数据」的语义。
在 C++11 之前,通过 data()
方法获取的 T *
并不保证 NUL
结尾,即仅保证 [data(), data() + size())
有效。而自 C++11 起,data()
变为保证 NUL
结尾,与 c_str()
的行为完全相同。
从 C++17 开始,我们还可以 data()
方法获取可修改的缓冲。
Windows API 中的字符串类型
不得不说,Windows 的各种大写的自定义类型实在丑陋得令人发指。经常不由得让人发出「好好起名字会死么?」的感叹。
#define CHAR char
#define LPSTR char *
#define LPCSTR char const *
#define WCHAR wchar_t
#define LPWSTR wchar_t *
#define LPCWSTR wchar_t const *
当然,微软如此做法自然有其历史原因,我对所谓的「向前兼容」表示非常无奈。不过话说回来,除了 Windows API 如此大张旗鼓地使用 wchar_t
,其余地方真不怎么能见到宽字符的身影。现代 Windows 内部使用 UTF-16,所以 wchar_t
能够保证是 16 位的。
Windows API 中所有与字符串相关的函数、数据结构等,都存在 ANSI 和 Unicode 两个版本,可以通过宏 UNICODE
来切换。比如一个普通的 MessageBox()
函数,实际定义是这样的:
int MessageBoxW(HWND hWnd, LPCWSTR text, LPCWSTR caption, UINT type); // 宽字符串版本
int MessageBoxA(HWND hWnd, LPCSTR text, LPCSTR caption, UINT type); // 多字节字符串版本
#ifdef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA
#endif
字符类型也是类似,使用 TCHAR
作为一个通用的字符名字。字符常量则使用 TEXT()
宏来定义。
#ifdef UNICODE
#define TCHAR WCHAR
#define TEXT(s) L ## s
#else
#define TCHAR CHAR
#define TEXT(s) s
#endif
如果你用的是 Visual C++,这个 UNICODE
宏通常并不需要手动设置,而是作为工程属性的一员出现的。
p.s. 不要被这个宏的名字骗了,还记得之前我们说过的 wchar_t
和 Unicode 一点关系都没有吗?可以这么理解:wchar_t
只在 Windows API 中表示 UTF-16 数据。
宽窄字符串的转换
Windows 平台也可以用之前提到的四个函数转换 char
字符串和 wchar_t
字符串。但 Windows 本身提供的 API 似乎更有效率:
MultiByteToWideChar()
多字节字符串转换为宽字符串
WideCharToMultiByte()
宽字符串转换为多字节字符串
再次吐槽 M$ 这蛋疼的命名,命名是转换字符串,起名字起得跟转换单个字符一样。
BSTR
BSTR
可以解释为 Basic String 或者 Binary String,是 Windows 下用于 COM、自动化的字符串类型。定义为:
typedef OLECHAR *BSTR;
OLECHAR
是 Windows 的历史遗留,类似于根据宏 OLE2ANSI
转换的 TCHAR
,但 OLE2ANSI
是 16 位时代的产物,于是 Win32 环境下,OLECHAR
永远是 WCHAR
,也就是 wchar_t
。
尽管如此,BSTR
并不能等同于 OLECHAR *
,因为它们的内存布局不同:BSTR
在它自身前拥有一个四个字节的前缀、以双 NUL
结尾。其中,前缀用于描述字符串内容所占的字节数,它本身所指向的是字符串内容而非长度。
-4 0 2 4 6 8 10
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 6 | H | i | ! | NUL | NUL |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
↑
bString
这一点,从用于创建 BSTR
的接口 SysAllocString()
便可以看出:
BSTR SysAllocString(OLECHAR const *psz);
倘若 BSTR
与 OLECHAR *
相同,那么岂不变成了鸡生蛋、蛋生鸡的问题?
虎头蛇尾
呃……前面讲 C 语言字符串的时候似乎还是觉得挺有说头,足足规划了一个星期之久,但后面似乎越来越不知道该讲什么,留待日后积累到新的东西再完善吧~
以上。
回到无锡一年了,能让人感觉有归属感的工作始终没有找到。因为这一年间换了两份工作,所以在面试时经常被质疑人品;尽管我也尽力解释,但还是感觉不吐个槽迟早会憋出心理疾病。
我起初有些太理想主义,但很快就不得不对现实作出妥协:我曾下决心,但凡在职位要求上写「精通 C++」的公司一律无视;无奈现实是,无锡这样的二线城市的公司,几乎没有不写这个让自己显得很外行的要求的。
家人总是说我自命清高、不明事理。回无锡后的第一份工作我就是因为受不了公司从上到下的各种「国企范儿」的陋习才离职的。为了图省事,我喜欢以「环境压抑,马屁随处可见,而且给人感觉公司的主业是混补助」作为总结。有一次面试时,面试官听毕,带着一副「你是怎么活到这么大的」的理所当然的表情陈述道:「这样的公司很多呀!」我哭笑不得,只好耸耸肩说:「我个人比较……呃……看不得这些吧……」。
我的运气大抵都消耗在毕业后的第一份工作上了,所以辞职回无锡后接二连三碰见奇葩公司。在无锡的第二份也就是最近这份工作简直是不堪回首:
- 老板不懂技术,却喜欢瞎指挥,提出不切实际的要求。(e.g. 老板每天的固定节目是和 C# 组开会,只为纠结某个控件的颜色、大小等等……没错,不是和美术组)
- 项目初期,花大时间制定各种计划,写各种文档;但制定完计划后,老板会对员工挨个谈话,要求缩减计划,要求在计划中排入周末加班时间;正式开始编码后,老板又会不断开会要求提前完成任务,他认为如果按时完成意味着计划的制定存在问题;文档从写出来以后再没有人提……
- 老板固执不听劝,谁提跟谁急,大家只好眼睁睁地看着自己在错误的道路上越走越远。
- 不信任员工。(e.g. 同事因为小长假高速堵车,早上没法及时赶来上班而请了半天假,老板纠结了好几天,最后决定让助理打电话去客运公司确认)
- 老板曾要求员工下班后在会议室集体阅读某本成功学书籍,并要求大家在 2 个月内交出 18 篇读后感。
- 不允许睡午觉;老板办公室开空调却不允许开外面办公室的空调,只允许开另买的电风扇。
- 楼层公共区域的灯一定要开着,尽管开着和关着没什么区别;园区物业表示已经怕了我们老板了。
- 因为周六几乎没人愿意加班,故而不断将「满足以下条件的员工,必须强制加班」的制度推陈出新。
- 老板出国前特意在办公室安装了监视摄像头,要求每天上班时录像。
- 恐吓提出辞职的员工「我认识好多人,我保证你找不到工作」,扣押退工单等等……
千言万语汇成一句话:老板太奇葩,以致于我和同事经常对这公司是不是一个大型实景整人秀表示怀疑。
有一个 HR 在交流中不断流露出「你这么频繁地换工作,是为了钱么」的暗示,我感觉我的玻璃心碎了,于是当晚写了一封长信去辩解。
有一位 HR 的话我觉得还是有道理的:这里很多软件公司都是趁着员工年轻赶紧压榨型的。我感觉,悲剧在于:一方面,程序员作为一个常见职业的时间并不长,但社会却已经把程序员自嘲的「苦逼」二字当真了,于是是个人就觉得程序员就应该很苦逼;另一方面,不少老板并非技术出身,不了解软件行业,以为软件开发是劳动力密集型产业,以为多干一小时就能多产出效果。
诶,不该心高气傲,找到个差不多的工作做着就行了……吧?
定公問:君使臣,臣事君,如之何。孔子對曰:君使臣以禮,臣事君以忠。
《论语·八佾》
所謂大臣者:以道事君,不可則止。
《论语·先进》
君有過則諫,反覆之而不聽,則去。
《孟子·万章》
我们平时所说的「文本」基本上都是在说「电脑屏幕上的字符」,但是小学生都知道「计算机只懂 0101」,那么电脑究竟是怎么处理三千世界中如此纷繁复杂的文字的呢?
开天辟地阿斯克
电脑是美国人发明的,所以最初的人们也只需要考虑「如何让电脑明白英文的字母和符号」的问题。
于是 ASCII(美国信息交换标准代码)诞生了。它参考了电报报文的设计,将 0 到 127 都赋予了对应的字母和符号。于是可以被电脑处理的数字序列 72 101 108 108 111 32 87 111 114 108 100 33
就可以通过查 ASCII 码表翻译成 Hello World!
。当然,用更「计算机」一些的十六进制表示每一个数,就是:
48 65 6c 6c 6f 20 57 6f 72 6c 64 21
H e l l o W o r l d !
那么,输入并显示一个字符的过程就变成了:
- 你按下键盘的 H 键
- 电脑在内存中保存 0x48
- 电脑在屏幕上显示 0x48 号图形
也就是说,电脑不需要能够「明白」字符,只需要能够用数字「表示」字符。
百家争鸣,被榨干的单字节
随着时代发展,电脑开始在各地使用。以法语、希腊语等为母语的人发现 ASCII 真心坑爹,没有 è、é 没有 α、β、θ、让我们怎么活?
好消息是:计算机终于迎来了以 8 位为 1 字节的时代,而 ASCII 只为 0x00 到 0x7F 规定了对应字符。也就是说,ASCII 只占了低 7 位 0XXX XXXX
,还有 0x80 到 0xFF 这余下的 128 个码位可以让人糟蹋。
这一利好消息的发现让人们大为振奋。
法国人开心地用 0xE8 表示 è,用 0xE9 表示 é;希腊人欢乐地用 0xE1 表示 α,用 0xE2 表示 β,用 0xE8 表示 θ……
E 9 7 1 7 5 6 9 7 6 6 1 7 5
1110 1001 0111 0001 0111 0101 0110 1001 0111 0110 0110 0001 0111 0101
é q u i v a u
所有 0 开头的字节在 ASCII 里找对应字符,所有 1 开头的字节在各自定义的字符集里找对应字符,问题解决了。
传说的巨龙,汉字的秘密
于是电脑来到了中国,但是中文不同于字母文字,如何给数万汉字编码就成了大问题。
一位伟人一拍脑袋:「一个字节只有 0 到 255,但两个字节就有 0 到 65535 啦!《新华字典》也就一万多个字,用两个字节表示一个汉字不就行了嘛~讨厌!」
没错,这就是一种后世所谓的多字节字符集(MBCS)。我们熟悉的 GB2312 和 GBK 使用的都是如下形式的二进制编码:
0XXX XXXX
1XXX XXXX XXXX XXXX
如果一个字节为最高位为 0,那么后续的 7 位表示一个字符(128 个码位)。如果最高位为 1,那么后续 15 位表示一个字符(32768 个码位)。
GB2312 前辈利用了这 32768 个码位中的 7445 个,而后辈 GBK 则利用了 21886 个。
例如二进制数据 48 69 20 CA C0 BD E7
的解析:
1| 4 8 6 9 2 0 C A C 0 B D E 7
2| 0100 1000 0110 1001 0010 0000 1100 1010 1100 0000 1011 1101 1110 0111
3| /100 1000 /110 1001 /010 0000 /100 1010 1100 0000 /011 1101 1110 0111
4| /4 8 /6 9 2 0 /4 A C 0 /3 D E 7
5| H i 世 界
- 字节序列(十六进制)
- 字节序列(二进制)
- 字节序列 --[GBK 解码]--> 码位(二进制)
- 字节序列 --[GBK 解码]--> 码位(十六进制)
- 码位 --[GBK 字符集]--> 字符
那么,几个之前没有讲的概念就比较明白了。
- 字符集:某种字符编码所能表示的所有字符集合。如:H、世
- 码位:为字符集中的每一个字符分配的唯一编号。如:0x48(H)、0x4AC0(世)
- 编码:将码位转换为字节序列。如:0x48→0x48、0x4AC0→0xCAC0
- 解码:将字节序列转换为码位。如:0x48→0x48、0xCACO→0x4AC0
危机!乱码的陷阱
于是,全世界人民都一本满足了,但是这番和谐景象的背后却隐藏着天大的危机。之前,大家都用着自己的字符编码相安无事,但全球化却导致乱码横行。
聪明的你也许已经发现了:之前我们说过,法国人的 è 用 0xE8 表示,而希腊人的 θ 也用 0xE8 表示。有一天,法国人写了封 Email 给希腊人:
Jeux de caractères codés
希腊人收到一看:
Jeux de caractθres codιs
这是毛啊?于是转发给了中国人,中国人打开一看:
Jeux de caract鑢es cod閟
擦,顿时感觉自己没文化了……于是回复:
我看不懂……
希腊人无辜地打开一看:
Ξ?Ώ΄²»Ά?‘‘
这这这……于是转发给法国人,法国人也一头雾水:
ÎÒ¿´²»¶®¡¡
「我还是删了吧,妈妈说不要跟外国人发邮件……」
1| J e u x d e c a r a c t è r e s c o d é s
2| 4a 65 75 78 20 64 65 20 63 61 72 61 63 74 e8 72 65 73 20 63 6f 64 e9 73
3| J e u x d e c a r a c t θ r e s c o d ι s
4| 4a/65/75/78/20/64/65/20/63/61/72/61/63/74/68 72/65/73/20/63/6f/64/68 73
5| J e u x d e c a r a c t 鑢 e s c o d 閟
- 法国人写的文字
- 法国人根据 latin-1 将文字转换为码位、并将码位编码得到实际保存的字节序列
- 希腊人根据 latin/greek 将序列解码得到码位、并将码位转换为字符,得到的文字
- 中国人根据 GBK 解码后得到的码位
- 中国人根据 GBK 字符集将码位转换到的字符
所以,二进制文本数据就相当于密文,而编码和解码如同加密和解密,只有用正确的密钥才能得到明文,也只有用正确的字符编码才能得到码位。然后通过码位在字符集里取得最终的字符。
(历史上,常常将字符集与字符编码等同起来。因为大部分字符集都是 8 位的,编码/解码形同虚设,N 编码后还是 N,可以直接 1:1 映射。例如上面例子中的 latin-1 和 lantin/greek,编码都是相同的 1:1 编码,只是字符集中相同码位对应着不同字符而已。)
大逆转,万国码的光荣
打开浏览器菜单,肯定存在一个叫做「编码」的选项,点开就能看到这世界上至少存在着多少种流行的字符编码。如果浏览器「自动检测」检测得不对,网页就乱码了。索性懒人是社会进步的阶梯。为了免去百家争鸣带来的麻烦,试图让全世界「书同文」的 Unicode 诞生了。
Unicode 标准化了一个字符集,包含了世界上所有的字符,每一个字符都拥有唯一的码位 U+XXXX。(起初的码位是 16 位的,可以容纳 65536 个字符。其后不断扩展,现今已经扩展到了 U+10FFFF。)
Unicode 还提供了几套编码方案,来将 U+XXXX 的码位编码为字节序列,例如:UTF-8 和 UTF-16。
UTF-8
UTF-8 顾名思义,是一套以 8 位为一个编码单位的可变长编码。会将一个码位编码为 1 到 4 个字节。
U+ 0000 ~ U+ 007F: 0XXXXXXX
U+ 0080 ~ U+ 07FF: 110XXXXX 10XXXXXX
U+ 0800 ~ U+ FFFF: 1110XXXX 10XXXXXX 10XXXXXX
U+10000 ~ U+1FFFF: 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX
例如「萌」在 Unicode 中的码位为 U+840C,对应的 UTF-8 编码为 E8 90 8C
:
1| 8 4 0 C
2| 1000 0100 0000 1100
3| 1000 010000 001100
4| 11101000 10010000 10001100
5| E 8 9 0 8 C
- 码位(十六进制)
- 码位(二进制)
- 根据 UTF-8 编码规则将码位分为三段
- 为每一段加上前缀,编码完成(二进制)
- 编码完成(十六进制)
UTF-16
UTF-16 则是一套以 16 位为一个编码单位的可变长编码。会将一个码位编码为 1 到 2 个双字节。编码算法大同小异并不重要,但有一个问题却亟待解决:字节序。
不同的系统,字节序可能不同。小端序系统的 0x1234 实际上是 34 12
,把这个字节序列发给大端序系统,34 12
就会被理解成 0x3412。
于是,Unicode 中引入了一个特殊字符:字节序标(BOM),码位 U+FEFF。用于加在被编码的数据之前,表示编码时的字节序。于是,解码时,首先读出第一个双字节:
- 0xFEFF 当前的字节序和编码时的字节序相同
- 0xFFFE 当前的字节序和编码时的字节序相反
当然,UTF-16 还有派生的 UTF-16LE 和 UTF-16BE,实际上就是按字节序特化的版本。
烫烫烫与屯屯屯
这两个乱码应该是不少 C/C++ 程序员的必经之路吧。
微软 Visual C++ 的 Debug 模式下,会为未初始化的内存填充一些初始值,以便调试。其中,栈上的内存都用 0xCC
初始化、堆上的内存都用 0xCD
初始化。
而如果把 0xCCCC
作为字符输出,在简体中文的 Windows 系统下,就会根据其使用的 GBK 编码将其解释为「烫」字;0xCDCD
则为「屯」。
变巨
如果你用过南极星,说明你已经老了。
在那个万码奔腾的年代,由于早年的《曹操传》和《三国志》等游戏使用的都是 Big5 编码的文本数据,而简体中文 Windows 系统使用的编码不同于 Big5。用「前朝的剑斩本朝的官」就产生了乱码。
「变巨」的 GBK 编码为 B1E4 BEDE
(GB2312 亦然),而在 Big5 编码中这四个字节对应的汉字为「曹操」。
俸俸伲购美病
这是《英雄传说VI 空之轨迹SC》简体中文版中的一句对白,也是最初被玩家讽刺得最惨的地方。
打上官方修正补丁后可以发现原文为「嘿嘿嘿,还好啦。」之所以会产生这样的乱码是因为 GBK 编码:
BA D9 BA D9 BA D9 A3 AC BB B9 BA C3 C0 B2 A1 A3 // 嘿嘿嘿,还好啦。
D9 BA D9 BA D9 A3 AC BB B9 BA C3 C0 B2 A1 // 俸俸伲购美病
缺了第一个字节……
锟斤拷
相对前面几个乱码的直白,这个乱码是很纠结的存在。
Unicode 中定义了一个特殊字符「�」即 U+FFFD
,称作 Replacement Character。用来表示无法显示的字符或是无法解析的数据。
如果一段数据本身是使用 GBK 编码的,那么其中可能有很多部分不符合 UTF-8 编码规则。一个处理 UTF-8 数据的程序得到这段数据后,可以选择将数据中检测到不合 UTF-8 编码规则的部分替换为 UTF-8 编码的 U+FFFD
即 EFBFBD
,这样,就在自动消除编码问题的同时对用户给出了数据编码错误的提示。
经过上面这步处理后,数据中就产生了很多 EFBFBD
的序列,此时如果试图以 GBK 将其解码,那么两个这样的序列就成了「锟斤拷」,即 EFBF BDEF BFBD
。
- «
- 1
- 2
- 3
- 4
- »