概述
在掌握 string 接口后,我们尝试从零实现一个完整的 string 类。为了避免与标准库冲突,这里使用 mystd 命名空间封装。
核心成员与迭代器
类内部主要维护三个私有成员:
_str: 指向动态分配的字符数组。_size: 当前有效字符长度(不含\0)。_capacity: 当前分配的总容量(不含\0)。 此外定义npos为无符号整型最大值,用于表示查找失败等异常状态。 迭代器直接复用指针类型别名,iterator对应char*,const_iterator对应const char*。
默认成员函数实现
构造函数
无参构造时,需预留一个 \0 作为空串标识,此时 _size 和 _capacity 均为 0。带参构造则根据传入 C 字符串长度计算所需空间,注意额外分配一位给终止符。
// 无参构造
string() : _str(new char[1]{'\0'}), _size(0), _capacity(0) {}
// 带参构造
string(const char* str) {
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
注:初始化列表顺序遵循成员声明顺序,但此处为了复用 strlen 结果,直接在函数体内计算更为直观。
析构函数
释放动态内存并重置状态。
~string() {
delete[] _str;
_size = 0;
_capacity = 0;
}
深拷贝与赋值策略
拷贝构造函数
浅拷贝会导致多个对象共享同一块内存,析构时引发重复释放错误。必须实现深拷贝,为新对象独立分配内存并复制内容。
传统写法如下:
string(const string& s) : _size(s._size), _capacity(s._size) {
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
现代推荐写法采用 Copy-and-Swap 惯用法,利用临时对象完成深拷贝后再交换,能自动处理资源清理且代码更简洁:
void swap(string& tmp) {
std::swap(_str, tmp._str);
std::swap(_size, tmp._size);
std::swap(_capacity, tmp._capacity);
}
string(const string& s) {
string tmp(s); // 调用拷贝构造创建临时对象
swap(tmp); // 交换资源
}
注意:若未对成员变量赋初值(如 nullptr),在交换前确保临时对象已正确初始化,否则可能导致悬空指针。
赋值运算符重载
同样面临深浅拷贝问题,且需防范自我赋值。
传统方式需先检查 this == &other,再释放旧内存并申请新内存。
Copy-and-Swap 版本通过按值传递参数,隐式触发拷贝构造生成临时副本,随后交换即可,无需手动 delete。
string& operator=(string tmp) {
swap(tmp);
return *this;
}
这种写法不仅安全,还天然支持链式赋值。
迭代器与访问
迭代器本质是指针。begin() 返回首字符地址,end() 返回最后一个字符后的位置(即 \0 处)。
下标运算符 operator[] 提供读写与只读两个版本,需断言下标有效性。
iterator begin() { return _str; }
const_iterator end() const { return _str + _size; }
char& operator[](size_t i) { assert(i < _size); return _str[i]; }
容量与大小管理
size() 和 capacity() 分别返回当前长度与容量。
reserve(n) 负责扩容,当请求容量大于当前容量时,重新分配更大内存并拷贝旧数据。一般只扩不缩,避免频繁分配开销。
empty() 判断是否为空,可通过比较 _size 是否为零实现,比调用 strcmp 更高效。
void reserve(size_t n) {
if (n > _capacity) {
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
bool empty() const { return _size == 0; }
字符串修改操作
尾插与追加
push_back(char c) 在末尾添加字符。若空间不足,采用二倍扩容策略。插入后记得补上 \0。
append(const char* s) 追加字符串,逻辑类似,需注意目标缓冲区剩余空间。
operator+= 可复用上述函数简化实现。
void push_back(char c) {
if (_size == _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size++] = c;
_str[_size] = '\0';
}
插入与删除
insert(pos, ...) 涉及中间位置的数据移动。
插入字符时,从尾部开始逐个向后挪动,腾出位置。注意 size_t 无符号特性,循环条件应设为 end > pos 而非 end >= pos,防止头部插入时 end 减至 0 后发生下溢变成极大正数导致死循环。
插入字符串同理,先移动尾部 \0 及后续字符,再填入新内容。
erase(pos, len) 删除指定范围字符,将后续字符向前覆盖。若删除至末尾,只需截断。
void insert(size_t pos, char c) {
assert(pos <= _size);
if (_size == _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size + 1;
while (end > pos) {
_str[end] = _str[end - 1];
--end;
}
_str[pos] = c;
_size++;
}
substr 截取子串,新建 string 对象并填充数据。
clear 仅将首字符置为 \0 并重置长度,不释放内存,效率更高。
string substr(size_t pos, size_t len) {
assert(pos < _size);
if (len >= _size - pos) len = _size - pos;
string tmp;
tmp.reserve(len);
for (size_t i = 0; i < len; ++i) {
tmp += _str[pos + i];
}
return tmp;
}
查找与比较
find 支持查找字符或子串。字符查找线性扫描即可;子串查找可利用 strstr 辅助,返回相对偏移量。
关系运算符重载中,只需实现 < 和 ==,其余可通过组合推导得出,减少冗余代码。
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 strcmp(s1.c_str(), s2.c_str()) == 0;
}
输入输出流
重载 >> 和 << 使 string 能配合标准流使用。
输入时跳过前导空白,读取非空白字符直到遇到分隔符。为避免频繁扩容,可使用局部缓冲数组暂存。
输出时遍历字符逐个写入流。
istream& operator>>(istream& in, string& s) {
s.clear();
const int N = 256;
char buff[N] = { 0 };
int i = 0;
char ch = in.get();
while (ch == ' ' || ch == '\n' || ch == '\t') ch = in.get();
while (ch != ' ' && ch != '\n' && ch != '\t') {
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;
}
ostream& operator<<(ostream& out, const string& s) {
for (auto ch : s) out << ch;
return out;
}
这样实现了一个功能完备的 string 类,涵盖了内存管理、资源获取即初始化(RAII)思想以及常用算法逻辑。


