C++字符编码详解及利用string遍历中文字符串

作者:非妃是公主
专栏:《笔记》《C++》
个性签:顺境不惰,逆境不馁,以心制境,万事可成。——曾国藩

C++字符编码详解及利用string遍历中文字符串_第1张图片

文章目录

  • C++遍历英文字符串
  • C++遍历中文字符串(不会出问题情况)
  • C++遍历中文字符串(会出现问题的情况)
  • 英文字符的表示
  • 中文字符的表示
    • 定长编码
    • 变长编码
    • Unicode编码
  • 正确的中文字符串遍历方式
  • 参考资料

C++遍历英文字符串

C++遍历英文字符串很简单,基本有两种方法

// 方法1
for(int i=0; i < str.size(); i++){
	cout << str[i];
}

// 方法2
for(auto it = str.begin(); it != str.end(); it++){
	cout << *it;
}

以上两种方法基本就可以很好地遍历英文字符串了!

C++遍历中文字符串(不会出问题情况)

但是中文字符串呢?我们来试一下:
C++字符编码详解及利用string遍历中文字符串_第2张图片
发现是可以正常输出的!

C++遍历中文字符串(会出现问题的情况)

再继续试!这次这样尝试,因为我们遍历字符串一般都是要对字符串中的字符进行操作,如果单纯只是输出或者显示而已,没必要去遍历字符,直接cout<就可以了,所以接下来试一下字符的操作:
C++字符编码详解及利用string遍历中文字符串_第3张图片
这次添加了一个if (str[i] == '非') continue;的判断,但是‘非’依然输出了出来
调试一下:
C++字符编码详解及利用string遍历中文字符串_第4张图片
i = 0,但是str[i] == '非'确实false,这是怎么回事呢?
我们知道,遍历字符串主要就是要对字符串进行操作,可是如今字符串判断相等却出现了问题……
这里给出测试代码,感兴趣的老哥可以自己去调试:

void test1() {
    string str = "非妃是公主";
    cout << "这时test1:";
    for (int i = 0; i < str.size(); i++) {       
        if (str[i] == '非') continue;
        cout << str[i];
    }
    cout << endl;
}

void test2() {
    string str = "非妃是公主";
    cout << "这时test2:";
    for (auto it = str.begin(); it != str.end(); it++) {
        cout << *it;
    }
    cout << endl;
}

int main() {
    test1();
    test2();
}

英文字符的表示

其实,这里涉及到了一个编码的问题,ASCII码值是一个字符集表,里面编码了26个英文字母的大小写(大写字母65~90,小写字母97~120),还有其它英文字符(比如空格、单引号……),其中有一些甚至是不可显示的(比如换行符、分组符……)。具体可以查看ASCII码表.

利用ASCII(美国信息交换标准代码)就可以实现英文的字符映射了,因为英文字母只有那么些,所有的单词都是根据这些字母进行排列组合形成的。

下面为ASCII码表的节选(开头和结尾部分):
C++字符编码详解及利用string遍历中文字符串_第5张图片
C++字符编码详解及利用string遍历中文字符串_第6张图片
可以看到,从0~127,ASCII码表有128个编码,而 2 8 = 256 2^8=256 28=256,也就是说可以用1个字节(Byte,等于8个bit)大小的内存空间来编码所有的英文字符,因此char利用这样的字节编码到字符进行映射就可以实现英文字符串的运算。

中文字符的表示

从上面不难看出,英文可以利用一个很小的字符集(ASCII——美国信息交换标准代码)去表示所有单词(因为只有26个字母等优先的符号),但中文不可以,中华汉字博大精深,其中包含了几千甚至上万的汉字(如果还包括一些繁体字、生僻字等数量会更大)!

因此,1个Byte不能满足中文编码的需要,我们需要2个、3个甚至4个Byte进行编码才能把中文表示出来!

这里就包含了两种编码的方式,定长编码和变长编码。

定长编码

定长编码:顾名思义,就是每个字符对应的编码的长度都是相等的,这里不得不提到GB2312编码和GBK编码。

  • GB2312编码:就是把汉字编码成两个字节,一个字节有 2 8 = 256 2^8=256 28=256种不同的编码,两个字节就有 2 16 = 65536 2^{16}=65536 216=65536种不同的编码,也就是说我们最多可以编码65536种情况,这些对于常用的文字应该可以了吧……但是,值得一提的是,GB2312并没有使用完全这些编码,它只用了一部分,那么剩下的呢?GB2312为了保持向下兼容ASCII,它避免了和ASCII进行冲突编码,这要浪费一部分编码空间,但依然还是有空余的,这些空余下的位置暂且留着,GB2312没有使用!
  • GBK编码:和 GB2312 一样,GBK 也是双字节编码,同样为了向下兼容 GB2312, GBK使用了GB2312 没有用到的那些编码区域,简单地说,就是进一步拓展了编码集,GBK比GB2312编码了更多的汉字。

