极客时间已完结课程限时免费阅读

11 | Unicode:进入多文字支持的世界

11 | Unicode:进入多文字支持的世界-极客时间

11 | Unicode:进入多文字支持的世界

讲述:吴咏炜

时长21:44大小19.85M

你好,我是吴咏炜。
这一讲我们来讲一个新话题,Unicode。我们会从编码的历史谈起,讨论编程中对中文和多语言的支持,然后重点看一下 C++ 中应该如何处理这些问题。

一些历史

ASCII [1] 是一种创立于 1963 年的 7 位编码,用 0 到 127 之间的数值来代表最常用的字符,包含了控制字符(很多在今天已不再使用)、数字、大小写拉丁字母、空格和基本标点。它在编码上具有简单性,字母和数字的编码位置非常容易记忆(相比之下,设计 EBCDIC [2] 的人感觉是脑子进了水,哦不,进了穿孔卡片了;难怪它和 IBM 的那些过时老古董一起已经几乎被人遗忘)。时至今日,ASCII 可以看作是字符编码的基础,主要的编码方式都保持着与 ASCII 的兼容性。
ASCII 里只有基本的拉丁字母,它既没有带变音符的拉丁字母(如 é 和 ä ),也不支持像希腊字母(如 α、β、γ)、西里尔字母(如 Пушкин)这样的其他欧洲文字(也难怪,毕竟它是 American Standard Code for Information Interchange)。很多其他编码方式纷纷应运而生,包括 ISO 646 系列、ISO/IEC 8859 系列等等;大部分编码方式都是头 128 个字符与 ASCII 兼容,后 128 个字符是自己的扩展,总共最多是 256 个字符。每次只有一套方式可以生效,称之为一个代码页(code page)。这种做法,只能适用于文字相近、且字符数不多的国家。比如,下图表示了 ISO-8859-1(也称作 Latin-1)和后面的 Windows 扩展代码页 1252(下图中绿框部分为 Windows 的扩展),就只能适用于西欧国家。
最早的中文字符集标准是 1980 年的国标 GB2312 [3],其中收录了 6763 个常用汉字和 682 个其他符号。我们平时会用到编码 GB2312,其实更正确的名字是 EUC-CN [4],它是一种与 ASCII 兼容的编码方式。它用单字节表示 ASCII 字符而用双字节表示 GB2312 中的字符;由于 GB2312 中本身也含有 ASCII 中包含的字符,在使用中逐渐就形成了“半角”和“全角”的区别。
国标字符集后面又有扩展,这个扩展后的字符集就是 GBK [5],是中文版 Windows 使用的标准编码方式。GB2312 和 GBK 所占用的编码位置可以参看下面的图(由 John M. Długosz 为 Wikipedia 绘制):
图中 GBK/1 和 GBK/2 为 GB2312 中已经定义的区域,其他的则是后面添加的字符,总共定义了两万多个编码点,支持了绝大部分现代汉语中还在使用的字。
Unicode [6] 作为一种统一编码的努力,诞生于八十年代末九十年代初,标准的第一版出版于 1991—1992 年。由于最初发明者的目标放得太低,只期望对活跃使用中的现代文字进行编码,他们认为 16 比特的“宽 ASCII”就够用了。这就导致了早期采纳 Unicode 的组织,特别是微软,在其操作系统和工具链中广泛采用了 16 比特的编码方式。在今天,微软的系统中宽字符类型 wchar_t 仍然是 16 位的,操作系统底层接口大量使用 16 位字符编码的 API,说到 Unicode 编码时仍然指的是 16 位的编码 UTF-16(这一不太正确的名字,跟中文 GBK 编码居然可以被叫做 ANSI 相比,实在是小巫见大巫了)。在微软以外的世界,Unicode 本身不作编码名称用,并且最主流的编码方式并不是 UTF-16,而是和 ASCII 全兼容的 UTF-8。
早期 Unicode 组织的另一个决定是不同语言里的同一个字符使用同一个编码点,来减少总编码点的数量。中日韩三国使用的汉字就这么被统一了:像“将”、“径”、“网”等字,每个字在 Unicode 中只占一个编码点。这对网页的字体选择也造成了不少麻烦,时至今日我们仍然可以看到这个问题 [7]。不过这和我们的主题无关,就不再多费笔墨了。

Unicode 简介

