栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

c++左值、右值引用和移动语义

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

c++左值、右值引用和移动语义

c++左值、右值引用和移动语义
  • 一、左值和右值
  • 二、左值引用&
  • 三、右值引用&&
    • 应用:右值引用优化性能,避免深拷贝
  • 四、移动语义 move
  • 五、完美转发forward

参考链接

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

一、左值和右值

左值是表达式结束后仍然存在的持久对象,右值是指表达式结束时就不存在的临时对象。
区分表达式的左右值属性:如果可对表达式用&符取址,则为左值,否则为右值

比如:

int a=10;
int b=a;

对于a=10,由于10是临时的,表达式结束这个值就不存在了,因此是右值。而a=10中,a作为接受的变量,是表达式结束后仍然存在的对象,因此是左值。
对于b=a,由于a、b都是持久存在的,因此都是左值。

二、左值引用&

&只能用作左值引用,因此对于下面的test(int& x),中的参数x一定是左值
对于一个常数10,它是右值,因此没法调用test(10);

void test(int& x){//这个x一定是左值
    cout<
    int a=10;
    //test(10); //传入的是右值,因此会报错
    test(a); //传入的是左值
}
三、右值引用&&

右值引用就是对一个右值进行引用的类型。因为右值没有名字,所以我们只能通过引用的方式找到它。
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所把绑定对象的内
存,只是该对象的一个别名。

通过右值引用的声明,该右值又“重获新生”,其生命周期其生命周期与右值引用类型变量的生命周期一
样,只要该变量还活着,该右值临时量将会一直存活下去。

如下面代码,10作为右值,正常情况下,在使用完后该临时变量就应该消失了。但是int&& x相当于给这个10取了别名为x,也叫右值引用。它的好处是避免了拷贝。

void test(int&& x){
    cout<
    int a=10;
    test(10); 
}

&&总结:

  • 左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值(也就是说对于&,只能用作左值引用,而&&既可以作为左值引用也可以作为右值引用,取决于传入的参数是左值还是右值)
  • auto&& 或函数参数类型自动推导的 T&& 是一个未定的引用类型,被称为 universal references,
    它可能是左值引用也可能是右值引用类型,取决于初始化的值类型。
  • 所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引 用。当 T&& 为
    模板参数时,输入左值,它会变成左值引用,而输入右值时则变为具名的右 值引用
  • 编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值
应用:右值引用优化性能,避免深拷贝

A(const A& a)是拷贝构造函数,如果没有移动构造函数A(A&& a)的情况下,执行 A a = Get(false),调用的是拷贝构造函数,它会将Get(flase)返回的结果拷贝给a,然后执行析构函数,但是这样会造成性能的损耗。
因此可以使用移动构造函数A(A&& a),由于Get(false)得到的是一个右值,因此会调用移动构造函数,只需要将需要构造的对象的指针拷贝就行了,将被移动的对象的指针置为空,就完成了移动拷贝构造。(避免了深拷贝)

移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少
不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护
(创建和销毁)对性能的影响。

#include 

using namespace std;

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;
        cout << "move  constructor A"  << endl;
    }

    ~A(){
        cout << "destructor A, m_ptr:" << m_ptr  << endl;
        if(m_ptr)
            delete m_ptr;
    }

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;
}

四、移动语义 move

我们知道移动语义是通过右值引用来匹配临时值的,那么,普通的左值是否也能借组移动语义来优化性
能呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语
义。move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝

int main()
{
	MyString a;
	a = MyString("Hello"); // move
	MyString b = a; // copy
	MyString c = std::move(a); // move, 将左值转为右值
	return 0;
}
五、完美转发forward

forward 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左
值,若是右值,则传递之后仍然是右值。

对于一个函数

Template
void func(T &&val);

根据前面所描述的,这种引用类型既可以对左值引用,亦可以对右值引用。
但要注意,引用以后,这个val值它本质上是一个左值!

看下面例子

int &&a = 10;
int &&b = a; //错误

注意这里,a是一个右值引用,但其本身a也有内存名字,所以a本身是一个左值,再用右值引用引用a这
是不对的
因此我们有了std::forward()完美转发,这种T &&val中的val是左值,但如果我们用std::forward (val),
就会按照参数原来的类型转发;

int &&a = 10;
int &&b = std::forward(a);

例子:

#include 

using namespace std;

template 
void Print(T &t)
{
    cout << "L" << t << endl;
}
template 
void Print(T &&t)
{
    cout << "R" << t << endl;
}
template 
void func(T &&t) // 左值右值都只能走这个
{
    cout << "func(T &&t)" << endl;
    Print(t);  // L   (什么都不做,默认当作左值)
    Print(std::move(t));       // 肯定调用"R"
    Print(std::forward(t)); // 不确定L  R  ,如果要完美往下层函数转发加forward
}


int main()
{
    cout << "-- func(1)" << endl;
    func(1);  //  1是R
    int x = 10;
    int y = 20;
    cout << "-- func(x)" << endl;
    func(x);  // x本身是左值     (因此后续调用Print(std::forward(t))的时候,传入的依然是左值)
    cout << "-- func(std::forward(y))" << endl;
    func(std::forward(y)); // std::forward(y)变成右值  (如果传入的参数,本身不带有&&符号,那么forward就把它当作右值)

    return 0;
}

可以看到结果,分别是3个fun调用的输出。

对于func(1),1是右值。func(T &&t)因此这边t的语言应该是右值,但是由于编译器会将已命名的右值看作左值,因此Print(t)中传入的是左值,Print(std::move(t))这里肯定是调用右值,因为显示地转为右值了。Print(std::forward(t)),传入的值是根据&&类型的值,原来的类型决定,因为传入&&前,是右值,因此Print(std::forward(t))在forward调用后是右值。

其他3种func情况也是类似的理解。
另外对于 func(std::forward(y));由于y本身并不带有&&符号,因此forward后,默认是变成右值的。(什么是带&&符号?就是func(T &&t)这个函数的参数带了&&)

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/935304.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号