1. string类的基本结构与设计思路
在C++标准库中,string是一个非常重要的类,它封装了字符串的常见操作,使得开发者可以更方便地处理字符串。理解string的底层实现对于掌握C++的内存管理和类设计非常有帮助。
1.1 成员变量解析
string类的核心成员变量通常包括三个部分:
cpp复制private:
char* _str; // 指向动态分配的字符数组
int _size; // 当前字符串长度(不包括'\0')
int _capacity; // 当前分配的存储空间大小
static const unsigned int npos = -1; // 特殊值,表示"未找到"
这种设计有几个关键考虑:
- 使用动态分配的字符数组(_str)来存储实际字符串内容,这样可以灵活处理不同长度的字符串
- _size记录当前字符串的实际长度,不包括结尾的'\0'
- _capacity记录当前分配的内存空间大小,通常大于等于_size
- npos是一个静态常量,用于表示"未找到"的特殊情况
注意:实际的标准库实现可能会更复杂,可能包含小字符串优化(SSO)等机制,但这里我们关注基本的实现原理。
1.2 构造函数实现分析
string类通常提供多种构造函数,这里我们看两种典型实现:
cpp复制// 第一种:分开写的构造函数
string() :
_str(new char[1]), // 多开辟一个放\0
_size(0),
_capacity(0)
{
_str[0] = '\0';
}
string(const char* str) :
_str(new char[strlen(str) + 1]),
_size(strlen(str)),
_capacity(strlen(str))
{
strcpy(_str, str);
}
// 第二种:全缺省构造函数
string(const char* str = "") :
_size(strlen(str))
{
_capacity = _size;
_str = new char[_size + 1];
strcpy(_str, str);
}
构造函数的设计要点:
- 默认构造函数创建一个空字符串,但仍需分配1字节空间存放'\0'
- 从C风格字符串构造时,需要分配足够空间(strlen+1)
- 全缺省构造函数可以同时作为默认构造函数和从C字符串构造的函数
2. 内存管理与拷贝控制
2.1 析构函数实现
cpp复制~string()
{
delete[] _str; // 注意别忘了这个方括号
_str = nullptr;
_size = _capacity = 0;
}
析构函数的关键点:
- 必须使用delete[]而不是delete,因为_str是用new[]分配的
- 将指针置为nullptr是良好的编程习惯
- 重置_size和_capacity为0不是必须的,但可以使对象状态更清晰
2.2 拷贝构造函数
拷贝构造有两种常见写法:传统写法和现代写法。
cpp复制// 传统写法
string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 现代写法
string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
现代写法的优势:
- 代码更简洁
- 利用已有的构造函数完成主要工作
- 通过swap操作实现资源转移,避免重复代码
2.3 赋值运算符重载
赋值运算符也有传统和现代两种写法:
cpp复制// 传统写法
string& operator=(const string& s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
return *this;
}
// 现代写法
string& operator=(string s)
{
swap(s);
return *this;
}
现代写法的精妙之处:
- 参数使用传值方式,自动调用拷贝构造函数
- 通过swap交换资源,原对象的资源会在临时对象析构时自动释放
- 代码异常安全,即使在new时抛出异常也不会影响原对象
3. 元素访问与迭代器
3.1 下标访问运算符
cpp复制inline char& operator[](int pos)
{
assert(pos < _size);
return _str[pos];
}
inline const char& operator[](int pos) const
{
assert(pos < _size);
return _str[pos];
}
下标访问的设计要点:
- 提供非const和const两个版本,以适应不同使用场景
- 使用assert检查越界(实际标准库可能抛出异常)
- 声明为inline以提高频繁调用时的性能
3.2 迭代器实现
cpp复制typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
迭代器实现的说明:
- 这里用原始指针模拟迭代器,简化实现
- begin()返回指向第一个元素的指针
- end()返回指向最后一个元素后面的指针
- 这种实现支持范围for循环:for (auto ch : str)
提示:实际标准库的迭代器实现要复杂得多,包含类型萃取等多种机制。
4. 字符串修改操作
4.1 内存管理函数
cpp复制void reserve(int n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
reserve函数的关键点:
- 只在需要扩大容量时才执行操作
- 新分配n+1的空间(多一个给'\0')
- 复制原内容后释放旧空间
- 更新_capacity但不改变_size
4.2 添加字符和字符串
cpp复制void push_back(char ch)
{
if (_size == _capacity)
reserve(_capacity == 0 ? 4 : 2 * _capacity);
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
void append(const char* str)
{
int len = strlen(str);
if (_size + len > _capacity)
reserve(_size + len);
strcpy(_str + _size, str);
_size += len;
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* s)
{
append(s);
return *this;
}
这些函数的设计考虑:
- push_back在空间不足时按2倍扩容(初始为4)
- append直接扩容到刚好能容纳新字符串的大小
- operator+=通过调用已有函数实现,避免代码重复
4.3 插入和删除操作
cpp复制void insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
reserve(_capacity == 0 ? 4 : 2 * _capacity);
int end = _size;
while (end >= (int)pos)
{
_str[end + 1] = _str[end];
end--;
}
_str[pos] = ch;
_size++;
}
void insert(size_t pos, const char* str)
{
int len = strlen(str);
if (_size + len > _capacity)
reserve(_size + len);
for (int i = pos; i < len + pos; i++)
{
if (i < _size)
_str[i + len] = _str[i];
_str[i] = str[i - pos];
}
_size += len;
_str[_size] = '\0';
}
插入操作的实现细节:
- 插入字符时需要将后面的字符都向后移动一位
- 插入字符串时需要处理更复杂的内存移动
- 两种操作都需要考虑扩容和边界检查
- 保持字符串以'\0'结尾
5. 实现中的注意事项与优化技巧
5.1 内存管理陷阱
- new/delete必须配对使用,new[]对应delete[]
- 拷贝构造和赋值运算符必须实现深拷贝
- 移动语义可以进一步优化性能(C++11)
- 扩容策略影响性能,通常采用2倍增长
5.2 性能优化建议
- 避免频繁的小规模扩容,预留足够空间
- strcpy比strcat效率更高
- 内联小函数减少调用开销
- 考虑实现移动构造函数和移动赋值运算符
5.3 边界条件处理
- 空字符串处理(包括默认构造)
- 插入/删除操作的边界检查
- 拷贝自赋值的情况处理
- 内存分配失败的处理
在实际项目中实现string类时,除了上述基本功能外,还需要考虑更多细节:
- 异常安全保证
- 迭代器失效规则
- 与其他字符串类型的互操作
- 国际化支持(宽字符等)
理解这些底层实现细节,不仅能帮助更好地使用标准库,也能提升对C++内存管理和类设计的理解。对于需要高性能字符串处理的场景,可以考虑进一步优化实现,如添加小字符串优化(SSO)、引用计数等高级特性。