【C++篇】C++11新特性详解(二):右值引用与移动语义

文章目录

C++11新特性详解(二):右值引用与移动语义

💬 欢迎讨论:右值引用和移动语义是C++11中最具革命性的特性,它从根本上改变了C++处理对象复制和资源管理的方式。如果你在学习过程中有任何疑问,欢迎在评论区留言交流!

👍 点赞、收藏与分享:这篇文章会比较深入,建议收藏后慢慢消化。如果觉得有帮助,请分享给更多的朋友!

🚀 系列导航:本文是C++11新特性系列的第二篇,重点讲解右值引用、移动语义、完美转发等核心概念。


一、左值与右值:概念的重新审视

1.1 传统的理解

在学习右值引用之前,我们必须先深入理解什么是左值,什么是右值。

传统定义

传统上,人们认为:

  • 左值(lvalue):可以出现在赋值运算符左边的表达式
  • 右值(rvalue):只能出现在赋值运算符右边的表达式
cpp 复制代码
int x = 10;  // x是左值,10是右值
x = 20;      // OK,左值可以被赋值
// 10 = x;   // 错误!右值不能被赋值

但这种理解是不完整的,因为:

cpp 复制代码
const int y = 10;
// y = 20;  // 错误!y不能被赋值,但y是左值

显然,仅凭"能否被赋值"来区分左值和右值是不够的。

1.2 现代C++的定义

在现代C++中,左值和右值的定义更加本质:

左值(Locator Value)

  • 拥有持久性状态,存储在内存中
  • 可以取地址
  • 明确的存储位置
  • 在多条语句间保持存在
cpp 复制代码
int a = 10;
int* p = &a;      // OK,左值可以取地址

int* ptr = new int(5);
*ptr = 20;        // OK,解引用指针得到左值

string s = "hello";
s[0] = 'H';       // OK,下标运算符返回左值

右值(Read Value)

  • 临时性质,不存储在持久的内存位置
  • 要么是字面值常量,要么是表达式求值过程中创建的临时对象
  • 通常在当前语句结束后就销毁
cpp 复制代码
10;                    // 字面值常量,右值
x + y;                 // 算术表达式结果是临时对象,右值
func();                // 函数返回的临时对象,右值
string("hello");       // 匿名临时对象,右值

// 以下都会编译错误,因为不能对右值取地址
// &10;
// &(x + y);
// &func();
// &string("hello");

核心区别

左值和右值的核心区别就是:能否取地址

通常左值可取地址,右值不可取地址,但这不是严格标准。 例如位域(bit-field)是左值但不能取地址;std::move(x)

是将亡值(xvalue)但可以取地址。 更本质的区别是表达式是否具有对象身份(identity)。

1.3 代码验证

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main()
{
    // ========== 左值示例 ==========
    int* p = new int(0);
    int b = 1;
    const int c = 2;
    
    string s("hello");
    
    // 这些都是左值,可以取地址
    cout << &b << endl;
    cout << &c << endl;
    cout << &(*p) << endl;
    cout << (void*)&s[0] << endl;
    
    // ========== 右值示例 ==========
    double x = 1.1, y = 2.2;
    
    // 这些都是右值,不能取地址
    10;                      // 字面值
    x + y;                   // 表达式结果
    fmin(x, y);              // 函数返回值
    string("world");         // 临时对象
    
    // 以下代码都会编译错误
    // cout << &10 << endl;
    // cout << &(x + y) << endl;
    // cout << &fmin(x, y) << endl;
    // cout << &string("world") << endl;
    
    return 0;
}

二、引用的扩展:左值引用与右值引用

2.1 C++98的引用回顾

在C++98中,引用就是给对象起别名:

cpp 复制代码
int x = 10;
int& r = x;  // r是x的别名

r = 20;      // 修改r就是修改x
cout << x;   // 输出20

C++11之后,为了区分,我们把C++98中的引用称为左值引用

2.2 左值引用的特性

基本规则

cpp 复制代码
int a = 10;
int& r1 = a;        // OK,左值引用绑定左值

// int& r2 = 10;    // 错误!左值引用不能直接绑定右值

const左值引用的特权

const左值引用是一个特例,它可以绑定右值:

cpp 复制代码
const int& r1 = 10;              // OK
const int& r2 = x + y;           // OK
const string& r3 = string("hi"); // OK

为什么const左值引用可以绑定右值?

这是C++为了支持函数传参的便利性而设计的特性。编译器会创建一个临时对象来存储右值,然后让const引用绑定到这个临时对象上:

cpp 复制代码
// 实际发生的事情:
// const int& r = 10;
// 等价于:
const int temp = 10;  // 创建临时对象
const int& r = temp;  // 绑定到临时对象

2.3 右值引用的诞生

C++11引入了右值引用 ,使用&&表示:

cpp 复制代码
int&& rr1 = 10;                // OK,右值引用绑定右值
int&& rr2 = x + y;             // OK
string&& rr3 = string("hi");   // OK

int a = 5;
// int&& rr4 = a;              // 错误!右值引用不能直接绑定左值

使用std::move绑定左值

虽然右值引用不能直接绑定左值,但可以通过std::move将左值转换为右值引用:

cpp 复制代码
int a = 10;
int&& rr = std::move(a);  // OK,std::move将a转换为右值引用

std::move的本质

std::move并不真的"移动"任何东西,它只是一个类型转换,将左值强制转换为右值引用:

cpp 复制代码
template <class T>
typename remove_reference<T>::type&& move(T&& arg)
{
    return static_cast<typename remove_reference<T>::type&&>(arg);
}

2.4 重要的属性规则

这是一个非常重要但容易混淆的概念:

变量表达式都是左值属性

即使一个变量被声明为右值引用类型,当它作为表达式使用时,它的属性仍然是左值!

cpp 复制代码
int&& rr1 = 10;

// rr1是一个右值引用类型的变量
// 但rr1这个变量表达式本身是左值!
cout << &rr1 << endl;  // OK,可以取地址,说明是左值

int& r = rr1;          // OK,左值引用可以绑定rr1
// int&& rr2 = rr1;    // 错误!右值引用不能绑定左值

int&& rr2 = std::move(rr1);  // 必须使用move

为什么这样设计?

这个设计看起来很奇怪,但实际上非常合理。我们将在移动语义部分看到,这个设计保证了移动操作的安全性------只有当我们明确表示"这个对象可以被移动"时,才会发生移动。


三、左值和右值的函数匹配

3.1 函数重载的优先级

C++11以后,我们可以针对左值引用、const左值引用和右值引用分别重载函数:

cpp 复制代码
void f(int& x)
{
    cout << "左值引用重载 f(" << x << ")" << endl;
}

void f(const int& x)
{
    cout << "const左值引用重载 f(" << x << ")" << endl;
}

void f(int&& x)
{
    cout << "右值引用重载 f(" << x << ")" << endl;
}

匹配规则

  1. 左值优先匹配左值引用版本
  2. const左值优先匹配const左值引用版本
  3. 右值优先匹配右值引用版本
  4. 如果没有右值引用版本,右值会退而求其次匹配const左值引用版本
cpp 复制代码
int main()
{
    int i = 1;
    const int ci = 2;
    
    f(i);             // 调用 f(int&)
    f(ci);            // 调用 f(const int&)
    f(3);             // 调用 f(int&&)
    f(std::move(i));  // 调用 f(int&&)
    
    // 右值引用变量的匹配
    int&& rr = 10;
    f(rr);            // 调用 f(int&),因为rr是左值!
    f(std::move(rr)); // 调用 f(int&&)
    
    return 0;
}

3.2 实际输出分析

运行上面的代码,输出如下:

bash 复制代码
左值引用重载 f(1)
const左值引用重载 f(2)
右值引用重载 f(3)
右值引用重载 f(1)
左值引用重载 f(10)
右值引用重载 f(10)

这个输出完美印证了我们前面说的规则:右值引用类型的变量表达式仍然是左值。


四、移动语义的实现与应用

4.1 为什么需要移动语义

在讲移动语义之前,让我们先看一个性能问题:

cpp 复制代码
string func()
{
    string str("hello world, this is a very long string!");
    return str;  // 这里会发生拷贝
}

int main()
{
    string s = func();  // 这里又会发生拷贝
    return 0;
}

在C++98中,上面的代码理论上会发生两次深拷贝(虽然编译器可能优化)。问题在于:

  1. func内部的str是一个即将销毁的临时对象
  2. 我们把这个临时对象的内容拷贝出来
  3. 然后临时对象被销毁,它的资源也被释放
  4. 这是一种资源浪费------我们完全可以"窃取"临时对象的资源,而不是拷贝

移动语义的核心思想

既然临时对象马上就要销毁了,我们为什么不直接"拿走"它的资源,而要费力去拷贝呢?这就是移动语义的核心思想。

4.2 实现移动构造函数

让我们基于之前实现的bit::string类,添加移动构造函数:

cpp 复制代码
namespace bit
{
    class string
    {
    public:
        typedef char* iterator;
        
        // 构造函数
        string(const char* str = "")
            : _size(strlen(str))
            , _capacity(_size)
        {
            cout << "string(const char*) - 构造" << endl;
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }
        
        // 拷贝构造函数
        string(const string& s)
            : _str(nullptr)
        {
            cout << "string(const string&) - 拷贝构造" << endl;
            string tmp(s.c_str());
            swap(tmp);
        }
        
        // 移动构造函数
        string(string&& s)
            : _str(nullptr)
            , _size(0)
            , _capacity(0)
        {
            cout << "string(string&&) - 移动构造" << endl;
            swap(s);  // 直接交换资源
        }
        
        // 析构函数
        ~string()
        {
            cout << "~string() - 析构" << endl;
            delete[] _str;
            _str = nullptr;
        }
        
        void swap(string& s)
        {
            ::swap(_str, s._str);
            ::swap(_size, s._size);
            ::swap(_capacity, s._capacity);
        }
        
        const char* c_str() const { return _str; }
        size_t size() const { return _size; }
        
    private:
        char* _str;
        size_t _size;
        size_t _capacity;
    };
}

移动构造的实现分析

移动构造函数接受一个右值引用参数string&& s。它的实现非常简单:

  1. 将自己的成员初始化为空/零
  2. 调用swap,将参数s的资源交换过来
  3. 此时,s变成了空状态,但没关系,因为s是一个即将销毁的临时对象

这个过程没有任何内存分配或数据拷贝,只是指针的交换,效率极高!

4.3 实现移动赋值运算符

cpp 复制代码
// 拷贝赋值
string& operator=(const string& s)
{
    cout << "string& operator=(const string&) - 拷贝赋值" << endl;
    if (this != &s)
    {
        string tmp(s);
        swap(tmp);
    }
    return *this;
}

// 移动赋值
string& operator=(string&& s)
{
    cout << "string& operator=(string&&) - 移动赋值" << endl;
    swap(s);
    return *this;
}

移动赋值同样简单:直接交换资源即可。

4.4 测试移动语义

cpp 复制代码
int main()
{
    bit::string s1("hello");
    
    // 拷贝构造
    bit::string s2 = s1;
    cout << "-------------------" << endl;
    
    // 构造临时对象 + 移动构造(编译器可能优化为直接构造)
    bit::string s3 = bit::string("world");
    cout << "-------------------" << endl;
    
    // 移动构造
    bit::string s4 = std::move(s1);
    cout << "-------------------" << endl;
    
    return 0;
}

输出分析

bash 复制代码
string(const char*) - 构造
string(const string&) - 拷贝构造
-------------------
string(const char*) - 构造
string(string&&) - 移动构造
~string() - 析构
-------------------
string(string&&) - 移动构造
-------------------
~string() - 析构
~string() - 析构
~string() - 析构

可以看到,使用右值或std::move时,调用的是移动构造而不是拷贝构造,效率大大提升!


五、移动语义解决传值返回问题

5.1 问题场景

之前我们提到,左值引用解决了大部分场景的拷贝问题,但有些场景无法使用引用返回:

cpp 复制代码
string addStrings(string num1, string num2)
{
    string str;
    // ...复杂的计算...
    return str;  // 局部对象,不能返回引用
}

这种情况下,str是一个局部对象,函数结束后就会销毁,我们不能返回它的引用。C++98只能接受拷贝的代价,或者使用输出型参数。

5.2 移动语义的解决方案

有了移动语义后,返回局部对象时会发生什么呢?

场景一:构造接收对象

cpp 复制代码
bit::string func()
{
    bit::string str("hello world");
    cout << "---返回前---" << endl;
    return str;
}

int main()
{
    bit::string ret = func();
    cout << ret.c_str() << endl;
    return 0;
}

不同编译器的处理

在vs2019 debug模式下(关闭优化):

bash 复制代码
string(const char*) - 构造
---返回前---
string(string&&) - 移动构造    // str移动构造临时对象
~string() - 析构                // str析构
string(string&&) - 移动构造    // 临时对象移动构造ret
~string() - 析构                // 临时对象析构
hello world
~string() - 析构                // ret析构

在vs2019 release模式或vs2022中(编译器优化):

bash 复制代码
string(const char*) - 构造      // 直接构造ret
---返回前---
hello world
~string() - 析构

编译器将连续的移动操作优化成了直接构造!这种优化叫做返回值优化(RVO)

场景二:赋值给已存在对象

cpp 复制代码
int main()
{
    bit::string ret;
    ret = func();
    cout << ret.c_str() << endl;
    return 0;
}

输出(debug模式):

bash 复制代码
string(const char*) - 构造      // ret的默认构造
string(const char*) - 构造      // func内的str
---返回前---
string(string&&) - 移动构造    // str移动构造临时返回对象
~string() - 析构                // str析构
string& operator=(string&&) - 移动赋值  // 临时对象移动赋值给ret
~string() - 析构                // 临时对象析构
hello world
~string() - 析构                // ret析构

即使在这种情况下,编译器不进行优化,我们使用的也是移动而不是拷贝,效率仍然很高。

5.3 编译器优化的层次

编译器对返回值的优化有不同的层次:

层次一:没有移动语义(C++98)

  • 需要两次深拷贝
  • 效率最低

层次二:有移动语义,但未优化

  • 两次移动操作
  • 比拷贝快很多,但仍有优化空间

层次三:返回值优化(RVO)

  • 直接在目标位置构造对象
  • 完全避免移动和拷贝
  • 效率最高

现代编译器通常会自动进行RVO优化,但即使没有优化,有了移动语义也比C++98的拷贝要高效得多。


六、移动语义在容器中的应用

6.1 STL容器的接口升级

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

cpp 复制代码
// vector的push_back
void push_back(const value_type& val);  // 左值版本
void push_back(value_type&& val);       // 右值版本

// vector的insert
iterator insert(const_iterator position, const value_type& val);
iterator insert(const_iterator position, value_type&& val);

使用示例

cpp 复制代码
#include <list>
using namespace std;

int main()
{
    list<bit::string> lt;
    
    // 场景1:插入左值(拷贝)
    bit::string s1("111111111111");
    lt.push_back(s1);
    cout << "-------------------" << endl;
    
    // 场景2:插入右值(移动)
    lt.push_back(bit::string("222222222222"));
    cout << "-------------------" << endl;
    
    // 场景3:字面值隐式转换(先构造临时对象,再移动)
    lt.push_back("333333333333");
    cout << "-------------------" << endl;
    
    // 场景4:move左值(移动)
    lt.push_back(std::move(s1));
    cout << "-------------------" << endl;
    
    return 0;
}

输出分析

bash 复制代码
string(const char*) - 构造
string(const string&) - 拷贝构造    // 场景1:拷贝s1
-------------------
string(const char*) - 构造
string(string&&) - 移动构造        // 场景2:移动临时对象
~string() - 析构
-------------------
string(const char*) - 构造          // 场景3:先构造临时对象
string(string&&) - 移动构造        // 再移动
~string() - 析构
-------------------
string(string&&) - 移动构造        // 场景4:移动s1
-------------------
~string() - 析构
~string() - 析构
~string() - 析构
~string() - 析构
~string() - 析构

6.2 在list中实现移动语义

让我们为之前实现的list添加移动语义的支持:

cpp 复制代码
namespace bit
{
    template<class T>
    struct ListNode
    {
        ListNode<T>* _next;
        ListNode<T>* _prev;
        T _data;
        
        // 左值构造
        ListNode(const T& data = T())
            : _next(nullptr)
            , _prev(nullptr)
            , _data(data)
        {}
        
        // 右值构造
        ListNode(T&& data)
            : _next(nullptr)
            , _prev(nullptr)
            , _data(std::move(data))  
        {}
    };
    
    template<class T>
    class list
    {
    public:
        // 左值版本的push_back
        void push_back(const T& x)
        {
            insert(end(), x);
        }
        
        // 右值版本的push_back
        void push_back(T&& x)
        {
            insert(end(), std::move(x));
        }
        
        // 左值版本的insert
        iterator insert(iterator pos, const T& x)
        {
            Node* cur = pos._node;
            Node* newnode = new Node(x);
            Node* prev = cur->_prev;
            
            prev->_next = newnode;
            newnode->_prev = prev;
            newnode->_next = cur;
            cur->_prev = newnode;
            
            return iterator(newnode);
        }
        
        // 右值版本的insert
        iterator insert(iterator pos, T&& x)
        {
            Node* cur = pos._node;
            Node* newnode = new Node(std::move(x));
            Node* prev = cur->_prev;
            
            prev->_next = newnode;
            newnode->_prev = prev;
            newnode->_next = cur;
            cur->_prev = newnode;
            
            return iterator(newnode);
        }
    };
}

七、引用折叠与完美转发

7.1 引用的引用问题

在C++中,我们不能直接定义引用的引用:

cpp 复制代码
int x = 10;
int& r = x;
// int& & rr = r;  // 编译错误!

但是,通过模板或typedef,可能会间接产生"引用的引用"的情况。这时,C++11引入了引用折叠规则来处理。

7.2 引用折叠规则

规则很简单

  • 所有的右值引用折叠到右值引用上仍然是一个右值引用
  • 所有的其他引用类型之间的叠加都将变成左值引用

换句话说:只有右值引用 + 右值引用 = 右值引用,其他情况都是左值引用

cpp 复制代码
typedef int& lref;
typedef int&& rref;

int n = 0;

lref& r1 = n;    // int& &  折叠为 int&
lref&& r2 = n;   // int& && 折叠为 int&
rref& r3 = n;    // int&& & 折叠为 int&
rref&& r4 = 1;   // int&& &&折叠为 int&&

7.3 万能引用

模板中的T&&

在函数模板中,T&&是一个特殊的存在:

cpp 复制代码
template<class T>
void f(T&& x)
{
    // ...
}

这里的T&&不一定是右值引用!它被称为万能引用转发引用

如下:我们定义了一个整型变量a

  • 如果传递的实参是左值,T被推导为int&,经过引用折叠后T&&变成int&
  • 如果传递的实参是右值,T被推导为intT&&就是int&&
cpp 复制代码
template<class T>
void f(T&& x) {}

int main()
{
    int a = 10;
    
    f(a);              // T推导为int&, T&&折叠为int&
    f(10);             // T推导为int,  T&&就是int&&
    f(std::move(a));   // T推导为int,  T&&就是int&&
    
    return 0;
}

7.4 完美转发

问题的提出

考虑下面的代码:

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<class T>
void Function(T&& t)
{
    Fun(t);  // 问题:t总是匹配左值版本!
}

int main()
{
    Function(10);           // 传入右值
    
    int a = 5;
    Function(a);            // 传入左值
    Function(std::move(a)); // 传入右值
    
    const int b = 10;
    Function(b);            // 传入const左值
    Function(std::move(b)); // 传入const右值
    
    return 0;
}

输出结果

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

问题分析

虽然Function能够通过万能引用接收任意类型的参数,但在函数内部调用Fun(t)时,由于t是一个变量表达式,它的属性总是左值,所以总是匹配左值版本的Fun

这样就失去了我们传递右值的意义------我们希望如果传入的是右值,就调用右值版本的Fun;如果传入的是左值,就调用左值版本的Fun

std::forward的作用

C++11提供了std::forward来解决这个问题,它能够完美转发参数的值类别:

cpp 复制代码
template<class T>
void Function(T&& t)
{
    Fun(std::forward<T>(t));  // 完美转发
}

int main()
{
    Function(10);           // 调用Fun(int&&)
    
    int a = 5;
    Function(a);            // 调用Fun(int&)
    Function(std::move(a)); // 调用Fun(int&&)
    
    const int b = 10;
    Function(b);            // 调用Fun(const int&)
    Function(std::move(b)); // 调用Fun(const int&&)
    
    return 0;
}

输出结果

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

完美!现在参数的值类别被正确地保持并转发了。

std::forward的实现原理

两个重载

cpp 复制代码
template <class T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {
    return static_cast<T&&>(t);
}

template <class T>
T&& forward(typename std::remove_reference<T>::type&& t) noexcept {
    static_assert(!std::is_lvalue_reference<T>::value, "bad forward");
    return static_cast<T&&>(t);
}

forward的原理也是利用引用折叠:

  • 如果Tint&(左值情况),T&&折叠为int&,返回左值引用
  • 如果Tint(右值情况),T&&就是int&&,返回右值引用

完美转发的典型应用场景

cpp 复制代码
template<class T>
class vector
{
public:
    // 使用完美转发实现通用的emplace_back
    template<class... Args>
    void emplace_back(Args&&... args)
    {
        // 完美转发参数包给元素的构造函数
        new (_finish) T(std::forward<Args>(args)...);
        ++_finish;
    }
    
private:
    T* _start;
    T* _finish;
    T* _endofstorage;
};

八、值类别的细化分类

8.1 C++11的值类别体系

C++11对值类别进行了更细致的划分:

bash 复制代码
        表达式
       /      \
    glvalue   rvalue
    /   \      /   \
lvalue  xvalue  prvalue

三种基本类别

  1. 左值(lvalue):传统意义上的左值,有持久的存储位置

    • 变量名、函数名
    • 返回左值引用的函数调用
    • 字符串字面值
    • 前置递增/递减运算符的返回值
  2. 纯右值(prvalue - pure rvalue):传统意义上的右值

    • 字面值常量(除字符串外):42truenullptr
    • 返回非引用类型的函数调用
    • 后置递增/递减运算符的返回值
    • 算术表达式、逻辑表达式的结果
  3. 将亡值(xvalue - expiring value):即将被移动的对象

    • 返回右值引用的函数调用:std::move(x)
    • 转换为右值引用的类型转换:static_cast<X&&>(x)

两种混合类别

  1. 泛左值(glvalue - generalized lvalue):左值 + 将亡值
  2. 右值(rvalue):纯右值 + 将亡值

8.2 实际应用中的理解

在实际编程中,我们通常只需要区分:

  • 左值:可以取地址的持久对象
  • 右值:临时对象或即将销毁的对象

将亡值的概念主要是为了在类型系统层面更精确地描述移动语义。


九、总结与最佳实践

9.1 核心概念回顾

左值与右值

  • 左值:有持久状态,可以取地址
  • 右值:临时对象,不能取地址
  • 判断标准:能否取地址

左值引用与右值引用

  • 左值引用(&):绑定左值
  • 右值引用(&&):绑定右值
  • const左值引用:可以绑定任何值类别
  • 变量表达式都是左值,即使它的类型是右值引用

移动语义

  • 移动构造:T(T&& other)
  • 移动赋值:T& operator=(T&& other)
  • 核心思想:窃取资源而不是拷贝
  • 使用场景:传值返回、容器操作

引用折叠与完美转发

  • 引用折叠:只有&& + && = &&,其他都是&
  • 万能引用:模板中的T&&
  • 完美转发:std::forward<T>(t)保持参数的值类别

9.2 实践建议

何时实现移动语义

  1. 类管理动态资源(堆内存、文件句柄等)
  2. 类的拷贝成本较高
  3. 类会被用作容器的元素类型

实现移动函数的注意事项

  1. 移动后的对象应处于有效但未指定的状态
  2. 移动操作应该是noexcept的(提升性能)
  3. 移动后源对象的析构函数仍会被调用
cpp 复制代码
class Buffer
{
public:
    // 移动构造应该标记为noexcept
    Buffer(Buffer&& other) noexcept
        : _data(other._data)
        , _size(other._size)
    {
        other._data = nullptr;  // 让源对象进入安全状态
        other._size = 0;
    }
    
    ~Buffer()
    {
        delete[] _data;  // 源对象的析构仍会执行
    }
    
private:
    char* _data;
    size_t _size;
};

使用std::move的场景

cpp 复制代码
// 场景1:传递给容器
vector<string> vec;
string str = "hello";
vec.push_back(std::move(str));  // str被移动,之后不应再使用

// 场景2:返回成员变量
class Widget
{
    string _data;
public:
    string getData() &&  // 右值限定符
    {
        return std::move(_data);  // 对象是右值时可以移动
    }
};

// 场景3:实现移动赋值
Widget& operator=(Widget&& other)
{
    _data = std::move(other._data);
    return *this;
}

不要过度使用std::move

cpp 复制代码
// 错误示例:不必要的move
string func()
{
    string str = "hello";
    return std::move(str);  // 多余!编译器会自动优化
}

// 正确做法
string func()
{
    string str = "hello";
    return str;  // 返回局部变量时自动视为右值
}

9.3 性能对比

让我们看一个实际的性能测试:

cpp 复制代码
#include <iostream>
#include <vector>
#include <chrono>

class BigObject
{
public:
    BigObject() : _data(new int[1000000]) {}
    
    // 拷贝构造
    BigObject(const BigObject& other)
        : _data(new int[1000000])
    {
        std::copy(other._data, other._data + 1000000, _data);
    }
    
    // 移动构造
    BigObject(BigObject&& other) noexcept
        : _data(other._data)
    {
        other._data = nullptr;
    }
    
    ~BigObject() { delete[] _data; }
    
private:
    int* _data;
};

int main()
{
    using namespace std::chrono;
    
    // 测试拷贝
    auto start = high_resolution_clock::now();
    {
        std::vector<BigObject> vec;
        BigObject obj;
        for (int i = 0; i < 1000; ++i)
        {
            vec.push_back(obj);  // 拷贝
        }
    }
    auto end = high_resolution_clock::now();
    cout << "拷贝耗时: " 
         << duration_cast<milliseconds>(end - start).count() 
         << "ms" << endl;
    
    // 测试移动
    start = high_resolution_clock::now();
    {
        std::vector<BigObject> vec;
        for (int i = 0; i < 1000; ++i)
        {
            vec.push_back(BigObject());  // 移动
        }
    }
    end = high_resolution_clock::now();
    cout << "移动耗时: " 
         << duration_cast<milliseconds>(end - start).count() 
         << "ms" << endl;
    
    return 0;
}

十、练习与思考

10.1 练习题

练习1:实现移动语义

为下面的动态数组类实现移动构造和移动赋值:

cpp 复制代码
class DynamicArray
{
public:
    DynamicArray(size_t size)
        : _data(new int[size])
        , _size(size)
    {}
    
    // TODO: 实现拷贝构造
    // TODO: 实现移动构造
    // TODO: 实现拷贝赋值
    // TODO: 实现移动赋值
    
    ~DynamicArray() { delete[] _data; }
    
private:
    int* _data;
    size_t _size;
};

练习2:使用万能引用

实现一个工厂函数,能够完美转发参数:

cpp 复制代码
template<class T, class... Args>
unique_ptr<T> make_unique(Args&&... args)
{
    // TODO: 使用完美转发创建对象
}

练习3:性能优化

优化下面的代码,使用移动语义减少不必要的拷贝:

cpp 复制代码
vector<string> getStrings()
{
    vector<string> result;
    result.push_back(string("hello"));
    result.push_back(string("world"));
    return result;
}

10.2 思考题

  1. 为什么右值引用变量的表达式是左值?这样设计的好处是什么?

  2. 在什么情况下编译器不会自动生成移动构造函数?

  3. std::movestd::forward有什么区别?什么时候用哪个?

  4. 移动后的对象必须保持什么样的状态?为什么?


十一、下一篇预告

在下一篇文章中,我们将学习C++11的高级特性:

  • 可变参数模板:编译期递归,参数包展开
  • lambda表达式:匿名函数,捕获列表的奥秘
  • function与bind:函数包装器,参数绑定
  • emplace系列接口:就地构造的魔法

这些特性会让你的C++代码更加简洁、优雅和高效!

通过本文的学习,我们深入理解了右值引用和移动语义这个C++11中最重要的特性。移动语义不仅提升了性能,更重要的是它改变了我们思考资源管理的方式。掌握这些知识,你就掌握了现代C++的精髓!

以上就是C++11右值引用与移动语义的全部内容,希望这篇文章能够帮助你深入理解这个强大的特性!如有疑问,欢迎在评论区交流讨论!❤️

相关推荐
罗湖老棍子2 小时前
瑞瑞的木板(洛谷P1334 )
c++·算法·优先队列·贪心·哈夫曼树
embrace992 小时前
【数据结构学习】数据结构和算法
c语言·数据结构·c++·学习·算法·链表·哈希算法
milan-xiao-tiejiang2 小时前
ROS2面试准备
c++·面试·自动驾驶
杨恒982 小时前
GESPC++三级编程题 知识点
数据结构·c++·算法
week_泽2 小时前
题目 3330: 蓝桥杯2025年第十六届省赛真题-01 串
c++·贪心算法·蓝桥杯
历程里程碑2 小时前
LeetCode 283:原地移动零的优雅解法
java·c语言·开发语言·数据结构·c++·算法·leetcode
kupeThinkPoem3 小时前
std::thread的使用
c++
Eloudy3 小时前
通过示例看 C++ 函数对象、仿函数、operator( )
开发语言·c++·算法