[C++]详解STL-string类

[STL] string类

0. 为什么要学习string类?

string类实际上就是对字符串进行了封装,在C语言中就有字符串数组,为什么要C++要对字符串进行封装?是因为C语言中的字符串数组在使用时有很多不方便的地方,比如对字符串的修改,甚至是字符串的长度不够需要进行扩容,使用时还容易造成越界,由于这些对字符串的使用需求,C++作为一个面向对象的语言,就对字符串进行了封装,这样再使用字符串时,用户不用再对内存空间进行操作,很多功能只需要使用string类提供的函数就可以实现。

注意:由于STL的实现有很多版本,此文中使用的是visual studio编译器实现的版本。不同版本的STL可能会在扩容规模等不同地方的实现有所不同。

1. 标准库的string类

1.1 关于string类

image-20230313101559926

image-20230313101902490

通过查阅文档发现string类是对basic_string类实例化后的重命名,为什么不直接定义string类?观察basic_string的说明:basic_string是任何字符类型的类字符串的泛化。实际上字符类型是不只有一种的,string类就是对char类型字符的实例化。

basic_string所实例化出的类有下面几种:

image-20230313102441388

basic_string实例化出多种类型是因为有多种字符类型,字符类型为什么会有很多种?这就涉及到编码的问题了,我们知道计算机底层只能存储0和1,但是我们会有存储和显示字母、数字、符号的需求,为了实现这些需求,就需要编码的帮助,学习C语言时ASCII码是很常见的编码:image-20230313104659503

image-20230313104808938

ASCII码表中每一个字母、数字、符号都对应一个数字,计算机虽然不能存储字母、数字、符号但是能够存储数字,在计算机上显示字母、数字、符号的原理就是对存储的数字按照编码规则翻译,然后用图像技术处理显示在屏幕上。

如果只是存储字母、数字、符号(使用ASCII码)用char类型字符占用一个字节空间来存储绰绰有余,但是计算机不会只显示字母、数字、符号、为了满足显示别的文字等等的需求,创造了除ASCII以外的编码:

image-20230313105342826

在统一码中显示不同的内容,需要存储编码的空间不同,为了满足存储不同内容的需求,需要有不同类型的字符(实际上就是字符的存储空间不同),因此basic_string实例化出许多类型。

几种类型字符存储的类型不同:

image-20230313110100397

image-20230313110208795

image-20230313110223035

汉字的编码在Windows下采用的是gbk编码:

image-20230313120820092

image-20230313121851277

image-20230313121835592

gbk兼容ASCII码,第一个比特位存储0也就是显示为正数会去ASCII码寻找对应编码,第一个比特位存储1也就是显示负数就会去汉字中寻找对应编码,编码基本上就是这样的原理。

image-20230313122212957

汉字的编码规则会把同音字编码到一块,这一点常常被应用于屏蔽词功能,比如在游戏中我们想和队友进行激情的交流时有些字会变成**,这些字我们打不出来就会去打同音字,利用这个规则很方便的就把同音字也一起屏蔽了。

1.2 了解构造函数

string类函数由于是早期设计的,设计出100多个接口函数,很多函数是不常被使用的,因此本文只会涉及比较常用的函数。

string类内重载了许多个构造函数:

image-20230313124507524

image-20230313124442107

首先我们来看第一个不传入参数的就是默认构造函数,第四个是使用常量字符串进行构造

image-20230313124935654

image-20230313125203076

image-20230313125211463

注意图中s3的构造过程,单参数的构造函数传入参数可能会发生隐式类型转换,"hello world!"字符串会被构造成string类,然后拷贝构造给s3,但是visual studio下会优化成直接用字符串构造s3。

image-20230313125639826

image-20230313125648344

第二个是拷贝构造函数,第三个是将string类从pos位置往后取len长度的字符进行拷贝构造:

image-20230313130128751

注意第三个函数,如果被拷贝的string类内没有len长度的字符可以拷贝就相当于把pos位置后所有字符拷贝,该函数第三个参数给了一个缺省值:

image-20230313130330400

