目录
这里我们就开始介绍我们string的模拟实现了,我相信在经过之前给大家介绍的标准库string类的使用后,大家对我们的string类都已经有一定的认识,心里对该底层实现也有了一定的猜想,那么现在我们就为大家打消疑虑,给大家揭开我们string神秘的面纱(注意:小编这里只给大家是实现了一部分我们经常使用的函数)。
首先我们需要使用我们命名空间来避免和我们的库中的string导致冲突
- namespace xhj
-
- {
-
- class string
-
- {
-
-
- };
-
- };
要知道我们string的成员变量,我们要从两个方面入手,首先是我们的string的存储结构,其次是根据我们string的成员函数。
1.很明显我们的string存储的是一串字符串,那么该底层的存储结构用的就是我们的char类型的变长数组数组,因此我们确定了第一个变量就是我们的char类型的指针
2.第二根据我们的size(),返回我们的有效字符个数,因此我们要使用一个int类型的变量记录我们的有效字符个数。
3.第三就是我们的capacity()了,这里我们就需要使用一个int类型变量,记录我们的容量大小
4.第四点比较难想到,但是小编在之前给大家提到了一个静态变量npos,这个在string常用来表示我们无限大,因此该是确定的一个size_t类型的常变量。
因此我们的成员变量如下:
-
- namespace xhj
-
- {
-
- class string
-
- {
-
- private:
-
- char* _str;
-
- size_t _capacity;
-
- size_t _size;
- const static size_t npos;
- };
- const size_t string::npos = -1;
- };
这里我们只需要实现我们比较重要的两个即可,也就是我们的无参构造,和我们的C-string进行的构造,但是这里我们可以使用我们的缺省参数将两个合并为一个,代码如下:
- string(const char* str = "")
- {
- _size = strlen(str);
- _capacity = _size;
- _str = new char[_capacity + 1];//由于多一位需要存储我们的‘/0’,因此要进行+1
- strcpy(_str, str);
- }
对于析构函数我们是需要自己实现的,因为这里都是内置类型,且我们这些内置类型中我们还开辟了新的空间如果我们不自己实现,很大程度会造成内存的泄露。
- ~string()
- {
- delete[] _str;
- _str = nullptr;
- _size = _capacity = 0;
- }
在实现拷贝构造前我们需要确认我们是否需要写我们的拷贝构造,很明显我们这里是非常有必要的,因为这里会涉及到浅拷贝的问题:

因此以下我们需要实现我们的深拷贝。
为什么我们在介绍拷贝构造函数之前,要先给大家介绍我们的交换函数呢?这里就涉及了我们拷贝构造函数的两种写法。
实现我们的swap函数是非常简单的,也就是:
- void swap(string& s)
- {
- //这里调用的是我们算法库中的函数
- std::swap(_str, s._str);
- std::swap(_size, s._size);
- std::swap(_capacity, s._capacity);
-
- }
那么我们的拷贝构造的两类写法是:
传统写法:
- string(const string& s)
- :_str(nullptr)
- ,_size(0)
- , _capacity(0)
- {
- //传统写法
- _size = s._size;
- _capacity = s._capacity;
- _str = new char[_capacity + 1];
- memcpy(_str, s._str,s._size+1);
- //C语言的字符数组,是以\0为终止算长度
- //string不看\0,而是以size算终止长度
- }
现代写法:
注意:对于传统写法,我们的现代写法依赖于编译器对数据的初始化,如果编译没有对数据进行初始化操作那么在交换的过程中很可能会出现随机值的情况,然后在最后对tmp进行析构的时候就会出现程序崩溃的情况,所以这里我们需要先走初始化列表
- string(const string& s)
- :_str(nullptr)
- ,_size(0)
- , _capacity(0)
- {
- //现代写法
- string tmp(s._str);
- swap(tmp);
-
- }
对于传统写法我相信大家都能理解,对于现代写法这里小编就需要给大家解释一下了,我们这里先调用构造函数,使用我们s这个对象的_str部分去构造我们的tmp对象,这里我们只需要将我们的tmp和我们的当前对象进行交换,这也就达成了我们的当前对象的所有成员对象都赋予了我们tmp的值,而我们的当前地址空间,只需要交给我们的tmp出局部作用域进行销毁,也就是:

