C++string类
目录
一、string的常见接口
1.1、string的构造函数
1.2、获取string字符串长度方法
1.3、operator[]
1.4、begin 和 end 方法
1.5、rbegin 和 rend 方法
1.6、push_back
1.7、append
1.8、+=
1.9、assign
1.10、insert
1.11、erase
1.12、capacity,reserve,resize方法
1.13、c_str 方法
1.14、find,rfind和substr
1.15、数字类型转字符串,字符串转数字类型
二、string 的模拟实现
2.1、成员变量
2.2、构造函数和析构函数
2.3、c_str
2.4、size方法和重载 [ ] 运算符
2.5、迭代器
2.6、reserve,push_back,append方法
2.7、+=
2.8、insert 和 erase
2.9、find
2.10、拷贝构造和赋值重载
2.11、swap 和 substr
2.12、比较大小
2.13、重载流插入和流提取
2.14、拷贝构造和赋值重载的其他写法
一、string的常见接口
1.1、string的构造函数
如下图所示,string一共有七个构造函数。
在这七个构造中,有三个较为常用,我们需要熟悉,分别是:默认构造,拷贝构造,用字符串构造。(即图中的第1,2,4个构造)使用方法如图:
string 是自定义类型,但是从上图中可以看到 string 支持 cout 和 cin,自定义类型想要支持 cout 和 cin 需要自己重载流插入和流提取,所以 string 也重载了流插入和流提取。
后面的构造了解一下就可以了。如第三个构造。
从构造的介绍中可以看出该构造的作用是拷贝某一个 string 对象的一部分,第一个形参是要拷贝的对象,第二个形参是从哪里开始拷贝,第三个形参是拷贝多少个字符。使用如图:
该构造函数中第三个参数给了一个缺省值,即npos。npos介绍如图:
从上图可以看出 npos 是 string 中的一个静态成员变量,且被设置为无符号整型的最大数值。
因为在第三个构造函数中,如果传入的第三个参数值(即想要拷贝的字符个数)大于第二个形参所指定的字符位置到字符串结尾的字符个数,那么该构造函数默认就会从第二个形参所指定的位置一直拷贝到字符串结尾。又因为 npos 是无符号整型最大值,所以当不传入第三个参数时,就会从第二个参数指定的位置一直拷贝到字符串结尾。如图所示:
第五个构造函数是拷贝一个字符串的前 n 个,第一个形参是要拷贝的字符串,第二个形参是需要拷贝前几个。第六个构造函数是构造具有 n 个相同字符的字符串,第一个形参传入的是 n,第二个形参传入的是字符。使用如图:
还有隐式类型转换需要注意一下,比较常用。如图:
1.2、获取string字符串长度方法
想获取string字符串的长度有两个方法,一个是 size 方法,一个是 length 方法。这两个方法没什么区别,之所以有 size 方法是为了和后面的容器保持一致。
size方法:
length方法:
使用如图:
1.3、operator[]
string类中重载了 [ ] 运算符,作用是返回指定下标处的字符,传入的参数为想访问的下标,这让我们可以更方便的访问每一个字符。同时从图中可以看出重载的这两个函数返回值均为引用,对于非const 对象,我们就可以直接通过 [ ] 运算符进行修改,这就是传引用返回的另一个好处。如图:
同时该函数还对越界进行了严格的检查。如图:
该函数一共重载了两个,重载第二个的意义在于首先 string 对象除了正常的还有被 const 修饰的,被 const 修饰的调用第一个函数的时候传 this 指针会涉及到权限放大的问题,无法调用第一个函数,其次正常的 string 对象调用 [ ] 返回值需要允许修改,而 const 修饰的 string 对象不允许修改,所以第二个重载的函数返回值被 const 修饰了。(一般的函数重载一个 this 指针被 const 修饰的就可以了,这样无论该类的对象是否被 const 修饰都可以正常调用)
1.4、begin 和 end 方法
上图中,begin 方法返回 string 对象起始位置的迭代器,end 方法返回 string 对象最后一个字符后一个位置的迭代器。我们可以通过迭代器对 string 对象进行遍历。如图:
这里 begin 和 end 方法返回的迭代器的类型是 iterator,iterator 又在 string 的类域里面,所以接收返回值时要指定类域,否则查找不到该类型。 访问迭代器所指向的数据要像指针解引用那样,想访问后面的数据时需要移动迭代器,即让迭代器++即可。这些操作和指针很像,所以可以把迭代器理解成类似指针的东西,但是迭代器的实现不一定是指针。我们也可以通过迭代器对 string 对象进行修改。如图:
通过迭代器还引申出一种遍历 string 对象的方法,范围 for。(范围 for 的底层其实就是使用的迭代器,所以除了 string 类,只要支持迭代器的类都可以支持范围 for)使用如图:
范围 for 会自动遍历 string 对象,并将每一个字符依次交给变量 e,这样通过变量 e 我们就可以访问到字符串里的每一个字符了,而且范围 for 还会自动判断字符串结尾,当读取到结尾时就会自动停下。但是通过图中可以看出,我们对变量 e 做修改时不会影响到真正的 string 对象 s。这是因为 e 只是一个临时变量,用来存储 string 对象中的每一个字符,string 对象中的字符会依次拷贝给变量 e。如果我们想通过范围 for 对 string 对象进行修改也可以,只需要将临时变量改成引用就可以了。如图:
在 string 类中 begin 和 end 方法各重载了两个,如图:
begin 方法中,这两个方法返回的迭代器本身都是可以修改的,但是第一个方法返回的迭代器指向的对象是可读可写的,第二个方法返回的迭代器指向的对象是只读的,正常的对象会调用第一个 begin 和 end 方法,被 const 修饰的对象会调用第二个 begin 和 end 方法。前面图片中演示的就是第一个 begin 和 end 方法。第二个的使用场景如图:
(注意第二个begin方法返回类是const_iterator类型)
1.5、rbegin 和 rend 方法
和 begin/end 方法类似,rbegin/rend 方法也是返回迭代器,只不过返回的是反向迭代器,begin,end 方法可以正向遍历 string 对象,rbegin,rend 方法可以反向遍历 string 对象,使用如图:
rbegin 和 rend 方法也重载了两个,第一个给正常的 string 对象使用,第二个给 const 修饰的 string 对象使用,第一个返回的反向迭代器指向的对象是可读可写的,如图:
第二个反向迭代器指向的对象是可读的,如图:
这里额外介绍一个 sort 方法,sort 方法不是 string 类内部的方法,是算法库里面的方法,包含在algorithm 头文件里,如果想对 string 对象按字典序排序可以使用这个方法,该方法需要传入两个迭代器,sort 方法会对传入的迭代器区间按照左闭右开的规则进行排序,即第一个传入的迭代器指向的数据会参与排序,第二个传入的迭代器指向的数据不会参与排序。具体声明如图:
使用如图:
1.6、push_back
push_back 方法可以在 string 对象末尾追加字符。如图:
注意:push_back 方法一次只能追加一个字符,无法追加字符串。
1.7、append
append 方法共重载了六个,第一个是在 string 对象后面追加另一个 string 对象,第二个是在 string 对象后面追加另一个 string 对象的一部分,第三个是追加一个字符串,第四个是追加一个字符串的前 n 个,第五个是追加 n 个相同字符,第六个是追加一段迭代器区间里的内容。其中最常用的是第三个,其他了解即可。第三个使用如图:
1.8、+=
string 的 += 共重载了三个版本,分别可以 += string对象,字符串,字符。如图:
1.9、assign
assign 接口的设计和 append 很像,通过 append 我们就可以知道 assign 每个接口如何使用,它们的不同之处在于 append 是在原有字符串上追加,而 assign 是用传入的内容覆盖掉以前的内容如图:
1.10、insert
insert 方法共重载了七个,第一个是在指定位置插入一个 string 对象,第二个是在指定位置插入一个 string 对象的一部分,第三个是在指定位置插入字符串,第四个是在指定位置插入一个字符串的前 n 个,第五个是在指定位置插入 n 个相同字符,第六个是在指定位置插入一个字符,第七个是在指定位置插入一段迭代器区间的内容,其中 pos 代表要插入的位置,p 也代表要插入的位置,只不过 p 是一个迭代器。方法使用如图:
1.11、erase
erase 方法共重载了三个版本,第一个是从指定位置开始删除 len 个,当不传入 len 值或传入 len值大于可以删除的字符数量时会从 pos 位置一直删除到结尾,且该方法会对传入的 pos 值进行检查,如果越界就会报错;第二个是传入一个迭代器,删除迭代器所指向位置的字符;第三个是删除一段迭代器区间里的内容(还是按照左闭右开的规则删除)。使用如图:
1.12、capacity,reserve,resize方法
capacity 方法可以返回当前 string 对象的容量大小,和 size 相比,size 是 string 对象的实际长度,而 capacity 是表示当前对象最多可以存储多少字符。使用如图:
上图中除了看到 capacity 方法的使用还可以看出 string 对象是可以自动扩容的,除了自动扩容,我们还可以通过 reserve 方法进行手动扩容。方法:
使用:
上图可以看出我们通过 reserve 方法手动扩容到一百,但是实际扩容后的容量却是111,这是因为c++中并没有规定扩容的标准,具体怎么扩容要看编译器的实现,在 Linux 中使用这个方法的效果就是传入参数是多少就会扩容到多少。同时如果我们想通过这个方法缩容,这个结果也是未知的,要看编译器,VS2022 不会缩容,但是如果缩小到十五以下会缩容,Linux 则会缩容,不管缩到多少,他都会按照方法传入的参数来开辟容量。VS2022 缩小到十五以下会缩容和 string 的结构有关。如图:
实际上当一个 string 对象存储的字符串中字符个数小于等于十五个字符时数据会存储在一个数组中,当字符个数大于十五时数据会存储在一个指针指向的空间中,所以正常缩容时 VS2022 是不会缩容的,但是小于等于十五时会从用指针指向的空间存储改回用数组存储,这时多出的数据就会丢掉。
关于 reserve 还有一个点就是当我们用 reserve 开好一块空间后,并不代表我们就可以访问开好的每一块空间,我们可以使用 string 的各种接口按顺序使用这些空间,并且可以访问或进行修改操作,但是我们不可以跳过已经使用的空间后再跳过一段没有使用的空间然后进行访问或者修改,例如:
resize方法:
resize 方法修改的是 string 对象里的 size 成员,size 的改变可能会间接引起 capacity 的改变。具体使用如图:
效果:
从图中可以看出,只传入一个参数时会默认初始化为 ‘/0’,传入两个参数时会在原有数据后面追加字符,不会覆盖原有字符。
1.13、c_str 方法
有些时候我们写代码会进行C和C++混编,但是C语言中没有 string 的概念,如果我们想要使用C语言的接口处理 string 对象里面的数据,就需要使用 c_str 方法返回指向字符串的指针(即char*的指针)。
1.14、find,rfind和substr
find:
rfind:
substr:
find 方法是用来从前向后在 string 对象中查找匹配的字符或者字符串的,rfind 和 find 用法和作用一样,只不过 rfind 方法是从后向前查找,substr 方法可以截取字符串的某一部分。如果需要使用 string 对象的其中一部分,就可以使用这三个函数来实现。如图:
1.15、数字类型转字符串,字符串转数字类型
数字转字符串:
字符串转数字:
使用如图:
二、string 的模拟实现
2.1、成员变量
通过VS2022我们可以看到库里面 string 的实现是有一个数组和一个指针,当数据量小的时候,数据存储在数组中,当数据量大的时候,数据就会存储在指针指向的空间里,这里我们就不采取复杂的结构了,直接用指针。如图:
2.2、构造函数和析构函数
声明:
定义:
构造函数中,不能够直接用传参过来的字符串指针初始化 _str,因为如果是使用常量字符串构造的string 对象,直接使用常量字符串的指针初始化,常量字符串是不允许修改的,那后续如果要执行修改操作就无法执行。这里直接用 new 新申请一块空间,并将用于初始化的字符串拷贝到申请的新空间中。
析构函数中,因为构造中用 new 申请了空间,析构中就要释放这块空间。
无参构造声明:
无参构造定义:
无参构造中,_str 不可以直接给空指针,这是因为在后面实现的 c_str 方法中需要返回 _str,如果给空指针,那对于使用无参构造创建出来的 string 对象,在没有赋值之前访问 c_str 方法返回的指针就会报错。
实践中一般不会这样写两个构造,可以将两个合并为一个全缺省函数。只需要将有参构造的声明处加一个缺省值就可以了。如图:
2.3、c_str
c_str 方法非常简单,只需要将 _str 指针返回就可以了,但是要注意 this 指针要被 const 修饰,这样 const 修饰的 string 对象也可以调用,返回值也要被 const 修饰,这样可以防止外面修改指针。
定义如图:
2.4、size方法和重载 [ ] 运算符
size 方法直接返回 _size 即可。
声明:
定义:
重载 [ ] 运算符,返回值要用引用,因为要允许外面修改 string 对象中的某一个字符,同时还要对 pos进行检查,防止越界访问。
声明:
定义:
除了要写一个能够通过返回值修改 string 对象的,还要写一个不允许外部修改的供给 const 对象使用。其实只要将 this 指针和返回值用 const 修饰就可以了。
声明:
定义:
测试:
2.5、迭代器
迭代器的实现不一定是指针,但也可以使用指针,这里我们使用指针来实现,而且使用指针来实现刚好和库里面的迭代器使用起来一样。但是我们要对指针进行一层封装,和库里保持一致。begin 方法返回字符串起始地址,end 方法返回最后一个字符后面的位置,即 '\0' 的地址。实现迭代器后就可以支持范围 for 了。
声明:
定义:
上面写的迭代器只能支持正常的 string 对象使用,还需要写一个支持 const 修饰的 string 对象可以使用的。
声明:
定义:
测试:
2.6、reserve,push_back,append方法
因为 push_back 和 append 方法涉及到扩容的问题,所以在实现这两个方法之前我们要先实现一个 reserve 方法,方便我们扩容。
reserve方法可以先判断一下传入的数据,如果传入的数据比当前的容量大,那么就扩容,如果比当前容量小,那么什么也不做,扩容的方法也很简单,只需要重新开辟一块空间,将原有数据拷贝到新空间里,并释放旧空间,然后再让 _str 指针指向新空间,最后在更新一下容量。
reserve 声明:
定义:
push_back 方法中需要先判断是否需要扩容,保证空间充足后再将数据插入就可以了,_size 既是有效元素个数,同时也是下一个元素应该插入位置的下标,但是这个位置原来应该是 '\0',插入新数据后 '\0' 被覆盖了,所以还需要将 '\0' 手动赋值回来,最后在更新一下 _size 就可以了。append思路也类似,只不过插入的是字符串,这时不知道插入字符串的长短,所以如果像 push_back 那样采取二倍扩容,扩容后的空间仍然未必够用,所以采取直接扩容到需要的大小的方法。扩容后将内容拷贝到 string 对象中,并更新 _size。
声明:
定义:
测试:
2.7、+=
+= 的实现直接复用前面实现的 push_back 和 append 即可。唯一需要注意的是 += 是有返回值的,返回值是对象本身。
声明:
定义:
2.8、insert 和 erase
insert 这里只实现两种,一种任意位置插入一个字符,一种任意位置插入一个字符串,这两个方法的实现思路是类似的,在插入之前先将原有字符进行移动,将需要插入的位置空缺出来,在将字符或者字符串放入就可以了。
声明:
定义:
erase 的实现只需要先判断从 pos 位置开始剩余的字符和要删除的字符数谁大,如果要删除的字符数大,那就代表要将 pos 位置以及后面的字符全部删掉,那么只需要将 pos 位置置为 '\0' 即可。如果剩余的字符数量多,那就直接将剩余字符向前移动覆盖要删除的字符即可。两种处理最后都要更新一下 _size。
声明:
定义:
测试:
2.9、find
find 也实现两个,一个查找字符,一个查找字符串,查找字符只需要遍历就可以了。查找字符串可以使用C语言中的 strstr 函数来查找,该函数查找到目标子串会返回目标子串的起始地址,找不到会返回空指针。
声明:
定义:
测试:
2.10、拷贝构造和赋值重载
编译器自动给我们生成的拷贝构造执行的是浅拷贝,也就是会将拷贝对象逐字节的拷贝到目标对象中,假设我们要将 s1 拷贝给 s2,浅拷贝带来的问题就是 s1 和 s2 的 _str 指向的是同一块空间,这样修改任意一方都会对另一方造成影响,且析构时还会造成同一块空间析构多次。所以我们要实现深拷贝。深拷贝我们只需要开辟一块和拷贝对象一样大的空间就行,然后将要拷贝的字符串内容拷贝到这块空间中,在将 _size 和 _capaciyt 更新即可。
声明:
定义:
赋值重载和拷贝构造的实现思路是一样的,只不过赋值重载是对已经存在的对象进行的,除了上述操作,还需要释放原有的空间。
声明:
定义:
测试:
2.11、swap 和 substr
算法库里面 swap 采用的方法是定义一个临时变量,通过临时变量实现两个数据的交换,但是如果直接用算法库里面的 swap 交换两个 string 对象,效率太低了,因为当我们要交换的两个对象比较大时,开辟这个临时变量消耗的时间就大,相互之间赋值的时间消耗也很大,所以更好的办法是再实现一个 swap 这个 swap 里面去调用算法库的 swap,但是不直接交换 string 对象,而是交换它们的成员变量。
声明:
定义:
substr的实现:
声明:
定义:
2.12、比较大小
比较大小写出其中两个其他的复用就可以。
声明:
定义:
2.13、重载流插入和流提取
流插入和流提取可以写成成员函数,但是使用起来不符合习惯,所以一般都写成全局函数,这里可以声明成友元函数访问私有成员来实现它们的功能,但不是必须的。
声明:
定义:
正常情况下每次使用流提取都会用新数据覆盖掉旧数据,所以这里我们在进行流提取的主要逻辑前先对原有数据进行清空,即 clean 方法,实现如下:(该方法为成员函数)
测试:
上面的写法是满足流提取的需求的,但是对于非常长的字符串的输入,上面的写法会面临频繁地扩容,这就导致了效率的降低,所以我们可以进行一些优化,建立一个字符数组,获取到的字符先放到字符数组中,当字符数组满了后再一次性的刷新到 string 对象中,最后当整个循环结束后还有对数组进行一次检查,如果还有剩余数据,也刷新到 string 对象中。如图:
2.14、拷贝构造和赋值重载的其他写法
拷贝构造:
赋值重载:
第一种写法:
第二种写法: