大家好,我是苏貝,本篇博客带大家了解C++如何自主实现string类,如果你觉得我写的还不错的话,可以给我一个赞👍吗,感谢❤️
目录
将自主实现的string放在命名空间中,string类有3个成员变量:_str(数组,里面存放字符),_size(数组的有效字符的个数,不算’\0’),_capacity(数组的容量,不算’\0’)
(A) 构造函数+析构函数+拷贝构造+operator=
a. 构造函数
写2个构造函数:无参+有参。
有参构造报错了,为什么?因为str是const修饰的,_str没有被const修饰,非const->const,权限放大,报错。
那能用const修饰_str吗?这样就不会报错了
不能,如果用const修饰_str,那么_str就不能被修改,即string的对象不能修改,这是错的。
因此我们先给_str开空间,空间为_capacity+1的原因是:因为_capacity不包括’\0’,所以要额外开辟一个字节的空间取存储’\0’。再将str的内容拷贝到_str中,strcpy会将str的‘\0’拷贝给_str
问:无参的构造函数有问题吗?
有的,不能将_str初始化为nullptr,因为如果这样,在c_str()函数的作用下可能会报错
为什么?因为如果我们创建一个没有给初始值的对象,那么它将调用无参构造,因此_str为空,c_str()返回_str,所以会打印nullptr,这是不被允许的
因此,无参构造也要修改一下
最后,我们想将无参和有参的构造函数合起来写,也就是写一个有缺省值的构造函数。
可以这样写吗?
不行,因为不能strlen(nullptr)
那可以将nullptr换成’\0’吗?不行,’\0’是字符字面量,类型是char。str类型是const char*,类型不同。那可以将’\0’换成”\0”吗?它是字符串
可以的。但是其实””是常量字符串,按C语言的规则,常量字符串的最后本身就有\0,所以再写一个\0就是”\0\0”,有些多余了。因此可以直接用””
b. 析构函数
如果我们定义的是一个不带初始值的对象,那_str就是new char(‘\0’),不应该用delete _str吗?
其实这种情况用delete[ ]也是对的,因为char是内置类型,所以编译器不需要在开辟的空间_str前再开辟4个字节存放new char 的个数,所以delete[ ]也可以。
c. 拷贝构造+operator=
因为两个函数的思路类似,所以放在一起写
operator=是两个对象都已存在,将一个赋值给另一个。拷贝构造是用已经存在的同类型的对象取初始化新创建的对象。operator=的被赋值的对象已被定义,它可能有初始值,因此要释放掉原来开辟的空间。拷贝构造创建的对象是新的,因此在拷贝构造前没有初始值,所以没有开辟空间,也就不需要释放
(B) 实现3种遍历
a. begin/end
先查看string类的begin/end,发现它们的返回值类型是迭代器,因此我们自主实现的返回值类型也要是迭代器。下面我们实现的是指针版的迭代器
指针版本的迭代器很好理解,就是将指针的类型char*取别名为iterator(最好是这个英文单词,原因后面说)
再来一个const修饰的this指针的begin/end
b. 范围for
事实上,只要我们将begin/end函数写好,就可以使用范围for,因为范围for的底层是迭代器。但是注意:要想只将begin/end函数写好,就可以使用范围for有条件,即迭代器类型名是iterator,且begin/end函数的函数名只能是begin/end,哪怕是Begin都不行
c. operator[ ]
要想用[ ]来遍历数组,就要写size()和[ ]重载
length()和size()都是返回_size,一起写了。capacity()也简单
现在来写[ ]重载,如果string对象是非const,那么它的内容就可以被修改。如果string对象是const,那么它的内容就不能被修改,需要对operator[ ]的this指针和返回值加const
operator[ ]是用断言来进行检查的。传统C语言的检查是否越界是一种抽查,对越界读检查不出来,对越界写可能检查出来。比如下图的a[11]就能检查出来,但对a[15]就检查不出来。这是因为C语言在设计越界检查时,是通过检查数组(如int a[n])的[n]和[n+1]等相距较近的这些位置,如果编译器发现这些位置的值被修改,就报错。所以如果修改比较远的空间,编译器就不会报错,因为不会检查这些位置
因此,C++的string类用断言就能确保[ ]重载不会越界,这相对于C语言是一个大的进步
C语言的a[i]是转换成*(a+i)来处理,可是C++中[ ]是函数,如果我们循环10000次,岂不是要建立10000个栈帧再销毁10000个栈帧,这是不是消耗太大了?其实,祖师爷已经考虑过这个问题,将[ ]设为内联函数,所以它在预处理阶段就会在调用位置展开,不用建立栈帧
© 扩容:resize/reserve
a. reserve
要求:当n<=_capacity时,什么都不做(一般不会缩容)。当n>_capacity时,一般会在异地扩容,扩容后的_capacity==n
b. resize
要求:当n<=_size时,string类对象的内容变为_str[0,n],_str[n]=’\0’,即有效的字符个数变为n,不缩容。当n>_size但n<=_capacity时,将_size=n,用c来初始化新增的有效字符的空间。当n>_capacity时,扩容,将_size=n,用c来初始化新增的有效字符的空间。
(D) 插入:push_back/append/+=/insert
a. push_back
要求:尾插一个字符
要先判断是否要扩容,当_size==_capacity时就要扩容,可以2倍扩容
b. append
append在string类里实现了许多重载,我们只实现(3),要求:尾插一个字符串
append也要先判断是否要扩容,也是当_size==_capacity时扩容吗?不是的,是要看_size+len(插入的字符串的长度)是否大于_capacity。
那是2倍扩容吗?不是,万一未扩容前的_size=_capacity=5,要插入长度len为10的字符串,那么2倍扩容_capacity==10<15就不够。所以我们选择reserve(_size+len)
c. +=
我们在这里实现(2)(3),其实就是复用前面的push_back和append即可
d. insert
我们在这里实现(3)和在pos位置插入一个字符的insert
- 先写插入一个字符的,因为只插入一个字符,所以如果要扩容,2倍扩容即可,和push_back相似
问:下面的代码有哪里错吗?
我们发现,给其他位置插入一个字符时没有问题,但给pos=0位置插入时好像进入了死循环,这是为什么?
因为end是int类型的,pos是size_t的,不同类型比较时要先转换成同种类型,所以将int转换成size_t,因为size_t是无符号整型unsigned int,它>=0,因此将end转换成size_t类型的临时变量与pos==0相比较,永久成立,因此会死循环(end会变成负数,但是将end转换成size_t类型的临时变量,该临时变量是>=0的)
所以该如何修改呢?
方法1:将pos强转成int,这样end和pos比较就是用int比较,end- -到-1就不再进入循环
方法2:
让end初始化为‘\0’后一位的下标,把_str[end-1]赋值给_str[end],这样的话,循环条件就是end>pos,当将第一个字符移到第二个位置上后,end–,end==0,不符合循环条件,退出循环
- 再来写插入一个字符串的insert,也是2种方法,思想和上面一样
- 写完了2个insert函数,就可以在push_back和append函数中复用insert了
(E) erase/substr
a. erase
实现(1),要求:从pos位置开始,删除len个字符
Len的缺省值是npos,npos是string类中定义的一个static 变量,值为-1。因此我们也要在自主实现的string里加该static变量
问:下面代码有问题吗?
有,npos是-1,换成size_t类型的即为最大值,如果我们传的实参len==npos-1,pos>1,那么就会栈溢出风险,因此我们将len单独放在一边
b. substr
要求:返回子串,子串从pos位置开始,持续len个字符
(F) swap/find
a. swap
问:C++有提供swap模板,为什么string类又重新实现了一个swap?
因为调用C++库里的swap模板,要先调用一次拷贝构造,再经过2次赋值,最后再调用1次析构函数,太麻烦了。所以写了一个专门给string类的swap函数,这样就不需要调用上述的拷贝构造/赋值/析构函数,提高了效率
那如何避免string对象使用C++库的swap模板呢?在string类外再实现一个全局的swap函数,它在函数体里调用类里的swap(如下图)
那为什么对string对象用swap(s1,s2)调用的是我们写的全局swap,而不是C++库里的swap呢?因为对于string类来说,我们写的和C++库的都是全局的函数,我们写的swap函数的形参类型就是string,而C++库的是模板,有现成的就要现成的,因此会选择我们写的swap函数
b. find
我们写(2)(4)
(G) >>/<</getline
a. <<
b. >>
向string对象输入时,需要先清空对象的内容,再将输入的内容拷贝到对象中。注意:当遇到空格或是换行时,一次cin结束
先写清空对象的函数clear
再写>>重载
问:下面代码有问题吗?
有问题,不管我们输入空格还是换行都不能让程序停止,为什么?
因为cin>>会忽略空格和换行,也就是说cin>>根本不会取到空格和换行,因此会死循环。C++库中提供cin.get()来取得所有输入的字符,包括空格和换行
有人认为,像上图这样只要不是空格和换行就让s+=ch这条语句效率不高,因为如果输入的字符串比较大,那么s就可能要扩容许多次。因此有人相出了下面的这种方法,能让效率提高
c. getline
getline的思路和>>重载类似
(H) 现代写法vs传统写法
a. 拷贝构造
b. operator=
好了,那么本篇博客就到此结束了,如果你觉得本篇博客对你有些帮助,可以给个大大的赞👍吗,感谢看到这里,我们下篇博客见❤️