C++ string 实现详解
文章目录
- [C++ string 实现详解](#C++ string 实现详解)
-
- 一、类设计概览
-
- [1.1 成员变量](#1.1 成员变量)
- [1.2 迭代器类型定义](#1.2 迭代器类型定义)
- [1.3 npos 静态成员](#1.3 npos 静态成员)
- 二、构造与析构
-
- [2.1 默认构造的演进:从两个构造函数到一个](#2.1 默认构造的演进:从两个构造函数到一个)
- [2.2 拷贝构造(深拷贝)](#2.2 拷贝构造(深拷贝))
- [2.3 析构](#2.3 析构)
- 三、三种遍历实现
-
- [3.1 operator\[\] 下标遍历](#3.1 operator[] 下标遍历)
- [3.2 迭代器遍历](#3.2 迭代器遍历)
- [3.3 范围 for 遍历](#3.3 范围 for 遍历)
- [四、const 迭代器](#四、const 迭代器)
-
- [4.1 定义](#4.1 定义)
- [4.2 const 对象与迭代器](#4.2 const 对象与迭代器)
- [4.3 权限缩小原则](#4.3 权限缩小原则)
- 五、容量管理
-
- [5.1 reserve 实现](#5.1 reserve 实现)
- [5.2 扩容策略](#5.2 扩容策略)
- [5.3 push_back 和 append 的委派设计](#5.3 push_back 和 append 的委派设计)
- 六、修改操作实现
-
- [6.1 push_back / append / operator+=](#6.1 push_back / append / operator+=)
- [6.2 insert 字符------无符号整数越界的巨大坑](#6.2 insert 字符——无符号整数越界的巨大坑)
- [6.3 insert 字符串](#6.3 insert 字符串)
- [6.4 erase](#6.4 erase)
- 七、查找
-
- [7.1 find 字符------暴力遍历](#7.1 find 字符——暴力遍历)
- [7.2 find 子串------strstr + 指针相减](#7.2 find 子串——strstr + 指针相减)
- 八、深浅拷贝与拷贝控制
-
- [8.1 浅拷贝的问题](#8.1 浅拷贝的问题)
- [8.2 深拷贝的拷贝构造](#8.2 深拷贝的拷贝构造)
- [8.3 赋值运算符的三种情况](#8.3 赋值运算符的三种情况)
- [8.4 现代写法 vs 传统写法](#8.4 现代写法 vs 传统写法)
- [九、swap 优化](#九、swap 优化)
-
- [9.1 std::swap 的三次深拷贝问题](#9.1 std::swap 的三次深拷贝问题)
- [9.2 成员 swap------只交换指针](#9.2 成员 swap——只交换指针)
- [9.3 全局 swap 特化与匹配优先级](#9.3 全局 swap 特化与匹配优先级)
- [十、substr 实现](#十、substr 实现)
- 十一、比较运算符
-
- [11.1 核心实现:只写 < 和 ==](#11.1 核心实现:只写 < 和 ==)
- [11.2 其余运算符的复用模式](#11.2 其余运算符的复用模式)
- 十二、流运算符
-
- [12.1 operator<<](#12.1 operator<<)
- [12.2 operator>>](#12.2 operator>>)
- [十三、clear 实现](#十三、clear 实现)
- 十四、写实拷贝(COW)与引用计数
-
- [14.1 动机:深拷贝的代价](#14.1 动机:深拷贝的代价)
- [14.2 设计原理(了解)](#14.2 设计原理(了解))
- [14.3 COW 机制的关键函数](#14.3 COW 机制的关键函数)
- [14.4 验证方法](#14.4 验证方法)
- [14.5 优缺点分析](#14.5 优缺点分析)
- [14.6 C++11 移动语义的冲击](#14.6 C++11 移动语义的冲击)
- [14.7 扩展阅读](#14.7 扩展阅读)
- [十五、输入优化 ------ getline 与 buffer 缓冲](#十五、输入优化 —— getline 与 buffer 缓冲)
-
- [15.1 朴素实现的扩容问题](#15.1 朴素实现的扩容问题)
- [15.2 buffer 优化方案](#15.2 buffer 优化方案)
- [15.3 优化效果](#15.3 优化效果)
- [15.4 设计取舍](#15.4 设计取舍)
- 结语
上篇文章我们把 string 的用法完整过了一遍:怎么构造、三种遍历和迭代器、reserve 与 resize 的区别、insert 和 erase 的效率坑、find 与 substr 的配合,还有编码体系和 getline 输入,解决的是"怎么把 string 用好"的问题。
这篇解决的是"string 自己是怎么实现的":跟着教学级的简化实现 my_str::string,把构造析构、扩容策略、数据挪动、深浅拷贝与赋值、swap 优化、比较与流运算符,再到写时拷贝 COW 和 getline 的缓冲优化,一行一行拆开讲,让你从"会用"走到"懂它"。
本文侧重 string 的底层实现原理、坑点分析和设计考量,适用于进阶学习和面试准备。相对「C++ STL string 详解」(侧重使用指南),本文聚焦每一行代码背后的为什么。
一、类设计概览
1.1 成员变量
cpp
namespace my_str
{
class string
{
// ...
private:
char* _str; // 指向堆上字符数组
size_t _size; // 有效字符个数(不含末尾 '\0')
size_t _capacity; // 容量(不含 '\0',实际开辟空间为 _capacity + 1)
};
}
| 成员 | 含义 | 注意事项 |
|---|---|---|
_str |
指向堆区动态数组的指针 | 始终保证末尾有 '\0' |
_size |
有效字符个数 | strlen(_str) 应等于 _size |
_capacity |
可容纳的字符数(不含 '\0') |
实际开辟 _capacity + 1 字节 |
为什么 _capacity 不含 '\0'? 这是约定俗称的设计:_capacity 表示用户可见的容量,而末尾的 '\0' 是实现细节,不占用用户视角的容量额度。
1.2 迭代器类型定义
cpp
typedef char* iterator;
typedef const char* const_iterator;
string 的迭代器就是裸指针 。因为 string 管理的内存是连续的字符数组,原生指针天然满足迭代器的所有要求(++、--、* 解引用、+n 偏移)。同理,vector<T> 的迭代器通常也是 T*------所有底层连续存储 的容器(string、vector),其迭代器都可以直接用原生指针实现。而链表、树等非连续结构的迭代器则必须用类封装来重载 ++ 等运算符。
1.3 npos 静态成员
cpp
// string.h 中声明
const static size_t npos;
// string.cpp 中定义
const size_t string::npos = -1;
npos 被定义为 -1,但由于类型是 size_t(无符号),-1 被转换为 size_t 的最大值(0xFFFFFFFF 或 0xFFFFFFFFFFFFFFFF),表示"直到末尾"的特殊语义。它是 erase、find、substr 等函数的默认参数值。
坑:
const static成员变量在类内初始化有类型限制------只有整型、枚举、constexpr字面量类型可以在类内直接初始化。浮点类型(如const static double N = 2.2)不支持类内初始化(C++17 前),必须在类外定义。正因如此,npos 在类内只是声明,在 .cpp 中给出定义。
二、构造与析构
2.1 默认构造的演进:从两个构造函数到一个
初版:无参构造单独写(已注释掉)
cpp
// string::string()
// {
// _str = new char[1]{'\0'};
// _size = 0;
// _capacity = 0;
// }
这段初版代码看似没有问题:开辟 1 字节存放 '\0',然后 _size 和 _capacity 都置 0。但它与带参构造是重复劳动。
终版:全缺省构造函数
cpp
string(const char* str = "");
将无参构造和带参构造合并为一个全缺省构造函数。当用户不传参时,str 默认为空字符串 ""(即只含 '\0' 的常量字符串),实现体只需处理一种情况:
cpp
string::string(const char* str)
: _size(strlen(str)) // 初始化列表:先计算长度
{
_str = new char[_size + 1]; // 开空间(+1 给 '\0')
_capacity = _size; // 容量等于长度
strcpy(_str, str); // 拷贝数据
}
为什么 _size 在初始化列表算,而 _str 在函数体 new? 因为 _size 可以直接用 strlen 结果赋值,不涉及复杂的控制流;而 _str 的 new 操作依赖于 _size 已经算好,同时 new 可能抛异常,放在函数体中更适合处理资源分配逻辑。
默认构造的行为: str = "",strlen("") = 0,所以 _size = 0,_capacity = 0,new char[1] 存 '\0'。一切合理。
2.2 拷贝构造(深拷贝)
cpp
// s2(s1)
string::string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
拷贝构造必须深拷贝:为 s2 开辟独立的内存空间,然后拷贝数据。如果使用浅拷贝(直接复制 _str 指针),两个对象会指向同一块堆内存,析构时导致 double free,修改一个也会影响另一个。详见第八章。
2.3 析构
cpp
string::~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
释放堆内存后将指针置空,并将 _size 和 _capacity 归零。虽然析构后对象生命周期已结束,但这些操作在 debug 阶段有助于快速发现野指针访问。
三、三种遍历实现
3.1 operator\[\] 下标遍历
非 const 版本:
cpp
char& string::operator[](size_t pos)
{
assert(pos < _size); // 越界直接断言
return _str[pos];
}
返回 char& 有两层含义:
- 可以读 :
char c = s[0]; - 可以写 :
s[0] = 'A';
const 版本:
cpp
const char& string::operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
返回 const char&,const 对象只能通过此版本访问,不可修改。
越界检查: assert(pos < _size) 在 debug 模式下捕获越界,release 模式下 assert 消失,行为同原生数组一样不可预测。标准库 at() 则通过抛异常提供运行时检查。
3.2 迭代器遍历
cpp
iterator string::begin() { return _str; }
iterator string::end() { return _str + _size; }
string 的迭代器就是原生指针,所以 begin() 直接返回首元素地址,end() 返回最后一个有效数据的下一个位置(即 '\0' 所在位置)。
使用示例:
cpp
my_str::string::iterator it = s.begin();
while (it != s.end())
{
cout << *it;
++it;
}
3.3 范围 for 遍历
cpp
for (auto ch : s)
{
cout << ch;
}
范围 for 的底层原理: 编译器将其替换为迭代器形式:
cpp
auto __begin = s.begin();
auto __end = s.end();
for (; __begin != __end; ++__begin)
{
auto ch = *__begin;
cout << ch;
}
因此只要类提供了 begin() 和 end() 成员函数,范围 for 就自动可用。
四、const 迭代器
4.1 定义
cpp
typedef const char* const_iterator;
const_iterator string::begin() const { return _str; }
const_iterator string::end() const { return _str + _size; }
4.2 const 对象与迭代器
当 string 对象被 const 修饰时,只能调用 const 版本的 begin()/end(),返回 const_iterator(即 const char*),指向的内容不能被修改:
cpp
void print(const my_str::string& s)
{
// s 是 const,只能调用 const 版本的 begin/end
my_str::string::const_iterator it = s.begin();
while (it != s.end())
{
// *it = 'A'; // 编译错误!const_iterator 不可写
cout << *it;
++it;
}
}
4.3 权限缩小原则
| 对象类型 | 可调用版本 | 返回类型 | 是否可修改 |
|---|---|---|---|
| 非 const 对象 | begin() |
iterator (char*) |
可以 |
| 非 const 对象 | begin() const |
const_iterator (const char*) |
不可以(权限缩小,允许) |
| const 对象 | begin() const |
const_iterator |
不可以(权限不变) |
| const 对象 | begin() |
iterator |
编译错误(权限放大,禁止) |
非 const 对象可以调用 const 版本的成员函数(读操作总是安全的),但 const 对象不能调用非 const 版本。这本质上是权限缩小 vs 权限放大 的规则:指针/引用可以自然地从 T* 转换为 const T*(缩小可以),反过来不行。
五、容量管理
5.1 reserve 实现
cpp
void string::reserve(size_t n)
{
if (n > _capacity) // 只扩容,不缩容
{
char* tmp = new char[n + 1];
strcpy(tmp, _str); // 拷贝旧数据
delete[] _str; // 释放旧空间
_str = tmp;
_capacity = n;
}
}
核心设计: reserve 只扩容不缩容 。当请求的 n 大于当前 _capacity 时才做操作,否则什么都不做。这是符合 C++ 标准的设计------reserve 只负责预留空间,不负责任何缩减操作(shrink_to_fit 是另一回事)。
为什么是 strcpy 而不是 memcpy? 因为 strcpy 会一直复制到遇到 '\0' 为止,正好复制完有效数据。但这里的场景用 memcpy(tmp, _str, _size + 1) 更明确------两种都可行,strcpy 写起来更短。
5.2 扩容策略
cpp
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
| 当前容量 | 扩容后 | 说明 |
|---|---|---|
| 0 | 4 | 空字符串首次插入,起步给 4 |
| 4 | 8 | 2 倍扩容 |
| 8 | 16 | 2 倍扩容 |
但 append 时不能简单 2 倍:
cpp
// append 的正确扩容判断
if (_size + len > _capacity)
{
reserve(_size + len); // 直接扩到刚好够用的容量
}
这是为何?假设 _capacity = 4,_size = 4,要 append 100 个字符 ------ 如果走 2 倍策略,需要扩容 5 次(4→8→16→32→64→128),而 reserve(_size + len) = reserve(104) 一步到位。时间换空间的选择:
| 场景 | 扩容策略 | 理由 |
|---|---|---|
push_back(单字符) |
2 倍扩容 | 少量追加,均摊 O(1) |
append(批量字符串) |
精确扩容 | 避免多次扩容,适合已知长度 |
5.3 push_back 和 append 的委派设计
实际的 push_back 和 append 内部都委派给 insert:
cpp
void string::push_back(char ch)
{
insert(_size, ch); // 在末尾插入一个字符
}
void string::append(const char* str)
{
insert(_size, str); // 在末尾插入字符串
}
这减少了代码重复。虽然性能上多了一层函数调用,但教学实现以简洁优先。
六、修改操作实现
6.1 push_back / append / operator+=
cpp
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
operator+= 返回 *this 的引用,支持链式调用:s += 'a' += 'b';。
6.2 insert 字符------无符号整数越界的巨大坑
cpp
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
// 1. 检查容量,不够则扩容
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
// 2. 从后往前挪动元素
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
// 3. 插入字符
_str[pos] = ch;
++_size;
}
无符号溢出的惨痛教训
初版代码的错误写法(见注释):
cpp
/* 错误写法 */
size_t end = _size;
while (end >= pos) // 当 pos == 0 时,end-- 到 0 后,下一轮 end-- 变成 SIZE_MAX
{
_str[end + 1] = _str[end];
--end;
}
当 pos = 0(头插)时,循环的终局分析:
| 轮次 | end 值 | end >= 0? | 操作后 end-- |
|---|---|---|---|
| 1 | _size | true | _size - 1 |
| ... | ... | true | ... |
| n | 1 | true | 0 |
| n+1 | 0 | true(0 >= 0) | 0 - 1 = 0xFFFFFFFF |
| n+2 | 0xFFFFFFFF | true | ...死循环或越界写入 |
错误本质: size_t 是无符号类型,0 - 1 得到的是 SIZE_MAX(42 亿或 18 亿亿),不是 -1。因此 end >= 0 永远为 true。
两种修正方案
方案一(强制类型转换):
cpp
int end = _size;
while (end >= (int)pos) // 将 pos 强转为 int,让 -1 有意义
{
_str[end + 1] = _str[end];
--end;
}
方案二(修改循环结构,最终采用的方案):
cpp
size_t end = _size + 1;
while (end > pos) // 用 > 代替 >=,避免了 end == 0 时的越界
{
_str[end] = _str[end - 1];
--end;
}
最终采用方案二,因为它避免了强制类型转换,逻辑更干净。
为什么从后往前挪?
#mermaid-svg-OrBKBAL8ee6UPmOW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OrBKBAL8ee6UPmOW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OrBKBAL8ee6UPmOW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OrBKBAL8ee6UPmOW .error-icon{fill:#552222;}#mermaid-svg-OrBKBAL8ee6UPmOW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OrBKBAL8ee6UPmOW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OrBKBAL8ee6UPmOW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OrBKBAL8ee6UPmOW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OrBKBAL8ee6UPmOW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OrBKBAL8ee6UPmOW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OrBKBAL8ee6UPmOW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OrBKBAL8ee6UPmOW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OrBKBAL8ee6UPmOW .marker.cross{stroke:#333333;}#mermaid-svg-OrBKBAL8ee6UPmOW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OrBKBAL8ee6UPmOW p{margin:0;}#mermaid-svg-OrBKBAL8ee6UPmOW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OrBKBAL8ee6UPmOW .cluster-label text{fill:#333;}#mermaid-svg-OrBKBAL8ee6UPmOW .cluster-label span{color:#333;}#mermaid-svg-OrBKBAL8ee6UPmOW .cluster-label span p{background-color:transparent;}#mermaid-svg-OrBKBAL8ee6UPmOW .label text,#mermaid-svg-OrBKBAL8ee6UPmOW span{fill:#333;color:#333;}#mermaid-svg-OrBKBAL8ee6UPmOW .node rect,#mermaid-svg-OrBKBAL8ee6UPmOW .node circle,#mermaid-svg-OrBKBAL8ee6UPmOW .node ellipse,#mermaid-svg-OrBKBAL8ee6UPmOW .node polygon,#mermaid-svg-OrBKBAL8ee6UPmOW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OrBKBAL8ee6UPmOW .rough-node .label text,#mermaid-svg-OrBKBAL8ee6UPmOW .node .label text,#mermaid-svg-OrBKBAL8ee6UPmOW .image-shape .label,#mermaid-svg-OrBKBAL8ee6UPmOW .icon-shape .label{text-anchor:middle;}#mermaid-svg-OrBKBAL8ee6UPmOW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OrBKBAL8ee6UPmOW .rough-node .label,#mermaid-svg-OrBKBAL8ee6UPmOW .node .label,#mermaid-svg-OrBKBAL8ee6UPmOW .image-shape .label,#mermaid-svg-OrBKBAL8ee6UPmOW .icon-shape .label{text-align:center;}#mermaid-svg-OrBKBAL8ee6UPmOW .node.clickable{cursor:pointer;}#mermaid-svg-OrBKBAL8ee6UPmOW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OrBKBAL8ee6UPmOW .arrowheadPath{fill:#333333;}#mermaid-svg-OrBKBAL8ee6UPmOW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OrBKBAL8ee6UPmOW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OrBKBAL8ee6UPmOW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OrBKBAL8ee6UPmOW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OrBKBAL8ee6UPmOW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OrBKBAL8ee6UPmOW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OrBKBAL8ee6UPmOW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OrBKBAL8ee6UPmOW .cluster text{fill:#333;}#mermaid-svg-OrBKBAL8ee6UPmOW .cluster span{color:#333;}#mermaid-svg-OrBKBAL8ee6UPmOW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OrBKBAL8ee6UPmOW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OrBKBAL8ee6UPmOW rect.text{fill:none;stroke-width:0;}#mermaid-svg-OrBKBAL8ee6UPmOW .icon-shape,#mermaid-svg-OrBKBAL8ee6UPmOW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OrBKBAL8ee6UPmOW .icon-shape p,#mermaid-svg-OrBKBAL8ee6UPmOW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OrBKBAL8ee6UPmOW .icon-shape .label rect,#mermaid-svg-OrBKBAL8ee6UPmOW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OrBKBAL8ee6UPmOW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OrBKBAL8ee6UPmOW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OrBKBAL8ee6UPmOW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 插入后 (pos=2, 插入 x)
h
e
x
l
l
o
\\0
插入前
h
e
l
l
o
\\0
从 _size + 1(即 '\0' 的下一个位置)开始,从后往前逐个往后挪,不会覆盖还未挪动的数据。若从前往后挪,则会将未挪的数据覆盖掉。
注意: 不需要单独处理 '\0'。由于挪动从 _size + 1 开始,'\0' 会自然地被往后挪一位。
6.3 insert 字符串
cpp
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
// 从后往前挪动 len 个位置
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
memcpy(_str + pos, str, len);
_size += len;
}
与 insert 字符相同的从后往前挪动思路,但一次挪 len 步。最终用 memcpy 批量拷贝插入的字符串。
头插的极端情况: pos = 0 时,所有数据后移 len 位,然后从开头拷贝。此时 end = _size + len,循环从末尾开始,不会出现无符号越界问题(因为 end > pos + len - 1 = len - 1,只要 _size > 0,end 初始值就大于 len - 1)。
6.4 erase
cpp
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
// 从 pos 到末尾全部删除
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else // 部分删除
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
两种情况:
| 条件 | 含义 | 操作 |
|---|---|---|
len >= _size - pos |
要求删除的长度大于等于剩余字符数 | 统一视为"删除到末尾",在 pos 处放 '\0',更新 _size = pos |
len < _size - pos |
部分删除 | 用 strcpy 将后面的数据往前覆盖 |
为什么不单独处理 '\0'? strcpy 会自动拷贝源字符串的 '\0',所以覆盖后的字符串末尾天然有终结符。
七、查找
7.1 find 字符------暴力遍历
cpp
size_t string::find(char ch, size_t pos)
{
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
return i;
}
return npos;
}
从 pos 位置开始逐个字符比较。找到返回下标,找不到返回 npos。
7.2 find 子串------strstr + 指针相减
cpp
size_t string::find(const char* sub, size_t pos)
{
char* p = strstr(_str + pos, sub);
return p - _str; // 指针相减得到下标
}
这里巧妙利用了 C 库函数 strstr 完成子串匹配。strstr 返回子串首次出现的指针,用该指针减去 _str 首地址就得到了下标。
#mermaid-svg-au1M0nVeGJIfkgPd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-au1M0nVeGJIfkgPd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-au1M0nVeGJIfkgPd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-au1M0nVeGJIfkgPd .error-icon{fill:#552222;}#mermaid-svg-au1M0nVeGJIfkgPd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-au1M0nVeGJIfkgPd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-au1M0nVeGJIfkgPd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-au1M0nVeGJIfkgPd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-au1M0nVeGJIfkgPd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-au1M0nVeGJIfkgPd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-au1M0nVeGJIfkgPd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-au1M0nVeGJIfkgPd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-au1M0nVeGJIfkgPd .marker.cross{stroke:#333333;}#mermaid-svg-au1M0nVeGJIfkgPd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-au1M0nVeGJIfkgPd p{margin:0;}#mermaid-svg-au1M0nVeGJIfkgPd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-au1M0nVeGJIfkgPd .cluster-label text{fill:#333;}#mermaid-svg-au1M0nVeGJIfkgPd .cluster-label span{color:#333;}#mermaid-svg-au1M0nVeGJIfkgPd .cluster-label span p{background-color:transparent;}#mermaid-svg-au1M0nVeGJIfkgPd .label text,#mermaid-svg-au1M0nVeGJIfkgPd span{fill:#333;color:#333;}#mermaid-svg-au1M0nVeGJIfkgPd .node rect,#mermaid-svg-au1M0nVeGJIfkgPd .node circle,#mermaid-svg-au1M0nVeGJIfkgPd .node ellipse,#mermaid-svg-au1M0nVeGJIfkgPd .node polygon,#mermaid-svg-au1M0nVeGJIfkgPd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-au1M0nVeGJIfkgPd .rough-node .label text,#mermaid-svg-au1M0nVeGJIfkgPd .node .label text,#mermaid-svg-au1M0nVeGJIfkgPd .image-shape .label,#mermaid-svg-au1M0nVeGJIfkgPd .icon-shape .label{text-anchor:middle;}#mermaid-svg-au1M0nVeGJIfkgPd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-au1M0nVeGJIfkgPd .rough-node .label,#mermaid-svg-au1M0nVeGJIfkgPd .node .label,#mermaid-svg-au1M0nVeGJIfkgPd .image-shape .label,#mermaid-svg-au1M0nVeGJIfkgPd .icon-shape .label{text-align:center;}#mermaid-svg-au1M0nVeGJIfkgPd .node.clickable{cursor:pointer;}#mermaid-svg-au1M0nVeGJIfkgPd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-au1M0nVeGJIfkgPd .arrowheadPath{fill:#333333;}#mermaid-svg-au1M0nVeGJIfkgPd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-au1M0nVeGJIfkgPd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-au1M0nVeGJIfkgPd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-au1M0nVeGJIfkgPd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-au1M0nVeGJIfkgPd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-au1M0nVeGJIfkgPd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-au1M0nVeGJIfkgPd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-au1M0nVeGJIfkgPd .cluster text{fill:#333;}#mermaid-svg-au1M0nVeGJIfkgPd .cluster span{color:#333;}#mermaid-svg-au1M0nVeGJIfkgPd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-au1M0nVeGJIfkgPd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-au1M0nVeGJIfkgPd rect.text{fill:none;stroke-width:0;}#mermaid-svg-au1M0nVeGJIfkgPd .icon-shape,#mermaid-svg-au1M0nVeGJIfkgPd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-au1M0nVeGJIfkgPd .icon-shape p,#mermaid-svg-au1M0nVeGJIfkgPd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-au1M0nVeGJIfkgPd .icon-shape .label rect,#mermaid-svg-au1M0nVeGJIfkgPd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-au1M0nVeGJIfkgPd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-au1M0nVeGJIfkgPd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-au1M0nVeGJIfkgPd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} _str: 'hello world'
strstr(_str, 'world')
p 指向 'world' 的 'w'
p - _str = 6
坑: 如果没找到,strstr 返回 nullptr,此时 nullptr - _str 是未定义行为。实际代码中应增加判空:
cpp
if (p == nullptr) return npos;
不过实现中未显式处理,因为 strstr 在参数非空的情况下总能返回有效值或 null,但严谨起见需要处理。
八、深浅拷贝与拷贝控制
8.1 浅拷贝的问题
当使用编译器默认生成的拷贝构造函数时:
#mermaid-svg-kjPjz62hZmSRdQqa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kjPjz62hZmSRdQqa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kjPjz62hZmSRdQqa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kjPjz62hZmSRdQqa .error-icon{fill:#552222;}#mermaid-svg-kjPjz62hZmSRdQqa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kjPjz62hZmSRdQqa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kjPjz62hZmSRdQqa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kjPjz62hZmSRdQqa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kjPjz62hZmSRdQqa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kjPjz62hZmSRdQqa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kjPjz62hZmSRdQqa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kjPjz62hZmSRdQqa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kjPjz62hZmSRdQqa .marker.cross{stroke:#333333;}#mermaid-svg-kjPjz62hZmSRdQqa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kjPjz62hZmSRdQqa p{margin:0;}#mermaid-svg-kjPjz62hZmSRdQqa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kjPjz62hZmSRdQqa .cluster-label text{fill:#333;}#mermaid-svg-kjPjz62hZmSRdQqa .cluster-label span{color:#333;}#mermaid-svg-kjPjz62hZmSRdQqa .cluster-label span p{background-color:transparent;}#mermaid-svg-kjPjz62hZmSRdQqa .label text,#mermaid-svg-kjPjz62hZmSRdQqa span{fill:#333;color:#333;}#mermaid-svg-kjPjz62hZmSRdQqa .node rect,#mermaid-svg-kjPjz62hZmSRdQqa .node circle,#mermaid-svg-kjPjz62hZmSRdQqa .node ellipse,#mermaid-svg-kjPjz62hZmSRdQqa .node polygon,#mermaid-svg-kjPjz62hZmSRdQqa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kjPjz62hZmSRdQqa .rough-node .label text,#mermaid-svg-kjPjz62hZmSRdQqa .node .label text,#mermaid-svg-kjPjz62hZmSRdQqa .image-shape .label,#mermaid-svg-kjPjz62hZmSRdQqa .icon-shape .label{text-anchor:middle;}#mermaid-svg-kjPjz62hZmSRdQqa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kjPjz62hZmSRdQqa .rough-node .label,#mermaid-svg-kjPjz62hZmSRdQqa .node .label,#mermaid-svg-kjPjz62hZmSRdQqa .image-shape .label,#mermaid-svg-kjPjz62hZmSRdQqa .icon-shape .label{text-align:center;}#mermaid-svg-kjPjz62hZmSRdQqa .node.clickable{cursor:pointer;}#mermaid-svg-kjPjz62hZmSRdQqa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kjPjz62hZmSRdQqa .arrowheadPath{fill:#333333;}#mermaid-svg-kjPjz62hZmSRdQqa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kjPjz62hZmSRdQqa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kjPjz62hZmSRdQqa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kjPjz62hZmSRdQqa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kjPjz62hZmSRdQqa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kjPjz62hZmSRdQqa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kjPjz62hZmSRdQqa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kjPjz62hZmSRdQqa .cluster text{fill:#333;}#mermaid-svg-kjPjz62hZmSRdQqa .cluster span{color:#333;}#mermaid-svg-kjPjz62hZmSRdQqa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kjPjz62hZmSRdQqa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kjPjz62hZmSRdQqa rect.text{fill:none;stroke-width:0;}#mermaid-svg-kjPjz62hZmSRdQqa .icon-shape,#mermaid-svg-kjPjz62hZmSRdQqa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kjPjz62hZmSRdQqa .icon-shape p,#mermaid-svg-kjPjz62hZmSRdQqa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kjPjz62hZmSRdQqa .icon-shape .label rect,#mermaid-svg-kjPjz62hZmSRdQqa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kjPjz62hZmSRdQqa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kjPjz62hZmSRdQqa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kjPjz62hZmSRdQqa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 浅拷贝 s2 = s1
s1._str
堆内存 (hello)
s2._str
两大问题:
- double free: s1 和 s2 析构时都会
delete[] _str,同一块内存释放两次,程序崩溃。 - 修改互相影响:
s2[0] = 'H'会同时改变 s1 的内容,违背了独立副本的语义。
cpp
my_str::string s1("hello");
my_str::string s2(s1); // 浅拷贝:s1._str == s2._str
// 问题 1: 修改互相影响
s2[0] = 'H'; // s1 也被改成了 "H ello"
// 问题 2: 析构时 double free
// s1 析构 delete[] _str 后,s2._str 变成野指针
// s2 析构再次 delete[],崩溃
8.2 深拷贝的拷贝构造
#mermaid-svg-L6EHQNzTRbQ6SDrM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-L6EHQNzTRbQ6SDrM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-L6EHQNzTRbQ6SDrM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-L6EHQNzTRbQ6SDrM .error-icon{fill:#552222;}#mermaid-svg-L6EHQNzTRbQ6SDrM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-L6EHQNzTRbQ6SDrM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-L6EHQNzTRbQ6SDrM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-L6EHQNzTRbQ6SDrM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-L6EHQNzTRbQ6SDrM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-L6EHQNzTRbQ6SDrM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-L6EHQNzTRbQ6SDrM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-L6EHQNzTRbQ6SDrM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-L6EHQNzTRbQ6SDrM .marker.cross{stroke:#333333;}#mermaid-svg-L6EHQNzTRbQ6SDrM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-L6EHQNzTRbQ6SDrM p{margin:0;}#mermaid-svg-L6EHQNzTRbQ6SDrM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-L6EHQNzTRbQ6SDrM .cluster-label text{fill:#333;}#mermaid-svg-L6EHQNzTRbQ6SDrM .cluster-label span{color:#333;}#mermaid-svg-L6EHQNzTRbQ6SDrM .cluster-label span p{background-color:transparent;}#mermaid-svg-L6EHQNzTRbQ6SDrM .label text,#mermaid-svg-L6EHQNzTRbQ6SDrM span{fill:#333;color:#333;}#mermaid-svg-L6EHQNzTRbQ6SDrM .node rect,#mermaid-svg-L6EHQNzTRbQ6SDrM .node circle,#mermaid-svg-L6EHQNzTRbQ6SDrM .node ellipse,#mermaid-svg-L6EHQNzTRbQ6SDrM .node polygon,#mermaid-svg-L6EHQNzTRbQ6SDrM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-L6EHQNzTRbQ6SDrM .rough-node .label text,#mermaid-svg-L6EHQNzTRbQ6SDrM .node .label text,#mermaid-svg-L6EHQNzTRbQ6SDrM .image-shape .label,#mermaid-svg-L6EHQNzTRbQ6SDrM .icon-shape .label{text-anchor:middle;}#mermaid-svg-L6EHQNzTRbQ6SDrM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-L6EHQNzTRbQ6SDrM .rough-node .label,#mermaid-svg-L6EHQNzTRbQ6SDrM .node .label,#mermaid-svg-L6EHQNzTRbQ6SDrM .image-shape .label,#mermaid-svg-L6EHQNzTRbQ6SDrM .icon-shape .label{text-align:center;}#mermaid-svg-L6EHQNzTRbQ6SDrM .node.clickable{cursor:pointer;}#mermaid-svg-L6EHQNzTRbQ6SDrM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-L6EHQNzTRbQ6SDrM .arrowheadPath{fill:#333333;}#mermaid-svg-L6EHQNzTRbQ6SDrM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-L6EHQNzTRbQ6SDrM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-L6EHQNzTRbQ6SDrM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-L6EHQNzTRbQ6SDrM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-L6EHQNzTRbQ6SDrM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-L6EHQNzTRbQ6SDrM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-L6EHQNzTRbQ6SDrM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-L6EHQNzTRbQ6SDrM .cluster text{fill:#333;}#mermaid-svg-L6EHQNzTRbQ6SDrM .cluster span{color:#333;}#mermaid-svg-L6EHQNzTRbQ6SDrM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-L6EHQNzTRbQ6SDrM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-L6EHQNzTRbQ6SDrM rect.text{fill:none;stroke-width:0;}#mermaid-svg-L6EHQNzTRbQ6SDrM .icon-shape,#mermaid-svg-L6EHQNzTRbQ6SDrM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-L6EHQNzTRbQ6SDrM .icon-shape p,#mermaid-svg-L6EHQNzTRbQ6SDrM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-L6EHQNzTRbQ6SDrM .icon-shape .label rect,#mermaid-svg-L6EHQNzTRbQ6SDrM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-L6EHQNzTRbQ6SDrM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-L6EHQNzTRbQ6SDrM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-L6EHQNzTRbQ6SDrM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 深拷贝 s2 = s1
s1._str
堆内存 A (hello)
s2._str
堆内存 B (hello) 独立副本
cpp
string::string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
每个对象都拥有独立的堆内存,互不影响。
8.3 赋值运算符的三种情况
cpp
string& string::operator=(const string& s)
{
if (this != &s) // 1. 自赋值判断
{
// 2. 先开新空间,再释放旧空间(异常安全)
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str; // 释放旧空间
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
赋值面临的三个场景:
| 场景 | 问题 | 解决方案 |
|---|---|---|
| 目标空间不足 | _capacity < s._size,拷贝到一半会越界 |
必须释放旧空间,开新空间 |
| 目标空间足够但过大 | _capacity >> s._size,浪费 |
可以复用,但目前实现统一处理 |
自赋值 s1 = s1 |
释放旧空间后把源数据也释放了 | if (this != &s) 判断 |
先 new 再 delete 的异常安全保证: 如果先 delete[] _str 再 new,而 new 抛出异常(内存不足),_str 已经处于释放状态,对象进入不可恢复的无效状态。先 new tmp,成功后再释放旧的,即使 new 失败抛出异常,原对象的数据仍然完好。
8.4 现代写法 vs 传统写法
核心思想: 直写版亲力亲为,自己开空间、拷数据;swap 版调用已有功能(构造函数/拷贝构造)完成"复制"劳动,再通过
swap完成所有权交换,连"释放旧资源"也一并代劳。类比人类社会进步的本质------分工。想吃烤鸭不必自己养鸭子,去挣钱买就行。
注意 :这种"构造 + swap"(copy-and-swap)本质上是赋值运算符 的经典惯用法;对拷贝构造 而言,8.2 的直写版才是规范写法,swap 版仅作思维拓展------它依赖
string(const char*)构造函数,且产物容量可能与直写版不同(详见下文)。
拷贝构造的现代写法
传统写法(8.2 已展示)------自己开空间、拷数据。
现代写法------借道构造函数 + swap:
cpp
string::string(const string& s)
: _str(nullptr)
, _size(0)
, _capacity(0)
{
string tmp(s._str); // 调用 string(const char*) 构造出副本
swap(tmp); // 交换 this 与 tmp 的内容
} // tmp 出作用域析构------它持有的只是 nullptr,安全
先将成员初始化为安全的空状态,然后用 s._str 构造一个临时对象 tmp,最后通过 swap 将 tmp 的资源交换给自己。tmp 出作用域析构------但此时它持有的只是原本 this 的 nullptr,析构安全。注意:拷贝构造场景下 this 是全新对象,没有"旧资源"可带,"顺手释放旧资源"是赋值运算符版本才有的效果。
⚠️ 必须初始化成员! 如果不将
_str置nullptr,进入函数体时_str是随机值。swap后随机值被交换给tmp,tmp析构时对野指针delete[]导致崩溃。初始化列表总是将成员设为安全空状态是现代写法的隐含前提。⚠️ 两个隐藏前提: ①
string tmp(s._str)走的是string(const char*)构造函数(而非拷贝构造),要求该类提供该构造函数且未标记explicit;②swap会把 tmp 的_capacity整体搬入this,若 char* 构造与拷贝构造的容量策略不同,同一字符串用两种写法拷贝出的容量会不同------行为依然正确,但 swap 版并非直写版的完全等价替代。
赋值运算符的现代写法
传统写法(8.3 已展示)------三步走:开新空间、拷数据、释放旧空间。
现代写法(显式版本):
cpp
string& operator=(const string& s)
{
string tmp(s); // 让拷贝构造干活(深拷贝 s 的内容)
swap(tmp); // 交换自己与临时对象
return *this; // tmp 出作用域,顺手释放旧空间
}
先构造 tmp(深拷贝 s),然后交换 tmp 与 *this。tmp 析构时释放原本 *this 的旧空间------连释放资源都让别人代劳了。
现代写法(一行极致版):
cpp
string& operator=(string s) // 传值触发拷贝构造
{
swap(s); // s 本身就是深拷贝副本,直接交换
return *this; // s 出作用域析构
}
传值传参触发拷贝构造,形参 s 已经是实参的深拷贝副本,直接交换即可。这个写法最为精炼,是 copy-and-swap 的标准形态。注意:本类尚未实现移动构造,所以即使实参是右值也会深拷贝一次;生产代码中该写法通常搭配移动构造使用(左值拷、右值移),效果最佳。
九、swap 优化
9.1 std::swap 的三次深拷贝问题
#mermaid-svg-9IOIIx8hbt6XU6Pp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9IOIIx8hbt6XU6Pp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9IOIIx8hbt6XU6Pp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9IOIIx8hbt6XU6Pp .error-icon{fill:#552222;}#mermaid-svg-9IOIIx8hbt6XU6Pp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9IOIIx8hbt6XU6Pp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9IOIIx8hbt6XU6Pp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9IOIIx8hbt6XU6Pp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9IOIIx8hbt6XU6Pp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9IOIIx8hbt6XU6Pp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9IOIIx8hbt6XU6Pp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9IOIIx8hbt6XU6Pp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9IOIIx8hbt6XU6Pp .marker.cross{stroke:#333333;}#mermaid-svg-9IOIIx8hbt6XU6Pp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9IOIIx8hbt6XU6Pp p{margin:0;}#mermaid-svg-9IOIIx8hbt6XU6Pp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9IOIIx8hbt6XU6Pp .cluster-label text{fill:#333;}#mermaid-svg-9IOIIx8hbt6XU6Pp .cluster-label span{color:#333;}#mermaid-svg-9IOIIx8hbt6XU6Pp .cluster-label span p{background-color:transparent;}#mermaid-svg-9IOIIx8hbt6XU6Pp .label text,#mermaid-svg-9IOIIx8hbt6XU6Pp span{fill:#333;color:#333;}#mermaid-svg-9IOIIx8hbt6XU6Pp .node rect,#mermaid-svg-9IOIIx8hbt6XU6Pp .node circle,#mermaid-svg-9IOIIx8hbt6XU6Pp .node ellipse,#mermaid-svg-9IOIIx8hbt6XU6Pp .node polygon,#mermaid-svg-9IOIIx8hbt6XU6Pp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9IOIIx8hbt6XU6Pp .rough-node .label text,#mermaid-svg-9IOIIx8hbt6XU6Pp .node .label text,#mermaid-svg-9IOIIx8hbt6XU6Pp .image-shape .label,#mermaid-svg-9IOIIx8hbt6XU6Pp .icon-shape .label{text-anchor:middle;}#mermaid-svg-9IOIIx8hbt6XU6Pp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9IOIIx8hbt6XU6Pp .rough-node .label,#mermaid-svg-9IOIIx8hbt6XU6Pp .node .label,#mermaid-svg-9IOIIx8hbt6XU6Pp .image-shape .label,#mermaid-svg-9IOIIx8hbt6XU6Pp .icon-shape .label{text-align:center;}#mermaid-svg-9IOIIx8hbt6XU6Pp .node.clickable{cursor:pointer;}#mermaid-svg-9IOIIx8hbt6XU6Pp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9IOIIx8hbt6XU6Pp .arrowheadPath{fill:#333333;}#mermaid-svg-9IOIIx8hbt6XU6Pp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9IOIIx8hbt6XU6Pp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9IOIIx8hbt6XU6Pp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9IOIIx8hbt6XU6Pp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9IOIIx8hbt6XU6Pp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9IOIIx8hbt6XU6Pp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9IOIIx8hbt6XU6Pp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9IOIIx8hbt6XU6Pp .cluster text{fill:#333;}#mermaid-svg-9IOIIx8hbt6XU6Pp .cluster span{color:#333;}#mermaid-svg-9IOIIx8hbt6XU6Pp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9IOIIx8hbt6XU6Pp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9IOIIx8hbt6XU6Pp rect.text{fill:none;stroke-width:0;}#mermaid-svg-9IOIIx8hbt6XU6Pp .icon-shape,#mermaid-svg-9IOIIx8hbt6XU6Pp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9IOIIx8hbt6XU6Pp .icon-shape p,#mermaid-svg-9IOIIx8hbt6XU6Pp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9IOIIx8hbt6XU6Pp .icon-shape .label rect,#mermaid-svg-9IOIIx8hbt6XU6Pp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9IOIIx8hbt6XU6Pp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9IOIIx8hbt6XU6Pp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9IOIIx8hbt6XU6Pp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 三次深拷贝
拷贝构造
赋值
赋值
s1(100字符)
tmp(100字符, 深拷贝)
s2(200字符)
如果使用 std::swap(s1, s2),其内部实现本质上是:
cpp
my_str::string tmp(s1); // 深拷贝构造,O(n) 内存 + O(n) 时间
s1 = s2; // 深拷贝赋值
s2 = tmp; // 深拷贝赋值
三次深拷贝操作,总时间复杂度和空间复杂度都是 O(n),对大型 string 代价极大。
9.2 成员 swap------只交换指针
cpp
void string::swap(string& s)
{
std::swap(_str, s._str); // 交换指针 O(1)
std::swap(_size, s._size); // 交换 size O(1)
std::swap(_capacity, s._capacity); // 交换 capacity O(1)
}
#mermaid-svg-UPo0H3wQDxxftcSF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UPo0H3wQDxxftcSF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UPo0H3wQDxxftcSF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UPo0H3wQDxxftcSF .error-icon{fill:#552222;}#mermaid-svg-UPo0H3wQDxxftcSF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UPo0H3wQDxxftcSF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UPo0H3wQDxxftcSF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UPo0H3wQDxxftcSF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UPo0H3wQDxxftcSF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UPo0H3wQDxxftcSF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UPo0H3wQDxxftcSF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UPo0H3wQDxxftcSF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UPo0H3wQDxxftcSF .marker.cross{stroke:#333333;}#mermaid-svg-UPo0H3wQDxxftcSF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UPo0H3wQDxxftcSF p{margin:0;}#mermaid-svg-UPo0H3wQDxxftcSF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UPo0H3wQDxxftcSF .cluster-label text{fill:#333;}#mermaid-svg-UPo0H3wQDxxftcSF .cluster-label span{color:#333;}#mermaid-svg-UPo0H3wQDxxftcSF .cluster-label span p{background-color:transparent;}#mermaid-svg-UPo0H3wQDxxftcSF .label text,#mermaid-svg-UPo0H3wQDxxftcSF span{fill:#333;color:#333;}#mermaid-svg-UPo0H3wQDxxftcSF .node rect,#mermaid-svg-UPo0H3wQDxxftcSF .node circle,#mermaid-svg-UPo0H3wQDxxftcSF .node ellipse,#mermaid-svg-UPo0H3wQDxxftcSF .node polygon,#mermaid-svg-UPo0H3wQDxxftcSF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UPo0H3wQDxxftcSF .rough-node .label text,#mermaid-svg-UPo0H3wQDxxftcSF .node .label text,#mermaid-svg-UPo0H3wQDxxftcSF .image-shape .label,#mermaid-svg-UPo0H3wQDxxftcSF .icon-shape .label{text-anchor:middle;}#mermaid-svg-UPo0H3wQDxxftcSF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UPo0H3wQDxxftcSF .rough-node .label,#mermaid-svg-UPo0H3wQDxxftcSF .node .label,#mermaid-svg-UPo0H3wQDxxftcSF .image-shape .label,#mermaid-svg-UPo0H3wQDxxftcSF .icon-shape .label{text-align:center;}#mermaid-svg-UPo0H3wQDxxftcSF .node.clickable{cursor:pointer;}#mermaid-svg-UPo0H3wQDxxftcSF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UPo0H3wQDxxftcSF .arrowheadPath{fill:#333333;}#mermaid-svg-UPo0H3wQDxxftcSF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UPo0H3wQDxxftcSF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UPo0H3wQDxxftcSF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UPo0H3wQDxxftcSF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UPo0H3wQDxxftcSF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UPo0H3wQDxxftcSF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UPo0H3wQDxxftcSF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UPo0H3wQDxxftcSF .cluster text{fill:#333;}#mermaid-svg-UPo0H3wQDxxftcSF .cluster span{color:#333;}#mermaid-svg-UPo0H3wQDxxftcSF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UPo0H3wQDxxftcSF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UPo0H3wQDxxftcSF rect.text{fill:none;stroke-width:0;}#mermaid-svg-UPo0H3wQDxxftcSF .icon-shape,#mermaid-svg-UPo0H3wQDxxftcSF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UPo0H3wQDxxftcSF .icon-shape p,#mermaid-svg-UPo0H3wQDxxftcSF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UPo0H3wQDxxftcSF .icon-shape .label rect,#mermaid-svg-UPo0H3wQDxxftcSF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UPo0H3wQDxxftcSF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UPo0H3wQDxxftcSF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UPo0H3wQDxxftcSF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 成员 swap 后
成员 swap 前
s1._str
堆 A (hello)
s2._str
堆 B (world)
s1._str
s2._str
只交换了三个内置类型成员(一个指针 + 两个 size_t),没有涉及任何堆内存操作,时间复杂度 O(1),空间复杂度 O(1)。效率天差地别。
9.3 全局 swap 特化与匹配优先级
标准库为 std::string 提供了非成员版本的 swap 特化。当调用 swap(s1, s2) 时,C++ 的重载决议会优先选择最匹配的特化版本 ,而不是走三次深拷贝的通用 std::swap 模板。
cpp
// 标准库中的特化(示意)
namespace std {
template<>
void swap<my_str::string>(my_str::string& a, my_str::string& b) noexcept
{
a.swap(b); // 直接调用成员 swap
}
}
使用建议: 总是调用 swap(s1, s2) 而非 s1.swap(s2),让编译器的重载决议自动选择最优版本。
十、substr 实现
cpp
string string::substr(size_t pos, size_t len)
{
// 情况一:len 超出剩余长度,直接取到末尾
if (len > _size - pos)
{
string sub(_str + pos); // 构造子串(从 pos 到末尾)
return sub;
}
else // 情况二:len 不超出,精确取 len 个字符
{
string sub;
sub.reserve(len); // 预留空间避免多次扩容
for (size_t i = 0; i < len; i++)
{
sub += _str[pos + i]; // 逐个追加字符
}
return sub;
}
}
两种情况的处理策略:
| 条件 | 含义 | 做法 |
|---|---|---|
len > _size - pos |
请求长度 > 剩余字符串长度 | 直接用 _str + pos 构造新 string,走带参构造一次搞定 |
len <= _size - pos |
请求长度 <= 剩余长度 | 先 reserve(len) 预留空间,避免循环追加时多次扩容 |
注意: 这里不能用 _str + pos + len 作为构造参数构造子串,因为情况二中只取前 len 个字符,而 _str + pos 到 _str + pos + len 之间可能没有 '\0' 终结,所以必须用循环逐个字符构建新的 string。
十一、比较运算符
11.1 核心实现:只写 < 和 ==
cpp
bool string::operator<(const string& s) const
{
return strcmp(_str, s._str) < 0;
}
bool string::operator==(const string& s) const
{
return strcmp(_str, s._str) == 0;
}
11.2 其余运算符的复用模式
cpp
bool string::operator>(const string& s) const
{
return !(*this <= s); // > 就是 <= 取反
}
bool string::operator<=(const string& s) const
{
return *this < s || *this == s; // <= 就是 < 或 ==
}
bool string::operator>=(const string& s) const
{
return !(*this < s); // >= 就是 < 取反
}
bool string::operator!=(const string& s) const
{
return !(*this == s); // != 就是 == 取反
}
复用关系图:
#mermaid-svg-MTAa2cR78lx17QqP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MTAa2cR78lx17QqP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MTAa2cR78lx17QqP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MTAa2cR78lx17QqP .error-icon{fill:#552222;}#mermaid-svg-MTAa2cR78lx17QqP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MTAa2cR78lx17QqP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MTAa2cR78lx17QqP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MTAa2cR78lx17QqP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MTAa2cR78lx17QqP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MTAa2cR78lx17QqP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MTAa2cR78lx17QqP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MTAa2cR78lx17QqP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MTAa2cR78lx17QqP .marker.cross{stroke:#333333;}#mermaid-svg-MTAa2cR78lx17QqP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MTAa2cR78lx17QqP p{margin:0;}#mermaid-svg-MTAa2cR78lx17QqP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MTAa2cR78lx17QqP .cluster-label text{fill:#333;}#mermaid-svg-MTAa2cR78lx17QqP .cluster-label span{color:#333;}#mermaid-svg-MTAa2cR78lx17QqP .cluster-label span p{background-color:transparent;}#mermaid-svg-MTAa2cR78lx17QqP .label text,#mermaid-svg-MTAa2cR78lx17QqP span{fill:#333;color:#333;}#mermaid-svg-MTAa2cR78lx17QqP .node rect,#mermaid-svg-MTAa2cR78lx17QqP .node circle,#mermaid-svg-MTAa2cR78lx17QqP .node ellipse,#mermaid-svg-MTAa2cR78lx17QqP .node polygon,#mermaid-svg-MTAa2cR78lx17QqP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MTAa2cR78lx17QqP .rough-node .label text,#mermaid-svg-MTAa2cR78lx17QqP .node .label text,#mermaid-svg-MTAa2cR78lx17QqP .image-shape .label,#mermaid-svg-MTAa2cR78lx17QqP .icon-shape .label{text-anchor:middle;}#mermaid-svg-MTAa2cR78lx17QqP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MTAa2cR78lx17QqP .rough-node .label,#mermaid-svg-MTAa2cR78lx17QqP .node .label,#mermaid-svg-MTAa2cR78lx17QqP .image-shape .label,#mermaid-svg-MTAa2cR78lx17QqP .icon-shape .label{text-align:center;}#mermaid-svg-MTAa2cR78lx17QqP .node.clickable{cursor:pointer;}#mermaid-svg-MTAa2cR78lx17QqP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MTAa2cR78lx17QqP .arrowheadPath{fill:#333333;}#mermaid-svg-MTAa2cR78lx17QqP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MTAa2cR78lx17QqP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MTAa2cR78lx17QqP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MTAa2cR78lx17QqP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MTAa2cR78lx17QqP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MTAa2cR78lx17QqP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MTAa2cR78lx17QqP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MTAa2cR78lx17QqP .cluster text{fill:#333;}#mermaid-svg-MTAa2cR78lx17QqP .cluster span{color:#333;}#mermaid-svg-MTAa2cR78lx17QqP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MTAa2cR78lx17QqP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MTAa2cR78lx17QqP rect.text{fill:none;stroke-width:0;}#mermaid-svg-MTAa2cR78lx17QqP .icon-shape,#mermaid-svg-MTAa2cR78lx17QqP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MTAa2cR78lx17QqP .icon-shape p,#mermaid-svg-MTAa2cR78lx17QqP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MTAa2cR78lx17QqP .icon-shape .label rect,#mermaid-svg-MTAa2cR78lx17QqP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MTAa2cR78lx17QqP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MTAa2cR78lx17QqP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MTAa2cR78lx17QqP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 复用 !<=
复用 !<
operator< (strcmp实现)
operator<=
operator>=
operator== (strcmp实现)
operator!=
operator>
只需要实现 < 和 ==(两者都调用 strcmp),其余运算符通过逻辑组合派生。这减少了代码量,也避免了在多个函数中重复实现 strcmp。
十二、流运算符
12.1 operator<<
cpp
ostream& operator<< (ostream& os, const my_str::string& str)
{
for (size_t i = 0; i < str.size(); i++)
{
os << str[i]; // 调用 operator[] 逐个输出
}
return os;
}
为什么不直接用 os << str.c_str()?因为当前实现通过逐个字符输出,为了展示 size() 和 operator[] 的组合使用。实际的标准库实现会直接输出 _str,性能更好。
12.2 operator>>
cpp
istream& operator>> (istream& is, my_str::string& str)
{
str.clear(); // 清空原有内容
char ch;
while (is.get(ch) && ch != ' ' && ch != '\n')
{
str += ch; // 追加字符
}
return is;
}
为什么用 is.get() 而不是 is >> ch?
| 方式 | 行为 | 问题 |
|---|---|---|
is >> ch |
跳过空白字符(空格、换行、制表符) | 无法读取空格,导致输入"hello world"只读到"hello" |
is.get() |
不跳过任何字符,原样读取 | 可以读到空格,但需要手动判断结束条件 |
operator>> 的语义应该读取一个非空白的单词 ,遇到空格或换行停止。如果用 >> 读取单个 char,它会跳过前导空白,这正是我们想要的"忽略前导空格"行为------但问题是它也会跳过大段文字中间的空格,导致判断结束条件变得复杂。
更安全的做法是先用 is.get() 逐个读取,显式判断是否为空格或换行。
clear 先行: 每次 >> 输入前先清空 string 原有内容。否则多次输入会在旧内容后面追加。
EOF 处理: 这里用 is.get(ch) 的返回值判断读取是否成功,而不是 char ch = is.get() 直接赋值------后者在读到文件末尾时,is.get() 返回的 EOF(-1)会被截断成 char 存入 ch,它既不是空格也不是换行,循环将永远不会结束。
十三、clear 实现
cpp
void string::clear()
{
_str[0] = '\0';
_size = 0;
// _capacity 不变
}
关键设计:clear 不改变 capacity。 这是有意为之------clear 后通常会再次写入数据,保留已有 capacity 可以避免不必要的重新分配。如果你真的需要释放内存,应该调用 reserve(0) 或 shrink_to_fit()(C++11 起标准库支持)。
十四、写实拷贝(COW)与引用计数
Copy on Write(写时拷贝) 是介于浅拷贝与深拷贝之间的优化策略:拷贝时不深拷贝,共享同一份数据;只有发生写操作时才执行真正的深拷贝。它结合引用计数(Reference Counting)来追踪资源的共享状态。
14.1 动机:深拷贝的代价
某些场景下拷贝 string 后只是读取,从不修改:
cpp
my_str::string s1("hello world");
my_str::string s2(s1); // 深拷贝 O(n)
// 后续只用 s2 打印、查找、substr------全都没有修改
如果 s2 只读不写,这份深拷贝的开销就是白费的。对于大型 string(如从文件读取的数万字符),或者频繁拷贝的代码路径,这种浪费不容忽视。
COW 的思路是:先浅拷贝共享数据、记录引用计数;如果后续确实写了,再在写的那一刻执行深拷贝------把深拷贝从"必然发生"推迟到"必要时才发生"。
14.2 设计原理(了解)
在内部维护一个引用计数(Reference Count),记录当前有多少个对象共享同一块堆内存:
s1._str ──→ 堆内存[hello\0] ref_count = 2
s2._str ──→ ↑
共享同一块内存
核心操作:
| 操作 | 行为 | 引用计数变化 |
|---|---|---|
| 拷贝构造 | 复制指针 + 引用计数 +1 | 1 → 2 |
| 析构 | 引用计数 -1;若归零则释放内存 | 2 → 1(不释放)→ 1 → 0(释放) |
| 写入前(COW) | 若 ref_count > 1,执行深拷贝后写入;计数 -1 |
2 → 1(写操作独享一份) |
14.3 COW 机制的关键函数
写操作前都需要调用一个类似 copy_on_write() 的检查函数:
cpp
void string::copy_on_write()
{
if (*_ref_count > 1) // 还有其他人共享
{
// 深拷贝一份独享的数据
char* new_str = new char[_capacity + 1];
strcpy(new_str, _str);
--(*_ref_count); // 减少原共享数据的引用计数
// 指向新空间,新空间计数为 1
_str = new_str;
_ref_count = new size_t(1);
}
}
所有修改操作(operator[] 写操作、insert、erase、push_back、+= 等)都必须先调用 copy_on_write():
cpp
char& string::operator[](size_t pos)
{
copy_on_write(); // 写前检查:是否需要深拷贝?
return _str[pos];
}
14.4 验证方法
通过拷贝构造后检查 c_str() 的地址即可判断实现方案:
cpp
string s1("hello");
string s2(s1);
// 如果地址相同 → COW(共享内存)
// 如果地址不同 → 深拷贝(非 COW)
printf("%p\n", s1.c_str()); // 强制打印地址
printf("%p\n", s2.c_str()); // 直接用 cout 会输出字符串
cpp
cout << (void*)s1.c_str() << endl; // 转型为 void* 打印地址
cout << (void*)s2.c_str() << endl;
| 编译器 | COW 实现 | 说明 |
|---|---|---|
| GCC (Linux) | 使用 COW(C++11 前) | C++11 前标准允许 COW,GCC 的 std::string 采用此方案 |
| MSVC (Windows) | 不使用 COW | 始终深拷贝,不共享内存 |
| C++11 之后 | 标准禁止 COW | C++11 要求 string 的 operator[] 保证 O(1) 且连续存储,COW 不再符合标准 |
14.5 优缺点分析
| 优点 | 缺点 |
|---|---|
| 拷贝后只读场景节省大量深拷贝开销 | 写操作路径变长(每次写入前多一次计数检查) |
引用计数本身成本极低(一个 size_t) |
多线程环境下引用计数的原子操作有额外开销 |
| 内存复用,缓存更友好 | 代码复杂度显著增加,所有写路径都必须经过 COW 检查 |
预估值: 在大量拷贝 + 只读不写的场景下,COW 可提升 20%--30% 的效率(左耳朵耗子/陈皓文章数据)。
14.6 C++11 移动语义的冲击
C++11 引入右值引用和移动语义后,COW 的意义大大减弱:
cpp
// 移动构造:直接"偷"走源对象的资源,O(1) 成本
string s2(std::move(s1)); // 指针交换,和 COW 一样高效
移动语义从根本上解决了"拷贝后只读"场景的效率问题------如果源对象不再需要,直接移动而非拷贝。这使得 COW 带来的额外复杂度显得不值得:
| 技术 | 拷贝构造 | 移动构造 | 适用场景 |
|---|---|---|---|
| 深拷贝 | O(n) | O(1) | 通用 |
| COW | O(1)(共享)+ 写时 O(n) | O(1) | 拷贝多、修改少 |
| 移动语义 | O(n) | O(1) | 明确不再需要源对象 |
C++11 标准也因此明确禁止了 std::string 的 COW 实现(要求 operator[] O(1) 且连续,COW 的写时拷贝会破坏引用稳定性)。
14.7 扩展阅读
COW 的概念不限于 string------在操作系统层面也有应用:
| 领域 | COW 应用 |
|---|---|
| Linux 进程创建 | fork() 后父子进程共享物理内存,任一进程写入时触发页级拷贝 |
| C++ string | 共享底层字符数组,写入字符时触发一整个数组的深拷贝(粒度较大) |
| 智能指针 | shared_ptr 的管控块使用引用计数管理资源生命周期(与 COW 相似但不完全相同) |
左耳朵耗子(陈皓)关于 COW 和 C++ 的高质量文章值得深入阅读,其博客 酷壳 对许多技术概念有深刻讲解。
十五、输入优化 ------ getline 与 buffer 缓冲
场景:
operator>>读取字符串时遇到空格即停止,无法读取整行输入。但实现getline时如果逐字符+=追加,对于长字符串会导致大量频繁的扩容操作。
15.1 朴素实现的扩容问题
cpp
istream& getline(istream& is, my_str::string& str)
{
str.clear();
char ch;
while (is.get(ch) && ch != '\n')
{
str += ch; // 每读一个字符就 +=,可能触发扩容
}
return is;
}
每次 str += ch 都可能触发扩容。如果输入字符串很长(数千字符),2 倍扩容策略会导致多次 realloc:
| 输入长度 | 扩容次数(2 倍策略,0→4 起步) | 总拷贝数据量 |
|---|---|---|
| 23 | 4 次 (4→8→16→32) | 约 60 字符 |
| 1000 | 9 次 | 约 2000 字符 |
| 10000 | 13 次 | 约 20000 字符 |
每次扩容都要 new + strcpy + delete[],高频次下性能损失明显。
15.2 buffer 优化方案
思路:不直接向 string 追加,而是先用一个固定大小的栈上 buffer 暂存,buffer 满了再一次性写入 string。
cpp
istream& getline(istream& is, my_str::string& str)
{
str.clear();
char buffer[128]; // 127 个有效位置 + 1 个 '\0' 位置
size_t i = 0;
char ch;
while (is.get(ch) && ch != '\n')
{
buffer[i++] = ch;
if (i == 127) // buffer 满了(留一位给 '\0')
{
buffer[i] = '\0';
str += buffer; // 一次性追加 127 个字符进 string
i = 0; // 重置 buffer 索引
}
}
// 处理剩余不足 127 个字符的部分
if (i != 0)
{
buffer[i] = '\0';
str += buffer; // 追加剩余字符
}
return is;
}
工作流程:
#mermaid-svg-HMwDnA8CtNOiQqxb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HMwDnA8CtNOiQqxb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HMwDnA8CtNOiQqxb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HMwDnA8CtNOiQqxb .error-icon{fill:#552222;}#mermaid-svg-HMwDnA8CtNOiQqxb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HMwDnA8CtNOiQqxb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HMwDnA8CtNOiQqxb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HMwDnA8CtNOiQqxb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HMwDnA8CtNOiQqxb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HMwDnA8CtNOiQqxb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HMwDnA8CtNOiQqxb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HMwDnA8CtNOiQqxb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HMwDnA8CtNOiQqxb .marker.cross{stroke:#333333;}#mermaid-svg-HMwDnA8CtNOiQqxb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HMwDnA8CtNOiQqxb p{margin:0;}#mermaid-svg-HMwDnA8CtNOiQqxb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HMwDnA8CtNOiQqxb .cluster-label text{fill:#333;}#mermaid-svg-HMwDnA8CtNOiQqxb .cluster-label span{color:#333;}#mermaid-svg-HMwDnA8CtNOiQqxb .cluster-label span p{background-color:transparent;}#mermaid-svg-HMwDnA8CtNOiQqxb .label text,#mermaid-svg-HMwDnA8CtNOiQqxb span{fill:#333;color:#333;}#mermaid-svg-HMwDnA8CtNOiQqxb .node rect,#mermaid-svg-HMwDnA8CtNOiQqxb .node circle,#mermaid-svg-HMwDnA8CtNOiQqxb .node ellipse,#mermaid-svg-HMwDnA8CtNOiQqxb .node polygon,#mermaid-svg-HMwDnA8CtNOiQqxb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HMwDnA8CtNOiQqxb .rough-node .label text,#mermaid-svg-HMwDnA8CtNOiQqxb .node .label text,#mermaid-svg-HMwDnA8CtNOiQqxb .image-shape .label,#mermaid-svg-HMwDnA8CtNOiQqxb .icon-shape .label{text-anchor:middle;}#mermaid-svg-HMwDnA8CtNOiQqxb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HMwDnA8CtNOiQqxb .rough-node .label,#mermaid-svg-HMwDnA8CtNOiQqxb .node .label,#mermaid-svg-HMwDnA8CtNOiQqxb .image-shape .label,#mermaid-svg-HMwDnA8CtNOiQqxb .icon-shape .label{text-align:center;}#mermaid-svg-HMwDnA8CtNOiQqxb .node.clickable{cursor:pointer;}#mermaid-svg-HMwDnA8CtNOiQqxb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HMwDnA8CtNOiQqxb .arrowheadPath{fill:#333333;}#mermaid-svg-HMwDnA8CtNOiQqxb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HMwDnA8CtNOiQqxb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HMwDnA8CtNOiQqxb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HMwDnA8CtNOiQqxb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HMwDnA8CtNOiQqxb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HMwDnA8CtNOiQqxb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HMwDnA8CtNOiQqxb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HMwDnA8CtNOiQqxb .cluster text{fill:#333;}#mermaid-svg-HMwDnA8CtNOiQqxb .cluster span{color:#333;}#mermaid-svg-HMwDnA8CtNOiQqxb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HMwDnA8CtNOiQqxb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HMwDnA8CtNOiQqxb rect.text{fill:none;stroke-width:0;}#mermaid-svg-HMwDnA8CtNOiQqxb .icon-shape,#mermaid-svg-HMwDnA8CtNOiQqxb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HMwDnA8CtNOiQqxb .icon-shape p,#mermaid-svg-HMwDnA8CtNOiQqxb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HMwDnA8CtNOiQqxb .icon-shape .label rect,#mermaid-svg-HMwDnA8CtNOiQqxb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HMwDnA8CtNOiQqxb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HMwDnA8CtNOiQqxb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HMwDnA8CtNOiQqxb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
否
是
是
否
读取一个字符
是换行?
结束
存入 buffer, i 自增
buffer 满?
i == 127?
给 buffer 加结束符 NUL
str += buffer
一次性追加 127 字符
i = 0 重置
退出循环后
i != 0?
bufferi 置 NUL
str += 剩余字符
15.3 优化效果
| 方案 | 输入 23 字符 | 输入 1000 字符 | 输入 10000 字符 |
|---|---|---|---|
朴素 += |
扩容 4 次 | 扩容 9 次 | 扩容 13 次 |
| buffer 优化 | 扩容 0--1 次 | 扩容 7--8 次(每 127 字符触发一次) | 扩容约 78 次 |
注意: buffer 优化减少了 扩容拷贝的总数据量,但不一定能减少扩容次数。对于超长字符串(10000 字符),每 127 字符 flush 一次到 string,string 仍然需要扩容;但每次 flush 的是 127 字符的批量数据而非 1 个字符,string 的扩容策略可以让每一次追加都接近"恰好够用"。
核心收益:
- 短输入(≤127 字符)完全不触发 string 扩容------buffer 在栈上,零堆分配
- 长输入时每次追加 127 字符,string 单次扩容的"放大倍数"从 1 字符变成了 127 字符,总扩容次数减少
- 避免提前
reserve(10000)造成浪费------短输入时栈 buffer 自动释放,string 按需分配
15.4 设计取舍
| 优势 | 劣势 | |
|---|---|---|
朴素 += |
代码简单,无需 buffer 管理 | 长字符串频繁扩容 |
reserve 大空间 |
预分配避免扩容 | 短字符串浪费堆空间 |
| buffer 缓冲 | 兼顾短/长输入,不浪费空间 | 代码复杂度增加,多一次 buffer 拷贝 |
buffer 优化本质上是用空间换时间 + 用栈空间换堆操作的典型策略,在 C 语言的高性能字符串读取实现中也是常见手法。
结语
把 string 亲手实现一遍之后,你会发现它远没有想象中神秘。整篇文章其实都在围绕一块堆上的字符数组做文章:_str 指向数据,_size 记录有效长度,_capacity 记录容量,构造析构、扩容、插入删除、拷贝赋值,全都是在这块空间上做文章,跟前面学的类和对象、内存管理一脉相承。
比功能更值得记住的是路上踩过的坑。无符号数在循环里从 0 再减 1 就变成天文数字,insert 头插差点写越界;浅拷贝让两个对象共享一块内存,析构时 double free;strstr 找不到子串返回空指针,指针相减就成了未定义行为。这些坑恰恰是面试爱问的点,也是"会用"和"真懂"的分界线。
后面的章节在展示怎么把代码写得更聪明:swap 用三次指针交换取代三次深拷贝,赋值运算符用"构造加 swap"让临时对象代劳释放旧资源,比较运算符只写 < 和 == 其余全部组合派生,getline 用栈上 buffer 攒一批字符再批量追加,把扩容的代价摊薄。写时拷贝 COW 更是把深拷贝从"必然发生"推迟到"写的时候才发生",思路巧妙,可惜 C++11 移动语义的出现让它功成身退。带着这些眼光再去看标准库,每个接口背后都有设计考量,下一步学 vector 时你会发现骨架几乎一样,会顺很多。