c++11新特性——右值引用和move语义

简介: c++11新特性——右值引用和move语义

一、背景

C++11中引用了右值引用和移动语义,可以避免无谓的复制,提高了程序性能。

二、move语义

作用:就是将左值转换为右值。

三、左值和右值

  • 左值可以取地址,位于等号左边
  • 右值不能取地址,位于等号右边

四、左值引用和右值引用

引用的本质是别名,传参时引用可以避免拷贝,并且在函数内部可以修改外部的值。

4.1 左值引用

定义:能指向左值,不能指向右值的引用称为左值引用。代码示例:

int a = 5;
int &left_ref_a = a; // 左值引用指向左值,ok 
int &left_ref_a = 5; //  左值引用指向右值,错

引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。但是,const 左值引用是可以指向右值的:

const int& left_ref_a = 5; // ok

因为,const 左值引用不会修改指向的值,因此可以指向右值。这也是为什么要使用const &作为函数参数的原因之一,因为const &即可以指向左值又可以指向右值。比如:vector的push_back成员函数,如果没有const &,那么在push_back右值的时候就会报错。

// void push_back(const T& val);
vector<int> vc;
vc.push_back(5); 
4.2 右值引用

顾名思义就是指向右值的引用,用来专门指向右值的,右值引用的标志是&&。

int a = 5;
int &&right_ref_a = 5; // ok
int &&left_ref_a = a; // 编译报错,指向了左值
right_ref_a  = 50; // 右值引用的作用,可以修改右值

右值引用可以指向左值吗?可以,通过move就可以。

int a = 5;
int &&right_ref_a = 5; // ok
int &&right_ref_a = std::move(a); // 通过move将左值转为右值
4.3 左值引用和右值引用的思考
  1. 实际上,std::move移动不了什么,唯一的作用就是把左值强制转化为右值
  2. 右值引用的本质是什么??为什么要有右值引用?右值引用能够指向右值,本质是把右值提升为左值,并定义一个右值引用通过std::move指向该左值。
int main()
{
  int &&right_ref_a = 5;
  right_ref_a  = 6;
  // 上面的代码等价于
  int tmp = 5;
  int &&right_ref_a = std::move(tmp);
  right_ref_a = 50;
  std::cout << "tmp: " << tmp << std::endl; // tmp=??? 5还是50,答案是:50
}
  1. 左值引用和右值引用本身是左值还是右值?答案是:左值。因为声明出来的左值引用和右值引用都是有地址的,位于等号左边,所以都是左值。验证代码如下:
// 形参是右值引用
void ChangeValue(int &&right_val)
{
  right_val = 100;
}
int main()
{
  int a = 5;
  int &left_ref_a = a;
  int &&right_ref_a = std::move(a);
  ChangeValue(a);  // 编译报错,a是左值
  ChangeValue(left_ref_a); // 编译报错,left_ref_a是左值
  ChangeValue(right_ref_a); // 编译报错,right_ref_a是左值
  ChangeValue(std::move(a)); // 编译ok
  ChangeValue(std::move(left_ref_a)); // 编译ok
  ChangeValue(std::move(right_ref_a)); // 编译ok
  // 这三个左值的地址是一样的
  std::cout << "&a " << &a << std::endl;
  std::cout << "&left_ref_a " << &left_ref_a << std::endl;
  std::cout << "&right_ref_a" << &right_ref_a << std::endl;
}
4.4 小结
  1. 从性能上讲,左值引用和右值引用都能避免拷贝,没什么区别;
  2. 右值引用即可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const T&也能指向右值);
  3. 作为函数形参时,右值引用更加灵活,虽然const的左值引用也能做到左右值都能接受,但是它无法修改,有一定的局限性。
4.5 右值引用和std::move的使用场景

右值引用优化性能,避免深拷贝

场景:对于还有堆内存的类,我们需要实现它的深拷贝构造函数,如果没实现,会调用该类的默认复制构造函数,导致多次释放同一资源。