Unicode 在今天已经大大超出了最初的目标。到 Unicode 12.1 为止,Unicode 已经包含了 137,994 个字符,囊括所有主要语言(使用中的和已经不再使用的),并包含了表情符号、数学符号等各种特殊字符。仍然要指出一下,Unicode 字符是根据含义来区分的,而非根据字形。除了前面提到过中日韩汉字没有分开,像斜体(italics)、小大写字母(small caps)等排版效果在 Unicode 里也没有独立的对应。不过,因为 Unicode 里包含了很多数学、物理等自然科学中使用的特殊符号,某些情况下你也可以找到对应的符号,可以用在聊天中耍酷,如 𝒷𝒶𝒹(但不适合严肃的排版)。
Unicode 的编码点是从 0x0 到 0x10FFFF,一共 1,114,112 个位置。一般用“U+”后面跟 16 进制的数值来表示一个 Unicode 字符,如 U+0020 表示空格,U+6C49 表示“汉”,U+1F600 表示“😀”,等等(不足四位的一般写四位)。
Unicode 字符的常见编码方式有:
UTF-32 [8]:32 比特,是编码点的直接映射。
UTF-16 [9]:对于从 U+0000 到 U+FFFF 的字符,使用 16 比特的直接映射;对于大于 U+FFFF 的字符,使用 32 比特的特殊映射关系——在 Unicode 的 16 比特编码点中 0xD800–0xDFFF 是一段空隙,使得这种变长编码成为可能。在一个 UTF-16 的序列中,如果看到内容是 0xD800–0xDBFF,那这就是 32 比特编码的前 16 比特;如果看到内容是 0xDC00–0xDFFF,那这是 32 比特编码的后 16 比特;如果内容在 0xD800–0xDFFF 之外,那就是一个 16 比特的映射。
UTF-8 [10]:1 到 4 字节的变长编码。在一个合法的 UTF-8 的序列中,如果看到一个字节的最高位是 0,那就是一个单字节的 Unicode 字符;如果一个字节的最高两比特是 10,那这是一个 Unicode 字符在编码后的后续字节;否则,这就是一个 Unicode 字符在编码后的首字节,且最高位开始连续 1 的个数表示了这个字符按 UTF-8 的方式编码有几个字节。
在上面三种编码方式里,只有 UTF-8 完全保持了和 ASCII 的兼容性,目前得到了最广泛的使用。在我们下面讲具体编码方式之前,我们先看一下上面提到的三个字符在这三种方式下的编码结果:
UTF-32:U+0020 映射为 0x00000020,U+6C49 映射为 0x00006C49,U+1F600 映射为 0x0001F600。
UTF-16:U+0020 映射为 0x0020,U+6C49 映射为 0x6C49,而 U+1F600 会映射为 0xD83D DE00。
UTF-8:U+0020 映射为 0x20,U+6C49 映射为 0xE6 B1 89,而 U+1F600 会映射为 0xF0 9F 98 80。
Unicode 有好几种(上面还不是全部)不同的编码方式,上面的 16 比特和 32 比特编码方式还有小头党和大头党之争(“汉”按字节读取时是 6C 49 呢,还是 49 6C?);同时,任何一种编码方式还需要跟传统的编码方式容易区分。因此,Unicode 文本文件通常有一个使用 BOM(byte order mark)字符的约定,即字符 U+FEFF [11]。由于 Unicode 不使用 U+FFFE,在文件开头加一个 BOM 即可区分各种不同编码:
如果文件开头是 0x00 00 FE FF,那这是大头在前的 UTF-32 编码;
否则如果文件开头是 0xFF FE 00 00,那这是小头在前的 UTF-32 编码;
否则如果文件开头是 0xFE FF,那这是大头在前的 UTF-16 编码;
否则如果文件开头是 0xFF FE,那这是小头在前的 UTF-16 编码(注意,这条规则和第二条的顺序不能相反);
否则如果文件开头是 0xEF BB BF,那这是 UTF-8 编码;
否则,编码方式使用其他算法来确定。
编辑器可以(有些在配置之后)根据 BOM 字符来自动决定文本文件的编码。比如,我一般在 Vim 中配置 set fileencodings=ucs-bom,utf-8,gbk,latin1。这样,Vim 在读入文件时,会首先检查 BOM 字符,有 BOM 字符按 BOM 字符决定文件编码;否则,试图将文件按 UTF-8 来解码(由于 UTF-8 有格式要求,非 UTF-8 编码的文件通常会导致失败);不行,则试图按 GBK 来解码(失败的概率就很低了);还不行,就把文件当作 Latin1 来处理(永远不会失败)。
在 UTF-8 编码下使用 BOM 字符并非必需,尤其在 Unix 上。但 Windows 上通常会使用 BOM 字符,以方便区分 UTF-8 和传统编码。

