1. 为什么需要自定义 string 类
在 C++ 标准库中,std::string 已经提供了完善的字符串操作功能。但自己动手实现一个简易版的 string 类,对于理解以下核心概念至关重要:
- 内存管理:动态内存分配与释放的底层机制
- 深浅拷贝:拷贝构造函数与赋值运算符的实现差异
- 运算符重载:使自定义类型拥有原生类型的操作体验
- 迭代器设计:STL 兼容性的关键要素
我曾在接手一个遗留项目时,遇到 std::string 在某些嵌入式平台上的异常行为。通过实现简化版 string 类,最终定位到是平台特定的内存对齐问题。这种底层认知,只有亲自动手实现才能获得。
2. 基础结构设计
2.1 类成员变量
一个最小化的 string 类需要以下核心成员:
cpp复制class MyString {
private:
char* m_data; // 存储字符串内容的堆内存指针
size_t m_size; // 当前字符串长度(不含结束符)
size_t m_cap; // 当前分配的内存容量
};
关键设计点:m_size 和 m_cap 的分离实现了 SSO(Small String Optimization)的简化版。当字符串较短时,可以省略堆内存分配。
2.2 构造函数实现
基础构造函数需要考虑多种初始化方式:
cpp复制// 默认构造
MyString() : m_data(nullptr), m_size(0), m_cap(0) {}
// C风格字符串构造
MyString(const char* str) {
m_size = strlen(str);
m_cap = m_size + 1;
m_data = new char[m_cap];
memcpy(m_data, str, m_cap);
}
// 拷贝构造(深拷贝)
MyString(const MyString& other) {
m_size = other.m_size;
m_cap = other.m_cap;
m_data = new char[m_cap];
memcpy(m_data, other.m_data, m_cap);
}
实测中发现,在拷贝构造函数中不检查 other.m_data 是否为 null 会导致某些编译器优化下的崩溃。正确的做法应该添加判空保护:
cpp复制if (other.m_data) {
// ...执行内存拷贝
} else {
m_data = nullptr;
m_size = m_cap = 0;
}
3. 关键成员函数实现
3.1 内存管理三件套
析构函数:
cpp复制~MyString() {
delete[] m_data; // delete[] 与 new[] 配对使用
}
拷贝赋值运算符:
cpp复制MyString& operator=(const MyString& other) {
if (this != &other) { // 防止自赋值
delete[] m_data; // 释放原有资源
m_size = other.m_size;
m_cap = other.m_cap;
m_data = new char[m_cap];
memcpy(m_data, other.m_data, m_cap);
}
return *this;
}
移动语义(C++11后):
cpp复制// 移动构造函数
MyString(MyString&& other) noexcept
: m_data(other.m_data),
m_size(other.m_size),
m_cap(other.m_cap)
{
other.m_data = nullptr; // 确保源对象处于可析构状态
other.m_size = other.m_cap = 0;
}
// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] m_data;
m_data = other.m_data;
m_size = other.m_size;
m_cap = other.m_cap;
other.m_data = nullptr;
other.m_size = other.m_cap = 0;
}
return *this;
}
3.2 常用操作符重载
下标访问:
cpp复制char& operator[](size_t pos) {
if (pos >= m_size) throw std::out_of_range("...");
return m_data[pos];
}
const char& operator[](size_t pos) const {
if (pos >= m_size) throw std::out_of_range("...");
return m_data[pos];
}
字符串拼接:
cpp复制MyString operator+(const MyString& rhs) const {
MyString result;
result.m_size = m_size + rhs.m_size;
result.m_cap = result.m_size + 1;
result.m_data = new char[result.m_cap];
memcpy(result.m_data, m_data, m_size);
memcpy(result.m_data + m_size, rhs.m_data, rhs.m_size + 1);
return result;
}
性能提示:多次拼接会产生临时对象,实际项目中建议实现 += 运算符和 reserve() 方法配合使用。
4. 核心功能实现
4.1 容量相关方法
cpp复制void reserve(size_t new_cap) {
if (new_cap <= m_cap) return;
char* new_data = new char[new_cap];
if (m_data) {
memcpy(new_data, m_data, m_size + 1);
delete[] m_data;
} else {
new_data[0] = '\0';
}
m_data = new_data;
m_cap = new_cap;
}
void resize(size_t new_size, char fill_char = '\0') {
if (new_size > m_size) {
reserve(new_size + 1);
memset(m_data + m_size, fill_char, new_size - m_size);
}
m_size = new_size;
m_data[m_size] = '\0';
}
4.2 查找算法实现
KMP 算法优化版:
cpp复制size_t find(const MyString& substr, size_t pos = 0) const {
if (substr.m_size == 0) return pos <= m_size ? pos : npos;
if (pos + substr.m_size > m_size) return npos;
// 构建部分匹配表
std::vector<size_t> lps(substr.m_size);
for (size_t i = 1, len = 0; i < substr.m_size; ) {
if (substr[i] == substr[len]) {
lps[i++] = ++len;
} else if (len != 0) {
len = lps[len - 1];
} else {
lps[i++] = 0;
}
}
// 执行搜索
for (size_t i = pos, j = 0; i < m_size; ) {
if (substr[j] == m_data[i]) {
i++; j++;
if (j == substr.m_size) {
return i - j;
}
} else if (j != 0) {
j = lps[j - 1];
} else {
i++;
}
}
return npos;
}
5. 迭代器支持
为使自定义 string 类兼容 STL 算法,需要实现迭代器:
cpp复制class iterator {
char* ptr;
public:
explicit iterator(char* p) : ptr(p) {}
char& operator*() { return *ptr; }
iterator& operator++() { ++ptr; return *this; }
bool operator!=(const iterator& other) const { return ptr != other.ptr; }
// 其他必要操作符...
};
iterator begin() { return iterator(m_data); }
iterator end() { return iterator(m_data + m_size); }
const_iterator cbegin() const { return const_iterator(m_data); }
const_iterator cend() const { return const_iterator(m_data + m_size); }
6. 性能优化实践
6.1 写时复制(COW)实现
通过引用计数实现共享内存:
cpp复制class MyString {
private:
struct StringData {
char* data;
size_t size;
size_t cap;
std::atomic<int> refcount;
// ...其他方法
};
StringData* m_data;
void detach() {
if (m_data && m_data->refcount > 1) {
auto* new_data = new StringData;
// ...执行深拷贝
--m_data->refcount;
m_data = new_data;
}
}
public:
char& operator[](size_t pos) {
detach(); // 写前分离
return m_data->data[pos];
}
};
6.2 SSO 优化实现
小型字符串直接存储在对象内部:
cpp复制class MyString {
private:
static constexpr size_t SSO_SIZE = 15; // 根据平台调整
union {
struct {
char* ptr;
size_t size;
size_t cap;
} heap;
char sso[SSO_SIZE + 1];
};
bool is_sso() const { return heap.size <= SSO_SIZE; }
public:
const char* c_str() const {
return is_sso() ? sso : heap.ptr;
}
// ...其他方法需要相应修改
};
7. 测试与验证
7.1 单元测试要点
使用 Catch2 测试框架示例:
cpp复制TEST_CASE("MyString functionality") {
MyString s1;
REQUIRE(s1.size() == 0);
MyString s2 = "hello";
REQUIRE(s2.size() == 5);
SECTION("Copy semantics") {
MyString s3 = s2;
REQUIRE(s3 == "hello");
s3[0] = 'H';
REQUIRE(s2 == "hello"); // 确保深拷贝
}
SECTION("Move semantics") {
MyString s4 = std::move(s2);
REQUIRE(s4 == "hello");
REQUIRE(s2.size() == 0); // 验证源对象被置空
}
}
7.2 性能对比测试
与 std::string 的关键操作对比:
cpp复制void benchmark() {
const int N = 1000000;
// 构造测试
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i) {
MyString s("test_string");
}
auto end = std::chrono::high_resolution_clock::now();
// ...输出耗时
// 拼接测试
MyString s1("a"), s2("b");
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i) {
MyString s3 = s1 + s2;
}
// ...对比结果
}
8. 生产环境注意事项
-
异常安全:所有可能抛出异常的操作(如内存分配)需要保证强异常安全
cpp复制MyString& operator=(const MyString& other) { char* new_data = new char[other.m_cap]; // 先分配 delete[] m_data; // 后释放 // ...其他操作 } -
线程安全:
- 基础版本默认非线程安全
- COW 版本需要原子操作保证引用计数安全
-
ABI 兼容性:
- 避免在 DLL 接口中直接使用
- 如需跨模块使用,提供 C 风格接口封装
-
内存调试:
cpp复制#ifdef DEBUG void* operator new(size_t size) { void* p = malloc(size); printf("Allocated %zu bytes at %p\n", size, p); return p; } #endif
实现完整的 string 类大约需要 800-1200 行代码。在实际项目中,建议优先使用 std::string,自定义实现主要用于特殊场景(如内存受限环境、特定优化需求)或教学目的。