• Index

编码、字符和字符串

Reads: 55

编码

之前提到的ASCII就是一种编码方式。我们知道,计算机只会处理和保存数字,而实际上,计算机上看到的文字,就是数字。

计算机会根据一个个的数字,然后去查一下字典,找出对应的文字的图片,然后交给显卡显示出来,这样我们就能看到文字了。

这个字典叫做字符集

ASCII就是一个字符集,用数字0~127来代表字母数字符号等字符。

字符集有ASCII、本地字符集(如GBK)和Unicode。

Unicode包括UTF-8、UTF-16、UCS-2、UTF-32、USC-4,而UTF-16与UCS-2可以理解为等价,UTF-32与USC-4等价。并且UTF-16和UTF-32又分别有两种:BE和LE。这些都是数字和文字对应的字典。

有时候我们会看到乱码,就是因为你用错字符集或者你把不是文字的数据用文字字典找文字;归根究底,就是 你!用!错!字!典!了!

C++支持UTF-8、UTF-16、UTF-32和本地字符集。

Windows默认的字符集是UTF-16。而Linux/OS X默认的字符集是UTF-8。所以写跨平台代码就要注意了。就算是同样是Windows,如果你用错字符集,你的程序在英文系统或者中文系统就会出现乱码。

以下是每个字符集和C++数据类型对应关系和如何选择合适的字符集:

字符集 字符数据类型 字符串数据类型 使用方式 优点 缺点 常见使用场景
UTF-8 char std::string u8"字符串" 可跨平台,以每个字符的最小占用来分配大小,所以占用小 以每个字符的最小占用来分配大小,ASCII字符用1个字节保存,而中文字符使用3个或4个字节保存,每个字符大小不一样,所以不能直接操作字符串;标准不支持终端直接输出 网络传输
UTF-16 char16_t std::u16string u"字符串" 可跨平台,每个字符都用2个字节来保存,占用比UTF-32小,比UTF-8更易运算处理 2个字节保存的文字有限,对于emoji表情和部分生僻文字要用多个 char16_t 保存,此时直接处理字符串有可能出错;标准不支持终端直接输出 字符串保证没有生僻字时使用
UTF-32 char32_t std::u32string U"字符串" 可跨平台,保存世界上所有字符,处理字符串容易且不会出错 每个字符都用4个字节来保存,占用非常大;标准不支持终端直接输出 有生僻字或者emoji表情时使用

剩下两种(""L"")方式,由于历史原因,在不同的操作系统会有不同的意义,所以会有点复杂:

字符数据类型 字符串数据类型 使用方式 字符集 使用场景
char std::string "字符串" 本地字符集(Windows) 字符串只用于显示而不需要进行操作
UTF-8(Linux/OS X)
wchar_t std::wstring L"字符串" UTF-16(Windows) 程序可跨不同语言的Windows平台,使用Windows API开发Windows桌面应用时推荐使用,跨不同操作系统不推荐使用,需要设置本地字符集后使用std::wcout输出
UTF-32(Linux/OS X) 不推荐使用,不能跨平台,建议直接使用 char32_t,需要设置本地字符集后使用std::wcout输出

不幸的补充:很惨的是std::cout只能输出"字符串"这样的字符串;std::wcout只能输出L"字符串"这样的字符串,而且还要设置本地字符集;而对于其他字符集的字符串,标准不支持输出。更惨的是,标准也不支持字符集之间的转换(以前支持现在不支持)。

普通字符

一个汉字是一个字符,一个字母是一个字符,一个数字是一个字符,一个符号也是一个字符。例如这些字符:a1*

基础示例

#include <iostream> // std::cout std::endl

int main(void)
{
    char ascii = 65; // 使用数字
    char multicharacter = 'a'; // 直接使用char
    char utf8character = u8'a'; // 使用UTF-8字符, 由于char只能保存1个字节而'你'至少2个字节, 所以不能使用u8'你'
    wchar_t widecharacter = L'你'; // 使用宽字符
    char16_t utf16character = u'好'; // 使用UTF-16, 而U'𪚥'(四个龍)至少占用3个字节, 所以不能使用u'𪚥'(四个龍)
    char32_t utf32character = U'𪚥'; // (四个龍)这个字至少占用3个字节

    std::cout << ascii << std::endl;
    std::cout << multicharacter << std::endl;
    std::cout << utf8character << std::endl;
    std::cout << widecharacter << std::endl;
    std::cout << utf16character << std::endl;
    std::cout << utf32character << std::endl;

    return 0;
}

