基本概念

c++11版本引入了char16_t和char32_t两个类型,他们的特点分别如下:

char16_t

  • 16位的unicode字符类型
  • 用于表示UTF-16编码
  • 大小:2字节
  • 字面量前缀:u

char32_t

  • 32位unicode字符类型
  • 用于表示UTF-32编码
  • 大小:4字节
  • 字面量前缀:U

基本示例

#include <iostream>
#include <string>

int main()
{
    char16_t c16 = u'中';   //单个utf16字符
    std::u16string Str16 = u"中国人";  //一个utf16字符串

    char32_t c32 = U'中';  //单个utf32字符
    std::u32string Str32 = U"中国人好";  //一个utf32字符串

    // 字符串长度,
    std::cout << "UTF-16 string length: " << str16.length() << std::endl;  //这里输出3
    std::cout << "UTF-32 string length: " << str32.length() << std::endl;  //这里输出4

    return 0;
}

注意上面示例中的std::u16stringstd::u32string的方法length获取的字符串中字符的个数,并不是这个字符串占用的内存长度。

char16_t/char32_t 与 std::u16string/std::u32string 的关系

// std::u16string 是 basic_string<char16_t> 的特化
typedef basic_string<char16_t> u16string;

// std::u32string 是 basic_string<char32_t> 的特化
typedef basic_string<char32_t> u32string;

思考1:为什么要引入这两个类型?

在c++11引入这两个类型之前,一直使用wchar_t(宽字符类型)来表示Unicode编码的字符。示例如下所示:

#include <wstring>

//使用宽字符
wchar_t wc = L'中';
std::wstring wstr = L"中国人";  //宽字符字符串

这里有一点需要注意:

  • 宽字符类型的字面量是L,这与char16_t和char32_t是不一样的

除此以外,还有一种方式来表示不同的编码,即在char数组中用对应的编码数值表示,如下所示:

char bytes[] = {0xE4, 0xB8, 0xAD, 0x00};  //“中”的UTF8编码
char bytes1[] = {0x4E,0X2D,0X00};  //“中”的UNICode编码

这种方式就是显式的告诉编译器这是一个数组,无序编译器做转码操作,直接按照对应的数值存储即可。

回到问题的原点,那为什么要引入这两个类型呢?因为wchar_t本身存在一些问题:

  • 在不同的平台上,它的长度不一定,可能会导致不一样的行为,如果以长度作为判断条件也不太适合跨平台的需求

在linux/unix平台上,wchar_t的长度为4各个字节

在window平台上,wchar_t的长度为2个字节

新语法特性引入的char16_t和char32_t则不同,他们的长度在各个平台上是固定的且编码也是明确的(unicode),在每个平台上的表现都是固定的,不依赖平台

思考2:不同的编码方式他们的特点是什么?

作为程序员开发者我们经常遇到的编码方式如下;

  • UTF-8:一种变长的编码规则,编码的长度从1个字节到4个字节,对于汉字来说一般是3个字节
  • UTF-16:也是一种变长的编码,最短2个字节,最长4个字节,没有3个字节的情况。
  • GBK/GB2312:这两种是简体中文字符编码的标准,是双字节编码。gb2312是1980年代发布的汉字编码标准,主要用于简体中文字符,它包含6763个汉字和682个非汉字字符(包括标点符号、拉丁字符等);GBK编码在在GB2312的基础上进行的一个拓展,发布于1995年。GBK含有21000多个汉字。覆盖了所有GB2312字符及其它字符。GBK支持部分繁体字,如果需要完整的繁体字支持,推荐使用Big5。

UTF-8编码规则

1字节:0xxxxxxx                    (0x00-0x7F)    ASCII字符
2字节:110xxxxx 10xxxxxx          (0x80-0x7FF)   部分中日韩字符
3字节:1110xxxx 10xxxxxx 10xxxxxx (0x800-0xFFFF) 大部分中日韩字符
4字节:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx       (0x10000-0x10FFFF) 其他字符

