👉前言👈
因为 string 类的函数接口组合在一起才好玩,所以我们模拟实现 string 类就一组一组函数来模拟实现。为了避免我们模拟实现的 string 类和库里的 string 类冲突,所以我们将自己实现的 string 类封装在命名空间里。
👉访问和遍历 string 类👈
首先,string 类是一个可以动态增长的字符数组,其实就相当于是存储字符的顺序表。那么 string 类的成员变量如下方代码:
namespace Joy { class string { public: // 成员函数 private: char* _str; // 指向动态开辟的数组 size_t _size; // 数组的有效字符个数 size_t _capacity; // 数组的容量 }; }
那现在我们就来模拟实现 string 类了。string 类最重要的就是构造函数了,所以我们先实现构造函数。
string 类的构造函数
string(const char* str = "") { _size = strlen(str); _capacity = _size; _str = new char[_capacity + 1]; strcpy(_str, str); }
注:string类的构造函数最好提供空串缺省参数,然后还有多开一个空间来存储\0
标识字符,标识字符不算是 string 类的有效字符,给 _size 和 _capacity 都赋好初值后,就借助 strcpy 函数来拷贝字符串了。
string 类的析构函数
~string() { delete[] _str; _str = nullptr; _size = _capacity = 0; }
为了 string 类能用起来像数组一样,我们需要提供以下函数接口size、c_str和[]运算符重载。
const char* c_str() const { return _str; // 返回数组首元素的地址 } size_t size() const { return _size; } // 普通对象:可读可写 char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; } // const对象:只读 const char& operator[](size_t pos) const { assert(pos < _size); return _str[pos]; }
以上实现的函数接口,足以帮我构造一个 string 类对象和访问 string 类对象了,那么现在我们就来测试一下。
这样,访问和遍历 string 类对象的第一种方式就搞定了。那我们现在来实现一下遍历 string 类对象的第二种方式:迭代器iterator
。
迭代器可能是指针,也有可能不是指针,所以我们实现 string 类迭代器的方式是指针。
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 类迭代器是使用指针实现的,但是库里的 string 类的迭代器就有可能不是用指针实现了,比如 string 类的反向迭代器就不是使用指针来实现的了。因为reverse_iterator rit = s.rbegin() rit++会走到s.rend(),而如果使用指针实现,应该it--才能走到s.end(),使用 string 类的反向迭代器不是使用指针来实现的。
现在我们已经实现了 string 类的正向迭代器,其实范围 for 也实现好了。因为范围 for 的底层原理就是迭代器,编译器会自动将范围 for 替换成迭代器。
如果我们将正向迭代器的代码屏蔽掉,那范围 for 的代码就无法编译通过了。
👉capacity、resize 和 reserve👈
size_t capacity() const { return _capacity; } void reserve(size_t n) { if (n > _capacity) { char* tmp = new char[n + 1]; strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = n; } } void resize(size_t n, char ch = '\0') { if (n > _size) { reserve(n); for (size_t i = _size; i < n; i++) { _str[i] = ch; } _size = n; _str[_size] = '\0'; } else { _str[n] = '\0'; _size = n; } }
reserve 函数接口说明
如果 n > _capacity,那么就先申请一块 n + 1的空间tmp,然后将数据拷贝到这块空间,_str = tmp,更新容量的大小_capacity = n。
如果n <= _capacity,那么就什么都不用做。如果缩容的话,就会造成效率低下。
resize 函数接口说明
- 当
n > _size
时,调用 reserve 函数调整容量的大小,让插入字符 ch。 - 当
n <= _size
时,直接_str[_size] = '\0'
_size = n
,注意:这个过程也不要缩容。
👉string 类插入字符或字符串👈
我们主要模拟实现最常用的几个插入的接口,目的不是造更好的轮子。那我们现在开干!
string& push_back(char ch) { if (_size == _capacity) { int newCapacity = _capacity == 0 ? 4 : _capacity * 2; reserve(newCapacity); } _str[_size] = ch; ++_size; _str[_size] = '\0'; return *this; } string& append(const char* str) { size_t len = strlen(str); if (_size + len > _capacity) { reserve(_size + len); } strcpy(_str + _size, str); _size += len; return *this; } string& operator+=(char ch) { push_back(ch); return *this; } string& operator+=(const char* str) { append(str); return *this; }
注:不管是 push_back 函数,还是 append 函数,都要考虑是否需要扩容(reverse 函数会多开一个空间存储标识字符'\0'),然后再插入数据,最后更新_size和加上标识字符'\0'。对于operator +=运算符重载就可以赋用 push_back 函数和 append 函数了。
除了以上在尾部的插入,还有在任意位置的插入。
string& insert(size_t pos, char ch) { assert(pos <= _size); if (_size == _capacity) { int newCapacity = _capacity == 0 ? 4 : _capacity * 2; reserve(newCapacity); } // 挪动数据 /* int end = _size; while (end >= (int)pos) { _str[end + 1] = _str[end]; --end; } _str[pos] = ch; ++_size; return *this; */ 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); int len = strlen(str); if (_size + len > _capacity) { reserve(_size + len); } // 挪动数据 /* int end = _size; while (end >= (int)pos) { _str[end + len] = _str[end]; --end; } strncpy(_str + pos, str, len); _size += len; return *this; */ 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; }
注:挪动数据就要注意对边界条件的控制,如果控制不会就会造成死循环和越界访问的问题。还有拷贝数据,不能使用strcpy函数,strcpy函数会将\0'
拷贝过去。如果是>=,要讲pos强转为int。
👉erase👈
bool empty() const { return _size == 0; } string& erase(size_t pos, size_t len = npos) { assert(!empty()); assert(pos < _size); if (len == npos || pos + len >= _size) { _str[pos] = '\0'; _size = pos; } else { strcpy(_str + pos, _str + pos + len); _size -= len; } return *this; }
注:const static size_t npos = -1
,npos
是 string 类的静态成员变量,其值为-1。
erase 函数接口说明
当len == nps || pos + len >= _size时,就相当于将pos位置及其之后的字符都删掉,所以此时只需要将pos位置的字符改为'\0'并更新_size的值为pos。
除此之外,用 strcpy 函数将后面的字符向前覆盖删除,然后_size -= len就行了。