前言
本文将讲述怎么模拟实现string类,有些同学可能会问了,我要实现这个有什么用?会用不就可以了吗?
你没有错,但是我们通过模拟实现string类可以帮助我们更加深入的了解字符串具体是怎么一回事?它的内部结构是怎么样的?如果以后我们写程序,碰到字符串某个地方报错,也能很快排查出问题哦~
🕺作者: 迷茫的启明星
专栏:《C++初阶》
😘欢迎关注:👍点赞🙌收藏✍️留言
🏇码字不易,你的👍点赞🙌收藏❤️关注对我真的很重要,有问题可在评论区提出,感谢阅读!!!
持续更新中~
string类 的模拟实现
一,搭建框架
我们首先把string类大概样子搭建出来,它不外乎构造函数、拷贝函数,析构函数、还有一些基于C语言实现的方便我们操作字符串的函数,那么我们就来搭建一下,需要提醒的是:一些注意事项我会放在代码的注释中便于理解。
#pragma once #include <cstring> #include <iostream> #include <cassert> using namespace std; namespace hxq { class string { public: typedef char* iterator; typedef const char* const_iterator; iterator begin() { return _str;//返回首字符地址 } iterator end() { //字符串一个字符占一个字节 return _str + _size; } //当然也有const类型 const_iterator begin() const { return _str;//返回首字符地址 } const_iterator end() const { //字符串一个字符占一个字节 return _str + _size;//返回尾巴的地址 } const char* c_str() const { return _str;//返回字符串首地址 } size_t size() const { return _size;//返回字符串长度 } size_t capacity() const { return _capacity;//返回string对象空间大小 } //字符串类(String)中的 operator[] 运算符重载函数, //用于访问字符串中指定位置上的字符(元素), //并返回该字符的引用。 const char& operator[](size_t pos) const { assert(pos < _size); return _str[pos]; } char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; } //析构函数,释放资源 ~string() { delete[] _str; _str = nullptr; _size=_capacity=0; } //一般来说我们尽量把参数放在一起 private: size_t _capacity;//实际空间 size_t _size;//字符串长度(不包括'\0') char* _str;//首字符地址 public: const static size_t npos=-1; //特殊值 //使用const static 语法修饰 //npos是最后一个字符的位置 }; }
二,重载输入输出操作符 ‘<<’ ‘>>’
1. 重载操作符 ‘<<’
将字符逐个输到输出流中
ostream& operator<<(ostream& out, const string& s) { for (char i : s) { out << i; } return out; }
2.重载操作符 ‘>>’
且看方式一
按照一般思路,我们设计一个循环读取字符直到遇到空格或者回车停止
于是就有了以下实现
istream& operator>>(istream& in ,string& s) { char ch; ch=in.get(); s.reserve(128);//预开128个字符的空间 while(ch!=' '&&ch!='\n') { s+=ch; ch=in.get(); } return in; }
但是在测试的时候出现这样的问题
当string对象有默认值时,输入的字符串会加在后面
就像这样:
结果:
不仅如此,当输入字符串很长,不断+=,频繁扩容,代码效率很低
来看方式二
我们开辟一个缓存字符数组来存储输入的字符串
达到条件就把缓存空间最后一个值赋值为‘\0’
再尾加缓存空间,也就是尾加字符串
istream& operator>>(istream& in,string& s) { s.clear(); char ch; ch=in.get(); const size_t N = 32; char buffer[N]; size_t i =0; //如果没有遇到空格或者回车就一直读取到buffer缓存区中 while(ch != ' '&&ch != '\n') { buffer[i++] = ch; //当它满了就让s尾加buffer //注意看buffer经过处理也是一个字符串 if(i == N-1) { buffer[i]='\0'; s+=buffer; i=0; } ch=in.get(); } //读取buffer中剩余字符 buffer[i]='\0'; s+=buffer; return in; }
三,实现构造函数
如何构造一个string的对象呢?
得考虑到三个成员变量:
_str 字符串的地址
_size 字符串长度
_capacity 字符串空间
我们需要将它们初始化
于是以下代码就出现了
string() :_str(new char[1])//先预存一个'\0'的空间 ,_size(0) ,_capacity(0) { _str[0]='\0'; }
有些同学就会想不能这样实现吗?
string() :_str(nullptr)//它没有给\0 预留空间 ,_size(0) ,_capacity(0) {}
不能这样子,它无法初始化空对象
但是如果构造函数有参数,这就不适用了,还要重载一个有参的构造函数,我们为什么不实现一个默认参数为“\0”的构造函数呢?
方式一
//string(const char* str = "\0")//二者都可以 string(const char* str = "")//""相当于"\0" :_str(new char[strlen(str)+1]) , _size(strlen(str)) , _capacity(strlen(str)) { strcpy(_str, str); }
但是我们会发现它的初始化的时候调用了三次strlen函数是不是看上去就很拉?
于是就有同学想这样了↓
string(const char* str = "") :_size(strlen(str)) ,_capacity(_size) ,_str(new char[_capacity+1]) { strcpy(_str,str); }
不能这样,初始化的顺序是按照成员变量的顺序依次进行的
_str => _capacity => _size
在初始化_str的时候,_capacity是随机值,就不可行
但是我们可以这样,把初始化放在里面,就不会受到它的限制
方式二
string(const char* str = "") { _size = strlen(str); _capacity = _size; _str = new char[_capacity + 1]; strcpy(_str, str); }
四,实现拷贝构造和重载赋值操作符
传统的写法
1.拷贝构造
s2(s1)
按照我们之前对拷贝构造的理解
我们需要将s1的’_str’,‘_size’,'_capacity’依次赋值给s2的成员变量
注意:capacity要加1,为‘\0’预留位置
实现:
string(const string& s) :_str(new char[s._capacity+1]) ,_size(s.size()) ,_capacity(s._capacity) { strcpy(_str,s._str); }
2. 重载赋值操作符
假如说是:s1=s3
思路:
新开辟一个空间tmp,将s3._str赋值给tmp
将s1的_str删除
将tmp的值给s1
string& operator=(const string&s) { if(this!=&s)//判断是否是自己给自己赋值 { //新开一个空间来存s3的值 char* tmp = new char[s._capacity+1];//多开一个给'\0' strcpy(tmp,s._str); //把delete[] _str放在开空间后面,避免空间不足,会友好一些 delete[] _str;//删除原来s1的值 //将tmp的值给s1 _str = tmp; _size = s._size; _capacity = s._capacity; } return *this; }
现代写法
1.拷贝构造
还有一种现代写法
这是一种有着资本主义或者说老板思维,让员工干活
具体怎么回事呢?
s2(s1)
我们可以新建一个string对象,并且以s1._str来初始化它
也就是说利用了前文的构造函数string(const char* str = “”)
再将”员工“的’_str’,‘_size’,'_capacity’与this的相交换
这样我们就可以借助这个”员工“来实现拷贝构造
需要注意的是,这里需要进行初始化为nullptr,0,0的操作
因为被调用的员工被处理(析构)的时候_str不能是随机空间(脏数据)(感兴趣的同学可以试着自己试试)
string(const string&s) :_str(nullptr) ,_size(0) ,_capacity(0) { string tmp(s._str); swap(_str,tmp._str); swap(_size,tmp._size); swap(_capacity,tmp._capacity); }
但是它似乎有点不太美观,我们将他放到一个函数中
void swap(string& tmp)//这是我们定义的函数 { //↓这是库里面的函数,我们以后再讲 ::swap(_str,tmp._str); ::swap(_size,tmp._size); ::swap(_capacity,tmp._capacity); } string(const string&s) :_str(nullptr) ,_size(0) ,_capacity(0) { string tmp(s._str); swap(tmp); //相当于this->swap(tmp) }
2. 重载赋值操作符
再想想,刚刚的赋值操作符是不是也有相似之处呢?
我们只要和拷贝构造那样”雇佣“一个”员工“帮助我们实现即可
可以让代码非常简洁,就像这个式子:s1=s2
我们不需要考虑后面那个值的下场,因为它只是走个过场
所以直接调用swap函数交换它们的’_str’,‘_size’,‘_capacity’
string& operator=(string s) { swap(s); return *this; }
五,实现string 类相关的函数
1. reserve函数
在 C++ 的 string 类中,reserve() 是一个成员函数,
它用于在字符串中预分配一定的内存空间,以提高字符串操作的效率。
具体来说,reserve() 函数的功能是控制字符串对象的容量(capacity),
也就是分配给字符串对象的内存空间大小。当字符串对象的长度超过容量时,
会导致再次分配内存空间,从而影响性能。
因此,通过使用 reserve() 函数可以提前预留一定的内存空间,
以避免频繁地重新分配内存,从而提高程序的运行效率。
实现思路:
如果需要分配的值大于字符串的_capacity就申请空间tmp
将原来的字符串按字节拷贝给tmp
删除原来的字符串的值
将tmp的地址给_str
代码:
void reserve(size_t n) { if(n > _capacity) { char* tmp=new char[n+1]; strcpy(tmp,_str); delete[] _str; _str = tmp; _capacity = n; } }
2.resize函数
在 C++ 的 string 类中,resize() 是一个成员函数,
它用于重新设置字符串的长度(size),并可以选择是否填充新添加的字符。
具体来说,resize() 函数的功能是控制字符串对象的长度和内容。
当需要改变字符串对象的长度时,可以使用 resize() 函数来进行操作。
如果新的长度小于或等于原长度,则会删除超出部分的字符;
如果新的长度大于原长度,则会在末尾添加新的字符。
实现思路:
分为两种情况
如果新的长度大于原长度就申请空间再将新的字符添加即可
如果新的长度小于或等于原长度,则会删除超出部分的字符
代码:
void resize(size_t n, char ch = '\0') { if (n > _size) { reserve(n); for (size_t i = _size; i < n; ++i) { _str[i] = ch; } _str[n]='\0'; _size = n; } else { _str[n]='\0'; _size = n; } }
3. insert函数
在 C++ 的 string 类中,insert() 是一个成员函数,
它用于将一个字符串或字符序列插入到另一个字符串的指定位置。
具体来说,insert() 函数的功能是在字符串对象的指定位置插入一个字符串或字符序列。
可以通过传递不同的参数组合来选择需要插入的内容和插入位置。
在这里我们仅实现两种常用的
1.插入字符
实现思路:
insert(size_t pos,char ch)
pos>字符串长度报错
如果空间不够扩容
将pos位置后面的字符往后挪
代码:
//使用引用,提高工作效率 string& insert(size_t pos,char ch) { assert(pos<=_size); if (_size == _capacity) { reserve(_capacity == 0 ? 4 : _capacity * 2); } size_t end = _size + 1; while(end>pos) { _str[end]=_str[end-1]; --end; } _str[pos] = ch; ++_size; return *this; }
2.插入字符串
实现思路:
insert(size_t pos,const char* str)
和插入单个字符类似
pos>字符串长度报错
如果空间不够扩容
将pos位置后面的字符往后挪
注意挪的长度是插入字符串的长度
代码:
string& insert(size_t pos,const char* str) { assert(pos<=_size); size_t len = strlen(str); if (_size + len > _capacity) { reserve(_size + len); } size_t end = _size + len; while(end>=pos+len) { _str[end]=_str[end - len]; --end; } strncpy(_str + pos,str,len); _size += len; return *this; }
4.push_back函数
尾插
实现思路:
push_back(char ch)
1
方式一:判断空间是否足够,在最后设置需要增加字符,再在其后加上‘\0’即可
方式二可以直接使用前面的insert函数进行尾插即可
代码:
//方式一 void push_back(char ch) { if(_size == _capacity) { reserve(_capacity == 0 ? 4 : _capacity * 2); } _str[_size]=ch; ++_size; _str[_size] = '\0'; } //方式二 void push_back(char ch) { insert(_size,ch); }
5. append函数
用来将一个字符串追加到另一个字符串的末尾,使其成为一个更长的字符串。
实现思路:
append(const char*str)
方式一:判断原字符串空间是否足够,直接将追加的字符串拷贝在它后面
方式二:使用前面的insert函数尾插字符串
代码:
//方式一 void append(const char*str) { //方式一 size_t len = strlen(str); if (_size+len>_capacity) { reserve(_size+len); } strcpy(_str+_size,str); //还有一个函数是strcat(_str,str); //但是思考一下它的实现方式就会发现它首先要找'\0',那么就不高效 _size+=len; } //方式二 void append(const char*str) { insert(_size,str); }
6.操作符 += 重载
相当于尾插字符和尾插字符串
实现思路:
string& operator+=(char ch)
尾插字符利用push_back函数
string& operator+=(const char* str)
尾插字符串利用append函数
代码:
//加字符 string& operator+=(char ch) { push_back(ch); return *this; }
//加字符串 string& operator+=(const char* str) { append(str); return *this; }
7.erase函数
从pos开始删除长度为len的字符串,默认npos即为删到尾
实现思路:
erase(size_t pos , size_t len = npos)
pos>字符串长度报错
如果是最后一个或者大于字符串的长度,那么就在pos这个位置上设为’\0’
否则将位置[pos+len]后面的字符串给到[pos]位置,复制结束时再在尾巴上给一个’\0’
代码:
void erase(size_t pos , size_t len = npos) { assert(pos<_size); //如果是最后一个或者大于字符串的长度,那么就在pos这个位置上设为'\0' if(len == npos || pos + len >= _size) { _str[pos] = '\0'; _size = pos; } else { //将位置[pos+len]后面的字符串给到[pos]位置 //复制结束时再在尾巴上给一个'\0' strcpy(_str + pos,_str+pos+len); _size -=len; } }
8.clear函数
字符清空,长度置为0
实现思路:
clear()
利用字符串以‘\0’结尾的性质
直接将字符串第一个位置置为‘\0’
再将长度置为0
代码:
void clear() { _str[0] = '\0'; _size = 0; }
9.find函数
从pos位置开始寻找返回子串在字符串中第一次出现的位置,如果没有找到,则返回 std::string::npos
实现思路:
//找某个字符 size_t find(char ch,size_t pos = 0) const
遍历字符串,寻找匹配字符
//找匹配的字符串 size_t find(const char* sub,size_t pos = 0)const
利用C语言库中的strstr函数
strstr 函数返回的指针指向的是源字符串中第一次出现子串的位置
代码:
//找某个字符 size_t find(char ch,size_t pos = 0) const { assert(pos<_size); for(size_t i= pos;i<_size;++i) { if(ch == _str[i]) { return i; } } return npos; }
//找匹配的字符串 size_t find(const char* sub,size_t pos = 0)const { assert(sub); assert(pos<_size); //strstr 函数返回的指针指向的是源字符串中第一次出现子串的位置 const char* ptr = strstr(_str+pos,sub); if(ptr == nullptr) { return npos; } else { return ptr-_str; } }
10.substr函数
用于从字符串中提取子串
实现思路:
string substr(size_t pos,size_t len = npos) const
新建一个string对象
从pos位置开始,截取长度为len的字符串
代码:
string substr(size_t pos,size_t len = npos) const { assert(pos<_size); size_t realLen = len; if (len == npos || pos+len>_size) { realLen = _size - pos; } string sub; for (size_t i = 0; i < realLen; ++i) { sub+=_str[pos+i]; } return sub; }
11.操作符 > == >= <= < != 重载
实现思路:
利用strcmp函数判断大小
代码:
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); }
到此我们就模拟了string类的大部分功能,还有一些都是相关的函数,我就不再过多赘述了,这里面最重要的就是构造函数和拷贝构造,一定要理解清楚!
源码:
namespace hxq { class string { //框架 public: typedef char* iterator; typedef const char* const_iterator; iterator begin() { return _str;//返回首字符地址 } iterator end() { return _str + _size; //字符串一个字符占一个字节 } //当然也有const类型 const_iterator begin() const { return _str;//返回首字符地址 } const_iterator end() const { return _str + _size; //字符串一个字符占一个字节 } string(const char* str = "") { _size = strlen(str); _capacity = _size; _str = new char[_capacity + 1]; strcpy(_str, str); } //拷贝构造 //s2(s1) //传统写法 // string(const string& s) // :_str(new char[s._capacity+1]) // ,_size(s.size()) // ,_capacity(s._capacity) // { // strcpy(_str,s._str); // } // //假设说是这样的式子:s1=s3 // string& operator=(const string&s) // { // if(this!=&s)//判断是否是自己给自己赋值 // { // //新开一个空间来存s3的值 // char* tmp = new char[s._capacity+1];//多开一个给'\0' // strcpy(tmp,s._str); // //把delete[] _str放在开空间后面,避免空间不足,会友好一些 // delete[] _str;//删除原来s1的值 // //将tmp的值给s1 // _str = tmp; // _size = s._size; // _capacity = s._capacity; // } // return *this; // } // string(const string&s) // :_str(nullptr) // ,_size(0) // ,_capacity(0) // { // string tmp(s._str); // swap(_str,tmp._str); // swap(_size,tmp._size); // swap(_capacity,tmp._capacity); // } //但是它似乎有点不太美观,我们将他放到一个函数中 //再简化 void swap(string& tmp)//这是我们定义的函数 { //↓这是库里面的函数,我们以后再讲 ::swap(_str,tmp._str); ::swap(_size,tmp._size); ::swap(_capacity,tmp._capacity); } string(const string&s) :_str(nullptr) ,_size(0) ,_capacity(0) { string tmp(s._str); swap(tmp); //相当于this->swap(tmp) } string& operator=(string s) { swap(s); return *this; } const char* c_str() const { return _str; } //一些函数 //分配空间 void reserve(size_t n) { if(n > _capacity) { char* tmp=new char[n+1]; strcpy(tmp,_str); delete[] _str; _str = tmp; _capacity = n; } } //重新设置长度,后面参数如果有的话就是加n个ch void resize(size_t n, char ch = '\0') { if (n > _size) { reserve(n); for (size_t i = _size; i < n; ++i) { _str[i] = ch; } _str[n]='\0'; _size = n; } else { _str[n]='\0'; _size = n; } } //插入字符 //使用引用,提高工作效率 string& insert(size_t pos,char ch) { assert(pos<=_size); if (_size == _capacity) { reserve(_capacity == 0 ? 4 : _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<=_size); size_t len = strlen(str); if (_size + len > _capacity) { reserve(_size + len); } size_t end = _size + len; while(end>=pos+len) { _str[end]=_str[end - len]; --end; } strncpy(_str + pos,str,len); _size += len; return *this; } void push_back(char ch) { //方式一 // if(_size == _capacity) // { // reserve(_capacity == 0 ? 4 : _capacity * 2); // } // // _str[_size]=ch; // ++_size; // _str[_size] = '\0'; //方式二 insert(_size,ch); } void append(const char*str) { //方式一 size_t len = strlen(str); if (_size+len>_capacity) { reserve(_size+len); } strcpy(_str+_size,str); //strcat(_str,str); //思考一下它的实现方式就会发现它首先要找'\0',那么就不高效 _size+=len; //方式二 insert(_size,str); } //加字符 string& operator+=(char ch) { push_back(ch); return *this; } //加字符串 string& operator+=(const char* str) { append(str); return *this; } //删除 //从pos开始删除长度为len的字符串,默认npos即为删到尾 void erase(size_t pos , size_t len = npos) { assert(pos<_size); //如果是最后一个或者大于字符串的长度,那么就在pos这个位置上设为'\0' if(len == npos || pos + len >= _size) { _str[pos] = '\0'; _size = pos; } else { // 将位置[pos+len]后面的字符串给到[pos]位置 // 复制结束时再在尾巴上给一个'\0' strcpy(_str + pos,_str+pos+len); _size -=len; } } //想到字符串是以'\0'判断结束的,于是_str[0] = '\0',再把_size设为0结束 void clear() { _str[0] = '\0'; _size = 0; } //找某个字符 size_t find(char ch,size_t pos = 0) const { assert(pos<_size); for(size_t i= pos;i<_size;++i) { if(ch == _str[i]) { return i; } } return npos; } //找匹配的字符串 size_t find(const char* sub,size_t pos = 0)const { assert(sub); assert(pos<_size); //strstr 函数返回的指针指向的是源字符串中第一次出现子串的位置 const char* ptr = strstr(_str+pos,sub); if(ptr == nullptr) { return npos; } else { return ptr-_str; } } //截取字符串 string substr(size_t pos,size_t len = npos) const { assert(pos<_size); size_t realLen = len; if (len == npos || pos+len>_size) { realLen = _size - pos; } string sub; for (size_t i = 0; i < realLen; ++i) { sub+=_str[pos+i]; } return sub; } 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); } //框架 size_t size() const { return _size; } size_t capacity() const { return _capacity; } const char& operator[](size_t pos) const { assert(pos < _size); return _str[pos]; } char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; } ~string() { delete[] _str; _str = nullptr; _size=_capacity=0; } //一般来说我们尽量把参数放在一起 private: size_t _capacity;//实际空间 size_t _size;//字符串长度(不包括'\0') char* _str;//首字符地址 public: const static size_t npos=-1; //特殊值 //使用const static 语法修饰 //npos是最后一个字符的位置 }; //重载操作符<< 按照字符逐字节输到输出流中 ostream& operator<<(ostream& out, const string& s) { for (char i : s) { out << i; } return out; } //方式一:输入字符串很长,不断+=,频繁扩容,效率很低 //而且会出现这样的情况,当string对象有默认值时,输入的字符串会加在后面 // istream& operator>>(istream& in ,string& s) // { // char ch; // ch=in.get(); // s.reserve(128); // while(ch!=' '&&ch!='\n') // { // s+=ch; // ch=in.get(); // } // return in; // } //方式二 //设计一个缓存数组 istream& operator>>(istream& in,string& s) { s.clear(); char ch; ch=in.get(); const size_t N = 32; char buffer[N]; size_t i =0; //如果没有遇到空格或者回车就一直读取到buffer缓存区中 while(ch != ' '&&ch != '\n') { buffer[i++] = ch; //当它满了就让s尾加buffer //注意看buffer经过处理也是一个字符串 if(i == N-1) { buffer[i]='\0'; s+=buffer; i=0; } ch=in.get(); } //读取buffer中剩余字符 buffer[i]='\0'; s+=buffer; return in; } }
后记
感谢大家支持!!!
respect!
下篇见