C++11 Unicode string literals 从入门到精通

一、引言
在当今全球化的时代,软件开发需要支持各种语言和字符集。Unicode 作为一种通用的字符编码标准,为解决多语言文本处理提供了有效的方案。C++11 引入了对 Unicode 的更好支持,特别是 Unicode string literals,使得开发者能够更方便地处理不同编码的字符串。本文将带领小白从入门到精通 C++11 Unicode string literals。
二、Unicode 基础
2.1 Unicode 简介
Unicode 是计算机领域的一项行业标准,它对世界上绝大部分的文字进行整理和统一编码。Unicode 的编码空间可以划分为 17 个平面(plane),每个平面包含 2 的 16 次方(65536)个码位。17 个平面的码位可表示为从 U+0000 到 U+10FFFF,共计 1114112 个码位。第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP),或称第零平面(Plane 0),其他平面称为辅助平面(Supplementary Planes)。基本多语言平面内,从 U+D800 到 U+DFFF 之间的码位区段是永久保留不映射到 Unicode 字符的,所以有效码位为 1112064 个。
2.2 Unicode 编码方式
Unicode 的实现方式有多种,其中最流行的是 UTF - 8、UTF - 16、UCS2 和 UCS4/UTF - 32 等,细分的话还有大小端的区别。
- UTF - 8(8 - bit Unicode Transformation Format):一种变长编码,对于一个 Unicode 的字符被编码成 1 至 4 个字节。绝大部分的中文用三个字节编码,部分中文用四个字节编码。优点是向后兼容 ASCII 编码,没有字节序(大小端)的问题,适合网络传输,存储英文和拉丁文等字符非常节省存储空间;缺点是变长编码不利于文本处理,对于 CJK 等文字比较浪费存储空间。
- UTF - 16(16 - bit Unicode Transformation Format):也是一种变长编码,对于一个 Unicode 字符被编码成 1 至 2 个码元,每个码元为 16 位。在基本多语言平面内的码位 UTF - 16 编码使用 1 个码元且其值与 Unicode 是相等的;在辅助平面内的码位在 UTF - 16 中被编码为一对 16bit 的码元(即 32bit,4 字节),称作代理对(surrogate pair)。优点是绝大部分的文字都可以用两个字节编码,对于 CJK 文字比较节省空间,文本处理比 UTF - 8 方便得多;缺点是存储和传输需要考虑字节序的问题,不兼容 ASCII。
- UCS2(2 - byte Universal Character Set):一种定长编码,编码范围为 0x0000 - 0xFFFF。在基本多语言平面内与 UTF - 16 是等价的,但没有类似于 UTF - 16 中代理对的概念,所以对于超出其编码范围的字符会识别成两个字符。因为是定长编码,所以文本处理很方便,但不能表示全部的 Unicode 字符。
- UCS4/UTF - 32(32 - bit Unicode Transformation Format):一种定长编码,使用 1 个 32bit 的码元,其值与 Unicode 编码值相等。优点是编码定长,容易处理;缺点是占用存储空间较大。
三、C++11 对 Unicode 的支持
3.1 新的字符类型
C++11 引入了两种新的字符类型:char16_t 和 char32_t,它们各自被设计用来存储 UTF - 16 以及 UTF - 32 的字符。char16_t 为 16 位,char32_t 为 32 位。而对于 UTF - 8 编码的 Unicode 字符,C++11 仍然使用 8 位的 char 类型数组来表示。
示例代码如下:
#include <iostream>
int main() {
char16_t c16 = u'\u4f60'; // 存储 UTF - 16 编码的字符 '你'
char32_t c32 = U'\U00004f60'; // 存储 UTF - 32 编码的字符 '你'
std::cout << "char16_t size: " << sizeof(c16) << std::endl;
std::cout << "char32_t size: " << sizeof(c32) << std::endl;
return 0;
}
3.2 新的字符串前缀
C++11 新增了三种前缀来定义不同编码的字符或字符串:
u8:表示 UTF - 8 编码。u:表示 UTF - 16 编码。U:表示 UTF - 32 编码。
加上之前 C++98 中使用的前缀 L 表示 wchar_t 类型的宽字符或字符串,以及普通字符串字面常量,C++ 中共有 5 种定义字符或字符串的方式。
示例代码如下:
#include <iostream>
int main() {
const char* s0 = "Hello"; // 普通字符串
const wchar_t* s1 = L"Hello"; // 宽字符串
const char16_t* s2 = u"Hello"; // UTF - 16 字符串
const char32_t* s3 = U"Hello"; // UTF - 32 字符串
const char* s4 = u8"你好"; // UTF - 8 字符串
return 0;
}
四、Unicode string literals 的使用
4.1 基本使用
可以直接使用新的前缀来创建不同编码的字符串字面量。
#include <iostream>
int main() {
const char* utf8_str = u8"这是一个 UTF - 8 字符串";
const char16_t* utf16_str = u"这是一个 UTF - 16 字符串";
const char32_t* utf32_str = U"这是一个 UTF - 32 字符串";
std::cout << "UTF - 8 字符串: " << utf8_str << std::endl;
// 对于 UTF - 16 和 UTF - 32 字符串,需要使用相应的输出方式,这里省略
return 0;
}
4.2 在字符串内插入 Unicode codepoints
当创建 Unicode 字符串字面值时,可以直接在字符串内插入 Unicode codepoints。C++11 提供了以下的语法:在 \u 之后跟 16 个比特的十六进制数值(不需要 0x 前缀),代表一个 16 位的 Unicode codepoint;如果要输入 32 位的 codepoint,使用 \U 和 32 个比特的十六进制数值。
示例代码如下:
#include <iostream>
int main() {
const char* utf8_str = u8"这是一个 Unicode 字符: \u2018";
const char16_t* utf16_str = u"这是一个更大的 Unicode 字符: \u2018";
const char32_t* utf32_str = U"这是一个 Unicode 字符: \U00002018";
std::cout << "UTF - 8 字符串: " << utf8_str << std::endl;
// 对于 UTF - 16 和 UTF - 32 字符串,需要使用相应的输出方式,这里省略
return 0;
}
4.3 原生字符串与 Unicode 字符串结合
C++11 还引入了原生字符串字面量,即“所见即所得”,不需要在字符串中添加转义字符或其他的格式控制字符调整字符串的格式。原生字符串可以与 Unicode 字符串结合使用,定义 UTF - 8、UTF - 16 和 UTF - 32 的原生字符串。
语法格式如下:
u8R"delimiter(字符串)delimiter":UTF - 8 原生字符串。uR"delimiter(字符串)delimiter":UTF - 16 原生字符串。UR"delimiter(字符串)delimiter":UTF - 32 原生字符串。
示例代码如下:
#include <iostream>
int main() {
const char* utf8_raw_str = u8R"XXX(I'm a "raw UTF - 8" string.)XXX";
const char16_t* utf16_raw_str = uR"*@(This is a "raw UTF - 16" string.)*@";
const char32_t* utf32_raw_str = UR"(This is a "raw UTF - 32" string.)";
std::cout << "UTF - 8 原生字符串: " << utf8_raw_str << std::endl;
// 对于 UTF - 16 和 UTF - 32 原生字符串,需要使用相应的输出方式,这里省略
return 0;
}
五、影响字符串处理的因素
在使用不同方式定义不同编码的字符串时,需要注意影响字符串处理和显示的几个因素:编辑器、编译器和输出环境。
5.1 编辑器
代码编辑器采用何种编码方式决定了字符串最初的编码。比如编辑器如果采用 GBK,那么代码文件中的所有字符都是以 GBK 编码存储。当编译器处理字符串时,可以通过前缀来判断字符串的编码类型,如果目标编码与原编码不同,则编译器会进行转换。
示例代码及问题分析:
// 代码文件为 GBK 编码
#include <iomanip>
#include <iostream>
using namespace std;
int main() {
const char* sTest = u8"你好";
for (int i = 0; sTest[i] != 0; ++i) {
cout << setiosflags(ios::uppercase) << hex << (uint32_t)(uint8_t)sTest[i] << " ";
}
return 0;
}
// 编译选项:g++ -std=c++0x -finput-charset=utf - 8 test.cpp
如果使用上述编译选项,编译器会认为代码文件是 UTF - 8 编码,但实际上是 GBK 编码,可能会导致编译器出现错误的认知,输出的码值可能是 GBK 的码值。正确的做法是使用 -finput-charset=gbk,让编译器按照 GBK 编码去处理代码文件。
5.2 编译器
编译器负责将代码文件中的字符串按照指定的编码进行处理。在 GCC 中,可以使用 -finput-charset=charset 设置输入字符集,用于将输入文件的字符集翻译为 GCC 使用的源字符集;使用 -fexec-charset=charset 设置执行字符集;使用 -fwide-exec-charset=charset 设置宽执行字符集。
5.3 输出环境
字符串的正确显示依赖于输出环境。C++ 输出流对象 cout 能够保证的是将数据以二进制输出到输出设备,但输出设备(比如 Linux Shell 或者 Windows console)是否能够支持特定的编码类型的输出,则取决于输出环境。比如 Linux 虚拟终端 XShell,配置终端编码类型为 GBK,则无法显示 UTF - 8 编码的字符串。
六、Unicode 编码转换
C++11 在标准库中增加了一些 Unicode 编码转换的函数,开发人员可以使用这些函数来完成各种 Unicode 编码间的转换。
6.1 多字节字符与 UTF - 16 编码的转换
#include <iostream>
#include <cwchar>
#include <clocale>
#include <cstdlib>
#include <string>
int main() {
std::setlocale(LC_ALL, "");
const char* mbstr = "你好";
char16_t wc[10];
std::mbstate_t state = std::mbstate_t();
std::size_t len = std::mbrtoc16(wc, mbstr, sizeof(wc), &state);
if (len != static_cast<std::size_t>(-1)) {
std::cout << "转换成功" << std::endl;
}
return 0;
}
6.2 UTF - 16 字符与多字节字符的转换
#include <iostream>
#include <cwchar>
#include <clocale>
#include <cstdlib>
#include <string>
int main() {
std::setlocale(LC_ALL, "");
char16_t wc = u'你';
char mb[10];
std::mbstate_t state = std::mbstate_t();
std::size_t len = std::c16rtomb(mb, wc, &state);
if (len != static_cast<std::size_t>(-1)) {
std::cout << "转换成功" << std::endl;
}
return 0;
}
6.3 多字节字符与 UTF - 32 编码的转换
#include <iostream>
#include <cwchar>
#include <clocale>
#include <cstdlib>
#include <string>
int main() {
std::setlocale(LC_ALL, "");
const char* mbstr = "你好";
char32_t wc[10];
std::mbstate_t state = std::mbstate_t();
std::size_t len = std::mbrtoc32(wc, mbstr, sizeof(wc), &state);
if (len != static_cast<std::size_t>(-1)) {
std::cout << "转换成功" << std::endl;
}
return 0;
}
七、使用第三方库处理 Unicode 字符串
在实际项目中,使用第三方库如 ICU(International Components for Unicode)可以大大简化 Unicode 字符串的处理。ICU 提供了一套完整的 Unicode 支持,包括字符串处理、格式化、排序等功能。
示例代码如下:
#include <unicode/unistr.h>
#include <unicode/ustream.h>
#include <iostream>
int main() {
icu::UnicodeString unicodeStr = UNICODE_STRING_SIMPLE("Hello, 世界!");
// 输出 UnicodeString
std::cout << unicodeStr << std::endl;
// 转换为 UTF - 8 编码的 std::string
std::string utf8Str;
unicodeStr.toUTF8String(utf8Str);
// 输出 UTF - 8 字符串
std::cout << utf8Str << std::endl;
return 0;
}
八、总结与建议
8.1 总结
C++11 引入的 Unicode string literals 为开发者提供了更强大的 Unicode 支持,通过新的字符类型和字符串前缀,能够方便地处理不同编码的字符串。同时,需要注意编辑器、编译器和输出环境对字符串处理的影响,以及掌握 Unicode 编码转换的方法。
8.2 建议
- 在编写代码时,尽量使用 UTF - 8 编码保存代码文件,这样可以避免很多编码转换的问题。
- 对于复杂的 Unicode 字符串处理,建议使用第三方库如 ICU,以提高开发效率和代码的可靠性。
- 在进行编码转换时,要注意处理可能出现的错误,确保程序的健壮性。
- 点赞
- 收藏
- 关注作者
评论(0)