STL——string类的模拟实现
前言
前面为大家介绍了string类的使用,本篇博客我们自己来实现一下string类,这里说明一下,我们只实现一些核心的接口,目的是让大家了解string的底层,加深对string类的理解和掌握,下面进入正文部分。
string类各函数接口总览
namespace cl
{//模拟实现string类class string{public:typedef char* iterator;typedef const char* const_iterator;//默认成员函数string(const char* str = ""); //构造函数string(const string& s); //拷贝构造函数string& operator=(const string& s); //赋值运算符重载函数~string(); //析构函数//迭代器相关函数iterator begin();iterator end();const_iterator begin()const;const_iterator end()const;//容量和大小相关函数size_t size();size_t capacity();void reserve(size_t n);void resize(size_t n, char ch = '\0');bool empty()const;//修改字符串相关函数void push_back(char ch);void append(const char* str);string& operator+=(char ch);string& operator+=(const char* str);string& insert(size_t pos, char ch);string& insert(size_t pos, const char* str);string& erase(size_t pos, size_t len);void clear();void swap(string& s);const char* c_str()const;//访问字符串相关函数char& operator[](size_t i);const char& operator[](size_t i)const;size_t find(char ch, size_t pos = 0)const;size_t find(const char* str, size_t pos = 0)const;size_t rfind(char ch, size_t pos = npos)const;size_t rfind(const char* str, size_t pos = 0)const;//关系运算符重载函数bool operator>(const string& s)const;bool operator>=(const string& s)const;bool operator<(const string& s)const;bool operator<=(const string& s)const;bool operator==(const string& s)const;bool operator!=(const string& s)const;private:char* _str; //存储字符串size_t _size; //记录字符串当前的有效长度size_t _capacity; //记录字符串当前的容量static const size_t npos; //静态成员变量(整型最大值)};const size_t string::npos = -1;//<<和>>运算符重载函数istream& operator>>(istream& in, string& s);ostream& operator<<(ostream& out, const string& s);istream& getline(istream& in, string& s);
}
1. 默认成员函数
1.1 构造函数
构造函数设置为缺省参数,若不传入参数,则默认构造为空字符串。字符串的初始大小和容量均设置为传入C字符串的长度(不包括’\0’)
//构造函数
string(const char* str = "")
{_size = strlen(str); //初始时,字符串大小设置为字符串长度_capacity = _size; //初始时,字符串容量设置为字符串长度_str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')strcpy(_str, str); //将C字符串拷贝到已开好的空间
}
这里大家在开空间的时候需要多开一个,因为capacity是不包括\0的,所以我们需要为\0多开一个空间。
1.2 拷贝构造函数
//拷贝构造
string(const string& s)
{_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;
}
在模拟实现拷贝构造函数前,我们应该首先了解深浅拷贝:
浅拷贝:拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间。其中一个对象的改动会对另一个对象造成影响。
深拷贝:深拷贝是指源对象与拷贝对象互相独立。其中任何一个对象的改动不会对另外一个对象造成影响。
我们如果不写,编译器生成的拷贝构造只能完成浅拷贝,在string类里浅拷贝显然无法完成我们的需求,所以我们需要自己实现拷贝构造函数完成深拷贝。
自己写拷贝构造也简单,先开辟一块足以容纳源对象字符串的空间,然后将源对象的字符串拷贝过去,接着把源对象的其他成员变量也赋值过去即可。
因为拷贝对象的_str与源对象的_str指向的并不是同一块空间,所以拷贝出来的对象与源对象是互相独立的。
1.3 赋值运算符重载函数
//传统写法
string& operator=(const string& s)
{if (this != &s) //防止自己给自己赋值{delete[] _str; //将原来_str指向的空间释放_str = new char[s._capacity + 1]; //重新申请一块空间strcpy(_str, s._str); //将s._str拷贝一份到_str_size = s._size; //_size赋值_capacity = s._capacity; //_capacity赋值}return *this; //返回左值(支持连续赋值)
}
赋值运算符重载函数与拷贝构造函数写法几乎相同,只是左值的_str在开辟新空间之前需要先将原来的空间释放掉,并且在进行操作之前还需判断是否是自己给自己赋值,若是自己给自己赋值,则无需进行任何操作。
1.4 析构函数
string类的析构函数需要我们进行编写,因为每个string对象中的成员_str都指向堆区的一块空间,当对象销毁时堆区对应的空间并不会自动销毁,为了避免内存泄漏,我们需要使用delete手动释放堆区的空间。
//析构函数
~string()
{delete[] _str; //释放_str指向的空间_str = nullptr; //及时置空,防止非法访问_size = 0; //大小置0_capacity = 0; //容量置0
}
这里插一句,我们前面介绍类和对象的内容时说过,只要类中需要实现析构,那么就需要我们自己实现拷贝构造,这是一个小技巧。
2. 迭代器相关函数
2.1 begin
string类中的迭代器实际上就是字符指针,只是给字符指针起了一个别名叫iterator而已。
begin函数的作用就是返回字符串中第一个字符的地址:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{return _str; //返回字符串中第一个字符的地址
}
const_iterator begin()const
{return _str; //返回字符串中第一个字符的const地址
}
2.2 end
这里与上面同理,end函数的作用就是返回字符串中最后一个字符的后一个字符的地址(即’\0’的地址):
iterator end()
{return _str + _size; //返回字符串中最后一个字符的后一个字符的地址
}
const_iterator end()const
{return _str + _size; //返回字符串中最后一个字符的后一个字符的const地址
}
3. 容器和大小相关函数
3.1 size和capacity
因为string类的成员变量是私有的,我们并不能直接对其进行访问,所以string类设置了size和capacity这两个成员函数,用于获取string对象的大小和容量。
size函数用于获取字符串当前的有效长度(不包括’\0’)。
//大小
size_t size()const
{return _size; //返回字符串当前的有效长度
}
capacity函数用于获取字符串当前的容量。
//容量
size_t capacity()const
{return _capacity; //返回字符串当前的容量
}
3.2 reserve和resize
reserve规则:
1、当n大于对象当前的capacity时,将capacity扩大到n或大于n。
2、当n小于对象当前的capacity时,什么也不做。
//改变容量,大小不变
void reserve(size_t n)
{if (n > _capacity) //当n大于对象当前容量时才需执行操作{char* tmp = new char[n + 1]; //多开一个空间用于存放'\0'strncpy(tmp, _str, _size + 1); //将对象原本的C字符串拷贝过来(包括'\0')delete[] _str; //释放对象原本的空间_str = tmp; //将新开辟的空间交给_str_capacity = n; //容量跟着改变}
}
注意:代码中使用strncpy进行拷贝对象C字符串而不是strcpy,是为了防止对象的C字符串中含有有效字符’\0’而无法拷贝(strcpy拷贝到第一个’\0’就结束拷贝了)。
resize规则:
1、当n大于当前的size时,将size扩大到n,扩大的字符为ch,若ch未给出,则默认为’\0’。
2、当n小于当前的size时,将size缩小到n。
//改变大小
void resize(size_t n, char ch = '\0')
{if (n <= _size) //n小于当前size{_size = n; //将size调整为n_str[_size] = '\0'; //在size个字符后放上'\0'}else //n大于当前的size{if (n > _capacity) //判断是否需要扩容{reserve(n); //扩容}for (size_t i = _size; i < n; i++) //将size扩大到n,扩大的字符为ch{_str[i] = ch;}_size = n; //size更新_str[_size] = '\0'; //字符串后面放上'\0'}
}
4, 修改字符串相关函数
4.1 push_back
push_back函数的作用就是在当前字符串的后面尾插上一个字符,尾插之前首先需要判断是否需要增容,若需要,则调用reserve函数进行增容,然后再尾插字符,注意尾插完字符后需要在该字符的后方设置上’\0’,否则打印字符串的时候会出现非法访问,因为尾插的字符后方不一定就是’\0’。
//尾插字符
void push_back(char ch)
{if (_size == _capacity) //判断是否需要增容{reserve(_capacity == 0 ? 4 : _capacity * 2); //将容量扩大为原来的两倍}_str[_size] = ch; //将字符尾插到字符串_str[_size + 1] = '\0'; //字符串后面放上'\0'_size++; //字符串的大小加一
}
4.2 append
append函数的作用是在当前字符串的后面尾插一个字符串,尾插前需要判断当前字符串的空间能否容纳下尾插后的字符串,若不能,则需要先进行增容,然后再将待尾插的字符串尾插到对象的后方,因为待尾插的字符串后方自身带有’\0’,所以我们无需再在后方设置’\0’。
//尾插字符串
void append(const char* str)
{size_t len = _size + strlen(str); //尾插str后字符串的大小(不包括'\0')if (len > _capacity) //判断是否需要增容{reserve(len); //增容}strcpy(_str + _size, str); //将str尾插到字符串后面_size = len; //字符串大小改变
}
4.3 qperator+=
//+=运算符重载
string& operator+=(char ch)
{push_back(ch); //尾插字符串return *this; //返回左值(支持连续+=)
}
//+=运算符重载
string& operator+=(const char* str)
{append(str); //尾插字符串return *this; //返回左值(支持连续+=)
}
这里直接进行复用即可。
4.4 insert
这里模拟实现两种插入:
在pos位置之前插入字符
void insert(size_t pos, char ch)
{assert(pos <= _size);size_t end = _size+1;while (end > pos){_str[end] = _str[end-1];--end;}_str[pos] = ch;++_size;
}
在pos位置之前插入字符串
void insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len > _capacity ? _size + len : 2 * _capacity);}size_t end = _size+len;while (end > pos+len-1){_str[end] = _str[end-len];--end;}for (size_t i = 0; i < len; i++){_str[pos + i] = str[i];}_size += len;
}
这里大家可以类比来看,其实两个是差不多的。
4.5 erase
void erase(size_t pos, size_t len)
{assert(pos < _size);if (len >= _size - pos){_str[pos] = '\0';_size = pos;}else{for (size_t i = pos + len; i <= _size; i++){_str[i - len] = _str[i];}_size -= len;}
}
这里删除分两种情况:
pos位置及其之后的有效字符都需要被删除。
pos位置及其之后的有效字符只需删除一部分。
4.6 clear
clear函数用于将对象中存储的字符串置空,实现时直接将对象的_size置空,然后在字符串后面放上’\0’即可。
//清空字符串
void clear()
{_size = 0; //size置空_str[_size] = '\0'; //字符串后面放上'\0'
}
4.7 c_str
c_str函数用于获取对象C类型的字符串,实现时直接返回对象的成员变量_str即可。
//返回C类型的字符串
const char* c_str()const
{return _str;
}
5. 访问字符串函数
5.1 operator[ ]
//[]运算符重载(可读可写)
char& operator[](size_t i)
{assert(i < _size); //检测下标的合法性return _str[i]; //返回对应字符
}
5.2 find
//正向查找第一个匹配的字符
size_t find(char ch, size_t pos = 0)
{assert(pos < _size); //检测下标的合法性for (size_t i = pos; i < _size; i++) //从pos位置开始向后寻找目标字符{if (_str[i] == ch){return i; //找到目标字符,返回其下标}}return npos; //没有找到目标字符,返回npos
}
6. 关系运算符重载函数
我们只需重载其中的两个,剩下的四个关系运算符可以通过复用已经重载好了的两个关系运算符来实现。
//>运算符重载
bool operator>(const string& s)const
{return strcmp(_str, s._str) > 0;
}
//==运算符重载
bool operator==(const string& s)const
{return strcmp(_str, s._str) == 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 !(*this == s);
}
7. >>和<<运算符的重载函数
7.1 >>运算符重载
重载>>运算符是为了让string对象能够像内置类型一样使用>>运算符直接输入。
输入前我们需要先将对象的C字符串置空,然后从标准输入流读取字符,直到读取到’ ‘或是’\n’便停止读取。
//>>运算符的重载
istream& operator>>(istream& in, string& s)
{s.clear(); //清空字符串char ch = in.get(); //读取一个字符while (ch != ' '&&ch != '\n') //当读取到的字符不是空格或'\n'的时候继续读取{s += ch; //将读取到的字符尾插到字符串后面ch = in.get(); //继续读取字符}return in; //支持连续输入
}
7.2 <<运算符的重载
重载<<运算符是为了让string对象能够像内置类型一样使用<<运算符直接输出打印。实现时我们可以直接使用范围for对对象进行遍历即可。
//<<运算符的重载
ostream& operator<<(ostream& out, const string& s)
{//使用范围for遍历字符串并输出for (auto e : s){cout << e;}return out; //支持连续输出
}
8. 总结
本篇博客为大家介绍了string类的模拟实现,我们只实现一部分接口,大家掌握这些就OK了,没必要每个都实现一下,毕竟我们不可能比库里实现得还好,我们去模拟实现是为了更好地理解和掌握string类,最后,希望本篇博客可以为大家带来帮助,感谢阅读!