c++ 左值、右值;左值引用、右值引用、对象移动

1. 左值、右值

  C++中所有的值都必然属于左值、右值二者之一。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值

  看见书上又将右值分为将亡值和纯右值。纯右值就是c++98标准中右值的概念,如非引用返回的函数返回的临时变量值;一些运算表达式,如1+2产生的临时变量;不跟对象关联的字面量值,如2,'c',true,"hello";这些值都不能够被取地址。

而将亡值则是c++11新增的和右值引用相关的表达式,这样的表达式通常时将要移动的对象、T&&函数返回值、std::move()函数的返回值等,

不懂将亡值和纯右值的区别其实没关系,统一看作右值即可,不影响使用。

示例:

int i=0;// i是左值, 0是右值

class A {
  public:
    int a;
};
A getTemp()
{
    return A();
}
A a = getTemp();   // a是左值  getTemp()的返回值是右值(临时变量)

2. 左值引用、右值引用

  c++98中的引用很常见了,就是给变量取了个别名,在c++11中,因为增加了右值引用(rvalue reference)的概念,所以c++98中的引用都称为了左值引用(lvalue reference)

  为了支持移动操作,新标准引入了一种新的引用类型——右值引用(rvalue reference)。所谓右值引用就是必须绑定到右值的引用。我们通过& &而不是&来获得右值引用。如我们将要看到的,右值引用有一个重要的性质——只能绑定到一个将要销毁的对象。因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。

int a = 10; 
int& refA = a; // refA是a的别名, 修改refA就是修改a, a是左值,左移是左值引用

int& b = 1; //编译错误! 1是右值,不能够使用左值引用

int&& a = 1; //实质上就是将不具名(匿名)变量取了个别名
int b = 1;
int && c = b; //编译错误! 不能将一个左值复制给一个右值引用
class A {
  public:
    int a;
};
A getTemp()
{
    return A();
}
A && a = getTemp();   //getTemp()的返回值是右值(临时变量)

  getTemp()返回的右值本来在表达式语句结束后,其生命也就该终结了(因为是临时变量),而通过右值引用,该右值又重获新生,其生命期将与右值引用类型变量a的生命期一样,只要a还活着,该右值临时变量将会一直存活下去。实际上就是给那个临时变量取了个名字。

  注意:这里a类型是右值引用类型(int &&),但是如果从左值和右值的角度区分它,它实际上是个左值。因为可以对它取地址,而且它还有名字,是一个已经命名的右值。

  所以,左值引用只能绑定左值,右值引用只能绑定右值,如果绑定的不对,编译就会失败。但是,常量左值引用却是个奇葩,它可以算是一个“万能”的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。

const int & a = 1; //常量左值引用绑定 右值, 不会报错

class A {
  public:
    int a;
};
A getTemp()
{
    return A();
}
const A & a = getTemp();   //不会报错 而 A& a 会报错

总结一下,其中T是一个具体类型:

  1. 左值引用, 使用 T&, 只能绑定左值
  2. 右值引用, 使用 T&&, 只能绑定右值
  3. 常量左值, 使用 const T&, 既可以绑定左值又可以绑定右值
  4. 已命名的右值引用,编译器会认为是个左值
  5. 编译器有返回值优化,但不要过于依赖

3. 移动构造函数和移动赋值运算符

  类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用。不同于拷贝构造函数的是,这个引用参数在移动构造函数中是一个右值引用。与拷贝构造函数一样,任何额外的参数必须有默认实参。

  除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一个状态——销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向被移动的资源——这些资源的所有权已经归属新创建的对象。

  需要注意的是,不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept.
移动赋值运算符  

  移动赋值运算符执行与析构函数和移动构造函数相同的工作。与移动构造函数样,如果我们的移动赋值运算符不抛出任何异常,我们就应该将它标记为noexcept。类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值:

strVec &StrVec: :operator=(StrVec & &rhs) noexcept{
    //直接检测自赋值
    if (this != &rhs) {
        free ( ); //释放已有元素
        elements =rhs.elements; // 从rhs接管资源
        first_free = rhs.first_free;  
        cap = rhs.cap; 
// 将rhs置于可析构的状态 rhs.elements = rhs.first_free = rhs.cap = nullptr; } return *this; }

  在此例中,我们直接检查this 指针与rhs的地址是否相同。如果相同,右侧和左侧运算对象指向相同的对象,我们不需要做任何事情。否则,我们释放左侧运算对象所使用的内存,并接管给定对象的内存。与移动构造函数一样,我们将rhs中的指针置为nullptr。这里将rhs的指针置为nullptr是为了让移后源处于可析构的状态。

合成的移动操作

  与处理拷贝构造函数和拷贝赋值运算符一样,编译器也会合成移动构造函数和移动赋值运算符。但是,合成移动操作的条件与合成拷贝操作的条件大不相同。

  与拷贝操作不同,编译器根本不会为某些类合成移动操作。特别是,如果一个类定义自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造数和移动赋值运算符了。因此,某些类就没有移动构造函数或移动赋值运算符。如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝操作来代替移动操作(没有移动,则默认用拷贝替代)。

  将合成的移动操作定义为删除函数遵循以下原则:

  • 与拷贝构造函数不同,移动构造函数被定义为删除的函数的条件是:有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值运算符的情况类似。
  • 如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的则类的移动构造函数或移动赋值运算符被定义为删除的。
  • 类似拷贝构造函数,如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的。
  • 类似拷贝赋值运算符,如果有类成员是const的或是引用,则类的移动赋值运算符被定义为删除的。

移动右值,拷贝左值

  如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数。

StrVec v1, v2;
v1 = v2; // v2是左值;使用拷贝赋值
strVec getVec (istream &); // getvec返回一个右值
v2 = getVec (cin) ; //getVec (cin)是一个右值;使用移动赋值

  在第一个赋值中,我们将v2传递给赋值运算符。v2的类型是strVec,表达式v2是个左值。因此移动版本的赋值运算符是不可行的,因为我们不能隐式地将一个右值引用绑定到一个左值。因此,这个赋值语句使用拷贝赋值运算符

  在第二个赋值中,我们赋予v2的是getvec调用的结果。此表达式是一个右值。在此情况下,两个赋值运算符都是可行的——将getvec的结果绑定到两个运算符的参数都是允许的。调用拷贝赋值运算符需要进行一次到const的转换,而strvec&&则是精确匹配。因此,第二个赋值会使用移动赋值运算符。

移动迭代器

  移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器。一般来说,一个迭代器的解引用运算符返回一个指向元素的左值。与其他迭代器不同,移动迭代器的解引用返回的是右值。

void Strvec: :reallocate()
{
    //分配大小两倍于当前规模的内存空间
    auto newcapacity = size() ? 2 * size( ) : 1;
    auto first = alloc.allocate (newcapacity);//移动元素
    auto last = uninitialized_copy(make_move_iterator(begin()),
                         make_move_iterator(end()) ,first);
    free(); //释放旧空间
    elements = first; //更新指针
    first_free = last;
}            

  值得注意的是,标准库不保证哪些算法适用移动迭代器,哪些不适用。由于移动一个对象可能销毁掉原对象,因此你只有在确信算法在为一个元素赋值或将其传递给一个用户定义的函数后不再访问它时,才能将移动迭代器传递给算法。

左、右值引用成员函数

  通过在参数列表后放置引用限定符来限制赋值运算的对象为左值还是右值。

class Foo 
{
public:
    Foo &operator= (const Foo&) & //只能向可修改的左值赋值
    // Foo的其他参数
};
  Foo &Foo: : operator= (const Foo &rhs)& //执行将rhs赋予本对象所需的工作
  return *this;
}

  引用限定符可以是&或&&,分别指出this可以指向一个左值或右值。类似const限定符,引用限定符只能用于(非static)成员函数,且必须同时出现在函数的声明和定义中。对于&限定的函数,我们只能将它用于左值:对于& &限定的函数,只能用于右值。

 

原文链接: https://www.cnblogs.com/z1141000271/p/13155875.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    c++ 左值、右值;左值引用、右值引用、对象移动

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/356637

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年3月2日 上午11:19
下一篇 2023年3月2日 上午11:20

相关推荐