【C++】string的模拟实现
目录
1. string的基本结构
2. 构造函数的实现
2.1 无参构造函数
2.2 带参构造函数
2.3 无参和带参构造函数合并
3. 析构函数的实现
4. [ ] 运算符重载
5. 观察运行范围for
6. 增删查改
6.1 插入字符
6.2 插入字符串
6.3 在pos位置插入字符
6.4 在pos位置插入字符串
6.5 删除从pos位置开始的len个字符
6.6 查找子串
7. 构造子串
8. 拷贝构造函数
9. 运算符重载
9.1 赋值运算符重载
9.2 比较运算符重载
9.3 <<和>>运算符重载
10. 源码
string.h
string.cpp
1. string的基本结构
三个成员变量:
- 字符指针_str指向开辟的动态数组
- _size标识有效数据个数
- _capacity记录容量的大小
class string{public://成员函数private:char* _str;size_t _size;size_t _capacity;};
2. 构造函数的实现
2.1 无参构造函数
//无参error
string():_str(nullptr)//这样写error!因为在打印空字符串时(string s1; cout << s1.c_str()) _str是野指针,cout << s1.c_str()会导致程序崩溃!必须确保char*指针指向的内存以'\0'结尾。,_size(0),_capacity(0)
{}
修改代码:
//无参right
string() :_str(new char[1]{'\0'})//给_str开辟1字节空间,并填充\0就可以解决问题,_size(0),_capacity(0)
{}
2.2 带参构造函数
//带参 error!
string(const char* str):_size(strlen(str)) //2, _str(new char[_size + 1]) //1 初始化列表按照成员变量在类中声明顺序进行初始化,所以这样写是错误的!, _capacity(_size) //3
{strcpy(_str, str);
}
修改代码:
形式1:
//带参 right!
string(const char* str):_str(new char[strlen(str) + 1]),_size(strlen(str)) , _capacity(_size)
{strcpy(_str, str);
}
形式2:
//带参 right!
string(const char* str)
{_size = strlen(str);_str = new char[_size + 1];_capacity = _size;strcpy(_str, str);
}
2.3 无参和带参构造函数合并
全缺省
//string(const char* str = "")
// :_str(new char[strlen(str) + 1])
// ,_size(strlen(str))
// , _capacity(_size)
//{
// strcpy(_str, str);
//}
3. 析构函数的实现
~string()
{delete[] _str;_str = nullptr;_size = _capacity = 0;
}
实现一个接口,返回字符指针,结合cout打印字符串的内容。
const char* c_str() const
{return _str;
}
4. [ ] 运算符重载
char& operator[](size_t pos)
{assert(pos < _size);return _str[pos];
}
const char& operator[](size_t pos) const
{assert(pos < _size);return _str[pos];
}
5. 观察运行范围for
for (auto e : s2)
{cout << e << " ";
}
cout << endl;
会报错:
范围for的底层就是迭代器,报错的关键在于自定义类未满足范围for的迭代器接口要求。
模拟使用指针来实现迭代器:为自定义string类中定义begin()和end()
这样范围for和迭代器都可正常运行了。
6. 增删查改
6.1 插入字符
void string::push_back(char ch){if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}
string& string::operator+=(char ch){push_back(ch);return *this;}
6.2 插入字符串
void string::append(const char* str){size_t len = strlen(str);if (_size + len > _capacity){//大于2倍,需要多少开多少,小于2倍,按2倍扩容reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);}strcpy(_str + _size, str);//目标地址:\0位置 strcpy会把\0也拷贝过去_size += len;}
string& string::operator+=(const char* str){append(str);return *this;}
6.3 在pos位置插入字符
//在pos位置插入字符 error
void string::insert(size_t pos, char ch)
{assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}//挪动数据size_t end = _size;while (end >= pos){_str[end + 1] = _str[end];end--;}_str[pos] = ch;++_size;
}
这样写对于在0位置处插入字符操作程序会崩掉,因为当end=0,end--之后为-1,由于end为无符号整型,所以end会变成很大的正数,end是极大值,_str[end + 1]会超出字符串数组合法内存范围,触发内存访问越界,最终导致程序崩溃。
如果我们把end的类型改为int呢?程序依然会崩溃。因为在表达式end>=pos中,end是int(有符号),pos是size_t(无符号),C++会触发隐式类型转换,将int类型的end转换为size_t(无符号整型)。
解决办法:
方法1. 将pos显示转换为int:
//在pos位置插入字符 rightvoid string::insert(size_t pos, char ch){assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}//挪动数据int end = _size;while (end >= (int)pos){_str[end + 1] = _str[end];end--;}_str[pos] = ch;++_size;}
方法2. 改变end的位置
//在pos位置插入字符
void string::insert(size_t pos, char ch)
{assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}//挪动数据size_t end = _size + 1; //让end为\0的下一个位置while (end > pos)//假设pos=0,end最后一次=1,所以终止条件end>pos{_str[end] = _str[end - 1];end--;}_str[pos] = ch;++_size;
}
6.4 在pos位置插入字符串
//在pos位置插入字符串
void string::insert(size_t pos, const char* s)
{assert(pos <= _size);size_t len = strlen(s);if (_size + len > _capacity){//大于2倍,需要多少开多少,小于2倍,按2倍扩容reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);}//挪动数据size_t end = _size + len;//while (end >= pos + len)//当pos=0,len=0时,--end到0,--end,end=-1,end变成极大值>0,程序崩溃!所以条件改成end > pos+len-1while (end > pos + len - 1){_str[end] = _str[end - len];--end;} for (size_t i = 0; i < len; i++){_str[pos + i] = s[i];}_size += len;}
6.5 删除从pos位置开始的len个字符
//删除pos开始的len个字符void string::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[pos] = _str[i];pos++;}_size -= len;}}}
6.6 查找子串
//查找子串size_t string::find(const char* s, size_t pos = 0){assert(pos < _size);const char* ptr = strstr(_str, s);if (ptr == nullptr){return npos;}else{return ptr - _str;//指针-指针得到元素个数,指针差直接对应字符串中的字符索引}}
7. 构造子串
//构造子串string string::substr(size_t pos, size_t len){assert(pos < _size);//len大于剩余字符长度,更新一下lenif (len > _size - pos){len = _size - pos;}string sub; //构造局部对象sub.reserve(len);for (size_t i = 0; i < len; i++){sub += _str[pos + i];}return sub;//传值返回 拷贝构造临时对象}//使用
void test_string3()
{string s("test.cpp.zip");size_t pos = s.find('.');cout << pos << endl;string suffix = s.substr(2);cout << suffix.c_str() << endl;}
这段代码在vs2019崩溃,在vs2022正常,核心在于返回值优化的差异上:
函数传值返回sub,会触发拷贝构造临时对象,再将临时对象拷贝给调用处的suffix,编译器的优化可能将临时对象省略,直接让sub拷贝构造suffix,但在拷贝过程中,两者的_str会指向同一块内存,当sub出作用域销毁时,释放内存导致suffix的_str成为野指针,访问时崩溃。
vs2022优化一步到位,把三次构造(局部对象sub、临时对象、suffix)合并成一次,直接在suffix的内存位置构造子串,完全避免了sub的拷贝,规避了浅拷贝问题。
8. 拷贝构造函数
若未显示定义,编译器会生成默认的拷贝构造函数,进行浅拷贝,所以string需要显示的提供拷贝构造:
确保string类的_str在拷贝时是深拷贝,而非浅拷贝(共享指针),需要提供拷贝构造函数:
//拷贝构造函数
string(const string& s)
{_str = new char[s._capacity + 1]; //新开辟内存复制内容strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;
}
9. 运算符重载
9.1 赋值运算符重载
同时,赋值也会存在浅拷贝问题,我们也要进行赋值运算符重载:
//赋值运算符重载(深拷贝问题)
string& operator=(const string& s)
{if (this != &s)//防止自己给自己赋值{delete[] _str;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;return *this;}
}
9.2 比较运算符重载
//只需要实现<和==运算符重载即可,其它运算符可以复用已重载的运算符bool operator<(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) < 0;}bool operator<=(const string& s1, const string& s2){return s1 < s2 || s1 == s2;//复用<和==}bool operator>(const string& s1, const string& s2){return !(s1 <= s2);//复用<=}bool operator>=(const string& s1, const string& s2){return !(s1 < s2);//复用<}bool operator==(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) == 0;}bool operator!=(const string& s1, const string& s2){return !(s1 == s2);//复用==}
使用:
void test_string(){string s1("abcdef");string s2("abcdef");cout << (s1 < s2) << endl;//0cout << (s1 > s2) << endl;//0cout << (s1 == s2) << endl;//1cout << ("hello world" < s2) << endl;//0 单参数构造函数支持隐式类型转换,通过构造函数将"hello world"转换成string临时对象cout << (s1 == "hello world") << endl;//0cout << ("hello world" =="hello world") << endl;//1//运算符重载的参数必须至少有一个类类型。//当没有合适的重载运算符时,编译器会优先使用原生运算符,这里实际比较的是指针的地址,结果为1。需用strcmp直接比较。}
9.3 <<和>>运算符重载
标准输入流的>>运算符设计为提取“单词”,cin的行为特性默认以空白字符(空格、换行、制表符等)作为分隔符,遇到空白就停止读取。这种设计符合大多数场景中“按单词解析”的需求。例如:
string s1,s2; cin>>s1>>s2; //输入"hello world"时,s1="hello",s2="world"(空格被跳过)
标准库的>>运算符提取字符串时,核心是将空格作为“单词分隔符”。
模拟实现流提取运算符重载(遇到空格或换行就停止)
istream& operator>>(istream& in, string& s){s.clear();//清空要存储输入内容的string对象sconst int N = 256;char buff[N];//定义一个字符数组,作为临时缓冲区,用来暂存从输入流读取的字符int i = 0;char ch; ch = in.get();//从输入流中提取单个字符while (ch != ' ' && ch != '\n')//遇到空格/换行停止读取{buff[i++] = ch;//存入缓冲区if (i == N - 1)//只剩最后一个位置来存储结束标志\0时{buff[i] = '\0';//添加\0,让buff能以C字符串形式被处理s += buff;i = 0;//重置缓冲区下标} ch = in.get();//继续从输入流读取下一个字符,进入下一轮循环判断}if (i > 0) //当循环遇到空格或换行符后,缓冲区里还有未处理的字符(i>0),拼接到s中{buff[i] = '\0';s += buff;}return in;}
使用:
void test_string5() {string s2;cin >> s2;cout << s2; }
运行结果:
上面代码完成了从输入流读取内容(遇到空格\换行就停止读取),并将内容存储起来。手动实现字符串读取逻辑, 如果需要包含空格的输入,标准库提供了std::getline,是string专门的函数,实现了空格的输入,例如:
string s; getline(cin,s); //输入"hello world"时,s="hello world"(包含空格)
模拟实现一下getline(读取到换行停止):
istream& operator>>(istream& in, string& s){s.clear();//清空要存储输入内容的string对象sconst int N = 256;char buff[N];//定义一个字符数组,作为临时缓冲区,用来暂存从输入流读取的字符int i = 0;char ch;ch = in.get();//从输入流中提取单个字符while (ch != '\n')//直到遇到换行符停止读取{buff[i++] = ch;//存入缓冲区if (i == N - 1)//只剩最后一个位置来存储结束标志\0时{buff[i] = '\0';//添加\0,让buff能以C字符串形式被处理s += buff;i = 0;//重置缓冲区下标}ch = in.get();//继续从输入流读取下一个字符,进入下一轮循环判断}if (i > 0)//当循环遇到换行符后,缓冲区里还有未处理的字符(i>0),拼接到s中{buff[i] = '\0';s += buff;}return in;}
使用:
void test_string5() {string s2;cin >> s2;cout << s2; }
运行结果:
10. 源码
string.h
#define _CRT_SECURE_NO_WARNINGS 1#pragma once
#include<iostream>
#include<string>
#include<assert.h>
using namespace std;namespace pig
{class string{public: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;}//无参string() :_str(new char[1]{'\0'}),_size(0),_capacity(0){}//带参 right!string(const char* str){_size = strlen(str);_str = new char[_size + 1];_capacity = _size;strcpy(_str, str);}//拷贝构造函数(解决浅拷贝问题)string(const string& s){_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}//赋值运算符重载(解决浅拷贝问题)string& operator=(const string& s){if (this != &s)//防止自己给自己赋值{delete[] _str;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;return *this;}}//析构函数~string(){delete[] _str;_str = nullptr;_size = _capacity = 0; }const char* c_str() const{return _str;}void clear(){_str[0] = '\0';//清掉所有数据,不释放空间_size = 0;}size_t size() const{return _size;}size_t capacity() const{return _capacity;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}const char& operator[](size_t pos) const{assert(pos < _size);return _str[pos];}void reserve(size_t n);void push_back(char ch);void append(const char*);string& operator+=(char ch);string& operator+=(const char* str);void insert(size_t pos, char ch);void insert(size_t pos, const char* str);void erase(size_t pos, size_t len = npos);size_t find(char ch, size_t pos = 0);size_t find(const char* s, size_t pos = 0);string substr(size_t pos = 0, size_t len = npos);private:char* _str;size_t _size;size_t _capacity;static const size_t npos = -1; //static允许类内初始化的特殊情况};//比较运算符重载bool operator<(const string& s1, const string& s2);bool operator<=(const string& s1, const string& s2);bool operator>(const string& s1, const string& s2);bool operator>=(const string& s1, const string& s2);bool operator==(const string& s1, const string& s2);bool operator!=(const string& s1, const string& s2); ostream& operator<<(ostream& out, const string& s);istream& operator>>(istream& in, string& s);
}
string.cpp
#include"string.h"namespace pig
{//const size_t string::npos = -1;void string::reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void string::push_back(char ch){if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}string& string::operator+=(char ch){push_back(ch);return *this;}void string::append(const char* str){size_t len = strlen(str);if (_size + len > _capacity){//大于2倍,需要多少开多少,小于2倍,按2倍扩容reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);}strcpy(_str + _size, str);//目标地址:\0位置 strcpy会把\0也拷贝过去_size += len;}string& string::operator+=(const char* str){append(str);return *this;}//在pos位置插入字符void string::insert(size_t pos, char ch){assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}//挪动数据size_t end = _size + 1; //让end为\0的下一个位置while (end > pos)//假设pos=0,end最后一次=1,所以终止条件end>pos{_str[end] = _str[end - 1];end--;}_str[pos] = ch;++_size;}//在pos位置插入字符串void string::insert(size_t pos, const char* s){assert(pos <= _size);size_t len = strlen(s);if (_size + len > _capacity){//大于2倍,需要多少开多少,小于2倍,按2倍扩容reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);}//挪动数据size_t end = _size + len;//while (end >= pos + len)//当pos=0,len=0时,--end到0,--end,end=-1,end变成极大值>0,程序崩溃!所以条件改成end > pos+len-1while (end > pos + len - 1){_str[end] = _str[end - len];--end;} for (size_t i = 0; i < len; i++){_str[pos + i] = s[i];}_size += len;}//删除pos位置开始的len个字符void string::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[pos] = _str[i];pos++;}_size -= len;}}//查找字符size_t string::find(char ch, size_t pos){for (size_t i = pos; i < _size; i++){if (_str[i] == ch){return i;} }return npos;}//查找子串size_t string::find(const char* s, size_t pos){assert(pos < _size);const char* ptr = strstr(_str, s);if (ptr == nullptr){return npos;}else{return ptr - _str;//指针-指针得到两指针之间的元素个数}}//构造子串string string::substr(size_t pos, size_t len){assert(pos < _size);//len大于剩余字符长度,更新一下lenif (len > _size - pos){len = _size - pos;}string sub; //构造局部对象sub.reserve(len);for (size_t i = 0; i < len; i++){sub += _str[pos + i];}return sub;}//只需要实现<和==运算符重载即可,其它运算符可以复用已重载的运算符bool operator<(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) < 0;}bool operator<=(const string& s1, const string& s2){return s1 < s2 || s1 == s2;//复用<和==}bool operator>(const string& s1, const string& s2){return !(s1 <= s2);//复用<=}bool operator>=(const string& s1, const string& s2){return !(s1 < s2);//复用<}bool operator==(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) == 0;}bool operator!=(const string& s1, const string& s2){return !(s1 == s2);//复用==}ostream& operator<<(ostream& out, const string& s){for (auto ch : s){out << ch;}return out;}//模拟实现流提取运算符重载istream& operator>>(istream& in, string& s){s.clear();//清空要存储输入内容的string对象sconst int N = 256;char buff[N];//定义一个字符数组,作为临时缓冲区,用来暂存从输入流读取的字符int i = 0;char ch; ch = in.get();//从输入流中提取单个字符while (ch != ' ' && ch != '\n')//遇到空格/换行停止读取{buff[i++] = ch;//存入缓冲区if (i == N - 1)//只剩最后一个位置来存储结束标志\0时{buff[i] = '\0';//添加\0,让buff能以C字符串形式被处理s += buff;i = 0;//重置缓冲区下标} ch = in.get();//继续从输入流读取下一个字符,进入下一轮循环判断}if (i > 0) //当循环遇到空格或换行符后,缓冲区里还有未处理的字符(i>0),拼接到s中{buff[i] = '\0';s += buff;}return in;}
}