C++笔记-string(下)
这篇我们自己来简单实现一下string类中的各个接口,来帮助我们更好地理解string类接口的底层原理。
1.构造函数和析构函数
对于构造函数我们要写两种情况:空字符串和非空字符串
因为我们要自己实现string类,所以就不能用std命名空间,这里我们要自己创建一个命名空间,在里面实现string类。
我们之前也讲过string类底层就是一个数组,所以我们在实现时也按数组来实现,并定义数组中数据的大小和容量,就和我们实现顺序表一样。
这里面我先实现了c_str接口,因为我们现在是自定义类型,所以不能直接输出变量,所以实现c_str接口便于我们输出我们实现的字符串。
我们首先讲空字符串的情况:
这里可能有人会疑惑我为什么这样初始化?
可能觉得既然是空字符串,直接定义为null不就好了。我想大多数人的第一想法也是这样,但其实这样写是有问题的。
当我们初始化一个空字符串时,我们调用c_str接口时,返回值是char*类型,而此时字符串是空指针,乍一看没问题,感觉那就输出空指针嘛,没设么没问题啊。
但是char*这个类型比较特殊,它默认是当字符串输出的,所以它会解引用,此时就出问题了,对空指针解引用是很危险的事,所以不能初始化为null。
而字符串在底层保存时,其实会多开辟一块空间,来存储'\0',所以我们创建一个空间来存放'\0',_size和_capacity初始化为0即可。
下面讲非空字符串:
给一个默认空字符串也是为了调用但不传参数这种情况,而""这种在底层就是'\0'。
而是为了模拟真实的string类,所以我们创建空间时多创建一个空间来存放'\0'。
再把传入的参数拷贝到我们创建的空间即可。
最后是析构函数:
析构函数就和之前讲的一样,new出来的空间delete就行,再把_str置为null即可。
2.遍历字符串
在遍历字符串中我们要实现的就是size接口,[]符号重载以及迭代器的实现。
首先讲size:
这个就很简单,直接返回_size即可。
下面讲[]符号重载:
[]符号重载我们要实现两种,const类型和非const类型。
实现起来也简单,直接返回是相应位置的值即可。const类型和非const类型区别就是能否通过解引用来改变字符串的内容。
最后是迭代器的实现:
迭代器我们就只实现begin和end,并且每种都实现const类型和非const类型。
迭代器不一定是指针,但在这里实现时我们就用指针来实现,毕竟和指针的作用一样。
实现起来也简单,begin就是返回第一个元素的地址,end就是返回最后一个数据的下一个位置的地址。
3.修改字符串
修改字符串要实现的比较多,我就一个一个讲:
3.1reserve
reserve虽然能扩容也能缩容,但是缩容是分编译器的,并且实现起来较为麻烦,涉及当前未学的知识,所以这里我只实现扩容功能。
1.创建一个临时变量来接收我们扩容后的空间
2.讲当前数组的内容拷贝到新空间中
3.讲究空间销毁,并将_str指向新空间
4._capacity记录新的容量
3.2push_back
push_back就是尾插,注意的点就是要首先判断数组是否满了,满了就要扩容,最后在原_size的位置处插入数据,_size++,再将新_size位置置为'\0'。
3.3append
实现append时要先计算字符串的长度,并判断我们之前的扩容方式扩容后与_size+len谁更大,取更大的作为我们的扩容方式。
接下来就是把字符串拷贝进数组,这里我用memcpy进行拷贝,避免字符串中中间就含有'\0'导致拷贝终止。
注意:拷贝时要拷贝len+1个字符,所以最后一步可写可不写,或者拷贝len个字符,这样就要写上最后一步。
3.4+=运算符重载
实现了push_back和append后,实现+=运算符重载就简单了很多,唯一需要注意的就是我们要返回的是字符串本身,所以要注意返回值是string。
3.5insert
3.5.1插入一个字符
1.assert判断pos是否在有效范围内
2.判断是否需要扩容
3.因为底层数组,所以在中间位置插入一个数据就要将pos位置以后的数据全部向后移一位
4.将pos位置处的值置为要插入的数据
5._size++
至于为什么要把pos强转成int,这是因为我们这个循环的结束条件是i<pos,如果是其他位置当然没问题,但如果是头插那就有问题了。
当时头插时,i只有等于-1时循环才会停止,但是当i等于-1时会出现类型提升的现象,类型提升就是范围小的向范围大的提升,有符号的向无符号的提升,此时i就会向无符号整型提升,-1转为无符号整型是整形最大值,所以这个程序会死循环导致崩溃。
将参数中的pos改为int也能解决问题,但是在底层编译器在实现时用的都是无符号,所以为了保持一致,这里也就用无符号。
3.5.2插入一个字符串
1.assert判断pos是否在有效范围内
2.计算字符串的长度
2.判断是否需要扩容(这里扩容是和append一样的原则)
3.将pos位置以后的值全部向后移动len个位置
4.将字符串一个一个插入数组中,注意字符串的下标是i-pos
5._size+len
3.6erase
这里说明一下,在vs编译器下被const修饰的静态的整型变量是可以在类内定义的,也就是给缺省值,这是一种特殊情况。
因为在底层如果不传参数len,其实有个默认值就是npos,这个值其实是整型的最大值,但在定义时是-1。
1.assert判断pos是否在有效范围内
2.判断是删完还是只删一部分
3.只删一部分直接把pos位置处的值置为'\0',再将_size移到pos处,这样也算完成了删除
4.只删一部分,这里我用了memmove的方式将pos位置后面的值移到以pos为起始的位置处,其实和后面数据全部向前移len个位置是一样的,当然也可以用循环来实现。
5._size-=len
3.7find
3.7.1查找单个字符
查找单个字符相对简单,遍历整个字符串,找到对应字符就返回下标,没有找到就返回-1。
注意:在参数中有个pos,这个参数给个缺省值0,代表如果不传参数,就默认从下标为0的位置开始查找。
3.7.2查找一个字符串
查找一个字符串这里我使用strstr,这里用strstr来实现会更方便,如果查找成功strstr返回的是首个字符的地址,查找失败返回null。
这里如果查找失败就返回-1,查找成功就返回首个字符的下标。
注意:因为查找成功返回的是首个字符的地址,所以要想返回下标,就需要指针相减来得到。
3.8substr
substr作用就是得到某个区间的字符,所以参数也要给上缺省值。
1.定义空字符串m
2.判断len是否超出范围,超出范围按最大值算
3.提前扩容,提高效率
4.通过循环,将区间上的字符拷贝到m
5.返回m
3.9=符号重载
1.new一个临时空间,注意空间大小是_capacity+1,最后一个位置用来存放'\0'
2.调用memcpy函数将str中的数据拷贝过去
3.delete掉原先的_str,让_str指向新的空间tmp
4.返回*this
3.10<<符号重载
这是我们平常输出所用的符号,这个符号属于ostream流中的,所以参数我们也要用ostream类型。
这里解释一下为什么是两个参数:
这是我们输出时写的形式,可以看出在符号左边是ostream流中定义的变量cout,而参数右边是我们要输出的字符串,所以在实现<<时参数是这两个的原因,并且第一个参数要用引用,道理是和传值调用是一样的,这里就不过多赘述了。
而输出也很简单,遍历整个数组挨个将字符输出即可。
注意:在声明时可以不用把这个重载放在类中,如果要放在类中需要用到友元,这里我是在类外声明和定义的,所以不需要友元声明,并且我在ostream前加上std::是当前类中并没有展开std标准库,而ostream是在标准库中的,下面的<<符号重载也是如此。
3.11clear
clear因为下面>>符号重载需要用到,所以提前写一下。
clear就是将当前字符串清空,所以实现起来也相对简单:
直接将数组第一个位置赋值为'\0',再将_size和_capacity置为0即可。
3.12>>符号重载
1.清空字符串
2.利用get函数得到输入的每一字符
3.利用循环使str加上每一个输入的字符
注意:1.参数中的str此时不需要加const,因为要对str进行修改
2.解释一下这里为什么这里不用>>符号:
因为>>符号会自动掠过空格和换行符号,而我们输入字符的停止条件就是遇到空格或者换行字符,所以这里不能用>>符号,否则会一直输入不会停止
4.判断字符串
判断字符串就是判断字符串之间的大小情况,而这里实现<和==符号即可,剩下的符号用这两个即可实现。
4.1<符号
1.len1和len2来记录两个字符串的大小
2.i1和i2记录下标
3.以其中一个结束为循环条件,判断大小
4.最后返回的这个表达式是当两个字符串长度不相等时,前面都相等,最后跳出循环判断是true只有表达式这一种情况,其他都是false,所以直接返回这个表达式的结果即可
注意:循环条件是<而不是<=。
4.2==符号实现
前面和<符号实现基本相同,在循环中判断条件作出修改,如果相应位置字符不相等直接返回false。
跳出循环后,只有表达式这一种情况是true,其余情况都为false,所以直接返回表达式即可。
4.3<=符号实现
4.4>符号实现
4.5>=符号实现
4.6!=符号实现
实现了<和==符号后,剩下的符号实现起来就很简单。
这里可能有人会有疑惑:为社么不用strcmp或者memcpy来实现呢?
先解释strcmp:
一般情况下确实没什么问题,但我们要清楚strcmp遇到'\0'会停止,如果字符串中间含有'\0'呢?此时就会出问题,比较就会停止,所以不能用。
memcpy:
memcpy虽然解决了strcmp的问题,但是它面临两个字符串长度不一,到底是按大的走还是按小的走呢?不管按那个走都会面临越界访问的问题,所以不能用。
到最后还是我们得自己来实现。
以上就是string(下)的内容。