手撕string
目录
引言
1,成员变量
2,先建一个可以跑的
2_1,构造函数
2_2, 扩容函数reserve
2_3,push_back
2_4,append[ ]
2_5,operator <<
2_6,测试一下,看猪跑
3,默认成员函数
3_1,拷贝构造函数
3_2, 赋值运算符重载
3_3,析构函数
4,string 里的便利操作
4_1,operator[ ]
4_2,operator +=
4_3,迭代器 iterator/const_iterator + begin() / end()
4_4,测试一下
5,完善功能
5_1,Member constants : npos
5_2,find
5_3,substr
5_4,insert
5_5,erase
5_6,resize
5_7,测试一下
6,信息获取功能一锅端
6_1,size
6_2,capacity
6_3,c_str
6_4,empty
6_5,clear
7,string 比较一锅端
8,operator >> 输入
补充说明
string 里的隐式类型的转换
不足
结束语
革命尚未成功,同志仍须努力
引言
前面已经学习了c++的基础知识,这一期就带领大家来手撕string。废话不多说,现在就开始吧。
1,成员变量
一个类最主要的部分莫过于成员变量了,那该怎么设计类string的成员变量勒?
string是对字符类型有着非常便捷的一系列操作的类,所以我们用 char* _str 来作为第一个成员变量,通过对其进行扩容等一系列操作,就可以非常灵活的控制其大小,不会浪费空间的同时也可以灵活变通可大可小,说到扩容,那就必然少不了int _size, int _capacity 俩件套。
这里解释一下为什么要在成员变量前加 “ _ ",仅仅是为成员变量加上一个” 身份标签 “,使我们在其他场合下能够区分成员变量,当然哈,在成员变量前加 “ _ "是我的习惯,在后面加也是可以的。
2,先建一个可以跑的
2_1,构造函数
在使用过程中,我们通常是用一个字符或者一个字符串来定义string的对象。
那么我们可以把参数设置成一个字符串,并给出一个缺省值空串。根据咱们传的参数给_str进行相对应的扩容(用new即可),然后再把参数的内容拷贝到 _str 里就 ok 了。
2_2, 扩容函数reserve
在后续添加字符的时候扩容是少不了的,接下来就来实现 reserve 函数。参数给定一个 len,reserve 将其_capacity 扩大到 len。
那再扩容函数里面是不是要用 realloc 呢?realloc 分为原地扩容和异地扩容,咱们都到c++了,咱们就用 new。
开好空间后将_str 的数据拷贝过去,delete[ ] 掉_str 原有的数据,再把其赋值给 _str。
2_3,push_back
尾插一个字符,先判断是否需要扩容,再尾插,记得_size++ 和 ' \0 '
2_4,append[ ]
追加一个字符串,先判断是否需要扩容,再追加,记得_size++ 和 ' \0 '
2_5,operator <<
目前咱们有了构造函数,有了添加操作,再来一个打印函数,咱们就可以看见猪跑了。
但是勒,这个 operator << 不能放在类里面,因为operator << 的左操作数会被 this 侵占。所以我们定义到类外面,再使用友元进行访问,
2_6,测试一下,看猪跑
从打印的结果上来看,目前咱们的代码是没有太大问题的
3,默认成员函数
3_1,拷贝构造函数
3_2, 赋值运算符重载
这里给大家介绍两种写法
3_3,析构函数
4,string 里的便利操作
4_1,operator[ ]
在字符串/字符数组中,[ ] 用着贼香,string 也应该有一个,咱们就来重载一个
要注意哈 访问一个, 修改一个
4_2,operator +=
虽然前面有了 push_back 和 append 但是频繁调用也很麻烦, += 应运而生,但是勒,咋没没有必要再写一遍,我们再 operator += () 里面调用 push_back 或者 append 函数即可,增加代码的复用性。
4_3,迭代器 iterator/const_iterator + begin() / end()
这里的迭代器本质就是 char* / const char* 啦,直接typedef一下。
有了迭代器,咱们就可以使用范围 for ,上面的 operator << 就可进行改善啦
4_4,测试一下
从打印的结果上来看,目前咱们的代码是没有太大问题的
5,完善功能
5_1,Member constants : npos
类外面进行初始化
使用过 string::find 的都知道,当没有找到的时候返回的是 npos 那么npos 是什么呢?
其实 npos 是一个公共静态成员变量(一般是 size_t 类型),表示该类型的最大值,主要作用是,查找操作的失败标记,或者是表示字符串末尾。在类外使用的时候要带上域作用限定符。
5_2,find
可以借助<string.h> 中的 strstr 函数帮助我们查询,但是要注意 strstr 找到返回的是指针,没找到返回NULL.
我们可以根据返回的指针和 begin( ) 做减法,就可以得到下标啦
5_3,substr
5_4,insert
在数据挪动的时候特别容易出错,一定要注意。
5_5,erase
在数据挪动的时候特别容易出错,一定要注意。
5_6,resize
resize的功能我们来复习一下,主要功能: 改变 _size 。
传入一个数 n 和 一个字符 c
如果 n <= _size ,就缩小size 并将多余的数据删除
如果 n > _size,就增加 _size 并 初始化
5_7,测试一下
ok的呀
6,信息获取功能一锅端
这些都比较简单直接上代码
6_1,size
6_2,capacity
6_3,c_str
6_4,empty
6_5,clear
7,string 比较一锅端
字符串的比较是按照字典序来比较的
一定要注意啊,比较咱们是不能用 strcmp 的,因为 strcmp 遇见 ' \0 ' 就停止了,而string 的对象中间可能会夹杂着 ' \0 ';
有了俩个之后,后面就不用写了,直接复用就ok了
8,operator >> 输入
输入就比输出麻烦太大了,输入了,咱们要先清空要输入的对象里面的原始数据,也需要解决前导空格的问题,而且还要保证效率......
istream& operator>>(istream& in, ssy::string& s)
{
s.clear(); //输入,会对之前的数据进行删除
char ch = in.get();
//跳过前导无用的空格
while (ch == ' ' || ch == '\n')
{
ch = in.get();
}
char buff[128];
int i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch; //为了防止扩容太频繁,借助buff数组
if (i == 127)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i != 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
输入测试:
补充说明
string 里的隐式类型的转换
在 append / push_back 我们分别写出了尾插一个字符/字符串。
而在插入 insert 和 find 函数里面我们没有去区分 单个字符和字符串,因为无论我们传参传的是单个字符还是一个字符串,单参数的构造函数支持隐式类型的转换,它都会被转换成 string 类型。
不足
虽然咱们手撕了一个 string 但是 咱们撕的只能说是一个简易版,我只是把 string 里面常用,经典的函数带着大家手撕了一下,库里面的 string 的功能是非常强大的,而且每个函数都有多种重载。
所以说,咱们手撕的还是会有很多功能做不到的,例如
ssy :: string s1(ssy);
ssy :: string s2(666);
s1 += s2;
咱们的 operator += 是不支持 对象间的 += 的。
结束语
大家可以通过这篇博客感受一下string 的创建,也可以继续完善咱们自己的string