C++左值右值和构造函数们

Posted 自动驾驶技术入门

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++左值右值和构造函数们相关的知识,希望对你有一定的参考价值。

1. 什么是左值(lvalues)、右值 (rvalues)

In C++ an lvalue is something that points to a specific memory location. On the other hand, a rvalue is something that doesn't point anywhere. In general, rvalues are temporary and short lived, while lvalues live a longer life since they exist as variables. It's also fun to think of lvalues as containers and rvalues as things contained in the containers. Without a container, they would expire.

  1. 左值(lvalue)表示一个占据内存中某个的位置(也就是一个地址)的对象,并能够获取这个对象的地址;
  2. 而右值(rvalues)可以用排除法定义,一个表达式不是左值就是右值。

下面是一些关于左值右值的例子,

int x = 666;   // ok
int* y = &x;   // ok
  1. 666是一个右值,它没有一个确切的内存地址。 x是一个变量,变量可以取地址,因此是一个左值;
  2. c++要求赋值运算符的左侧的运算数为左值( x为变量,左值);
  3. &取地址运算符要求它的参数是一个左值( x),返回值为一个右值(=运算符的右操作数);
  4. y为一个 int型的指针,是变量,左值。
  int y;
  666 = y;

代码无法编译通过,GCC报的错误和前面说法一致,赋值运算符的左操作数必须是左值。

error: lvalue required as left operand of assignment

同样,下面的代码也无法编译通过,&运算符需要参数为左值。

int* y = &666// error!
error: lvalue required as unary ‘&’ operand

关于函数返回值是左值右值?看下面的例子,

int setValue()
{
    return 6;
}
setValue() = 3// error!

很明显,setValue()函数返回临时的值6,因此返回值是一个右值,而右值是不能作为=的左操作数的。

int global = 100;

intsetGlobal()
{
    return global;    
}

// ... somewhere in main() ...

setGlobal() = 400// OK

在上述代码中,setGlobal()函数返回了全局变量global的引用,因此是左值,可以被用来赋值。因此,

  1. 函数返回值是值传递 (return by value)的,返回值就是个右值;
  2. 函数返回值是引用传递 (return by reference)的,返回值就是个左值。

2. 左值和右值的转换

首先从左值转为右值是非常直接的(隐式转换),例如,

int x = 1;
int y = 3;
int z = x + y;   // ok

c++要求+运算符的左右操作数为右值,上述xy都是左值,隐式转换为右值,生成一个右值作为=的右操作数。

那么,从右值转为左值可以吗?不可以。

3. 左值引用(lvalue references)

int y = 10;
int& yref = y;
yref++;        // y is now 11

上面的代码是c++最基本的引用的用法,定义了一个左值y,然后再定义了一个指向y的引用(别名,y的内存真实存在),因此也称为左值引用(lvalue references)。

int& yref = 10;  // will it work?

上述的代码无法编译通过,因为左侧是一个左值引用,而右侧是一个右值。

下面的代码也同样无法编译,和上面的原因相同,fnc()需要传入一个左值,但是10是右值。

void fnc(int& x)
{
}

int main()
{
    fnc(10);  // Nope!
    // This works instead:
    // int x = 10;
    // fnc(x);
}
error: cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int

上面的编译错误提示,非const的左值引用不能绑定到右值上。那么是不是const就可以了?确实可以!这是c++一个常用的技巧!

const int& ref = 10;  // OK!

void fnc(const int& x)
{
}

int main()
{
    fnc(10);  // OK!
}

以上的代码可以正确编译并运行,10是一个右值,是个随时可能消失的对象,所以对它的引用是没有意义的,因为不能通过左值引用对其进行修改。但是如果是const 引用就不一样了,const引用无法对值进行修改,这时候就避免了对10这个右值的修改。

我们在很多代码中都会看到形如const int& x这样的写法,函数的参数接收const的左值引用,它可以避免临时对象的不必要的拷贝和新构建。其内部的实现类似如下的代码,

// the following...
const int& ref = 10;

// ... would translate to:
int __internal_unique_name = 10;
const int& ref = __internal_unique_name;

编译器创建了一个隐藏的变量保存右值10,然后const左值引用到这个值。因此,接下来我们能对这个引用进行使用,但是不能修改,

const int& ref = 10;
std::cout << ref << "\n";   // OK!
std::cout << ++ref << "\n"; // error: increment of read-only reference ‘ref’

顺便提一下,c++的const真是有趣的很,

  int __internal_unique_name = 10;
  const int& ref = __internal_unique_name;
  __internal_unique_name = 11;
  std::cout<<__internal_unique_name<<std::endl;
  std::cout<<ref<<std::endl;
  ref = 12;//error: assignment of read-only reference ‘ref’

