当前位置: 首页 > news >正文

C++初阶(10)string类

来学习STL的第一个容器——string。

1. 为什么学习string类

1.1 C语言中的字符串

C语言中,字符串是以'\0'结尾的一些字符的集合。

C语言中的字符串有两种存储方式:

  • 字符数组:
    • 用常量字符串初始化字符数组;(字符串拷贝在栈上,可改)
    • 用单个字符的元素列表来初始化字符数组;(不自动存储'\0')
  • 字符指针:
    • 用常量字符串初始化(常)字符指针;(字符串在常量区,不可改)

为了操作方便,C标准库中提供了一些str-系列库函数(字符串操作函数),但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。

面向对象程序设计、面向对象编程(Object Oriented Programming)

1.2 两个面试题(暂不做讲解)

字符串转整形数字

字符串相加

在OJ中,有关字符串的题目基本以string类的形式出现;而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。

2. 标准库中的string

来学习STL的第一个容器——string。string的含义就是“串”。

可以把string理解为传统的字符数组。

string在日常中使用得非常非常多的——这些数据结构本质就是存储各种各样的数据,:

整型、浮点是表示数据的大小,还有一些其他更复合的信息都是用string存的。


例如:身份证号码就不能用整型存,原因:

  1. 表示大小范围的问题;
  2. 有些身份证号码里面带X,就只能用字符串存。

名称、地址……现实中有很多信息都需要用字符串去存。


C语言的字符串是面向过程的实现方式(Procedure Oriented Programming)——数据和方法是分离的,数据(内存空间)是用户自己管理,方法是标准库提供,就特别不方便管理。

举个例子:

  • strcpy:把一块空间拷贝到另一块空间——两块空间都得自己提供,还得保证目的空间至少和被拷贝空间一样大或更大,如果目的空间更小,就越界了。但strcpy不管,在copy之前你自己要注意;

    数据和方法分离就会有很多的问题,用起来就会很麻烦——既要管空间,又要管方法/使用。
     
  • strcat:在当前字符串后面追加,有两个很挫的地方:

    1.会从前到尾地去找\0,效率很低;
    2.追加的空间得自己准备好,得有足够的空间——strcat不负责,它只管追加数据;

所以C语言的串使用起来是不好用的,C++的string明显好用多了。

  • string是一个管理字符数组的一个类。

string和C的char数组有相似之处,也有差别。

严格来说,string不是属于STL。

string的发展历史:

  • 祖师爷搞完模版,就去搞库,直接搞可能不太好,就先搞了个探路的——string,其实就是属于是数据结构里的串,那在祖师爷设计string的时候,HP就把STL整个给设计出来了。

string在下面,不属于STL。

可以理解为string的产生早于STL,属于是一个探路的,打先锋的。

string设计好之后,C++标准库的团队还没来得及设计STL,发现已经有人写好了。

后面就直接把这个设计好的数据结构库纳入了C++标准库里面。

学string的时候会感受到它的设计跟后面的STL的设计是有所不同的——相对不那么成熟。

2.1 string类(了解)

string类的文档介绍

其中:

string类的原名是basic_string<char>,是一个类模版。为什么是类模版???

简单来说,编码有:

  • ASCode——char的编码,表示英文;
  • UNICode——万国码,表示全世界其他国家的文字,例如中文(GBK),UNIcode里面一个非常有名的东西叫UTF8……

可以认为string是一个管理char的数组,wstring就是管理wchar(两个字节)的数组……

最常用的就是string,string就是一个char数组的串(顺序表)。


学习STL,或者学习库函数,最重要的就是要学会看文档。

string有一百多个成员函数——string的设计有点冗余。

【说明】

  1. 字符串是表示字符序列的类
  2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
  3. string类是使用char作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。
  4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(关于更多的模板信息请参考basic_string)。
  5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。

【总结】

  1. string是表示字符串的字符串类。
  2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
  3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator> string;
  4. 不能操作多字节或者变长字符的序列。

在使用string类时,必须包含头文件#include <string>以及using namespace std;

2.2 string类的常用接口说明(注意下面我只讲解最常用的接口)

2.2.1 默认成员函数

2.2.1.1 构造函数

C++11中string有9个构造——多的2个构造类型还没学过,暂时先以C++98为标准学。


七个构造函数·文档解读

前2个

  • default:默认构造——构造空串,有0个字符。
  • copy:拷贝构造——构造一个串的拷贝。

第3~5个——构造的再扩展

  • substring:子串构造——部分拷贝,仅拷贝str的pos位置(下标)开始的len个字符,或者直到str的结尾——如果str太短或者len是 string::npos 的话。
    (len带缺省参数npos,默认拷贝到结束)
  • from C-string:字符串常量构造。
  • from buffer:字符串常量部分构造——取前n个。(不足n个会越界访问)
  • fill:填充构造——用n个c字符去构造。
  • range:迭代器构造。

【说明】

  • pos位置指的是下标,即pos==3的话,是第4个元素。
  • npos是string里面的一个静态的const成员变量, 值为-1——存补码,全f,size_t类型(无符号整型)的-1是最大的正整型42亿多 (注:最大的整型INT_MAX是21亿多)。


    string不可能有那么长,所以len=npos就告诉编译器拷贝的时候直接找\0,找到才结束拷贝。

“二八原则”:只需要掌握常用的二三十个成员函数,其余的了解有这么个东西,用到的时候再查文档。


七个构造函数·使用练习
#include <iostream>
#include <string>void Teststring()
{// 常用的3个——重点掌握string s1;                //第1个构造——默认构造:构造一个空的string类对象string s2("hello world"); //第4个构造(最常用)——带参构造:用一个字符串常量去构造string s3(s2);            //第2个构造,拷贝构造cout << s1 << endl;cout << s2 << endl;cout << s3 << endl;cin >> s1;cout << s1 << endl;
}

执行结果:

原因是string默认用的char底层对应的编码是变长编码,可以兼容ASCode(英文)和UTF-8(中文)。


可以看到:

  • string是支持直接用cout打印、cin输入的——自定义类型想支持流插入、流提取,必须有流插入、流提取运算符重载。
  • 而流插入、流提取为了迎合默认使用习惯,不能重载为成员函数,只能重载为非成员函数,即全局函数。

【注意】

  • c语言的scanf和printf和cin、cout相比需要取地址,而cin、cout有了引用就方便多了。
  • 其次,对于自定义类型,cin、cout就成了唯一选择,c语言那套在这就行不通了――成员变量是私有的。

