我们这篇文章进行string的模拟实现。
为了防止标准库和我们自己写的string类发生命名冲突,我们将我们自己写的string类放在我们自己的命名空间中:
我们先来搭一个class string的框架:
namespace CYF{
public://各种成员函数
private:char _str;//存储字符串数组的指针size_t _size;//记录当前字符串的有效长度size_t _capacity;//记录当前字符串的容量static const size_t npos;//静态成员变量,很多地方的缺省值
}
默认成员函数
构造函数
string(const char* str=""){_size = strlen(str);//初始字符串有效长度_capacity = _size;//初始字符串容量设为字符串有效长度_str = new char[_capacity + 1];//为存储字符串开辟空间,+1是为了保存'\0'strcpy(_str, str);//将str字符串拷贝到开好的空间}
拷贝构造函数
关于拷贝构造函数,我们首先需要了解一个知识点:深浅拷贝
浅拷贝就是拷贝出来的对象和原先的对象指向的是同一块空间,这样的话,其中一个对象对这块空间做了改变,也会影响另外一个对象。
深拷贝就是拷贝出来的对象跟原来的对象,指着的是两块不同的空间,两者相同指的是不同空间中的内容是相同的。
下图是深浅拷贝区别的形象化表现:
而在这里,显然我们并不想两者之间相互影响,所以我们要用到的是深拷贝。
所以我们要先开辟块容纳原有对象字符串的空间,然后将字符串拷贝过去,再将其他成员变量赋值过去即可,这是传统写法:
string(const string& str)//拷贝构造函数的传统写法:_size(0),_capacity(0),_str(new char[_capacity + 1]){strcpy(_str, str._str);_size = str._size;_capacity = str._capacity;}
我们还有一种现代写法:
我们先根据原有字符串通过构造函数构造出一个tmp对象,然后再将tmp对象和拷贝对象的数据交换即可,这样的话,通过构造函数构造出来的tmp对象指向的空间和原对象的空间不同,并且交换之后,tmp是一个局部变量,出了作用域就会自动调用析构函数销毁,也就将tmp此时自身里拷贝对象原有的无用的数据全部删除了,一举两得:
string(const string& str)//拷贝构造函数的现代写法:_str(new char[str._capacity+1]),_size(0),_capacity(0){string tmp(str._str);//调用构造函数,构造出一个C字符串为str._str的对象swap(tmp);//交换这两个对象,我们在后面会介绍}
关于拷贝构造函数我们还需要注意一点就是:传参的时候一定要传引用,如果传值的话,会再次调用拷贝构造函数,进而导致无限循环的调用拷贝构造函数。
赋值运算符重载
与拷贝构造函数一样,赋值运算符重载也涉及深浅拷贝问题,我们同样需要深拷贝,下面还是介绍传统和现代两种写法:
传统写法
我们首先要防止自己给自己赋值,然后释放原空间,开辟新空间,而后操作跟拷贝构造函数一样,最后返回值返回左值*this,以保证连续赋值。
string& operator=(const string& str)//赋值运算符重载的传统写法{if (this != &str){delete[] _str;//释放原来的空间_str = new char[str._capacity + 1];//开辟新的空间strcpy(_str, str._str);//拷贝赋值_size = str._size;_capacity = str._capacity;}return *this;//返回左值(支持连续赋值)}
现代写法
也和拷贝构造函数的现代写法类似,只不过我们这里可以直接采取传值的方式传参,在传参的过程中拷贝构造出tmp对象,因为拷贝构造函数要防止无限调用拷贝构造函数的错误,所以必须采用引用传参,而这里我们只需要用传值传参即可:
string& operator=(string str)//赋值运算符重载的现代写法1
{swap(str);//交换两个对象return *this;//返回左值(支持连续赋值)
}
但是这样做的弊端就是无法防止为自己赋值,当我们使用上面的operator+给自己赋值的时候,虽然操作后,对象的_str指向的字符串的内容不变,但是字符串的地址发生了改变,我们想改变的话就用下面的写法:
string& operator=(const string& str)//赋值运算符重载的现代写法2{if (this != &str)//防止给自己赋值{string tmp(str);//用str拷贝构造出对象tmpswap(tmp);//交换两个对象}return *this;//返回左值(支持连续赋值)}
析构函数
由于string内的成员对象_str指向一块从堆区开辟的空间,当对象销毁时,堆区对应的空间并不会自动销毁,所以为了避免内存泄漏,我们需要手动delete释放:
~string(){delete[]_str;_str = nullptr;_size = 0;_capacity = 0;}
迭代器有关函数
string类的迭代器实际上就是字符指针,只是将char* typedef成iterator而已:
typedef char* iterator;
typedef const char* const_iterator;
begin && end
string中的begin和end函数实现的很简单:
string::iterator string::begin(){return _str;//返回字符串第一个字符的地址}string::const_iterator string::begin()const{return _str;//返回字符串第一个字符的const对象的地址}string::iterator string::end(){return _str + _size;//返回'\0'的地址}string::const_iterator string::end()const{return _str + _size;//返回'\0'的const对象的地址 }
所以我们在这就明白了,用迭代器遍历string对象的时候,实际上就是在用指针遍历字符数组而已:
string::iterator it = s.begin();
while (it != s.end())
{cout <<*it;it++;
}
cout << endl;
而且,实际上,范围for本质上也是通过迭代器来工作的,在代码编译的时候,编译器会自动将范围for替换成迭代器的形式,所以说要有迭代器的容器才会支持范围for,我们此时已经实现了我们自己的string的迭代器,所以我们可以实现范围for的使用:
for (auto& e : s){cout << e;}cout << endl;
与容量大小有关的函数
size && capacity
size()返回的是当前字符串的有效长度,capacity()返回的是字符串的容量:
size_t size(){return _size;}size_t capacity(){return _capacity;}
直接将_size和_capacity返回即可。
reserve && resize
我们首先要对这两个函数做一下区分
我们先看reserve函数:
- 当n大于对象当前capacity时,将capacity扩大到n或者大于n
- 当n小于对象当前capacity时,什么操作都不做
void reserve(size_t n)//若n>容量,才会扩容:则什么都不做{if (n > _capacity){char* tmp = new char[n + 1];//+1是为了放'\0'strncpy(tmp, _str, _size + 1);//为了防止对象中有有效的字符'\0',strcpy无法拷贝delete[]_str;_str = tmp;_capacity = n;}}
resize函数:
- 当n小于当前_size时,将_size缩小到n
- 当n大于当前_size时,将_size扩大到n,后面补的字符为c,c的缺省值为'\0'
void resize(size_t n, char c = '\0'){if (n <= _size)//n小于_size{_size = n;//_size调整为n_str[_size] = '\0';//在第_size个字符后加\0}else{if (n > _capacity)//先看看是否用扩容{reserve(n);}for (size_t i = _size; i < n; i++)//将原先有效字符后直到第n个字符全都赋值成c{_str[i] = c;}_size = n;_str[_size] = '\0';//字符串后面放上\0}}
empty
判断string对象是否为空,我们比较两个字符串的时候使用strcmp来实现,使用strcmp函数时若两个字符串大小相等返回0,两个字符串比较的时候不能使用==。
bool empty()//判断是否为空{return strcmp(_str, "") == 0;//两个字符串比较要用strcmp,不能直接用==}
修改字符串相关函数
push_back
push_back的作用就是尾插一个字母,我们需要先判断是否需要增容,然后再进行尾插,而且我们需要在该字符的后面设置'\0',否则打印字符串的时候就很可能会非法越界,因为尾插的字符后面不一定就是'\0'。
void push_back(char c){if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size] = c;_str[_size + 1] = '\0';_size++;}
append
append的作用就是尾插一个字符串,我们依旧是需要先判断是否需要扩容,而后尾插。这里我们不需要在最后设置'\0',因为尾插的字符串最后自带'\0'。
void append(const char* str){if (_capacity < _size + strlen(str))//若容量不够,则扩容{reserve(_size + strlen(str));}strcpy(_str + _size, str);_size += strlen(str);}
operator+=
operator+=的重载实现了字符串后面尾插字符和字符串的作用,我们可以直接调用上面实现的push_back和append函数:
string& operator+=(const string& str)//传string对象{append(str._str);return *this;}string& operator+=(const char* str)//传C类型字符串{append(str);return *this;}string& operator+=(char c)//传一个字符{push_back(c);return *this;}
insert
insert函数的目的是在任意位置插入字符或字符串,我们首先要判断pos的合法性,而后判断capacity是否能容纳插入字符或字符串后的内容,若不能则调用reserve函数进行扩容,而后进行插入:
//插入字符string& insert(size_t pos, char c){assert(pos <= _size);//检测pos是否合法if (_size == _capacity)//判断是否需要增容{reserve(_capacity == 0 ? 4 : 2 * _capacity);}size_t i = _size;while (i >= pos){_str[i + 1] = _str[i];i--;}_str[pos] = c;_size++;return *this;}//插入字符串string& insert(size_t pos, const char* str){assert(pos <= _size);//检测pos是否合法if (_size + strlen(str) > _capacity)//判断是否需要增容{reserve(_size + strlen(str));}char* end = _str + _size;while (end >= _str + pos){*(end + strlen(str)) = *end;end--;}strncpy(_str + pos, str, strlen(str));_size += strlen(str);return *this;}
我们要注意插入字符串的时候,要用strncpy,不能用strcpy,否则会将'\0'也拷贝进去。
erase
我们依然首先要判断pos是否合法,而后分两种情况进行操作。
我们这里只模拟实现下面这一种形式的erase函数:
string& erase (size_t pos = 0, size_t len = npos);
1.当pos位置及后面的有效字符都需要被删除时:
我们在pos位置上放置'\0'即可。
2.当pos位置及后面的有效字符只需要被删除一部分时:
我们将后面需要保留的有效字符覆盖前面需要删除的字符即可,此时也不用在字符串后面加'\0',因为字符串末尾有'\0'。
string& erase(size_t pos, size_t len = npos){assert(pos < _size);//判断pos是否合法size_t n = _size - pos;if (len >= n)//说明pos后面的字符全部删除{_size = pos;_str[_size] = '\0';//字符串后面放上'\0'}else//说明pos后面还有一部分字符保留着{strcpy(_str + pos, _str + pos + len);//用需要保留的字符覆盖掉需要删除的字符_size -= len;}return *this;}
clear
clear函数用于将字符串清空
void clear()//将对象中存储的字符串置空{_size = 0;_str[_size] = '\0';}
swap
这里的swap函数是我们用于交换两个对象的数据,我们直接调用库里的swap模板函数将对象的各个成员变量进行交换即可,但是这样的话我们就需要在swap函数前加上std::,告诉编译器这是在std中的swap函数,否则根据就近原则,编译器会以为是我们正在实现的swap函数。
void swap(string& str)//交换两个string对象{std::swap(_size, str._size);//使用库函数std::swap(_capacity, str._capacity);std::swap(_str, str._str);}
c_str
用于获取string对象中的C类型字符串
const char* c_str(){return _str;}
用于访问字符串的函数
operator[ ]
operator[ ]是为了让string对象能够通过下标的方式进行随机访问。
1.我们通过operator[ ]的方式可能会需要进行读取和修改操作
char& operator[](size_t i)//可读可写{assert(i < _size);//检测下标的合法性return _str[i];}
2.某些场景下,我们只需要通过operator[ ]的方式读取字符而不冷修改。例如我们对一个const的string类对象进行[ ]+下标操作时就只能读,不能写。
const char& operator[](size_t i)const//只读{assert(i < _size);//检查下标的合法性return _str[i];}
find
find函数用于正向查找一个字符或者字符串,返回找到的字符或者字符串下标
1、查找第一个字符
首先要判断pos的合法性,然后遍历的从前往后找目标字符,若找到返回下标,没找到,返回npos。
size_t find(char c, size_t pos = 0)//正向寻找{assert(pos < _size);for (size_t i = pos; i < _size; i++)//从pos位置开始向后找目标字符{if (_str[i] == c){return i;}}return npos;}
2.查找第一个字符串
首先还是判断pos的合法性,然后我们通过strstr函数进行查找。strstr函数若找到了会返回目标字符串的起始地点,否则返回一个空指针。
size_t find(const char* str, size_t pos = 0)//正向寻找{assert(pos < _size);const char* ret = strstr(_str + pos, str);//用strstr进行查找if (ret)//若找到子字符串,则返回子字符串的起始位置{return ret - _str;//返会字符串第一个字符的下标}else//找不到就返回nullptr{return npos;//返回npos}}
关系运算符重载>,<,<=,>=,==,!=
>,<,<=,>=,==,!=这六个关系运算符很好模拟,我们只写几个,剩下的复用其他的即可。
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->operator>(s) || this->operator==(s));}bool operator!=(const string& s)const{return !(*this == s);}
<<,>>运算符重载及getline函数
>>运算符重载
>>运算符重载是为了让我们能够使用>>直接进行输入。输入前我们需要先将对象中的C字符串置空,然后从标准输入流中读取字符,直到读到' '或'\n'停止。
std::istream& operator>>(std::istream& in, CYF::string& str){str.clear();//先清空字符串char ch = in.get();//读取一个字符while (ch != ' ' && ch != '\n')//若读取的字符不是空格或\n的话,尾插到str后面后继续读{str.push_back(ch);ch = in.get();}return in;//支持连续赋值}
<<运算符重载
这是为了我们能直接用<<运算符进行输出,我们直接进行遍历即可。
std::ostream& operator<<(std::ostream& out, CYF::string& str){for (size_t i = 0; i < str.size(); i++){out << str[i];}return out;}
getline
getline函数用于读取一行含有空格的字符串。直到读到'\n'的时候停下来,其余跟operator>>一样。
//getline跟>>基本相同,只不过是读取含有空格的字符串,知道读到\n的时候才停std::istream& getline(std::istream& in, CYF::string& str){str.clear();char ch = in.get();while (ch != '\n'){str.push_back(ch);ch = in.get();}return in;}
下面贴上完整代码:
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <string>
#include <cassert>
#include <iostream>namespace CYF
{class string{public://string类的迭代器实际上就是字符指针typedef char* iterator;typedef const char* const_iterator;string(const char* str=""){_size = strlen(str);//初始字符串有效长度_capacity = _size;//初始字符串容量设为字符串有效长度_str = new char[_capacity + 1];//为存储字符串开辟空间,+1是为了保存'\0'strcpy(_str, str);//将str字符串拷贝到开好的空间}string(const string& str)//拷贝构造函数的传统写法:_size(0),_capacity(0),_str(new char[_capacity + 1]){strcpy(_str, str._str);_size = str._size;_capacity = str._capacity;}//string(const string& str)//拷贝构造函数的现代写法// :_str(new char[str._capacity+1])// ,_size(0)// ,_capacity(0)//{// string tmp(str._str);//调用构造函数,构造出一个C字符串为str._str的对象// swap(tmp);//交换这两个对象//}~string(){delete[]_str;_str = nullptr;_size = 0;_capacity = 0;}string& operator=(const string& str)//赋值运算符重载的传统写法{if (this != &str){delete[] _str;//释放原来的空间_str = new char[str._capacity + 1];//开辟新的空间strcpy(_str, str._str);//拷贝赋值_size = str._size;_capacity = str._capacity;}return *this;//返回左值(支持连续赋值)}//string& operator=(string str)//赋值运算符重载的现代写法1//{// swap(str);//交换两个对象// return *this;//返回左值(支持连续赋值)//}//string& operator=(const string& str)//赋值运算符重载的现代写法2//{// if (this != &str)//防止给自己赋值// {// string tmp(str);//用str拷贝构造出对象tmp// swap(tmp);//交换两个对象// }// return *this;//返回左值(支持连续赋值)//}void swap(string& str)//交换两个string对象{std::swap(_size, str._size);//使用库函数std::swap(_capacity, str._capacity);std::swap(_str, str._str);}iterator begin();const_iterator begin()const;iterator end();const_iterator end()const;size_t size(){return _size;}size_t capacity(){return _capacity;}void reserve(size_t n)//若n>容量,才会扩容:则什么都不做{if (n > _capacity){char* tmp = new char[n + 1];//+1是为了放'\0'strncpy(tmp, _str, _size + 1);//为了防止对象中有有效的字符'\0',strcpy无法拷贝delete[]_str;_str = tmp;_capacity = n;}}void resize(size_t n, char c = '\0'){if (n <= _size){_size = n;_str[_size] = '\0';}else{if (n > _capacity){reserve(n);}for (size_t i = _size; i < n; i++){_str[i] = c;}_size = n;_str[_size] = '\0';}}bool empty()//判断是否为空{return strcmp(_str, "") == 0;//两个字符串比较要用strcmp,不能直接用==}void push_back(char c){if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size] = c;_str[_size + 1] = '\0';_size++;}void append(const char* str){if (_capacity < _size + strlen(str))//若容量不够,则扩容{reserve(_size + strlen(str));}strcpy(_str + _size, str);_size += strlen(str);}string& operator+=(const string& str){append(str._str);return *this;}string& operator+=(const char* str){append(str);return *this;}string& operator+=(char c){push_back(c);return *this;}string& insert(size_t pos, char c){assert(pos <= _size);//检测pos是否合法if (_size == _capacity)//判断是否需要增容{reserve(_capacity == 0 ? 4 : 2 * _capacity);}size_t i = _size;while (i >= pos){_str[i + 1] = _str[i];i--;}_str[pos] = c;_size++;return *this;}string& insert(size_t pos, const char* str){assert(pos <= _size);//检测pos是否合法if (_size + strlen(str) > _capacity)//判断是否需要增容{reserve(_size + strlen(str));}char* end = _str + _size;while (end >= _str + pos){*(end + strlen(str)) = *end;end--;}strncpy(_str + pos, str, strlen(str));_size += strlen(str);return *this;}string& erase(size_t pos, size_t len = npos){assert(pos < _size);//判断pos是否合法size_t n = _size - pos;if (len >= n)//说明pos后面的字符全部删除{_size = pos;_str[_size] = '\0';//字符串后面放上'\0'}else//说明pos后面还有一部分字符保留着{strcpy(_str + pos, _str + pos + len);//用需要保留的字符覆盖掉需要删除的字符_size -= len;}return *this;}void clear()//将对象中存储的字符串置空{_size = 0;_str[_size] = '\0';}const char* c_str(){return _str;}char& operator[](size_t i)//可读可写{assert(i < _size);//检测下标的合法性return _str[i];}const char& operator[](size_t i)const//只读{assert(i < _size);//检查下标的合法性return _str[i];}size_t find(char c, size_t pos = 0)//正向寻找{assert(pos < _size);for (size_t i = pos; i < _size; i++)//从pos位置开始向后找目标字符{if (_str[i] == c){return i;}}return npos;}size_t find(const char* str, size_t pos = 0)//正向寻找{assert(pos < _size);const char* ret = strstr(_str + pos, str);//用strstr进行查找if (ret)//若找到子字符串,则返回子字符串的起始位置{return ret - _str;//返会字符串第一个字符的下标}else//找不到就返回nullptr{return npos;//返回npos}}void reverse(iterator left, iterator right){right = right - 1;while (left < right){char c = '\0';c = *left;*left = *right;*right = c;left++;right--;}}//size_t rfind(char c, size_t pos = npos)//{// string tmp(*this);// reverse(tmp.begin(), tmp.end());// if (pos > _size)// {// pos = _size - 1;//若pos大于等于字符串有效长度时,看作pos为字符串最后一个字符的下标// }// pos = _size - 1 - pos;//将pos改为镜像对称后的位置// size_t ret = tmp.find(c, pos);// if (ret != npos)// return _size - 1 - ret;//若找到了,返回ret镜像对称后的位置// else// return npos;//若没找到,返回npos//}//size_t rfind(const char* str, size_t pos= npos)//{// string tmp(*this);//拷贝构造对象tmp// reverse(tmp.begin(), tmp.end());//逆置tmp的C字符串// size_t len = strlen(str);//待查找的字符串长度// char* arr = new char[len + 1];//开辟空间,用于拷贝待查找的字符串// strcpy(arr, str);// std::cout << arr << std::endl;// //逆置待查找的字符串// size_t left = 0;// size_t right = len - 1;// while (left < right)// {// std::swap(arr[left], arr[right]);// left++;// right--;// }// if (pos >= _size)//pos大于字符串有效长度,pos设为字符串最后一个字符的下标// {// pos = _size - 1;// }// pos = _size - 1 - pos;//将pos改为镜像对称后的位置// size_t ret = tmp.find(arr, pos);//复用find函数正向查找// delete[]arr;// if (ret != npos)// {// return _size - ret - len;//找到了,返回ret再镜像逆置回去的位置// }// else// {// return npos;//没找到,返回npos// }//}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->operator>(s) || this->operator==(s));}bool operator!=(const string& s)const{return !(*this == s);}private:char* _str;//存储字符串size_t _size;//字符串当前有效长度size_t _capacity;//当前字符串最大容量static const size_t npos;//整形最大值(很多地方的默认值)};const size_t string::npos = (size_t) - 1;string::iterator string::begin(){return _str;//返回字符串第一个字符的地址}string::const_iterator string::begin()const{return _str;//返回字符串第一个字符的const地址}string::iterator string::end(){return _str + _size;//返回'\0'的地址}string::const_iterator string::end()const{return _str + _size;//返回'\0'的const地址 }std::istream& operator>>(std::istream& in, CYF::string& str){str.clear();//先清空字符串char ch = in.get();//读取一个字符while (ch != ' ' && ch != '\n')//若读取的字符不是空格或\n的话,尾插到str后面后继续读{str.push_back(ch);ch = in.get();}return in;//支持连续赋值}std::ostream& operator<<(std::ostream& out, CYF::string& str){for (size_t i = 0; i < str.size(); i++){out << str[i];}return out;}//getline跟>>基本相同,只不过是读取含有空格的字符串,知道读到\n的时候才停std::istream& getline(std::istream& in, CYF::string& str){str.clear();char ch = in.get();while (ch != '\n'){str.push_back(ch);ch = in.get();}return in;}}
大家可能会发现,我的代码中还实现了一个rfind,但是rfind函数我一直没找到错在了哪里,因为他会在析构函数处报内存泄漏的错误,如果大家发现哪里出错了,欢迎大家在评论区留言或私信我!!谢谢大家!