输出为,

11
11

ref为一个const引用,它是只读的,这点很重要,只有它一厢情愿的认为它自己所指向的是个常量,所以自己不能去修改值,否则编译无法通过。但是可以通过修改__internal_unique_name去修改。

4. 三法则(Rule of three)

说完了左值引用,接下来介绍一下c++的Rule of three,析构函数、拷贝构造函数、赋值运算符。

  • destructor;
  • copy constructor;
  • copy assignment operator.

首先明确一些非常简单的概念,

  1. c++的对象(class)在创建的时候都会调用构造函数;

  2. 析构函数是在对象内存释放的时候使用;

  3. 赋值运算符是在将一个对象赋值(=)给另外一个已经创建、已经存在的对象;

  4. 拷贝构造函数也是一种构造函数,那么它的功能就是创建一个新的对象实例。调用的是拷贝构造函数还是赋值运算符,主要是看是否有新的对象实例产生。如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符。

    调用拷贝构造函数主要有以下场景:

  • 对象作为函数的参数,以 值传递的方式传给函数;
  • 对象作为函数的返回值,以 值的方式从函数返回;
  • 使用一个对象给另一个对象 初始化。

如果你设计的类中,都是一些基础的数据类型,例如double、int等,这时候我们是可以不需要去考虑自己设计析构、拷贝构造、赋值运算符函数,用编译器默认提供的即可。

class Student{
 Student(int age,float score):age_(age),score_(score){
 }
private:
    int age_;
    float score_;
};

如果你设计的类中牵扯到内存的分配时,这时候你的类中就需要涉及到Rule of three。下面用一个例子,介绍上面的一些概念,注意在编译的时候需要关闭返回值优化Return Value Optimization (RVO),我用的cmake,

add_compile_options(-fno-elide-constructors)
#include <string>
#include <iostream>



class Holder
{

  public:

    Holder(int size)         // Constructor
    {
      m_data = new int[size];
      m_size = size;
      std::cout<<"[ptr "<<this<< "]"<< m_size <<" constructor"<<std::endl;
    }
    Holder(const Holder& other)
    {
        m_data = new int[other.m_size];  // (1)
        std::copy(other.m_data, other.m_data + other.m_size, m_data);  // (2)
        m_size = other.m_size;
        std::cout<<"[ptr "<<this<< "]"<<m_size <<" copy constructor"<<std::endl;
    }
    Holder& operator=(const Holder& other) 
    {
        
        if(this == &other) return *this;  // (1)
        delete[] m_data;  // (2)
        m_data = new int[other.m_size];
        std::copy(other.m_data, other.m_data + other.m_size, m_data);
        m_size = other.m_size;
        std::cout<<"[ptr "<<this<< "]"<<m_size <<" = operator"<<std::endl;
        return *this;  // (3)
    }
    ~Holder()                // Destructor
    {
      std::cout<<"[ptr "<<this<< "]"<<m_size <<" Destructor"<<std::endl;
      delete[] m_data;
    }

  private:

    int*   m_data;
    size_t m_size;
};

Holder createHolder(int size)
{
  return Holder(size);
}

int main()
{
  Holder h1(1000);                // regular constructor
  Holder h2(h1);                  // copy constructor (lvalue in input)
  Holder h3 = createHolder(2000); // copy constructor (rvalue in input) (1) 
  h2 = h3;                        // assignment operator (lvalue in input)
  return 0;
}

执行结果如下所示,

[ptr 0x7ffe4f96eb60]1000 constructor
[ptr 0x7ffe4f96eb70]1000 copy constructor
[ptr 0x7ffe4f96eb20]2000 constructor
[ptr 0x7ffe4f96eb90]2000 copy constructor
[ptr 0x7ffe4f96eb20]2000 Destructor
[ptr 0x7ffe4f96eb80]2000 copy constructor
[ptr 0x7ffe4f96eb90]2000 Destructor
[ptr 0x7ffe4f96eb70]2000 = operator
[ptr 0x7ffe4f96eb80]2000 Destructor
[ptr 0x7ffe4f96eb70]2000 Destructor
[ptr 0x7ffe4f96eb60]1000 Destructor
  1. h1调用的默认构造函数;
  2. h2也是新创建,利用h1去构造,因此调用拷贝构造函数;
  3. createHolder()中在栈上创建一个临时的 Holder(2000),因此先调用一次默认构造函数,因为函数是值传递(return by value),又会将刚才创建的临时对象拷贝一份返回,又调用一次拷贝构造函数;
  4. h3是新创建,利用 createHolder()返回的临时对象进行初始化,因此又调用一次拷贝构造函数;
  5. h2是已经建立的对象, h2 = h3;所以这里调用的是赋值运算符。

