【C++】string的底层原理及实现
文章目录
- string类的存储结构
- 默认成员函数
- 构造函数
- 析构函数
- 拷贝构造函数
- 赋值重载
- 容量操作
- size()
- capacity()
- reserve()
- resize()
- clear()
- 遍历与访问
- operator[ ]
- 迭代器范围与for
- 增删查改
- push_back()
- pop_back()
- append()
- operator+=
- insert()
- erase()
- c_str()
- find()
- substr()
- 非成员函数
- operator+
- 关系运算符
- 流插入
- getline()
本篇参考C++string类参考手册,实现一些string的常用接口,接口原型在该网站查阅。
string类的存储结构
string类的底层实际上是char类型的顺序表,所以结构上也比较相似。
namespace lw { class string { public: static const size_t npos; private: size_t _size;//有效数据个数 size_t _capacity;//可存储的容量 char* _str;//指向字符串起始位置的地址 }; const size_t string::npos = -1; };STL源码中,许多整型变量类型都是size_t无符号整型(没有负值);npos是string类常用到的一个值,有些函数的参数或者返回值是npos,并且这个值设为-1(表示232-1),参考标准库中的定义。
为什么string定义在一个命名空间中?
我们如果展开了标准库using namespace std; 那string默认使用的就是标准库中的。如果不展开标准库,那么所有的cin和cout都需要加上std:: 比较繁琐。
用一个命名空间对我们自己实现的string类进行封装,可以方便我们随时测试代码,与库里的string类进行测试对比,只需要改::前面的作用域即可。
#include using namespace std; int main() { string s("hello world");//默认标准库 std::string s("hello world");//标准库 lw::string s("hello world");//自己定义的类 cout //strcpy(_str, str);//strcpy也可以,只是为了与后面统一,换成memcpy memcpy(_str, str, _size + 1);//'\0'也要拷贝 } delete[] _str; _str = nullptr; _size = _capacity = 0; } _str = new char[s.capacity() + 1];//多一个空间给'\0' //strcpy(_str, s._str);//遇到'\0'终止,后面内容无法拷贝 memcpy(_str, s._str, s._size + 1);//末尾'\0'也拷贝过来 _size = s._size; _capacity = s._capacity; } if (this != &s) { char* tmp = new char[s._capacity + 1]; memcpy(tmp, s._str, s._size + 1); delete[] _str;//释放原始空间 _str = tmp; _size = s._size; _capacity = s._capacity; } return *this;//支持连续赋值 } std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); } //现代写法 string& operator=(string s)//不能传引用也不能加const { swap(s); return *this; } return _size; } return _capacity; } if (n _capacity) { char* tmp = new char[n + 1]; //strcpy(tmp, _str);//错误,无法拷贝'\0'后面的内容 memcpy(tmp, _str, _size + 1); delete[] _str; _str = tmp; _capacity = n; } }hr / h3resize()/h3 presize()只改变有效数据个数,不会改变容量。/ppn > _size:缩小长度为n,多余内容删掉。n
void resize (size_t n); void resize (size_t n, char c);
库里的resize()函数有两个版本,同样我们可以合成一个版本,第二个参数给缺省值\0即可。
void resize(size_t n, char ch = '\0') { assert(n >= 0); if (nclear()
void clear() { _size = 0; _str[0] = '\0'; }遍历与访问
operator[ ]
需要实现两个版本:普通对象调用[ ]可读可写,const对象调用[ ]只可读不可写。
char& operator[](size_t pos)//可读可写 { assert(pos迭代器范围与for
迭代器不一定是指针,而string的迭代器我们可以用指针来模拟实现。将指针重命名为迭代器。
typedef char* iterator;//普通迭代器 可读可写 iterator begin() { return _str; } iterator end() { return _str + _size; } typedef const char* const_iterator;//const迭代器 只可读 const_iterator begin() const { return _str; } const_iterator end() const { return _str + _size; }左闭右开区间,所以begin()返回字符串起始位置,end()返回末尾字符的下一个位置\0。
范围for
实现迭代器后,我们就可以使用范围for了。范围for是C++11的新语法,它的底层是傻瓜式地替换成迭代器,支持迭代器就支持范围for。
int main() { lw::string s1("hello world"); lw::string::iterator it = s1.begin(); while (it != s1.end()) { *it += 1; cout cout if (_size == _capacity) { reserve(_capacity == 0 ? 10 : _capacity * 2); } _str[_size++] = ch; _str[_size] = '\0'; } assert(_size 0); _size--; _str[_size] = '\0'; } size_t len = strlen(str); if (_size + len _capacity) { reserve(_size + len); } //strcpy(_str + _size, str);//strcat效率O(n+m) strcpy效率O(m) memcpy(_str + _size, str, len + 1);//末尾的'\0'也要拷贝 _size += len; return *this; } string& append(const string& s) { if (_size + s._size _capacity) { reserve(_size + s._size); } memcpy(_str + _size, s._str, s._size + 1);//末尾的'\0'也要拷贝 _size += s._size; return *this; } push_back(ch); return *this; } string& operator+=(const char* str) { append(str); return *this; } string& operator+=(const string& s) { append(s); return *this; } assert(pos reserve(_size + n); } for (size_t end = _size; end = pos; end--) { //pos==0时end会减到-1 //无符号整型 end=-1时表示42亿多 不加判断会无限循环 if (end == npos) { break; } _str[end + n] = _str[end]; } for (size_t i = 0; i = _size) { //pos及pos后面所有字符都删掉 _size = pos; _str[_size] = '\0'; } else { memcpy(_str + pos, _str + pos + len, len); _size -= len; } return *this; }c_str()
返回字符串首地址,以\0结束。这个接口主要是用来兼容C语言的。
const char* c_str() const { return _str; }find()
find查找失败会返回npos,找到则返回下标
//查找字符 size_t find(char ch, size_t pos = 0) { assert(possubstr()
返回子串
string substr(size_t pos = 0, size_t len = npos) const { assert(pos _size) { n = _size - pos; } string tmp; for (size_t i = 0; i非成员函数
operator+
实际上+操作符很少用,直接用+=更方便省事;这里只实现了一个接口,重载为友元函数。
string operator+(const string& s1, const string& s2) { string tmp(s1); tmp += s2; return tmp; }关系运算符
C++官方手册中,每种运算符都有3种重载版本,这里每个只实现了一种,重要的是理解本质,加深对string的理解。
为什么用C语言的memcmp函数进行字节上的比较,不用strcmp?
因为strcmp遇到\0终止,而string不看\0,以_size为终止。
当然也可以不用memcmp,遍历每个字符进行比较。
bool operator==(const string& s1, const string& s2) { return s1._size == s2._size && memcmp(s1._str, s2._str, min(s1._size, s2._size)) == 0; } bool operator int ret = memcmp(s1._str, s2._str, min(s1._size, s2._size)); return ret == 0 ? s1._size 永远无法读取空格和换行,程序会一直运行一直可以输入。3.如果字符串前面有空格或者换行,标准库的>>会默认清理空格和换行。所以要预先处理前面的空格和换行。
istream& operator>>(istream& in, string& s) { s.clear();//清空之前的内容 char ch = in.get();//可以读取空格和换行 //处理掉缓冲区前面的空格或者换行 while (ch == ' ' || ch == '\n') { ch = in.get(); } while (ch != ' ' && ch != '\n') { s += ch; ch = in.get(); } return in; }getline()
getline可以自定义读取的分隔符,遇到分隔符就不再读取,字符参数默认为\n,换行截断。
istream& getline(istream& in, string& s, char delim = '\n') { s.clear();//清空之前的内容 char ch = in.get(); while (ch != delim) { s += ch; ch = in.get(); } return in; }整体代码->:string模拟实现代码