C++ 中的 Unicode 字符类型

C++98 中有 charwchar_t 两种不同的字符类型,其中 char 的长度是单字节,而 wchar_t 的长度不确定。在 Windows 上它是双字节,只能代表 UTF-16,而在 Unix 上一般是四字节,可以代表 UTF-32。为了解决这种混乱,目前我们有了下面的改进:
C++11 引入了 char16_tchar32_t 两个独立的字符类型(不是类型别名),分别代表 UTF-16 和 UTF-32。
C++20 将引入 char8_t 类型,进一步区分了可能使用传统编码的窄字符类型和 UTF-8 字符类型。
除了 stringwstring,我们也相应地有了 u16stringu32string(和将来的 u8string)。
除了传统的窄字符 / 字符串字面量(如 "hi")和宽字符 / 字符串字面量(如 L"hi"),引入了新的 UTF-8、UTF-16 和 UTF-32 字面量,分别形如 u8"hi"u"hi"U"hi"
为了确保非 ASCII 字符在源代码中可以简单地输入,引入了新的 Unicode 换码序列。比如,我们前面说到的三个字符可以这样表达成一个 UTF-32 字符串字面量:U" \u6C49\U0001F600"。要生成 UTF-16 或 UTF-8 字符串字面量只需要更改前缀即可。
使用这些新的字符(串)类型,我们可以用下面的代码表达出 UTF-32 和其他两种 UTF 编码间是如何转换的:
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
const char32_t unicode_max =
0x10FFFF;
void to_utf_16(char32_t ch,
u16string& result)
{
if (ch > unicode_max) {
throw runtime_error(
"invalid code point");
}
if (ch < 0x10000) {
result += char16_t(ch);
} else {
char16_t first =
0xD800 |
((ch - 0x10000) >> 10);
char16_t second =
0xDC00 | (ch & 0x3FF);
result += first;
result += second;
}
}
void to_utf_8(char32_t ch,
string& result)
{
if (ch > unicode_max) {
throw runtime_error(
"invalid code point");
}
if (ch < 0x80) {
result += ch;
} else if (ch < 0x800) {
result += 0xC0 | (ch >> 6);
result += 0x80 | (ch & 0x3F);
} else if (ch < 0x10000) {
result += 0xE0 | (ch >> 12);
result +=
0x80 | ((ch >> 6) & 0x3F);
result += 0x80 | (ch & 0x3F);
} else {
result += 0xF0 | (ch >> 18);
result +=
0x80 | ((ch >> 12) & 0x3F);
result +=
0x80 | ((ch >> 6) & 0x3F);
result += 0x80 | (ch & 0x3F);
}
}
int main()
{
char32_t str[] =
U" \u6C49\U0001F600";
u16string u16str;
string u8str;
for (auto ch : str) {
if (ch == 0) {
break;
}
to_utf_16(ch, u16str);
to_utf_8(ch, u8str);
}
cout << hex << setfill('0');
for (char16_t ch : u16str) {
cout << setw(4) << unsigned(ch)
<< ' ';
}
cout << endl;
for (unsigned char ch : u8str) {
cout << setw(2) << unsigned(ch)
<< ' ';
}
cout << endl;
}
输出结果是:
0020 6c49 d83d de00
20 e6 b1 89 f0 9f 98 80

平台区别

下面我们看一下在两个主流的平台上一般是如何处理 Unicode 编码问题的。

Unix

现代 Unix 系统,包括 Linux 和 macOS 在内,已经全面转向了 UTF-8。这样的系统中一般直接使用 char[]string 来代表 UTF-8 字符串,包括输入、输出和文件名,非常简单。不过,由于一个字符单位不能代表一个完整的 Unicode 字符,在需要真正进行文字处理的场合转换到 UTF-32 往往会更简单。在以前及需要和 C 兼容的场合,会使用 wchar_tuint32_t 或某个等价的类型别名;在新的纯 C++ 代码里,就没有理由不使用 char32_tu32string 了。
Unix 下输出宽字符串需要使用 wcout(这点和 Windows 相同),并且需要进行区域设置,如下所示:
std::locale::global(
std::locale(""));
std::wcout.imbue(std::locale());
由于没有什么额外好处,反而可能在某些环境因为区域设置失败而引发问题,Unix 平台下一般只用 cout,不用 wcout

Windows

