首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >《C++进阶之C++11》【移动语义 + 完美转发】

《C++进阶之C++11》【移动语义 + 完美转发】

作者头像
序属秋秋秋
发布2025-12-18 16:13:43
发布2025-12-18 16:13:43
2290
举报

往期《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++ 领域中堪称“性能优化利器”的核心内容:【移动语义 + 完美转发】 ٩(ˊᗜˋ*)و✨ 准备好一起把拷贝按在地上摩擦了吗?准备好的话,就赶紧上车,让我们出发吧!(≖‿‿≖✿)

------------移动语义------------

1. 左值引用的主要使用场景有哪些?

左值引用的核心价值是减少拷贝

  • 函数传参:通过 T& 直接操作实参,避免拷贝大对象
  • 函数返回值:通过 T& 返回对象,避免拷贝返回值(需保证返回对象生命周期独立于函数)

但左值引用 无法解决 “返回局部对象” 的问题:

  • 若函数 返回局部对象(如:栈上的临时对象),无论用左值引用还是右值引用返回,都会因对象销毁导致悬垂引用
  • 此时只能用传值返回,但会带来拷贝开销(C++98 时代的痛点)

场景 1:函数 返回局部对象 时触发深拷贝导致的大量性能开销

代码语言:javascript
复制
#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:函数 返回局部对象 时触发深拷贝导致的大量性能开销

代码语言:javascript
复制
#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引入的新特性一定可以解决这个问题吧!” 当然也一定会有一些认真看了上面内容的小伙伴回答:“移动语义才是解决该问题的关键!” 所以接下来,我们聚焦探讨两个核心问题:

  • 为何右值引用无法解决 “返回局部对象” 的问题?
  • 移动语义究竟是如何攻克 “返回局部对象” 难题的?

2. 为何右值引用无法解决 “返回局部对象” 的问题?

下面我们将场景二中的函数generate的返回类型进行修改:“左值引用 —> 右值引用”

代码语言:javascript
复制
vector<vector<int>> generate(...) 
{ 
    //... 
}

vector<vector<int>>&& generate(...) 
{ 
    //... 
}

本质问题:函数结束时,局部对象 vv 会被销毁,右值引用返回的是已销毁对象的引用(悬垂引用)

  • 因此:右值引用和左值引用一样无法解决本质问题,所以即使将函数的返回类型改为右值引用也无法解决 “局部对象返回” 问题。

产生本质问题的本质矛盾:

局部对象的生命周期由函数作用域决定,任何引用(左值/右值)都无法延长其生命周期,因此必须用传值返回。

3. 移动语义究竟是如何攻克 “返回局部对象” 难题的?

1. 什么是移动语义? 移动语义(Move Semantics):是 C++11 引入的一项重要特性,主要用于提高程序性能,特别是在对象拷贝赋值操作频繁的场景下,通过避免不必要的深拷贝,实现资源的高效转移。


2. 移动语义产生的背景是什么? 在 C++ 传统的拷贝构造函数赋值运算符重载中,进行对象拷贝时会进行深拷贝,即把源对象的所有数据成员都复制一份到目标对象中。

  • 这在处理大型对象(如:传值返回的拷贝等)时,会带来较高的性能开销
  • 为了解决类似“传值返回的拷贝开销” 这样的问题,C++11 引入了 移动语义
  • 移动语义让传值返回的对象可以通过 “移动” 而非 “拷贝” 返回,大幅提升性能

总结:通过理解左值引用的局限传值返回的问题,才能体会移动语义的价值 —— 它让 “传值返回” 在语法简洁的同时,兼顾了性能!


3. 怎么实现移动语义? 移动语义的核心:在于允许资源从一个对象转移到另一个对象,而不是进行传统的深拷贝。

  • 要实现移动语义,核心是定义 移动构造函数移动赋值运算符
  • 通过 右值引用Type&&)接管临时对象的资源,避免深拷贝

移动语义依赖两个特殊成员函数:

  1. 移动构造函数:接收右值引用参数(临时对象)“窃取” 临时对象的资源初始化新对象时调用,转移资源(避免深拷贝)
  2. 移动赋值运算符:用右值为已有对象赋值时调用,转移资源
3.1:移动构造函数