4.5.1 浅拷贝重复释放

#include <iostream> 
using namespace std; 
class A {
public: 
  A() :m_ptr(new int(0)) { 
    cout << "constructor A" << endl; 
  }
  ~A(){
    cout << "destructor A, m_ptr:" << m_ptr << endl; 
    delete m_ptr;
    m_ptr = nullptr; 
  } 
private: 
  int* m_ptr; 
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{ 
  A a; 
  A b; 
  cout << "ready return" << endl; 
  if (flag) return a; 
  else return b; 
}
int main() { 
{ 
  A a = Get(false); 
  cout << "main finish" << endl; 
  return 0; 
}

4.5.2 深拷贝构造函数

class A {
public: 
  A() :m_ptr(new int(0)) { 
    cout << "constructor A" << endl; 
  }
  A(const A& a):m_ptr(new int(*a.m_ptr)){
    cout << "Copy constructor A" << endl; 
  }
  ~A(){
    cout << "destructor A, m_ptr:" << m_ptr << endl; 
    delete m_ptr;
    m_ptr = nullptr; 
  } 
private: 
  int* m_ptr; 
};

4.5.3 移动构造函数

核心:当复制构造函数和移动构造函数同时存在时,会优先调用移动构造函数。移动构造函数只是将对象的资源做了浅拷贝,从而避免的深拷贝,提高性能。这也就是所谓的移动语义,右值引用的一个重要作用就是支持移动语义。前提是需要实现移动构造函数。如果没有实现就会调用复制构造函数!

class A {
public: 
  A() :m_ptr(new int(0)) { 
    cout << "constructor A" << endl; 
  }
  A(const A& a):m_ptr(new int(*a.m_ptr)){
    cout << "Copy constructor A" << endl; 
  }
  // 移动构造函数,可以浅拷贝
  A(A&& a):m_ptr(a.m_ptr){
    a.m_ptr = nullptr;  // 为防止a析构时delete data,提前置空其m_ptr
    cout << "Move constructor A" << endl; 
  }
  ~A(){
    cout << "destructor A, m_ptr:" << m_ptr << endl; 
    delete m_ptr;
    m_ptr = nullptr; 
  } 
private: 
  int* m_ptr; 
};

文章参考与<零声教育>的C/C++linux服务期高级架构


相关文章
|
5月前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
190 59
|
4月前
|
存储 安全 C++
【C++11】右值引用
C++11引入的右值引用(rvalue references)是现代C++的重要特性,允许更高效地处理临时对象,避免不必要的拷贝,提升性能。右值引用与移动语义(move semantics)和完美转发(perfect forwarding)紧密相关,通过移动构造函数和移动赋值运算符,实现了资源的直接转移,提高了大对象和动态资源管理的效率。同时,完美转发技术通过模板参数完美地转发函数参数,保持参数的原始类型,进一步优化了代码性能。
66 2
|
4月前
|
安全 编译器 C++
【C++11】新特性
`C++11`是2011年发布的`C++`重要版本,引入了约140个新特性和600个缺陷修复。其中,列表初始化(List Initialization)提供了一种更统一、更灵活和更安全的初始化方式,支持内置类型和满足特定条件的自定义类型。此外,`C++11`还引入了`auto`关键字用于自动类型推导,简化了复杂类型的声明,提高了代码的可读性和可维护性。`decltype`则用于根据表达式推导类型,增强了编译时类型检查的能力,特别适用于模板和泛型编程。
39 2
|
5月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(三)
【C++】面向对象编程的三大特性:深入解析多态机制
|
5月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(二)
【C++】面向对象编程的三大特性:深入解析多态机制
|
5月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(一)
【C++】面向对象编程的三大特性:深入解析多态机制
|
5月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
107 1
|
6月前
|
编译器 C++ 容器
C++ 11新特性之语法甜点2
C++ 11新特性之语法甜点2
53 1
|
5月前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
78 0
|
5月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析继承机制(三)
【C++】面向对象编程的三大特性:深入解析继承机制