Windows 由于历史原因和保留向后兼容性的需要(Windows 为了向后兼容性已经到了大规模放弃优雅的程度了),一直用 char 表示传统编码(如,英文 Windows 上是 Windows-1252,简体中文 Windows 上是 GBK),用 wchar_t 表示 UTF-16。由于传统编码一次只有一种、且需要重启才能生效,要得到好的多语言支持,在和操作系统交互时必须使用 UTF-16。
对于纯 Windows 编程,全面使用宽字符(串)是最简单的处理方式。当然,源代码和文本很少用 UTF-16 存储,通常还是 UTF-8(除非是纯 ASCII,否则必须加入 BOM 字符来和传统编码相区分)。这时可能会有一个小小的令人惊讶的地方:微软的编译器会把源代码里窄字符串字面量中的非 ASCII 字符转换成传统编码。换句话说,同样的源代码在不同编码的 Windows 下编译可能会产生不同的结果!如果你希望保留 UTF-8 序列的话,就应该使用 UTF-8 字面量(并在将来使用 char8_t 字符类型)。
#include <stdio.h>
template <typename T>
void dump(const T& str)
{
for (char ch : str) {
printf(
"%.2x ",
static_cast<unsigned char>(ch));
}
putchar('\n');
}
int main()
{
char str[] = "你好";
char u8str[] = u8"你好";
dump(str);
dump(u8str);
}
下面展示的是以上代码在 Windows 下系统传统编码设置为简体中文时的编译、运行结果:
c4 e3 ba c3 00
e4 bd a0 e5 a5 bd 00
Windows 下的 wcout 主要用在配合宽字符的输出,此外没什么大用处。原因一样,只有进行了正确的区域设置,才能输出跟该区域相匹配的宽字符串(不匹配的字符将导致后续输出全部消失!)。如果要输出中文,得写 setlocale(LC_ALL, "Chinese_China.936");,这显然就让“统一码”输出失去意义了。
但是(还是有个“但是”),如果你只用 wcout,不用 cout 或任何使用窄字符输出到 stdout 的函数(如 puts),这时倒有个还不错的解决方案,可以在终端输出多语言。我也是偶然才发现这一用法,并且没有在微软的网站上找到清晰的文档……代码如下所示:
#include <fcntl.h>
#include <io.h>
#include <iostream>
int main()
{
_setmode(_fileno(stdout),
_O_WTEXT);
std::wcout
<< L"中文 Español Français\n";
std::wcout
<< "Narrow characters are "
"also OK on wcout\n";
// but not on cout...
}
由于窄字符在大部分 Windows 系统上只支持传统编码,要打开一个当前编码不支持的文件名称,就必需使用宽字符的文件名。微软的 fstream 系列类及其 open 成员函数都支持 const wchar_t* 类型的文件名,这是 C++ 标准里所没有的。

统一化处理

