
博主名称:deM0one ⭐️C++专栏: C++ ⭐️C++string第一讲: string第一讲 ⭐️C++string第二讲: string第二讲 ⭐️C++string第三讲: string第三讲
文章目录
- [C++ string 深拷贝、swap 与容量管理(今日学习内容)](#C++ string 深拷贝、swap 与容量管理(今日学习内容))
-
- 一、引言
-
- [1.1 本文内容概览](#1.1 本文内容概览)
- [1.2 前置知识](#1.2 前置知识)
- [二、string 的深拷贝:构造函数](#二、string 的深拷贝:构造函数)
-
- [2.1 思路](#2.1 思路)
- [2.2 代码实现](#2.2 代码实现)
- [三、拷贝构造:复用构造函数 + swap](#三、拷贝构造:复用构造函数 + swap)
-
- [3.1 思路](#3.1 思路)
- [3.2 代码实现](#3.2 代码实现)
- [四、赋值运算符 operator=](#四、赋值运算符 operator=)
-
- [4.1 思路](#4.1 思路)
- [4.2 代码实现](#4.2 代码实现)
- [4.3 理解要点](#4.3 理解要点)
- [五、swap 的实现与优化](#五、swap 的实现与优化)
-
- [5.1 成员函数 swap](#5.1 成员函数 swap)
- [5.2 通用 std::swap 的问题](#5.2 通用 std::swap 的问题)
- [5.3 string 特化的 std::swap](#5.3 string 特化的 std::swap)
- 六、引用计数与写时拷贝(COW)
-
- [6.1 为什么会有 COW](#6.1 为什么会有 COW)
- [6.2 浅拷贝的两个经典问题](#6.2 浅拷贝的两个经典问题)
- [6.3 写时拷贝(Copy On Write)思想](#6.3 写时拷贝(Copy On Write)思想)
- [6.4 实验:同一份代码,不同平台表现不同](#6.4 实验:同一份代码,不同平台表现不同)
- [6.5 底层结构(g++ COW 实现)](#6.5 底层结构(g++ COW 实现))
- [七、vector 扩容实验](#七、vector 扩容实验)
-
- [7.1 实验代码](#7.1 实验代码)
- [7.2 实验结果对比](#7.2 实验结果对比)
- [7.3 结论](#7.3 结论)
- 八、reserve:请求调整容量
-
- [8.1 std::string::reserve](#8.1 std::string::reserve)
- [8.2 std::vector::reserve](#8.2 std::vector::reserve)
- [8.3 示例:reserve 只影响 capacity](#8.3 示例:reserve 只影响 capacity)
- [8.4 注意事项](#8.4 注意事项)
- [九、size 与 capacity 的关系](#九、size 与 capacity 的关系)
-
- [9.1 示意图](#9.1 示意图)
- [9.2 理解要点](#9.2 理解要点)
- 十、总结
- 任何的批评与建议欢迎指出,我们共同进步!
C++ string 深拷贝、swap 与容量管理(今日学习内容)
本文档基于学习笔记整理,内容覆盖:string 的构造/拷贝构造/赋值、swap 的优化、引用计数与写时拷贝(COW)、vector 扩容规律、reserve 的作用,以及 size 与 capacity 的关系。
一、引言
1.1 本文内容概览
- 自己实现 string 的"拷贝三件套":构造函数、拷贝构造、赋值运算符
- 用 swap 实现高效且异常安全的拷贝语义
- 深拷贝 vs 浅拷贝:为什么会有引用计数与写时拷贝(COW)
- 从实验数据看 vector 的扩容策略(VS 与 g++ 的差异)
- reserve 的语义与使用注意事项
1.2 前置知识
- string 的基本使用、new / delete 动态内存管理
- size(有效字符数)与 capacity(容量)的区别
二、string 的深拷贝:构造函数
2.1 思路
- 用 strlen 求长度,按需开空间,再用 strcpy 拷贝内容
- 注意:capacity 要额外包含结尾的 '\0'
2.2 代码实现
cpp
string(const char* str)
: _str(nullptr), _size(0), _capacity(0)
{
_size = strlen(str);
_capacity = _size; // _capacity 包含 '\0'
_str = new char[_capacity + 1]; // 多开一个位置放 '\0'
strcpy(_str, str);
}
三、拷贝构造:复用构造函数 + swap
3.1 思路
- 不自己写拷贝逻辑,而是复用已有的构造函数生成临时对象,再交换过来
- 代码简洁、异常安全
3.2 代码实现
cpp
string(const string& s)
: _str(nullptr), _size(0), _capacity(0)
{
string tmp(s._str); // 复用构造函数,完成深拷贝
swap(tmp); // 与临时对象交换,tmp 销毁时带走旧资源
}
四、赋值运算符 operator=
4.1 思路
- 先判断自赋值 :
this != &s - 同样用"拷贝 + swap"的方式,简单且安全
4.2 代码实现
cpp
string& operator=(const string& s)
{
if (this != &s) // 防止自赋值
{
string tmp(s); // 深拷贝
swap(tmp); // 交换,tmp 析构时释放旧空间
}
return *this;
}
4.3 理解要点
- 拷贝构造、赋值都走"临时对象 + swap",统一了逻辑
- swap 只交换内部成员,代价极低(O(1))
五、swap 的实现与优化
5.1 成员函数 swap
cpp
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
5.2 通用 std::swap 的问题
- 通用模板的行为等价于:
cpp
template <class T>
void swap(T& a, T& b)
{
T c(a); // 1 次深拷贝
a = b; // 1 次深拷贝
b = c; // 1 次深拷贝
}
- 即3 次深拷贝,对 string 这种带堆内存的类代价很大
5.3 string 特化的 std::swap
- 标准库为 string 提供了
std::swap(string& x, string& y)的重载 - 作用:交换两个 string 对象的值,效果等价于调用
x.swap(y) - 优化原理:只交换内部数据(指针),不交换字符内容,性能更好
六、引用计数与写时拷贝(COW)
6.1 为什么会有 COW
- 深拷贝:每次拷贝都复制全部字符,代价高
- 浅拷贝:多个对象共享同一块资源,只需要一个引用计数
- 引用计数 = 有几个对象指向这块资源
6.2 浅拷贝的两个经典问题
- 析构两次:两个对象析构时都去 delete 同一块内存
- 一个修改影响另一个:共享内存导致数据互相干扰
6.3 写时拷贝(Copy On Write)思想
- 博弈:如果拷贝后不写,是不是就赚了?
- 策略:拷贝时先共享资源、只增加引用计数;真正要修改时才深拷贝一份
6.4 实验:同一份代码,不同平台表现不同
cpp
std::string s1("111111111111111111111111");
std::string s2(s1); // 拷贝构造
printf("%p\n", s1.c_str());
printf("%p\n", s2.c_str());
- VS2019:两次打印地址不同(如 009315D8 / 009316C8)→ 默认深拷贝
- g++ 4.8.5(Linux):两次打印地址相同(如 0x17a6028 / 0x17a6028)→ 使用引用计数 + 写时拷贝
6.5 底层结构(g++ COW 实现)
- 内部用
_Rep_base结构管理共享资源:_M_length:长度_M_capacity:容量_M_refcount:引用计数(原子变量,保证线程安全)
七、vector 扩容实验
7.1 实验代码
cpp
void TestVectorExpand()
{
vector<int> v;
size_t sz = v.capacity();
cout << "making v grow:" << endl;
for (int i = 0; i < 199; ++i)
{
v.push_back(i);
if (sz != v.capacity())
{
sz = v.capacity();
cout << "capacity changed: " << sz << endl;
}
}
}
7.2 实验结果对比
| 平台 | 扩容倍数 | capacity 变化序列 |
|---|---|---|
| VS2019 | 约 1.5 倍 | 0, 1, 2, 3, 4, 6, 9, 13, 19, 28, 42, 63, 94, 141 |
| g++ 4.8 | 2 倍 | 0, 1, 2, 4, 8, 16, 32, 64, 128 |
7.3 结论
- VS2019 约 1.5 倍扩容,g++ 约 2 倍扩容------扩容策略由标准库实现决定,不同平台可能不同
- 扩容会重新分配内存并拷贝已有元素,代价较高
八、reserve:请求调整容量
8.1 std::string::reserve
cpp
void reserve(size_t n = 0);
- 作用:请求把容量调整为"最多容纳 n 个字符"
- 如果 n 大于当前容量:扩容到 n(或更大)
- 其他情况:只是一个非强制请求,实现可以优化或保持原容量
- 不影响字符串长度,也不能修改内容
8.2 std::vector::reserve
cpp
void reserve(size_type n);
- 作用:请求容量至少能容纳 n 个元素
- n 大于当前容量:重新分配存储,扩容到 n(或更大)
- n 不大于当前容量:不会重新分配,容量不受影响
- 不影响 size,也不能修改元素
8.3 示例:reserve 只影响 capacity
cpp
void test_vector2()
{
vector<int> v(10); // size = 10, capacity = 10
v.reserve(20); // 扩容到 20
cout << v.size() << endl; // 10
cout << v.capacity() << endl; // 20
v.reserve(0); // 请求缩容
cout << v.size() << endl; // 10(size 不变)
cout << v.capacity() << endl; // 20(不缩容)
}
8.4 注意事项
- reserve 只影响 capacity,不会改变 size
- 用 reserve 提前预留空间,可以避免频繁扩容
九、size 与 capacity 的关系
9.1 示意图
|----- 有效数据 (size = 5) -----|--------- 预留空间 (capacity = 20) ---------|
| 删除数据 | 插入数据,空间不够就扩容 |
9.2 理解要点
- size 之前的区域存放有效数据,删除数据在此范围内进行
- size 到 capacity 之间是预留空间,插入数据时先使用预留空间,不够了才扩容
- 扩容的本质:重新开一块更大的空间,把已有数据搬过去
十、总结
- 深拷贝三件套(构造/拷贝构造/赋值)用"临时对象 + swap"实现,简洁且异常安全
- swap 只交换内部指针,是 O(1) 操作;string 特化了 std::swap,避免通用版本的 3 次深拷贝
- 浅拷贝有"析构两次"和"互相影响"两个问题,写时拷贝(COW)通过引用计数在"读共享、写才拷"之间做平衡
- 同一份代码在不同平台(VS2019 / g++ 4.8)可能表现出深拷贝与 COW 的差异
- vector 扩容策略不统一:VS2019 约 1.5 倍,g++ 约 2 倍
- reserve 只调整 capacity、不影响 size 和内容,适合提前预留空间