对于赋值运算符我们也是需要进行重载的,这里也牵涉到我们的浅拷贝带来的问题,因此我们这里也是需要重新开辟一段空间进行我们数据的存储的,那么这里我们也有我们的两种写法:
注意:1.我们的原本空间可能小于我们的形参的数据空间,因此我们要重新开辟新空间
2.不要使用原空间指针开辟新空间,以免开空间失败破坏原空间
传统版本:
- string& operator=(const string& s)
- {
- if (this != &s)
- {
- //传统写法
- char* temp = new char[s._capacity + 1];//避免我们原本指针开空间失败导致旧空间被破坏
- memcpy(temp, s._str, s._size + 1);
- delete[]_str;
- _str = temp;
- _size = s._size;
- _capacity = s._capacity;
- }
- return *this;
- }
现代版本:
- string& operator=(string s)//传值传参调用拷贝构造
- {
- swap(s);
- return *this;
- }
对于传统版本相信凭借大家的基础一定是随便掌握,这里小编仅给大家介绍一下我们的现代版本,这里我们先让此处直接进行传值传参,调用我们的拷贝构造,那么我们此时的s对象就是我们外部参数的一个拷贝,那么我们直接使用老方法,将我们s产生的新空间给我们的当前对象,我们的旧空间就给我们的s出作用域的时候销毁即可。
对于迭代器部分,首先我们要想到该使用方式:
- string s1("hello world");
- string::iterator it = s1.begin();
- while (it != s1.end())
- {
- cout << *it << " ";
- it++;
- }
通过该定义的方式我们可以看出,我们的迭代器也是一个类型,一个被定义在string类中的类型,而对于该使用方式来看我们的迭代器很类似于我们的指针,而实际上我们的迭代器就是我们的指针,或者是对我们指针进行封装的类,那么该打印结果是:
![]()
那么很明显,我们的是从头到尾的一个遍历的过程,而我们的begin()函数返回的就是我们的数组首元素的地址,我们的end()函数返回的就是我们数组末尾的下一个位置的地址。此外我们的迭代器(这里仅仅给大家介绍我们的正向迭代器,对于反向迭代器,小编会在之后的内容给大家介绍)在库中一共分为两个版本:

一个是我们的普通版本,一个是我们的const版本,那么这两者又有着什么不同呢?首先对于我们普通迭代器,我们即允许了读,也允许了写,而我们的const版本只允许读而不允许写,其次就是我们调用对象的不同,我们的iterator是给普通对象调用的,我们的const_iterator就需要我们用我们的const修饰我们的this指针,虽然按照语法来说该既可以被我们的普通对象调用(权限缩小),也可以被我们的const对象调用(权限平移),但是在iterator版本出现时我们的编译器在每次调用中会给我们最匹配的那个,因此也就达到了我们的普通对象调用我们的普通版本,const对象调用我们const版本。
那么该具体实现如下:
- typedef char* iterator;
- typedef const char* const_iterator;
- iterator begin()
- {
- return _str;
- }
- const_iterator begin() const
- {
- return _str;
- }
-
- iterator end()
- {
- return _str + _size;
- }
- const_iterator end() const
- {
- return _str + _size;
- }
那么之前给大家介绍了范围for,实际上我们的范围for就是我们以上正向迭代器的使用放式,只不过上层做了一层封装,因此只有有迭代器才可以使用我们的范围for
数据的容量控制,我们就需要实现以下几个函数:
首先是我们获取我们有效数据和容量大小的函数
-
- size_t size()const
- {
- return _size;
- }
- size_t capacity()const
- {
- return _capacity;
- }
其次是我们判断我们的有效数据是否为空
- bool empty()const//这里不仅仅要被我们的普通对象调用,也要被我们的const对象调用
- {
- return _size == 0;
- }
最后比较关键的两个就是我们有效数据控制和我们容量控制的函数:
容量控制函数:
- void reserve(size_t n)
- {
- if (n > _capacity)//只有空间大小大于当前才需要进行扩容
- {
- char* temp;
- temp = new char[n + 1];//多的一个用于存储'/0'
- memcpy(temp, _str,_size+1);
- delete[]_str;
- _str = temp;
- _capacity = n;
- }
- }
对于扩容逻辑我想大家并不陌生,这里就是我们开辟一个新的扩容后的空间,再将我们当前的内容拷贝到我们扩容后的空间,最后将我们的当前指针指向新开辟好的空间即可。