UTF-16编码规则

 //基本语言平面(BMP):包含了绝大多数常用字符,包括拉丁字母、汉字等。BMP 是最常用的平面

U+0000 - U+FFFF:最常用的字符
- U+4E00 - U+9FFF:CJK统一汉字
//辅助平面:包括 Supplemental Ideographic Plane、Supplementary Multilingual Plane、Supplementary Special-purpose Plane 和 Supplementary Private Use Area,存放一些较为稀有的字符、古代文本、符号以及私人使用字符等

U+10000 - U+10FFFF:较少使用的字符

 GBK编码规则

  • 第一个字节范围:0x81 到 0xFE(包含了更多汉字)
  • 第二个字节范围:0x40 到 0xFE(同样增加了汉字和非汉字)
  • GBK 使用双字节,可以表示 GB2312 的所有字符,同时支持更多汉字、标点和其他字符。

不同编码的示例

 // "中" 的UTF-8编码
// Unicode码点:U+4E2D
// UTF-8:E4 B8 AD(3字节)
const char utf8[] = {0xE4, 0xB8, 0xAD, 0x00};

// "中" 的UTF-16编码
// 占用2字节:4E 2D
const char16_t utf16[] = {0x4E2D, 0x0000};

// "中" 的GB2312编码
// 占用2字节:D6 D0
const char gb[] = {0xD6, 0xD0, 0x00};

思考3:如果一个源文件的编码格式是UTF8,那么文件存储时在磁盘上存储的形式是怎样的?

 程序员编写文件,如果源文件的编码格式是UTF8进行存储,假设编写了了这样一行代码:

std::string str="hello";

那这行代码写入放到磁盘上保存后,存储的内容是什么样的呢?肯定是按照UTF-8格式进行存储,所有的字符都进行存储,包括双引号、分号、等号,换行符等。

利用十六进制方式查看文件如下所示:

73 74 64 3A 3A 73 74 72 69 6E 67 20 73 74 72 3D 22 68 65 6C 6C 6F 22 3B

//存在如下对应

s  t  d  :  :  s  t  r  i  n  g     s  t  r  =  "  h  e  l  l  o  "  ;
73 74 64 3A 3A 73 74 72 69 6E 67 20 73 74 72 3D 22 68 65 6C 6C 6F 22 3B

思考4:字符显示乱码是如何产生的?

字符乱码的形式有如下几种情况:

  • 汉字在源文件中打开显示乱码
  • 程序运行时控制台显示乱码
  • 程序运行时页面显示乱码

我们分析一下字符乱码是如何产生的:字符肯定是有输入的地方(或存文件,或存内存,或直接发送),输入的场景如果选择了utf-8编码,如果字符显示用的地方却选择utf16方式的进行解码,那么解码得出的一定会是乱码。所以我们在涉及字符显示(尤其是汉字字符)时,一定要与数据来源方协商好字符编码格式,才能保证字符显示的正确。下面有几种场景需要打开重点考虑:

  • 字符来源于其它机器拷贝来的文件,如果显示乱码,使用notepad++查看文件的编码,如果notepad++的转码功能转换一下编码再使用。
  • 字符来源于网络通信,那么需要与数据发送方确认它的字符编码,也要确认本地平台字符显示编码格式。

思考5:如果一个源文件使用默认使用utf-8方式编码,但是在该文件中使用了char16_t的字面量,会发生什么?

这里涉及到了2个问题:

  1. 源文件如何保存的?
  2. 源文件生成的二进制程序如何保存的?

下面通过一个例子来说明这两个问题:

假设有一个源文件,该文件使用UTF-8格式编码,文件中有如下内容:

char16_t c16 = u'中';

那么源文件在磁盘上存储如下所示:

// 源文件是UTF-8编码,"中"字在文件中的存储是UTF-8格式
// "中"字在UTF-8源文件中占用3个字节:E4 B8 AD 

这个源文件经过编译器处理后:

// 编译器看到 u'中' 前缀时,会将UTF-8编码的"中"字转换为UTF-16编码,这个转码是由编译器自动完成的
// 最终在可执行文件中存储的是UTF-16格式:4E2D 

编译器的处理过程如下:

// 编译器处理过程:

// 1. 读取源文件时,"中"字被识别为UTF-8编码:E4 B8 AD

// 2. 看到 u 前缀,编译器知道需要创建UTF-16字符

// 3. 将UTF-8编码转换为UTF-16编码:4E2D

// 4. 在目标文件中存储UTF-16编码值

// 最终在内存中:

// c16变量中存储的是UTF-16编码值:4E2D 

思考6:如果源文件采用GBK编码方式,那么各种编译器编译出的二进制可执行程序中的编码是哪种形式?

 这个问题很有意思,首先可以肯定的源文件必然以GBK方式保存,那么问题来了:

  • 编译器是否能检测到源文件采用何种编码方式
  • 编译器是否关心源文件采用何种编码方式
  • 编译器的处理策略是怎样的

下面我将一一回答这个问题。

  • 根据我的了解,编译器不检测源文件采用何种编码形式,它一般使用默认的编码格式读取源文件
  • 可以通过编译选项的方式告知编译器应该采用何种编码方式读取源文件
  • 编译器默认采用默认的编码格式读取源文件,如果源文件的编码格式是GBK,而编译器的默认编码格式如果是utf-8(如gcc),那么编译器就会按照utf8的方式读取源文件并编译,这种情况下编译到可执行程序中的汉字字符串可能就会出现乱码。

根据这种情况,有一些建议:

  • 首先了解编译器的默认编码格式

Window系统默认的编码格式是ANSI

Windows中文简体系统下MSVC默认的编码格式是GBK

GCC/CLANG默认的编码格式是UTF-8

  • 开始阶段不同的开发人员要统一源文件的编码格式

思考7:Unicode包含哪几种编码格式?

Unicode包含如下几种编码格式:

  • UTF-8(Unicode Transformation Format - 8-bit)
  • UTF-16(Unicode Transformation Format - 16-bit)
  • UTF-32(Unicode Transformation Format - 32-bit)

它们各自的特点如下:

  1. UTF-8

    • 变长编码(1-4字节)
    • ASCII兼容
    • 节省空间(对于英文文本)
    • 适合网络传输
    • Web标准
  2. UTF-16

    • 变长编码(2或4字节)
    • 对于中日韩文字较为高效
  3. UTF-32

    • 固定长度(4字节)
    • 空间占用大

它们的用途:

  • UTF8适合存储和传输,因为对于英文文本友好
  • UTF16适合系统接口,如qt的QString则采用utf16编码
  • UTF32适合内部处理

之前我存在错误的认知:认为Unicode=UTF-16,实际上这是错误的理解,Unicode是一种字符集,它并不是编码方式,Unicode可以以不同的方式编码,还有一些错误的理解,比如我经常用使用qt框架进行开发,qt框架中的QString采用的就是UTF-16编码,在有些文章中没有描述清楚,仅仅是说QString采用的是Unicode字符串进行编码的,造成了我认为Unicode=UTF-16的印象。

使用建议

使用现代的c++ Unicode字面量

void modernCppWay() {
    // UTF-8字面量
    std::string utf8_str = u8"你好";
    
    // UTF-16字面量
    std::u16string utf16_str = u"你好";
    
    // UTF-32字面量
    std::u32string utf32_str = U"你好";
}

显式的告诉编译器以何种方式进行编码,如果不显示的指定,那么编译器会默认的编码方式编解码。 

内部统一编码方式推荐使用UTF-8

推荐使用新引入的类型char16_t/char32_t/std::u16string/std::u32string

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