手写一个C++字符串类:从底层理解String的实现
这是我通过自己动手实现一个字符串类,希望能够让你更好地理解C++的类设计、内存管理和运算符重载等概念。
基本结构设计
首先我们来看一下字符串类的基本框架:
namespace yzq {class String {private:char* _str; // 指向动态分配的字符数组size_t _size; // 当前字符串长度size_t _capacity; // 当前容量static const size_t npos; // 表示无效位置};
}这里我们使用动态分配的字符数组来存储字符串,_size记录当前字符串的实际长度,_capacity记录当前分配的内存容量。
构造函数和析构函数
构造函数
String(const char* s1 = "") {_size = strlen(s1);_str = new char[_size + 1]; // 多分配一个位置存放'\0'_capacity = _size;strcpy(_str, s1);
}构造函数接收一个C风格字符串,默认是空字符串。我们使用strlen获取字符串长度,然后动态分配足够的内存(记得要多分配一个位置给结束符'\0'),最后用strcpy拷贝内容。
拷贝构造函数
String(const String& s1) {char* tmp = new char[s1._capacity + 1];strcpy(tmp, s1._str);_str = tmp;_size = s1._size;_capacity = s1._capacity;
}拷贝构造函数实现深拷贝,先创建新内存,再拷贝内容,最后更新成员变量。
析构函数
~String() {delete[] _str;_str = nullptr;
}析构函数负责释放动态分配的内存,避免内存泄漏。
内存管理:reserve函数
void String::reserve(size_t n) {if (n > _capacity) {char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}
}reserve函数用于提前分配足够的内存。只有当请求的大小大于当前容量时才进行扩容,这样可以避免频繁的内存重新分配。
字符串操作函数
尾插字符 push_back
void String::push_back(char s1) {if (_size == _capacity) {reserve(_capacity == 0 ? 4 : 2 * _capacity); // 2倍扩容}_str[_size++] = s1;_str[_size] = '\0';
}在尾部插入字符时,先检查容量是否足够,不够就进行2倍扩容,然后插入字符并更新大小。
追加字符串 append
void String::append(const char* s1) {size_t len = strlen(s1);if (_size + len > _capacity) {reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);}strcpy(_str + _size, s1);_size += len;
}追加字符串时,先计算需要追加的长度,如果空间不够就扩容(采用更智能的扩容策略:需要的大小超过2倍容量就按需分配,否则按2倍扩容),然后用strcpy拷贝内容。
运算符重载 +=
String& String::operator+=(char s1) {push_back(s1);return *this;
}String& String::operator+=(const char* s1) {append(s1);return *this;
}+=运算符重载直接复用push_back和append函数,让代码更简洁。
插入和删除操作
插入 insert
void String::insert(size_t pos, char s1) {assert(pos <= _size);if (_size == _capacity) {reserve(_capacity == 0 ? 4 : 2 * _capacity);}size_t end = _size + 1;while (end > pos) {_str[end] = _str[end - 1];end--;}_str[pos] = s1;_size++;
}插入字符时,先把从插入位置开始的字符都向后移动一位,腾出位置后再插入新字符。
删除 erase
void String::erase(size_t pos, size_t len) {assert(pos < _size);if (len == npos || pos + len >= _size) {_str[pos] = '\0';_size = pos;}else {for (size_t i = pos + len; i <= _size; i++) {_str[i - len] = _str[i];}_size -= len;}
}删除操作分两种情况:如果要删除到字符串末尾,直接在删除位置设置结束符;否则把后面的字符向前移动覆盖要删除的部分。
查找和子串
查找 find
size_t String::find(char c, size_t pos) const {assert(pos < _size);for (size_t i = pos; i < _size; i++) {if (_str[i] == c) return i;}return String::npos;
}size_t String::find(const char* s, size_t pos) const {assert(pos < _size);char* pos_ptr = strstr(_str + pos, s);if (pos_ptr == nullptr) {return String::npos;}return pos_ptr - _str;
}查找字符使用遍历,查找子串使用strstr函数,找到后通过指针相减计算位置。
获取子串 substr
String String::substr(size_t pos, size_t len) const {assert(pos < _size);String s1;if (pos + len > _size) {len = _size - pos;}s1.reserve(len);for (size_t i = 0; i < len; i++) {s1 += _str[i + pos];}return s1;
}获取子串时先处理长度越界的情况,然后预分配内存,最后逐个字符追加。
输入输出重载
流输出 operator<<
ostream& operator<<(ostream& out, const String& s) {for (auto ch : s) {out << ch;}return out;
}使用范围for循环遍历字符串输出每个字符。
流输入 operator>>
istream& operator>>(istream& in, String& s) {char ch = in.get();const int N = 256;char buff[N];int i = 0;while (ch != ' ' && ch != '\n') {buff[i++] = ch;if (i == N - 1) {buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i > 0) {buff[i] = '\0';s += buff;}return in;
}这里使用缓冲区机制:先积累256个字符再一次性添加到字符串中,避免频繁扩容,提高效率。
比较运算符重载
bool String::operator==(const String& d) {return strcmp(_str, d._str) == 0;
}bool String::operator!=(const String& d) {return !(*this == d);
}// 其他比较运算符类似...比较运算符都基于strcmp函数实现,注意!=可以复用==的实现。
