STL之string类:知其然,需知其所以然

目录

前言

一,构造及初始化

1.1constuct类函数

1.2构造函数及其模拟实现

1.3拷贝构造及其模拟实现

1.4赋值操作符

1.5string类的swap接口

二,迭代器

2.1初识迭代器即模拟实现

2.1什么是反向迭代器???

2.3范围for

三,空间有关

3.1空间大小接口

3.2空间开辟以及使用接口

3.2.1reserve接口

3.2.2resize接口

3.2.3空间使用接口

四,元素获取

五,尾插

5.1尾插push_back

5.2append尾插 

5.3 += 尾插

六,指定位置的插入删除

6.1查找

6.1.1STL中string类的查找函数

6.1.2查找模拟实现 

6.2指定位置插入

6.2.1单个字符的插入

6.2.2字符串的插入

6.3指定位置删除

七,操作符接口

7.1比较操作符

7.2流插入,流删除 

八,其他接口

8.1size_t最大值npos

8.2getline获取输入流对象内容

8.3substr返回指定子字符串

8.4 c_ str 以C的形式返回字符串 


前言

小伙伴们大家好啊!转眼见已经快要两周没有更新了,小菜鸡库森已经迫不及待了!今天为大家带来的是C++中最重要的一个章节,STL的string类。那么在之前的C语言中,其实我们会使用很多字符串的函数,但是使用起来是比较繁琐的,那么随之而来的就是C++中,提出了一种类似于字符串的类,从而方便了我们的使用。那么我们废话不多说,这进入正题。

一,构造及初始化

1.1constuct类函数

首先我们需要考虑的一定是该类的初始化,也就是构造函数:

那么如上图所示,我们看到,对于string类对象来说,是有很多种构造类的函数的,那么我们可以大致将其分为三类:构造函数,拷贝构造,类模板。

因为很多内容都是类似的,所以我们不一一实现,但是大概的框架体系我们都会有实现。

1.2构造函数及其模拟实现

那么对于函数的初始化来说,如下图所示,是有很多种形式的。

我们可以通过空内容初始化,可以通过字符串初始化,当然也可以通过已经存在的对象去初始化。

那么对于前两种形式来说,是属于构造函数,而第三种形式明显是通过拷贝构造初始化。那么我们知道可以这样初始化,那么它的底层是怎样实现的呢?如下图所示:

如上图所示,其实string类在底层是一个类似于数组的结构,所以是有size和capacity的。因为我们不知道它会使用空内容还是字符串初始化,所以我们使用缺省值实现,同时因为我们必须要开辟空间,此时我们就需要先实现size的大小,也就是当前字符串中有效字符的个数。所以我们先使用初始化列表初始化 _size,然后再通过 _size 大小初始化 _capacity,也就是当前字符串一共能存放的有效字符个数。

而在真正开空间的时候,我们需要给容量_capacity 多开辟一个空间,这里不同于数组,因为字符串结尾是有 \0 表示字符串结束标志的!

1.3拷贝构造及其模拟实现

那么对于拷贝构造函数来说,首先我们需要知道的是,既然是拷贝构造,那么就一定得是身拷贝,否在在最后释放空间的时候会报错,具体原因就是浅拷贝不会开空间,它和原空间共用同一块空间,那么在释放的时候,同一块空间释放第二次,就会出现非法内存访问的问题。

所以我们直接使用深拷贝实现,但是对于深拷贝来说,目前我们有两种实现方式。首先是传统的写法,也就是首先通过原对象的内容去开辟一个一样大的空间,然后再将原对象内容拷贝过去。 

其次是比较新颖的写法,也就是我们首先将新对象的内容指针 _str 指向空,然后再通过原对象的内容去初始化 tmp 对象,也就是复用构造函数,最后再将 tmp 对象和即将要新建的对象交换,同时因为 tmp 对象是局部对象,在完成拷贝构造之后,tmp 对象的生命周期结束,就会调用析构函数,将其释放。

那么相对来说,我们更喜欢使用新型写法,因为复用了构造函数,我们就不需要再次开辟空间了。而且,新型写法确实更加简洁。

1.4赋值操作符

那么同样的,因为赋值是类似于拷贝构造的。所以同样的,我们也是使用比较新颖的写法,不去使用 _str 完成内容交换,而是直接在传递参数的时候,使用拷贝构造函数产生一个临时对象 s ,然后再将其和 this 对象(也就是我们赋值的对象)进行对象的交换,就可以完成赋值操作。

而如果不去复用拷贝构造,而是通过拷贝构造的方式去进行赋值操作,就会很繁琐,而且不利于我们操作,所以我们直接复用拷贝构造去实现。

1.5string类的swap接口

