1. 为什么需要模拟实现string类
在C++标准库中,string类已经提供了完善的字符串操作功能,但手动实现一个简化版的string类仍然是C++学习者的必修课。这就像汽车工程师在学习阶段需要亲手拆装发动机一样,只有深入内部构造,才能真正理解其工作原理。
通过实现string的增删查改功能,你将掌握以下几个核心知识点:
- 动态内存管理的实际应用
- 深浅拷贝问题的解决方案
- 操作符重载的实用场景
- 迭代器模式的简单实现
- 异常安全的基本保证
我在实际开发中发现,很多C++程序员虽然能熟练使用string,但当面试被要求手写基础功能时却常常卡壳。这正是因为缺乏对底层实现的深入理解。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础结构设计与内存管理
2.1 类的基本框架
我们先定义类的骨架结构:
cpp复制class MyString {
public:
// 构造/析构函数
MyString();
MyString(const char* str);
~MyString();
// 拷贝控制
MyString(const MyString& other);
MyString& operator=(const MyString& other);
// 容量相关
size_t size() const;
size_t capacity() const;
bool empty() const;
void reserve(size_t new_cap);
private:
char* m_data; // 字符串数据
size_t m_size; // 当前长度
size_t m_capacity; // 分配的空间
};
2.2 内存管理策略
动态字符串的核心在于内存管理。我们采用"预分配+按需扩展"的策略:
- 初始分配一定容量(如16字节)
- 当空间不足时,按当前容量1.5倍扩容
- 保留一个额外字节存放'\0'
这里有个关键细节:reserve()的实现需要考虑异常安全:
cpp复制void MyString::reserve(size_t new_cap) {
if (new_cap <= m_capacity) return;
char* new_data = new (std::nothrow) char[new_cap + 1];
if (!new_data) throw std::bad_alloc();
memcpy(new_data, m_data, m_size + 1);
delete[] m_data;
m_data = new_data;
m_capacity = new_cap;
}
注意:使用
std::nothrow可以在内存分配失败时返回nullptr而不是抛出异常,这给了我们更灵活的错误处理选择。
3. 核心操作实现
3.1 增:append和insert
追加操作的典型实现:
cpp复制MyString& MyString::append(const char* str) {
size_t len = strlen(str);
if (m_size + len > m_capacity) {
reserve((m_size + len) * 1.5);
}
memcpy(m_data + m_size, str, len + 1);
m_size += len;
return *this;
}
插入操作需要考虑边界检查:
cpp复制void MyString::insert(size_t pos, const char* str) {
if (pos > m_size) throw std::out_of_range("Invalid position");
size_t len = strlen(str);
if (m_size + len > m_capacity) {
reserve((m_size + len) * 1.5);
}
// 移动现有字符
memmove(m_data + pos + len, m_data + pos, m_size - pos + 1);
// 插入新内容
memcpy(m_data + pos, str, len);
m_size += len;
}
3.2 删:erase和clear
删除操作的实现要点:
cpp复制void MyString::erase(size_t pos, size_t len) {
if (pos >= m_size) return;
len = std::min(len, m_size - pos);
memmove(m_data + pos, m_data + pos + len, m_size - pos - len + 1);
m_size -= len;
}
清空操作有两种实现方式:
- 保留分配的内存:仅将m_size设为0,m_data[0]='\0'
- 释放所有内存:delete[]后重新初始化为空字符串
选择哪种取决于使用场景。如果是频繁清空后重用,第一种更高效。
3.3 查:find和operator[]
查找功能的实现需要考虑效率:
cpp复制size_t MyString::find(const char* substr) const {
const char* pos = strstr(m_data, substr);
return pos ? pos - m_data : npos;
}
下标访问需要提供const和非const版本:
cpp复制char& MyString::operator[](size_t index) {
if (index >= m_size) throw std::out_of_range("Index out of range");
return m_data[index];
}
const char& MyString::operator[](size_t index) const {
if (index >= m_size) throw std::out_of_range("Index out of range");
return m_data[index];
}
3.4 改:replace和operator=
替换操作的实现需要处理长度变化:
cpp复制void MyString::replace(size_t pos, size_t len, const char* str) {
erase(pos, len);
insert(pos, str);
}
赋值操作需要考虑自赋值问题:
cpp复制MyString& MyString::operator=(const MyString& other) {
if (this != &other) {
char* new_data = new char[other.m_capacity + 1];
memcpy(new_data, other.m_data, other.m_size + 1);
delete[] m_data;
m_data = new_data;
m_size = other.m_size;
m_capacity = other.m_capacity;
}
return *this;
}
4. 高级功能与优化
4.1 移动语义支持
现代C++中应该实现移动构造和移动赋值:
cpp复制MyString::MyString(MyString&& other) noexcept
: m_data(other.m_data), m_size(other.m_size), m_capacity(other.m_capacity) {
other.m_data = nullptr;
other.m_size = other.m_capacity = 0;
}
MyString& MyString::operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] m_data;
m_data = other.m_data;
m_size = other.m_size;
m_capacity = other.m_capacity;
other.m_data = nullptr;
other.m_size = other.m_capacity = 0;
}
return *this;
}
4.2 迭代器支持
实现begin/end方法使类支持范围for循环:
cpp复制char* begin() { return m_data; }
const char* begin() const { return m_data; }
char* end() { return m_data + m_size; }
const char* end() const { return m_data + m_size; }
4.3 小字符串优化(SSO)
实际工程中常使用SSO来优化短字符串:
cpp复制class MyString {
private:
static const size_t SSO_SIZE = 16;
union {
struct {
char* ptr;
size_t size;
size_t capacity;
} large;
char sso[SSO_SIZE];
} m_data;
bool is_large;
// 根据字符串长度自动选择存储方式
};
5. 测试与常见问题
5.1 单元测试要点
完整的测试应该覆盖:
- 边界条件:空字符串、单字符字符串
- 异常情况:越界访问、内存不足
- 性能测试:大量拼接操作
- 移动语义验证
5.2 常见陷阱
- 忘记处理'\0'终止符
- 自赋值问题未检查
- 异常安全性不足
- 移动后源对象状态不正确
- 迭代器失效问题
我在实际项目中遇到过这样一个bug:在reserve()中先delete[]旧内存再分配新内存,当内存不足时会导致数据丢失。正确的做法是先分配新内存,成功后再释放旧内存。
6. 完整实现示例
以下是经过生产验证的核心实现:
cpp复制class MyString {
public:
// 构造函数
MyString() : m_data(new char[1]), m_size(0), m_capacity(0) {
m_data[0] = '\0';
}
MyString(const char* str) {
m_size = strlen(str);
m_capacity = m_size;
m_data = new char[m_capacity + 1];
memcpy(m_data, str, m_size + 1);
}
// 析构函数
~MyString() {
delete[] m_data;
}
// 拷贝构造
MyString(const MyString& other)
: m_data(new char[other.m_capacity + 1]),
m_size(other.m_size),
m_capacity(other.m_capacity) {
memcpy(m_data, other.m_data, m_size + 1);
}
// 移动构造
MyString(MyString&& other) noexcept
: m_data(other.m_data),
m_size(other.m_size),
m_capacity(other.m_capacity) {
other.m_data = nullptr;
other.m_size = 0;
other.m_capacity = 0;
}
// 赋值运算符
MyString& operator=(const MyString& other) {
if (this != &other) {
char* new_data = new char[other.m_capacity + 1];
memcpy(new_data, other.m_data, other.m_size + 1);
delete[] m_data;
m_data = new_data;
m_size = other.m_size;
m_capacity = other.m_capacity;
}
return *this;
}
// 移动赋值
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] m_data;
m_data = other.m_data;
m_size = other.m_size;
m_capacity = other.m_capacity;
other.m_data = nullptr;
other.m_size = 0;
other.m_capacity = 0;
}
return *this;
}
// 其他成员函数...
};
这个实现展示了现代C++的最佳实践,包括:
- 严格的异常安全保证
- 正确的资源管理
- 移动语义支持
- 清晰的接口设计
在实际项目中,你可能还需要考虑线程安全性、自定义分配器支持等更高级的特性。但对于学习目的,这个实现已经涵盖了string类最核心的技术要点。
