前言
string在C++里是一个已经被封装好的类,类中实现了各种功能的函数,可以让我们更轻松的对字符串进行操作。在此通过对string类的模拟实现来进一步了解string类函数的使用。
一、string类简单了解
1.string的大小
定义一个库中的string类的对象s1,并计算其大小。如下图
可以看到,string类的大小为28
2.string类的成员变量
类的大小仅和成员变量有关。
按照我们的理解,string的成员变量应该和顺序表一样,存储有三个变量。
一个char*的指针,用来存放字符数据,size_t 类型的size,用来表示字符的个数,size_t类型的capacity,用来表示开辟空间大小,按照这样来计算,在32位平台下,string的大小应该是12。可真实的结果却是28。
通过调试,我们可以直观的看到,在string类变量中,size和capacity确实存在,但确新增了另外两个我们不懂的东西,alloctor和原始视图,而他们就是string类大小变化的原因。
如果不断展开原始视图,最终会发现一个叫做_Buf的数组。
实际上,在vs平台下,string类的成员变量中存在一个16字节大小数组_Buf,当string对象存储的字符数据没有超过它的大小时,就会存储在_Buf中,而存储的字符数据超过它的上限,就不会在其内存储。
例如,对于字符个数没有超过_Buf数组大小的string对象s1,_Buf中存储数据。如下图
但是,对于初始化存储18个字符的string类对象s2,不会在_Buf数组中存储添加的数据
并且,当s1存储数据超过_Buf数组的上限后,_Buf数组也不会在存储。如下图
也就是说,除了我们猜测的char*指针,size,capacity,string的成员变量还要一个16字节大小的字符数组,这也是为什么string类的大小是28的原因,但实际上,string还有一个静态成员变量npos,它的类型是size_t ,对于静态成员变量,在计算类大小时,因为是被所有该类对象共有,因此不计算在内。下图是库中对npos的定义。
3.string类内的函数
string类实现的函数有很多,除了成员函数等对字符串进行操作的函数,最重要的是引入了迭代器Iterator。
3.1Member functions部分函数
构造函数
对于string类的构造函数,实现了不同的重载。但本质上,构造函数的功能只是初始化,根据不同的初始化需求,所以有多个重载的构造函数。
其中(2)是拷贝构造,(7)则是利用模板,通过迭代器进行初始化
析构函数
对于string的析构函数而言,想要完成清理任务很简单,简单的释放动态申请的空间即可。
赋值运算符重载
自定义类型的赋值必须进行重载,因为其内可能有多个成员变量,因此需要对每个成员变量依次赋值
3.2Iterators部分函数
关于迭代器,在string类中,我们将其当作指针来进行使用即可。该部分函数的功能一目了然,需要注意的是,end指向的位置是最后一个字符的下一个位置以及const迭代器的实现
3.3Capacity部分函数
该部分的重点是扩容,reserve和resize,具体内容会在模拟实现中提及
3.4Element access部分函数
operator[]使得可以通过下标来访问对于的字符,而at可以返回对应下标位置的字符
3.5Modifiers部分函数
增删操作等等
3.6String operations部分函数
简单函数的应用,精华在于find和substr
3.7Non-member function overloads部分函数
关键是重载输入和输出,重载后可以实现对自定义类型(自己写的)的输入和输出。
二、对string类的简单模拟实现
1.string类的成员变量
因为是模拟,因此没有实现_Buf数组,但是不会有什么影响
对于static成员变量,必须在类中声明,类外定义。
2.string类的成员函数
2.1构造函数的实现
注意事项:
缺省参数的”“没有空格,但是会存在一个字符‘\0’,并不需要再添加。
初始化列表的顺序和成员变量的定义顺序保持一致。
多开一个空间是留给字符串的结束标志\0的。
构造函数的形参类型是char* ,因此缺省参数只能是”“,而不能是‘’(单个字符)。
常量字符串的类型是const char*, const char* 到char* 涉及权限的放大,所以形参在接收时也加了const。
2.2拷贝构造
拷贝构造也只是构造函数的重载,也是用来初始化对象的,代码实现大同小异。
注意事项:
关于_arr的初始化,strcpy和memcpy都可以,只要在初始化_arr时,不是写成_arr(str)的直接赋值即可。
_arr(str)的初始化方式会让两个指针指向同一块空间,因为拷贝构造是用一个对象初始化另一个对象,这样就会导致两个对象中的_arr指向同一块空间,会引发同一块空间要析构两次的错误
函数效果如下图
2.3operator=
此处的赋值运算符重载的实现复用了拷贝构造,形参中的string没有添加引用,在接收参数的时候会先发生拷贝构造,再利用swap进行交换,产生的临时变量s出了作用域就会销毁。
因为是复用的拷贝构造,而实现的拷贝构造已经解决了浅拷贝的问题,所以没有问题。
2.4析构函数
回到最初的模样,四大皆空。
3.capacity部分函数的实现
3.1扩容函数reserve和resize
reserve和resize的区别
reserve只负责开辟新空间并将旧空间的数据拷贝过来,
而resize还要再reserve的基础上,将剩余的空间全部填充
reserve函数实现
注意事项:
_arr的真实空间大小是n+1,但最后是用n来更新_capacity,最后一个空间始终是用来存放\0的,也就是说,_capacity表示的含义应该是能够存储的字符个数
memcpy和strcpy都可以用。
reserve的功能仅仅是开辟新的空间,并将旧空间的数据拷贝过来,所以
reserve开空间之后,改变的是整个空间_capacity的大小,_size的大小并不会改变。所以只需要更新_capacity。
只有先将旧空间的数据拷贝一份后,才可以释放旧空间。
resize函数实现
注意事项:
resize复用了reserve函数。在对剩余空间进行填充字符时,直接用下标进行访问填充。
实际上,string也实现了对[]的重载,使得string对象也可以用下标来访问字符。如下图
可以看到,下标访问是被_size限制的,只有_size下标内的数据才可以用下标访问,那为什么resize可以用下标的方式来进行访问填充字符呢?
这是因为,resize中的下标访问并不是调用重载的[]函数,重载的[]对象是string类。
函数后的const修饰的是隐含的this指针,该函数只有加了const,才能被const对象使用,并且也能够被普通对象使用。
3.2其余简单函数的实现
4.Modifires部分函数的实现
4.1push_back
函数实现步骤
1.判断是否需要扩容
2.访问尾部,插入数据
3.更新_size,放置\0
实现代码:
4.2insert
函数实现
注意事项:
如果是在中间部位插入字符串,需要先从后向前依次挪动数据,再将要插入的字符串插入到挪开的位置上。
pos最小为0,因为end的类型为size_t ,为了防止end从0变为npos,所以在每次挪动数据的时候加以判断。
4.3append
函数实现
注意事项:
此处实现的函数是将整个字符串加在原本的字符串末尾,所以写法较为简单。如果要控制追加字符串的起始位置和追加的字符个数,还需另作判断。
4.4operator+=
函数实现
复用的append,函数效果和append完全相同,都是在原本字符串的基础上再追加字符串。
另外复用push_back函数实现每次追加一个字符
4.5erase
函数实现
对于erase来说,只有两种情况,一种是要删除的字符是字符串中间的某一部分,要么是从pos位置开始往后的字符全部删除。如果是后者,只需要更新_size后,放入字符串结束标志\0即可。如果是前者,需要从前向后移动数据,包括\0,这也是为什么end可以等于_size的原因。
5.String operations部分函数实现
5.1c_str
5.2find
在一个字符串中查找某一个字符,这很简单,只需遍历一遍即可。
如果是在一个字符串中找一个字符串,并且是第一次出现的字符串,又该怎么做呢
复用了库中的字符串函数strstr,该函数的功能就是找在第一个字符串中找第一次出现第二个字符串的起始位置的字符。如果找不到就返回空。
5.3.substr
该函数的功能是返回字符串的子串。需要注意的是字串长度如果是npos或者从pos开始的len个字符已经超出母串的长度,需要更新len的长度。
6.迭代器部分
在string的模拟中,迭代器就是对指针的重命名。之所以添加const,是因为普通对象可以使用没有const修饰的,也可以使用有const修饰的,但对于const对象来说,只能使用有const修饰的函数。
有了迭代器后,我们就可以利用迭代器来进行初始化。如下图
7.operator>>和operator<<
首先要明白一点,无论是istream还是ostream都可以看做是库中的类,我们平常使用的cin就是istream类的对象,cout就是ostream的对象。之所以cin和cout可以支持输入和输出某些自定义类型,是因为库中实现了对这些自定义类型的operator>>和operator<<,也就是运算符重载。
如果是我们自己定义的自定义类型,要想cin和cout实现对自己自定义类型的输入和输出,也需要进行重载。
运算符重载的作用,就是让运算符两端的参数按照我们想要的方式去做出相应的操作。
也就是说,无论是istream还是ostream,cin还是cout都是库中本来就实现好的,对于我们自己写的自定义类型,编译器不清楚要以什么样的格式进行输入和输出,所以才需要我们进行运算符重载,运算符重载的关键在于函数体内部的实现,以及运算符两端的参数。
任何一个类内的函数都会有一个隐含的this指针,如果operator>>和operator<<的实现要在类内实现,那么只能再添加一个参数,并且默认的第一个参数就是隐含的this指针。
如下图,当我们在类内实现重载时,如果设置两个参数时,就会提醒参数过多
如果是在类内实现,相应的参数需要变为*this,并且左右操作数的顺序也会和日常使用的相反。如下图