那么在之前我们的拷贝构造和赋值操作符中,我们都进行了交换对象的操作,那么对于它的基本内容,这里我们还是需要实现一下:

如下图所示,可以看到string类的swap函数只有一个参数是对象的引用,所以我们大概模拟实现。也就是将该对象和 this 对象的所有内容进行交换。

当然真正的底层实现不会只有这些内容,但是因为我们考虑不到那些内容,所以我们深究,但是我们应该知道的是,它在交换的时候,大概做了什么。

但是同时我们又知道,在C++中,是有一个全局的swa函数的,那么为什么我们不使用全局的swap呢?接下来我们一起看看。

如上图所示,全局的swap函数是一个函数模板,当然最主要的是,我们发现,在该模板函数中,会有三次拷贝构造,所以就会导致程序效率比较低,所以我们一般不会使用全局的swap函数。(当然这仅仅是对于C++98来说)。

二,迭代器

2.1初识迭代器即模拟实现

那么我们知道,在STL中,迭代器是会经常使用的一种标注某个位置的利器,但是现阶段我们依旧可以将它理解为是一种类似于指针的事物。那么首先,我们需要了解一下迭代器到底是如何的,如下图所示:

迭代器,begin 代表字符串首元素,而 end 则代表字符串末尾后面的那个元素,所以我们可以直接通过循环来实现它的迭带打印。

接下来我们通过模拟实现来看看它到底是如何实现的:

如上图所示,我们看到 iterator 迭代器虽然是有两种,但是他们都是字符型指针,所以这里可以看到,本质上迭代器就是通过指针实现的。所谓 begin 可以取到字符串首元素,是因为它返回了字符串首元素的地址。所以我们就可以理解到底什么是迭代器了。

2.1什么是反向迭代器???

但是,我们可以看到,上面这种我们可以称为正向迭代器,而后面两种带 r 的是反向迭代器。所以反向就是正好和正向相反,rbegin 代表的是字符串末尾后面那个元素的地址,所以这里我们就不做过多介绍了。 

当然,迭代器不仅仅只有这几种,但是因为其他的不常用,如果需要的时候,我们只需要直接在官网上查看即可。

2.3范围for

在之前的文章中,我们提到过,范围 for 的作用,那么其实对于范围 for 而言,它就是通过迭代器实现的,它的所有功能,包括自动计数,自动遍历,都是由于底层是用迭代器实现的。如下图所示:

三,空间有关

那么对于string类来说,有关空间的函数接口是有很多的,如下图所示:

但是我们依旧还是挑最主要的来演示,那么这里我们分为两类来一一分析:

3.1空间大小接口

首先就是使用空间的四个接口:size,length,capacity,max_size,如下图所示:

那么我们看到size函数表示当前空间存放的有效字符个数;而length函数和size的作用是一样的,那么为什么需要这样设计呢?岂不是多此一举吗?

还真是的,因为STL中有很多容器都需要使用size这个接口,但是他们却不一定有strlen这个接口,所以为了大家保持一致,所以才有了这两个接口,但是实际山我们用的一般都是size,因为大家(容器)都在用。所以strlen接口就基本不会怎么用了。

capacity接口,表示的当前空间总的大小。那么对于capacity接口来说,不同的平台它的增容倍数是不一样的,当前 vs2019 平台是大概 1.5 倍的增容,而在Linux平台下是 2 倍的增容。当然对于该接口,我们不能忘记的是,它当前显示的大小,并不是它实际的大小,因为它总要多出一个字符来保存 \0 ,这个字符串结束标志。

那么最后一个接口max_size表示的当前字符串可申请的最大的空间。

3.2空间开辟以及使用接口

3.2.1reserve接口

那么对于空间开辟,我们是有两个函数实现的, reserve 接口相对比较简单,它只是增大空间使用,不会缩小空间,也不会修改或者初始化空间内容。因为需要避免空间 “ 抖动 ”。

所以它的实现就相对比较简单了:

那么首先,这里我们是首先开辟了 2 倍的 n 加 1 的空间,然后使用了一个临时空间, 在将原空间内容拷贝到新空间之后,再将原空间释放,最后再将新空间赋值给原空间,这样就完成了内容的移植。当然,最后一定不能忘记的是需要将 capacity 的值更新。

3.2.2resize接口

而resize 接口表示在开辟空间的同时,可能需要初始化空间,同时如果空间变小,也需要清除空间中其他内容,如下图所示:

如上图所以,通过官网给的说明,如果 n 的值小于现有的空间,则直接将空间修改为当前 n 的大小;而如果 n 的值大于现有空间,则需要将新开辟的空间全部初始化为,第二个参数的字符或者如果第二个参数没有,则全部初始化为空。