void test_string1()
{string s2("hello world"); //第4个构造(最常用)——带参构造:用一个字符串常量去构造// 不常用——了解string s4(s2, 3, 5);    //第3个构造——部分拷贝,从s2的第3个位置(下标3——第四个字符)开始拷贝5个字符string s5(s2, 3);       //第3个构造——部分拷贝,使用缺省值,有多少拷贝多少string s6(s2, 3, 30);   //第3个构造——部分拷贝,类似的场景,当错误地给到len一个长于数组剩余长度的值,也是拷贝到\0停止cout << s2 << endl;cout << s4 << endl;cout << s5 << endl;cout << s6 << endl;
}

执行结果:

len > 后面的字符长度有两种情况:

  1. 不传给len缺省值npos;
  2. 或者给len一个较大的值;

都会触发str is too short,拷贝到\0结束。


测试第5个构造:

跟第4个类似,用字符串常量进行初始化,多了一个参量n——使用前n个字符来初始化

这个也是被吐槽的部分, 很多人觉得这个构造压根就没必要, 大家几乎都不太会这么去用,太呆了。其实就是一开始设计的时候想多了 觉得大家会这样用,会那样用, 其实不是的。


测试第6个构造:

第7个构造涉及迭代器,后面再说。


【完整测试代码】

#include <iostream>
#include <string>void Teststring()
{// 常用的3个——重点掌握string s1;                //第1个构造——默认构造:构造一个空的string类对象string s2("hello world"); //第4个构造(最常用)——带参构造:用一个字符串常量去构造string s3(s2);            //第2个构造,拷贝构造// 不常用——了解string s4(s2, 3, 5);//第3个构造,部分拷贝构造,从s2的第3个位置(下标3——第四个字符)开始拷贝5个字符string s5(s2, 3);//第3个构造,部分拷贝,使用缺省值string s6(s2, 3, 30);//第3个构造,部分拷贝,类似的场景,当错误地给到len一个长于数组剩余长度的值,也是拷贝到\0停止string s7("hello world", 5);//第5个构造string s8(10, 'x');//第6个构造cout << s1 << endl;cout << s2 << endl;cout << s3 << endl;cout << s4 << endl;cout << s5 << endl;cout << s6 << endl;cout << s7 << endl;cout << s8 << endl;cin >> s1;cout << s1 << endl;
}

【总结】

(constructor)函数名称
功能说明
string() (重点)
构造空的string类对象,即空字符串
string(const char* s) (重点)
用C-string来构造string类对象
string(size_t n, char c)
string类对象中包含n个字符c
string(const string&s) (重点)
拷贝构造函数

第3个、第5个不常用。


2.2.1.2 析构函数

析构函数:

  • 释放底层空间,清理资源;
  • 自动调用,程序员不用管;

2.2.1.3 赋值重载


