目录
7. string类 重载操作符、运算符和流插入、提取操作符
string 介绍
在C语言中,我们如果要实现一个字符串,只能用char str[] 和char* str。而在C++库里,有string可以更好地方便我们对字符串进行增删查改。
string - C++ Reference (cplusplus.com)中我们可以进行查询相关文档来了解string的各个用法。
可以从文档里看出,要实现一个string类,工作量很大,需要实现各个接口函数,而且需要注意很多的细节。但是就是因为c++库的函数做了过多的重载与不常用的函数接口,也被吐槽过不少次。
所以,本文章只模拟实现我们在日常生活中比较常用的接口函数,且为了方便理解,我们也尽量简化了它的实现。
string 的模拟实现
1.string类的成员变量
为了不与全局中的string类相冲突,我们可以使用namespace创建一个命名空间域,防止与全局的string相冲突。
而我们后续写的测试用例也是在命名域中实现。
class string
{
private:
char* _str; //用于存放字符串
size_t _size; //已存放的有效字符数量
size_t _capacity; //可存放的有效字符数量
};
char* _str: string所存放字符串本质其实也是用的char* ,并没有用其他的东西。而_str就是用于存放字符串。
size_t _size: 它用于记录我们已经存放的有效字符的数量。 '\0'不属于我们的有效字符
size_t _capacity: 它用于记录该字符串最大可以存放的有效字符的数量。 '\0'不属于我们的有效字符
注意: C++库中的string类的成员变量其实比上述三种成员变量更加复杂,而string类的成员函数简化来看的话,其实就是以上三种。
2.string的默认成员函数
每个类都有自己对应的成员函数,string更是不例外,而且因为涉及深浅拷贝问题,我们必须要自己来实现他们的默认成员函数!
<1> 构造函数
既然是构造函数,我们首先要支持它可以初始化,而string的初始化只需要输入字符串即可,所以我们的参数是 const char* str = ""(注意这里的缺省是空字符串)
在构造函数,我们其实就已经面临了深浅拷贝问题,如果使用默认的构造函数,会使用浅拷贝,后面析构的时候会导致一个指针析构两次!
而我们深拷贝的过程则是先new一个足够空间大小的字符串,再使用strcpy进行字符串的深拷贝。
注意: 我们在进行new新空间的时候一定要记得给'\0'留下一个位置,因为'\0'是判断一个字符串的结束标志,所以我们上面写的是new char[_capacity + 1],而'\0'是不属于我们的有效字符。
<2>析构函数
析构函数没有什么需要注意的地方,因为我们已经在构造函数实现了深拷贝,所以析构函数不会重复析构一个指针。
<3>拷贝构造函数
拷贝构造也要注意深浅拷贝的问题,如果是默认的拷贝构造函数也会是浅拷贝,也会导致一个指针析构两次,所以要进行深拷贝。
如果说我们称上面那种写法为 传统写法,那么还有一种写法可以称之为现代写法。
不知道大家是否可以看出来这种写法的精髓所在
它最大的亮点就是通过一个临时的string 对象,用构造函数来完成我们的拷贝构造。
注意: 现代写法的初始化列表的_str必须初始化为nullptr,否则析构的时候会析构一个野指针,会报错。
<4> 赋值函数
和拷贝构造一样,这种写法也有一种现代写法,更为巧妙
它的精髓比拷贝构造还要精,这种写法让这个临时变量的作用到了极致。
它不带来了深拷贝的内容,还把原先的不需要的内容帮忙析构清理了,所谓是用到了极致。
3.后面会用到的一些基础接口函数
<1> reserve
reserve的作用就是重新开辟一块更大的空间用于存储字符串,后面我们进行插入操作的时候必须要用到。
<2> size
作用: 返回目前的_size
<3> capacity
作用: 返回目前的_capacity
<4> c_str
作用:返回字符串内容以字符串形式
<5> operator[]
重载完[],后面对_str进行操作会轻松很多。
再也写一个const 版本兼容const string。
注意:pos不能比_size小,否则会出现非法访问,导致运行出错,最好使用assert断言一下。
<6> clear
作用:让字符串成为一个空字符串
注意:'\0'后面可能会有内容,但是不影响,因为'\0'是字符串结束的标志,并且_size被置为了0
<7> resize
作用:如其名,重置字符串的_size为n,如果空间不够就调用reserve来扩容,如果_size要大于n,就会删除n后面的内容,相当于调用了erase,这个也可以用来删除数据,但是我们习惯用erase来进行删除数据。
4.string类常用的插入操作接口函数
<1> push_back
过程很简单,先判断原先的空间(_capacity)是否足够存放添加后的内容,如果不够就调用reserve函数进行扩容,再进行插入。
后面如果实现了insert函数可以直接调用insert。
注意: 记得手动去添加‘\0’,否则会出现字符串找不到停止标志
<2> append
它的作用是用于在字符串后面追加一个字符串或者字符内容。
过程很简单,先判断原先的空间(_capacity)是否足够存放添加后的内容,如果不够就调用reserve函数进行扩容,再进行一个一个插入。
调用push_back。
注意: 记得手动去添加‘\0’,否则会出现字符串找不到停止标志
<3> insert
insert可以在pos位置的下标位置进行插入一个字符串或者字符内容。
它的过程也和append一样,先检查空间够不够,不够就扩容。然后再把pos位置后面的内容往后挪,再进行pos的插入,以免数据重叠丢失。
注意: 一定要对下标pos的位置有很清晰的空间概念,不然很有可能导致挪动位置不对等问题,最好先画图理解
5. string类常用的删除操作接口函数
<1> erase
erase的作用是删除pos位置下坐标后面的len长度的字符。
删除的过程是先将pos+len位置后面的数据包括'\0'覆盖掉pos位置要删除的数据
erase很简单,只不过在这里我要补充一个string类重要的特殊常量npos。
nops 是一个size_t 类型 ,而又被赋值为-1,所以-1的补码如果变成一个无符号整形将会变成一个非常大的数字,所以它的作用一般用于取到字符串的尾。
比如说上面的erase,如果你没有传len的值,他会默认删除pos位置后面的全部内容。
npos是一个string类中的const静态成员变量
6. string类的查找接口函数
<1> find
如果找到了则返回下标,如果没找到则返回npos
查找string类的字符串
查找c形式的字符串
查找字符
C++库里还有其他的查找函数,但是我们一般只会用到原版的find,所以这里只实现这一个,大家有兴趣可以实现其他的
7. string类 重载操作符、运算符和流插入、提取操作符
<1> operatro+=
重载+=其实可以直接理解为调用append,并且我们在使用string时也习惯使用+=,而不是append,+=用着更舒服。
<2> operator==
我们可以调用全局中的strcmp来直接进行比较。
<3> operator>
<4> operator>=
只要实现了两个重载条件操作符,就可以实现套用
<5> operator<
<6> operator<=
<7> operator!=
<8> operator>>
注意:流插入提取操作符是声明定义在类的外面的,并且string类的重载流操作符可以不需要添加友元函数
<9> operator<<
流提取操作符需要注意,如果仅仅使用<<进行提取字符,会导致忽略" ",永远没有办法提取完,所以我们需要使用in.get()来提取空格和'\n',
8. string类 子串提取
<1> substr
作用:从pos位置提取len长度的字符串
注意:如果不穿len的长度,则默认为npos,即取到尾
9.string类的迭代器
string类的迭代器其实就是原生指针
当我们实现了begin和end,范围for也可以使用
总结
以上便是本章的内容,我们一步步从了解string类到 初步模拟实现。
实际的C++库中的string更加复杂,其实也能说是冗杂,有许多不必要的功能,但是如果大家有兴趣,也可以去了解一下。