要想写出跨平台的处理字符串的代码,我们一般考虑两种方式之一:
源代码级兼容,但内码不同
源代码和内码都完全兼容
微软推荐的方式一般是前者。做 Windows 开发的人很多都知道 tchar.h 和 _T 宏,它们就起着类似的作用(虽然目的不同)。根据预定义宏的不同,系统会在同一套代码下选择不同的编码方式及对应的函数。拿一个最小的例子来说:
#include <stdio.h>
#include <tchar.h>
int _tmain(int argc, TCHAR* argv[])
{
_putts(_T("Hello world!\n"));
}
如果用缺省的命令行参数进行编译,上面的代码相当于:
#include <stdio.h>
int main(int argc, char* argv[])
{
puts("Hello world!\n");
}
而如果在命令行上加上了 /D_UNICODE,那代码则相当于:
#include <stdio.h>
int wmain(int argc, wchar_t* argv[])
{
_putws(L"Hello world!\n");
}
当然,这个代码还是只能在 Windows 上用,并且仍然不漂亮(所有的字符和字符串字面量都得套上 _T)。后者无解,前者则可以找到替代方案(甚至自己写也不复杂)。C++ REST SDK 中就提供了类似的封装,可以跨平台地开发网络应用。但可以说,这种方式是一种主要照顾 Windows 的开发方式。
相应的,对 Unix 开发者而言更自然的方式是全面使用 UTF-8,仅在跟操作系统、文件系统打交道时把字符串转换成需要的编码。利用临时对象的生命周期,我们可以像下面这样写帮助函数和宏。
utf8_to_native.hpp:
#ifndef UTF8_TO_NATIVE_HPP
#define UTF8_TO_NATIVE_HPP
#include <string>
#if defined(_WIN32) || \
defined(_UNICODE)
std::wstring utf8_to_wstring(
const char* str);
std::wstring utf8_to_wstring(
const std::string& str);
#define NATIVE_STR(s) \
utf8_to_wstring(s).c_str()
#else
inline const char*
to_c_str(const char* str)
{
return str;
}
inline const char*
to_c_str(const std::string& str)
{
return str.c_str();
}
#define NATIVE_STR(s) \
to_c_str(s)
#endif
#endif // UTF8_TO_NATIVE_HPP
utf8_to_native.cpp:
#include "utf8_to_native.hpp"
#if defined(_WIN32) || \
defined(_UNICODE)
#include <windows.h>
#include <system_error>
namespace {
void throw_system_error(
const char* reason)
{
std::string msg(reason);
msg += " failed";
std::error_code ec(
GetLastError(),
std::system_category());
throw std::system_error(ec, msg);
}
} /* unnamed namespace */
std::wstring utf8_to_wstring(
const char* str)
{
int len = MultiByteToWideChar(
CP_UTF8, 0, str, -1,
nullptr, 0);
if (len == 0) {
throw_system_error(
"utf8_to_wstring");
}
std::wstring result(len - 1,
L'\0');
if (MultiByteToWideChar(
CP_UTF8, 0, str, -1,
result.data(), len) == 0) {
throw_system_error(
"utf8_to_wstring");
}
return result;
}
std::wstring utf8_to_wstring(
const std::string& str)
{
return utf8_to_wstring(
str.c_str());
}
#endif
在头文件里,定义了在 Windows 下会做 UTF-8 到 UTF-16 的转换;在其他环境下则不真正做转换,而是不管提供的是字符指针还是 string 都会转换成字符指针。在 Windows 下每次调用 NATIVE_STR 会生成一个临时对象,当前语句执行结束后这个临时对象会自动销毁。
使用该功能的代码是这样的:
#include <fstream>
#include "utf8_to_native.hpp"
int main()
{
using namespace std;
const char filename[] =
u8"测试.txt";
ifstream ifs(
NATIVE_STR(filename));
// 对 ifs 进行操作
}
上面这样的代码可以同时适用于现代 Unix 和现代 Windows(任何语言设置下),用来读取名为“测试.txt”的文件。

编程支持

结束之前,我们快速介绍一下其他的一些支持 Unicode 及其转换的 API。

Windows API

上一节的代码在 Windows 下用到了 MultiByteToWideChar [12],从某个编码转到 UTF-16。Windows 也提供了反向的 WideCharToMultiByte [13],从 UTF-16 转到某个编码。从上面可以看到,C 接口用起来并不方便,可以考虑自己封装一下。

iconv

Unix 下最常用的底层编码转换接口是 iconv [14],提供 iconv_openiconv_closeiconv 三个函数。这同样是 C 接口,实践中应该封装一下。

ICU4C

ICU [15] 是一个完整的 Unicode 支持库,提供大量的方法,ICU4C 是其 C/C++ 的版本。ICU 有专门的字符串类型,内码是 UTF-16,但可以直接用于 IO streams 的输出。下面的程序应该在所有平台上都有同样的输出(但在 Windows 上要求当前系统传统编码能支持待输出的字符):
#include <iostream>
#include <string>
#include <unicode/unistr.h>
#include <unicode/ustream.h>
using namespace std;
using icu::UnicodeString;
int main()
{
auto str = UnicodeString::fromUTF8(
u8"你好");
cout << str << endl;
string u8str;
str.toUTF8String(u8str);
cout << "In UTF-8 it is "
<< u8str.size() << " bytes"
<< endl;
}

codecvt

C++11 曾经引入了一个头文件 <codecvt> [16] 用作 UTF 编码间的转换,但很遗憾,那个头文件目前已因为存在安全性和易用性问题被宣告放弃(deprecated)[17]。<locale> 中有另外一个 codecvt 模板 [18],本身接口不那么好用,而且到 C++20 还会发生变化,这儿也不详细介绍了。有兴趣的话可以直接看参考资料。

内容小结

本讲我们讨论了 Unicode,以及 C++ 中对 Unicode 的支持。我们也讨论了在两大主流桌面平台上关于 Unicode 编码支持的一些惯用法。希望你在本讲之后,能清楚地知道 Unicode 和各种 UTF 编码是怎么回事。

课后思考

请思考一下:
为什么说 UTF-32 处理会比较简单?
你知道什么情况下 UTF-32 也并不那么简单吗?
哪种 UTF 编码方式空间存储效率比较高?
欢迎留言一起讨论一下。