有效数据个数控制函数:
- void resize(size_t n, char c = '\0')
- {
- if (n < _size)
- {
- _str[n] = '\0';
- _size = n;
- }
- else
- {
- reserve(n);
- for (int i = _size; i < n; i++)
- {
- _str[i] = c;
- }
- _size = n;
- _str[_size] = '\0';
-
- }
- }
以上我们一共存在三种情况:
n<_size 直接删除数据:只需要我们将有效位置的位置的下一个置为‘/0’,然后改变我们的——_size即可
_size
这里只需要从原先的_size开始依次往后填写直到达到我们有效数据个数即可,最后需要改变我们的_size大小,然后最后一位存上我们的'/0'。 n>capacity 扩容+初始化:我们这里的操作只是比我们的情况二多了一个扩容操作,这里小编将情况三和情况二的判断放在了我们的reserve函数中,大家可以体会一下。
我们的push_back通常只是在后面添加字符,但是在添加字符的过程中我们需要注意到的就是我们在添加过后是否需要进行增容,代码如下:
- void push_back(char c)
- {
- if (_capacity = _size)
- {
- reserve(_capacity == 0 ? 4 : _capacity * 2);
- }
- _str[_size] = c;
- _size++;
- _str[_size] = '\0';
- }
这里我们扩容逻辑是,当我们的_capacity和我们的_size相等时就需要进行扩容,也就是说当我们的有效数据和我们的容量大小相等时就需要进行扩容,这里我们的扩容逻辑就是,当我们的容量为0的时候就只开四个空间,其余情况按照旧容量的两倍进行扩容,最后就是将我们的添加的字符放在我们的数组末,然后将我们的_size++,最后记得将有效数据的下一位赋值上我们的'/0'即可。
这里小编仅仅给大家实现了我们append添加字符串的那个版本,在实现的过程中我们任然需要注意的是我们的扩容操作,代码如下:
- void append(const char* str)
- {
- size_t len = strlen(str);
- if (_size + len > _capacity)//判断是否需要进行扩容
- {
- reserve(_size + len);//扩容到能够存储我们新增字符串大小
- }
- strcpy(_str + _size, str);//从_size位置开始将我们新增字符串复制到该后面
- _size = _size + len;//更新我们的_size
-
- }
我们的+=运算符,是我们string类,常用于添加我们的字符串或者字符的一个操作符,那么该如何同时能添加字符串和我们的操作符的呢?很简单,那就是我们的函数重载。
字符版本:
- string& operator+=(char c)//这里我们的*this并没有被销毁,所以可以使用引用返回
- {
- push_back(c);
- return *this;//注意我们的+=需要返回+=后的值
- }
这里我们发现实际上这里只是对我们push_back的一个复用,那么字符串版本呢?相信聪明的小伙伴已经猜到了,没错,这里就是对我们append的一个复用。
字符串版本:
- string& operator+=(const char* str)
- {
- append(str);
- return* this;
- }
clear函数的作用就是清除我们string对象中所有有效元素,这实际上是非常简单的一种操作,只需要更改我们的_size为,以及将我们数组的起始位置填上'/0'即可。
- void clear()
- {
- _str[0] = '\0';
- _size = 0;
- }
我们的insert,小编这里也给大家实现两个版本,一个是在pos位置前插入我们n个字符c,一个是在我们pos位置前插入字符串。
版本一:
- void insert(size_t pos,size_t n, char c)
- {
- assert(pos < _size);//判断pos位置的合理性
- if (_size + n > _capacity)//判断是否需要进行扩容操作
- {
- reserve(_size + n);
- }
- size_t end = _size;//end指向我们的数组末尾
- while (end >= pos && end != npos)//当我们的end大于我们的pos,且我们end值合理时
- {
- _str[end + n] = _str[end];//往后移动数据
- --end;//pos位置以及该后的得全部要往后移n位
- }
-
- for (size_t i = 0; i < n; i++)//从pos位置写入我们n个c
- {
- _str[pos + i] = c;
- }
-
- _size += n;//修改我们的_size的值
- }

