首页 > 编程语言 >【C++】右值引用

【C++】右值引用

时间:2022-10-30 15:24:04浏览次数:66  
标签:std 右值 int 左值 C++ 引用 ref

来源于:https://zhuanlan.zhihu.com/p/335994370

1.什么是右值引用

  • 左值可以取地址、位于等号左边。
  • 右值没法取地址、位于等号右边。
  • 有地址的变量就是左值,没有地址的字面值、临时值就是右值。

例子1:

int a = 5;
  • a 可以通过 & 取地址,位于等号左边,所以a是左值
  • 5 位于等号右边,5没法通过 & 取地址,所以 5 是个右值

例子2:

struct A {
    A(int a = 0) {
        a_ = a;
    }
 
    int a_;
};
 
A a = A();
  • a可以通过&取地址,位于等号左边,所以a是左值。
  • A()是个临时值,没法通过&取地址,位于等号右边,所以A()是个右值。

2.左值引用和右值引用

引用本质是别名,可以通过引用修改变量的值传参时传引用可以避免拷贝,其实现原理和指针类似。

2.1 左值引用

左值引用是指能指向左值不能指向右值的就是左值引用。

例子:

int a = 5;
/* 左值引用指向左值,编译通过。
这边的a其实就是上面一行定义的变量,所以是左值,
然后这边引用的是a,所以是左值引用*/
int &ref_a = a;  
int &ref_a = 5; // 左值引用指向了右值,会编译失败

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

但是,const左值引用是可以指向右值的:

const int &ref_a = 5; // 编译通过

因为 const 左值引用不会修改指向值,因此可以指向右值,也就是为什么要使用const &作为函数参数的原因。

2.2 右值引用

右值引用的标志是&&可以指向右值,不能指向左值

int &&ref_a_right = 5;  // ok

int a = 5;
int &&ref_a_left = a;  // 编译不过,右值引用不可以指向左值

ref_a_right = 6;  // 右值引用的用途,可以修改右值。

2.3 左右值引用的本质

2.3.1 右值引用指向左值

使用 std::move

int a = 5;  // a 是个左值
int &ref_a_left = a;  // 左值引用指向左值
int &&ref_a_right = std::move(a);  // 通过std::move将左值转换为右值,可以被右值引用指向

cout << a;  // 打印结果:5

再如:

    int a = 5;
    int &ref_a_left = a;
    int &&ref_a_right = std::move(a);

    cout << "before change : " << endl;
    cout << "a = " << a << "  ref_a_left = " << ref_a_left << "  ref_a_right = " << ref_a_right << endl;
    a = 6;
    ref_a_left = 7;
    ref_a_right = 8;

    cout << "after change : " << endl;
    cout << "a = " << a << "  ref_a_left = " << ref_a_left << "  ref_a_right = " << ref_a_right << endl;

// 输出结果为 :
// before change : 
// a = 5  ref_a_left = 5  ref_a_right = 5
// after change : 
// a = 8  ref_a_left = 8  ref_a_right = 8

从上面程序可以看出,无论是使用左值引用还是使用std::move()右值引用,都是可以更改原变量的值。

std::move是一个非常有迷惑性的函数,但事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。
右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值:

int &&ref_a = 5;
ref_a = 6;
// 等同于:
int temp = 5;
int &&ref_a = std::move(temp);
ref_a = 6;

2.3.2 左值引用、右值引用本身是左值还是右值?

被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。

// 形参是个右值引用
void change(int&& right_value) {
    right_value = 8;
}
 
int main() {
    int a = 5; // a是个左值
    int &ref_a_left = a; // ref_a_left是个左值引用
    int &&ref_a_right = std::move(a); // ref_a_right是个右值引用
 
    change(a); // 编译不过,a是左值,change参数要求右值
    change(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值
    change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值
     
    change(std::move(a)); // 编译通过
    change(std::move(ref_a_right)); // 编译通过
    change(std::move(ref_a_left)); // 编译通过
 
    change(5); // 当然可以直接接右值,编译通过
     
    cout << &a << ' ';
    cout << &ref_a_left << ' ';
    cout << &ref_a_right;
    // 打印这三个左值的地址,都是一样的
}

std::move会返回一个右值引用int &&,它是左值还是右值呢?
从表达式int &&ref = std::move(a)来看,右值引用ref指向的必须是右值,所以move返回的int &&是个右值。
右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值。

或者说:作为函数返回值的 && 是右值,直接声明出来的 && 是左值。这句话怎么理解呢?
我的理解是返回的时候是参数本身,没有地址,所以是右值,直接声明出来的是有地址的,所以是左值。

总结:

  1. 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
  2. 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
  3. 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
void f(const int& n)
{
  n+=1;  // 编译失败,const左值引用不能修改指向变量
}

void f2(int && n)
{
  n +=1;  // 可以编译
  cout << "n = : " << n << endl; // 通过右值引用,然后对传入的值进行修改
}

int main()
{
  f(5);
  f2(5);
}

3.右值引用和std::move的应用场景

3.0.0 深拷贝/浅拷贝

来源于:
https://www.cnblogs.com/hellowooorld/p/11259560.html
一、区别:

  1. 在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的;但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象,所以,此时,必须采用深拷贝
  2. 深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。

拷贝构造函数就是数据成员之间的简单赋值,如果你设计了一个类而没有提供它的复制构造函数,当用该类的一个对象去类另一个对象赋值时所执行的过程就是浅拷贝。

#include <iostream>

using namespace std;

class A
{
public:
    int data;

public:
    A(int _data) : data(_data) {}
    A() {}
};

int main(int argc, char const *argv[])
{

    A a(5), b = a; // b=a就是浅拷贝

    return 0;
}

这一句b = a;就是浅拷贝,执行完这句后b.data = 5;
如果对象中没有其他的资源(如:堆,文件,系统资源等),则深拷贝和浅拷贝没有什么区别,
但当对象中有这些资源时,例子:

class A 
{ 
    public: 
    A(int _size) : size(_size)
    {
        data = new int[size];
    } // 假如其中有一段动态分配的内存 
    A(){};
     ~A()
    {
        delete [] data;
    } // 析构时释放资源
    private: 
    int* data;
    int size; 
}
int main() 
{ 
    A a(5), b = a; // 注意这一句 
}

这里的b = a会造成未定义行为,因为类A中的复制构造函数是编译器生成的,所以b = a执行的是一个浅拷贝过程。我说过浅拷贝是对象数据之间的简单赋值,比如:
b.size = a.size;
b.data = a.data; // Oops!
这里b的指针data和a的指针指向了堆上的同一块内存,a和b析构时,b先把其data指向的动态分配的内存释放了一次,而后a析构时又将这块已经被释放过的内存再释放一次。对同一块动态内存执行2次以上释放的结果是未定义的,所以这将导致内存泄露或程序崩溃。
所以这里就需要深拷贝来解决这个问题,深拷贝指的就是当拷贝对象中有对其他资源(如堆、文件、系统等)的引用时(引用可以是指针或引用)时,对象的另开辟一块新的资源,而不再对拷贝对象中有对其他资源的引用的指针或引用进行单纯的赋值。如:

class A 
{ 
    public: 
    A(int _size) : size(_size)
    {
        data = new int[size];
    } // 假如其中有一段动态分配的内存 
    A(){};
    A(const A& _A) : size(_A.size)
    {
        data = new int[size];
    } // 深拷贝 
    ~A()
    {
        delete [] data;
    } // 析构时释放资源
    private: 
    int* data; 
     int size;
 }
int main() 
{ 
    A a(5), b = a; // 这次就没问题了 
}

总结:深拷贝和浅拷贝的区别是在对象状态中包含其它对象的引用的时候,当拷贝一个对象时,如果需要拷贝这个对象引用的对象,则是深拷贝,否则是浅拷贝。

3.0.1 c++拷贝构造函数

https://www.runoob.com/cplusplus/cpp-copy-constructor.html
拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。
通常用于:

  • 通过使用另一个同类型的对象来初始化新创建的对象
  • 复制对象把它作为参数传递给函数
  • 复制对象,并从函数返回这个对象

如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。拷贝构造函数的最常见形式如下,其中obj是一个对象引用,该对象是用于初始化另一个对象的:

classname(const classname &obj)
{
  // 构造函数的主体
}
点击查看代码
#include <iostream>

using namespace std;

#include <iostream>

using namespace std;

class Line
{
public:
    int getLength(void);
    Line(int len);         // 简单的构造函数
    Line(const Line &obj); // 拷贝构造函数
    ~Line();               // 析构函数

private:
    int *ptr;
};

// 成员函数定义,包括构造函数
Line::Line(int len)
{
    cout << "调用构造函数" << endl;
    // 为指针分配内存
    ptr = new int;
    *ptr = len; // 这边 *ptr = len 可以理解为往ptr指针所指的地方存数字10
}

Line::Line(const Line &obj)
{
    cout << "调用拷贝构造函数并为指针 ptr 分配内存" << endl;
    ptr = new int;
    *ptr = *obj.ptr; // 拷贝值
}

Line::~Line(void)
{
    cout << "释放内存" << endl;
    delete ptr;
}
int Line::getLength(void)
{
    return *ptr;
}

void display(Line obj)
{
    cout << "line 大小 : " << obj.getLength() << endl; // 这边可以理解为把 obj对象的指针 ptr所指的内存中的数拿出来
}

// 程序的主函数
int main()
{
    Line line(10);

    display(line);

    return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
点击查看代码
调用构造函数
调用拷贝构造函数并为指针 ptr 分配内存
line 大小 : 10
释放内存
释放内存

下面的实例对上面的实例稍作修改,通过使用已有的同类型的对象来初始化新创建的对象:

点击查看代码
#include <iostream>

using namespace std;

#include <iostream>

using namespace std;

class Line
{
public:
    int getLength(void);
    Line(int len);         // 简单的构造函数
    Line(const Line &obj); // 拷贝构造函数
    ~Line();               // 析构函数

private:
    int *ptr;
};

// 成员函数定义,包括构造函数
Line::Line(int len)
{
    cout << "调用构造函数" << endl;
    // 为指针分配内存
    ptr = new int;
    *ptr = len; // 这边 *ptr = len 可以理解为往ptr指针所指的地方存数字10
}

Line::Line(const Line &obj)
{
    cout << "调用拷贝构造函数并为指针 ptr 分配内存" << endl;
    ptr = new int;
    *ptr = *obj.ptr; // 拷贝值
}

Line::~Line(void)
{
    cout << "释放内存" << endl;
    delete ptr;
}
int Line::getLength(void)
{
    return *ptr;
}

void display(Line obj)
{
    cout << "line 大小 : " << obj.getLength() << endl; // 这边可以理解为把 obj对象的指针 ptr所指的内存中的数拿出来
}

// 程序的主函数
int main()
{
    Line line1(10);
    /*  输出
        调用构造函数
        释放内存
    */
    Line line2 = line1; // 这里也调用了拷贝构造函数
    /*  同时运行以上两条输出:
        调用构造函数
        调用拷贝构造函数并为指针 ptr 分配内存
        释放内存
        释放内存
    */

    display(line1);
    /*  运行以上三条输出:
        调用构造函数
        调用拷贝构造函数并为指针 ptr 分配内存
        调用拷贝构造函数并为指针 ptr 分配内存
        line 大小 : 10
        释放内存
        释放内存
        释放内存
    */

    display(line2);
    /*  运行以上三条输出:
        调用构造函数
        调用拷贝构造函数并为指针 ptr 分配内存
        调用拷贝构造函数并为指针 ptr 分配内存
        line 大小 : 10
        释放内存
        调用拷贝构造函数并为指针 ptr 分配内存
        line 大小 : 10
        释放内存
        释放内存
        释放内存
    */


   /*   总结:
        也就是说,运行 Line line2 = line1;时
        就只调用了拷贝构造函数
        等到全部运行结束之后,挨个运行析构

        运行 display(line1); 时
        看函数的定义:void display(Line obj)
        首先这个函数自己会先调用一个拷贝构造函数
        然后执行该函数里面的东西,
        等到该函数运行结束后,函数形参中的拷贝构造对象自己会析构
   */

    return 0;
}

3.1实现移动语义

在实际场景中,右值引用和std::move被广泛用于在STL和自定义类中实现移动语义,避免拷贝,从而提升程序性能
在没有右值引用之前,一个简单的数组类通常实现如下:拷贝函数拷贝构造函数赋值运算符重载析构函数等。

在STL的很多容器中,都实现了以右值引用为参数移动构造函数移动赋值重载函数,或者其他函数,最常见的如:std::vectorpush_backemplace_back
参数为左值引用意味着拷贝,为右值引用意味着移动。

class Array {
public:
    ......
 
    // 优雅
    Array(Array&& temp_array) {
        data_ = temp_array.data_;
        size_ = temp_array.size_;
        // 为防止temp_array析构时delete data,提前置空其data_      
        temp_array.data_ = nullptr;
    }
     
 
public:
    int *data_;
    int size_;
};

调用:

// 例1:Array用法
int main(){
    Array a;
 
    // 做一些操作
    .....
     
    // 左值a,用std::move转化为右值
    Array b(std::move(a));
}

3.2 vector::push_back使用std::move提高性能

#include <iostream>
#include <vector>

using namespace std;

int main(int argc, char *argv[])
{

    string str1 = "asdasda";
    std::vector<std::string> vec;

    vec.push_back(str1); // 传统方法,copy
    cout << "-----------------------" << endl;
    cout << "str1 = " << str1 << endl;
    for (auto it : vec)
        cout << it << "  ";
    cout << endl;

    cout << "-----------------------" << endl;
    vec.push_back(std::move(str1)); // 调用移动语义的push_back方法,这边使用move之后,原值就没有了
    cout << "str1 = " << str1 << endl;
    for (auto it : vec)
        cout << it << "  ";
    cout << endl;
    cout << "-----------------------" << endl;
    string str2 = "1231231";
    vec.emplace_back(std::move(str2)); // 使用emplace_back效果与 push_back 效果一样,原值也会没有
    cout << "str2 = " << str2 << endl;
    for (auto it : vec)
        cout << it << "  ";
    cout << endl;

    cout << "-----------------------" << endl;
    vec.emplace_back("adqwdqwqw"); // 也可以直接接右值
    for (auto it : vec)
        cout << it << "  ";
    cout << endl;
    return 0;
}

vectorstring这个场景,加个std::move会调用到移动语义函数,避免了深拷贝。
除非设计不允许移动,STL类大都支持移动语义函数,即可移动的。 另外,编译器会默认在用户自定义的class和struct中生成移动语义函数,但前提是用户没有主动定义该类的拷贝构造等函数(具体规则自行百度哈)。 因此,可移动对象在<需要拷贝且被拷贝者之后不再被需要>的场景,建议使用std::move触发移动语义,提升性能。

moveable_objecta = moveable_objectb; 
改为: 
moveable_objecta = std::move(moveable_objectb);

还有些STL类是move-only的,比如unique_ptr,这种类只有移动构造函数,因此只能移动(转移内部对象所有权,或者叫浅拷贝),不能拷贝(深拷贝):

std::unique_ptr<A> ptr_a = std::make_unique<A>();

std::unique_ptr<A> ptr_b = std::move(ptr_a); // unique_ptr只有‘移动赋值重载函数‘,参数是&& ,只能接右值,因此必须用std::move转换类型

std::unique_ptr<A> ptr_b = ptr_a; // 编译不通过

std::move本身只做类型转换,对性能无影响。 我们可以在自己的类中实现移动语义,避免深拷贝,充分利用右值引用和std::move的语言特性。

4. 完美转发 std::forward

std::move一样,std::forward也是做类型转换。
move相比,forward更强大,move只能转出来右值,forward都可以。
std::forward(u)有两个参数:T与 u。 a. 当T为左值引用类型时,u将被转换为T类型的左值; b. 否则u将被转换为T类型右值。

#include <iostream>

using namespace std;

void B(int &&ref_r)
{
    ref_r += 1;
    cout << ref_r << endl;
}

void C(int &ref)
{
    cout << ref + 1 << endl;
}

// A,B的入参都是右值引用
// 有名字的右值引用是左值,因此ref_r 是左值
void A(int &&ref_r)
{
    // B(ref_r); // 错误,B的入口参数是右值引用,需要接右值,ref_r 是左值,编译失败
    B(std::move(ref_r)); // std::move把左值转换为右值,编译通过
    B(std::forward<int>(ref_r));    // std::forward的T是int类型,属于条件b,因此把ref_r转为右值
}

int main(int argc, char *argv[])
{

    int a = 5;
    cout << "a = " << a << endl;

    B(std::move(a));
    cout << "a = " << a << endl;

    B(std::forward<int>(a));        // 条件b,作为右值
    C(std::forward<int&>(a));   // 条件a,因为T是引用类型,所以作为左值

    return 0;
}

例2

void change2(int&& ref_r) {
    ref_r = 1;
}
 
void change3(int& ref_l) {
    ref_l = 1;
}
 
// change的入参是右值引用
// 有名字的右值引用是 左值,因此ref_r是左值
void change(int&& ref_r) {
    change2(ref_r);  // 错误,change2的入参是右值引用,需要接右值,ref_r是左值,编译失败
     
    change2(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
    change2(std::forward<int &&>(ref_r));  // ok,std::forward的T是右值引用类型(int &&),符合条件b,因此u(ref_r)会被转换为右值,编译通过
     
    change3(ref_r); // ok,change3的入参是左值引用,需要接左值,ref_r是左值,编译通过
    change3(std::forward<int &>(ref_r)); // ok,std::forward的T是左值引用类型(int &),符合条件a,因此u(ref_r)会被转换为左值,编译通过
    // 可见,forward可以把值转换为左值或者右值
}
 
int main() {
    int a = 5;
    change(std::move(a));
}

标签:std,右值,int,左值,C++,引用,ref
From: https://www.cnblogs.com/Balcher/p/16839001.html

相关文章

  • C++哈夫曼树
    C++哈夫曼树【讨论问题3】二叉树的应用—哈夫曼树[问题描述]在数据通信系统中,电文传送是经常遇到的问题,传送电文时需要将字符转换成二进制组成的字符串,当然在传送电文......
  • c++左值、右值、右值引用
    c++左值、右值、右值引用前言这一部分对于规范代码、提高安全性、加速调试等方方面面都很重要、、问就是天天在引用和const上报红;出现诸如''表达式必须是lvalue或xval......
  • C++模板的偏特化与全特化
    全特化的目的:当为特殊类型时,需要特殊处理。偏特化的目的:固定几个类型,其他类型不确定。函数模板是不允许偏特化的,但函数允许重载,从而声明另一个函数模板即可替代偏特化的需......
  • 第三十一章 使用 CSP 进行基于标签的开发 - 转义和引用HTTP输出
    第三十一章使用CSP进行基于标签的开发-转义和引用HTTP输出转义和引用HTTP输出要创建HTML中使用的特殊字符的文字显示,必须使用转义序列。例如,要在HTML中显示>(右尖......
  • 实验3 数组、指针与现代C++标准库
    task1代码:1#include<iostream>23usingstd::cout;4usingstd::endl;56//绫籄鐨勫畾涔?7classA{8public:9A(intx0,inty0):x{x0}......
  • c++,真有趣啊
    由于笔者的水平不太行,在这个贴里记录一些自己犯过的不太容易被发现的错误20221029基类classCBase{public:virtual~CBase(){}private:virtualbool__......
  • C++11 unistring 类(编码转换)
    C++11 的编码转换程序: #ifndefUNISTRING_HPP#defineUNISTRING_HPP#include<algorithm>#include<codecvt>#include<cstdio>#include<cstdarg>#include<i......
  • C++ Primer Plus学习笔记之复合类型(上)
    前言个人觉得学习编程最有效的方法是阅读专业的书籍,通过阅读专业书籍可以构建更加系统化的知识体系。一直以来都很想深入学习一下C++,将其作为自己的主力开发语言。现在为......
  • C++求高精度pi(1)BBP公式
    C++求高精度pi(1)前言(之后再写)BBP公式由arctan1展开得到的莱布尼茨级数是一个交错级数,并且条件收敛而不绝对收敛,这注定了莱布尼兹级数方法会非常低效而BBP公式$$\sum......
  • C++ STL
    概述STL主要有container,algorithm和iterator三大部分构成容器用于存放数据对象算法用于操作容器中的数据对象迭代器是算法和容器之间的中介STL容器STL容器是一种数据结构......