1. 为什么需要模拟实现string类?
在C++标准模板库(STL)中,string类是最常用的容器之一,但很多初学者只是停留在表面使用层面。当我第一次尝试自己实现string类时,才真正理解了内存管理、深浅拷贝这些核心概念。模拟实现string类能让你:
- 深入理解动态内存管理的本质
- 掌握RAII(资源获取即初始化)原则
- 体会STL设计者的编程思想
- 提升对指针和引用的理解深度
注意:本实现基于C++11标准,会与现代编译器的优化实现有所差异,但核心原理完全一致。我们重点在于理解底层机制而非追求性能极致。
2. 基础框架搭建
2.1 类的基本结构
我们先定义最基础的MyString类框架:
cpp复制class MyString {
public:
// 构造函数们
MyString();
MyString(const char* str);
MyString(const MyString& other); // 拷贝构造
~MyString(); // 析构函数
// 基本操作
size_t size() const;
const char* c_str() const;
private:
char* m_data; // 存储字符串数据
size_t m_length; // 字符串长度(不含'\0')
size_t m_capacity; // 当前分配的内存容量
};
这个框架包含了string类最核心的三个成员变量:
m_data:指向动态分配的字符数组m_length:当前字符串的实际长度m_capacity:当前分配的内存容量(通常>=length)
2.2 内存管理策略
标准string类采用了一种聪明的内存分配策略:
- 初始分配一定量的内存(比如15字节)
- 当字符串增长超过当前容量时,按一定比例扩容(通常是2倍)
- 缩容时通常更保守(避免频繁重新分配)
我们实现一个简单的扩容函数:
cpp复制void MyString::reserve(size_t new_capacity) {
if (new_capacity <= m_capacity) return;
char* new_data = new char[new_capacity + 1]; // +1 for '\0'
strcpy(new_data, m_data);
delete[] m_data;
m_data = new_data;
m_capacity = new_capacity;
}
实际工程中,这里应该考虑异常安全问题。如果new失败,应该保持原对象不变。
3. 关键操作实现
3.1 构造函数与析构函数
默认构造函数创建一个空字符串:
cpp复制MyString::MyString()
: m_data(new char[1]), m_length(0), m_capacity(0) {
m_data[0] = '\0';
}
带参构造函数接受C风格字符串:
cpp复制MyString::MyString(const char* str) {
if (!str) str = "";
m_length = strlen(str);
m_capacity = m_length;
m_data = new char[m_capacity + 1];
strcpy(m_data, str);
}
拷贝构造函数(深拷贝):
cpp复制MyString::MyString(const MyString& other) {
m_length = other.m_length;
m_capacity = other.m_capacity;
m_data = new char[m_capacity + 1];
strcpy(m_data, other.m_data);
}
析构函数释放内存:
cpp复制MyString::~MyString() {
delete[] m_data;
}
3.2 赋值操作符重载
赋值操作需要考虑自赋值情况:
cpp复制MyString& MyString::operator=(const MyString& other) {
if (this != &other) { // 防止自赋值
delete[] m_data;
m_length = other.m_length;
m_capacity = other.m_capacity;
m_data = new char[m_capacity + 1];
strcpy(m_data, other.m_data);
}
return *this;
}
更高效的实现(copy-and-swap惯用法):
cpp复制MyString& MyString::operator=(MyString other) {
swap(*this, other);
return *this;
}
void swap(MyString& a, MyString& b) noexcept {
using std::swap;
swap(a.m_data, b.m_data);
swap(a.m_length, b.m_length);
swap(a.m_capacity, b.m_capacity);
}
4. 常用功能实现
4.1 字符串连接
实现operator+=和operator+:
cpp复制MyString& MyString::operator+=(const MyString& other) {
if (other.m_length == 0) return *this;
reserve(m_length + other.m_length);
strcat(m_data, other.m_data);
m_length += other.m_length;
return *this;
}
MyString operator+(const MyString& lhs, const MyString& rhs) {
MyString result(lhs);
result += rhs;
return result;
}
4.2 下标访问
提供const和非const版本:
cpp复制char& MyString::operator[](size_t pos) {
if (pos >= m_length)
throw std::out_of_range("Index out of range");
return m_data[pos];
}
const char& MyString::operator[](size_t pos) const {
if (pos >= m_length)
throw std::out_of_range("Index out of range");
return m_data[pos];
}
4.3 比较操作
实现operator==等比较操作:
cpp复制bool operator==(const MyString& lhs, const MyString& rhs) {
if (lhs.size() != rhs.size()) return false;
return strcmp(lhs.c_str(), rhs.c_str()) == 0;
}
bool operator!=(const MyString& lhs, const MyString& rhs) {
return !(lhs == rhs);
}
5. 高级功能扩展
5.1 移动语义(C++11)
添加移动构造函数和移动赋值:
cpp复制MyString::MyString(MyString&& other) noexcept
: m_data(other.m_data),
m_length(other.m_length),
m_capacity(other.m_capacity) {
other.m_data = nullptr;
other.m_length = 0;
other.m_capacity = 0;
}
MyString& MyString::operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] m_data;
m_data = other.m_data;
m_length = other.m_length;
m_capacity = other.m_capacity;
other.m_data = nullptr;
other.m_length = 0;
other.m_capacity = 0;
}
return *this;
}
5.2 迭代器支持
实现基本的迭代器功能:
cpp复制class MyString {
public:
// 迭代器类型定义
using iterator = char*;
using const_iterator = const char*;
iterator begin() { return m_data; }
iterator end() { return m_data + m_length; }
const_iterator begin() const { return m_data; }
const_iterator end() const { return m_data + m_length; }
const_iterator cbegin() const { return m_data; }
const_iterator cend() const { return m_data + m_length; }
};
这样就能支持范围for循环:
cpp复制MyString str("Hello");
for (char c : str) {
std::cout << c << " ";
}
6. 性能优化与边界处理
6.1 短字符串优化(SSO)
现代string实现常用SSO来优化小字符串:
cpp复制class MyString {
private:
static const size_t SSO_SIZE = 15;
union {
struct {
char* m_data;
size_t m_length;
size_t m_capacity;
} m_large;
char m_small[SSO_SIZE + 1];
};
bool m_is_small;
// 根据字符串长度决定使用哪种存储
void init_storage(const char* str, size_t len) {
if (len <= SSO_SIZE) {
strcpy(m_small, str);
m_is_small = true;
} else {
m_large.m_data = new char[len + 1];
strcpy(m_large.m_data, str);
m_large.m_length = len;
m_large.m_capacity = len;
m_is_small = false;
}
}
};
6.2 异常安全
确保操作在异常发生时保持对象一致性:
cpp复制MyString& MyString::operator+=(const MyString& other) {
if (other.m_length == 0) return *this;
try {
reserve(m_length + other.m_length);
strcat(m_data, other.m_data);
m_length += other.m_length;
} catch (...) {
// 保持对象不变
throw;
}
return *this;
}
7. 测试与验证
编写测试用例验证我们的实现:
cpp复制void test_MyString() {
// 基础构造测试
MyString s1;
assert(s1.size() == 0);
assert(strcmp(s1.c_str(), "") == 0);
// C字符串构造测试
MyString s2("Hello");
assert(s2.size() == 5);
assert(strcmp(s2.c_str(), "Hello") == 0);
// 拷贝构造测试
MyString s3(s2);
assert(s3.size() == 5);
assert(strcmp(s3.c_str(), "Hello") == 0);
// 赋值测试
s1 = s3;
assert(s1.size() == 5);
assert(strcmp(s1.c_str(), "Hello") == 0);
// 连接测试
s1 += " World";
assert(s1.size() == 11);
assert(strcmp(s1.c_str(), "Hello World") == 0);
// 比较测试
MyString s4("Hello");
assert(s2 == s4);
assert(s1 != s2);
std::cout << "All tests passed!" << std::endl;
}
8. 实际应用中的注意事项
- 内存对齐:实际工程中应考虑内存对齐问题,提高访问效率
- 多线程安全:标准string通常不是线程安全的,需要外部同步
- 编码问题:处理UTF-8等编码时需要额外考虑
- 性能权衡:SSO虽然优化了小字符串,但增加了分支判断
我在实际项目中发现的一个常见错误是忘记处理自赋值情况。比如在赋值运算符中:
cpp复制// 错误示例!
MyString& operator=(const MyString& other) {
delete[] m_data; // 如果this == &other,这里就删除了other的数据!
// ...
}
另一个容易忽略的点是异常安全。任何可能抛出异常的操作都应该确保在异常发生时对象仍处于有效状态。
