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服务期高级架构


相关文章
|
1月前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
109 59
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(三)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(二)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
34 0
|
2天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
15 2
|
8天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
33 5
|
14天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
46 4
|
15天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
43 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
28 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
25 4