参考资料

[1] Wikipedia, “ASCII”. https://en.wikipedia.org/wiki/ASCII
[2] Wikipedia, “EBCDIC”. https://en.wikipedia.org/wiki/EBCDIC
[3] Wikipedia, “GB 2312”. https://en.wikipedia.org/wiki/GB_2312
[3a] 维基百科, “GB 2312”. https://zh.wikipedia.org/zh-cn/GB_2312
[4a] 维基百科, “EUC-CN”. https://zh.wikipedia.org/zh-cn/EUC#EUC-CN
[5a] 维基百科, “汉字内码扩展规范”. https://zh.wikipedia.org/zh-cn/ 汉字内码扩展规范
[6] Wikipedia, “Unicode”. https://en.wikipedia.org/wiki/Unicode
[6a] 维基百科, “Unicode”. https://zh.wikipedia.org/zh-cn/Unicode
[7] 吴咏炜, “Specify LANG in a UTF-8 web page”. http://wyw.dcweb.cn/lang_utf8.htm
[8] Wikipedia, “UTF-32”. https://en.wikipedia.org/wiki/UTF-32
[9] Wikipedia, “UTF-16”. https://en.wikipedia.org/wiki/UTF-16
[10] Wikipedia, “UTF-8”. https://en.wikipedia.org/wiki/UTF-8
[11] Wikipedia, “Byte order mark”. https://en.wikipedia.org/wiki/Byte_order_mark
[11a] 维基百科, “字节顺序标记”. https://zh.wikipedia.org/zh-cn/ 位元組順序記號
[14] Wikipedia, “iconv”. https://en.wikipedia.org/wiki/Iconv
[15] ICU Technical Committee, ICU—International Components for Unicode. http://site.icu-project.org/
[16] cppreference.com, “Standard library header <codecvt>”. https://en.cppreference.com/w/cpp/header/codecvt
[17] Alisdair Meredith, “Deprecating <codecvt>”. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0618r0.html
[18] cppreference.com, “std::codecvt”. https://en.cppreference.com/w/cpp/locale/codecvt
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 1

提建议

上一篇
10 | 到底应不应该返回对象?
下一篇
12 | 编译期多态:泛型编程和模板入门
 写留言