由于是无符号整数,-1的补码会被解析成4294967295,当然string类也不能存储这么多的字符,该缺省值的意义就是如果不给值,该函数会把pos位置后的所有字符拷贝。

image-20230313130724604

image-20230313130730062

第五个函数的功能是用字符串的前n个进行构造:

image-20230313130932856

image-20230313131229786

image-20230313131236408

第六个函数是用n个字符构造string类:

image-20230313131409062

1.3 了解容量相关函数

和容量相关的函数如下:

image-20230313135957452

image-20230313140040404

image-20230313140102020

image-20230313135937961

size和length都是返回string类内字符的长度的函数,用法是一致的,由于STL中其他容器的长度函数都是size建议使用size函数。

image-20230313140229886

image-20230313140609437

文档中说max_size函数返回string类能达到的最大值,实际上它返回的是int的最大值,string类达不到这么大,这个函数实际意义不大。

image-20230313140440794

image-20230313140524905

capacity函数返回的是string类申请的空间能够存储的字符大小(visual studio 下不包含\0的大小)。

通过监视窗口来查看string类申请的空间大小:

image-20230313143308535

在讲解扩容函数前,可以看看visual studio下的一种扩容机制:

#include <iostream>
#include <string>

using namespace std;

int main()
{

	// 观察扩容情况  -- 1.5倍扩容
	string s;

	size_t sz = s.capacity();
	cout << "making s grow:\n";
	cout << "capacity changed: " << sz << '\n';
	for (int i = 0; i < 100; ++i)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity changed: " << sz << '\n';
		}
	}
}

image-20230313145339826

在vs下对string类字符不断插入单个字符观察它的扩容情况(注意capacity所指空间不包含\0)第一扩容是二倍扩容,后续扩容大概是1.5倍扩容,实际上每次扩容都是1.5倍扩容,这vs下的实现有关:

image-20230313145643876

通过监视窗口查看vs下的string类有两个成员变量一个是buf一个是ptr一个string类构造时默认会开16个字节的空间也就是buf所指的空间,一旦buf所指的空间不够用了就会先开一个32个字节的空间交给ptr,然后将数据给到ptr所指的空间,在ptr中每次空间不够用就会1.5倍扩容,在使用ptr所指的空间后,buf所指的空间就被抛弃了。

//模拟vs下的string类成员变量实现
class string
{
private:
	char _buf[16];
	char* _ptr;
	int size;
	int capacity;
};

image-20230313150509585

由于不同的STL实现不同,Linux下的string类就没有类似于buf的成员变量,每次扩容是二倍扩容,上面的代码在Linux下的运行结果:

image-20230313150648678

了解了一些vs下的底层实现,我们来看string类的扩容函数:

image-20230313151621081

image-20230313151607356

reserve函数的功能是保留原有数据进行扩容,vs下扩容后的空间一般都比用户所指定的大,这和它的底层实现有关,该函数可以帮我们提前开好空间,减少扩容,提高效率。

还有一个扩容函数是resize函数:

image-20230313152851183

resize函数还有初始化的功能:

image-20230313152950168

同样是扩容100,reserve函数不会对数据进行修改,resize函数会把扩容出的空间内除了原有数据以外的数据默认修改为0

image-20230313153420278

image-20230313153325824

resize重载了可以自定义修改内容的函数:

image-20230313153552843

image-20230313153519070

resize函数有缩容的功能,可以将不要的数据部分清除

image-20230313153902083

resize的缩容实际上就是修改了可以读取的字符个数,并没有将清除的数据从空间上清除,因为C++不支持清除一部分空间的操作,如果支持了内容管理的机制将会很复杂。

image-20230313141803826

shrink_to_fit函数的功能是将string类的空间容量(capacity)相应数据容量(size)进行缩容

image-20230313154630311

如果capacity远大于size大概率是会进行缩容的,如果capacity和size接近大概率不会缩容,由于C++中内存管理机制,缩容函数的代价较大,使用缩容函数需要谨慎。

1.4 了解修改函数

image-20230313142300606

image-20230313142715063

image-20230313142745917

image-20230313142621552

push_back函数的功能是再string类的字符后面插入单个字符,append函数的功能是在string类的字符后插入字符串。

