深入剖析自定义 string 类的实现细节
这是一个完全从头实现的 C++ string 类,位于yyq命名空间下,避免了与标准库的命名冲突。该实现涵盖了字符串处理的核心功能,包括动态内存管理、深拷贝、迭代器支持、运算符重载和流操作等。cpp-1赋给无符号size_t会变成最大可能值这是模拟标准库的行为RAII 原则:构造函数获取资源,析构函数释放资源深拷贝实现:正确处理拷贝和赋值现代 C++ 技巧:使用swap实现拷贝控制和异常安全智能扩
目录
11.7 test_string7 和 test_string8:深拷贝和交换测试
一、概述与设计理念
这是一个完全从头实现的 C++ string 类,位于 yyq 命名空间下,避免了与标准库 std::string 的命名冲突。该实现涵盖了字符串处理的核心功能,包括动态内存管理、深拷贝、迭代器支持、运算符重载和流操作等。
二、核心数据结构
2.1 类的成员变量
cpp
private:
char* _str = nullptr; // 指向动态分配的字符数组
size_t _size = 0; // 当前字符串长度(不包含结尾的\0)
size_t _capacity = 0; // 当前分配的容量
static const size_t npos; // 特殊值,表示未找到
设计分析:
-
使用动态分配的字符数组存储字符串内容
-
_size表示实际字符数,_capacity表示分配的容量 -
npos是静态常量,模仿标准库设计,表示查找失败的特殊值
2.2 迭代器实现
cpp
public:
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两个版本,支持常量和非常量迭代 -
实现了
begin()和end()方法,支持范围 for 循环
三、构造函数与析构函数
3.1 默认构造函数
cpp
string()
:_str(new char[1] {'\0'})
,_size(0)
,_capacity(0)
{}
实现细节:
-
分配 1 字节内存,初始化为
\0 -
_size和_capacity都设为 0 -
注意:虽然
_capacity为 0,但实际分配了 1 字节内存,这可能导致概念上的混淆
3.2 C 字符串构造函数
cpp
string(const char* str)
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
设计选择:
-
这是单参数构造函数,支持隐式类型转换
-
精确分配所需内存,没有预留额外空间
-
使用
strcpy复制字符串,包括结尾的\0
3.3 注释中的合并构造函数
cpp
//string(const char* str = "")
//{
// _size = strlen(str);
// _capacity = _size;
// _str = new char[_capacity + 1];
// strcpy(_str, str);
//}
为什么没有采用:
-
将默认构造函数和 C 字符串构造函数合并为一个
-
使用空字符串
""作为默认参数 -
虽然代码更简洁,但可能降低可读性,作者选择分开实现
3.4 拷贝构造函数(现代写法)
cpp
string(const string& s)
{
string temp(s._str);
swap(temp);
}
实现原理:
-
先通过 C 字符串构造函数创建临时对象
temp -
然后与当前对象交换资源
-
临时对象在函数结束时析构,释放原资源
对比传统写法:
cpp
// 传统写法
string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
现代写法的优势:
-
代码更简洁
-
利用已有构造函数,减少重复代码
-
通过
swap实现,异常安全
3.5 析构函数
cpp
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}
安全考虑:
-
检查
_str是否为nullptr,避免对空指针调用delete[] -
释放后将指针置空,防止悬空指针
-
重置
_size和_capacity,确保对象状态一致
四、赋值运算符与资源管理
4.1 现代写法的赋值运算符
cpp
string& operator=(string temp)
{
swap(temp);
return *this;
}
工作原理:
-
参数
temp通过值传递,会调用拷贝构造函数创建副本 -
交换当前对象与副本的资源
-
副本在函数结束时析构,释放原对象的资源
优势分析:
-
自动处理自赋值情况(
s = s) -
代码简洁,利用拷贝构造函数和
swap -
异常安全:如果拷贝构造失败,原对象状态不变
4.2 swap 成员函数
cpp
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
实现细节:
-
使用
std::swap交换三个成员变量 -
std::swap对于内置类型是高效的 -
这个函数是拷贝和赋值操作的核心
4.3 注释中的传统赋值实现
cpp
//string& operator=(const string& s)
//{
// if (this != &s)
// {
// delete[] _str;
// _str = new char[s._capacity + 1];
// strcpy(_str, s._str);
// _size = s._size;
// _capacity = s._capacity;
// }
// return *this;
//}
传统写法的问题:
-
需要显式检查自赋值
-
如果
new失败抛出异常,原对象的内存已被释放,对象处于无效状态 -
代码较冗长
五、容量管理与扩容策略
5.1 reserve 函数
cpp
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* temp = new char[n + 1]; // +1 给 \0 预留空间
strcpy(temp, _str);
delete[] _str;
_str = temp;
_capacity = n;
}
}
特点:
-
只扩大容量,不缩小
-
使用
strcpy复制原字符串(包括\0) -
新容量为
n,实际分配n+1字节
5.2 push_back 的扩容策略
cpp
void string::push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
扩容规则:
-
初始容量为 0 时,扩容到 4
-
后续每次容量不足时,容量翻倍
-
这种指数级扩容策略均摊时间复杂度为 O(1)
5.3 append 的智能扩容
cpp
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
// 需要的大小超过2倍容量时,按需扩容;否则按2倍扩
reserve(len + _size > 2 * _capacity ? len + _size : 2 * _capacity);
}
strcpy(_str + _size, str);
_size += len;
}
智能策略:
-
如果要添加的内容很大(超过当前容量的 2 倍),按实际需要扩容
-
否则,按 2 倍扩容
-
平衡了内存使用和扩容次数
六、插入操作与边界处理
6.1 插入单个字符
cpp
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
}
边界处理:
-
使用
assert(pos <= _size)确保位置合法 -
如果
pos == _size,相当于在末尾插入 -
从后向前移动字符,避免覆盖未移动的数据
6.2 插入字符串的复杂情况
cpp
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(len + _size > 2 * _capacity ? len + _size : 2 * _capacity);
}
size_t end = _size + len;
while (end > len + pos - 1)
{
_str[end] = _str[end - len];
--end;
}
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;
}
关键技巧:
-
循环条件
while (end > len + pos - 1)避免了pos-1在pos=0时下溢 -
先移动原字符串内容,再插入新字符串
-
处理了空字符串的情况(虽然注释中检查
len==0的代码被注释掉了)
6.3 注释中的问题代码
cpp
// 方法一:有问题的实现
//int end = _size;
//while (end >= (int)pos)
//{
// _str[end + 1] = _str[end];
// --end;
//}
问题分析:
-
end是int类型,pos是size_t类型 -
比较时
int会被提升为无符号类型 -
当
pos=0,end=-1时,-1被提升为很大的无符号数,循环条件不满足
七、删除操作实现
7.1 erase 函数
cpp
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
return;
}
size_t end = pos + len;
while (end <= _size)
{
_str[end - len] = _str[end];
end++;
}
_size -= len;
}
两种情况的处理:
-
删除到末尾:直接截断字符串
-
删除中间部分:向前移动后续字符
注意:
-
循环条件
end <= _size确保拷贝结尾的\0 -
使用
assert(pos < _size),不允许在pos == _size处删除
7.2 注释中的替代实现
cpp
//if (len >= _size - pos)
//{
// _str[pos] = '\0';
// _size = pos;
//}
//else
//{
// for (size_t i = pos + len; i <= _size; i++)
// {
// _str[i - len] = _str[i];
// }
// _size -= len;
//}
对比分析:
-
逻辑基本相同,只是结构略有差异
-
当前实现提前返回,可能更清晰
-
for循环版本更紧凑
八、查找与子串操作
8.1 查找字符
cpp
size_t string::find(char ch, size_t pos)
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
简单实现:
-
线性查找,时间复杂度 O(n)
-
使用
assert(pos < _size)确保起始位置有效 -
找不到时返回
npos
8.2 查找子串
cpp
size_t string::find(const char* str, size_t pos)
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
利用标准库:
-
使用 C 标准库的
strstr函数 -
ptr - _str计算偏移量(指针算术) -
注意:需要确保
_str以\0结尾
8.3 获取子串
cpp
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
if (len > _size - pos)
{
len = _size - pos;
}
string str;
str.reserve(len);
for (size_t i = 0; i < len; i++)
{
str += _str[pos + i];
}
return str;
}
实现细节:
-
调整
len确保不超过字符串边界 -
先
reserve预留空间,减少扩容次数 -
逐个字符添加,确保正确性
-
传值返回,可能触发拷贝构造(编译器可能优化)
九、比较运算符实现
9.1 基本比较运算符
cpp
bool operator < (const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) < 0;
}
为什么是全局函数?
-
全局函数支持隐式类型转换:
"hello" < s和s < "hello"都有效 -
不能直接访问私有成员,必须使用
c_str()公共接口
9.2 运算符复用策略
cpp
bool operator <= (const string& s1, const string& s2)
{
return s1 < s2 || s1 == s2;
}
bool operator > (const string& s1, const string& s2)
{
return !(s1 <= s2);
}
设计理念:
-
只实现
operator<和operator== -
其他运算符通过这两个复用
-
修改比较逻辑时只需改两个地方
9.3 相等性判断
cpp
bool operator == (const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator != (const string& s1, const string& s2)
{
return !(s1 == s2);
}
注意:
-
"hello world" == "hello world"比较的是指针地址,不是字符串内容 -
只有当
const char*隐式转换为string时,才会调用重载的operator==
十、流操作符实现
10.1 输出运算符
cpp
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
简洁实现:
-
使用范围 for 循环遍历字符串
-
逐个字符输出到流
-
返回流引用以支持链式调用
10.2 输入运算符(优化前)
cpp
// 原始版本(频繁扩容)
//istream& operator>>(istream& in, string& s)
//{
// s.clear();
// char ch;
// ch = in.get();
// while (ch != ' ' && ch != '\n')
// {
// s += ch;
// ch = in.get();
// }
// return in;
//}
问题:
-
每次读取一个字符就追加,可能频繁扩容
-
性能较差,特别是读取长字符串时
10.3 输入运算符(优化后)
cpp
istream& operator>>(istream& in, string& s)
{
s.clear();
const int N = 256;
char buff[N];
int i = 0;
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == N - 1)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
优化策略:
-
使用 256 字节缓冲区暂存输入
-
缓冲区快满时批量追加到字符串
-
大大减少扩容次数,提高性能
10.4 getline 函数
cpp
istream& getline(istream& in, string& s)
{
s.clear();
const int N = 256;
char buff[N];
int i = 0;
char ch = in.get();
while (ch != '\n')
{
buff[i++] = ch;
if (i == N - 1)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
与 operator>> 的区别:
-
只以换行符
\n为分隔符 -
可以读取包含空格的整行输入
-
实现逻辑与
operator>>类似
十一、测试代码分析
11.1 test_string1:基础功能测试
测试构造函数、下标访问、迭代器和范围 for 循环。
11.2 test_string2:插入功能测试
测试 += 运算符和 insert 函数的各种情况。
11.3 test_string3:删除功能测试
测试 erase 函数的边界情况,包括删除到末尾和删除部分字符。
11.4 test_string4:拷贝和子串测试
测试拷贝构造、赋值运算符、子串提取和自赋值。
11.5 test_string5:比较和流操作测试
测试比较运算符、输入输出运算符和隐式类型转换。
11.6 test_string6:getline 测试
测试整行输入功能。
11.7 test_string7 和 test_string8:深拷贝和交换测试
测试深拷贝的正确性和 swap 功能。
十二、静态成员的特殊性
12.1 npos 的定义
cpp
const size_t string::npos = -1;
类型转换:
-
-1赋给无符号size_t会变成最大可能值 -
这是模拟标准库
std::string::npos的行为
12.2 静态常量初始化规则
cpp
// 在类内:static const size_t npos; // 在类外:const size_t string::npos = -1;
C++ 规则:
-
静态成员需要在类外单独定义和初始化
-
只有静态整型常量可以在类内初始化(特殊情况)
-
例如:
static const int N = 10;可以在类内初始化
十三、设计总结与优化建议
13.1 设计亮点
-
RAII 原则:构造函数获取资源,析构函数释放资源
-
深拷贝实现:正确处理拷贝和赋值
-
现代 C++ 技巧:使用
swap实现拷贝控制和异常安全 -
智能扩容策略:平衡内存使用和性能
-
完整迭代器支持:兼容 STL 算法和范围 for 循环
13.2 潜在改进
-
小字符串优化:对于短字符串,使用栈存储避免堆分配
-
移动语义:添加移动构造函数和移动赋值运算符
-
异常安全增强:某些操作可能不是强异常安全的
-
性能优化:
substr可以一次性拷贝而不是逐个字符添加 -
Unicode 支持:当前只支持单字节字符
这个实现作为教学项目,完整展示了 string 类的核心原理,对于理解 C++ 的类设计、内存管理和运算符重载有重要价值。
更多推荐


所有评论(0)