字符
编码
由于现在世界各地都在使用自己国家或地区的字符串,字符串里字符种类的数量已经非常非常大了,传统的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都是什么鬼?
除了这个问题之外,还有一个问题,涉及到了两个关键点:
- 源码字符集(source character set):源代码文本文件使用什么作为编码,至今还没要求如何实现,
Linux
一般使用UTF8
,Windows
一般使用本地字符集(如GB2312
等) - 执行字符集(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
========================================
可以得出结论:此时char
和char8_t
都是使用utf8
编码,wchar_t,char16_t,char32_t
都是使用utf32
编码,这是因为中文并没有超过Unicode
中的0xFFFF
。从地址上面看,此时char
和char8_t
都是1个字节,wchar_t
和char32_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;