【C++】C++11:右值引用和移动语义 - 教程

目录

一、左值和右值
二、左值引用和右值引用
2.1 引用延长生命周期
2.2 左值和右值的参数匹配
三、右值引用和移动语义的使用场景
3.1 左值引用主要使用场景
3.2 移动构造和移动赋值
3.3 右值引用解决传值返回问题
3.4 右值引用在传参中的提效
四、类型分类
五、引用折叠
六、完美转发
七、总结


一、左值和右值

在C++中,每个表达式都有两个属性:类型值类别。值类别主要分为左值和右值。

左值 (lvalue)

  • 是一个表示数据的表达式(如变量名或解引用的指针)
  • 具有持久状态,存储在内存中
  • 可以获取它的地址
  • 可以出现在赋值符号的左边或右边
  • 被const修饰的左值不能赋值,但可以取地址

右值 (rvalue)

  • 也是一个表示数据的表达式
  • 包括字面值常量、表达式求值过程中创建的临时对象等
  • 可以出现在赋值符号的右边,但不能出现在左边
  • 不能取地址

现代C++解释

  • lvalue:locator value,可定位的值,有明确存储地址的对象
  • rvalue:read value,只读的值,提供数据值但不可寻址
#include<iostream>
  using namespace std;
  int main()
  {
  // 左值:可以取地址
  int *p = new int(0);
  int b = 1;
  const int c = b;
  *p = 10;
  string s("111111");
  s[0] = 'x';
  cout << &c << endl;
  cout << (void*)&s[0] << endl;
  // 右值:不能取地址
  double x = 1.1, y = 2.2;
  10;
  x + y;
  fmin(x, y);
  string("11111");
  // 以下代码编译错误,因为右值不能取地址
  // cout << &10 << endl;
  // cout << &(x+y) << endl;
  // cout << &(fmin(x, y)) << endl;
  // cout << &string("11111") << endl;
  return 0;
  }

二、左值引用和右值引用

2.1 基本概念

  • 左值引用:给左值取别名,Type&
  • 右值引用:给右值取别名,Type&&
int main()
{
// 左值
int* p = new int(0);
int b = 1;
const int c = b;
string s("111111");
double x = 1.1, y = 2.2;
// 左值引用给左值取别名
int& r1 = b;
int*& r2 = p;
int& r3 = *p;
string& r4 = s;
// 右值引用给右值取别名
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("11111");
return 0;
}

2.2 引用规则

  • 左值引用不能直接引用右值,但const左值引用可以引用右值
  • 右值引用不能直接引用左值,但可以引用move(左值)
// 左值引用不能直接引用右值,但const左值引用可以
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
// 右值引用不能直接引用左值,但可以引用move(左值)
int&& rrx1 = move(b);
int&& rrx2 = move(*p);
string&& rrx3 = move(s);

2.3 引用延长生命周期

右值引用和const左值引用都可以延长临时对象的生命周期,但只有右值引用允许修改对象。

int main()
{
std::string s1 = "Test";
const std::string& r2 = s1 + s1;  // OK: const左值引用延长生命周期
// r2 += "Test";                  // 错误:不能通过const引用修改
std::string&& r3 = s1 + s1;       // OK: 右值引用延长生命周期
r3 += "Test";                     // OK: 能通过非const引用修改
std::cout << r3 << '\n';
return 0;
}

2.4 左值和右值的参数匹配

C++11支持根据实参的值类别进行函数重载解析:

#include<iostream>
  using namespace std;
  void f(int& x)
  {
  std::cout << "左值引用重载 f(" << x << ")\n";
  }
  void f(const int& x)
  {
  std::cout << "到 const 的左值引用重载 f(" << x << ")\n";
  }
  void f(int&& x)
  {
  std::cout << "右值引用重载 f(" << x << ")\n";
  }
  int main()
  {
  int i = 1;
  const int ci = 2;
  f(i);               // 调用 f(int&)
  f(ci);              // 调用 f(const int&)
  f(3);               // 调用 f(int&&), 如果没有 f(int&&) 重载则会调用 f(const int&)
  f(std::move(i));    // 调用 f(int&&)
  int&& x = 1;
  f(x);               // 调用 f(int&) - 注意:右值引用变量本身是左值!
  f(std::move(x));    // 调用 f(int&&)
  return 0;
  }

重要特性

  • 右值引用变量在用于表达式时属性是左值
  • 这种设计在移动语义中有重要价值

2.5 move函数

std::move本质上是一个强制类型转换,将左值转换为右值引用:

template <class T>
  std::remove_reference_t<T>&& move(T&& arg)
    {
    return static_cast<std::remove_reference_t<T>&&>(arg);
      }

remove_reference_t是一个类型特性,用于移除类型的引用修饰符。
作用示例:

  • remove_reference_t<int>int
  • remove_reference_t<int&>int
  • remove_reference_t<int&&>int
