1. 项目概述
在C++标准库中,string类是最常用的容器之一,但很多初学者对其底层实现机制并不了解。本文将带你从零开始实现一个简化版的string类,重点解析其核心的增删查改接口实现原理。通过这个项目,你不仅能深入理解string的工作机制,还能掌握C++类设计的关键技巧。
我选择实现string类作为示例,主要基于以下考虑:
- string底层采用动态数组实现,能很好展示内存管理技术
- 它包含了构造函数、拷贝控制等经典C++特性
- 迭代器、运算符重载等特性在string中都有典型应用
- 通过实现基础版本,可以更好理解标准库的设计取舍
2. 基础框架搭建
2.1 类基本结构
我们先定义string类的基本框架:
cpp复制namespace achieveString {
class string {
private:
char* _str; // 指向动态分配的字符数组
size_t _capacity; // 不包括'\0'的容量
size_t _size; // 不包括'\0'的实际大小
};
}
这里有几个关键设计点:
- 使用动态分配的char数组作为底层存储
- _capacity表示当前分配的存储空间大小(不包括终止符)
- _size表示实际存储的字符数(不包括终止符)
- 命名空间隔离避免与标准库冲突
注意:标准库实现通常更复杂,会考虑小字符串优化(SSO)等特性,这里我们保持简化。
2.2 内存布局示例
假设我们有一个字符串"Hello",其内存布局如下:
code复制索引: [0][1][2][3][4][5]
值: H e l l o \0
- _size = 5 (有效字符数)
- _capacity ≥ 5 (实际分配空间)
- 最后一个位置必须保留给'\0'
3. 默认成员函数实现
3.1 默认构造函数
默认构造需要创建一个空字符串,但不是简单的空指针:
cpp复制string()
: _str(new char[1])
, _capacity(0)
, _size(0) {
_str[0] = '\0';
}
关键细节:
- 必须分配至少1字节空间存放'\0'
- 初始化列表确保成员按声明顺序初始化
- _capacity和_size都不包括'\0'
常见错误:
- 直接置_str为nullptr,后续操作会崩溃
- 忘记添加终止符导致未定义行为
- 成员初始化顺序错误
3.2 析构函数
cpp复制~string() {
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
析构相对简单,但要注意:
- 使用delete[]匹配new[]的分配方式
- 将指针置空是良好习惯
- 不需要手动设置'\0',因为内存将被释放
3.3 拷贝构造函数
3.3.1 传统实现
cpp复制string(const string& s)
: _str(new char[s._capacity + 1])
, _capacity(s._capacity)
, _size(s._size) {
strcpy(_str, s._str);
}
这种实现直接分配新内存并复制内容,简单直接但代码重复。
3.3.2 现代实现
cpp复制string(const string& s)
: _str(nullptr)
, _capacity(0)
, _size(0) {
string tmp(s._str);
swap(tmp);
}
现代实现利用了临时对象和swap,减少了代码重复。swap实现如下:
cpp复制void swap(string& s) {
std::swap(_str, s._str);
std::swap(_capacity, s._capacity);
std::swap(_size, s._size);
}
3.4 赋值运算符
3.4.1 传统实现
cpp复制string& operator=(const string& s) {
if (this != &s) {
char* newstr = new char[s._capacity + 1];
strcpy(newstr, s._str);
delete[] _str;
_str = newstr;
_capacity = s._capacity;
_size = s._size;
}
return *this;
}
3.4.2 现代实现
cpp复制string& operator=(string s) {
swap(s);
return *this;
}
现代版本通过值传递参数自动获得拷贝,然后交换内容,异常安全且简洁。
4. 迭代器与范围for支持
4.1 迭代器基础
在C++中,迭代器是泛化的指针,为容器提供统一的访问接口。string的迭代器本质就是字符指针:
cpp复制typedef char* iterator;
typedef const char* const_iterator;
iterator begin() { return _str; }
iterator end() { return _str + _size; }
const_iterator begin() const { return _str; }
const_iterator end() const { return _str + _size; }
4.2 范围for支持
范围for是语法糖,编译器会将其转换为迭代器操作。有了上述迭代器支持后,我们的string自动支持范围for:
cpp复制achieveString::string s = "hello";
for (char ch : s) {
cout << ch;
}
5. 元素访问接口
5.1 下标运算符重载
提供const和非const两个版本:
cpp复制char& operator[](size_t pos) {
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const {
assert(pos < _size);
return _str[pos];
}
关键点:
- 使用assert检查边界,防止越界
- const版本返回const引用,保证不会修改const对象
- 非const版本允许修改元素值
6. 容量相关操作
6.1 基本查询接口
cpp复制size_t size() const { return _size; }
size_t capacity() const { return _capacity; }
bool empty() const { return _size == 0; }
6.2 reserve实现
reserve用于保证至少能容纳n个字符:
cpp复制void reserve(size_t n) {
if (n > _capacity) {
char* newstr = new char[n + 1];
strcpy(newstr, _str);
delete[] _str;
_str = newstr;
_capacity = n;
}
}
注意事项:
- 只扩容不缩容是标准库行为
- 多分配1字节给'\0'
- 需要拷贝原有内容
- 更新_capacity但不改变_size
6.3 resize实现
resize改变字符串大小并用指定字符填充:
cpp复制void resize(size_t n, char ch = '\0') {
if (n <= _size) {
_str[n] = '\0';
_size = n;
} else {
reserve(n);
for (size_t i = _size; i < n; ++i) {
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
处理逻辑:
- 缩小情况:直接截断,设置新终止符
- 扩大情况:先扩容,再用ch填充新增空间
- 总是保证终止符存在
7. 修改操作实现
7.1 push_back
追加单个字符:
cpp复制void push_back(char ch) {
if (_size == _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
扩容策略采用常见的2倍增长,初始容量为4。
7.2 append
追加C风格字符串:
cpp复制void append(const char* str) {
size_t len = strlen(str);
if (_size + len > _capacity) {
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
7.3 operator+=
通过复用已有接口实现:
cpp复制string& operator+=(char ch) {
push_back(ch);
return *this;
}
string& operator+=(const char* str) {
append(str);
return *this;
}
8. 字符串操作
8.1 c_str
返回C风格字符串:
cpp复制const char* c_str() const {
return _str;
}
8.2 find
查找字符或子串:
cpp复制size_t find(char ch, size_t pos = 0) const {
for (; pos < _size; ++pos) {
if (_str[pos] == ch) {
return pos;
}
}
return npos;
}
size_t find(const char* str, size_t pos = 0) const {
const char* p = strstr(_str + pos, str);
return p ? p - _str : npos;
}
9. 实用技巧与注意事项
- 内存管理:所有new必须有对应的delete,避免内存泄漏
- 异常安全:现代实现通常更安全,如拷贝交换惯用法
- 性能优化:2倍扩容策略是时间与空间的折中
- 边界检查:所有访问操作都应检查边界
- const正确性:正确使用const修饰成员函数
- 调试技巧:可以添加打印语句跟踪关键操作
实现过程中常见的坑:
- 忘记处理'\0'导致字符串操作异常
- 浅拷贝导致双重释放
- 没有检查自赋值情况
- 迭代器失效问题(如扩容后)
10. 测试用例示例
cpp复制void test() {
achieveString::string s1; // 默认构造
achieveString::string s2 = "hello"; // 构造
achieveString::string s3 = s2; // 拷贝构造
s1 = s3; // 赋值
s1 += " world"; // +=操作
for (char ch : s1) { // 范围for
cout << ch;
}
s1.reserve(100); // 扩容
s1.resize(3); // 缩小
size_t pos = s1.find('e'); // 查找
if (pos != achieveString::string::npos) {
cout << "Found at " << pos;
}
}
这个简化版string实现涵盖了最核心的功能,可以帮助理解标准库的基本工作原理。实际标准库实现会考虑更多边界条件和优化,如SSO、更精细的内存管理等。