目录
前言:
1.成员变量
2.构造函数与拷贝构造函数
3.析构函数
4.赋值重载
5.[]重载
6.比较关系重载
7.reserve
8.resize
9.push_back,append和重载+=
10.insert
11.erase
12.find
14.迭代器
15.流插入,流提取重载
16.swap
17.c_str
18.完整代码+测试
总结:
前言:
本篇模拟string类的底层实现,只会调一些重要的接口实现,结尾附上完整代码。
1.成员变量
private: //const char* _str //不使用const修饰,string类的字符串是可以修改的,而且例如扩容的时候也需要修改指针的指向 // 然后就是_str的初始化是最好new出来的,如果直接初始化为空指针,例如在流插入的时候需要解引用,就不行了。 char* _str; size_t _capacity; size_t _size; //static const size_t npos=-1;//静态成员变量要在类外声明初始化,不能给缺省值,但是如果加上const,又支持这样给缺省值的语法了 static const size_t npos;
首先注意的就是_str,不能是const类型的,因为我们的string字符串是可以改的,而且如果是const类型的,在扩容等也需要改变指针的指向;而且我们在流插入的时候解引用,所以在初始化的时候最好是new出来的。
静态成员常量npos需要在类外声明初始化,注意的是,本来静态成员变量是不能给缺省值的,但是加上了const就又可以给缺省值了。
2.构造函数与拷贝构造函数
string(const char* str="")//strlen遇到字符0就停了,这样传参数也正好,无参的size也是0,或者写成"\0",但是不能写成'\0'类型不匹配 :_size(strlen(str))//strlen内部获取字符串长度是需要解引用的,所以最好初始化不要给空指针, { _capacity = _size == 0 ? 3 : _size; _str = new char[_capacity + 1];//+1给字符0准备,实际capacity没有字符0 strcpy(_str, str); } string(const string& st) :_size(st._size) ,_capacity(st._capacity) { _str = new char[st._capacity + 1];//+1给字符0准备,实际capacity没有字符0 strcpy(_str, st._str); }
构造的参数这样写是因为这样传参数相当于无参的构造只有一个字符0,正好符合strlen计算长度的规则,无参的时候_size就是0了。
_capacity需要判断是否为0,不然什么都没有直接二倍扩容会出错。
_str的容量加1是给字符0准备的,但是实际_size与_capacity是没有计算字符0的,而且在调试的时候也看不到,但是还是要加上。
构造的思路就是根据传参字符串的的容量开一块新的空间,进行拷贝完成初始化。
拷贝构造就是深拷贝了,所以就是根据要拷贝的对象的大小开一块空间,再让空间拷贝给给调用的对象,注意容量与大小也要变。
3.析构函数
~string() { delete[] _str; _str = nullptr; _size = 0; _capacity = 0; }
注意配套使用delete就行。
4.赋值重载
string& operator=(const string& st) { //这样写如果new出错了,原来的对象也被破坏了 //delete[] _str; //_str = new char[st._capacity + 1]; //strcpy(_str, st._str); //_capacity = st._capacity; //_size = st._size; if (this != &st) { char* tmp = new char[st._capacity + 1]; strcpy(tmp,st._str); delete[] _str; _str = tmp; _capacity = st._capacity; _size = st._size; return *this; } }
赋值重载就要考虑空间的问题了,同时也是一个深拷贝。
假设s1=s2,如果s1容量多,直接将s2赋值过去没问题,但是会造成空间的浪费。
如果s1的空间少,此时再将s2赋值就会出现问题了,所以综合一下:先让原空间也就是被赋值的空间给释放掉,然后再根据赋值的内容开辟空间,让被赋值的指向这块新的空间,再进行拷贝,并完成大小和容量的更新。
上面的为注释的写法,但是还有一点问题,如果new失败了,那被赋值的空间早已被释放了,这就破坏了原来的空间,所以为了不破坏原有的空间,先根据赋值的内容开一块空间,再将赋值的内容拷贝给这块空间,此时再释放旧空间,再将旧空间指向新空间即可。
注意要判断自己不能给自己赋值。
5.[]重载
const char& operator[](size_t pos) const { assert(pos注意断言,下标位置不能等于_size,_size指向最后一个字符的下一个也就是字符0,但是实际库中的string的字符0我们是可以访问到的,这里主要表示有效数据的访问。
要提供两个版本,方便const对象调用。
6.比较关系重载
//string比较 bool operator>(const string& st) const { return strcmp(_str, st._str) > 0; } bool operator==(const string& st) const { return strcmp(_str, st._str) == 0; } bool operator>=(const string& st) const { return *this > st || *this == st;//如果不用const修饰==的重载,这里调用就不能st==*this了,因为st是const类型的 } bool operator= st); } bool operator st); } bool operator!=(const string& st) const { return !(*this == st); }这个没什么好说的了,就是写两个复用就完了。
注意没有修改成员变量的成员函数最好还是就是const,像>=复用==的逻辑的时候,如果==不是const的成员函数,就不能倒过来写了。
7.reserve
void reserve(size_t n) { if (n > _capacity)//如果n小于这块空间,相当于缩容了,原字符串再拷贝给缩容的空间就越界了 { char* tmp = new char[n + 1];//这里一样是放在原对象被破坏,+1用来存放字符0 strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = n;//但是capacity实际还是没有字符0的 }注意判断扩容的大小不能小于原有的容量,不然就是缩容了,缩容再拷贝数据就越界了。
8.resize
void resize(size_t n, char ch = '\0') { if (n _capacity) { reserve(n); } size_t i = _size;//直接在后面补初始化的数据 while (i如果扩容大小小于容量,那就保留前n个数据,末尾改成字符0。
如果大,那就扩容,原有数据不变,扩容后面的数据都默认初始化为字符0。如果传的有参数,就往末尾补数据,最后别忘了更新size,还要在最后补字符0。
9.push_back,append和重载+=
void push_back(char ch) { if (_size + 1 > _capacity) { reserve(_capacity * 2); } _str[_size] = ch; ++_size; _str[_size] = '\0';//插入完了,字符串后面的字符0就找不到了,所以size更新完后再补一个字符0 //复用insert //insert(_size,ch) } void append(const char* str) { size_t len = strlen(str); if (_size + len > _capacity) { reserve(_size + len); } strcpy(_str + _size, str);//正好覆盖了原字符串的字符0 _size += len; //复用insert //insert(_size,str) } string& operator+=(char ch) { push_back(ch); return *this; } string& operator+=(const char* str) { append(str); return *this; }注意尾插一个字符时的字符0的补充以及扩容即可。
10.insert
string& insert(size_t pos, char ch) { assert(pos _capacity) { reserve(_capacity * 2); } size_t end = _size + 1; while (end > pos) { _str[end] = _str[end - 1]; --end; } _str[pos] = ch; ++_size; return *this; } string& insert(size_t pos, const char* str) { assert(pos _capacity) { reserve(_size + len); } size_t end = _size + len; while (end >pos+len-1) { _str[end] = _str[end - len]; --end; } strncpy(_str + pos, str, len); _size += len; return *this; }这个建议画图理解,也一样先扩容。
注意断言,在等于size的时候插入就是尾插入,不用添加字符0,直接交换了,注意pos是下标。
11.erase
string& erase(size_t pos, size_t len = npos)//删除pos位置(包括pos位置)后的len个字符,如果不够删,就删完 { assert(pos = _size) { _str[pos] = '\0';//pos也被删了 _size = pos; } else { strcpy(_str + pos, _str + pos + len); _size -= len; } return *this; }当等于缺省值的时候或者要删的字符正好删到了最后一个有效数据,直接就是在pos的位置放字符0,更新size就行了(注意pos位置的值也被删了)。
如果没有删完,就将删除后右边的子串与原字符串连接起来,更新size。
12.find
size_t find(char ch, size_t pos=0)//pos为0就是代表找这个字符出现的第一个位置,返回下标;为1就是返回出现第二次的位置的下标 { assert(pos查找字符就是遍历一遍就行了,查找字符串需要使用strstr或者算法,也就是查找子串,strstr返回的是子串的首元素位置。
14.迭代器
typedef char* iterator; typedef const char* const_iterator; iterator begin() { return _str; } iterator end() { return _str + _size; } const_iterator begin() const { return _str; } const_iterator end() const { return _str + _size; }对指针的重命名,后面容器的实现更复杂,后面再说。
注意还有const迭代器,供const对象调用,这里分开划分const成员与非const成员函数主要是因为我们使用普通迭代器大部分会修改数据。
15.流插入,流提取重载
ostream& operator(const string& st) const { return strcmp(_str, st._str) > 0; } bool operator==(const string& st) const { return strcmp(_str, st._str) == 0; } bool operator>=(const string& st) const { return *this > st || *this == st;//如果不用const修饰==的重载,这里调用就不能st==*this了,因为st是const类型的 } bool operator= st); } bool operator st); } bool operator!=(const string& st) const { return !(*this == st); } void reserve(size_t n) { if (n > _capacity)//如果n小于这块空间,相当于缩容了,原字符串再拷贝给缩容的空间就越界了 { char* tmp = new char[n + 1];//这里一样是放在原对象被破坏,+1用来存放字符0 strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = n;//但是capacity实际还是没有字符0的 } } void push_back(char ch) { if (_size + 1 > _capacity) { reserve(_capacity * 2); } _str[_size] = ch; ++_size; _str[_size] = '\0';//插入完了,字符串后面的字符0就找不到了,所以size更新完后再补一个字符0 //复用insert //insert(_size,ch) } void append(const char* str) { size_t len = strlen(str); if (_size + len > _capacity) { reserve(_size + len); } strcpy(_str + _size, str);//正好覆盖了原字符串的字符0 _size += len; //复用insert //insert(_size,str) } string& operator+=(char ch) { push_back(ch); return *this; } string& operator+=(const char* str) { append(str); return *this; } void resize(size_t n, char ch = '\0') { if (n _capacity) { reserve(n); } size_t i = _size;//直接在后面补初始化的数据 while (i pos) { _str[end] = _str[end - 1]; --end; } _str[pos] = ch; ++_size; return *this; } string& insert(size_t pos, const char* str) { assert(pos _capacity) { reserve(_size + len); } size_t end = _size + len; while (end >pos+len-1) { _str[end] = _str[end - len]; --end; } strncpy(_str + pos, str, len); _size += len; return *this; } string& erase(size_t pos, size_t len = npos)//删除pos位置(包括pos位置)后的len个字符,如果不够删,就删完 { assert(pos = _size) { _str[pos] = '\0';//pos也被删了 _size = pos; } else { strcpy(_str + pos, _str + pos + len); _size -= len; } return *this; } void swap(string& st) { std::swap(_str, st._str); std::swap(_size, st._size); std::swap(_capacity, st._capacity); } size_t find(char ch, size_t pos=0)//pos为0就是代表找这个字符出现的第一个位置,返回下标;为1就是返回出现第二次的位置的下标 { assert(pos