这是我通过自己动手实现一个字符串类,希望能够让你更好地理解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函数实现,注意!=可以复用==的实现。