以上两个函数的使用不是十分的方便,string类内还提供了+=运算符重载:

image-20230313143425814

+=不仅能实现插入单个字符还能实现插入字符串。

image-20230313143702194

因为有了push_back和append函数,实际上+=运算符重载是对push_back和append函数的封装。

image-20230320153401574

assign函数的功能就像变量赋值一样,不论变量原有数据是什么,让变量的数据变成赋值后的数据。

image-20230320153848743

image-20230320154453789

insert函数提供了在string对象的数据内插入数据的功能。

image-20230320154605769

使用insert函数的第三种接口在string对象的数据的头部插入数据:

image-20230320154432036

image-20230320154916737

使用insert函数的第五种接口往数据中插入空格字符:

image-20230320154834798

image-20230320155159447

使用第六种接口往string对象迭代器指向的位置插入空格字符:

image-20230320155215884

insert不建议频繁使用,效率很低。

有插入函数同样也有删除函数:

image-20230320160040605

调用第一种接口删除数据第五个位置开始后面三个字符:

image-20230320160306234

第一种接口给了缺省值npos,前面提到了npos是一个很大的数,string数据个数不可能有npos那么多,该缺省值的意义就是在不传入值时将指定位置后的全部数据删除。

image-20230320160529097

第二种接口的功能是删除迭代器指向的数据:

image-20230320160650922

erase函数和insert函数一样不建议频繁使用,由于经常挪动数据,效率很低。

image-20230321110359028

replace函数的功能是将string对象数据进行替换。

使用第一种接口将第九个字符换成***字符串:

image-20230321113241721

image-20230321114037405

swap函数的功能是将两个string对象的数据进行交换。

image-20230321114115858

在swap函数的介绍中提及了algorithm库中也提供了swap函数,功能类似string中的swap函数。既然库中都实现了swap函数为什么string中还要实现swap函数?因为两种swap函数的实现方式不同。

image-20230321114929603

库中实现的swap函数的实现方式是创建中间变量然后进行赋值。

image-20230321115120749

而string中的swap实现方式是交换两个string对象指向的数据。

显然只是交换指针的指向的效率远高于赋值,因此string中实现的swap函数还是有意义的。

实际上在vs的实现下如果数据存在_buf中实现方式与库中没有区别,如果数据存在 _ptr中才会采用交换指针的方式。

1.5 了解迭代器

STL中的数据访问都采用迭代器的方式,因此使用迭代器是很重要的方式。

image-20230320142041345

迭代器的使用起来是像指针一样。

我们先看看如何用迭代器遍历输出string类对象的数据:

image-20230320143020738

迭代器使用起来就像指针,首先迭代器指向数据的头,然后解引用,然后迭代器加1,循环此过程直到迭代器指向数据的尾部的后一个位置。

下面我们来看看迭代器提供的接口函数。

image-20230320144652055

begin函数返回一个指向string对象第一个数据位置的迭代器。

image-20230320143558239

end函数返回一个指向string对象最后一个数据后一个位置的迭代器。

我们在来看这段代码:

#include <iostream>

using namespace std;

int main()
{
	string s("hello world");

	string::iterator it = s.begin();//定义一个string类的迭代器指向string数据的第一个位置

	while (it != s.end())//循环直到迭代器指向string对象最后一个数据的后面一个位置
	{
		cout << *it << " ";
		it++;
	}

	return 0;
}

迭代器的指向如下图:

image-20230320144110699

为了实现反向遍历,string类还提供了反向迭代器:

image-20230320144539540

rbegin函数会返回一个指向string对象最后一个对象数据的反向迭代器。

image-20230320144716857

rend函数返回一个指向字符串第一个字符之前的理论元素的反向迭代器。

我们来看看反向迭代器和正向迭代器遍历同一组数据的效果:

image-20230320144914645

注意:反向迭代器加1是从数据末尾往数据头部挪动。

迭代器还提供了const版本:

image-20230320150146719

image-20230320150220596

image-20230320150240382

image-20230320150250394

