1. 为什么需要自己实现string类?
在C++标准库中,string类已经提供了完善的字符串操作功能,但作为C++开发者,手动实现一个简化版的string类仍然是极有价值的学习实践。这就像汽车工程师虽然可以直接使用现成的发动机,但亲手拆装一次能更深入理解内部构造。
通过这个项目,你将掌握:
- 动态内存管理的核心技巧
- 深浅拷贝的实际应用场景
- 运算符重载的语法细节
- 类设计的封装原则
我在实际面试中发现,90%的C++岗位都会考察string类的实现原理。一个完整的string类实现,往往能反映出开发者对C++核心机制的理解深度。
2. 基础结构设计与内存管理
2.1 类成员变量设计
最精简的string类只需要三个成员变量:
cpp复制class MyString {
private:
char* m_data; // 字符串数据指针
size_t m_size; // 当前字符串长度
size_t m_cap; // 分配的内存容量
};
这里采用经典的"指针+双长度"设计:
m_data指向堆内存,存储实际字符串m_size记录当前有效字符数(不含结尾的'\0')m_cap记录分配的总容量(通常大于等于m_size+1)
经验:初始容量建议设为16字节,后续按2倍扩容,这是平衡内存浪费和频繁扩容的常用策略
2.2 构造函数与析构函数实现
基础构造函数需要考虑多种初始化方式:
cpp复制// 默认构造
MyString() : m_data(new char[16]), m_size(0), m_cap(16) {
m_data[0] = '\0';
}
// C风格字符串构造
MyString(const char* str) {
m_size = strlen(str);
m_cap = m_size + 1;
m_data = new char[m_cap];
strcpy(m_data, str);
}
// 拷贝构造(深拷贝)
MyString(const MyString& other) {
m_size = other.m_size;
m_cap = other.m_cap;
m_data = new char[m_cap];
strcpy(m_data, other.m_data);
}
// 析构函数
~MyString() {
delete[] m_data;
}
常见坑点:
- 忘记在默认构造中初始化空字符串
- 拷贝构造时直接复制指针(浅拷贝错误)
- 析构函数未加
[]导致内存泄漏
3. 核心运算符重载实现
3.1 赋值运算符重载
赋值运算符需要处理自赋值情况:
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];
strcpy(m_data, other.m_data);
}
return *this;
}
3.2 下标访问运算符重载
提供const和非const两个版本:
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];
}
3.3 流运算符重载
实现输入输出支持:
cpp复制friend std::ostream& operator<<(std::ostream& os, const MyString& str) {
os << str.m_data;
return os;
}
friend std::istream& operator>>(std::istream& is, MyString& str) {
char buffer[1024];
is >> buffer;
str = buffer; // 利用已有的赋值运算符
return is;
}
4. 字符串操作功能实现
4.1 内存扩容机制
采用经典的2倍扩容策略:
cpp复制void reserve(size_t new_cap) {
if (new_cap <= m_cap) return;
char* new_data = new char[new_cap];
strcpy(new_data, m_data);
delete[] m_data;
m_data = new_data;
m_cap = new_cap;
}
void resize(size_t new_size, char ch = '\0') {
if (new_size > m_size) {
reserve(new_size + 1);
memset(m_data + m_size, ch, new_size - m_size);
}
m_size = new_size;
m_data[m_size] = '\0';
}
4.2 字符串连接操作
实现+=和+运算符:
cpp复制MyString& operator+=(const MyString& other) {
if (m_size + other.m_size >= m_cap) {
reserve((m_size + other.m_size) * 2);
}
strcat(m_data, other.m_data);
m_size += other.m_size;
return *this;
}
MyString operator+(const MyString& lhs, const MyString& rhs) {
MyString result(lhs);
result += rhs;
return result;
}
5. 完整功能测试与边界处理
5.1 测试用例设计
应覆盖以下场景:
cpp复制// 构造测试
MyString s1;
MyString s2("hello");
MyString s3 = s2;
// 运算符测试
s1 = s2 + " world";
s3 += "!";
// 边界测试
try {
char ch = s2[10]; // 应抛出异常
} catch(...) {...}
// 流操作测试
MyString s4;
std::cin >> s4;
std::cout << s4;
5.2 性能优化建议
- 实现移动语义(C++11)
- 添加SSO(短字符串优化)
- 实现自定义分配器
6. 现代C++特性扩展
6.1 移动语义支持
添加移动构造和移动赋值:
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 = 0;
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 = 0;
other.m_cap = 0;
}
return *this;
}
6.2 迭代器支持
实现标准迭代器接口:
cpp复制typedef char* iterator;
typedef const char* const_iterator;
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; }
实现这个string类后,你会对C++的类设计、内存管理和运算符重载有更深刻的理解。在实际项目中,建议优先使用std::string,但这个练习过程对提升C++底层认知非常有帮助。