目录
一、前言
二、模拟实现
1、构造函数
2、拷贝构造函数
3、operator=
4、operator[]
5、迭代器
6、string类的比较
7、string类的扩容
7.1、reserve
7.2、resize
8、string类的尾插
8.1、push_back 与 append
8.2、operator+=
9、string类的insert
9.1、插入字符
9.2、插入字符串
10、string类的erase
11、string类的swap
12、string类的find
12.1、查找字符
12.2、查找字符串
13、string类的流
13.1、流插入
13.2、流提取
三、不同编译器下的string类
一、前言
我在上篇文章《string类的使用》中已经对string类进行了简单的介绍,大家学习之后已经可以正常的使用string类了。本篇文章主要针对string类的底层进行较为深入的剖析,以方便同学们更加深刻的理解这部分内容。
二、模拟实现
1、构造函数
string类的构造函数的大致格式如下:
在构造函数的初始化列表中,只对成员变量 _size 进行了初始化,这是因为初始化列表中的初始化顺序是以成员变量的声明顺序为准的,为了防止有些情况下把 _capacity 声明在 _size 之前,造成 _capacity 被初始化为随机值的问题,就把 _capacity 的初始化工作放到了函数体中进行。当 _capacity 被初始化完成后,在函数体内使用关键字 new 开辟空间,空间大小为 _capacity + 1 (_capacity为string类最多存放字符串的个数,不包括 '\0' ),最后把字符串拷贝到对象中。
而在默认构造函数的第 11 行中,我们 new 了一个 char 类型大小的空间,注意一定要带上方括号 '[]' ,这是为了以后写析构函数时使用 delete[] 更加统一。
以上为string类的构造函数与析构函数的整体框架,现在对这个框架可以做一些优化。我们知道,对于一个类,最好提供一个缺省的默认构造函数,那么该如何缺省呢?
1、第一种写法
对构造函数的参数进行缺省,缺省值为 nullptr ,这种写法是错误的,因为在初始化列表中使用了 strlen 函数,对空指针进行了解引用,造成程序崩溃。
2、第二种写法
对构造函数的参数进行缺省,缺省值为 '\0' ,这种写法也是错误的,因为类型不匹配, '\0' 被转换成了 int 类型,同样被当作了空指针,造成程序崩溃。
3、第三种写法
对构造函数的参数进行缺省,缺省值为常量字符串 "\0" ,这种写法才是正确的,也可以写成:
2、拷贝构造函数
当我们使用默认拷贝构造函数来实例化对象时,会出现以下错误:
这就是由于浅拷贝问题引起的。
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。这方面内容我曾在文章《类和对象(二)》中有过较为详细的讲解。
为了避免出现浅拷贝问题,我们需要自己写一个深拷贝的拷贝构造函数:
3、operator=
与拷贝构造函数相同,string类的赋值运算符重载同样要考虑深拷贝的问题,具体操作如下:
首先进行 if 判断,防止自己给自己赋值,造成资源浪费与不必要的错误,再在条件语句中先 new 出指定大小的空间,并把字符串拷贝进来之后,再 delete[] 掉被复制对象原有的空间,以防止申请空间失败,造成了对原有对象的破坏。
4、operator[]
const 与 非const 函数构成重载,以满足不同情况时的调用需求。
5、迭代器
我们这里模拟实现的是原生指针实现的迭代器:
begin() 指向字符串第一个字符的位置, end() 指向字符串最后一个字符的下一个位置。
为了满足不同情况下的调用需求,分别设置了普通类型的迭代器和 const 类型的迭代器。
由于迭代器已经被我们模拟实现了,所以 范围for 也已经被实现了:
6、string类的比较
通过多个复用,可以较低成本实现所有的比较逻辑。需要注意的是,所有在内部不用修改成员变量数据的成员函数,最好都写成 const 类型。
7、string类的扩容
7.1、reserve
在写 reserve 时,考虑到有可能指定大小 n 还要小于对象原本空间大小,造成在使用 strcpy 进行字符串拷贝时出现越界的错误,所以需要使用 if 语句判断一下。
需要注意的是,我们 new 出新空间的大小一定要比指定大小 n 多 1 , 用于存放 '\0' 。最后把字符串最大有效字符数目 _capacity 赋值为 n 。
7.2、resize
我们知道string类成员函数 resize 的功能是指定对象占据空间的大小,并把空余的部分填充成指定字符。那么就可以分为如下三种情况: n <= _size 、 _size < n <= _capacity 、 n > _capacity ,需要分别讨论:
注意, resize 是不支持缩容操作的,即不会更改 _capacity 的值。因为缩容操作相当于另外开辟一块更小的空间,并把原有空间前 n 个数据拷贝到这个更小的空间中,如果以后又要插入数据,还要重新扩容,代价太大。
8、string类的尾插
8.1、push_back 与 append
对于 push_back 仅在字符串尾部插入一个字符,可以直接扩容二倍,但是对于 append 在字符串尾部插入一个字符串时,则需要按照需求进行扩容,以防止二倍扩容不够用。
注意,在 push_back 函数最后,要记得加上 '\0' 。在拷贝字符串时,使用的函数是 strcpy ,而不是 strcat ,这是因为 strcat 需要自己从头向后寻找 '\0' ,效率太低。而用 strcpy 可以直接指定字符串结尾的位置,不用重新寻找。
8.2、operator+=
通过函数重载与复用,实现了单个字符和字符串的尾插。
9、string类的insert
9.1、插入字符
我们先来看一种错误的写法:
我们使用这种代码,当 pos 为 0 时,就会出现报错。这是因为 end 的类型是 size_t ,即无符号整型,所以当 pos == 0 , end 为 0 时,依然可以进入循环,并且 end 的值经过 -- 后变为了最大的整型数值,从而陷入无限的循环。
为了避免这种错误,我们可以采取如下写法:
9.2、插入字符串
与插入字符的思想相似,具体代码如下:
需要注意的是,while 循环的条件是 end > pos + len - 1,要把 pos 的内容也拷贝到后面。使用函数 strncpy 来拷贝字符串,防止 '\0' 对字符串造成影响。
10、string类的erase
具体实现如下:
11、string类的swap
string类的swap只交换对象所指向的空间以及对应的成员变量信息,逻辑图如下:
实现代码:
12、string类的find
12.1、查找字符
实现代码如下:
12.2、查找字符串
实现代码如下:
13、string类的流
我们首先需要知道在类中,流插入与流提取的重载不能写成类的成员函数,因为 this 指针会抢占第一个参数的位置,在《类和对象(三)》中,我们使用的解决方法是通过设置友元函数的方式来使类外函数可以访问类中的 private 成员变量。但是并不是所有的流插入、流提取操作都要设置友元函数的,只要不需要访问 private 与 protect 成员变量就无需设置。
13.1、流插入
实现代码如下:
13.2、流提取
我们首先来看一下一种错误的写法:
如果采用这种写法,那么我们在输入字符 ' ' 或 '\n' 时,会因为C++默认该字符是多个字符之间的间隔而不读取该字符,从而出现不符合预期的结果。
为了避免这种错误,我们应该采用函数 get 来读取字符:
解决了这个问题后,其实还有一个问题,那就是如果该字符串中本来就有字符,再使用我们重载的流插入后出现的结果与库中string类的流插入不符:
库中string类的流提取直接刷新了对象中字符串的内容,而我们自己实现的流提取是在尾部追加,所以我们可以在流提取函数最前面增加一个 clear 函数:
又考虑到如果一次性提取的字符数量过多,导致对象进行多次扩容,造成资源的浪费,为了避免这种情况,我们可以开辟一个缓冲数组,用于存储字符,数组满了之后再进行一次性扩容。
当读取到字符 ' ' 或 '\n' 时,如果 buff 中还有字符,就把这些字符再尾插到对象的字符串中。
三、不同编译器下的string类
我们可以写出下面代码观察在vs下string类对象的大小:
可以发现我们模拟实现的string类与库中的string类的大小有所区别,这是因为库中的string类里有一个 16 和字符大小的数组, 这方面的内容我曾在文章《string类的使用》中有过讲解。
而在 Linux 的 g++ 编译器中,string类的大小只有 4 个字节(32位) 或 8 个字节(64位),即一个指针的大小。这与他们的底层实现有关,g++编译器下的string类的结构如下图所示:
string类中只有一个指针 _ptr ,其他属性以及数据都存放在堆区开辟的空间中,如果后面进行了拷贝构造,则直接实例化出一个新的对象,并把指针指向同一块位置,同时更改 _refcount 的数值:
在析构时,如果 _refcount 的值大于 1 ,则进行 -- 操作,等于 1 ,就把该空间释放掉。
如果想要修改 s1 或 s2 的内容,那么谁写谁就触发写时拷贝,再在堆区上另外开辟出一块空间,进行写操作:
关于string类模拟实现的内容就讲到这里,希望同学们多多支持,如果有不对的地方欢迎大佬指正,谢谢!