移动构造函数(Move Constructor):是一种特殊的构造函数,用于实现 “移动语义”。 移动构造函数的核心将一个临时对象(右值)的资源 “窃取” 过来为己所用,而不是进行耗时的深拷贝,从而提高程序性能。

  • 尤其适用于管理动态内存、文件句柄等资源的对象
代码语言:javascript
复制
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)在扩容时优先选择移动而非拷贝(提升性能)
3.2:移动赋值运算符

移动赋值运算符(Move Assignment Operator):是实现移动语义的另一个核心工具。 移动赋值运算符的核心用于将一个临时对象(右值)的资源 “窃取” 并赋值给一个已存在的对象,而非进行耗时的深拷贝赋值,从而优化性能。

代码语言:javascript
复制
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),避免内存泄漏
  • 资源转移:同移动构造函数,接管原对象的资源并重置原对象

3.3:各类默认函数的对比

在 C++ 中,移动赋值运算符 它与 移动构造函数 的区别在于: 移动构造函数用于初始化新对象,而移动赋值运算符用于更新已存在的对象


拷贝函数和移动函数的对比:

函数类型

参数类型

行为

性能

拷贝构造

const MyString&

深拷贝(重新分配资源)

O(n)

移动构造

MyString&&

转移资源(复制指针)

O(1)

拷贝赋值

const MyString&

深拷贝 + 释放旧资源

O(n)

移动赋值

MyString&&

转移资源 + 释放旧资源

O(1)

4. 如何触发移动语义?

移动构造和移动赋值不会自动触发,需满足以下场景:

1. 传递临时对象(右值):

当函数返回一个局部对象时,如果满足一定条件(如没有发生返回值优化 RVO 或命名返回值优化 NRVO)

编译器会调用移动构造函数将局部对象的资源移动到调用者的对象中,而不是进行深拷贝

代码语言:javascript
复制
MyString func() 
 {
 	 return MyString("临时对象"); // 返回临时对象(右值)
}

int main() 
{
  	MyString s1 = func(); // 触发移动构造(而非拷贝)
 }

2. 使用 std::move 转换左值为右值:进行 std::move 操作时,也会触发移动语义。

代码语言:javascript
复制
int main() 
{
    MyString s2("左值对象");
    
    MyString s3 = std::move(s2); // 用 std::move 强制转换左值为右值,触发移动构造
    //注意:s2 被移动后变为“有效但未定义状态”,不应再使用(除非重新赋值)
}

注意

  • std::move 函数的作用是将左值强制转换为右值引用,从而允许对左值进行移动操作
  • 它本身并不移动资源,只是为移动语义的触发创造条件

3. 容器操作:std::vectorstd::list 等标准库容器中插入元素时,会触发移动语义。

代码语言:javascript
复制
#include <vector>
int main() 
{
    std::vector<MyString> vec;
    
    vec.push_back(MyString("hello")); // 临时对象触发移动构造
}

代码案例:拷贝函数和移动函数在实际中的应用

代码语言:javascript
复制
#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;
} 
在这里插入图片描述
在这里插入图片描述

关于移动语义的注意事项:

  1. 被移动对象的状态:移动后原对象(如 s2)必须处于 “可析构且安全” 的状态(通常指针置空、长度为 0),但不应再使用其资源(内容可能已失效)
  2. 编译器优化:若函数返回局部对象,编译器可能触发 RVO(返回值优化),直接在调用方构造对象,跳过移动 / 拷贝(性能更优)
  3. 默认移动函数:若类未手动定义移动构造 / 赋值,编译器会在未定义拷贝函数和析构函数时自动生成默认版本。但建议手动定义以确保资源正确转移

通过实现移动构造函数移动赋值运算符,即可利用移动语义将资源 “转移” 而非 “拷贝” 大幅提升处理大型对象(如:字符串、容器)时的性能,尤其适合函数返回局部对象容器插入元素等场景。

5. 移动语义的价值是什么?

移动语义的价值是什么?

  • 提高容器性能:在 std::vectorstd::liststd::map 等容器中插入元素时,如果元素类型支持移动语义,会大大提高插入操作的效率
    • 例如:向 vector 中插入大量元素时,移动语义可以避免不必要的深拷贝,提升性能
  • 函数返回值优化:对于返回大型对象的函数,移动语义可以在不改变函数返回方式的前提下,减少拷贝开销,提高函数的执行效率
  • 资源管理:在管理动态分配内存、文件句柄、网络连接等资源的类中,移动语义可以更高效地转移资源所有权,避免资源泄露和不必要的资源复制