输出结果:

A
a
a
20320
22909
173733

基础讲解

std::coutchar变量会以文字的形式输出,所以第一个65会直接输出ASCII对应的值,即A

char ascii = 65;

为了使代码更加容易阅读,所以C++允许在代码中直接用字符代替数字,但是需要用两个单引号'包裹着,让编译器知道这是字符而不是变量:

char multicharacter = 'a';

将字符aUTF-8字面量赋值给变量utf8character。由于char只能保存1个字节,所以UTF-8字面量仅允许ASCII字符:

char utf8character = u8'a';

宽字符需要前缀L标记字符串,UTF-16需要前缀u,UTF-32需要前缀U。由于𪚥(四个龍)至少需要3个字节,所以不能使用char16_t保存。而且由于std::cout只对char变量特殊输出,所以wchar_tchar16_tchar32_t将被当作数字输出:

wchar_t widecharacter = L'你';
char16_t utf16character = u'好';
char32_t utf32character = U'𪚥';

注意std::cout对于charunsigned char数据类型的变量,会以文字的形式输出而不是输出数字。所以如果你使用char保存unsigned char非文字数据时,使用std::cout并不能输出你想要的数字。你需要把数据转成其他数字类型再输出,例如:可以新建一个int变量保存char变量的值。

转义字符

我们现在知道,在代码中要用单引号'把需要用到的字符引起来。但是假如我要用变量保存单引号'这个字符,明显不能在代码上写三个单引号,这样写会导致编译报错。

对于这种情况,当然可以使用数字表示,但是数字不好记忆也不好阅读。所以标准规定,在代码中使用反斜杠\配合一个字符来表示这些不可见的字符。这个\叫做转义符,使用\配合一个字符来表示的字符叫做转义字符

常用的转义字符有:

  • 空字符'\0',用于来作为字符串的结束标志。
  • 回车字符'\r',配合\n一起使用,字符串"\r\n"用于换行。
  • 换行字符'\n',配合\r一起使用,字符串"\r\n"用于换行。
  • 水平制表符'\t',和空格差不多,但它可以使输出对齐。
  • 单引号'\'',当需要使用单引号时,为了与包裹字符的单引号区分开,需要用到转义符。
  • 双引号'\"',当需要在字符串中使用双引号时,为了与包裹字符串的双引号区分开,需要用到转义符。
  • 反斜杠'\\',当需要使用反斜杠时,由于反斜杠在字符串中起转义作用,为了在字符串中使用反斜杠,需要两个反斜杠'\\',来说明我要使用字符\

注意:转义字符只是在代码中起作用,如果你从程序中输入\,那么它只是字符'\',没有转义的作用。

字符串

一个及以上的字符组成字符串。为了与变量区分开,需要使用两个双引号"将字符串引起来。

使用字符串则需要用到数据类型std::stringstd::wstringstd::16stringstd::32string,它们在string标准库中。

为了简化说明,std::stringstd::wstringstd::16stringstd::32string在下面将统一叫做string系列

基础示例

#include <iostream> // std::cout std::endl
#include <string> // std::string std::wstring std::u16string std::u32string

int main(void)
{
    std::string multistr = "a"; // 只含有一个字符的字符串
    std::string utf8str = u8"C++教程"; // UTF-8字符串
    std::wstring widestr = L"C++教程"; // 宽字符
    std::u16string utf16str1 = u"C++教程"; // 使用UTF-16
    std::u16string utf16str2 = u"𪚥"; // 使用UTF-16, 由于U'𪚥'(四个龍)至少占用3个字节, 所以这里将占用两个char16_t
    std::u32string utf32str = U"复杂的字𪚥"; // 使用UTF-32

    std::cout << multistr << std::endl;
    std::cout << utf8str.size() << std::endl;
    std::cout << widestr.size() << std::endl;
    std::cout << utf16str1.size() << std::endl;
    std::cout << utf16str2.size() << std::endl;
    std::cout << utf32str.size() << std::endl;

    return 0;
}

输出结果:

a
9
5
5
2
5

基础讲解

由于std::cout只能输出""的字符串,所以这里我只输出字符串multistr

std::string multistr = "a";
std::cout << multistr << std::endl;