前面提到了迭代器的使用就像指针一样,const版本的迭代器就像在指针前加了const,const修饰的指针指向可以改变,但是指向的内容不能改变,const版本的指针也是一样。

image-20230320150556915

image-20230320150614577

图中代码const版本的迭代器加1就是迭代器在改变指向,但是迭代器指向的数据是不能改变的,使用起来的感觉和const修饰的指针一样。

image-20230320151132283

向函数传入const类型的string对象引用,只能返回得到const版本的迭代器。

image-20230320151310496

由于迭代器的名字过长,可以使用auto来使用,auto会通过函数返回值识别类型。

image-20230320151542512

在C++11中,还提供了cbegin,cend,crbegin,crend函数用来返回const版本的迭代器:

image-20230320151708038

1.6 了解迭代器外其他访问数据方式

迭代器使用起来给人的感觉很麻烦,除了使用迭代器外string类还提供了其他访问数据的方式:

image-20230320151821569

string类重载了[]运算符使得string对象能够像数组一样进行访问和修改数据。

image-20230320152135907

image-20230320152000623

用[]运算符访问越界时会报断言错误:

image-20230320152635678

除了使用[],string类还提供了at函数访问数据:

image-20230320152737143

image-20230320152824035

at函数访问数据越界时,会采用抛异常的方式:

image-20230320152933395

image-20230320153032801

虽然使用迭代器访问string对象的数据很麻烦,并且string类提供了除使用迭代器以外的访问数据方式,但是迭代器是STL提供的所有容器通用的数据访问方式,因此迭代器的使用很重要。

1.7 了解string数据操作函数

image-20230321110832216

image-20230321111242942

find函数的功能是寻找string对象数据中第一次出现的字符或字符串,然后将其位置返回,如果寻找失败会返回npos(即一个无效的位置)。

find函数配合replace可以实现对string对象的数据中指定的数据进行替换:

image-20230321113343310

用find函数配合replace函数将string对象的数据中空格字符全部替换:

image-20230321113429745

image-20230321111952608

如果不传入值,使用缺省值,每次都是从数据的头部开始查找,效率会降低,并且replace还会频繁扩容,对于上面find配合replace的代码可以进行优化:

image-20230321113023896

当然如果不考虑空间消耗还有另一种优化方式:

image-20230321112857294

+=的效率远高于replace,相当于空间换时间的做法。

image-20230321123853346

有正向寻找,当然也有反向寻找,缺省值给npos表明默认从string对象数据末尾开始寻找。

image-20230321124013381

image-20230321115934968

c_str函数的功能是将string对象的数据以字符串指针的形式返回。

image-20230321121603627

C/C++中打印时传入字符串指针会按照字符串打印,按照字符串打印的方式遇到’\0’会停止打印,但是string的打印是根据成员变量给size的大小来打印的,不会因为’\0’的存在而停止打印:

image-20230321121818031

image-20230321122113799

substr函数的功能是将string对象数据中取一部分然后返回一个保存取出数据的string对象。

image-20230321123025817

substr函数配合rfind函数能够实现得到文件后缀的功能:

image-20230321123809241

image-20230321124820818

find_first_of函数的功能是在string对象中寻找含有指定字符串中任意字符的位置的函数。

image-20230321125213109

image-20230321125228541

find_first_not_of函数的功能是寻找string对象中不含指定字符串的位置。

image-20230321125323472

image-20230321125337729

find_last_of函数的功能是从string对象数据的末尾开始寻找含有指定字符串的位置。

image-20230321125531583

image-20230321125602814

find_last_not_of函数的功能是从string对象的末尾开始寻找不含指定字符串的位置。

image-20230321125551605

1.8 了解非成员函数的重载

image-20230321125749008

image-20230321130213330

string对关系运算符的重载使得string对象可以和string对象、字符串比较,但是这样的实现实际上是多余的,因为单参数的构造函数会发生隐式类型转化。

image-20230321130205608

image-20230321132033099

getline函数的作用是接受一整行的数据输入。

image-20230321132633984

同样是输入一行字符,cin会在有空格和换行时分割,getline则是将整行获取,不论有无空格和回车。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

好想写博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值