版本二:
- void insert(size_t pos, const char* str)
- {
- assert(pos < _size);//判断pos位置的合法性
- size_t len = strlen(str);//获取字符串长度,方便后续操作
- if (_size + len> _capacity)//判断是否需要扩容
- {
- reserve(_size + len);
- }
- size_t end = _size;
- while (end >= pos && end != npos)//移动元素
- {
- _str[end + len] = _str[end];
- --end;
- }
-
- for (size_t i = 0; i < len; i++)//写入元素
- {
- _str[pos + i] = str[i];
- }
-
- _size += len;
- }
这里我们的版本二实际上和我们版本一的思路是一样的,只不过该插入字符串时需要判断字符串长度,才能进行元素的移动和元素的写入。
删除pos位置开始的len长度的字符
这里我们的删除我们是需要分情况讨论的

2.诺pos+len 代码如下: 该函数的作用是截取从pos位置开始的len长度的字符串,注意该函数的返回值是一个我们的string对象,该函数也有两类情况: 情况一:len==npos或者len+pos>size,这里就需要将我们pos后面的值全部截取,但是对于截取我们部分截取和全部截取的逻辑都是一致的,这里我们需要注意的是我们需要修正我们的len值,否则就会造成我们的越界截取。 情况二:pos+len 为什么要重载我们的[]呢?因为我们的string类是将我们的底层数组封装了,外界并不能直接访问,因此我们要提供我们的[]接口,给大家使用从而间接访问到我们的底层数组,但是需要注意的是我们[]涉及到数据的写入和读取,因此该要提供给我们的普通对象读取和写入的权力,给我们的const对象只提供读取的权力,因此这里也就要实现两个版本: 之前给大家说过,我们这个接口是为了和我们C语言进行配合,因此我们返回的就是我们C语言字符串类型,也就是我们的字符指针 对于我们的关系运算符,我们这里使用的是C语言的memcmp函数去比较我们的大小,由于我们的memcmp是按一个字节,一个字节去进行比较的,因此我们是按string中有效数据个数最短的那个对象去进行我们的比较操作,但是最短的字符比较肯会出现以下两类情况: 这里我就给大家简单的介绍一下我们的<运算符以及==运算符的重载逻辑,其他的都是对两者的复用 <预算符重载: ==运算符重载: 对于find函数我们这里给大家实现了两个版本,一个是查找单个字符,一个是查找字符串,对于查找字符串,我们可以使用我们C语言中学习过的strstr函数进行字串的查找 单个字符版本: 字符串版本: 对于这里的实现都比较简单,大家只需要注意找不到返回我们的npos即可。 在给大家介绍友元函数的时候,就给大家介绍过一次我们Date类的流插入运算符的重载,由于我们的流插入的调用参数的原因,我们不得不把我们的该函数写在类外,然后又由于我们要直接去访问我们的私有成员变量,又不得不去构造我们的友元关系。那么实际上我们也可以通过间接的函数去获得我们的内部成员,但是我们C++语言是不常使用的,但是对于我们string的<<操作符我们是否可以调用我们的C-str接口去实现我们这个接口呢? 答案是不行的,原因是我们的C-str返会的是我们C语言的字符串,因此遇到\0,会自动停止打印,但是我们的string类是以我们的size作为结束标志,因此这里是不可取的。 因此我们这里是通过构造友元关系实现的,具体实现代码如下: 在实现流提取我们需要注意一点就是,我们这里的流提取在遇到空格和我们的\n就会停止读取,因此我们要在此处加以我们的判断。 这里我给大家提供了一个版本,不过这个版本是一个错误版本,且就算成功该也会带来极大的资源损耗原因在于 那么对于以上问题我们各自采用的解决方案是什么呢? 此外我们的clear是对该对象中原先的值进行清理,以达到我们后输入的值对其进行覆盖的效果。
9.7 substr

9.8 []运算符的重载
10.c_str
11.关系运算符



12. find函数
13.<<流插入操作符重载
14.>>流提取
总代码: