字符

编码

由于现在世界各地都在使用自己国家或地区的字符串,字符串里字符种类的数量已经非常非常大了,传统的ASCII编码完完全全不够了,因此,Unicode编码被提出,它为世界上每一种符号提供了一个唯一的编码。目前已经有0x0000-0x10FFFF种编码了,其实质上是把0xFFFF作为一个单位,称之为Plane(平面),那么总共有0x10(17)个平面的。常用的字符都在第一个平面中,也称之为BMP(Basic Multilingual Plane)。

Unicode只是做了编码,但是具体怎么在计算中实现呢,那么就是UTF(Unicode Transformation Format)的事情,任何UTF字符把其转换成Unicode都是一样的。

  • UTF32:直接使用32bit(4Byte)来存储,大多数语言中,也就是一个int型的宽度,能够表示的范围是0xFFFFFFFF,比0x10FFFFFF多出太多,很浪费,但是却很简单粗暴。
  • UTF16:对于0xFFFF以内的字符,使用两个字节来表示,完全不浪费。超过0xFFFF的字符,就使用四个字节表示。属于可变长字符集,至于如何实现可变长的,那就是使用一些标识来做到。但是UTF16的处理,也是两个两个字节为一个单位的,所以最终字符串的长度,仍然是2的整数倍。
  • UTF8:同样是可变长字符,但是UTF8使用1,2,3,4个字节来表示。并且兼容了ASCII字符集,所以应用的比较广泛。UTF8的处理是一个一个字节为单位的,最终字符串的长度就只会是1的整数倍。

字节序

除了字符集会影响一个数据的编码,还有字节序也会影响,字节序分为小端序(LE, little endian)和大端序(BE, big endian)。

  • 小端序:高位放在高字节,低位放在低字节。对于数据0x12345678来说,数据的高位就是0x12到低位0x78。对于地址0x0001-0x0004来说,地址的低位就是0x0001到高位0x0004。因此存储方式就是,0x0001(0x78), 0x0002(0x56), 0x0003(0x34), 0x0004(0x12)
  • 大端序:高位放在低字节,低位放在高字节。对于数据0x12345678来说,数据的高位就是0x12到低位0x78。对于地址0x0001-0x0004来说,地址的低位就是0x0001到高位0x0004。因此存储方式就是,0x0001(0x12), 0x0002(0x34), 0x0003(0x56), 0x0004(0x78)

显然大端序适合人类阅读,它也是网络传输的标准序。这是因为,从左到右,数据序的位是降低的,内存序的位是增高的。

编码方式

编码和字节序可以组成不同的文件编码方式,比如UTF16 LE, UTF16 BE, UTF32 LE, UTF32 BE,但是UTF8是一个一个字节为单位,所以没有大小端序之分。

可打印字符

在互联网中,常常使用UTF8作为编码方式,但是UTF8表示的是一个Unicode字符,并不是所有字符都能打印出来,事实上,能打印字符远小于Unicode表示的字符。但是,如何将这些在互联网上传输呢?可以使用字节流的方式,毕竟互联网规定了端序,也可以规定编码。如何使用控制台打印字节流呢?最简单就是使用16进制,两个字符表示一个字节,即FE就表示字节流11111110,那么带宽成本变成两倍。因此,还可以使用base64来打印字节流。

Base64将一个字节的前6位提取出来,并加上00。然后使用大小写字母,数字和+, /共,26 + 26 + 10 + 2 = 64个可打印字符来表示,大大减小了带宽。把前6位提出并拼接00,那么只需要64个符号来表示即可,因此,就可以编码0-63依次为A-Z a-z 0-9 + /

问题起源

C/C++中,对于字符与字符串的处理,一直是个头疼的问题,这是由于历史遗留问题导致的,以前,只有英文字母才接触编程的时代,字符只需要一个char来表示即可,字符串可以是char的数组或其他。但是,随着各国编程,字符变的越来越多,char显然不能满足要求。

C/C++一直是得兼容以前的代码,因此,只能加功能,不能减功能,导致现在的字符集合有如下的家族了:

| 类型 | 引入C标准 | 引入C++标准 | 备注说明 |
| ————- | ——— | ———– | ———————————————————— |
| char | K&R C | C++98 | 表示一个字节的字符 |
| signed char | K&R C | C++98 | 有符号一个字节字符 |
| unsigned char | K&R C | C++98 | 无符号一个字节字符 |
| wchar_t | C89 | C++98 | 宽字符 |
| char16_t | C11 | C++11 | UTF16 |
| char32_t | C11 | C++11 | UTF32 |
| char8_t | C23 | C++20 | UTF-8编码字符 |

表格来源:知乎文章:C++中char、signed char、unsigned char、wchar_t、char16_t、char32_t、char8_t都是什么鬼?

除了这个问题之外,还有一个问题,涉及到了两个关键点:

  1. 源码字符集(source character set):源代码文本文件使用什么作为编码,至今还没要求如何实现,Linux一般使用UTF8Windows一般使用本地字符集(如GB2312等)
  2. 执行字符集(execution character set):可执行程序中,对于字符串的编码使用何种字符集,现代c++中已经规定好了。char8_t,char16_t,char32_t都是用于执行字符集的

尽量别用wchar_t

用法示例:

auto char_ = 'c';    // 普通 char
auto wchar_ = L'c';  // wchar_t
auto char8_ = u8'c'; // char8_t
auto char16_ = u'c'; // char16_t
auto char32_ = U'c'; // char32_t