总的来说:移动语义是 C++11 中一项非常强大的特性,它让 C++ 程序在处理对象拷贝和资源管理时更加高效,是编写高性能 C++ 代码的重要手段之一。

6. 深究“右值引用和移动语义解决传值返回问题”

代码语言:javascript
复制
#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;
}

-------右值对象构造-------

6.1:只有拷贝构造,没有移动构造的场景
在这里插入图片描述
在这里插入图片描述

上图展示了同一段代码在不同的环境下,编译器对拷贝构造的优化:

  • 最左:为未优化场景,函数返回值传递会触发两次拷贝构造
  • 中间:经初级优化后,连续步骤中的多次拷贝被合并,仅需一次拷贝构造
  • 最右:经高级优化后,不需拷贝构造

注意:在 Linux 环境验证时:

  • 可将代码保存为test.cpp
  • 通过 g++ test.cpp -fno-elide-constructors 编译(-fno-elide-constructors 作用是关闭编译器的返回值优化
  • 运行后,输出会呈现类似最左侧的未优化结果(触发两次拷贝构造 )

以下详细拆解 “构造→拷贝构造” 反复出现的完整流程,解释每一行输出的根源:

本质函数参数构造、函数内部对象构造、返回值传递 三个阶段的叠加。


阶段 1:addStrings 的参数构造(num1 和 num2)

调用 addStrings(“11111”, “2222”) 时,需要先构造两个参数 num1 和 num2:

代码语言:javascript
复制
string addStrings(string num1, string num2) { ... }

num1 的普通构造: "11111" 是 C 风格字符串,触发 string(const char* str) 普通构造:

代码语言:javascript
复制
string(char* str) -- 构造  // 输出第1行:构造 num1

num1 的拷贝构造: 由于 addStrings 的参数是按值传递(string num1),需要将实参拷贝到函数栈帧,触发拷贝构造

代码语言:javascript
复制
string(const string& s) -- 拷贝构造  // 输出第2行:num1 拷贝到函数参数

num2 的普通构造: "2222" 同理,触发 string(const char* str) 普通构造:

代码语言:javascript
复制
string(char* str) -- 构造  // 输出第3行:构造 num2

num2 的拷贝构造: 按值传递触发拷贝构造:

代码语言:javascript
复制
string(const string& s) -- 拷贝构造  // 输出第4行:num2 拷贝到函数参数

阶段 2:addStrings 内部构造 str

进入 addStrings 函数后,第一行代码:

代码语言:javascript
复制
string str; 

触发 string普通构造(因默认构造被 string(const char* str = "") 覆盖,等价于构造空字符串):

代码语言:javascript
复制
string(char* str) -- 构造  // 输出第5行:构造 addStrings 内部的 str

阶段 3:addStrings 返回值的传递(关闭优化时)

函数返回时,由于编译参数 "-fno-elide-constructors" 关闭了返回值优化(RVO),编译器会:

代码语言:javascript
复制
return str;

先构造一个临时对象(右值),通过拷贝构造将str的内容复制到临时对象:

代码语言:javascript
复制
string(const string& s) -- 拷贝构造  // 输出第6行:str 拷贝到临时对象

然后销毁 addStrings 内部的 str,析构输出。


阶段 4:main 中 ret 的构造

临时对象返回后,main 中:

代码语言:javascript
复制
mySpace::string ret = ...;

会通过拷贝构造将临时对象的内容复制到 ret

代码语言:javascript
复制
string(const string& s) -- 拷贝构造  // 输出第7行:临时对象拷贝到 ret

阶段 5:析构流程

程序结束时,会按构造逆序析构对象:

  • 析构 ret
  • 析构临时对象
  • 析构 num2 的函数参数拷贝
  • 析构 num2
  • 析构 num1 的函数参数拷贝
  • 析构 num1
  • 析构 addStrings 内部的 str

每次析构都会输出:

代码语言:javascript
复制
~string() -- 析构

输出对应关系总结:

你看到的输出:

代码语言:javascript
复制
//与上述流程的对应关系为:
    
构造       → num1 普通构造 
拷贝构造   → num1 按值传递的拷贝构造
构造  	  → num2 普通构造
拷贝构造	  → num2 按值传递的拷贝构造
-------------
构造  	  → addStrings 内部 str 的普通构造
-------------
拷贝构造	  → str 拷贝到临时对象
拷贝构造	  → 临时对象拷贝到 ret 

关键结论:为什么只有拷贝构造?

在 “没有移动构造” 的场景下,本质是:

  • 若类未显式实现 移动构造函数(string(string&&),编译器不会自动生成移动构造(或生成的默认移动构造被忽略)
  • 所有按值传递返回值传递 的操作,即使对象是右值,也会因缺少移动构造而退化为 拷贝构造(深拷贝资源)

若要触发移动构造,需:

  1. 显式实现 string(string&&) 移动构造函数
  2. 确保返回值是右值(如:临时对象),且编译器未优化掉移动操作

详细拆解三种输出场景的差异根源:

三种输出结果的差异的本质编译器对返回值优化(RVO/NRVO)的应用程度不同 以及是否显式实现移动构造函数

  1. 第一种情况(多次构造 + 多次拷贝): -fno-elide-constructors 完全关闭优化,严格按值传递逻辑触发所有拷贝构造
  2. 第二种情况(三次构造 + 一次拷贝): 部分优化生效,合并了部分拷贝操作,但仍保留一次关键拷贝
  3. 第三种情况(仅三次构造): 优化全开(RVO/NRVO),直接在目标对象内存位置构造返回值,跳过所有拷贝

逐场景详细解析:

场景 2:三次构造 + 一次拷贝

代码语言:javascript
复制
string(char* str) -- 构造  
string(char* str) -- 构造  
string(char* str) -- 构造  
string(const string& s) -- 拷贝构造  

流程解析

num1num2 的构造

代码语言:javascript
复制
mySpace::string ret = mySpace::addStrings("11111", "2222");
  • "11111" → 构造 num1
  • "2222" → 构造 num2

addStrings 内部 str 的构造

代码语言:javascript
复制
string str;  // 触发普通构造

返回值优化(RVO)生效

  • 编译器发现 str 是函数返回值,直接在 ret 的内存位置构造 str(跳过临时对象)

场景 3:仅三次构造

代码语言:javascript
复制
string(char* str) -- 构造  
string(char* str) -- 构造  
string(char* str) -- 构造  

流程解析

num1num2 的构造

代码语言:javascript
复制
mySpace::string ret = mySpace::addStrings("11111", "2222");
  • "11111" → 构造 num1
  • "2222" → 构造 num2

addStrings 内部 str 的构造

代码语言:javascript
复制
string str;  // 触发普通构造

无拷贝构造

  • 因 RVO 优化,str 的构造直接复用 ret 的内存,无需任何拷贝
6.2:既有拷贝构造,也有移动构造的场景
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

-------右值对象赋值-------

6.3:只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

上图展示了同一段代码在不同的环境下,编译器对拷贝构造的优化:

  • 最左:Linux 下 g++ test.cpp -fno-elide-constructors 关闭构造优化的环境
  • 中间:VS2019 Debug 模式下
  • 最右:VS2019 Release 模式、VS2022 的 Debug/Release 模式下

步骤 1:ret 的默认构造

代码语言:javascript
复制
mySpace::string ret;

触发 string默认构造(因 string(const char* str = "") 覆盖默认构造,等价于构造空字符串):

代码语言:javascript
复制
string(char* str) -- 构造  // 输出第1行:构造 ret(空字符串)

步骤 2:addStrings 的参数构造(num1 和 num2)

调用 addStrings(“1111”, “2222”) 时,需构造参数 num1 和 num2:

代码语言:javascript
复制
string addStrings(string num1, string num2) { ... }

num1 的构造"1111" 是 C 风格字符串,触发 string(const char* str) 普通构造:

代码语言:javascript
复制
string(char* str) -- 构造  // 输出第2行:构造 num1

num1 的拷贝构造: 因参数是按值传递string num1),需将实参拷贝到函数栈帧,触发拷贝构造

代码语言:javascript
复制
string(const string& s) -- 拷贝构造  // 输出第3行:num1 拷贝到函数参数

num2 的构造"2222" 同理,触发 string(const char* str) 普通构造:

代码语言:javascript
复制
string(char* str) -- 构造  // 输出第4行:构造 num2

num2 的拷贝构造: 按值传递触发拷贝构造:

代码语言:javascript
复制
string(const string& s) -- 拷贝构造  // 输出第5行:num2 拷贝到函数参数

步骤 3:addStrings 内部构造 str

进入 addStrings 函数后,第一行代码:

代码语言:javascript
复制
string str; 

触发 string普通构造(空字符串):

代码语言:javascript
复制
string(char* str) -- 构造  // 输出第6行:构造 addStrings 内部的 str

步骤 4:addStrings 返回值的拷贝构造(关闭优化时)

函数返回时:

代码语言:javascript
复制
return str;

因为未显式实现移动构造,且关闭优化,所以触发拷贝构造生成临时对象:

代码语言:javascript
复制
string(const string& s) -- 拷贝构造  // 输出第7行:str 拷贝到临时对象

步骤 5:ret 的拷贝赋值

临时对象返回后,执行:

代码语言:javascript
复制
ret = 临时对象;

触发拷贝赋值运算符(因无移动赋值):

代码语言:javascript
复制
string& operator=(const string& s) -- 拷贝赋值  // 输出第8行:临时对象拷贝赋值给 ret

步骤 6:析构流程:

程序结束时,按构造逆序析构对象:

  • 析构 ret
  • 析构临时对象
  • 析构 num2 的函数参数拷贝
  • 析构 num2
  • 析构 num1 的函数参数拷贝
  • 析构 num1
  • 析构 addStrings 内部的 str

每次析构输出:

代码语言:javascript
复制
~string() -- 析构

三种输出场景的差异解析 : 场景 1:完整拷贝流程(多次构造 + 拷贝)

  • 完全未实现移动语义
  • 且关闭优化(-fno-elide-constructors),严格触发所有拷贝

场景 2:部分优化(减少拷贝次数)

  • 编译器开启部分优化(如:RVO)
  • 合并了参数传递或返回值的拷贝步骤,但因无移动语义,仍触发拷贝赋值

场景 3:优化全开(仅必要构造)

  • 编译器通过 RVO/NRVO 直接在 ret 内存位置构造返回值
  • 跳过所有拷贝/赋值(需显式实现移动语义或编译器强制优化)

移动语义未触发的原因? 在 “只有拷贝 构造/赋值,没有 移动 构造/赋值” 的场景中,本质是类未显式实现移动构造和移动赋值函数,导致:

  • 所有右值对象(如:临时对象)的传递,退化为拷贝 构造/赋值(深拷贝资源)
  • 即使对象是右值(如:addStrings 的返回值),因无移动语义支持,编译器只能调用拷贝语义

在 “只有拷贝 构造/赋值” 的场景中:

  • 类未实现移动语义
    • 若未显式编写 string(string&&)string& operator=(string&&),编译器无法触发移动 构造/赋值
  • 右值退化为左值传递
    • 即使返回临时对象(右值),因无移动语义支持,编译器只能调用拷贝 构造/赋值(深拷贝资源)
  • 编译器优化的影响
    • 关闭优化会强制暴露所有拷贝步骤
    • 开启优化可能合并部分拷贝,但本质仍依赖移动语义实现高效传递
6.4:既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7. 什么是引用折叠?

引用折叠:是 C++11 引入的一个重要特性,它和模板、右值引用等概念紧密相关,在一些复杂的类型推导场景中发挥着关键作用。


在 C++ 中:

  • 直接定义 “引用的引用” ,比如:int& && r = i;i 是一个int类型变量)是不被允许的,会直接导致编译报错
  • 但在模板实例化使用typedef进行类型操作时,可能会出现多个引用叠加的情况,这时候就需要引用折叠规则来确定最终的引用类型