string系列的变量,都可以使用成员函数size(),意思是这个字符串的大小。也就是说,用来获取std::string变量中char的个数、std::wstring变量中wchar_t的个数、std::16string变量中char16_t的个数、std::32string变量中char32_t的个数。

保存UTF-8字符串也是用数据类型std::string。UTF-8中两个字符分别需要3个字节来保存,C++3个字符各1个字节,而char变量只有1个字节,所以UTF-8字符串C++教程占用1 + 1 + 1 + 3 + 3 = 9char变量。以下代码输出的是char的数量,换句话说,输出的是C++教程占用的字节数:

std::string utf8str = u8"C++教程";
std::cout << utf8str.size() << std::endl;

由于UTF-8字符串中只能获取字节数而不是字符数,所以对字符串进行字符修改基本上是不可能的。

保存UTF-16字符串需要用std::u16string。UTF-16中每个字符都用2个字节保存,所以一般情况下,成员函数size()获取到的是字符数。所以下面代码输出5

std::u16string utf16str1 = u"C++教程";
std::cout << utf16str1.size() << std::endl;

UTF-16只用2个字节来保存字符,只能保存各国常用文字,保存不了生僻字和emoji表情。所以遇到像以下生僻字,则需要两个char16_t来保存:

std::u16string utf16str2 = u"𪚥";
std::cout << utf16str2.size() << std::endl;

当使用UTF-16字符串时遇到上面这个字符,成员函数size()获取到的并不是字符数,所以如果对这个字符串中的字符进行操作,将很有可能出现错误。

UTF-32包含了世界上所有的文字,所以使用成员函数size()获取到的绝对是字符数。所以下面输出的是5

std::u32string utf32str = U"复杂的字𪚥";
std::cout << utf32str.size() << std::endl;

宽字符字符串需要使用std::wstring。由于字符串C++教程没有生僻字,所以下面代码输出的是5

std::wstring widestr = L"C++教程";
std::cout << widestr.size() << std::endl;

原生字符串

当我们写的字符串含有的转义字符比较多,例如:Windows上的文件路径:

std::string dir = "C:\\Program Files\\Microsoft Office 15\\ClientX64";

在代码中,往往这样写字符串并不方便,原生字符串为了解决这个问题而出现。

基础示例

#include <iostream> // std::cout std::endl
#include <string> // std::string

int main(void)
{
    std::string dir1 = "\"C:\\Program Files\\Microsoft Office 15\\ClientX64\"";
    std::string dir2 = R"xxx("C:\\Program Files\\Microsoft Office 15\\ClientX64")xxx";
    std::cout << dir1 << std::endl;
    std::cout << dir2 << std::endl;
    return 0;
}

输出结果:

"C:\Program Files\Microsoft Office 15\ClientX64"
"C:\\Program Files\\Microsoft Office 15\\ClientX64"

基础讲解

根据代码可以发现,变量dir2中转义符不起作用。双引号"也不需要转义符。

原生字符串,只需要在字符串前面加上R,并且使用"xxx()xxx"包裹字符串,其中xxx叫做原生字符串的结束分隔符,可以自定义,例如:R"(字符串)"或者R"abcdefg(字符串)abcdefg"。所以,使用"xxx()xxx"包裹的字符串中,所有的\都只是字符\而不是转义符。

注意:原生字符串的结束分隔符只能是ASCII中的可见字符,所以不能用中文作为结束分隔符。

以下是其他字符集的原生字符串的写法:

  • UTF-8的原生字符串:u8R"xxx("字符串")xxx"
  • UTF-16的原生字符串:uR"xxx("字符串")xxx"
  • UTF-32的原生字符串:UR"xxx("字符串")xxx"
  • 宽字符的原生字符串:LR"xxx("字符串")xxx"

补充知识(了解即可)

  1. 类型char16_t、类型char32_t、类型std::u16string、类型std::u32string、字符串前缀u8、字符和字符串前缀u、字符和字符串前缀U、原生字符串都是从C++11开始加入。
  2. 字符字面量u8从C++17起开始加入。
  3. C++11提供库codecvt使字符串可以在不同字符集之间转换,从C++17开始被废弃。所以编码转换建议使用操作系统API或者第三方库

Comments

Make a comment

  • Index

WARNING: You are using an old browser that does not support HTML5. Please choose a modern browser (Chrome / Microsoft Edge / Firefox / Sarafi) to get a good experience.