1. 从零实现一个C++ String类
在C++开发中,字符串处理是最基础也最频繁的操作之一。标准库提供的std::string虽然功能完善,但了解其底层实现原理对于深入理解内存管理、拷贝控制和运算符重载等核心概念至关重要。今天我们就来拆解如何从零实现一个工业级的String类,这不仅是面试常见考点,更是提升C++内功的绝佳练习。
2. 基础架构设计
2.1 内存管理模型
现代C++字符串通常采用COW(Copy-On-Write)或SSO(Small String Optimization)策略,我们先实现最基础的动态内存版本:
cpp复制class String {
private:
char* m_data; // 字符数组指针
size_t m_size; // 当前字符串长度
size_t m_capacity; // 分配的内存容量
static const size_t npos = -1; // 类似std::string的npos
};
关键设计点:
- 使用
m_data指针管理堆内存,而非固定大小数组 - 分离
m_size(实际长度)和m_capacity(分配空间) - 遵循RAII原则,构造函数分配内存,析构函数释放
2.2 核心接口规划
一个完整的String类应支持以下操作:
cpp复制// 构造/析构
String(const char* str = "");
~String();
// 拷贝控制
String(const String& other);
String& operator=(const String& other);
// 容量相关
size_t size() const;
size_t capacity() const;
void reserve(size_t new_cap);
// 元素访问
char& operator[](size_t pos);
const char& operator[](size_t pos) const;
// 修改操作
String& append(const char* str);
String& operator+=(const char* str);
void clear();
3. 关键实现细节
3.1 构造函数与内存分配
基础构造函数需要正确处理空字符串和常规字符串:
cpp复制String::String(const char* str) {
m_size = strlen(str);
m_capacity = m_size + 1; // 预留null终止符
m_data = new char[m_capacity];
memcpy(m_data, str, m_size + 1); // 包含'\0'
}
关键点:必须为null终止符预留空间,memcpy比逐个字符赋值效率更高
3.2 拷贝控制实现
3.2.1 深拷贝实现
cpp复制String::String(const String& other) {
m_size = other.m_size;
m_capacity = other.m_capacity;
m_data = new char[m_capacity];
memcpy(m_data, other.m_data, m_size + 1);
}
String& String::operator=(const String& rhs) {
if (this != &rhs) { // 自赋值检查
delete[] m_data; // 释放原有资源
m_size = rhs.m_size;
m_capacity = rhs.m_capacity;
m_data = new char[m_capacity];
memcpy(m_data, rhs.m_data, m_size + 1);
}
return *this;
}
3.2.2 移动语义优化(C++11)
cpp复制String::String(String&& 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;
}
String& String::operator=(String&& rhs) noexcept {
if (this != &rhs) {
delete[] m_data;
m_data = rhs.m_data;
m_size = rhs.m_size;
m_capacity = rhs.m_capacity;
rhs.m_data = nullptr;
rhs.m_size = 0;
rhs.m_capacity = 0;
}
return *this;
}
3.3 动态扩容策略
append操作需要考虑容量不足时的扩容:
cpp复制void String::reserve(size_t new_cap) {
if (new_cap <= m_capacity) return;
char* new_data = new char[new_cap];
memcpy(new_data, m_data, m_size + 1);
delete[] m_data;
m_data = new_data;
m_capacity = new_cap;
}
String& String::append(const char* str) {
size_t append_len = strlen(str);
size_t required_cap = m_size + append_len + 1;
if (required_cap > m_capacity) {
// 通常采用1.5或2倍增长因子避免频繁扩容
reserve(std::max(required_cap, m_capacity * 2));
}
memcpy(m_data + m_size, str, append_len + 1);
m_size += append_len;
return *this;
}
4. 高级功能实现
4.1 迭代器支持
为兼容STL算法,需要实现迭代器:
cpp复制class String {
public:
using iterator = char*;
using const_iterator = const char*;
iterator begin() { return m_data; }
iterator end() { return m_data + m_size; }
const_iterator cbegin() const { return m_data; }
const_iterator cend() const { return m_data + m_size; }
};
4.2 查找算法实现
实现类似std::string的find功能:
cpp复制size_t String::find(const char* substr, size_t pos = 0) const {
if (pos >= m_size) return npos;
const char* result = strstr(m_data + pos, substr);
return result ? result - m_data : npos;
}
4.3 流输出支持
重载<<运算符方便调试输出:
cpp复制std::ostream& operator<<(std::ostream& os, const String& str) {
return os << str.c_str(); // 假设实现了c_str()
}
5. 性能优化技巧
5.1 SSO优化实现
对小字符串(通常<=15字节)直接存储在对象内部:
cpp复制class String {
private:
union {
struct {
char* ptr;
size_t size;
size_t capacity;
} large;
char small[16]; // SSO缓冲区
};
bool is_small() const { return large.capacity == 0; }
};
5.2 COW实现要点
写时复制需要引用计数:
cpp复制class String {
private:
struct ControlBlock {
size_t refcount;
size_t capacity;
char data[1]; // 柔性数组
};
ControlBlock* m_control;
void detach() {
if (m_control->refcount > 1) {
// 执行深拷贝
--m_control->refcount;
m_control = create_control_block(...);
}
}
};
6. 测试与验证
6.1 基础功能测试用例
cpp复制void test_construction() {
String s1; // 默认构造
String s2("hello"); // C字符串构造
String s3(s2); // 拷贝构造
String s4 = std::move(s3); // 移动构造
assert(s2.size() == 5);
assert(strcmp(s2.c_str(), "hello") == 0);
assert(s3.empty()); // 移动后源对象应为空
}
6.2 边界条件测试
cpp复制void test_edge_cases() {
String s;
s.reserve(100);
assert(s.capacity() >= 100);
assert(s.empty());
s += "1234567890";
s += s; // 自追加
assert(s.size() == 20);
s[s.size()] = '\0'; // 确保null终止符
assert(strlen(s.c_str()) == 20);
}
7. 常见问题与解决
7.1 内存泄漏排查
使用Valgrind检测:
bash复制valgrind --leak-check=full ./string_test
常见泄漏场景:
- 忘记在析构函数中释放m_data
- 赋值运算符中自赋值检查遗漏
- 异常安全处理不当
7.2 性能优化方向
- 使用内存池替代直接new/delete
- 实现更智能的扩容策略(如Fibonacci增长)
- 添加多线程安全支持(对COW特别重要)
- 针对短字符串优化比较操作
7.3 与std::string的差异
- 不实现allocator支持
- 省略部分不常用接口(如rfind)
- 异常规范可能不同
- SSO实现细节差异
实现一个完整的String类需要考虑的细节远比表面看起来复杂。在实际项目中,建议优先使用std::string,但通过这个练习可以深入理解C++的许多核心概念。我在实现过程中最大的收获是真正理解了移动语义的价值——通过一个简单的资源所有权转移,就能大幅提升字符串操作的效率。