简单来说引用折叠就是在特定情况下,将多个连续的引用类型 “折叠” 成一个引用类型


当通过 模板/typedef 构造出 “引用的引用” 时,C++11 用引用折叠规则统一处理:

  • T& &T& (左值引用 —> 折叠为 —> 左值引用)
  • T& &&T& (左值引用和右值引用 —> 折叠为 —> 左值引用)
  • T&& &T& (右值引用和左值引用 —> 折叠为 —> 左值引用)
  • T&& &&T&& (右值引用 —> 折叠为 —> 右值引用)

引用折叠规则总结:

  • 右值引用的右值引用折叠成右值引用
  • 其他组合均折叠成左值引用

模板/typedef 场景的演示与理解:

代码语言:javascript
复制
template <typename T>
void func(T&& x) 
{ 
    /* ... */
}

这里的 T&& x 看起来是 “右值引用参数”,但结合引用折叠规则后,它会根据传入实参的类型 “自适应”:

  • 传入左值时:T 会被推导为 左值引用类型(如:int&),结合引用折叠后,形参会变成 左值引用int&
  • 传入右值时:T 会被推导为 非引用类型(如:int),结合引用折叠后,形参会变成 右值引用int&&

这种能同时适配左值、右值的模板参数,也被称为万能引用(或 “转发引用”)


