1. 从零实现C++ string类核心功能
在C++开发中,string类是最基础也最常用的组件之一。虽然标准库提供了现成的实现,但理解其底层原理对提升编程能力至关重要。今天我将带大家完整实现一个简化版的string类,重点讲解增删查改等核心功能的实现细节。
我们实现的string类底层采用动态分配的字符数组(顺序表结构),包含三个核心成员变量:
char* _str:指向堆区字符串的指针size_t _size:当前字符串长度(不含'\0')size_t _capacity:当前分配的空间容量(不含'\0')
注意:所有容量计算都需要额外+1以容纳结尾的'\0'标识符
2. 基础成员函数实现
2.1 构造函数与内存管理
2.1.1 默认构造函数
cpp复制string()
: _str(new char[1])
, _size(0)
, _capacity(0) {
_str[0] = '\0';
}
关键点解析:
- 必须分配至少1字节空间存放'\0',不能直接置为nullptr
- 初始化列表按成员声明顺序执行,这里
_str必须先初始化 _size和_capacity不包含'\0',所以初始值为0
2.1.2 带参构造函数(C字符串初始化)
cpp复制string(const char* str)
: _size(strlen(str))
, _capacity(_size) {
_str = new char[_capacity + 1];
strcpy(_str, str);
}
性能优化技巧:
- 避免多次调用strlen,先计算长度再分配空间
- strcpy会连带拷贝'\0',无需手动添加
2.2 拷贝控制函数
2.2.1 传统拷贝构造函数
cpp复制string(const string& s)
: _size(s._size)
, _capacity(s._capacity) {
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
2.2.2 现代写法(推荐)
cpp复制string(const string& s)
: _str(nullptr)
, _size(0)
, _capacity(0) {
string tmp(s._str); // 利用构造函数创建临时对象
swap(tmp); // 交换资源所有权
}
现代写法的优势:
- 代码更简洁,复用构造函数逻辑
- 异常安全性更高
- 自动处理自赋值情况
2.2.3 赋值运算符重载
cpp复制string& operator=(string s) { // 注意这里是传值而非引用
swap(s); // 与临时对象交换资源
return *this;
}
这种写法巧妙地利用了:
- 拷贝构造函数创建临时对象
- swap交换资源后临时对象自动析构
- 天然处理了自赋值情况
2.3 析构函数实现
cpp复制~string() {
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
注意事项:
- 必须使用delete[]释放数组内存
- 将指针置空避免悬垂指针
- 重置size和capacity为0是良好习惯
3. 迭代器与元素访问
3.1 迭代器实现
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; }
实现要点:
- 迭代器本质是指针的抽象
- 需要提供const和非const版本
- 实现了迭代器就自动支持范围for循环
3.2 元素访问操作符
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];
}
安全考虑:
- 添加越界检查断言
- const版本用于const对象
- 返回引用允许修改元素
4. 容量相关操作
4.1 基础容量查询
cpp复制size_t size() const { return _size; }
size_t capacity() const { return _capacity; }
bool empty() const { return _size == 0; }
4.2 内存预留(reserve)
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'
- 先分配新空间再释放旧空间保证异常安全
4.3 调整大小(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';
}
}
行为逻辑:
- 缩小:直接截断,添加'\0'
- 扩大:用指定字符填充多余空间
- 自动处理容量不足的情况
5. 修改操作实现
5.1 清空内容
cpp复制void clear() {
_str[0] = '\0';
_size = 0;
}
注意:
- 不释放内存,只重置状态
- 保持capacity不变以便重用
5.2 追加字符/字符串
cpp复制void push_back(char ch) {
if (_size == _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
void append(const char* str) {
size_t len = strlen(str);
if (_size + len > _capacity) {
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
扩容策略:
- 初始容量为0时分配4字节
- 后续按2倍增长
- 追加字符串时精确扩容
5.3 插入操作
cpp复制void insert(size_t pos, char ch) {
assert(pos <= _size);
if (_size == _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
for (size_t i = _size + 1; i > pos; --i) {
_str[i] = _str[i - 1];
}
_str[pos] = ch;
++_size;
_str[_size] = '\0';
}
实现技巧:
- 先检查是否需要扩容
- 从后向前移动元素避免覆盖
- 最后更新size和'\0'
5.4 删除操作
cpp复制void erase(size_t pos, size_t len = npos) {
assert(pos < _size);
if (len == npos || pos + len >= _size) {
_str[pos] = '\0';
_size = pos;
} else {
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
删除策略:
- 不释放内存,只修改内容
- 处理删除到结尾的特殊情况
- 使用strcpy高效移动剩余元素
6. 字符串操作
6.1 查找功能
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;
}
实现选择:
- 单字符查找直接遍历
- 子串查找复用strstr函数
- 返回npos表示未找到
6.2 子串操作
cpp复制string substr(size_t pos = 0, size_t len = npos) const {
assert(pos < _size);
if (len == npos || pos + len > _size) {
len = _size - pos;
}
string sub;
sub.reserve(len);
for (size_t i = 0; i < len; ++i) {
sub.push_back(_str[pos + i]);
}
return sub;
}
注意事项:
- 处理默认参数和边界情况
- 预先分配足够空间提高效率
- 通过push_back保证'\0'正确设置
7. 实用工具函数
7.1 交换操作
cpp复制void swap(string& s) {
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
为什么重要:
- 被拷贝构造函数和赋值运算符复用
- 高效交换资源所有权
- 不抛出异常保证安全
7.2 流操作符重载
cpp复制friend ostream& operator<<(ostream& os, const string& s) {
for (size_t i = 0; i < s._size; ++i) {
os << s._str[i];
}
return os;
}
friend istream& operator>>(istream& is, string& s) {
s.clear();
char ch;
while (is.get(ch) && !isspace(ch)) {
s.push_back(ch);
}
return is;
}
实现细节:
- 输出时直接遍历字符
- 输入时逐个读取非空白字符
- 先清空目标字符串
8. 性能优化与异常安全
8.1 写时复制(Copy-On-Write)
高级技巧:通过引用计数实现共享内存
cpp复制struct StringData {
char* _str;
size_t _refCount;
size_t _size;
size_t _capacity;
};
class string {
private:
StringData* _data;
void detach() {
if (_data->_refCount > 1) {
StringData* newData = new StringData;
// ...复制数据...
--_data->_refCount;
_data = newData;
}
}
public:
char& operator[](size_t pos) {
detach();
return _data->_str[pos];
}
};
适用场景:
- 字符串拷贝频繁但修改较少
- 需要极致的内存优化
- 多线程环境需要额外同步
8.2 短字符串优化(SSO)
当字符串较短时,直接存储在对象内部:
cpp复制class string {
private:
static const size_t SSO_SIZE = 15;
union {
struct {
char* _ptr;
size_t _size;
size_t _capacity;
} _long;
char _short[SSO_SIZE + 1];
};
bool _isShort;
// ...
};
优势:
- 避免小字符串的堆分配
- 提高缓存局部性
- 减少内存碎片
9. 测试与验证
9.1 基础功能测试用例
cpp复制void test_constructor() {
string s1; // 默认构造
string s2("hello"); // C字符串构造
string s3(s2); // 拷贝构造
string s4 = s3; // 赋值运算
assert(s1.empty());
assert(s2.size() == 5);
assert(s3 == s2);
assert(s4 == s3);
}
9.2 修改操作测试
cpp复制void test_modifiers() {
string s("hello");
s.push_back('!');
assert(s == "hello!");
s.insert(5, " world");
assert(s == "hello world!");
s.erase(5, 6);
assert(s == "hello!");
}
9.3 性能测试要点
- 大量小字符串操作的性能
- 大字符串拷贝和修改的开销
- 内存使用情况检查
- 异常安全测试(内存不足时)
10. 与标准库的差异
- 简化了分配器支持
- 省略了模板参数
- 未实现move语义
- 缺少异常规范
- 简化了迭代器类别
实际项目中建议直接使用std::string,此实现主要用于学习目的。通过这个练习,我们深入理解了字符串类的内部工作原理,这对理解C++的内存管理、异常安全和性能优化都有很大帮助。
