10.C++ string 实现详解

C++ string 实现详解

文章目录

  • [C++ string 实现详解](#C++ string 实现详解)
    • 一、类设计概览
      • [1.1 成员变量](#1.1 成员变量)
      • [1.2 迭代器类型定义](#1.2 迭代器类型定义)
      • [1.3 npos 静态成员](#1.3 npos 静态成员)
    • 二、构造与析构
    • 三、三种遍历实现
      • [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 的委派设计)
    • 六、修改操作实现
    • 七、查找
      • [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 的最大值(0xFFFFFFFF0xFFFFFFFFFFFFFFFF),表示"直到末尾"的特殊语义。它是 erasefindsubstr 等函数的默认参数值。

坑: 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 结果赋值,不涉及复杂的控制流;而 _strnew 操作依赖于 _size 已经算好,同时 new 可能抛异常,放在函数体中更适合处理资源分配逻辑。

默认构造的行为: str = ""strlen("") = 0,所以 _size = 0_capacity = 0new 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& 有两层含义:

  1. 可以char c = s[0];
  2. 可以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_backappend 内部都委派给 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 > 0end 初始值就大于 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

两大问题:

  1. double free: s1 和 s2 析构时都会 delete[] _str,同一块内存释放两次,程序崩溃。
  2. 修改互相影响: 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[] _strnew,而 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,最后通过 swaptmp 的资源交换给自己。tmp 出作用域析构------但此时它持有的只是原本 thisnullptr,析构安全。注意:拷贝构造场景下 this 是全新对象,没有"旧资源"可带,"顺手释放旧资源"是赋值运算符版本才有的效果。

⚠️ 必须初始化成员! 如果不将 _strnullptr,进入函数体时 _str 是随机值。swap 后随机值被交换给 tmptmp 析构时对野指针 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*thistmp 析构时释放原本 *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[] 写操作、inserterasepush_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 要求 stringoperator[] 保证 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 时你会发现骨架几乎一样,会顺很多。

相关推荐
小飞学编程...1 小时前
【equals 、Comparable、Comparator 三者的区别】
java·python
骇客野人1 小时前
Java分布式任务调度方案(完整落地指南)
java·开发语言·分布式
JacksonMx1 小时前
Spring @Async 坑点 + 改造方案
java·开发语言
你怎么知道我是队长1 小时前
IPv6 地址完全解析:从结构到实战
开发语言·php
会周易的程序员1 小时前
软件接入大模型实现 Agent —— 从原理到 C++ 落地完全指南
c++·人工智能·物联网·架构·agent·工业协议·mcp
Dovis(誓平步青云)1 小时前
牛奶不能减成负数:冰箱库存的筛选、去重与不可变更新
android·java·开发语言
OPEN-F2 小时前
Python进阶教程:算法与数据结构入门
开发语言·python
薛定猫AI2 小时前
【技术干货】Claude Code多模型代理与反馈闭环:Python实现可验证的AI编程工作流
开发语言·python·ai编程
XR1234567882 小时前
工厂车间无线网络怎么选?AGV 与复杂环境是分水岭
开发语言·php