【C++ 基础入门】C++11 新特性(中):右值引用、移动语义与完美转发

👋 欢迎阅读

🔥愿旖旎 · 个人主页

📘学习专栏: 《算法专栏》《LangChain学习》《贪心算法》

🌄 钱塘江上潮信来,今日方知我是我

✨当前学习内容: 《C++学习》

📑 目录


一、前置讲解

在进入正文前,先花 10 秒了解本篇会反复用到的核心概念,带着印象去读正文,学习效率更高。

🧩 前置知识一:左值引用的短板

做参数、做返回值都能省拷贝 ,但当返回的是函数内局部对象 时,只能传值返回,于是又产生拷贝。

🚚 前置知识二:移动构造 / 移动赋值

把源对象(右值)的资源直接窃取过来 ,不做深拷贝,代价远小于拷贝。

🔁 前置知识三:std::move

它不搬任何东西 ,只是把左值强制转换成右值引用,从而让移动构造能接上。

📮 前置知识四:万能引用 T&&

模板里的 T&& 不是右值引用 ,左右值都能收------但具名变量本身是左值。

➡️ 前置知识五:std::forward

完美转发 :把实参的原生类型和值类别原样传下去,左值还是左值、右值还是右值。


二、右值引用的使用场景和意义

前面我们看到左值引用既可以引用左值、又可以引用右值 ,那为什么 C++11 还要提出右值引用?是不是画蛇添足?------先看左值引用的短板,再看右值引用如何补齐。

2.1 左值引用的使用场景

左值引用做参数和做返回值都可以提高效率。

cpp 复制代码
void func1(bit::string s)          // 传值:拷贝一份,代价大
{}
​
void func2(const bit::string& s)   // 传引用:不拷贝
{}
​
int main()
{
    bit::string s1("hello world");
​
    // func1 和 func2 的调用对比,可以看出左值引用做参数能减少拷贝
    func1(s1);
    func2(s1);
}

2.2 左值引用的短板

但是当函数返回的对象是一个局部变量 时,它出了函数作用域就不存在了 ,就不能使用左值引用返回,只能传值返回。

例如 bit::string to_string(int value) 函数,这里只能使用传值返回 ,而传值返回会导致至少 1 次拷贝构造(旧一点的编译器可能是两次拷贝构造)。

在函数中传值返回是返回一个临时对象 ,如果该临时对象符合将亡值的概念 ,那么就可以采用移动构造 而不采取拷贝构造(前提是有移动构造)。

右值引用和移动语义解决的正是上述问题:

在 bit::string 中增加移动构造 ,移动构造的本质是将参数右值的资源窃取过来、占为己有 ,那么就不用深拷贝 了------所以它叫做移动构造 ,就是窃取别人的资源来构造自己。

不仅仅有移动构造,还有移动赋值:

在 bit::string 类中增加移动赋值 函数,再去调用 bit::to_string(1234)------不过这次是将 bit::to_string(1234) 返回的右值对象赋值给 ret1 对象 ,这时调用的是移动赋值。

cpp 复制代码
// 普通赋值:左值引用,需要生成对象
string operator=(const string& s)
{
    if (this != &s)          // 自赋值检测
    {
        string tmp(s);       // 先拷贝一份
        swap(tmp);           // 交换后 *this 拿到新内容
    }
    return *this;            // 按值返回:返回 *this 的一份副本
}

// 移动赋值:右值引用,直接窃取资源
string operator=(string&& s)
{
    if (this != &s)          
    {
        swap(s);             // 窃取 s 的资源
    }
    return *this;            // 按值返回
}

运行结果(关键输出):

复制代码
string& operator=(string&& s) -- 移动语义
对比项 拷贝构造 / 拷贝赋值 移动构造 / 移动赋值
参数 const T&(常量左值引用) T&&(右值引用)
做的事 重新分配资源 + 深拷贝内容 窃取源对象资源(换指针)
源对象 保持不变 被置空/失效(资源已交出去)

因此右值引用解决的场景:

场景 说明
需要深拷贝、且只能用传值返回/值传递的类 即不能用左值引用的情况,用移动构造/移动赋值省掉深拷贝
独占资源的类 该类对象不能被拷贝、只能被转移,也只能靠右值引用

2.3 右值引用引用左值:std::move

按照语法,右值引用只能引用右值 ------但右值引用一定不能引用左值吗?有些场景下,可能真的需要用右值去引用左值以实现移动语义 。当需要用右值引用引用一个左值时,可以通过 move 函数将左值转化为右值。

C++11 中,std::move() 函数位于 <utility> 头文件中 。这个函数名字具有迷惑性 :它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。

move 的理解:

move 不会改变左值对象本身,而是返回一个"把该左值转换成右值引用"的结果 (本质是 static_cast<T&&>)。

验证 move 到底做了什么:

cpp 复制代码
int main()
{
    int a = 10;
​
    // std::move 只是把 a 转成右值引用,a 本身没有任何变化
    int&& r = std::move(a);
​
    cout << a << endl;    // 10:a 的值没变
    cout << r << endl;    // 10:r 绑定的就是 a
}

运行结果:

复制代码
10
10

三、完美转发

3.1 模板中的 && 是万能引用

cpp 复制代码
void Fun(int &x)        { cout << "左值引用" << endl; }
void Fun(const int &x)  { cout << "const 左值引用" << endl; }
void Fun(int &&x)       { cout << "右值引用" << endl; }
void Fun(const int &&x) { cout << "const 右值引用" << endl; }
​
// 模板中的 && 不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
template<typename T>
void PerfectForward(T&& t)
{
    Fun(t);     // 这里直接把 t 传下去
}
​
int main()
{
    PerfectForward(10);            // 右值
​
    int a;
    PerfectForward(a);             // 左值
    PerfectForward(std::move(a));  // 右值
​
    const int b = 8;
    PerfectForward(b);             // const 左值
    PerfectForward(std::move(b));  // const 右值
}

运行结果:

复制代码
左值引用
左值引用
左值引用
const 左值引用
const 左值引用

💡 为什么传进去的明明是右值,最后全走了左值版本 ?因为 t 是一个有名字的变量 ------具名变量本身就是左值 。T&& 只是让形参能接收 左右值,一旦进了函数体,t 就退化成左值,Fun(t) 自然只能匹配左值引用的重载。

3.2 std::forward 完美转发

std::forward 在传参的过程中保留对象原生类型属性:

cpp 复制代码
template<typename T>
void PerfectForward(T&& t)
{
    Fun(std::forward<T>(t));   // 保持 t 的原生类型属性往下传
}

int main()
{
    PerfectForward(10);            // 右值

    int a;
    PerfectForward(a);             // 左值
    PerfectForward(std::move(a));  // 右值

    const int b = 8;
    PerfectForward(b);             // const 左值
    PerfectForward(std::move(b));  // const 右值
}

运行结果:

复制代码
右值引用
左值引用
右值引用
const 左值引用
const 右值引用
调用 传进来是什么 不转发(Fun(t)) 转发(Fun(std::forward<T>(t)))
PerfectForward(10) 右值 左值引用 右值引用
PerfectForward(a) 左值 左值引用 左值引用
PerfectForward(std::move(a)) 右值 左值引用 右值引用
PerfectForward(b) const 左值 const 左值引用 const 左值引用
PerfectForward(std::move(b)) const 右值 const 左值引用 const 右值引用

💡 每一次向下传递,都要重新做一次完美转发 ------因为每经过一层函数,具名的形参又会退化成左值。也就是说:转发不是一次性的属性,而是每传递一层就要再 forward 一次。

完美转发总结:函数模板 + 转发引用(T&&)+ std::forward。

它的作用是:在泛型编程中,无损传递实参的类型和值类别。


四、类的新功能

4.1 两个新增的默认成员函数

C++11 新增了两个默认成员函数:移动构造函数 和移动赋值运算符重载。

针对移动构造函数和移动赋值运算符重载,有一些需要注意的点:

如果你没有声明析构函数、拷贝构造、赋值重载、移动构造、移动赋值 ,那么编译器会自动生成一个默认移动构造。

默认生成的移动构造函数:

成员类型 处理方式
内置类型 直接复制值(源对象不变)
自定义类型成员 看它是否实现了移动构造/赋值 :实现了就调用移动 ,没实现就调用拷贝
cpp 复制代码
class Member        // 自定义类型成员:自带移动构造
{
public:
    Member() { cout << "Member()" << endl; }
    Member(const Member&) { cout << "Member(const Member&) -- 拷贝" << endl; }
    Member(Member&&)      { cout << "Member(Member&&) -- 移动" << endl; }
};

class Test          // 不声明任何函数:编译器自动生成【默认移动构造】
{
public:
    int GetA() const { return _a; }

private:
    int _a = 10;    // 内置类型成员
    Member _m;      // 自定义类型成员
};

int main()
{
    Test t1;
    cout << "--- Test t2(std::move(t1)); ---" << endl;
    Test t2(std::move(t1));   // 调用编译器生成的默认移动构造

    cout << "t1._a = " << t1.GetA() << endl;   // 内置类型只复制,源对象不变
}

运行结果:

复制代码
Member()
--- Test t2(std::move(t1)); ---
Member(Member&&) -- 移动
t1._a = 10

4.2 类成员变量初始化

C++11 允许在类定义时给成员变量初始缺省值 ,默认生成的构造函数会使用这些缺省值初始化。

4.3 强制生成默认函数的关键字 default

C++11 可以让你更好地控制要使用的默认函数。假设你要使用某个默认函数,但因为一些原因这个函数没有默认生成 ------比如:我们提供了拷贝构造,就不会生成默认移动构造 了。那么可以用 default 关键字显式指定默认移动构造生成:

cpp 复制代码
class Person
{
public:
    Person(const char* name = "", int age = 0)
        : _name(name), _age(age)
    {}

    Person(const Person& p)      // 提供了拷贝构造
        : _name(p._name), _age(p._age)
    {}

    Person(Person&& p) = default;   // 显式要回默认移动构造

private:
    string _name;
    int _age;
};

五、复盘(附答案)

💡 思考题

  1. 左值引用的短板 :为什么"做参数"和"做返回值"都能省拷贝,但遇到局部对象返回就省不了了?传值返回至少几次拷贝构造?

  2. 移动构造 :它的本质是什么?相比拷贝构造省在哪?移动之后源对象还能用吗?

  3. std::move :它到底做了什么?int&& r = std::move(a); 之后 a 的值变了吗?

  4. 万能引用 :模板里的 T&& 和普通的右值引用有什么区别?为什么 PerfectForward 里直接 Fun(t) 全走左值版本?

  5. std::forward:它的作用是什么?为什么说"每向下传递一层就要转发一次"?

📝 答案

  1. 做参数时形参绑的是外部对象的引用 ,不产生拷贝;做返回值时,如果返回的是函数内局部对象 ,函数一结束它就销毁了,引用会悬空 ,所以只能传值返回 。传值返回会产生临时对象 ,至少 1 次拷贝构造(旧编译器可能两次)。

  2. 本质是把参数右值的资源窃取过来、占为己有 ------直接接手对方的指针/内存,而不做深拷贝 。相比拷贝构造省掉了分配空间 + 复制全部内容 的开销。移动之后源对象被掏空 、资源已交出去,不应再使用它的值。

  3. std::move 不搬移任何东西 ,唯一功能是把左值强制转换成右值引用 (本质是 static_cast<T&&>)。int&& r = std::move(a); 之后 a 的值没变 ,r 只是绑到了 a 上;真正"搬资源"的是后续的移动构造/移动赋值。

  4. 普通的 T&& 只能绑右值;模板里的 T&& 是万能引用(转发引用) ,左右值都能收 。PerfectForward 里直接 Fun(t) 全走左值版本,是因为 t 是有名字的变量、具名变量本身是左值 ------T&& 只负责"能接收",进了函数体 t 就退化成左值了。

  5. std::forward<T>(t) 在传递时保留 t 的原生类型和值类别 :左值还是左值、右值还是右值。说"每传递一层都要转发一次",是因为每一层函数的具名形参都会退化成左值 ,想继续保留右值属性就必须在每层 再 forward 一次。


结语

如果本文对你有帮助,欢迎:

👍 点赞 | ⭐ 收藏 | 👤 关注作者 | 💬 留言交流你的疑问或补充

你的每一次互动都是我继续更新的动力,我们下一篇见!🚀

相关推荐
ShineWinsu31 分钟前
对于Qt:QTabWidget常见属性的解析
开发语言·数据库·c++·qt·浏览器·qtabwidget·标签页
普通人61 小时前
WebGL与WebGPU入门指南
开发语言·webgl·1024程序员节
꧁细听勿语情꧂1 小时前
直接选择排序,堆排序,冒泡排序,递归快速排序、hoare版本、挖坑法、lomuto前后指针,非递归快速排序
c语言·开发语言·数据结构·算法
SomeOtherTime1 小时前
Rust集合(AI回答)
开发语言·后端·rust
曦樂~2 小时前
【Web Development】03JavaScript 基础和事件
开发语言·前端·javascript
郝学胜-神的一滴2 小时前
LangChain Skill 实现(渐进披露 On-Demand Skill,LangChain 官方 Deep Agent Skill 模式)
开发语言·python·langchain·numpy
俊俊几年的2 小时前
动态规划(Dynamic Programming)详解:从入门到 LeetCode Hot 100
linux·c++·算法·leetcode·嵌入式·动态规划·c
程序员Query2 小时前
day01-认识Go语言 + golang环境的安装
开发语言·后端·golang
星恒随风11 小时前
MySQL 从零入门(五):MySQL 内置函数详解——日期、字符串、数学函数与实战案例
linux·数据库·笔记·sql·mysql