C++深入右值引用之移动语义与完美转发(最新推荐)

 更新时间:2026年05月12日 09:27:26   作者:无限进步_  
本文给大家介绍了C++深入右值引用之移动语义与完美转发,文章讨论了移动构造与移动赋值的概念,以及它们在容器中的应用,感兴趣的朋友一起看看吧

1. 左值与右值

左值和右值不是新概念,C++98就有,但C++11赋予了它们更重要的地位。

  • 左值:可以取地址的表达式,有持久状态。变量、解引用的指针、数组元素都是左值。
  • 右值:不能取地址。字面量、临时对象、表达式求值的中间结果。
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;            // *p 是左值
string s("hello");
s[0] = 'x';         // s[0] 是左值
10;                 // 右值
x + y;              // 右值
string("hello");    // 右值
// cout << &10;     // 错误:右值不能取地址

C++11进一步细化了:右值分为纯右值(字面量、临时对象等)和将亡值move返回的右值引用)。泛左值包含左值和将亡值。这些概念不必死记,关键是记住核心区别:能否取地址

2. 左值引用与右值引用

左值引用(T&)给左值取别名,右值引用(T&&)给右值取别名。

int& r1 = b;          // 左值引用绑定左值
int&& rr1 = 10;       // 右值引用绑定右值
double&& rr2 = x + y;
string&& rr3 = string("hello");

几个规则:

  • 左值引用不能直接绑定右值,但const左值引用可以
  • 右值引用不能直接绑定左值,但可以通过std::move转换。
const int& cr = 10;           // OK
int&& rr4 = move(b);          // OK,move后的b仍可使用但资源已被取走

move()本质上是一个强制类型转换:static_cast<remove_reference_t<T>&&>(arg)。它本身不移动任何东西,只是给了你一个右值引用,真正的动作由移动构造函数或移动赋值完成。

有一个容易困惑的点:右值引用变量本身的表达式属性是左值。因为它有名字、可以取地址。

int&& rr = 10;
cout << &rr << endl;   // 可以取地址,rr是左值
int& lr = rr;          // OK,rr作为左值可以绑定左值引用
// int&& rr2 = rr;     // 错误,rr是左值,不能直接绑定右值引用
int&& rr2 = move(rr);  // OK

这个设计看似别扭,但恰恰是移动语义能正常工作的基础——后面会看到。

3. 移动构造与移动赋值

左值引用已经在函数传参和返回值中减少了大部分拷贝,但有一种场景它搞不定:返回局部对象。

string addStrings(string num1, string num2) {
    string str;
    // ... 构建str ...
    return str;  // str是局部对象,函数结束就销毁,不能传引用返回
}

C++98被迫拷贝,C++11通过移动语义解决了这个问题:既然str马上就要销毁,把它的资源“偷”过来就行,没必要深拷贝。

移动构造和移动赋值接受一个右值引用参数,核心操作是交换资源,而不是复制。

namespace demo {
class string {
public:
    string(const char* str = "")
        : _size(strlen(str)), _capacity(_size)
    {
        _str = new char[_capacity + 1];
        strcpy(_str, str);
    }
    // 拷贝构造
    string(const string& s) {
        // 深拷贝...
    }
    // 移动构造
    string(string&& s) {
        swap(s);   // 直接把s的资源换到自己身上
    }
    // 拷贝赋值
    string& operator=(const string& s) {
        // 深拷贝...
    }
    // 移动赋值
    string& operator=(string&& s) {
        swap(s);
        return *this;
    }
    void swap(string& s) {
        std::swap(_str, s._str);
        std::swap(_size, s._size);
        std::swap(_capacity, s._capacity);
    }
    ~string() { delete[] _str; }
private:
    char* _str = nullptr;
    size_t _size = 0;
    size_t _capacity = 0;
};
}

使用效果:

demo::string s1("hello");
demo::string s2 = s1;              // 拷贝构造
demo::string s3 = demo::string("world");  // 移动构造(临时对象)
demo::string s4 = move(s1);        // 移动构造(显式move)

编译器还会进一步优化。在VS2022的release模式下,demo::string s3 = demo::string("world");可能直接被优化成一次原地构造,移动构造都不会调用——这就是返回值优化(RVO)。

返回值场景中,如果提供了移动构造,没有优化时编译器会优先选择移动而不是拷贝,显著提高效率。

4. 移动语义在容器中的应用

C++11之后,STL容器的push_backinsert都增加了右值引用版本:

void push_back(const T& x);   // 左值版本,内部拷贝
void push_back(T&& x);        // 右值版本,内部移动

当传入左值,走拷贝;传入右值,走移动。

list<demo::string> lt;
demo::string s1("hello");
lt.push_back(s1);                          // 拷贝
lt.push_back(move(s1));                    // 移动
lt.push_back("world");                     // 移动(临时string)
lt.push_back(demo::string("world"));       // 移动

自定义容器也可以实现相应的重载,内部在节点构造时用move把参数转成右值。

5. 完美转发与引用折叠

考虑一个模板函数,想要把参数原样转发给另一个函数:

template<class T>
void relay(T&& t) {
    func(t);  // t是左值,永远调用func的左值版本,即使传进来的是右值
}

问题在于:t作为右值引用变量,表达式属性是左值,所以func(t)调不到右值重载版本。要保留参数的原始值类别,需要完美转发

template<class T>
void relay(T&& t) {
    func(forward<T>(t));
}

forward的实现依赖于引用折叠规则。C++不允许直接定义引用的引用(int& &),但在模板和类型推导中可以出现。规则只有一条:右值引用的右值引用折叠成右值引用,其余全部折叠成左值引用

typedef int&  lref;
typedef int&& rref;
lref&  r1 = n;  // int&  &  → int&
lref&& r2 = n;  // int&  && → int&
rref&  r3 = n;  // int&& &  → int&
rref&& r4 = 1;  // int&& && → int&&

relay中,T的推导结果配合引用折叠,实现了“左值传左值,右值传右值”:

  • 实参是左值 int a → T推导为int& → T&&折叠为int&
  • 实参是右值 10 → T推导为int → T&&int&&

forward内部也是通过static_cast<T&&>实现的,配合引用折叠,左值情况返回左值引用,右值情况返回右值引用。这样就把参数的原始属性一路传下去了。

6. 默认移动构造与移动赋值

编译器在特定条件下会自动生成移动构造和移动赋值:

  • 如果用户没有定义拷贝构造、拷贝赋值、析构函数中的任何一个,编译器会尝试自动生成移动构造和移动赋值。
  • 对于内置类型成员,逐字节拷贝;对于自定义类型成员,如果能移动就移动,否则拷贝。

一旦你提供了移动构造或移动赋值,编译器就不会自动生成拷贝构造和拷贝赋值。所以如果你想两者都有,需要自己显式声明,或者用= default

class Person {
public:
    Person(const Person&) = default;   // 显式要拷贝
    Person(Person&&) = default;        // 显式要移动
};

也可以用= delete禁止某个函数。

移动语义是C++11最核心的特性之一。它让“快”从“避免拷贝”的编码技巧,变成了语言级别的优化路径。

到此这篇关于C++深入右值引用之移动语义与完美转发(最新推荐)的文章就介绍到这了,更多相关C++右值引用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C++实现LeetCode(648.替换单词)

    C++实现LeetCode(648.替换单词)

    这篇文章主要介绍了C++实现LeetCode(648.替换单词),本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-08-08
  • C语言实现扫雷小游戏的示例代码

    C语言实现扫雷小游戏的示例代码

    这篇文中主要为大家详细介绍了如何利用C语言实现经典的扫雷小游戏。扫雷小游戏主要是利用字符数组、循环语句和函数实现,感兴趣的小伙伴可以了解一下
    2022-10-10
  • 基于C语言实现猜数字游戏

    基于C语言实现猜数字游戏

    这篇文章主要为大家详细介绍了基于C语言实现猜数字游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-05-05
  • C++中成员函数和友元函数的使用及区别详解

    C++中成员函数和友元函数的使用及区别详解

    大家好,本篇文章主要讲的是C++中成员函数和友元函数的使用及区别详解,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下
    2022-01-01
  • C++的友元和内部类你了解吗

    C++的友元和内部类你了解吗

    这篇文章主要为大家介绍了C++的友元和内部类,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-01-01
  • c++使用单例模式实现命名空间函数案例详解

    c++使用单例模式实现命名空间函数案例详解

    这篇文章主要介绍了c++使用单例模式实现命名空间函数,本案例实现一个test命名空间,此命名空间内有两个函数,分别为getName()和getNameSpace(),本文结合实例代码给大家讲解的非常详细,需要的朋友可以参考下
    2023-04-04
  • C++实现商品管理程序

    C++实现商品管理程序

    这篇文章主要为大家详细介绍了C++实现商品管理程序,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-12-12
  • C++ explicit关键字的使用详解

    C++ explicit关键字的使用详解

    这篇文章主要介绍了C++ explicit关键字的使用详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-09-09
  • 示例详解C++语言中的命名空间 (namespace)

    示例详解C++语言中的命名空间 (namespace)

    C++名字空间是一种描述逻辑分组的机制,也就是说,如果有一些声明按照某种准则在逻辑上属于同一个模块,就可以将它们放在同一个名字空间,以表明这个事实,这篇文章主要给大家介绍了关于C++语言中命名空间 (namespace)的相关资料,需要的朋友可以参考下
    2021-08-08
  • 简单了解C++常见编程问题解决方案

    简单了解C++常见编程问题解决方案

    这篇文章主要介绍了C++常见编程问题解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-07-07

最新评论