那么对于resize其实是相对比较复杂的一个接口,接下来我们通过模拟实现来了解一下它是如何实现的:

那么这里我们看到,我们需要对于容量增大和减小这两个部分,分别进行修改。如果是缩小,则可以直接将容量缩小,然后将 _size 位置赋值 \0;而如果是增容的话,就需要对于新增的空间初始化,最后同样的需要给字符串结尾赋值结束标识符。 

3.2.3空间使用接口

这里我们需要简单介绍一下 clear 接口以及empty:

 和他们各自的名字一样clear函数用来清空当前空间中所有内容,当然这就很简单可以实现了,而empty函数是检测当前空间是否为空。

四,元素获取

那么在获取元素这节,我们可以看到有四个函数,但是因为string类似于数组,所以我们如果是取某个位置元素的话,会直接使用下标取,所以一般我们会用下标引用操作符 [ ] ,以及 at 函数。

当然,对于 at 函数而言,我们在实际中使用的也不多,因为下标引用操作符明显比 at 更方便。这里我们以下标引用操作符为例。如下图所示,我们可以看到,对于该接口其实是有两种形式的,分别为 const 和非const 修饰的函数。

因为对于 [ ] 的使用,我们已经见得很多了,所以这里我们直接通过模拟实现了解一下。 

pos 则表示我们要查询的元素的位置,当然首先,我们需要判断当前位置是否满足条件,如果不是,则直接断言报错即可。而如果满足条件的话,则需要返回当前位置的元素。

五,尾插

那么对于string类的插入修改等,是有很多接口的,因为这里不涉及在具体位置的插入删除,所以我们暂且将其称为简单部分。那么在这部分,我们需要实现的接口有三个,分别是尾插 +=,append,puch_back,接下来我们一一实现。

5.1尾插push_back

如上图所示,我们可以看到,对于push_back函数来说,参数只有一个,所以它的模拟实现就相对比较简单了。首先我们需要判断当前空间是否已满,如果满了之后,我们需要扩容。然后再将该字符插入末尾,最后在 size 自增 1 的情况下,将最后的 \0 插入最后一个元素的后面。

5.2append尾插 

虽然append函数的接口类型是很多的,但是总结起来就是,字符,字符串,string类对象都可以插入,因为很多道理都是相同的,所以这里我们只通过字符串来模拟实现一下。

如下图所示,同样的,首先我们依旧需要判断当尾插该字符串之后,空间是否满足,如果不满足,则需要重新开辟空间,然后再将str内容尾插到原空间后面,最后再更新 size 的值即可。

5.3 += 尾插

如下图所示, 那么对于尾插而言,可以有三种形式的插入,但是我们常用的还是插入一个字符或者一个字符串,但是我们发现,这两种实现我们在前面 push_back 和 append 已经实现过了。

所以我们发现,其实操作符 += 的实现,是复用了前面的push_bakc和append,如下图所示: 

那么模拟实现也就很简单了。

六,指定位置的插入删除

6.1查找

6.1.1STL中string类的查找函数

前面我们探讨了有关尾插的一些操作,那么当然如果我们想要去在指定位置进行修改,一定也是可以的。但是前提是我们需要找到该位置。所以首先我们需要实现查找接口。

可以看到有关查找的函数是很多的,但是我们真正常用的依旧只有两个,find 和 rfind ,顾名思义,rfind 就是从尾部开始查找。因为 rfind 是从尾部开始查找,所以功能使用和 find 基本是一样的,所以我们通过模拟实现 find 函数来了解他们的使用。

同样的,这里find函数也是有很多接口的,但是最主要的依旧是查找某个具体的字符或者字符串,所以我们还是通过这两个函数接口来实现。

6.1.2查找模拟实现 

那么对于find函数来说,首先就是查找某个字符所在的位置:

如果是单个字符的查找,一般我们会在初始位置开始往后找,然后在找到后,直接将其下标返回,如果找不到则直接返回npos,npos 代表的是无符号整型的最大值,是string类的自己定义的。这个在稍后会有介绍。

而对于子一个字符串的查找,我们就需要使用strstr或者substr来比较在 pos 位置处是否有和参数字符串相等的字符串,同样的,如果可以找到,则直接返回该位置的下标,而这个下标是由找到位置的指针减去原空间的指针得到的,因为指针减指针,最后的结果是两个指针之间的元素个数。

那么对于strstr函数和substr我们会在后面实现。这里我们需要知道的是,这两个函数都是匹配字符串是否相等的函数,但是实际用途依旧有不同。

当我们在找到某个元素指定位置之后,接下来我们就可以进行指定位置的插入删除操作了。

6.2指定位置插入