C++98支持3种类型的赋值,C++11支持5种类型的赋值。

  • string赋值给string;
  • 常量字符串赋值给string;;(用得少
  • char赋值给string;(用得少

也是一个比较偏冗余的设计。


代码测试——拷贝构造

void test_string2()
{//最常用的——第4种构造(两种写法)string s1("hello world");string s2 = "hello world";	//隐式类型转换(构造一个临时对象) + 拷贝构造——>优化成一个构造
}

代码测试——引用

void test_string2()
{//隐式类型转换(构造一个临时对象,具有常性)const string& s3 = "hello world";
}
  • 引用的话,好处就是没有拷贝构造,只有构造;
  • 引用的不是这个常量字符串,而是是一个临时string类对象;
  • 不能直接引用,要加const;

假设其他数据结构(如:链表、顺序表)的pushback尾插,插入的数据是一个string,以前得先创建一个有名对象,或者写匿名对象,但是现在最方便的方式是单参构造的隐式类型转换:

//尾插
void push_back(const string& s)    //引用接收
{}void test_string2()
{// 以前调用尾插,需要创建有名对象string s4("hello world");    //只有构造(没有拷贝构造。没有优化)push_back(s4);// 或者写匿名对象// 现在最方便的方式是直接传字符串——隐式类型转换push_back("hello world");    //只有构造(没有拷贝构造。没有优化)
}

2.2.2 string类对象的容量操作

怎么去遍历一个字符串???——有两种方式:

  1. 获取长度len,然后使用operator[];
  2. 迭代器;

2.2.2.1 size()
(1)遍历方式1:size() + [ ]重载

获取长度不是使用strlen——只适应于常量字符串、C语言的字符数组。

返回字符串长度——不算\0。

它们没有什么不同,那为什么会有两个不同的接口???

  • string出来地比STL早,一开始设计的string的长度接口函数叫length。
  • 后来STL出来之后又增加了一个size,就是为了跟后面的STL里面的一些容器的长度接口size保持一致。
  • 字符串string、顺序表vector的长度接口叫length都没问题,而树set还叫length就不合适了,所以STL里面为了保持一致,无论是谁,计算数据规模都是使用size,没有length这个接口。
  • 总而言之:string设计length是从个例的角度去考虑的,STL设计size是从整体去考虑的。
  • 结论:以后统一使用size()

【】重载:返回pos位置的字符。

运算符重载:1.提高程序的可读性;2.可以让自定义类型完成内置类型的操作。

来看一下一个简易的string类:

//简易的string,类似的底层结构就是这样的
class string
{
public://【】运算符重载——返回数组里面第i个位置的字符char& operator[](size_t i){assert(i < _size);	//暴力检查——STL喜欢暴力解决问题,越界直接报错return _str[i];}//传值返回char——返回的是返回对象的拷贝//引用返回char&——返回的是返回对象的别名,减少拷贝——在深拷贝的时候能有效减少消耗,浅拷贝时优势不明显//引用的2个意义——之前只讲了第一个// 1、减少拷贝——浅拷贝时优势不明显,这时候第2个意义就来了// 2、修改返回对象private:char* _str;size_t _size;size_t _capacity;//管理字符数组的顺序表,有一个指向字符数组的指针、数据规模size、容量capacity——不够就扩容
};//这个简易的string只作演示,要注释掉,留下来就跟库里面冲突了

这就是传引用返回,之前没有讲到的第2层意义——能修改返回对象。

调用operator[],返回数组第i个位置字符的别名,因为出了作用域这个数组依然还在(对象在堆上),所以可以返回别名,因为调用时使用的是this*指针,所以出了作用域还在。


常规数组越界访问,不一定能检查出来,这时候就很危险,读程序检查不出来,执行也没有报错。

string像数组一样使用时,若越界访问,则程序会暴力中止(有assrt越界检查)。

报了一个断言错误。

这也是C++比C优越的地方。


【】重载写了两个版本:

  • 普通版本;
  • const版本;(size、length都只有const版本)

为了应对常对象不能被修改。

第2种【】的重载——不是直接返回引用,而是返回常引用,保证常对象不能被修改。

这也体现了有函数重载时,编译器进行匹配的时候是匹配更适合的:

  • 如果没有下面的第2个重载,常对象也不能调用第1个 ——权限的放大;
  • 如果只提供了一个const版本,非const对象也能调用;
    • 但是普通的string对象也不让修改了,不能修改不符合需求;

【原则】

  1. 一般的成员函数只需要写一个const版本(只读)。——const对象、普通对象都能调用
  2. 引用返回的这类非const版本的成员函数,要多重载一个const版本,接收常对象,返回常引用。(可读可写)
2.2.2.2 max_size、clear、empty、capacity

capacity就是返回容量,没什么好说的。

clear清除数据;

empty判断数据是否为空。


max_size是早期的理想设计,告诉一下这个字符串能有多长,结果变成一个没啥用的接口。

实际上开不了这么大,所以max_size成了一个没什么用的接口。

21亿多字节,大概是2G的空间。

2.2.2.3 reserve()、resize()

2.2.2.3.1 reserve()

【说明】

  • VS 下reverse之后的capacity可能比预置的更大;
  • 传的值比当前capacity小,VS默认是不缩容的;
  • 除非传的值比15小,就会缩到buf;

【总结】reverse会扩容,不一定会缩容,不同的编译器不一样。
(扩容后的容量不一定是指定容量)

一般也不用reserve()来缩容。reserve的应用一般是知道需要多少空间,提前开好。

(扩容不一定指定多少就开多少,可能比指定的大——VS)

场景1:

场景2:

  • reserve之后最容易犯的一个错误就是直接访问扩容的空间。
  • []会调底层的operator[],会检查[]里的数字是不是小于size。
  • 空间变大了,数据量(size)没变,【】只允许访问有效数据部分。
2.2.2.3.2 resize()

重载了两个版本:

  • 第一个版本是不给cahr
  • 第二个版本是给char——n大于原size,会给后面补char;否则这个char没用。

当已知num1和num2的和str为76543,要存到str就不能用reserve——只扩容不扩size。

就要用到resize——对有效数据个数size的扩充,必要时会扩capacity。

s1.resize(5, '0');//开5个位置,每个位置都初始给成0

2.2.2.4 shrink_to_fit()

前面提到reserve不用于缩容——C++标准并没有规定:给一个较小值时,reserve是否要缩容。

缩容有专门的函数——shrink_to_fit()。

函数功能:让capacity去适应size,一般是让capacity和size保持一致。

使用场景:频繁地对一个容器插入、插入、删除、插入、插入、删除、删除、删除……,然后决定现在数据量不大,空间浪费得有点多,想把空间释放一下,就可以调用这个接口。

不要经常调用这个接口——这个接口效率不高。

缩容不是在原空间上缩——空间不能分段释放!!!

  • 实际的缩容不是把多的释放掉,而是开一块更小的空间,拷贝数据,再释放原空间。

所以缩容的本质是以时间换空间。 建议:缩容的消耗很大,不要轻易缩容。


【总结】

函数名称
功能说明
size(重点)
返回字符串有效字符长度
length
返回字符串有效字符长度
capacity
返回空间总大小
empty (重点)
检测字符串释放为空串,是返回true,否则返回false
clear (重点)
清空有效字符
reserve (重点)
为字符串预留空间**
resize (重点)
将有效字符的个数该成n个,多出的空间用字符c填充

【注意】

  1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
  2. clear()只是将string中有效字符清空,不改变底层空间大小。
  3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
  4. reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。

2.2.3 string类对象的访问及遍历操作

2.2.3.1 迭代器
(2)遍历方式2:迭代器

所有的iterator(迭代器)都是定义在类域里面的,受类域限制,使用的时候需要指定类域,有点像内部类,但是实际实现不一定是内部类,是typedef过的。



输出的是解引用的it1。

迭代器的遍历,就类似于指针的遍历,访问的方式——解引用,也和指针类似。

用法上类似于指针,但一定是指针吗???——>不一定,即可能是指针,也可能不是指针。

  • 迭代器的底层到底是什么,不同的平台(编译器)实现的是不同的。标准只规定执行规则和执行结果,至于底层怎么实现,不作规定。

就像构造(拷贝构造)+构造的优化,C++标准没说要不要优化,按原生语法应该全都要走,但是效率确实有点低,只要你能保证优化之后不出问题,优化不优化最终程序的结果应该是一致的,优化达到的效果不影响程序的正确性。


像VS下就不是指针,而是一个类_string_iterator,后面套了一些东西。

行为上像指针,但底层不一定是指针。


typeid既可以打印对象的类型,也可以打印类型的原类型——有些类型被typedef过。

linux下使用typeid需要包含头文件<typeinfo>,VS下不用——间接地包含了。

不同编译器,针对同一个对象的大小、类型可能都不同。

这是比较新版的g++,有些旧版的g++打印出来可能会是char*。


可以简单解释一下VS2019(X86)下的28——和正常的顺序表没什么差异,只是多了一个16字节数组。

string有:

  • 1个char*指针;
  • 2个int变量(size,capacity)
  • 1个16字节数组(buf数组)

总共是28 = 12 + 16,当字符串较小就存到16字节数组,较大就存到堆上。

VS2022的x86也是28,x64是40。


讨论:两种方式都可修改,下标+【】还更方便,那有下标+【】的形式遍历,为什么还要设计迭代器??

解答:只能说是因为——下标+【】不是主流(通用)的访问方式。

数据结构这块都得写成类模版,不然写了之后只支持一种数据类型。

写成类模版想存什么数据,就给什么——链表就是一个类模版。

  • 下标+【】的方式:要求底层是连续的物理空间——数组。
  • 只有string、vector支持这种方式
  • 所有的迭代器都支持迭代器遍历。


(3)遍历方式3:范围for

所有容器都支持这种遍历方式。

范围for是C++11从其他语言抄的,C++11里面有一些小语法比较好用,提前把范围for这个语法讲一下,以后再统一讲C++11里面的其他语法。

使用范围for,自动化程度极高。


  • 为什么所有容器都支持呢,原因就是范围for的底层其实就是迭代器。

所有的“自动”都是编译器帮你干了你该干的活。

范围for看起来全自动很优越,很神秘,但是对编译器而言,这两段代码在底层翻译成指令之后是一样的,跟模版int、double、……替代T一样,在实际语法编译之前,在预处理的时候被替换成上面那段,迭代器每次的it2解引用给e,即e=*it2。

自动++其实是迭代器自动++,自动判断结束其实是迭代器自动判断结束。


【范围for的引用】

范围for就是直直地替换,不仅能替换库里面的,自己实现的STL的拓展也能替换。

遇到范围for,编译器就去看有没有实现好的迭代器,有就调用迭代器,没有就报语法错误。


从语法层来看,学了3种字符串的遍历方式,从底层来看是两种:下标+[]、迭代器。


迭代器就学完了第一部分,简单使用。

2.2.3.2 const-迭代器

再来看到迭代器的其他内容

【const 迭代器】

string的迭代器也实现了两个版本:

  • 普通迭代器:普通对象用;
  • const迭代器:const对象用;

s1是const,所以会调用下面这个begin(),返回值是const_iterator,不是iterator。

用iterator接收,编译器就会报错。

迭代器分为const_iterator和iterator,区别就是一个只读,一个是可读可写。

  • const iterator——保护迭代器本身不能写/修改;
  • const_iterator——保护迭代器指向的数据不能写/修改;

it1本身可以修改,确保it1可以迭代往后走。

string是类模版,而模版这部分的一个缺陷就是编译报错会复杂很多:


正确用法:

可以遍历const的string对象,但是不能修改数据。


【最好的写法】——auto

  • const对象,调用const的begin,返回的是const_iterator。

【小技巧】

  • VS下把鼠标放在变量、函数上,就能知道 它的类型/返回值类型;

【切记】

  • 在清楚的情况下使用auto。
2.2.3.3 反向迭代器

除了:

  • 普通迭代器begin/end;
  • const迭代器begin/end;

还有:

  • 反向迭代器rbegin/rend;
    • 普通版;
    • const版;

iterator就是从头到尾遍历,假如想倒着遍历,

就使用rbegin、rend,返回的是反向迭代器reverse_iterator。

【反向迭代器】

  1. 位置是反的;
  2. ++的方向也是反的;(重载++,封装的--,反向迭代器是使用适配器模式实现的)


总结:严格来说,这里的迭代器总共有4种

  • iterator(最常用)
  • const_iterator
  • reverse_iterator
  • const_reverse_iterator

实际:string不爱用迭代器,无论正向反向迭代器,下标+【】都能完成,而且更方便。


2.2.3.4 c-迭代器

迭代器就讲到这,实际上还有一些迭代器——C-系列(设计冗余)

迭代器讲了这么多,总结一下: string不爱用迭代器的用法。

但是从学习的角度要学得细一点,需要掌握好, 因为后面通用遍历方式还是迭代器。

实际上就是想把begin的第2个重载——const版本,给独立出来。

cbegin相当于begin的const版本。

是C++11增加的,意在形成区分,不想混着用,但是很多人用惯了之前的用法,不爱用新函数。


之前讲string的遍历,相比于迭代器,更倾向于使用下标+【】。

那迭代器有没有下标+【】替代不了的功能呢???——有,例如:排序。

sort对字符串的迭代器,按字典序、升序进行排序。

我们去访问容器:借助迭代器。

算法去访问容器:借助迭代器。

  • C++的迭代区间一定是左闭右开[ , )。

sort的第一个迭代器参数:是一个闭区间位置,指向一个有效位置。

sort的第二个迭代器参数:是一个开区间位置——即不作为本次算法的有效位置。


sort的优点:

  1. 是函数模版,可以支持各种类型的数据结构——string、vector(list不行)
  2. 传的是区间,就很灵活,可以对整个数据进行处理,也可以对部分数据进行处理

2.2.3.5 at

at可以访问pos位置的字符。

at和[ ]的区别:

  • []是暴力检查,一旦越界访问,程序就直接中断并报错——断言;
  • at是抛异常;

功能是一样的,只是底层处理越界的方式不同。

第2种方式的优势在于可以捕获异常。

2.2.3.6 front和back

功能:访问首、尾元素。

[ ]和at也能访问首尾元素。

str[0];
str.at(str.size()-1);

【总结】

函数名称

功能说明

operator[](重点)

返回pos位置的字符,const string类对象调用

begin + end

begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器

rbegin + rend

begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器

范围for

C++11支持更简洁的范围for的新遍历方式

2.2.4 string类对象的修改操作

接下来看字符串修改。

2.2.4.1 push_back()

第一种修改:插入——3个函数。

2.2.4.2 append()

为什么说string设计得比较挫,没有什么经验呢,从这个地方就能看出来:

插入有push_back——尾插一个字符。

还有append(追加)——不仅能追加一个字符,还能追加一个字符串。

append设计得很复杂——和string的构造设计得很相似。

代码练习:

2.2.4.3 +=重载(重点)

实际上,push_back和append都不喜欢用,最常用的还是+=的重载。

重载哪些运算符是看实际需求,+=重载就比较好用了。

注:没有-=的重载。


2.2.4.4 pop_back()、erase()

尾删pop_back,删除一个字符,就不太好用了。

erase可以从一个位置开始删除一段。

(1)删除pos位置开始的len个字符——直接调用erase(),使用缺省参数,全部删完。

(2)删除迭代器位置的1个字符。

(3)删除迭代区间内的字符。

之前写数据结构删除用erase不用delete,就是跟着STL库走的。


2.2.4.5 assign()、insert()

再来看看modifiers里面的其他函数。

assign本质是一种变相的赋值:清空当前的内容,填入新的内容。

但是已经有默认成员函数operator=了。

assign在实践当中用得非常少,用assign也是一般只用(1)或(3)——string设计得比较冗余。

代码练习:


  • string类有尾插尾删,但是没有头插头删——效率不高。
  • 但是间接支持了头插、头删——insert()、erase()。
  • 慎用,效率不高
    (顺序表的插入、删除,时间复杂度O(N))
  • 实践中需求也不高。

string的设计确实是当时经验偏少,设计得比较乱。

第(5)个:在pos位置插入n个c——由于没有(size_t pos,char c)这个形式,做不到在指定位置插入一个字符,所以只能用(size_t pos,size_t n,char c)来头插一个字符——(0, 1, 'x')。

第(7)个:在s2的p位置之前插入s1的[first,last)迭代区间的数据。

  • C-Str:C语言风格的字符串——常量字符串、const char*、字符数组char ch[]。
  • 特点:以'\0'结尾。

  • 不可能用字符串的插入取代字符的插入。

注意使用迭代器,在写迭代区间时要写正确:

string的设计冗余是C++的缺陷之一,这是语言发展的历史导致的,当时人家设计出来可能感觉也还蛮好,但是后来出现的了更好的东西,就会发现string设计得确实不太好。但是也无法改变,一旦改变,很多陈旧的代码就需要大改特改,所以后面的C++标准只能在兼容旧标准的同时增加新标准,而不太好直接修改旧标准。

2.2.4.6 replace()

replace——替换,把我的一部分替换。

replace这个接口也是设计得非常冗余:

(1)下标版本:把从pos位置开始的len个字符替换成string。

        迭代器版本:把迭代区间[i1, i2)替换成string。

(2)把从pos位置开始的len个字符替换成str的一部分。

(3)把从pos位置开始的len个字符替换成char*
(有了(1),这个版本就不必要——const char* 可以类型转换,构造const string&,但是效率会低)

(4)把从pos位置开始的len个字符替换成char*的前n个。

(5)把从pos位置开始的len个字符替换成n个c。

(6)把s1的迭代区间[i1, i2)替换成s2的迭代区间[first, last)。


总结一句话:把str从pos位置开始的len个字符替换成string

  • string能完全插入进去。
  • str也完全固定消失从pos位置起的len个字符。

那显然len小于string的长度时,string要完全替代进去就需要扩容。

同时len大于string长度时,string替代进去后,字符串缩容了。

void test_string9()
{//C语言做过一个题,把字符串里的所有空格,都替换成%20string s3("hello world hello bit");for (size_t i = 0; i < s3.size(); ){if (s3[i] == ' '){s3.replace(i, 1, "%20");i += 3;}else{i++;}}cout << s3 << endl;//不建议这样写,因为s3.replace(i, 3, "%20")是不用挪动数据//而s3.replace(i, 1, "%20")是要挪动数据,效率比较低//建议写法1:如果在原串上搞,建议从后往前替换,并且数一下多少个空格一次全部挪动到位//建议写法2:如下string s4("hello world hello bit");string s5;for (auto ch : s4)//用范围for遍历s4{if (ch != ' '){s5 += ch;//不是空格就接收}else{s5 += "%20";}}cout << s5 << endl;//效率高多了(但是这种写法牺牲了空间,绝大多数情况下都是要时间不要空间)
}
2.2.4.7 string类的扩容测试

其实第一次不算2倍扩容,一开始是存在string内的buf数组(16字节) ——栈上。

满了之后,再到堆上开新的空间来存储。

如何扩容:1.5倍、2倍……,C++标准并没有规定,由编译器自己实现。

linux的g++(版本4.8)底层就没有一个预留数组buf,容量从1开始,逐渐2倍扩容下去。

  • VS下扩容的基准:capacity + 1,扩容1.5倍;(实际大小比capacity大1)
  • G++下扩容的基准:capacity,扩容2倍,再+1;(实际大小比capacity大1)

由于连续的扩容(一般都是异地扩容)有较大的消耗,这里可以用reserve提前开好空间。

【空串】

  • 有效数据个数:都是0。
  • 实际大小:
    • VS下的空字符数组也有一个\0;
    • G++的空string也有一个\0;
    • VS的空string有16个\0

VS的string相当于是空间换时间,处理起来相对麻烦,但是长度小于16的大量常用字符串都不用去堆上申请空间,提高了效率,减少了内存碎片。(有内存池的思想在这里)

linux环境下,string的\0也不算在capacity内。


swap()函数暂时不太能讲清楚,之后把深、浅拷贝讲了,模拟实现的时候再来看这个swap()。


【总结】

函数名称
功能说明
push_back
在字符串后尾插字符c
append
在字符串后追加一个字符串
operator+=(重点)
在字符串后追加字符串str
c_str(重点)
返回C格式字符串
find + npos(重点)
从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
rfind
从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置
substr
在str中从pos位置开始,截取n个字符,然后将其返回

【注意】

  1. 在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
  2. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。

2.2.5 string类操作函数

2.2.5.1 c_str()

VS下就是返回数组首元素的地址。

应用场景:用适配C的接口函数去操作string时没法传参,string需要转换成C风格的字符串。

有些库只提供C的接口,这样C++兼容C,C++和C都能使用这个库。

2.2.5.2 data()

data和c_str类似,平时只用c_str就够了。

data和c_str的关系类似于size和length——不同时期的产物导致的。


2.2.5.3 其他
  • get_allocator():是指string下面的空间配置器,这些数据结构底层都用了一个内存池,后面再讲。
  • copy():就是把string的一部分拷贝到char*(C风格字符串)里面去。(很少用)
2.2.5.4 find()、rfind()、substr()

(1):查找一个string对象;(可指定开始查找的位置,默认从0位置开始查找)

(2):查找一个C-字符串

(3):查找一个C-字符串的前n个

(4):查找一个字符

注:const char*不一定是常量字符串,而是C风格字符串,非常量C字符串可以权限的缩小传参。

rfind()就是倒着找。

用从pos位置开始的len个字符生成一个子串返回。

  • len比较长、或、len没给,就取npos——pos之后全用于生成子串。
void test_string12()
{//find用于取后缀名——不能只找“.”——因为可能有多个“.”,而后缀是最后一个点后面的字符串//所以可以使用rfind倒着找“.”string file("string.cpp.zip");size_t pos = file.rfind('.');		//找到.的位置pos//找到之后取后缀——substr//string suffix = file.substr(pos, file.size() - pos);//具体给出字符个数就是——file.size() - posstring suffix = file.substr(pos);	//也可以不给字符个数——从pos位置之后一直取完cout << suffix << endl;
}


【练习】划分URL的三大组件

域名用来获取服务器的IP地址。

想把网络连接的3部分分别取出来:

  • 协议https    
  • 域名gitee.com 
  • 路径ailiangshilove/cpp-class/blob/master/TestString.cpp

用这个协议,在这个域名描述的服务器上,取这些资源——不同的网页,后面的资源定位符部分都是不一样的。

  • 核心操作:不断地用find找到这些标志,然后取子串。
void test_string12()
{string url("https://gitee.com/ailiangshilove/cpp-class/blob/master/TestString.cpp");//核心操作:查找+取子串// 1.查找——查找协议// 从起始位置开始找(从h开始找),找“冒号”size_t pos1 = url.find(':');			//不给pos,默认从pos=0(缺省值)开始找// 2.取子串——取协议string url1 = url.substr(0, pos1 - 0);	//左闭右开的区间[0,10),这样的区间,左右下标相减就是个数;左闭右闭,则:右-左+1,才是个数cout << url1 << endl;// 1.查找——域名// 从中间开始找(从g开始找),找“第一个斜杠”size_t pos2 = url.find('/', pos1 + 3);        //g在pos1+3的位置// 2.取子串——取域名string url2 = url.substr(pos1 + 3, pos2 - (pos1 + 3));cout << url2 << endl;// 2.取子串——取路径// 从中间开始取到结束(从a开始取到结束)string url3 = url.substr(pos2 + 1);           //a在pos2+1的位置cout << url3 << endl;
}

注意取子串substr函数的两个参数:

  • 取子串的起始位置
  • 取子串的长度

所以取域名要:从pos1+3位置开始取,一共取pos2-pos1-3个字符。

2.2.5.5 find_first_of()、find_last_of()

2.2.5.6 find_last_of()

相当于倒着走的find_first_of:

倒着找,可以用于分装路径和文件:

//find_last_of
void SplitFilename(const std::string& str)
{std::cout << "Splitting: " << str << '\n';					//split分装std::size_t found = str.find_last_of("/\\");				//实际的调用是str.find_last_of("/\")std::cout << " path: " << str.substr(0, found) << '\n';		//对路径、文件进行分装std::cout << " file: " << str.substr(found + 1) << '\n';
}
int main()
{std::string str1("/usr/bin/man");				//linux下的路径是右斜杠std::string str2("c:\\windows\\winhelp.exe");	//windows下的路径是左斜杠//这里双左斜杠是转义成单斜杠,单斜杠表示转义不是真的斜杠//实际的str2是c:\windows\winhelp.execout << str2 << endl;//打印出来看只有单斜杠SplitFilename(str1);SplitFilename(str2);return 0;
}

  • find_first_not_of:找不是这些里面的任何一个在str中首次出现的位置。
  • find_last_not_of:倒着找不是这些里面的任何一个在str中首次出现的位置。
2.2.5.7 compare()


2.2.6 string类非成员函数

2.2.6.1 +重载
  • =重载是默认成员函数
  • +=重载是成员函数
  • +重载是非成员函数

string operator+(const char* lhs,const char* lhs),两个参数(对应两个操作数)都是内置类型。

这是不支持的——不支持对内置类型进行运算符重载,至少有一个是自定义类型。
——成员函数天然满足。

  • +重载为了支持后两种用法的加法交换律,所以重载为了全局函数。

2.2.6.2 关系运算符重载

这6个重载函数中,它们每个的后两个版本的const char*可以隐式类型转换成const string&,属于是没必要。

可能是不想要消耗,因为早期的电脑性能不高,能少一点消耗是一点。

所以就考虑了很多现在看来很冗余的设计。
(早期电脑性能不高,所以早期代码风格的{}并不提行写)

  • 为了支持C风格的字符串、C++的字符串在比较运算符哪一端都能正常比较,只能重载为全局函数。

注意操作符优先级问题,要加括号。

【字符串比较】——不是按首字母,而是按ASCII码比(按字典序)

  1. 比的不是总体的ASCII码总值,而是按顺序比,相同才往下继续比,不同直接返回。
  2. 如果前面都相同,谁先走到\0谁小,\0不参与比较,例:“hello” < “hello11”
  3. 返回值都是布尔类型。
2.2.6.3 其他

返回一个istream类的对象。


【总结】

函数
功能说明
operator+
尽量少用,因为传值返回,导致深拷贝效率低
operator>>(重点)
输入运算符重载
operator<<(重点)
输出运算符重载
getline(重点)
获取一行字符串
relational operators(重点) 大小比较

上面的几个接口大家了解一下,下面的OJ题目中会有一些体现他们的使用。string类中还有一些其他的操作,这里不一一列举,大家在需要用到时不明白了查文档即可。

2.2.7 vs和g++下string结构的说明

注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。

(1)vs下string的结构

string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字符串的存储空间:

  • 当字符串长度小于16时,使用内部固定的字符数组来存放。
  • 当字符串长度大于等于16时,从堆上开辟空间。

这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。

其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量

最后:还有一个指针做一些其他事情。

故总共占16+4+4+4=28个字节。

(2)g++下string的结构

G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:

  • 空间总大小
  • 字符串有效长度
  • 引用计数
  • 指向堆空间的指针,用来存储字符串。

2.2.8 <string>库函数

C语言的字符串转整型是用atoi。

C++也有现成的库函数:string_to_int、string_to_long、string_to_unsignedlong……

注意:字符串转整型有可能存在整型溢出的问题——存不下。


OJ题“大数运算”不能转成int来算。

2.2.9 OJ练习

(1)仅仅反转字母

经典的左右双指针法的应用就是单趟快排。
(单趟快排只能确定一个key的位置,之后还要递归调用)

这里一趟左右双指针法跑完,题目的要求就完成了。

class Solution {
public://判断是字母bool isLetter(char ch){if(ch >= 'a' && ch <= 'z')return true;if(ch >= 'A' && ch <= 'Z')return true;return false;}string reverseOnlyLetters(string S) {if(S.empty())return S;//和快排一样,左边0,右边size-1,快排右边先走因为左边作key,这里哪边先走都可以size_t begin = 0, end = S.size()-1;while(begin < end){  //不是字母,left一直往右走  while(begin < end && !isLetter(S[begin]))++begin;//不是字母,right一直往左走 while(begin < end && !isLetter(S[end]))--end;//是字母,则交换完再继续走   //交换——当begin和end在同一位置,交换也没事,下面直接就能出循环了swap(S[begin], S[end]);++begin;--end;}return S;}
}

【说明】

  • 交换可以用下标+[],也可以用迭代器。
  • 使用迭代器要注意:left=begin(),right=end()-1注意right不要用rbegin——类型不一样,不能比较。
(2)找字符串中第一个只出现一次的字符

力扣(接口型)、牛客(IO型)

找单身狗是整型数组(异或法),这道题是字符数组。

这道题的要求还是找第一个唯一字符,说明可能有多个唯一字符。

异或还有一个缺陷就是只能解决重复元素的重复次数为偶次的场景,当重复元素的重复次数为奇次时,异或也是不适用的。

【思路】 

  1. 异或——有较大的局限性,在这里不适用。
    ① 有多个唯一字符的时候,不适用;
    ② 异或只适用于偶数个重复——如成对重复,奇数个重复用不了。
  2. 暴力查找——可以用,效率极低——O(N^2)。
  3. 统计字符出现次数——映射,类比计数排序的思想(适合范围集中的),这里字符最多256个,更何况这里只包含小写字母,范围集中。【建立映射的思想】

统计完次数,如何找第一个只出现1次的字母?

思路1:去看计数数组里的第一个1对应的元素——>不行。

上图第一个1是a,但是a不是第一个只出现一次的,所以不能按计数数组里的顺序找第一个1。

思路2:按字符串的顺序,从头到尾遍历字符串,去对应看计数数组的出现的第一个1。

这道题要求返回索引(下标),所以第2个for循环不能用范围for。

接口型的OJ题更简单,只需要写一个接口函数。

IO型的OJ题比较复杂,要写完整的测试代码。

【说明】

  • ①:连续的“判断+输出”,最好创建一个变量接收函数的返回值;不然有两个麻烦:
    • 第一个麻烦就是会进行两次函数调用,有不必要的多一次消耗。
    • 第二个麻烦就是对于记忆性的函数调用,第二次函数调用的结果不一样时就会出错
  • ②:要注意操作符的优先级,最好加个括号,否则一直输出0(cout << ret == -1)
  • ③:三目操作符的经典陷阱

(3)字符串里面最后一个单词的长度

跟之前的找字符串后缀类似。(倒着找)

#include<iostream>
#include<string>
using namespace std;
int main()
{string line;    //存储流插入的数据// 不要使用cin>>line,因为会它遇到空格就结束了// while(cin>>line)while(getline(cin, line))            {size_t pos = line.rfind(' ');    //倒着找空格cout << line.size() - (pos + 1) <<endl;}return 0;
}

  • 空格和换行都会导致cin读取结束。
  • 一敲换行就下来了。
  • 但是敲空格不会马上下来,cin不会马上去读。
  • 刷新缓冲去读取数据,的标志是换行,空格不会,但是空格是默认的分割符。
  • 按换行,刷新缓冲区之前,还能移动光标位置、删除控制台以打出来的字符、……


循环结束方式①:Ctrl+z、回车

循环结束方式②:Ctrl+C(杀进程)


(4)验证一个字符串是否是回文

只有字母才是小的+32==大的,用来判断大小写。

所以想用那个三目判断,还要先判断小的是字母。

class Solution {
public://判断是:字母、数字bool isLetterOrNumber(char ch)    {return (ch >= '0' && ch <= '9')|| (ch >= 'a' && ch <= 'z')|| (ch >= 'A' && ch <= 'Z');}bool isPalindrome(string s) {// 先遍历string,把小写字母转换成大写,再进行判断(或大→小)for(auto& ch : s){if(ch >= 'a' && ch <= 'z')ch -= 32;}//for(auto& ch : s){//    if(ch >= 'A’&& ch <= 'Z')//        ch += 32;//}//遍历把大写全部转换成小写——范围for的参数加个&,可修改//不用判断“大写 == 小写”,直接全部转换成大写(小写),然后判断相等int begin = 0, end = s.size()-1;while(begin < end){while(begin < end && !isLetterOrNumber(s[begin]))++begin;while(begin < end && !isLetterOrNumber(s[end]))--end;if(s[begin] != s[end])    //直接判断是不是都是大写(小写)、同一个数字{return false;}else{++begin;--end;}}return true;}
};
(5)字符串相加

这道题是大数运算的一部分。

字符串的加减法,很滑稽的思路是使用atoi(),算完在itoa()——这道题本来就是int存不下才转换成string。

使用string的遍历之处:

  1. 方便遍历
  2. 加完的结果存入ret_string,若是C语言解就需要在计算之前,预开好用于存储结果的空间,但是不确定具体开多少合适。(加法还好,加完最多多开一位的空间,乘法就不好开了)
class Solution {
public:string addstrings(string num1, string num2)     {// 找到个位,从后往前相加// 相加的结果到字符串可以使用insert头插// 或者+=尾插以后再reverse过来//下标变量(位置变量)——使用下标比迭代器更好操控int end1 = num1.size()-1;int end2 = num2.size()-1;int value1 = 0, value2 = 0, next = 0;    //保存进位值的变量string addret;while(end1 >= 0 || end2 >= 0)  //长的字符串结束,加法才结束,短的字符串结束,加法继续{//value1 = num1[end1]//value2 = num1[end2]      //不能直取字符,要转换成对应的数字,还要注意迭代,还要注意先判断字符串还没结束再取if(end1 >= 0)value1 = num1[end1--]-'0';     //char→intelsevalue1 = 0;                    //短的结束了,要取0给长的字符串继续加(或者直接给短的把前面的0全部补上)if(end2 >= 0)value2 = num2[end2--]-'0';elsevalue2 = 0;//取各位置的数据,求该位的和(注意加上进位值)    //value1 = end1 >= 0 ? num1[end1--] - '0' : 0;//后置--是返回--之前的值//value2 = end2 >= 0 ? num2[end2--] - '0' : 0;//在里面--的好处是end小于0就直接走数字0,就不减了,在外面减,就end<0之后还会继续减//求和int valueret = value1 + value2 + next;        //处理进位——在下一次循环发挥作用if(valueret > 9){next = 1;valueret -= 10;}else{next = 0;}//处理进位,两句代码就够了——不用判断//next = valueret / 10;        商//valueret = valueret % 10;    余数//string的头插一个字符——没有直接的(0,'0'+valueret)的调用方法,有以下两种写法//addret.insert(0, 1, valueret+'0');              //这种可读性不如下一行那种//addret.insert(addret.begin(), valueret+'0');    //int→char//连续头插,每次挪动的次数成等差数列//尾插+reverseaddret += (valueret+'0');}//处理最高位的进位if(next == 1){//头插//addret.insert(addret.begin(), '1');//尾插addret += '1';}//若是头插,就不用;若是尾插,就需要reversereverse(addret.begin(), addret.end());return addret;}
};

【总结】

  1. 从后往前遍历计算;
  2. 位置变量 & 接收变量,str变量,进位变量;
  3. 循环结束条件;
  4. 如何接收数据;
  5. 迭代——在什么位置;
  6. char<——>int转换;
  7. 处理进位;
  8. 取和,头插;
  9. 处理最后的进位;
  10. 时间复杂度;

原本时间复杂度是O(N)——遍历,取值,求和。

但是头插把时间复杂度拉到O(N^2)。


更好的方式是尾插+逆置——时间复杂度O(N)。

reverse在算法头文件里面。

这是个模版函数,可以传任意类的迭代器。(通用模版函数)


优化:


其他思路:提前开好空间,然后使用下标访问的方式存储每一位的和。

思路分析:不太好,只有算完了才知道是不是会多一位,要是没多还得往前挪一位

思路分析:再者而言加法还好,对于乘法就不适用了。

思路分析:这样子的优化也不能用reserve,要用resize。


(6)翻转字符串II:区间部分翻转

……

(7)翻转字符串III:翻转字符串中的单词

……

(8)字符串相乘

……

x. 编码

  • buff1内存里存的不是字符,是ASICC码;
  • buff2里面也是存的编码——负码;

监视的对应行的值97旁边的'a'只是方便观察内容。

从最真实的内存窗口来看,只存储了97(十六进制61)。

最开始发明计算机的先驱,希望在计算机中表示文字——只考虑英文,面临的2个问题:

  • 文字怎么存储?
  • 文字怎么表示?

计算机中只有“0、1”两种信号,怎么表示文字?

美国使用的文字系统:字母abcd……、数字1234……、特殊符号><&%#……合计100+。

于是想到给每一个“符号”一串“二进制代码”,这个过程就叫 编码

(映射:一串码值,对应一个具体的符号)

于是形成了 编码表 。最早产生的编码表就是 ASCII编码表

  • 在内存中存储的是二进制数字编码,在显示器上显示的是对应编码映射的字符。

这个数字编码的范围是:0~127,128个数字——7位二进制,就能完成对美国文字体系的编码。


早期的计算机只在英美使用,后来要加入表示世界各国的文字、符号,就需要新的编码表。

这时就产生了 Unicode(万国码)


有3套编码方案:

  • UTF-8
  • UTF16
  • UTF32


【UTF-8的特点】

  1. 变长编码;
  2. 兼容ASCII;

注1:基于这两个特点,UTF-8是最常用的“非ASCII”编码

注2:linux下的默认编码就是UTF-8。

注3:windows默认是GBK。(汉化版)




UTF-8的变长编码:

  1. 如果是1个字节:第一个比特位是0(标识位),实际编码位是7个,执行ASCII码对应关系
  2. 如果是2个字节:第一字节前3个比特位是110,第二字节前两个比特位是10,实际编码位是11个,执行2字节码表。
  3. 以此类推……

如此:

常见的汉字(约2000个)编成双字节,不那么常见的编成三字节,再不那么常见的编成四字节

大部分国家的文字都可以用双字节编码覆盖。(256*256 = 65535)


UTF-8是支持全世界文字的编码系统,对中国文字体系的支持并没有特别完善,一些不常用的字支持得不是特别好,于是中国出了自己的编码标准GBK。


  • UTF-8针对不同情况有1字节、2字节、3字节、4字节不同的编码表;
  • UTF-16就是定长2字节编码;
  • UTF-32就是定长4字节编码;

  • UTF-16和UTF-32的存储成本都比较高,实践中用得比较少


  • string本质就是basic_string<char>(类型重命名)

有时候会看到typedef那个位置是using,这是把using这个关键字又重新定义了一种用法,在这里和typedef是同一个含义,极个别地方using有一些些的好处。

string默认支持UTF-8编码,但是:

无论编码如何,string 的所有操作(如 length()、迭代器等)都以字节为单位不会考虑字符的实际编码边界


c++98只有string、wstring(存储宽字符),c++11新增u16string、u32string。


总结:计算机底层存储的还是0-1序列,只是说按哪种编码方式去存,显式的时候就按哪种编码方式去取。

ASCII值就是字符在内存中按ASCII编码方式编码的对应存储值。

汉字在内存中按对应编码方式编码的对应存储值可能叫UTF-8值。

http://www.dtcms.com/a/394262.html

相关文章:

  • 高性能开源 Web 服务器软件--Nginx
  • 软考中级习题与解答——第十章_多媒体技术(2)
  • 【字符串】1.最⻓公共前缀(easy)
  • 新闻源发稿平台推荐,企业形象宣传新闻源收录平台
  • 梯度提升框架深度解析:LightGBM、XGBoost 与 CatBoost
  • Win10服务器远程连接断开后.bat脚本进程中断的全面解决方案
  • Java与Vue构建资产设备全周期管理系统,覆盖采购、入库、使用、维护至报废全流程,支持移动端实时操作与后台智能管理,提供完整源码便于二次开发
  • Spring Boot 3 + MyBatis-Plus + SelectDB整合方案
  • xtuoj 0x05-B Colombian Number
  • elasticsearch8.1.0 中聚合功能的执行链路
  • WindowTop:提升工作效率的窗口管理工具
  • 每天新增1000万条订单,如何选择合适的数据库?
  • LLaVA模型学习-周报十四
  • LwIP 1.4.0 移植到 uCOSII 参考
  • 【LeetCode 每日一题】3541. 找到频率最高的元音和辅音
  • Arithmetics Competition(贪心+排序+前缀和)
  • 运维安全07 ,JumpServer(堡垒机)介绍以及使用
  • 数据结构算法学习:LeetCode热题100-双指针篇(移动零、盛水最多的容器、三数之和、接雨水)
  • 2025年ESWA SCI1区TOP,复杂威胁环境下带偏差采样的多无人机路径规划、候选物评估与路径重构问题,深度解析+性能实测
  • SeaTunnel 迁移 MySQL 数据到 Easysearch 之批量导入(Batch)
  • JavaWeb 课堂笔记 —— 19 SpringBootWeb案例 文件上传
  • 《时空回响--时之鳞》的现代意义与2025年的现实映射
  • Qwen3Next注意力机制详解与实现
  • .net 8自包含应用发布在Linux怎么运行
  • 第十七周 学习周报
  • 手眼标定问题总结
  • 第一章 假设二:走势是时间信息序列
  • MLP全连接网络
  • 任天堂GBA游戏ROM分类精选合集 GBA工具、GBA模拟器分享 GBA金手指全集+GBA转换器
  • STM32光强传感器实验详解