目录
前言
1.命名空间的使用
2.string的成员变量
3.构造函数
4.析构函数
5.拷贝构造
5.1 swap交换函数的实现
6.赋值运算符重载
7.迭代器部分
8.数据容量控制
8.1 size和capacity
8.2 empty
9.数据修改部分
9.1 push_back
9.2 append添加字符串
9.3 +=运算符重载
9.4 clear函数
9.5 insert
9.6 erase
9.7 substr
9.8 []运算符的重载
10.c_str
11.关系运算符
12. find函数
13.<<流插入操作符重载
14.>>流提取
总代码:
前言
这里我们就开始介绍我们string的模拟实现了,我相信在经过之前给大家介绍的标准库string类的使用后,大家对我们的string类都已经有一定的认识,心里对该底层实现也有了一定的猜想,那么现在我们就为大家打消疑虑,给大家揭开我们string神秘的面纱(注意:小编这里只给大家是实现了一部分我们经常使用的函数)。
1.命名空间的使用
首先我们需要使用我们命名空间来避免和我们的库中的string导致冲突
namespace xhj{class string{};};
2.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;
};
3.构造函数
这里我们只需要实现我们比较重要的两个即可,也就是我们的无参构造,和我们的C-string进行的构造,但是这里我们可以使用我们的缺省参数将两个合并为一个,代码如下:
string(const char* str = ""){_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];//由于多一位需要存储我们的‘/0’,因此要进行+1strcpy(_str, str);}
4.析构函数
对于析构函数我们是需要自己实现的,因为这里都是内置类型,且我们这些内置类型中我们还开辟了新的空间如果我们不自己实现,很大程度会造成内存的泄露。
~string()
{delete[] _str;_str = nullptr;_size = _capacity = 0;}
5.拷贝构造
在实现拷贝构造前我们需要确认我们是否需要写我们的拷贝构造,很明显我们这里是非常有必要的,因为这里会涉及到浅拷贝的问题:
因此以下我们需要实现我们的深拷贝。
5.1 swap交换函数的实现
为什么我们在介绍拷贝构造函数之前,要先给大家介绍我们的交换函数呢?这里就涉及了我们拷贝构造函数的两种写法。
实现我们的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出局部作用域进行销毁,也就是:
6.赋值运算符重载
对于赋值运算符我们也是需要进行重载的,这里也牵涉到我们的浅拷贝带来的问题,因此我们这里也是需要重新开辟一段空间进行我们数据的存储的,那么这里我们也有我们的两种写法:
注意: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出作用域的时候销毁即可。
7.迭代器部分
对于迭代器部分,首先我们要想到该使用方式:
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
8.数据容量控制
数据的容量控制,我们就需要实现以下几个函数:
8.1 size和capacity
首先是我们获取我们有效数据和容量大小的函数
size_t size()const{return _size;}size_t capacity()const{return _capacity;}
8.2 empty
其次是我们判断我们的有效数据是否为空
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<n<capacity 只需要将剩余的空间初始化:这里只需要从原先的_size开始依次往后填写直到达到我们有效数据个数即可,最后需要改变我们的_size大小,然后最后一位存上我们的'/0'。
n>capacity 扩容+初始化:我们这里的操作只是比我们的情况二多了一个扩容操作,这里小编将情况三和情况二的判断放在了我们的reserve函数中,大家可以体会一下。
9.数据修改部分
9.1 push_back
我们的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'即可。
9.2 append添加字符串
这里小编仅仅给大家实现了我们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}
9.3 +=运算符重载
我们的+=运算符,是我们string类,常用于添加我们的字符串或者字符的一个操作符,那么该如何同时能添加字符串和我们的操作符的呢?很简单,那就是我们的函数重载。
字符版本:
string& operator+=(char c)//这里我们的*this并没有被销毁,所以可以使用引用返回{push_back(c);return *this;//注意我们的+=需要返回+=后的值}
这里我们发现实际上这里只是对我们push_back的一个复用,那么字符串版本呢?相信聪明的小伙伴已经猜到了,没错,这里就是对我们append的一个复用。
字符串版本:
string& operator+=(const char* str){append(str);return* this;}
9.4 clear函数
clear函数的作用就是清除我们string对象中所有有效元素,这实际上是非常简单的一种操作,只需要更改我们的_size为,以及将我们数组的起始位置填上'/0'即可。
void clear(){_str[0] = '\0';_size = 0;}
9.5 insert
我们的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;}
这里我们的版本二实际上和我们版本一的思路是一样的,只不过该插入字符串时需要判断字符串长度,才能进行元素的移动和元素的写入。
9.6 erase
删除pos位置开始的len长度的字符
这里我们的删除我们是需要分情况讨论的
- 当我们的pos+len>=size或者我们的len=npos,那么说明我们pos位置开始的值是要全部删除的,也就是。
2.诺pos+len<size,那么我们只需要删除我们pos位置的len长度的字符即可,这里我的 思路是将pos+len位置后的值按次序移到我们pos位置以及该后面位置进行覆盖直到我们的/0(_size的位置就是我们/0存储位置)也被移过来之后就完成了我们的删除。
代码如下:
void erase(size_t pos, size_t len=npos){assert(pos < _size);//判断pos位置的合法性if (len == npos || pos + len > _size){//全部删除_str[pos] = '\0';_size = pos;}else{//部分删除size_t end = pos + len;while (end <= _size)//注意我们是<=,因为此处需要将/0也移过来{_str[pos++] = _str[end++];}_size = _size - len;}}
9.7 substr
该函数的作用是截取从pos位置开始的len长度的字符串,注意该函数的返回值是一个我们的string对象,该函数也有两类情况:
情况一:len==npos或者len+pos>size,这里就需要将我们pos后面的值全部截取,但是对于截取我们部分截取和全部截取的逻辑都是一致的,这里我们需要注意的是我们需要修正我们的len值,否则就会造成我们的越界截取。
情况二:pos+len<size,这里我们只需要做到部分截取即可,这里的逻辑是首先构造一个string对象,将其空间开辟好,然后将pos位置极其以后的值全部写入到该对象即可
string substr(size_t pos = 0, size_t len = npos){assert(pos < _size);size_t n = len;if (len == npos || pos + len > _size){//修正lenn = _size - pos;}//截取逻辑string tmp;tmp.reserve(n);for (size_t i = pos; i < pos + n; i++){tmp += _str[i];//复用}return tmp;}
9.8 []运算符的重载
为什么要重载我们的[]呢?因为我们的string类是将我们的底层数组封装了,外界并不能直接访问,因此我们要提供我们的[]接口,给大家使用从而间接访问到我们的底层数组,但是需要注意的是我们[]涉及到数据的写入和读取,因此该要提供给我们的普通对象读取和写入的权力,给我们的const对象只提供读取的权力,因此这里也就要实现两个版本:
char& operator[](size_t index){assert(index < _size&&index>=0);return _str[index];}const char& operator[](size_t index)const{assert(index < _size&& index >= 0);return _str[index];}
10.c_str
之前给大家说过,我们这个接口是为了和我们C语言进行配合,因此我们返回的就是我们C语言字符串类型,也就是我们的字符指针
const char* c_str()const{return _str;}
11.关系运算符
bool operator<(const string& s) const{int ret = memcmp(_str, s._str, _size > s._size ? s._size : _size);return ret == 0 ? _size < s._size : ret < 0;}bool operator<=(const string& s) const{return(*this < s || *this == s);}bool operator>(const string& s) const{return !(*this <= s);}bool operator>=(const string& s) const{return !(*this < s);}bool operator==(const string& s) const{return _size == s._size && memcmp(_str, s._str, _size)==0;}bool operator!=(const string& s)const{return !(*this == s);}
对于我们的关系运算符,我们这里使用的是C语言的memcmp函数去比较我们的大小,由于我们的memcmp是按一个字节,一个字节去进行比较的,因此我们是按string中有效数据个数最短的那个对象去进行我们的比较操作,但是最短的字符比较肯会出现以下两类情况:
这里我就给大家简单的介绍一下我们的<运算符以及==运算符的重载逻辑,其他的都是对两者的复用
<预算符重载:
==运算符重载:
12. find函数
对于find函数我们这里给大家实现了两个版本,一个是查找单个字符,一个是查找字符串,对于查找字符串,我们可以使用我们C语言中学习过的strstr函数进行字串的查找
单个字符版本:
size_t find(char c, size_t pos = 0) const{assert(pos < _size);for (size_t i = pos; i < _size; i++){if (_str[i] == c){return i;}}return npos;}
字符串版本:
size_t find(const char* s, size_t pos = 0) const{assert(pos < _size);const char* ptr = strstr(_str + pos, s);if (ptr){return ptr - _str;}else{return npos;}}
对于这里的实现都比较简单,大家只需要注意找不到返回我们的npos即可。
13.<<流插入操作符重载
在给大家介绍友元函数的时候,就给大家介绍过一次我们Date类的流插入运算符的重载,由于我们的流插入的调用参数的原因,我们不得不把我们的该函数写在类外,然后又由于我们要直接去访问我们的私有成员变量,又不得不去构造我们的友元关系。那么实际上我们也可以通过间接的函数去获得我们的内部成员,但是我们C++语言是不常使用的,但是对于我们string的<<操作符我们是否可以调用我们的C-str接口去实现我们这个接口呢?
答案是不行的,原因是我们的C-str返会的是我们C语言的字符串,因此遇到\0,会自动停止打印,但是我们的string类是以我们的size作为结束标志,因此这里是不可取的。
因此我们这里是通过构造友元关系实现的,具体实现代码如下:
ostream& operator<<(ostream& out, const string& s){for (auto ch : s){out << ch;}return out;}
14.>>流提取
在实现流提取我们需要注意一点就是,我们这里的流提取在遇到空格和我们的\n就会停止读取,因此我们要在此处加以我们的判断。
istream& operator>>(istream& _cin, xhj::string& s){char ch;_cin >> ch;while (ch != ' ' && ch != '\n'){s += buff;_cin >> ch;}return _cin;}
这里我给大家提供了一个版本,不过这个版本是一个错误版本,且就算成功该也会带来极大的资源损耗原因在于
- _cin在输入数据到缓冲区的时候,我们的空格和\n,并不能被存储在我们的缓冲区,因为这里被认为是我们多个值的间隔,会造成死循环
- 这里就算不会造成死循环,每次读取一个值就写入我们的对象中,就会导致我们空间的扩容过于频繁,导致资源损耗。
那么对于以上问题我们各自采用的解决方案是什么呢?
- 首先是解决我们空格和我们\n的读取问题,这里就需要我们使用我们的get函数,这个函数就会对其进行读取
- 然后扩容过于频繁,我们这里的解决方案就是使用一个数组进行写入,当这个数组被写满之后,直接一次性写入到我们的对象中,接下来请看代码
istream& operator>>(istream& _cin, xhj::string& s){char ch = _cin.get();s.clear();//为了每次达到输入后覆盖的效果char buff[128];//这里使用数组首先是避免了每次+=造成的多次开辟空间而导致效率上的降低//其次避免了我们预先在堆上开辟大量空间而可能造成空间浪费的情况//清理缓冲区while (ch == ' ' || ch == '\n'){ch = _cin.get();}int i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){ buff[i] = '\0';s += buff;i = 0;}ch = _cin.get();}if (i != 0){buff[i] = '\0';s += buff;}return _cin;}
此外我们的clear是对该对象中原先的值进行清理,以达到我们后输入的值对其进行覆盖的效果。
总代码:
#include<iostream>
#include<assert.h>
using namespace std;
namespace xhj{class string{friend ostream& operator<<(ostream& _cout, const xhj::string& s);friend istream& operator>>(istream& _cin, xhj::string& s);public:typedef char* iterator;typedef const char* const_iterator;public:string(const char* str = ""){_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];//由于多一位需要存储我们的‘/0’,因此要进行+1strcpy(_str, str);}//对于传统写法,我们的现代写法依赖于编译器对数据的初始化,如果编译没有对数据进行初始化操作那么在交换的过程中很可能会出现随机值的情况//然后在最后对tmp进行析构的时候就会出现程序崩溃的情况,所以这里我们需要先走初始化列表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算终止长度//现代写法string tmp(s._str);swap(tmp);}/*string& operator=(const string& s){if (this != &s){//传统写法char* temp = new char[s._capacity + 1];memcpy(_str, s._str, s._size + 1);delete[]_str;_str = temp;_size = s._size;_capacity = s._capacity//现代写法:拷贝构造一个新的对象,让两者进行交换,可以将新的值搞到我们对应的对象,然后就空间可以让我们的局部对象出了作用域出了析构对象进行销毁string temp(s);std::swap(_str, temp._str);std::swap(_size, temp._size);std::swap(_capacity, temp._capacity);}return *this;}*/string& operator=(string s)//传值传参调用拷贝构造{swap(s);return *this;}~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}// iteratoriterator begin(){return _str;}const_iterator begin() const{return _str;}iterator end(){return _str + _size;}const_iterator end() const{return _str + _size;}// modifyvoid push_back(char c){if (_capacity = _size){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = c;_size++;_str[_size] = '\0';}string& operator+=(char c){push_back(c);return *this;}void append(const char* str){size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}strcpy(_str + _size, str);_size = _size + len;}string& operator+=(const char* str){append(str);return* this;}void clear(){_str[0] = '\0';_size = 0;}void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}const char* c_str()const{return _str;}/// capacitysize_t size()const{return _size;}size_t capacity()const{return _capacity;}bool empty()const{return _size == 0;}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';}}void reserve(size_t n){if (n > _capacity){char* temp;temp = new char[n + 1];memcpy(temp, _str,_size+1);delete[]_str;_str = temp;_capacity = n;}}char& operator[](size_t index){assert(index < _size&&index>=0);return _str[index];}const char& operator[](size_t index)const{assert(index < _size&& index >= 0);return _str[index];}///relational operatorsbool operator<(const string& s) const{int ret = memcmp(_str, s._str, _size > s._size ? s._size : _size);return ret == 0 ? _size < s._size : ret < 0;}bool operator<=(const string& s) const{return(*this < s || *this == s);}bool operator>(const string& s) const{return !(*this <= s);}bool operator>=(const string& s) const{return !(*this < s);}bool operator==(const string& s) const{return _size == s._size && memcmp(_str, s._str, _size)==0;}bool operator!=(const string& s)const{return !(*this == s);}// 返回c在string中第一次出现的位置size_t find(char c, size_t pos = 0) const{assert(pos < _size);for (size_t i = pos; i < _size; i++){if (_str[i] == c){return i;}}return npos;}// 返回子串s在string中第一次出现的位置size_t find(const char* s, size_t pos = 0) const{assert(pos < _size);const char* ptr = strstr(_str + pos, s);if (ptr){return ptr - _str;}else{return npos;}}// 在pos位置上插入字符c/字符串str,并返回该字符的位置void insert(size_t pos,size_t n, char c){assert(pos < _size);if (_size + n > _capacity){reserve(_size + n);}size_t end = _size;while (end >= pos && end != npos){_str[end + n] = _str[end];--end;}for (size_t i = 0; i < n; i++){_str[pos + i] = c;}_size += n;}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位置上的元素,并返回该元素的下一个位置void erase(size_t pos, size_t len=npos){assert(pos < _size);//判断pos位置的合法性if (len == npos || pos + len > _size){_str[pos] = '\0';_size = pos;}else{size_t end = pos + len;while (end <= _size){_str[pos++] = _str[end++];}_size = _size - len;}}string substr(size_t pos = 0, size_t len = npos){assert(pos < _size);size_t n = len;if (len == npos || pos + len > _size){n = _size - pos;}string tmp;tmp.reserve(n);for (size_t i = pos; i < pos + n; i++){tmp += _str[i];}return tmp;}private:char* _str;size_t _capacity;size_t _size;const static size_t npos;};const size_t string::npos = -1;ostream& operator<<(ostream& out, const string& s){for (auto ch : s){out << ch;}return out;}istream& operator>>(istream& _cin, xhj::string& s){char ch = _cin.get();s.clear();//为了每次达到输入后覆盖的效果char buff[128];//这里使用数组首先是避免了每次+=造成的多次开辟空间而导致效率上的降低//其次避免了我们预先在堆上开辟大量空间而可能造成空间浪费的情况//清理缓冲区while (ch == ' ' || ch == '\n'){ch = _cin.get();}int i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){ buff[i] = '\0';s += buff;i = 0;}ch = _cin.get();}if (i != 0){buff[i] = '\0';s += buff;}return _cin;}};