std::move的实际使用场景
场景1:移动构造和移动赋值
class MyString {
private:
char* data_;
size_t size_;
public:
// 移动构造函数
MyString(MyString&& other)
: data_(std::move(other.data_))  // 移动指针
, size_(std::move(other.size_))  // 移动size
{
other.data_ = nullptr;  // 置空源对象
other.size_ = 0;
}
// 移动赋值运算符
MyString& operator=(MyString&& other){
if (this != &other) {
delete[] data_;  // 释放当前资源
// 移动资源
data_ = std::move(other.data_);
size_ = std::move(other.size_);
// 置空源对象
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
};
场景2:容器操作优化
#include <vector>
  #include <string>
    void containerOperations() {
    std::vector<std::string> vec;
      std::string largeString = "这是一个很长的字符串...";
      // 传统方式:拷贝构造(性能差)
      vec.push_back(largeString);           // 拷贝
      // 现代方式:移动语义(性能好)
      vec.push_back(std::move(largeString)); // 移动
      // largeString现在处于有效但未定义状态
      // 不应该再使用它,除非重新赋值
      largeString = "重新赋值后可以继续使用";
      }
场景3:函数返回值优化
class HeavyObject {
// 假设这是一个包含大量数据的对象
};
HeavyObject createHeavyObject() {
HeavyObject obj;
// ... 初始化obj
return std::move(obj);  // 明确要求移动
}
// 更好的做法:依赖RVO(返回值优化)
HeavyObject createHeavyObjectBetter() {
HeavyObject obj;
// ... 初始化obj  
return obj;  // 编译器会自动优化,可能比std::move更好
}
std::move的注意事项和陷阱
陷阱1:误用内置类型

对于内置类型,std::move没有意义,因为内置类型的"移动"就是拷贝。

int x = 42;
int y = std::move(x);  // 这仍然是拷贝!内置类型没有移动语义
陷阱2:移动后继续使用对象
std::string str1 = "Hello";
std::string str2 = std::move(str1);
// str1现在处于有效但未定义状态
// 以下使用是危险的:
std::cout << str1;  // 未定义行为!
// 安全做法:移动后重新赋值或不再使用
str1 = "New Value";  // 重新赋值后可以安全使用
陷阱3:不必要的移动
std::string getName() {
std::string name = "John";
return std::move(name);  // 不必要!可能阻止RVO(返回值优化)
}
// 编译器通常能更好地优化:
std::string getNameBetter() {
std::string name = "John";
return name;  // 让编译器决定是否优化
}
结论
  • 它不移动数据,只是类型转换
  • 它标记对象为可移动,启用移动语义
  • 使用要谨慎,避免阻止编译器优化
  • 移动后对象状态需要特别注意

三、右值引用和移动语义的使用场景

3.1 左值引用主要使用场景

  • 左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的值。
  • 左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如下面两个函数,C++98中的解决方案只能是被迫使用输出型参数解决。
  • 那么C++11以后这里可以使用右值引用做返回值解决吗?显然也是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法改变对象已经析构销毁的事实。
class Solution {
public:
// 传值返回需要拷贝
string addStrings(string num1, string num2) {
string str;
int end1 = num1.size()-1, end2 = num2.size()-1;
// 进位
int next = 0;
while(end1 >= 0 || end2 >= 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 += ('0'+ret);
}
if(next == 1)
str += '1';
reverse(str.begin(), str.end());
return str;
}
};
class Solution {
public:
// 这里的停值返回拷贝代价就太大了
vector<vector<int>> generate(int numRows) {
  vector<vector<int>> vv(numRows);
    for(int i = 0; i < numRows; ++i)
    {
    vv[i].resize(i+1, 1);
    }
    for(int i = 2; i < numRows; ++i)
    {
    for(int j = 1; j < i; ++j)
    {
    vv[i][j] = vv[i-1][j] + vv[i-1][j-1];
    }
    }
    return vv;
    }
    };

3.2 移动构造和移动赋值

  • 移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类型类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
  • 移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类型类型的引用,但是不同的是要求这个参数是右值引用。
  • 对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,他的本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从而提高效率。下面的codebyv::string样例实现了移动构造和移动赋值,我们结合场景理解。
自定义string示例
namespace codebyv
{
class string
{
public:
// 构造函数
string(const char* str = "")
: _size(strlen(str)), _capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 拷贝构造
string(const string& s)
: _str(nullptr)
{
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
// 拷贝赋值
string& operator=(const string& s)
{
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
return *this;
}
// 移动构造
string(string&& s)
{
swap(s);  // 窃取资源
}
// 移动赋值
string& operator=(string&& s)
{
swap(s);  // 窃取资源
return *this;
}
~string()
{
delete[] _str;
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}
int main()
{
codebyv::string s1("xxxxx");
codebyv::string s2 = s1;           // 拷贝构造
codebyv::string s3 = codebyv::string("yyyyy");  // 构造+移动构造,优化后直接构造
codebyv::string s4 = move(s1);     // 移动构造
return 0;
}
移动构造和拷贝构造的区别:
  • 拷贝构造采用的是const左值引用接收参数,无论拷贝构造对象时传入的是左值还是右值,都只会调用拷贝构造函数。
  • 移动构造采用的是右值引用接收参数,如果拷贝构造对象时传入的是右值,那么就会调用移动构造函数(最匹配原则)。
  • string的拷贝构造函数做的是深拷贝,而移动构造函数中只需要调用swap函数进行资源的转移,因此调用移动构造的代价比调用拷贝构造的代价小。
移动赋值和拷贝赋值的区别:
  • 拷贝赋值函数采用的是const左值引用接收参数,无论赋值时传入的是左值还是右值,都会调用原有的operator=函数。
  • 移动赋值采用的是右值引用接收参数,如果赋值时传入的是右值,那么就会调用移动赋值函数(最匹配原则)。
  • 拷贝赋值做的是深拷贝,而移动赋值函数中只需要调用swap函数进行资源的转移,因此调用移动赋值的代价比调用原有operator=的代价小。

为什么说移动语义是“窃取”资源?
因为在移动操作中,资源(如内存)从源对象(移动源)直接转移到目标对象(移动目标),源对象失去了对这些资源的控制权。这就像是目标对象“窃取”了源对象的资源。
移动操作后,源对象通常处于一个有效但未定义的状态。这意味着源对象仍然存在,但其内部状态是不确定的,可能包含一些残留的数据。这种状态类似于资源被“窃取”后留下的空壳。

3.3 右值引用解决传值返回问题

在函数返回局部对象时,传统方式需要拷贝构造,使用移动语义可以避免不必要的拷贝。
在下面的例子中,虽然addStrings当中返回的局部string对象是一个左值,但由于该string对象在当前函数调用结束后就会立即被销毁,我们可以把这种即将被销毁的值叫做“将亡值”,比如匿名对象也可以叫做“将亡值”。
既然“将亡值”马上就要被销毁了,那还不如把它的资源转移给别人用,因此编译器在识别这种“将亡值”时会将其识别为右值,这样就可以匹配到参数类型为右值引用的移动构造或赋值函数。

namespace codebyv
{
string addStrings(string num1, string num2)
{
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
int next = 0;
while (end1 >= 0 || end2 >= 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 += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
return str;  // 这里会调用移动构造而不是拷贝构造
}
}
// 场景1:构造新对象
int main()
{
codebyv::string ret = codebyv::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
// 场景2:赋值给已有对象  
int main()
{
codebyv::string ret;
ret = codebyv::addStrings("11111", "2222");  // 调用移动赋值
cout << ret.c_str() << endl;
return 0;
}

3.4 右值引用在传参中的提效

STL容器在C++11后增加了右值引用版本的插入接口,显著提升性能。

int main()
{
std::list<bit::string> lt;
  codebyv::string s1("!!!!!!!!!!!!!!!!!!!!!!");
  lt.push_back(s1);  // 左值,调用拷贝构造
  lt.push_back(bit::string("222222222222222222222222222222"));  // 右值,调用移动构造
  lt.push_back("33333333333333333333333333333");  // 右值,调用移动构造
  lt.push_back(move(s1));  // 将左值转为右值,调用移动构造
  return 0;
  }
自定义容器支持右值引用
namespace codebyv
{
template<class T>
  class list
  {
  public:
  void push_back(const T& x)
  {
  insert(end(), x);
  }
  void push_back(T&& x)
  {
  insert(end(), move(x));  // 移动语义
  }
  iterator insert(iterator pos, const T& x)
  {
  // 拷贝构造新节点
  Node* newnode = new Node(x);
  // ... 链接节点
  }
  iterator insert(iterator pos, T&& x)
  {
  // 移动构造新节点  
  Node* newnode = new Node(move(x));
  // ... 链接节点
  }
  };
  }

四、类型分类

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

4.1 新的值类别体系

在这里插入图片描述

4.2 各类别定义

  • 纯右值 (pvalue)

    • 字面值常量:42truenullptr
    • 求值结果相当于字面值的表达式
    • 不具名的临时对象
    • 示例:str.substr(1, 2)str1 + str2a++a+b
  • 将亡值 (xvalue)

    • 返回右值引用的函数调用表达式
    • 转换为右值引用的转换函数调用表达式
    • 示例:move(x)static_cast<X&&>(x)
  • 泛左值 (gValue)

    • 包含左值和将亡值

五、引用折叠

5.1 基本概念

C++中不能直接定义引用的引用,但通过模板或typedef可以间接构成引用的引用,此时会触发引用折叠规则。

5.2 引用折叠规则

  • 右值引用的右值引用折叠成右值引用
  • 所有其他组合均折叠成左值引用
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n;        // r1 的类型是 int&
lref&& r2 = n;       // r2 的类型是 int&
rref& r3 = n;        // r3 的类型是 int&
rref&& r4 = 1;       // r4 的类型是 int&&

5.3 万能引用 (Universal Reference)

在模板函数中,T&&参数由于引用折叠规则,可以根据实参的值类别实例化为左值引用或右值引用,因此被称为"万能引用"。

template<class T>
  void Function(T&& t)
  {
  int a = 0;
  T x = a;
  cout << &a << endl;
  cout << &x << endl << endl;
  }
  int main()
  {
  int a;
  const int b = 8;
  Function(10);            // T推导为int,实例化为void Function(int&& t)
  Function(a);             // T推导为int&,折叠为void Function(int& t)
  Function(std::move(a));  // T推导为int,实例化为void Function(int&& t)
  Function(b);             // T推导为const int&,折叠为void Function(const int& t)
  Function(std::move(b));  // T推导为const int,实例化为void Function(const int&& t)
  return 0;
  }

六、完美转发

6.1 问题背景

在函数模板中,即使参数是右值引用类型,在函数内部该参数也是左值(因为它是变量)。

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);  // 期望调用右值引用版本,实际调用左值引用版本
  return 0;
  }

6.2 解决方案:std::forward

std::forward实现完美转发,保持参数的原始值类别。

template <class T>
  void Function(T&& t)
  {
  Fun(std::forward<T>(t));  // 完美转发
    }
    int main()
    {
    int a = 0;
    const int b = 8;
    Function(10);           // 右值 → 调用右值引用版本
    Function(a);            // 左值 → 调用左值引用版本  
    Function(std::move(a)); // 右值 → 调用右值引用版本
    Function(b);            // const左值 → 调用const左值引用版本
    Function(std::move(b)); // const右值 → 调用const右值引用版本
    return 0;
    }

6.3 forward实现原理

// 左值引用版本
template <class T>
  T&& forward(std::remove_reference_t<T>& arg) {
    return static_cast<T&&>(arg);
    }
    // 右值引用版本  
    template <class T>
      T&& forward(std::remove_reference_t<T>&& arg) {
        static_assert(!std::is_lvalue_reference_v<T>,
          "Cannot forward an rvalue as an lvalue");
          return static_cast<T&&>(arg);
          }

工作原理

  • 传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回
  • 传递给Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用返回

七、总结

核心要点

  1. 值类别体系

    • 左值:持久对象,可取地址
    • 纯右值:临时对象,不可取地址(C++98右值)
    • 将亡值:可移动的右值
  2. 引用类型

    • 左值引用:绑定左值
    • 右值引用:绑定右值,延长临时对象生命周期
  3. 引用折叠

    • 实现万能引用的关键机制
    • 只有&& &&折叠为&&,其他都折叠为&
  4. 移动语义

    • 移动构造和移动赋值通过"窃取"资源避免深拷贝
    • 显著提升包含资源管理类的性能
  5. 完美转发

    • 保持参数的原始值类别
    • 通过std::forward和引用折叠实现

最佳实践

  1. 类设计

    class ResourceManager
    {
    public:
    // 移动构造
    ResourceManager(ResourceManager&& other) noexcept
    : data_(std::move(other.data_))
    {
    other.data_ = nullptr;
    }
    // 移动赋值
    ResourceManager& operator=(ResourceManager&& other) noexcept
    {
    if (this != &other) {
    delete[] data_;
    data_ = std::move(other.data_);
    other.data_ = nullptr;
    }
    return *this;
    }
    private:
    int* data_ = nullptr;
    };
  2. 函数设计

    // 使用完美转发处理各种值类别
    template<typename T>
      void processData(T&& data)
      {
      // 使用std::forward保持值类别
      internalProcess(std::forward<T>(data));
        }
  3. 容器使用

    std::vector<std::string> vec;
      std::string str = "hello";
      vec.push_back(str);           // 拷贝
      vec.push_back(std::move(str)); // 移动
      vec.push_back("world");       // 移动(构造临时对象)

重要提醒

  • 右值引用变量是左值:这是理解移动语义和完美转发的关键
  • 引用延长生命周期:右值引用和const左值引用都可以延长临时对象生命周期
  • 编译器优化:现代编译器会对返回值进行优化,但移动语义提供了标准保证

右值引用和移动语义是C++11最重要的特性之一,它们彻底改变了C++中资源管理的方式,使得现代C++代码在保持安全性的同时获得了接近C语言的性能。

posted @ 2025-12-11 14:36  gccbuaa  阅读(22)  评论(0)    收藏  举报