C++测试

Linux下的GCC编译器测试这几种字符,先通过网上的转换工具,可以知道hello, 世界!这一个字符串的UTF8编码是68,65,6C,6C,6F,2C,20,E4B896,E7958C,EFBC81,而其UTF16,UTF32的编码都跟Unicode一样是0068,0065,006c,006c,006f,002c,0020,4e16,754c,ff01

#include <string>
#include <iostream>

template <class STR>
void print_str(const STR* str, const char* info) {
    std::cout << info << " : " << std::endl;
    size_t counter = 0;
    STR* p = const_cast<STR*>(str);
    // 每行输出 地址: 十六进制,使用printf方式
    while (*p != '\0') {
        printf("%p: 0x%hhx: %c\n", p, *reinterpret_cast<char *>(p), static_cast<char>(*p));
        ++p;
        ++counter;
    }
}
int main() {
    auto str_char = "hello, 世界!";
    auto str_wchar = L"hello, 世界!";
    auto str_char8 = u8"hello, 世界!";
    auto str_char16 = u"hello, 世界!";
    auto str_char32 = U"hello, 世界!";
    print_str<char>(str_char, "char");
    print_str<wchar_t>(str_wchar, "wchar_t");
    print_str<char8_t>(str_char8, "char8_t");
    print_str<char16_t>(str_char16, "char16_t");
    print_str<char32_t>(str_char32, "char32_t");
    return 0;
}

// 结果如下:
char : 
0x559f7a8eb008: 0x68: h
0x559f7a8eb009: 0x65: e
0x559f7a8eb00a: 0x6c: l
0x559f7a8eb00b: 0x6c: l
0x559f7a8eb00c: 0x6f: o
0x559f7a8eb00d: 0x2c: ,
0x559f7a8eb00e: 0x20:  
0x559f7a8eb00f: 0xe4: �
0x559f7a8eb010: 0xb8: �
0x559f7a8eb011: 0x96: �
0x559f7a8eb012: 0xe7: �
0x559f7a8eb013: 0x95: �
0x559f7a8eb014: 0x8c: �
0x559f7a8eb015: 0xef: �
0x559f7a8eb016: 0xbc: �
0x559f7a8eb017: 0x81: �
count = 16
========================================
wchar_t : 
0x559f7a8eb020: 0x68: h
0x559f7a8eb024: 0x65: e
0x559f7a8eb028: 0x6c: l
0x559f7a8eb02c: 0x6c: l
0x559f7a8eb030: 0x6f: o
0x559f7a8eb034: 0x2c: ,
0x559f7a8eb038: 0x20:  
0x559f7a8eb03c: 0x16: 
0x559f7a8eb040: 0x4c: L
0x559f7a8eb044: 0x1: 
count = 10
========================================
char8_t : 
0x559f7a8eb008: 0x68: h
0x559f7a8eb009: 0x65: e
0x559f7a8eb00a: 0x6c: l
0x559f7a8eb00b: 0x6c: l
0x559f7a8eb00c: 0x6f: o
0x559f7a8eb00d: 0x2c: ,
0x559f7a8eb00e: 0x20:  
0x559f7a8eb00f: 0xe4: �
0x559f7a8eb010: 0xb8: �
0x559f7a8eb011: 0x96: �
0x559f7a8eb012: 0xe7: �
0x559f7a8eb013: 0x95: �
0x559f7a8eb014: 0x8c: �
0x559f7a8eb015: 0xef: �
0x559f7a8eb016: 0xbc: �
0x559f7a8eb017: 0x81: �
count = 16
========================================
char16_t : 
0x559f7a8eb04c: 0x68: h
0x559f7a8eb04e: 0x65: e
0x559f7a8eb050: 0x6c: l
0x559f7a8eb052: 0x6c: l
0x559f7a8eb054: 0x6f: o
0x559f7a8eb056: 0x2c: ,
0x559f7a8eb058: 0x20:  
0x559f7a8eb05a: 0x16: 
0x559f7a8eb05c: 0x4c: L
0x559f7a8eb05e: 0x1: 
count = 10
========================================
char32_t : 
0x559f7a8eb020: 0x68: h
0x559f7a8eb024: 0x65: e
0x559f7a8eb028: 0x6c: l
0x559f7a8eb02c: 0x6c: l
0x559f7a8eb030: 0x6f: o
0x559f7a8eb034: 0x2c: ,
0x559f7a8eb038: 0x20:  
0x559f7a8eb03c: 0x16: 
0x559f7a8eb040: 0x4c: L
0x559f7a8eb044: 0x1: 
count = 10
========================================

可以得出结论:此时charchar8_t都是使用utf8编码,wchar_t,char16_t,char32_t都是使用utf32编码,这是因为中文并没有超过Unicode中的0xFFFF。从地址上面看,此时charchar8_t都是1个字节,wchar_tchar32_t都是4个字节,char16_t是两个字节。

字符串

C语言字符串其实就是一系列字符加上\0组成的,在C++中,STL库封装了basic_string<CharT>类,并对char, wchar_t, char8_t, char16_t, char32_t进行了封装,如下所示:

typedef basic_string<char>    string;     // 足以应付日常需求了
typedef basic_string<wchar_t> wstring;   
typedef basic_string<char8_t> u8string;
typedef basic_string<char16_t> u16string; 
typedef basic_string<char32_t> u32string;