前言
在前面的学习中,我们已经初步掌握了 string 类接口函数的使用方法。本文将带领大家从零开始,逐步实现一个完整的 string 类,深入理解 C++ 内存管理与对象生命周期。
一、String 类总览
为了避免与标准库中的 string 产生命名冲突,我们使用 mystd 命名空间进行封装。
namespace mystd {
class string {
public:
// 迭代器
typedef char* iterator;
typedef const char* const_iterator;
// 默认成员函数
string();
string(const char* str);
string(const string& s);
string& operator=(const string& s);
~string();
// 迭代器相关函数
iterator begin();
iterator end();
const_iterator begin() const;
const_iterator end() const;
// 容量和大小相关函数
size_t size();
size_t capacity();
void reserve(size_t n);
void resize(size_t n, char ch = '\0');
bool empty() const;
// 修改字符串相关函数
void push_back(char ch);
void append(const char* str);
string& operator+=(char ch);
string& operator+=(const char* str);
string& insert(size_t pos, char ch);
string& insert(size_t pos, const char* str);
string& erase(size_t pos, size_t len);
void clear();
void swap(string& s);
const char* c_str() const;
// 访问字符串相关函数
char& operator[](size_t i);
const char& operator[](size_t i) const;
size_t find(char ch, size_t pos = 0) const;
size_t find(const char* str, size_t pos = 0) const;
size_t rfind(char ch, size_t pos = npos) const;
size_t rfind(const char* str, size_t pos = 0) const;
private:
char* _str; // 存储字符串
size_t _size; // 记录当前有效长度
size_t _capacity;// 记录当前容量
static const size_t npos;
};
const size_t string::npos = -1;
// 关系运算符重载函数
bool operator<(const string& s1, const string& s2);
bool operator<=(const string& s1, const string& s2);
bool operator>(const string& s1, const string& s2);
bool operator>=(const string& s1, const string& s2);
bool operator==(const string& s1, const string& s2);
bool operator!=(const string& s1, const string& s2);
// << 和 >> 运算符重载函数
ostream& operator<<(ostream& out, const string& s);
istream& operator>>(istream& in, string& s);
}
成员变量说明:
char* _str:用于存储字符串数据。size_t _size:记录当前字符串的有效长度(不包含 '\0')。size_t _capacity:记录字符串当前的容量(不包含 '\0')。static const size_t npos:静态成员变量,值为整型最大值,通常用作异常返回值。
迭代器说明:
typedef char* iterator:普通迭代器,允许读写。typedef const char* const_iterator:常量迭代器,只读。
二、默认成员函数
2.1 无参构造函数
string() : _str(new char[1]{'\0'}), _size(0), _capacity(0) {}
初始化时预留一个 '\0' 作为空字符表示。此时 _size 和 _capacity 均为 0,但实际分配了 1 字节空间存放终止符。
2.2 带参构造函数
string(const char* str) {
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
注意:这里没有使用初始化列表,主要是为了复用 strlen 的计算结果。开辟空间时需额外加 1 以容纳 '\0'。
2.3 拷贝构造函数
在模拟实现前,需明确深浅拷贝的区别:浅拷贝会导致多个对象共享同一块内存,修改一处影响另一处;深拷贝则为每个对象独立分配资源。
传统写法:
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._str); // 调用带参构造创建临时对象
swap(tmp);
}
利用按值传递参数自动触发深拷贝构造,生成临时对象 tmp,再通过 swap 交换资源。函数结束时 tmp 析构,自动释放原对象的旧内存,无需手动管理。
2.4 赋值运算符重载
同样需要考虑深浅拷贝及自我赋值问题。
传统写法:
string& operator=(const string& other) {
if (this == &other) return *this;
delete[] _str;
_str = new char[other._capacity + 1];
strcpy(_str, other._str);
_size = other._size;
_capacity = other._capacity;
return *this;
}
关键步骤:防范自我赋值、清理旧资源、深拷贝新资源、返回自身引用支持链式操作。
现代写法 (Copy-and-Swap):
string& operator=(string tmp) {
swap(tmp);
return *this;
}
参数按值传递,编译器自动调用拷贝构造函数完成深拷贝。进入函数体后执行 swap,将新数据赋予当前对象,局部临时对象 tmp 在析构时清理旧内存。这种方式代码更简洁且异常安全。
2.5 析构函数
~string() {
delete[] _str;
_size = 0;
_capacity = 0;
}
释放动态分配的内存并重置状态。
三、迭代器
迭代器本质是字符指针,iterator 只是类型别名。
iterator begin() { return _str; }
const_iterator begin() const { return _str; }
iterator end() { return _str + _size; }
const_iterator end() const { return _str + _size; }
begin 返回首字符地址,end 返回最后一个字符后的位置(即 '\0' 的地址)。
四、容量和大小相关函数
4.1 size 与 capacity
直接返回成员变量值,因私有属性无法直接访问。
size_t size() const { return _size; }
size_t capacity() const { return _capacity; }
4.2 reserve
当请求的容量超过当前 capacity 时进行扩容,否则保持不变。
void reserve(size_t n) {
if (n > _capacity) {
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
一般只扩容不缩容,预留空间给 '\0'。
4.3 empty
判断字符串是否为空。
bool empty() const { return _size == 0; }
五、字符串修改函数
5.1 push_back
尾插字符,若空间不足则扩容。
void push_back(char c) {
if (_size == _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size++] = c;
_str[_size] = '\0';
}
插入字符后必须补充 '\0' 终止符,防止非法访问。
5.2 append
尾插字符串。
void append(const char* s) {
size_t len = strlen(s);
if (len + _size > _capacity) {
size_t newcapacity = (_size + len > _capacity * 2) ? _size + len : _capacity * 2;
reserve(newcapacity);
}
strcpy(_str + _size, s);
_size += len;
}
待插入字符串本身包含 '\0',无需额外添加。
5.3 operator+=
复用 push_back 和 append 实现。
string& operator+=(char c) {
this->push_back(c);
return *this;
}
string& operator+=(const char* s) {
this->append(s);
return *this;
}
5.4 insert
在指定位置插入字符或字符串。
插入字符:
void insert(size_t pos, char c) {
assert(pos <= _size);
if (_size == _capacity) {
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
size_t end = _size + 1;
while (end > pos) {
_str[end] = _str[end - 1];
--end;
}
_str[pos] = c;
_size++;
}
注意 end 初始化为 _size + 1,避免头部插入时 0 - 1 导致 size_t 下溢死循环。
插入字符串:
void insert(size_t pos, const char* s) {
assert(pos <= _size);
size_t len = strlen(s);
if (len == 0) return;
if (len + _size > _capacity) {
size_t newcapacity = len + _size > _capacity * 2 ? len + _size : _capacity * 2;
reserve(newcapacity);
}
size_t end = _size + len;
while (end > pos + len - 1) {
_str[end] = _str[end - len];
--end;
}
for (size_t i = 0; i < len; i++) {
_str[pos + i] = s[i];
}
_size += len;
}
先移动尾部字符腾出空间,再填入新字符串。
5.5 erase
删除从 pos 开始的 len 个字符。
void erase(size_t pos, size_t len) {
assert(pos < _size);
if (len >= _size - pos) {
_str[pos] = '\0';
_size = pos;
} else {
size_t begin = pos + len;
while (begin <= _size) {
_str[begin - len] = _str[begin];
++begin;
}
_size -= len;
}
}
5.6 substr
截取子串。
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;
}
5.7 clear
清空字符串。
void clear() {
_str[0] = '\0';
_size = 0;
}
六、访问字符串相关函数
6.1 operator[]
支持下标访问。
char& operator[](size_t pos) {
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const {
assert(pos < _size);
return _str[pos];
}
6.2 find
查找字符或字符串。
size_t find(char c, size_t pos) {
assert(pos < _size);
for (size_t i = pos; i < _size; i++) {
if (_str[i] == c) return i;
}
return npos;
}
size_t find(const char* s, size_t pos) {
assert(pos < _size);
const char* ptr = strstr(_str, s);
if (ptr != nullptr) return ptr - _str;
return npos;
}
七、比较运算符重载
只需重载 < 和 ==,其余可通过组合实现。
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;
}
bool operator<=(const string& s1, const string& s2) {
return s1 < s2 || s1 == s2;
}
bool operator>(const string& s1, const string& s2) {
return !(s1 <= s2);
}
bool operator>=(const string& s1, const string& s2) {
return !(s1 < s2);
}
bool operator!=(const string& s1, const string& s2) {
return !(s1 == s2);
}
八、字符串输入与输出
8.1 >> 运算符重载
跳过前导空白,读取单词。
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;
}
使用缓冲数组减少频繁扩容,遇到空白字符停止。
8.2 << 运算符重载
遍历输出字符。
ostream& operator<<(ostream& out, const string& s) {
for (auto ch : s) {
out << ch;
}
return out;
}


