
往期《C++初阶》回顾:《C++初阶》目录导航
往期《C++进阶》回顾:
/------------ 继承多态 ------------/
【普通类/模板类的继承 + 父类&子类的转换 + 继承的作用域 + 子类的默认成员函数】
【final + 继承与友元 + 继承与静态成员 + 继承模型 + 继承和组合】
【多态:概念 + 实现 + 拓展 + 原理】
/------------ STL ------------/
【二叉搜索树】
【AVL树】
【红黑树】
【set/map 使用介绍】
【set/map 模拟实现】
【哈希表】
【unordered_set/unordered_map 使用介绍】
【unordered_set/unordered_map 模拟实现】
/------------ C++11 ------------/
【列表初始化 + 右值引用】
🌈Hi~ 小伙伴们大家上午好!☀️ 中午好!🌞 晚上好!🌙👋,嗯~ o( ̄▽ ̄)o还有身体也保养好!(≖‿‿≖✿)
先跟大家敲个重点:这节课的内容,建议大家拿出百分百的专注度来对待!💡(゚Д゚)ノ 特别是我们要深入探讨的 “移动语义”,作为 C++11 标准重磅引入的特性,它直击了之前 C++ 代码中一个非常棘手的性能痛点—— 比如我们在使用拷贝构造、函数返回值传递大型对象(像是包含海量数据的 vector、string,或是自定义的复杂类对象)时,频繁发生的“不必要的深拷贝”问题。╰(‵□′)╯
而移动语义的出现,正是为了将这类“资源复制”转为“资源转移”,从而实现性能的质的飞跃。理解了它,就等于握住了 C++ 高效编程的一把金钥匙。♪(´▽`)ʕ•̀ω•́ʔ✧
所以今天,我们要聚焦的就是 C++ 领域中堪称“性能优化利器”的核心内容:【移动语义 + 完美转发】 ٩(ˊᗜˋ*)و✨
准备好一起把拷贝按在地上摩擦了吗?准备好的话,就赶紧上车,让我们出发吧!(≖‿‿≖✿)
左值引用的核心价值是减少拷贝:
T& 直接操作实参,避免拷贝大对象T& 返回对象,避免拷贝返回值(需保证返回对象生命周期独立于函数)但左值引用 无法解决 “返回局部对象” 的问题:
返回局部对象(如:栈上的临时对象),无论用左值引用还是右值引用返回,都会因对象销毁导致悬垂引用 场景 1:函数 返回局部对象 时触发深拷贝导致的大量性能开销
#include<iostream>
using namespace std;
//大数加法:将两个字符串表示的非负整数相加,返回结果字符串
class Solution
{
public:
/* 注意事项:“传值返回”由于返回的是函数内的局部对象,必须通过值返回
* C++98 时代:返回时会执行“拷贝构造”(深拷贝,性能开销大)
* C++11 以后:可能优化为“移动构造”(仅转移资源所有权,避免深拷贝)
*/
string addStrings(string num1, string num2)
{
//1.存储计算结果的临时字符串(局部对象,存储在栈上)
string str;
//2.指向两个字符串末尾的指针(用于逐位处理)
int end1 = num1.size() - 1; // num1 的最后一个字符索引
int end2 = num2.size() - 1; // num2 的最后一个字符索引
//3.进位值(初始为0)
int next = 0;
//4.从低位到高位逐位相加(只要有一个数未处理完就继续)
while (end1 >= 0 || end2 >= 0)
{
//4.1:获取当前位的数值(如果已经处理完所有位,则视为0)
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
//4.2:计算当前位的和(包括上一位的进位)
int ret = val1 + val2 + next;
//4.3:更新进位值(大于等于10时产生进位)
next = ret / 10; // 例如:15/10 = 1(进位为1)
//4.4:计算当前位的结果(取模运算)
ret = ret % 10; // 例如:15%10 = 5(当前位为5)
//4.5:将当前位的结果追加到临时字符串中(注意:此时是逆序存储的)
//例如:计算 123 + 456 时,会先存储 9 再存储 7 最后存储 5
str += ('0' + ret); // 将数字转换为字符(如 5 → '5')
}
//5.处理最后可能的进位(例如:999 + 1 会产生额外的进位)
if (next == 1)
{
str += '1';
}
//6.由于之前是逆序存储的(低位在前),需要反转字符串得到正确结果
reverse(str.begin(), str.end()); //例如:之前存储的是 "975",反转后变为 "579"
return str;
/* 返回局部对象str:
* C++98 会调用 string 的拷贝构造函数(深拷贝整个字符串)
* C++11 及以后会优先调用移动构造函数(仅转移内部指针,避免深拷贝)
*/
}
};场景 2:函数
返回局部对象时触发深拷贝导致的大量性能开销
#include<iostream>
#include<vector>
using namespace std;
//生成杨辉三角,返回包含 numRows 行的二维向量
class Solution
{
public:
/* 传值返回说明:
* 返回的是函数内定义的局部对象 vector<vector<int>>
* C++98 中会触发深拷贝(性能差,大对象拷贝代价高)
* C++11 及之后会优先触发移动构造(转移资源,避免深拷贝)
*/
vector<vector<int>> generate(int numRows)
{
//1.定义局部二维向量 vv,用于存储杨辉三角的所有行
vector<vector<int>> vv(numRows); // numRows 是杨辉三角的行数,vv 初始包含 numRows 个空的子 vector
/*------------------第一步:初始化每一行的大小和默认值------------------*/
//杨辉三角第 i 行(从 0 开始计数)有 i+1 个元素,且首尾元素为 1
for (int i = 0; i < numRows; ++i)
{
//例如:
// i=0 → 第 0 行大小为 1 → [1]
// i=1 → 第 1 行大小为 2 → [1, 1]
// i=2 → 第 2 行大小为 3 → [1, 1, 1](后续会被覆盖中间值)
vv[i].resize(i + 1, 1); //调整第 i 行的大小为 i+1,所有元素初始化为 1
}
/*------------------第二步:递推生成杨辉三角的中间值(从第三行开始,即 i=2)------------------*/
//杨辉三角的性质:第 i 行(i≥2)的第 j 个元素(j≥1 且 j<i)
//等于
//上一行(i-1 行)的第 j 个元素和第 j-1 个元素之和
for (int i = 2; i < numRows; ++i)
{
/* 注意事项:
* 遍历第 i 行的中间元素(跳过首尾,因为已经初始化为 1)
* j 的范围是 [1, i-1],因为首尾元素不需要计算(始终为 1)
*/
for (int j = 1; j < i; ++j)
{
//根据杨辉三角的递推公式计算:
// vv[i][j] = 上一行(i-1 行)的j列 + 上一行的j-1列
vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
}
}
return vv;
/* 返回局部对象 vv:
* C++98 会调用 vector 的拷贝构造函数,深拷贝整个二维向量(性能差)
* C++11 及之后会优先调用移动构造函数,直接转移资源(避免深拷贝)
*/
}
};整理汇总上面的内容: 好了现在我们先停一下,整理回顾一下上面的内容,明确一下接下来我们应该做什么?
我想此时,或许有小伙伴会踊跃提议:“既然左值引用不行,那就使用右值引用吧!C++11引入的新特性一定可以解决这个问题吧!” 当然也一定会有一些认真看了上面内容的小伙伴回答:“移动语义才是解决该问题的关键!” 所以接下来,我们聚焦探讨两个核心问题:
下面我们将场景二中的函数generate的返回类型进行修改:“左值引用 —> 右值引用”
vector<vector<int>> generate(...)
{
//...
}
vector<vector<int>>&& generate(...)
{
//...
}本质问题:函数结束时,局部对象 vv 会被销毁,右值引用返回的是已销毁对象的引用(悬垂引用)
产生本质问题的本质矛盾:
局部对象的生命周期由函数作用域决定,任何引用(左值/右值)都无法延长其生命周期,因此必须用传值返回。
1. 什么是移动语义?
移动语义(Move Semantics):是 C++11 引入的一项重要特性,主要用于提高程序性能,特别是在对象拷贝和赋值操作频繁的场景下,通过避免不必要的深拷贝,实现资源的高效转移。
2. 移动语义产生的背景是什么?
在 C++ 传统的拷贝构造函数和赋值运算符重载中,进行对象拷贝时会进行深拷贝,即把源对象的所有数据成员都复制一份到目标对象中。
传值返回的拷贝等)时,会带来较高的性能开销移动语义“移动” 而非 “拷贝” 返回,大幅提升性能 总结:通过理解左值引用的局限和传值返回的问题,才能体会移动语义的价值 —— 它让 “传值返回” 在语法简洁的同时,兼顾了性能!
3. 怎么实现移动语义? 移动语义的核心:在于允许资源从一个对象转移到另一个对象,而不是进行传统的深拷贝。
移动构造函数和移动赋值运算符右值引用(Type&&)接管临时对象的资源,避免深拷贝移动语义依赖两个特殊成员函数:
移动构造函数(Move Constructor):是一种特殊的构造函数,用于实现 “移动语义”。 移动构造函数的核心:将一个临时对象(右值)的资源 “窃取” 过来为己所用,而不是进行耗时的深拷贝,从而提高程序性能。
class MyClass
{
private:
//1.动态分配的资源(如:数组、缓冲区等)
//2.资源大小(如:数组长度)
int* data;
size_t size;
public:
/*------------------------------移动构造函数的定义格式------------------------------*/
//移动构造函数:通过右值引用接收临时对象,实现资源的高效转移
MyClass(MyClass&& other) noexcept //noexcept关键字声明:承诺不会抛出异常(移动操作应避免抛异常,否则可能导致资源泄漏)
{
// 核心逻辑:将临时对象(右值)的资源所有权转移到当前对象,而非复制资源
/*--------------第一步:接管资源--------------*/
data = other.data; //直接获取原对象的指针(无需分配新内存,避免深拷贝)
/*--------------第二步:置空原对象指针--------------*/
other.data = nullptr; //防止原对象析构时释放已转移的资源(必须操作)
//注意:这一步使原对象进入“有效但可析构”的状态(通常称为“被移动状态”)
/*--------------第三步:复制资源元数据--------------*/
size = other.size; //例如:大小信息
/*--------------第四步:重置原对象状态--------------*/
other.size = 0; //确保原对象析构时不会影响已转移的资源
//注意:原对象仍需保持可析构状态(如:调用默认析构函数不会崩溃)
}
};
移动构造函数的核心逻辑:
参数类型:必须是右值引用 MyString&&,确保只接收右值(临时对象)资源转移:直接拷贝原对象的资源指针(data = other.data),而非重新分配内存重置原对象:将原对象的指针置空(other.data = nullptr),避免析构时双重释放资源noexcept关键字:声明函数不会抛出异常,使容器(如:vector)在扩容时优先选择移动而非拷贝(提升性能)
移动赋值运算符(Move Assignment Operator):是实现移动语义的另一个核心工具。 移动赋值运算符的核心:用于将一个临时对象(右值)的资源 “窃取” 并赋值给一个已存在的对象,而非进行耗时的深拷贝赋值,从而优化性能。
class MyClass
{
private:
//1.动态分配的资源(如:数组、缓冲区等)
//2.资源大小(如:数组长度)
int* data;
size_t size;
public:
/*------------------------------移动赋值运算符的定义格式------------------------------*/
//移动赋值运算符:通过右值引用接收临时对象,实现资源的高效转移
MyClass& operator=(MyClass&& other) noexcept //noexcept关键字声明:承诺不会抛出异常(移动操作应避免抛异常,否则可能导致资源泄漏)
{
//1.自赋值检查:避免自己给自己赋值(如:a = std::move(a))
if (this != &other) //注意:若不检查,delete[] data 会释放自己的资源,后续又从自己窃取资源,导致悬空指针
{
//2.释放当前对象的旧资源:防止内存泄漏
delete[] data; //例如:当前对象持有动态分配的数组,需先释放
//3.从other中“窃取”资源 + 置空原对象指针
data = other.data; // 接管指针(如:动态数组、文件句柄等)
other.data = nullptr; // 置空原对象指针:防止原对象析构时重复释放资源
//4.复制资源元数据 + 重置原对象状态
size = other.size;
other.size = 0; //确保原对象析构时不会影响已转移的资源
//注意:原对象仍需保持可析构状态(如:调用默认析构函数不会崩溃)
}
//5.返回当前对象引用
return *this; //支持链式赋值(如 a = b = c)
}
};
移动赋值运算符的核心逻辑:
参数类型:移动赋值运算符同样接收一个右值引用参数
自我赋值检查:if (this != &other) 避免自己赋值给自己时出错
释放旧资源:先删除当前对象的资源(delete[] data),避免内存泄漏
资源转移:同移动构造函数,接管原对象的资源并重置原对象
在 C++ 中,
移动赋值运算符它与移动构造函数的区别在于: 移动构造函数用于初始化新对象,而移动赋值运算符用于更新已存在的对象
拷贝函数和移动函数的对比:
函数类型 | 参数类型 | 行为 | 性能 |
|---|---|---|---|
拷贝构造 | const MyString& | 深拷贝(重新分配资源) | O(n) |
移动构造 | MyString&& | 转移资源(复制指针) | O(1) |
拷贝赋值 | const MyString& | 深拷贝 + 释放旧资源 | O(n) |
移动赋值 | MyString&& | 转移资源 + 释放旧资源 | O(1) |
移动构造和移动赋值不会自动触发,需满足以下场景:
1. 传递临时对象(右值):
当函数返回一个局部对象时,如果满足一定条件(如没有发生返回值优化 RVO 或命名返回值优化 NRVO)
编译器会调用移动构造函数将局部对象的资源移动到调用者的对象中,而不是进行深拷贝
MyString func()
{
return MyString("临时对象"); // 返回临时对象(右值)
}
int main()
{
MyString s1 = func(); // 触发移动构造(而非拷贝)
}2. 使用 std::move 转换左值为右值:进行 std::move 操作时,也会触发移动语义。
int main()
{
MyString s2("左值对象");
MyString s3 = std::move(s2); // 用 std::move 强制转换左值为右值,触发移动构造
//注意:s2 被移动后变为“有效但未定义状态”,不应再使用(除非重新赋值)
}注意:
3. 容器操作:在 std::vector、std::list 等标准库容器中插入元素时,会触发移动语义。
#include <vector>
int main()
{
std::vector<MyString> vec;
vec.push_back(MyString("hello")); // 临时对象触发移动构造
}代码案例:拷贝函数和移动函数在实际中的应用
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
#include<string.h>
#include<algorithm>
using namespace std;
namespace mySpace
{
class string
{
private:
char* _str; // 存储字符串的动态数组(以'\0'结尾)
size_t _size; // 字符串有效长度(不包含'\0')
size_t _capacity;// 最多可存储的字符数(不包含'\0')
public:
/*--------------------------类型重定义--------------------------*/
// 迭代器类型定义(支持范围for循环、算法库操作)
//1.对“普通迭代器”进行重命名:char* ---> iterator
typedef char* iterator;
//2.对“常量迭代器”进行重命名:const char* ---> const_iterator
typedef const char* const_iterator;
/*--------------------------获取迭代器的成员函数--------------------------*/
//1.实现:获取“普通迭代器”
iterator begin() { return _str; }
iterator end() { return _str + _size; }
//2.实现:获取“const迭代器”
const_iterator begin() const { return _str; }
const_iterator end() const { return _str + _size; }
/*--------------------------默认成员函数--------------------------*/
//1.实现:“普通构造函数”:用C风格字符串初始化
string(const char* str = "") //参数默认值为空字符串,支持无参构造
: _size(strlen(str)) // 初始化字符串长度(不包含'\0')
, _capacity(_size) // 初始化容量(与长度相同)
{
//1.打印提示信息
cout << "string(char* str) -- 普通构造" << endl;
//2.分配内存
_str = new char[_capacity + 1]; //注意:+1用于存储'\0'
//3.复制字符串内容
strcpy(_str, str); //注意:包含'\0'
}
//2.实现:“交换函数”
void swap(string& s) //作用:交换两个string对象的资源,用于拷贝/移动操作中的资源转移,避免深拷贝
{
//本质:调用全局swap函数交换成员变量(指针、大小、容量)
//1.指针
::swap(_str, s._str);
//2.大小
::swap(_size, s._size);
//3.容量
::swap(_capacity, s._capacity);
}
//3.实现:“拷贝构造函数”:用已存在的string对象初始化新对象(深拷贝)
string(const string& s) //参数为const左值引用,接收左值(如:已定义的变量)
: _str(nullptr) //注意:先初始化为空指针,避免swap时释放随机内存
{
//1.打印提示信息
cout << "string(const string& s) -- 拷贝构造" << endl;
//2.预分配与s相同的容量
reserve(s._capacity);
//3.遍历s的每个字符,逐个插入到新对象中(深拷贝)
for (auto ch : s)
{
push_back(ch);
}
}
//4.实现:“移动构造函数”
string(string&& s) //参数为右值引用(string&&),仅接收右值(如:临时对象、std::move转换的左值)
{
//1.打印提示信息
cout << "string(string&& s) -- 移动构造" << endl;
//2.直接交换当前对象与临时对象的资源
swap(s); //注意:交换后,临时对象会持有原空资源,析构时不会影响新对象
}
//5.实现:“拷贝赋值运算符”:
string& operator=(const string& s) //用左值对象赋值(深拷贝);返回值为引用,支持链式赋值(如:a = b = c)
{
//1.打印提示信息
cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
//2.进行自赋值检查避免自己给自己赋值
if (this != &s)
{
//2.清空当前对象内容(准备接收新数据)
_str[0] = '\0';
_size = 0;
//3.预分配足够容量
reserve(s._capacity);
//4.逐个拷贝s的字符(深拷贝)
for (auto ch : s)
{
push_back(ch);
}
}
//5.返回当前对象引用
return *this;
}
//6.实现:“移动赋值运算符”:用右值对象赋值(资源转移)
string& operator=(string&& s) //参数为右值引用,接收临时对象或std::move转换的左值
{
//1.打印提示信息
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
//2.交换资源
swap(s); //当前对象接管s的资源,s接管原空资源
//3.返回当前对象引用
return *this;
}
//7.实现:“析构函数”:释放动态分配的资源
~string()
{
//1.打印提示信息
cout << "~string() -- 析构" << endl;
//2.释放字符串数组
delete[] _str; //
//3.置空指针 + 置零大小和容量
_str = nullptr;
_size = 0;
_capacity = 0;
}
/*--------------------------基本操作的成员函数--------------------------*/
//1.实现:“下标访问运算符”
char& operator[](size_t pos) //注意:修改字符(非const对象使用)
{
//1.断言检查下标合法性
assert(pos < _size); //注意:若pos >= _size,程序中断并提示
//2.返回字符引用,支持修改
return _str[pos];
}
//2.实现:“预留容量”
void reserve(size_t n)
{
if (n > _capacity) //注意:参数n为目标容量,仅当n > 当前容量时扩容
{
//1.分配新内存
char* tmp = new char[n + 1]; //+1存'\0'
//2.若当前有资源
if (_str)
{
//2.1:先复制内容
strcpy(tmp, _str);
//2.2:再释放旧内存
delete[] _str;
}
//3.指向新内存
_str = tmp;
//4.更新容量
_capacity = n;
}
}
//3.实现:“尾插字符”
void push_back(char ch)
{
//1.若容量不足,先扩容
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
//2.插入字符
_str[_size] = ch;
//3.更新长度
++_size;
//4.维护字符串结束符
_str[_size] = '\0';
}
//4.实现:“复合赋值运算符”
string& operator+=(char ch)
{
//1.调用尾插函数
push_back(ch);
//2.返回当前对象引用
return *this;
}
//5.实现:“获取C风格字符串”
const char* c_str() const
{
return _str; //返回常量指针,确保字符串不可修改
}
//6.实现:“获取当前字符串长度”
size_t size() const
{
return _size;
}
};
}
// 测试函数:验证各种构造和赋值操作的行为
int main()
{
//1.普通构造:用C字符串初始化s1
mySpace::string s1("xxxxx");
//2.拷贝构造:用左值s1初始化s2(触发深拷贝)
mySpace::string s2 = s1;
//3.临时对象构造+移动构造:
// 编译器优化(RVO):直接构造s3,跳过临时对象和移动构造
mySpace::string s3 = mySpace::string("yyyyy");
//4.移动构造:用std::move将s1转为右值,s4接管s1的资源
mySpace::string s4 = move(s1);
cout << "******************************" << endl;
return 0;
} 
关于移动语义的注意事项:
被移动对象的状态:移动后原对象(如 s2)必须处于 “可析构且安全” 的状态(通常指针置空、长度为 0),但不应再使用其资源(内容可能已失效)编译器优化:若函数返回局部对象,编译器可能触发 RVO(返回值优化),直接在调用方构造对象,跳过移动 / 拷贝(性能更优)默认移动函数:若类未手动定义移动构造 / 赋值,编译器会在未定义拷贝函数和析构函数时自动生成默认版本。但建议手动定义以确保资源正确转移 通过实现移动构造函数和移动赋值运算符,即可利用移动语义将资源 “转移” 而非 “拷贝”
大幅提升处理大型对象(如:字符串、容器)时的性能,尤其适合函数返回局部对象、容器插入元素等场景。
移动语义的价值是什么?
std::vector、std::list、std::map 等容器中插入元素时,如果元素类型支持移动语义,会大大提高插入操作的效率
vector 中插入大量元素时,移动语义可以避免不必要的深拷贝,提升性能总的来说:移动语义是 C++11 中一项非常强大的特性,它让 C++ 程序在处理对象拷贝和资源管理时更加高效,是编写高性能 C++ 代码的重要手段之一。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include <assert.h>
using namespace std;
namespace mySpace
{
class string
{
private:
char* _str; // 存储字符串的动态数组(以'\0'结尾)
size_t _size; // 字符串有效长度(不包含'\0')
size_t _capacity;// 最多可存储的字符数(不包含'\0')
public:
/*--------------------------类型重定义--------------------------*/
// 迭代器类型定义(支持范围for循环、算法库操作)
//1.对“普通迭代器”进行重命名:char* ---> iterator
typedef char* iterator;
//2.对“常量迭代器”进行重命名:const char* ---> const_iterator
typedef const char* const_iterator;
/*--------------------------获取迭代器的成员函数--------------------------*/
//1.实现:获取“普通迭代器”
iterator begin() { return _str; }
iterator end() { return _str + _size; }
//2.实现:获取“const迭代器”
const_iterator begin() const { return _str; }
const_iterator end() const { return _str + _size; }
/*--------------------------默认成员函数--------------------------*/
//1.实现:“普通构造函数”:用C风格字符串初始化
string(const char* str = "") //参数默认值为空字符串,支持无参构造
: _size(strlen(str)) // 初始化字符串长度(不包含'\0')
, _capacity(_size) // 初始化容量(与长度相同)
{
//1.打印提示信息
cout << "string(char* str) -- 构造" << endl;
//2.分配内存
_str = new char[_capacity + 1]; //注意:+1用于存储'\0'
//3.复制字符串内容
strcpy(_str, str); //注意:包含'\0'
}
//2.实现:“交换函数”
void swap(string& s) //作用:交换两个string对象的资源,用于拷贝/移动操作中的资源转移,避免深拷贝
{
//本质:调用全局swap函数交换成员变量(指针、大小、容量)
//1.指针
::swap(_str, s._str);
//2.大小
::swap(_size, s._size);
//3.容量
::swap(_capacity, s._capacity);
}
//3.实现:“拷贝构造函数”:用已存在的string对象初始化新对象(深拷贝)
string(const string& s) //参数为const左值引用,接收左值(如:已定义的变量)
: _str(nullptr) //注意:先初始化为空指针,避免swap时释放随机内存
{
//1.打印提示信息
cout << "string(const string& s) -- 拷贝构造" << endl;
//2.预分配与s相同的容量
reserve(s._capacity);
//3.遍历s的每个字符,逐个插入到新对象中(深拷贝)
for (auto ch : s)
{
push_back(ch);
}
}
//4.实现:“移动构造函数”
string(string&& s) //参数为右值引用(string&&),仅接收右值(如:临时对象、std::move转换的左值)
{
//1.打印提示信息
cout << "string(string&& s) -- 移动构造" << endl;
//2.直接交换当前对象与临时对象的资源
swap(s); //注意:交换后,临时对象会持有原空资源,析构时不会影响新对象
}
//5.实现:“拷贝赋值运算符”:
string& operator=(const string& s) //用左值对象赋值(深拷贝);返回值为引用,支持链式赋值(如:a = b = c)
{
//1.打印提示信息
cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
//2.进行自赋值检查避免自己给自己赋值
if (this != &s)
{
//2.清空当前对象内容(准备接收新数据)
_str[0] = '\0';
_size = 0;
//3.预分配足够容量
reserve(s._capacity);
//4.逐个拷贝s的字符(深拷贝)
for (auto ch : s)
{
push_back(ch);
}
}
//5.返回当前对象引用
return *this;
}
//6.实现:“移动赋值运算符”:用右值对象赋值(资源转移)
string& operator=(string&& s) //参数为右值引用,接收临时对象或std::move转换的左值
{
//1.打印提示信息
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
//2.交换资源
swap(s); //当前对象接管s的资源,s接管原空资源
//3.返回当前对象引用
return *this;
}
//7.实现:“析构函数”:释放动态分配的资源
~string()
{
//1.打印提示信息
cout << "~string() -- 析构" << endl;
//2.释放字符串数组
delete[] _str; //
//3.置空指针 + 置零大小和容量
_str = nullptr;
_size = 0;
_capacity = 0;
}
/*--------------------------基本操作的成员函数--------------------------*/
//1.实现:“下标访问运算符”
char& operator[](size_t pos) //注意:修改字符(非const对象使用)
{
//1.断言检查下标合法性
assert(pos < _size); //注意:若pos >= _size,程序中断并提示
//2.返回字符引用,支持修改
return _str[pos];
}
//2.实现:“预留容量”
void reserve(size_t n)
{
if (n > _capacity) //注意:参数n为目标容量,仅当n > 当前容量时扩容
{
//1.分配新内存
char* tmp = new char[n + 1]; //+1存'\0'
//2.若当前有资源
if (_str)
{
//2.1:先复制内容
strcpy(tmp, _str);
//2.2:再释放旧内存
delete[] _str;
}
//3.指向新内存
_str = tmp;
//4.更新容量
_capacity = n;
}
}
//3.实现:“尾插字符”
void push_back(char ch)
{
//1.若容量不足,先扩容
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
//2.插入字符
_str[_size] = ch;
//3.更新长度
++_size;
//4.维护字符串结束符
_str[_size] = '\0';
}
//4.实现:“复合赋值运算符”
string& operator+=(char ch)
{
//1.调用尾插函数
push_back(ch);
//2.返回当前对象引用
return *this;
}
//5.实现:“获取C风格字符串”
const char* c_str() const
{
return _str; //返回常量指针,确保字符串不可修改
}
//6.实现:“获取当前字符串长度”
size_t size() const
{
return _size;
}
};
// 大数加法函数:将两个string表示的数字相加(模拟字符串加法)
string addStrings(string num1, string num2)
{
string str; // 存储结果的空字符串
int end1 = num1.size() - 1; // num1 的最后一位索引
int end2 = num2.size() - 1; // num2 的最后一位索引
int next = 0; // 进位值(初始为0)
// 从低位到高位逐位相加(处理两个数的每一位)
while (end1 >= 0 || end2 >= 0)
{
// 获取当前位的数值(越界时视为0)
int val1 = (end1 >= 0) ? (num1[end1--] - '0') : 0;
int val2 = (end2 >= 0) ? (num2[end2--] - '0') : 0;
// 计算当前位总和(包含进位)
int ret = val1 + val2 + next;
next = ret / 10; // 更新进位(商)
ret = ret % 10; // 当前位结果(余数)
// 将当前位结果追加到str(注意:此时是逆序存储)
str += ('0' + ret);
}
// 处理最后剩余的进位(如 999 + 1 会产生进位)
if (next == 1)
{
str += '1';
}
// 反转字符串,恢复正确顺序(因为之前是逆序存储的)
reverse(str.begin(), str.end());
// 调试输出(观察中间结果)
cout << "*************************" << endl;
// 返回结果字符串(触发移动构造或拷贝构造,取决于编译器优化)
return str;
}
}
// 场景1:直接用返回值初始化新对象
int main()
{
//1.调用addStrings,返回临时对象,用ret接收
mySpace::string ret = mySpace::addStrings("11111", "2222");
// 输出结果(如 "13333")
cout << ret.c_str() << endl;
return 0;
}
// 场景2:先构造对象,再用返回值赋值
int main()
{
//1.先默认构造一个空对象
mySpace::string ret;
//2.调用addStrings,返回临时对象,赋值给ret
ret = mySpace::addStrings("1111", "2222");
//3.输出结果
cout << ret.c_str() << endl;
return 0;
}
上图展示了同一段代码在不同的环境下,编译器对拷贝构造的优化:
注意:在 Linux 环境验证时:
test.cpp
g++ test.cpp -fno-elide-constructors 编译(-fno-elide-constructors 作用是关闭编译器的返回值优化)
以下详细拆解 “构造→拷贝构造” 反复出现的完整流程,解释每一行输出的根源:
本质:函数参数构造、函数内部对象构造、返回值传递 三个阶段的叠加。
阶段 1:addStrings 的参数构造(num1 和 num2)
调用 addStrings(“11111”, “2222”) 时,需要先构造两个参数 num1 和 num2:
string addStrings(string num1, string num2) { ... }num1 的普通构造:
"11111" 是 C 风格字符串,触发 string(const char* str) 普通构造:
string(char* str) -- 构造 // 输出第1行:构造 num1num1 的拷贝构造: 由于 addStrings 的参数是按值传递(string num1),需要将实参拷贝到函数栈帧,触发拷贝构造:
string(const string& s) -- 拷贝构造 // 输出第2行:num1 拷贝到函数参数num2 的普通构造:
"2222" 同理,触发 string(const char* str) 普通构造:
string(char* str) -- 构造 // 输出第3行:构造 num2num2 的拷贝构造: 按值传递触发拷贝构造:
string(const string& s) -- 拷贝构造 // 输出第4行:num2 拷贝到函数参数阶段 2:addStrings 内部构造 str
进入 addStrings 函数后,第一行代码:
string str; 触发 string 的普通构造(因默认构造被 string(const char* str = "") 覆盖,等价于构造空字符串):
string(char* str) -- 构造 // 输出第5行:构造 addStrings 内部的 str阶段 3:addStrings 返回值的传递(关闭优化时)
函数返回时,由于编译参数 "-fno-elide-constructors" 关闭了返回值优化(RVO),编译器会:
return str;先构造一个临时对象(右值),通过拷贝构造将str的内容复制到临时对象:
string(const string& s) -- 拷贝构造 // 输出第6行:str 拷贝到临时对象然后销毁 addStrings 内部的 str,析构输出。
阶段 4:main 中 ret 的构造
临时对象返回后,main 中:
mySpace::string ret = ...;会通过拷贝构造将临时对象的内容复制到 ret:
string(const string& s) -- 拷贝构造 // 输出第7行:临时对象拷贝到 ret阶段 5:析构流程
程序结束时,会按构造逆序析构对象:
retnum2 的函数参数拷贝num2num1 的函数参数拷贝num1addStrings 内部的 str每次析构都会输出:
~string() -- 析构输出对应关系总结:
你看到的输出:
//与上述流程的对应关系为:
构造 → num1 普通构造
拷贝构造 → num1 按值传递的拷贝构造
构造 → num2 普通构造
拷贝构造 → num2 按值传递的拷贝构造
-------------
构造 → addStrings 内部 str 的普通构造
-------------
拷贝构造 → str 拷贝到临时对象
拷贝构造 → 临时对象拷贝到 ret 关键结论:为什么只有拷贝构造?
在 “没有移动构造” 的场景下,本质是:
string(string&&)),编译器不会自动生成移动构造(或生成的默认移动构造被忽略)若要触发移动构造,需:
string(string&&) 移动构造函数详细拆解三种输出场景的差异根源:
三种输出结果的差异的本质:编译器对返回值优化(RVO/NRVO)的应用程度不同 以及是否显式实现移动构造函数:
-fno-elide-constructors 完全关闭优化,严格按值传递逻辑触发所有拷贝构造逐场景详细解析:
场景 2:三次构造 + 一次拷贝
string(char* str) -- 构造
string(char* str) -- 构造
string(char* str) -- 构造
string(const string& s) -- 拷贝构造 流程解析:
num1 和 num2 的构造:
mySpace::string ret = mySpace::addStrings("11111", "2222");"11111" → 构造 num1"2222" → 构造 num2addStrings 内部 str 的构造:
string str; // 触发普通构造返回值优化(RVO)生效:
str 是函数返回值,直接在 ret 的内存位置构造 str(跳过临时对象)场景 3:仅三次构造
string(char* str) -- 构造
string(char* str) -- 构造
string(char* str) -- 构造 流程解析:
num1 和 num2 的构造:
mySpace::string ret = mySpace::addStrings("11111", "2222");"11111" → 构造 num1"2222" → 构造 num2addStrings 内部 str 的构造:
string str; // 触发普通构造无拷贝构造:
str 的构造直接复用 ret 的内存,无需任何拷贝



上图展示了同一段代码在不同的环境下,编译器对拷贝构造的优化:
步骤 1:ret 的默认构造
mySpace::string ret;触发 string 的默认构造(因 string(const char* str = "") 覆盖默认构造,等价于构造空字符串):
string(char* str) -- 构造 // 输出第1行:构造 ret(空字符串)步骤 2:addStrings 的参数构造(num1 和 num2)
调用 addStrings(“1111”, “2222”) 时,需构造参数 num1 和 num2:
string addStrings(string num1, string num2) { ... }num1 的构造:
"1111" 是 C 风格字符串,触发 string(const char* str) 普通构造:
string(char* str) -- 构造 // 输出第2行:构造 num1num1 的拷贝构造:
因参数是按值传递(string num1),需将实参拷贝到函数栈帧,触发拷贝构造:
string(const string& s) -- 拷贝构造 // 输出第3行:num1 拷贝到函数参数num2 的构造:
"2222" 同理,触发 string(const char* str) 普通构造:
string(char* str) -- 构造 // 输出第4行:构造 num2num2 的拷贝构造: 按值传递触发拷贝构造:
string(const string& s) -- 拷贝构造 // 输出第5行:num2 拷贝到函数参数步骤 3:addStrings 内部构造 str
进入 addStrings 函数后,第一行代码:
string str; 触发 string 的普通构造(空字符串):
string(char* str) -- 构造 // 输出第6行:构造 addStrings 内部的 str步骤 4:addStrings 返回值的拷贝构造(关闭优化时)
函数返回时:
return str;因为未显式实现移动构造,且关闭优化,所以触发拷贝构造生成临时对象:
string(const string& s) -- 拷贝构造 // 输出第7行:str 拷贝到临时对象步骤 5:ret 的拷贝赋值
临时对象返回后,执行:
ret = 临时对象;触发拷贝赋值运算符(因无移动赋值):
string& operator=(const string& s) -- 拷贝赋值 // 输出第8行:临时对象拷贝赋值给 ret步骤 6:析构流程:
程序结束时,按构造逆序析构对象:
retnum2 的函数参数拷贝num2num1 的函数参数拷贝num1addStrings 内部的 str每次析构输出:
~string() -- 析构三种输出场景的差异解析 : 场景 1:完整拷贝流程(多次构造 + 拷贝)
-fno-elide-constructors),严格触发所有拷贝场景 2:部分优化(减少拷贝次数)
场景 3:优化全开(仅必要构造)
ret 内存位置构造返回值移动语义未触发的原因? 在 “只有
拷贝 构造/赋值,没有移动 构造/赋值” 的场景中,本质是类未显式实现移动构造和移动赋值函数,导致:
在 “只有拷贝 构造/赋值” 的场景中:
string(string&&) 和 string& operator=(string&&),编译器无法触发移动 构造/赋值拷贝 构造/赋值(深拷贝资源)

引用折叠:是 C++11 引入的一个重要特性,它和模板、右值引用等概念紧密相关,在一些复杂的类型推导场景中发挥着关键作用。
在 C++ 中:
int& && r = i;(i 是一个int类型变量)是不被允许的,会直接导致编译报错
模板实例化或使用typedef进行类型操作时,可能会出现多个引用叠加的情况,这时候就需要引用折叠规则来确定最终的引用类型
简单来说:引用折叠就是在特定情况下,将多个连续的引用类型 “折叠” 成一个引用类型
当通过 模板/typedef 构造出 “引用的引用” 时,C++11 用引用折叠规则统一处理:
T& & → T& (左值引用 —> 折叠为 —> 左值引用)T& && → T& (左值引用和右值引用 —> 折叠为 —> 左值引用)T&& & → T& (右值引用和左值引用 —> 折叠为 —> 左值引用)T&& && → T&& (右值引用 —> 折叠为 —> 右值引用)引用折叠规则总结:
模板/typedef 场景的演示与理解:
template <typename T>
void func(T&& x)
{
/* ... */
}这里的 T&& x 看起来是 “右值引用参数”,但结合引用折叠规则后,它会根据传入实参的类型 “自适应”:
T 会被推导为 左值引用类型(如:int&),结合引用折叠后,形参会变成 左值引用(int&)T 会被推导为 非引用类型(如:int),结合引用折叠后,形参会变成 右值引用(int&&)这种能同时适配左值、右值的模板参数,也被称为万能引用(或 “转发引用”)
代码案例:引用折叠实战
//由于引用折叠规则,f1 实例化后总是左值引用
template<class T>
void f1(T& x) // 形参是“左值引用”,不参与折叠
{
// 函数体为空,仅用于演示类型推导
}
//由于引用折叠规则,f2 实例化后可以是左值引用或右值引用
template<class T>
void f2(T&& x) // 形参是“万能引用”,会触发引用折叠
{
// 函数体为空,仅用于演示类型推导
}
int main()
{
//1. 用 typedef 定义引用类型
typedef int& lref; // 左值引用类型别名
typedef int&& rref; // 右值引用类型别名
int n = 0; // 定义左值 n
//2. 测试 typedef 引用的“引用折叠”
lref& r1 = n; // 等价于 int& & → 折叠为 int& (左值引用)
lref&& r2 = n; // 等价于 int& && → 折叠为 int& (左值引用)
rref& r3 = n; // 等价于 int&& & → 折叠为 int& (左值引用)
rref&& r4 = 1; // 等价于 int&& && → 折叠为 int&& (右值引用)
/*----------------------------测试 f1 模板----------------------------*/
//1. 测试 f1 模板 + int类型参数
// 无折叠 → 实例化为 void f1(int& x)
f1<int>(n); // 正确:n 是左值,匹配 int&
// f1<int>(0); // 报错:0 是右值,无法绑定到 int&
//2. 测试 f1 模板 + 左值引用参数
// 折叠 → 实例化为 void f1(int& x)
f1<int&>(n); // 正确:n 是左值,T& 是 int& → 形参是 int&
// f1<int&>(0); // 报错:0 是右值,无法绑定到 int&
//3. 测试 f1 模板 + 右值引用参数
// 折叠 → 实例化为 void f1(int& x)
f1<int&&>(n); // 正确:n 是左值,T& 是 int&& → 折叠为 int&
// f1<int&&>(0); // 报错:0 是右值,但 f1 形参是 T&(折叠后仍为左值引用)
//4. 测试 f1 模板 + const 左值引用参数
// 折叠 → 实例化为 void f1(const int& x)
f1<const int&>(n); // 正确:n 是左值,匹配 const int&
f1<const int&>(0); // 正确:0 是右值,匹配 const int&
//5. 测试 f1 模板 + const 右值引用参数
// 折叠 → 实例化为 void f1(const int& x)
f1<const int&&>(n); // 正确:n 是左值,T& 是 const int&& → 折叠为 const int&
f1<const int&&>(0); // 正确:0 是右值,匹配 const int&
/*----------------------------测试 f2 模板(万能引用)----------------------------*/
//1. 测试 f2 模板 + int类型参数
// 无折叠 → 实例化为 void f2(int&& x)
// f2<int>(n); // 报错:n 是左值,无法绑定到 int&&
f2<int>(0); // 正确:0 是右值,匹配 int&&
//2. 测试 f2 模板 + 左值引用参数
// 折叠 → 实例化为 void f2(int& x)
f2<int&>(n); // 正确:n 是左值,T&& 是 int& → 折叠为 int&
// f2<int&>(0); // 报错:0 是右值,无法绑定到 int&
//3. 测试 f2 模板 + 右值引用参数
// 折叠 → 实例化为 void f2(int&& x)
// f2<int&&>(n); // 报错:n 是左值,无法绑定到 int&&
f2<int&&>(0); // 正确:0 是右值,匹配 int&&
return 0;
}
代码案例:万能引用实战 + 完美转发基础
#include <iostream>
using namespace std;
/* 万能引用(转发引用):根据传入实参的左值/右值属性,自动推导 T 的类型
* 1.传入左值时,T 推导为左值引用类型(如:int&)
* 2.传入右值时,T 推导为非引用类型(如:int)
*/
template<class T>
void Function(T&& t)
{
//1.创建局部变量 a(左值)
int a = 0;
//2.根据 T 的类型进行绑定
T x = a;
/* 根据 T 的类型,此行行为差异巨大:
* 1. 若 T 是 int(传入右值) → 等价于 int x = a;(拷贝构造)
* 2. 若 T 是 int&(传入左值) → 等价于 int& x = a;(引用绑定,无拷贝)
* 3. 若 T 是 const int&(传入 const 左值)→ 等价于 const int& x = a;(常量引用绑定)
*/
// x++; // 若 T 是 const 类型,此行会报错(如:传入 const int&)
//3.打印局部变量 a 的地址
cout << &a << endl;
//4.打印 x 的地址
cout << &x << endl;
/* 核心观察点:
* 1. 若T是int ---> x 是“引用” (如:int&),则 &x 与 &a 地址相同(复用资源) (地址不同 → 资源拷贝)
* 2. 若T是int&/const int& ---> x 是“值类型”(如:int),则 &x 与 &a 地址不同(资源拷贝) (地址相同 → 引用绑定)
*
*/
}
int main()
{
cout << "传入右值(10)" << endl;
//
//1. 传入右值(10)
// 推导 T 为 int → 形参是 int&&(右值引用)
Function(10);
cout << "------------------\n" << endl;
cout << "传入左值(a)" << endl;
//2. 传入左值(a)
// 推导 T 为 int& → 形参是 int&(左值引用,引用折叠)
int a;
Function(a);
cout << "------------------\n" << endl;
cout << "传入const左值(b)" << endl;
//3. 传入 const 左值(b)
// 推导 T 为 const int& → 形参是 const int&(左值引用,引用折叠)
// 注意:Function 内 x 是 const int,x++ 会报错
const int b = 8;
Function(b);
cout << "------------------\n" << endl;
cout << "传入const右值(move(b))" << endl;
//4. 传入 const 右值(move(b))
// 推导 T 为 const int → 形参是 const int&&(右值引用,引用折叠)
// 注意:Function 内 x 是 const int,x++ 会报错
Function(move(b));
return 0;
}
完美转发:是 C++11 及以后引入的一项重要特性,它允许函数模板将其参数原封不动地转发给另一个函数,保持参数的原始值类别(左值或右值)和常量性
引用折叠 和 std::forward 实现,用于在函数模板中精准保留参数的 “左值/右值属性”,并将其原封不动地转发给下一层函数右值引用的 “左值属性” 问题:
T&& )绑定,右值引用变量本身仍属于左值(可被取地址、赋值等 )这会导致一个问题:
Function 函数内部,若把 t 传递给下一层函数 Fun,t 会被当作左值处理,只能匹配 Fun 的左值引用版本(如:Fun(int&) )
t 原始的 “左值/右值属性”(比如:让右值继续匹配 Fun 的右值引用版本 ),这时就需要 完美转发 来解决
示例问题(无完美转发时):
#include <iostream>
using namespace std;
void Fun(int& x) { cout << "左值引用版本\n"; }
void Fun(int&& x) { cout << "右值引用版本\n"; }
template<typename T>
void Function(T&& t)
{
Fun(t); //t 是右值引用变量,但本身是左值 → 调用 Fun(int&)
}
int main()
{
Function(10); //期望调用 Fun(int&&),实际调用 Fun(int&)
return 0;
}
完美转发通过 std::forward 函数模板实现,核心依赖 引用折叠 和 模板参数推导
std::forward 的简化原理:
std::forward 利用引用折叠和模板参数推导,“还原” 参数原始的左值/右值属性
// std::forward函数模板的关键逻辑可简化为:
template <typename T>
T&& forward(typename remove_reference<T>::type& arg)
{
return static_cast<T&&>(arg); // 强制类型转换,触发引用折叠
}完美转发的流程
以 Function(10) 为例,完整流程:
10 → Function 的 T 推导为 int(非引用类型 ),形参 t 是 int&&(右值引用 )
Function 中调用 Fun(std::forward<T>(t)) → T 是 int,std::forward<int>(t) 会:
static_cast<int&&>(t) 将 t 强转为 int&&(右值引用 )
Fun(int&&) 重载,保留右值属性
#include <iostream>
using namespace std;
void Fun(int& x) { cout << "左值引用版本\n"; }
void Fun(int&& x) { cout << "右值引用版本\n"; }
//简化版 std::forward 实现(实际标准库更复杂)
template <typename T>
T&& forward(typename remove_reference<T>::type& arg)
{
return static_cast<T&&>(arg);
/* 核心逻辑:根据 T 的推导类型,通过 static_cast 强制转换 arg 的类型
* 触发引用折叠规则:
* 1. 若 T 是左值引用(如 int&),T&& 折叠为 int&
* 2. 若 T 是右值引用(如 int) ,T&& 折叠为 int&&
*/
}
// 4. 万能引用模板函数:接收左值/右值,保留原始值类别
template<typename T>
void Function(T&& t)
{
Fun(std::forward<T>(t));
/* 完美转发关键:将 t 的原始值类别(左值/右值)通过 forward 传递给 Fun
* T 的推导规则:
* 1. 传入左值时,T 推导为 int&,forward<int&>(t) 返回 int&
* 2. 传入右值时,T 推导为 int,forward<int>(t) 返回 int&&
*/
}
int main()
{
int a = 10;
Function(a); // 传入左值 → 调用 Fun(int&)
Function(20); // 传入右值 → 调用 Fun(int&&)
return 0;
}
// 调用路径解析:
// 1. Function(a):
// - 传入左值 → T 推导为 int& → Function 实例化为 void Function(int& && t)
// - 引用折叠后,形参 t 变为 int&(左值引用)
// - std::forward<int&>(t) 返回 int&,调用 Fun(int&)
// 2. Function(20):
// - 传入右值 → T 推导为 int → Function 实例化为 void Function(int&& t)
// - 形参 t 是右值引用,但 t 本身是左值(变量)
// - std::forward<int>(t) 返回 int&&,调用 Fun(int&&)