5. 右值引用(rvalue references)

到目前为止,Holder类可以正确运行,但不是很高效,例如下面的代码,createHolder函数中会调用两次拷贝构造函数,而h的创建又会调用一次拷贝构造函数,而在拷贝构造函数中会涉及到内存的复制std::copy,这些操作是非常重的。

Holder createHolder(int size)
{
  return Holder(size);
}
int main()
{
  Holder h = createHolder(1000);
}

在c++11以后,引入了右值引用(rvalue references)的概念,和左值引用不同的是,它使用了两个&符号,即&&。

  std::string   s1     = "Hello ";
  std::string   s2     = "world";
  std::string&& s_rref = s1 + s2;    // the result of s1 + s2 is an rvalue
  s_rref += ", my friend";           // I can change the temporary string!
  std::cout << s_rref << '\n';       // prints "Hello world, my friend"

如上面的代码所示,s1 + s2是右值,s_rref是一个右值引用。利用右值引用,我们可以修改右值的值了!

6. 五法则(Rule of five)

在c++11以后,由于右值引用的引入,新增加了move构造函数和move赋值运算符,所以从三法则变成了五法则。

我们先看move构造函数,

Holder(Holder&& other)     // <-- rvalue reference in input
{
  m_data = other.m_data;   // (1)
  m_size = other.m_size;
  other.m_data = nullptr;  // (2)
  other.m_size = 0;
}
  1. move构造函数的输入是一个右值引用(&&),右值引用我们可以修改它;
  2. 在(1)中首先把它的数据 偷出来,然后设置为nullptr。这里没有涉及深拷贝,就像move名字那样,我们仅仅移动了资源(将指针从右值中移动到了当前对象)。然后将临时的右值对象中的数据设置为了nullptr,因为右值对象可能马上就要析构,调用 delete[] m_data

接下来是move赋值运算符,

Holder& operator=(Holder&& other)     // <-- rvalue reference in input  
{  
  if (this == &other) return *this;

  delete[] m_data;         // (1)

  m_data = other.m_data;   // (2)
  m_size = other.m_size;

  other.m_data = nullptr;  // (3)
  other.m_size = 0;

  return *this;
}
  1. move赋值运算符,输入同样是一个右值引用(&&);
  2. 由于是赋值运算符,在赋值之前当前的对象已经创建,所以先要清到当前对象中的数据(2);
  3. 和move构造函数类似,将数据move过来,同时将右值中的数据状态等清空。

总结:

  • 当输入是左值时,调用的是拷贝构造函数和赋值运算符;
  • 当输入是右值时,调用的是move构造函数和move赋值运算符。

7. 一个完整的例子

下面是一个完整的例子,

#include <iostream>



class Holder
{

  public:

    Holder(int size)         // Constructor
    {
      m_data = new int[size];
      m_size = size;
      std::cout<<"[ptr "<<this<< "]"<< m_size <<" constructor"<<std::endl;
    }
    Holder(const Holder& other)
    {
        m_data = new int[other.m_size];  // (1)
        std::copy(other.m_data, other.m_data + other.m_size, m_data);  // (2)
        m_size = other.m_size;
        std::cout<<"[ptr "<<this<< "]"<<m_size <<" copy constructor"<<std::endl;
    }
    Holder& operator=(const Holder& other) 
    {
        
        if(this == &other) return *this;  // (1)
        delete[] m_data;  // (2)
        m_data = new int[other.m_size];
        std::copy(other.m_data, other.m_data + other.m_size, m_data);
        m_size = other.m_size;
        std::cout<<"[ptr "<<this<< "]"<<m_size <<" = operator"<<std::endl;
        return *this;  // (3)
    }

    Holder(Holder&& other)     // <-- rvalue reference in input
    {
        m_data = other.m_data;   // (1)
        m_size = other.m_size;
        other.m_data = nullptr;  // (2)
        other.m_size = 0;
        std::cout<<"[ptr "<<this<< "]"<<m_size <<" move constructor"<<std::endl;
    }
    Holder& operator=(Holder&& other)     // <-- rvalue reference in input  
    {  
        if (this == &other) return *this;

        delete[] m_data;         // (1)

        m_data = other.m_data;   // (2)
        m_size = other.m_size;

        other.m_data = nullptr;  // (3)
        other.m_size = 0;

        return *this;
        std::cout<<"[ptr "<<this<< "]"<<m_size <<" move operator="<<std::endl;
    }
    ~Holder()                // Destructor
    {
      std::cout<<"[ptr "<<this<< "]"<<m_size <<" Destructor"<<std::endl;
      delete[] m_data;
    }

  private:

    int*   m_data;
    size_t m_size;
};

Holder createHolder(int size)
{
  return Holder(size);
}

int main()
{
  Holder h1(1000);                // regular constructor
  Holder h2(h1);                  // copy constructor (lvalue in input)
  Holder h3 = createHolder(2000); // move constructor (rvalue in input) (1) 

  h2 = h3;                        // assignment operator (lvalue in input)
  h2 = createHolder(500);         // move assignment operator (rvalue in input)
  return 0;
}

程序的输出为,

[ptr 0x7fff461e2a10]1000 constructor
[ptr 0x7fff461e2a20]1000 copy constructor
[ptr 0x7fff461e29d0]2000 constructor
[ptr 0x7fff461e2a40]2000 move constructor
[ptr 0x7fff461e29d0]0 Destructor
[ptr 0x7fff461e2a30]2000 move constructor
[ptr 0x7fff461e2a40]0 Destructor
[ptr 0x7fff461e2a20]2000 = operator
[ptr 0x7fff461e29d0]500 constructor
[ptr 0x7fff461e2a40]500 move constructor
[ptr 0x7fff461e29d0]0 Destructor
[ptr 0x7fff461e2a40]0 Destructor
[ptr 0x7fff461e2a30]2000 Destructor
[ptr 0x7fff461e2a20]500 Destructor
[ptr 0x7fff461e2a10]1000 Destructor
  1. h1是普通的构造函数;
  2. h2的构造,输入h1是左值,调用的拷贝构造函数;
  3. h3,先在栈上创建一个临时对象调用普通构造函数,然后返回调用move构造函数。由于 createHolder(2000)函数返回的是右值,所以这时候h3构造调用的是move构造函数;
  4. h2是已经创建的对象,h3是左值,这时候调用赋值运算符;
  5. h2是已经创建的对象, createHolder(2000)函数返回的是右值,所以调用的是move构造函数。

8. std::move

In particular, std::move produces an xvalue expression that identifies its argument t. It is exactly equivalent to a static_cast to an rvalue reference type.

标准库中的std::move()可以创造一个右值引用的对象。


int main()
{
  Holder h1(1000);     // h1 is an lvalue
  Holder h2(h1);       // copy-constructor invoked (because of lvalue in input)
}

上述h1是左值,因此会调用复制构造函数,输出如下所示,

[ptr 0x7ffdf53c7d10]1000 constructor
[ptr 0x7ffdf53c7d20]1000 copy constructor
[ptr 0x7ffdf53c7d20]1000 Destructor
[ptr 0x7ffdf53c7d10]1000 Destructor
int main()
{
  Holder h1(1000);           // h1 is an lvalue
  Holder h2(std::move(h1));  // move-constructor invoked (because of rvalue in input)
}

h1被move后变成了右值引用,因此调用move构造函数,输出如下所示,

[ptr 0x7ffc41bc03b0]1000 constructor
[ptr 0x7ffc41bc03c0]1000 move constructor
[ptr 0x7ffc41bc03c0]1000 Destructor
[ptr 0x7ffc41bc03b0]0 Destructor

9. c++11后标准库中关于右值引用的变化

在c++11以后,标准库中容器等都增加了右值引用接口,类似于vector

void push_back( T&& value );(since C++11)(until C++20)

可以通过下面的例子测试,

int main()
{
  Holder h1(1000);                // regular constructor
  Holder h2(2000);                  // regular constructor

  std::vector<Holder> h_box;
  h_box.push_back(h1); //( T& value )
  h_box.push_back(std::move(h2));//( T&& value )
  return 0;
}

输出结果如下,

[ptr 0x7fff9fe54400]1000 constructor
[ptr 0x7fff9fe54410]2000 constructor
[ptr 0x55693716f180]1000 copy constructor
[ptr 0x556937170160]2000 move constructor
[ptr 0x556937170150]1000 copy constructor
[ptr 0x55693716f180]1000 Destructor
[ptr 0x556937170150]1000 Destructor
[ptr 0x556937170160]2000 Destructor
[ptr 0x7fff9fe54410]0 Destructor
[ptr 0x7fff9fe54400]1000 Destructor

从上面可以看出来,h2通过std::move转为右值引用后,重载的是push_back( T&& value )接口,往vecotr中添加时比h1少了一次拷贝。


以上是关于C++左值右值和构造函数们的主要内容,如果未能解决你的问题,请参考以下文章

C++内功修炼干货,进大厂必须会的C++左值与右值,最适合小白看的文章!

c++中的左值和右值,右值引用到底是啥?关于引用这一节看得很迷糊。

C++:神一样的左值

C语言 啥叫做左值?右值?

左值左值引用右值右值引用

什么是C++引用形参?