可以说,GBK编码是对GB2312编码的补充!

关于定长编码的详细规则可以在这篇文章里看到,总结的十分全面https://zhuanlan.zhihu.com/p/453675608

变长编码

变长编码:是一种包含多个长度编码的字节结构!换句话说,这种类型的编码既可以使用1个字节,也可以使用2个字节,3个字节,以及4个字节,那么就来了一个问题,我怎么知道这个编码到底是用了几个字节呢?到底是1个字节,2个字节还是3个、4个字节呢?也就是如何进行解析呢?

这里,以GB18030编码为例进行说明,它也是一种变长编码,有1个字节,2个字节以及4个字节大小的编码,下图为GB18030编码的字节结构示意图:
C++字符编码详解及利用string遍历中文字符串_第7张图片
从图中可以很容易地看出:

  1. 前8位为00-80大小的,只有一个字节
  2. 前8位为81-FE,9~16位位40-FE的有两个字节,基本就是兼容GBK编码(但是和GBK还是有区别的,详细区别读者可以自行查阅)
  3. 前8位位81-84,9-16位为30-39的,17-24位为81-FE的,25-32位为30-39的有4个字节!
  4. ……
  5. 根据表格,按照上述的字节区间编码结构就可以进行解码了。

4个字节可以表示更多的字符。

其实GB是国标的意思:

国家标准GB18030-2000《信息交换用汉字编码字符集基本集的补充》是我国继GB2312-1980和GB13000-1993之后最重要的汉字编码标准,是我国计算机系统必须遵循的基础性标准之一。
GB18030-2000编码标准是由信息产业部和国家质量技术监督局在2000年3月17日联合发布的,并且将作为一项国家标准在2001年的1月正式强制执行。 GB18030-2005《信息技术中文编码字符集》是我国制订的以汉字为主并包含多种我国少数民族文字(如藏、蒙古、傣、彝、朝鲜、维吾尔文等)的超大型中文编码字符集强制性标准,其中收入汉字70000余个。

也就是说,这一个标准是我国制定的,并没有在国际上通用!它只编码了我国的汉字以及少数名族文字等。

Unicode编码

Unicode 其实是一个字符集,这个字符集给世界上常用的字符都进行了编码,每一个字符对应一个唯一的编码。但值得注意的是,它并不是一个字符编码,Unicode还需要依靠一些字符编码规范,才能发挥作用,后面会提到。Unicode 字符集的编码范围是 0x0000 - 0x10FFFF,相比于上面提到的字符编码标准(带GB的都是国标的汉语拼音首字母,因此都是国内的标准),Unicode是一个国际化的标准。换句话说,如果说GB2312、GBK、GB18030是国家级的字符编码,那么Unicode就是一个国际级的字符集!

从上面提到的Unicode的范围可以看出,如果直接编码,我们只需要三个字符就可以编码它。但是,比如第1个字符,如果用3个Byte进行编码,那么它的编码应该是0x000001,问题来了,前面的0并没有包含什么信息,本来1个字节可以存储的,却消耗了3个字节,这是一种存储空间,以及计算机效率的浪费!

因此这里同样采用边长编码,这也就解释了上面为什么Unicode是字符集,而不是一种字符编码了,因为如果直接使用它进行编码会浪费大量的空间和时间

Unicode的编码规则对应utf-8、utf-16、utf-32,每个都代表一种不同的编码规则,utf是Unicode transform format的缩写,Unicode变换格式的缩写。

  • utf-8编码:是一种边长编码规则,可以使用1~4个字节,具体地说:
    • 对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的, 所以 UTF-8 能兼容 ASCII 编码,这也是互联网普遍采用 UTF-8 的原因之一。
    • 对于 n 字节的符号( n > 1),第一个字节的前 n 位都设为 1,第 n + 1 位设为 0,后面字节的前两位一律设为 10 。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
    • 那么紧接着就有一个问题,没有占满怎么半,答案是补0,从右往左占,高位占不满的自动补0。
    • 还有1个问题,不够位数怎么半,这个问题好解决,增加字节就是了。4个字节最多可以表示3+6+6+6=21,仔细想一下,刚好覆盖Unicode 字符集的编码范围是 0x0000 - 0x10FFFF,没有任何问题。
  • utf16编码:它也是一种变长编码规则,但是它将字符编码成2字节或者4字节。
    • 对于 Unicode 码小于 0x10000 的字符, 使用 2 个字节存储,并且是直接存储 Unicode 码,不用进行编码转换;
    • 对于 Unicode 码在 0x10000 和 0x10FFFF 之间的字符,使用 4 个字节存储,这 4 个字节分成前后两部分,每个部分各两个字节,其中,前面两个字节的前 6 位二进制固定为 110110,后面两个字节的前 6 位二进制固定为 110111, 前后部分各剩余 10 位二进制表示符号的 Unicode 码 减去 0x10000 的结果。20位bit正好可以表示0xFFFF。
    • 大于 0x10FFFF 的 Unicode 码无法用 UTF-16 编码。
  • utf-32编码:UTF-32 是固定长度的编码,始终占用 4 个字节,足以容纳所有的 Unicode 字符,所以直接存储 Unicode 码即可,不需要任何编码转换。虽然浪费了空间,但提高了效率。

正确的中文字符串遍历方式

C++字符编码详解及利用string遍历中文字符串_第8张图片

从图中可以看到非妃是公主5个字的汉字字符,但是,无论size和length都是10,这说明:length()和size()返回的并不是字符串的长度,而是字符串占用了多少个Byte。

进一步推测:s[i]指的是第i个Byte,it++也指的是前进1个Byte。

而GBK和GB13080都对GB2312向下兼容,而GB2312就包含了汉字中绝大多数,部分生僻字和繁体字是不包含的,GB2312是用2个字节进行表示的。

因此,这里i和i+1才能表示1个字符。

而2个Byte就不能用char(1个Byte)来表示了,string底层是由char实现的,而汉字至少包含两个char的大小,所以要继续用string来表示一个汉字:

遍历算法应该如下:

void test3() {
    string str = "非妃是公主";
    cout << "这时test3:";
    for (int i = 0; i < str.size(); i = i + 2) {
        string tmp = "";
        tmp = tmp + str[i] + str[i + 1];
        if (tmp == "非") continue;
        cout << tmp;
    }
    cout << endl;
}

执行结果如下:

C++字符编码详解及利用string遍历中文字符串_第9张图片

从输出结果中可以看出if (tmp == "非") continue;已经被执行了。

同时,经过调试可以发现,因为字符串的==运算符应该是经过重载生成的,所以在调试时显示没有与操作数匹配的“==”运算符,无法进行监视。

但是从图中可以看出,tmp的值已经是“非”了。也实现了预期的结果,进而可以实现字符串遍历中对单个中文字符串的操作。

C++字符编码详解及利用string遍历中文字符串_第10张图片

同时也在交流群里向大佬交流了一下,大佬帮忙给找了一个参考代码,此处一并贴出,并已标明出处[5],我在这里加上注释,对代码进行解释,如下:

string text = "今天周五123";
for(size_t i = 0; i < text.length();)
{
    int cplen = 1;
    if((text[i] & 0xf8) == 0xf0) cplen = 4; 	 // 占用4个字节,前5位为11110
    else if((text[i] & 0xf0) == 0xe0) cplen = 3; // 占用3个字节,前4位为1110
    else if((text[i] & 0xe0) == 0xc0) cplen = 2; // 占用2个字节,前3位为110
    // 个人感觉这行代码好像没什么用,如果三种情况都不符合,那么cplen就为初始化的0,是符合utf-8编码定义的
    if((i + cplen) > text.length()) cplen = 1;
    cout << text.substr(i, cplen) << endl;
    i += cplen;
}

其实2个Byte基本已经可以表示大多数中文了,除了极少的繁体字和生僻字,但是上面的代码包含了3个Byte和4个Byte的情况,感叹大佬的代码确实更加完善!

最后还要感谢yyl1025老哥的答疑,问题已采纳![6]

参考资料

[1] https://zhuanlan.zhihu.com/p/453675608
[2] https://zhuanlan.zhihu.com/p/427488961
[3] https://baike.baidu.com/item/ASCII/309296
[4] https://baike.baidu.com/item/GB18030/3204518
[5] https://stackoverflow.com/questions/40054732
[6] https://ask.csdn.net/questions/7874166?spm=1001.2014.3001.5505

你可能感兴趣的:(笔记,C++,c++,开发语言)