精选留言(20)

  • c1rew
    2020-02-14
    http://c1rew.github.io/2017/03/02/字符集和字符编码/ 很早之前写的一篇文章,供大家参考

    作者回复: 既然你贴在这里,我就指出一下文中的一些小问题吧。 - 时间线不对。GB18030(甚至 GBK)是在 Unicode 之后出现的。GB18030 的四字节编码是基于 Unicode 的。 - 有一处 UCS 写成了 USC。 - “目前的用于实用的 Unicode 版本对应于 UCS-2,即使用16位来表示一个Unicode字符”:对于 2017 年的文章,这么写是不合适的。不说很多汉字在 BMP 之外,表情字符大家总要用的吧? - UCS-4 虽然最初被定义为 31 位,但从 2003 年开始就被规定为范围是从 0 到 0x10FFFF 了。换句话说,只用 21 位。 - UTF-16 和 UCS-2 是不同的。UTF-16 有 16 位和 32 位两种情况,而 UCS-2 只有 16位一种。 - 虽然 U+FEFF 在文件中间出现应当被当作 zero width no-break space,但在文件开头,它只被叫做 BOM。此外,zero width no-break space 这一用法从 Unicode 3.2 开始就被反对使用(deprecate)了。目前,U+FEFF 就只有 BOM 一种意思。 可能像是挑刺,抱歉了。但我不希望别人对这些事实有错误的理解。

    共 3 条评论
    35
  • 皮皮侠
    2020-02-27
    问个题外问题,Qt里中文的处理总是有问题,老师怎么看,有什么终极解决办法呢?

    作者回复: Qt 不熟。不过,也就不外乎这几种情况了。搞清楚源代码的编码、字符串的内码、输入输出要求的编码,作合适的转换,应当可以解决。 不要忘了在 Windows 上编译 UTF-8 源码的文件,一定要有 BOM 字符。这也是很多错误的来源。网上搜 Qt 的编码问题,似乎很多问题就是跟源代码的编码相关的。

    共 3 条评论
    2
  • Milittle
    2020-02-26
    1. 有一个问题想问下老师,传统编码是不是相对于UTF编码之外的那些编码方式。 2. 再就是对于我们编程中使用的不同编码方式的字面量来说,最后生成的字节数对应是不同的么?是这个意思么? 3. 还有就是u8string u16string u32string 默认是一个元素所占的字节数分辨是 1 2 4么? 4. 老师文章中提出一个UTF-16在0XD800-0XDFFF之间没有用作unicode code point。这是历史原因决定么 然后就是怎么可以找到unicode code point所对应的文字关系表? 从UTF-32转UTF-8和UTF-16老师给出例子,这个可以当作一个算法来记忆。 5. 就是操作系统的编码方式是最终决定你的字符串显示在终端的样子么?我举一个例子,加入你程序中一个字符串是UTF-8,但是你的操作系统使用的是GBK,那么显示出来的样子应该会乱码吧? 我的理解也就是程序什么编码方式,操作系统也应该是什么编码方式,才能输出正确的文字? 以上问题有些可能是我没有理解对 王老师给予指导 谢谢~
    展开

    作者回复: 1. “传统编码”在Windows上有特别含义。对于简体中文Windows,就是GBK编码了。 2. 不同的编码方式,字面量的值会不同,这是最重要的。字节数可能一样,可能不一样。比如繁体中文的汉字,在GBK里和在Big5里就不同。编码不正确,字符可能出不来,比如简体中文在Big5里就都没有。 3. 是,每个元素分别是8、16、32比特。 4. 0XD800-0XDFFF正好没有用吧,然后就用于UTF-16的扩展了。转换算法我描述得不算太详细,因为这个资料真的是很多的。 完整的Unicode文字表网上也可以找到,如: https://unicode.org/charts/ https://unicode-table.com/ 5. 编码是可以转换的。如果不加转换输出就会乱码。 从操作系统来说,Windows内部使用UTF-16,但如果代码中使用char/string,Windows接口一般当成“传统编码”来转换处理。Unix系统里一般全面使用UTF-8。

    1
  • 天天
    2020-02-05
    1. utf32字节数固定支持随机访问字符,其他2个则需要遍历前面的字节,经过计算才能解出第N个字符 3. 要看具体存储的是啥字符, 英文utf8省空间, 中文utf16

    作者回复: OK,1、3 的回答都没什么问题。👍

    1
  • 三味
    2019-12-20
    这两天写玩具代码, 环境就是win10, vs2017, 写控制台. 我源代码是utf8无bom格式. 什么都好, 就是用fgets获取输入, 无法获取中文字符串... 折腾好久... 最后还是都改成了ansi... ansi这个诡异的名字确实值得吐槽啊...明明就是GBK... 字符编码真是太麻烦了...

    作者回复: ANSI 是指 Windows 的传统系统编码。源代码中如果含中文,必须用 UTF-8 + BOM 保存(专栏中说过的)。

    1
  • Geek_0705cc
    2023-01-25 来自河南
    大头在前、小头在前是什么意思呢?不如这样说的清楚。大端模式指的是:高位字节存储在内存的低地址处;

    作者回复: 大小头(端)不是我要说明的重点,因此我只是用例子来说明。 另外,你的表述方式是有问题的,因为文件没有高地址和低地址。

  • chang
    2021-06-03
    文中“由于 Unicode 不使用 U+FFFE,在文件开头加一个 BOM 即可区分各种不同编码:”,应该是U+FEFF吧

    作者回复: 没写错,是“不使用” U+FFFE。再读一遍。

    共 2 条评论
  • 蓦然回首
    2021-01-27
    上面这几种之间相互有影响吗?到底之间关系是如何?平时我们开发过程中,应该怎么理解这几者之间的关系?感觉这方面一直没弄清楚

    作者回复: 参见前面的回答。 互相基本没影响。但是,如果你要把程序里硬编码的字符串跟输入一起混合输出,那就必须保证编码相同了。如果输出到终端,那还要保证终端的设置与之相匹配——Linux上通常为UTF-8,(中文)Windows上通常为GBK。

  • 蓦然回首
    2021-01-27
    老师,请教个问题,我们平时开发,好像编码有几种方面的编码问题: 1.源码文件本身的编码,IDE或者编辑器设置的编码等 2.程序运行时刻当中程序本身的编码, 3.程序运行过程中所打开文件的编码 4.接收或者发送网络数据的不同编码 在编码方面一直这方面有些困惑,希望能解答一下!
    展开

    作者回复: 1. 源代码的编码通常影响不大,主要影响是字符串的编码结果。 2. 基本上没有程序本身编码的概念。但是,运行时有缺省编码的概念,影响对输入输出的解释。 3、4. 通常由程序自行解释,除非你使用了运行时库提供的到宽字符的转换函数。

    共 4 条评论
  • 木瓜777
    2020-09-23
    你好,如果c++通过socket专递带 中文的字符串 到 web前端,请问是不是需要 转码啊? 转码的话,是不是 从 utf8 到 gb2312? 目前后端是linux系统

    作者回复: 这个没法说死,取决于你后端中文是用什么编码,和前端期待什么编码。最常见的情况,两者都是 UTF-8,不需要转。

    共 2 条评论
  • Geek_68d3d2
    2020-04-19
    1 UTF-32是定长编码,而且能表示的字符比较多,所以处理起来比较容易且少了这种字符表示不了的尴尬局面。 2 UTF-32在需要转换成其他编码的时候可能比较麻烦,比如转成UTF-8还需要计算本字符实际占几个字节,还要更改前几个比特位 3 肯定是UTF-8空间利用率高啊
    展开

    作者回复: 1对。2、3可以看一下参考答案。

  • Initiative Thinker
    2020-04-18
    老师问个问题,WIndows下命令行输出的是由ANSI编码还是UTF-16编码? ANSI提出就是为了兼容非英文字符吗? 对老师的例子做了一些修改 #include <fcntl.h> #include <io.h> #include <iostream> int main() { /*_setmode(_fileno(stdout), _O_WTEXT);*/ std::cout << "中文Español Français\n"; std::cout << "Narrow characters are " "also OK on wcout\n"; // but not on cout... } 输出法语部分直接是?,这个?的产生原因是由于窄字符输出无法识别造成的吗?
    展开

    作者回复: Windows 下把窄字符集当成当前的 ANSI(传统字符编码)字符集。如果你的输入是 UTF-8(需要带 BOM 字符),那编译器会把当前字符集(你的情况下应该是 GBK)无法表达的字符全部变成问号。换句话说,在编译的时候,西班牙语和法语的两个特殊符号已经变成问号了。

  • zKerry
    2020-02-25
    干货满满

    作者回复: 😇

  • 清风静婷
    2020-02-24
    问题2-不知是不是因为. UTF-32 如果有字节全部为0,转换成其他会认为是结束符

    作者回复: 不是这种情况……急着想知道可以去瞟一眼答案……

  • 贾陆华
    2020-02-13
    2. UTF-32不兼容ASCII编码,会导致处理之前的文件和系统接口都会转换一次,有些麻烦,这就是为什么采用兼容的UTF-8编码的原因之一

    作者回复: 这和我考虑的角度不一样。虽然不能算错,但这是 UTF-16 和 UTF-32 共有的问题了。 此外,你假设了操作系统用了 UTF-8。并不一定如此。Windows 内部使用 UTF-16。理论上可能存在用 UTF-32 的操作系统的。

    共 2 条评论
  • 晴朗
    2019-12-25
    找到一个对字符编码讲解很详细的系列文章 https://www.cnblogs.com/benbenalin/tag/Unicode/
  • 泰伦卢
    2019-12-21
    唉,一直对这个字符编码不太感冒

    作者回复: 理解透了,也没有多复杂。比编程简单多了。

  • _呱太_
    2019-12-21
    老师好!能不能提前透露一下单元测试库是用的哪个库啊,最近在看 TDD,想上手一下,感觉现在最缺的也是测试方面的能力。

    作者回复: 我考虑讲的是 Boost.Test 和 Catch2。

  • LiKui
    2019-12-20
    通过BOM判断文件编码方式: 如果文件开头是0x0000 FEFF,则是UTF-32 BE方式编码 如果文件开头是0xFFFE 0000,则是UTF-32 LE方式编码 如果文件开头是0xFFFE,则是UTF-16 LE的方式编码 如果文件开头是0xFEFF,则是UTF-16 BE的方式编码 如果文件开头是0xEF BB BF,则是UTF-8方式编码 否则编码方式使用其它算法确定
    展开
  • chado
    2019-12-20
    1.每个字符的字节数固定,可能需要判断的就是一个大小端的问题 2.有些表情符号(如emoji),会使用两个字符来产生,增加了可见字符的判断 3.utf-8是最省空间的编码方式,每个编码只用到需要最少的字节数

    作者回复: 1. 部分正确。处理过程中(读入文件后)连大小端都不会考虑的。 2. 不是。Emoji 在 UTF-32 里仍然是一个字符。 3. 不对。再想想。

    共 3 条评论