那么对于插入函数,在库中的接口是很多的,但是同样的,我们可以将其大致分为三类:字符,字符串,string类对象。

虽然接口很多,但是我们依旧还是先通过最简单的字符和字符串的插入来简单认识对于insert接口的实现。

6.2.1单个字符的插入

我们知道,string类是一种类似于数组的结构,所以我们需要明白,如果在中间或者头部插入,那么开销将会很大,因为我们需要将该位置及以后的元素全部往后移动一位,然后再将该元素插入。如下图所示:

我们在保证参数 pos 位置不越界的情况下,再判断空间是否满,如果满了之后,需要扩容以保证有足够的空间来插入新元素。而且需要注意的是,因为 string 表示的都是都是字符串,所以结尾是有 \0 的,\0 也是需要移动的,所以这里end 的位置我们给的是 _size +1 的位置。最后再将 pos 位置及以后的元素全部移动之后,再将 ch 插入即可。 

6.2.2字符串的插入

而对于整个字符串的插入来说,我们不需要考虑原先字符串末尾的 \0,因为需要插入字符串也带有 \0,所以我们只需要在将指定pos位置及之后的元素往后移动 len 个位置后,再使用strncpy将字符串拷贝到移动之后的空间即可。

但同时我们需要注意的是,在移动的时候,因为需要插入的是len个元素,所以移动的元素个数也应该是len个,所以在判断的时候,是 end - len > pos。然后最后因为拷贝的时候,不拷贝最后的 \0,所以是内容拷贝,所以我们用strncpy,而不用strcpy。

6.3指定位置删除

那么对于指定位置删除,也就很简单了,因为我们不需要过多的挪动数据。

我们的思路是,如果pos位置,加上要删除的元素个数,大于当前现有元素个数的话,则可以直接将pos位置为 \0,然后再更新 size 值为当前 pos 位置的大小。

七,操作符接口

那么对于操作符接口来说,其实是比较简单的。因为我们都是直接复用现有的函数来实现。这里我们分为两类接口,通过这两类接口,相信大家也一定能了解了有关操作符的内容。

7.1比较操作符

那么在这一类操作符中,我们只需要实现两个即可。因为其他的都可以复用实现。如下图所示,我们通过strcmp来比较字符串的大小,然后根据该函数的特性去判断。

那么之后所有的比较操作符都可以直接复用这两个函数实现。

7.2流插入,流删除 

那么对于我们自己实现的流插入和流删除来说。最主要的就是我们需要在类外实现,但是为了避免和库中的冲突,所以我们将其和string一起实现在一个命名空间中,但是在类外实现,这样就没有隐含的this指针的问题了。

但是如果我们不这样去实现,而是放在类中去实现,就会导致我们在调用的时候,和我们平常的操作符函数的调用形式不一致,所以为了统一,这里我们将其实现在类外。 

八,其他接口

还有一些接口虽然不具体属于哪一个群体,但依旧很重要,那对于这些接口我们最好也是可以做到耳熟能详。

那么首先就是我们在指定位置修改时,可能会遇到的,无符号整型的最大值 -1,在string类中是属于一个全局变量。

8.1size_t最大值npos

因为在string类中,下标不存在负数的情况,所以在使用有关下标的数字的时候,我们都是使用size_t 实现,而size_t 是有可能等于 -1 的,如果等于 -1 的话,在 size_t 看来,就等于无符号的42亿多了,也就是当变量达到npos 时,表明一般是不可能或是出错了。

8.2getline获取输入流对象内容

那么对于getline这个函数来说,它最主要的功能是在当我们需要读取一个变长字符串时,有一些编译器可能并不支持我们这么做,那么此时就需要getline函数去实现了。我们只需要定义一个string类对象,然后将其传递给getline函数,即可帮我们读取到变长字符串。(注:在oj中实用性很明显)

8.3substr返回指定子字符串

substr函数,两个参数,一个是指定位置的参数 pos ,另一个则是有缺省值的参数表示字符串的长度 len,所以对于substr来说,它的作用就是对于当前 string 对象从 pos 位置开始,取出长度为 len的字符串,如果不显示传参,则直接取到 npos 位置,也就是取到结尾,直到遇见 \0 ,就终止,然后再将取到的字符串构造出一个对象返回。

8.4 c_ str 以C的形式返回字符串 

对于该函数来说,其实就是在string类对象中,取出字符串,然后将地址返回。也就是说,c_str是取对象中的字符串,然后将其返回。为我们直接使用对象中的字符串提供了便利。

好的,那么对于string类的实现,到这里就结束啦!如果小伙伴们有问题的话,可以提问哦!up主的文章如有不妥之处,还请指正呀!

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值