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.
-
左值(lvalue)表示一个占据内存中某个的位置(也就是一个地址)的对象,并能够获取这个对象的地址; -
而右值(rvalues)可以用排除法定义,一个表达式不是左值就是右值。
下面是一些关于左值右值的例子,
int x = 666; // ok
int* y = &x; // ok
-
666是一个右值,它没有一个确切的内存地址。 x
是一个变量,变量可以取地址,因此是一个左值; -
c++要求赋值运算符的左侧的运算数为左值( x
为变量,左值); -
&
取地址运算符要求它的参数是一个左值(x
),返回值为一个右值(=运算符的右操作数); -
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;
int& setGlobal()
{
return global;
}
// ... somewhere in main() ...
setGlobal() = 400; // OK
在上述代码中,setGlobal()
函数返回了全局变量global
的引用,因此是左值,可以被用来赋值。因此,
-
函数返回值是值传递 (return by value)
的,返回值就是个右值; -
函数返回值是引用传递 (return by reference)
的,返回值就是个左值。
2. 左值和右值的转换
首先从左值转为右值是非常直接的(隐式转换),例如,
int x = 1;
int y = 3;
int z = x + y; // ok
c++要求+
运算符的左右操作数为右值,上述x
和y
都是左值,隐式转换为右值,生成一个右值作为=
的右操作数。
那么,从右值转为左值可以吗?不可以。
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.
首先明确一些非常简单的概念,
-
c++的对象(class)在创建的时候都会调用构造函数;
-
析构函数是在对象内存释放的时候使用;
-
赋值运算符是在将一个对象赋值(
=
)给另外一个已经创建、已经存在的对象; -
拷贝构造函数也是一种构造函数,那么它的功能就是创建一个新的对象实例。调用的是拷贝构造函数还是赋值运算符,主要是看是否有新的对象实例产生。如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符。
调用拷贝构造函数主要有以下场景:
-
对象作为函数的参数,以 值传递的方式传给函数; -
对象作为函数的返回值,以 值的方式从函数返回; -
使用一个对象给另一个对象 初始化。
如果你设计的类中,都是一些基础的数据类型,例如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
-
h1调用的默认构造函数; -
h2也是新创建,利用h1去构造,因此调用拷贝构造函数; -
createHolder()
中在栈上创建一个临时的Holder(2000)
,因此先调用一次默认构造函数,因为函数是值传递(return by value),又会将刚才创建的临时对象拷贝一份返回,又调用一次拷贝构造函数; -
h3是新创建,利用 createHolder()
返回的临时对象进行初始化,因此又调用一次拷贝构造函数; -
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;
}
-
move构造函数的输入是一个右值引用(&&),右值引用我们可以修改它; -
在(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;
}
-
move赋值运算符,输入同样是一个右值引用(&&); -
由于是赋值运算符,在赋值之前当前的对象已经创建,所以先要清到当前对象中的数据(2); -
和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
-
h1是普通的构造函数; -
h2的构造,输入h1是左值,调用的拷贝构造函数; -
h3,先在栈上创建一个临时对象调用普通构造函数,然后返回调用move构造函数。由于 createHolder(2000)
函数返回的是右值,所以这时候h3构造调用的是move构造函数; -
h2是已经创建的对象,h3是左值,这时候调用赋值运算符; -
h2是已经创建的对象, createHolder(2000)
函数返回的是右值,所以调用的是move构造函数。
8. std::move
In particular,
std::move
produces an xvalue expression that identifies its argumentt
. It is exactly equivalent to astatic_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++左值与右值,最适合小白看的文章!