代码案例:引用折叠实战

代码语言:javascript
复制
//由于引用折叠规则,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;
}
在这里插入图片描述
在这里插入图片描述

代码案例:万能引用实战 + 完美转发基础

代码语言:javascript
复制
#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;
}
在这里插入图片描述
在这里插入图片描述

------------完美转发------------

1. 什么是完美转发?

完美转发:是 C++11 及以后引入的一项重要特性,它允许函数模板将其参数原封不动地转发给另一个函数,保持参数的 原始值类别(左值或右值)和 常量性

  • 主要借助 引用折叠std::forward 实现,用于在函数模板中精准保留参数的 “左值/右值属性”,并将其原封不动地转发给下一层函数

2. 为什么需要完美转发?

右值引用的 “左值属性” 问题:

  • 结合变量表达式的规则(所有变量表达式都是左值
  • 即使右值被右值引用(T&& )绑定,右值引用变量本身仍属于左值(可被取地址、赋值等 )

这会导致一个问题:

  • Function 函数内部,若把 t 传递给下一层函数 Funt 会被当作左值处理,只能匹配 Fun左值引用版本(如:Fun(int&)
  • 但我们希望保留 t 原始的 “左值/右值属性”(比如:让右值继续匹配 Fun 的右值引用版本 ),这时就需要 完美转发 来解决

示例问题(无完美转发时):

代码语言:javascript
复制
#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;
}
在这里插入图片描述
在这里插入图片描述

3. 怎么实现完美转发?

完美转发通过 std::forward 函数模板实现,核心依赖 引用折叠模板参数推导


std::forward 的简化原理:

std::forward 利用引用折叠模板参数推导,“还原” 参数原始的左值/右值属性

代码语言:javascript
复制
// std::forward函数模板的关键逻辑可简化为:


template <typename T>
T&& forward(typename remove_reference<T>::type& arg) 
{
 return static_cast<T&&>(arg);  // 强制类型转换,触发引用折叠
}

完美转发的流程

Function(10) 为例,完整流程:

  • 模板参数推导: 传入右值 10FunctionT 推导为 int(非引用类型 ),形参 tint&&(右值引用 )
  • 调用 std::forward: 在 Function 中调用 Fun(std::forward<T>(t))Tintstd::forward<int>(t) 会:
    • 通过 static_cast<int&&>(t)t 强转为 int&&(右值引用 )
    • 触发 Fun(int&&) 重载,保留右值属性
代码语言:javascript
复制
#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&&)
在这里插入图片描述
在这里插入图片描述
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-12-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言:
  • ------------移动语义------------
    • 1. 左值引用的主要使用场景有哪些?
    • 2. 为何右值引用无法解决 “返回局部对象” 的问题?
    • 3. 移动语义究竟是如何攻克 “返回局部对象” 难题的?
      • 3.1:移动构造函数
      • 3.2:移动赋值运算符
      • 3.3:各类默认函数的对比
    • 4. 如何触发移动语义?
    • 5. 移动语义的价值是什么?
    • 6. 深究“右值引用和移动语义解决传值返回问题”
    • -------右值对象构造-------
      • 6.1:只有拷贝构造,没有移动构造的场景
      • 6.2:既有拷贝构造,也有移动构造的场景
    • -------右值对象赋值-------
      • 6.3:只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
      • 6.4:既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
    • 7. 什么是引用折叠?
  • ------------完美转发------------
    • 1. 什么是完美转发?
    • 2. 为什么需要完美转发?
    • 3. 怎么实现完美转发?
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档