左值与右值的区别
左值: 就是有确定的内存地址、有名字的变量,可以被赋值,可以在多条语句中使用;
右值: 没有名字的临时变量,不能被赋值,只能在一条语句中出现,如:字面常量和临时变量。
如何区分左值与右值
- 看能不能对表达式取地址
能否用“取地址&”运算符获得对象的内存地址。
对于临时对象,它可以存储于寄存器中,所以是没办法用“取地址&”运算符;
对于常量,它可能被编码到机器指令的“立即数”中,所以是没办法用“取地址&”运算符;
这也是C/C++标准的规定。
int a = 5;
int b = 6;
int *ptr = &a;
vector v1;
string str1 = "hello " ;
string str2 = "world" ;
const int &m = 1 ;
a+b//临时对象
a++//右值,是先取出持久对象a的一份拷贝,再使持久对象a的值加1,最后返回那份拷贝,而那份拷贝是临时对象(不可以对其取地址),故其是右值。
++a//左值,使持久对象a的值加1,并返回那个持久对象a本身(可以对其取地址),故其是左值。
v1[0]//调用了重载的[]操作符,而[]操作符返回的是一个int &,为持久对象(可以对其取地址)是左值。
string("hello")//临时对象(不可以对其取地址),是右值;
str1+str2//调用了+操作符,而+操作符返回的是一个string(不可以对其取地址),故其为右值;
str1//左值
*p//左值
左值引用和右值引用
左值引用:引用是C++语法做的优化,引用的本质还是靠指针来实现的。引用相当于变量的别名。引用可以改变指针的指向,还可以改变指针所指向的值。声明引用的时候必须初始化,且一旦绑定,不可把引用绑定到其他对象;对引用的一切操作,就相当于对原对象的操作。
右值引用:在C++中总会有一些临时的、生命周期较短的值(右值),这些值我们无法改变。但c++引用了右值引用的概念:它是一个可以被绑定到临时对象的类型,允许改变临时对象的值。并且右值引用的对象,不会在其它地方使用。
int i = 2;//左值
int &a = i; //左值引用
int &&a = 10; //右值引用
根据修饰符的不同,左值引用可分为:非const左值、const左值;右值引用可分为:非const右值、const右值。
int i = 10;//非const左值
const int j = 20;
//非const左值引用
int &a = i;//非const左值引用 绑定到 非const左值,编译通过
int &a = j;//非const左值引用 绑定到 const左值,编译失败
int &a = 5;//非const左值引用 绑定到 右值,编译失败
//const左值引用
const int &b = i;//const左值引用 绑定到 非const左值,编译通过
const int &b = j;//const左值引用 绑定到 const左值,编译通过
const int &b = 5;//const左值引用 绑定到 右值,编译通过
//非const右值引用
int &&c = 30;//非const右值引用 绑定到 右值,编译通过
int &&c = i;//非const右值引用 绑定到 非const左值,编译失败(不能将一个右值引用绑定到左值上)
//const右值引用
const int &&d = 40;//const右值引用 绑定到 右值,编译通过
const int &&d = c;//const右值引用 绑定到 非常量右值,编译通过
总结
非常量的左值引用只能被绑定到非常量左值
非常量的右值引用只能被绑定到非常量右值
move(移动)语义
move语义是和拷贝语句相对的,是一个最佳移动资源的方法,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高C++应用程序的性能。可以类比文件的剪切和拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度比剪切慢很多。move语义的实现依赖于右值引用,允许程序员编写将资源(例如动态分配的内存)从一个对象传输到另一个对象的代码,move语义行之有效,因为它允许从程序中其他地方无法引用的临时对象转移资源,即临时对象中的资源能够转移其他的对象里。为了实现move语义,需要在类中提供一个动态构造函数(移动构造函数),和可选择的动态赋值运算符(移动赋值操作符)。对于右值的拷贝和赋值会调用移动构造函数和移动赋值操作符。
通过如下例子分析move语义对于提升效率的作用。
class Holder
{
public:
Holder(int size) // Constructor
{
m_data = new int[size];
m_size = size;
}
~Holder() // Destructor
{
delete[] m_data;
}
private:
int* m_data;
size_t m_size;
};
这是一个处理动态内存块的类,由于该类对象的数据是由指针管理的堆,系统默认的函数对于处理动态资源是完全不够的。需要自己定义复制构造函数以及重载赋值运算符。首先定义可以实现深拷贝的复制构造函数以及重载赋值运算符:
//复制构造函数,用于初始化
Holder(const Holder& other)
{
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;
}
//重载赋值运算符,用于赋值而非初始化
Holder& operator=(const Holder& other)
{
if(this == &other) return *this;
delete[] m_data;
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;
return *this;
}
这里我使用一个已经存在的对象other来初始化或者赋值给一个新的Holder对象,创建了一个同样大小的数组并且将other里面m_data的数据拷贝到this.m_data中,即实现深拷贝。然后可以调用这两个函数从另一个已经存在的对象来构造新的对象或将一个已存在的对象替换为另一个已存在的对象:
对于上述代码中的这些全局对象来说,通过复制构造函数和赋值运算符实现深拷贝(生成一个它们所属类的一个副本)是有必要的,因为我们可能在整个程序中还要用到这些对象,它们不会很快被销毁。但是对于一些临时对象来说,比如当一个函数的返回值为一个对象时,需要创建临时对象并调用复制构造函数,造成很大的系统开销。如以下代码:
Holder createHolder(int size)
{
return Holder(size);
}
它用传值的方式返回了一个Holder对象。我们知道,当函数返回一个值时,编译器会创建一个临时且完整的对象(右值)。由于Holder类有着内部的内存分配,以现有的类设计返回这些东西的值会导致多次内存分配,如下:
int main()
{
Holder h = createHolder(1000);
}
由createHolder()创建的临时对象(是一个右值,调用结束后就会消亡)被传入复制构造函数中,根据我们现有的设计,拷贝构造函数通过拷贝临时对象的数据分配了它自己的m_data指针。这里有两次内存分配:
- 创建临时对象
- 拷贝构造函数调用
同样地,在赋值操作符中也会有复制过程:
int main()
{
Holder h = createHolder(1000); // 复制构造函数
h = createHolder(500); // 赋值操作符
}
以上代码复制的过程太多,我们希望避开这些复制过程,直接实现对象的传递,改变资源的拥有者,但上面提供的复制构造函数和重载的赋值运算符的参数都是常量引用,无法修改其值,而非常量引用的初始值又必须为左值,也无法实现我们的要求。这时便可以通过使用基于右值引用的move语义来实现。因此需要提供新的版本的拷贝构造函数和赋值运算符,即移动构造函数和移动赋值运算符:
//移动构造函数
Holder(Holder&& other) // 右值引用为函数形参
{
m_data = other.m_data; // (1)
m_size = other.m_size;
other.m_data = nullptr; // (2)
other.m_size = 0;
}
//移动赋值运算符
Holder& operator=(Holder&& other) // 右值引用为函数形参
{
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;
}
在移动构造函数中,使用一个右值引用来构造Holder对象,其关键是:作为一个右值引用,我们可以修改它,所以可以先偷他的数据(1),然后将它设置为一个右值nullptr(2)。这里没有深拷贝,我们仅仅移动了这些资源。注意:将右值引用的数据设置为nullptr是很重要的,因为一旦临时对象走出其作用域,它就会调用析构函数中的delete[] m_data。通常来说,为了让代码看上去更加的整洁,最好让被偷取的对象的数据处于一个良好定义的状态。类似地,对于移动赋值运算符,我们先清理已有对象的数据(1)再从其它对象处偷取数据(2)当然还要把临时对象的数据设置为正确的状态(3)剩下的就是常规的赋值运算所做的操作。
int main()
{
Holder h1(1000); // 调用构造函数
Holder h2(h1); // 调用复制构造函数(左值h1是输入)
Holder h3 = createHolder(2000); // 调用移动构造函数,输入是一个右值(临时对象)
h2 = h3; // 调用赋值运算符(输入h3是一个左值)
h2 = createHolder(500); // 调用移动赋值运算符,输入是一个右值(临时对象)
}
此外,通过标准库中的工具函数 std::move,可以移动左值。它被用来将左值转化为右值,假设我们想要从一个左值盗取数据:
int main()
{
Holder h1(1000); // h1是一个左值
Holder h2(h1); // 调用复制构造函数(左值h1是输入)
}
由于h2接收了一个左值,复制构造函数被调用。我们需要强制调用移动构造函数从而避免无意义的拷贝,所以可以这样:
int main()
{
Holder h1(1000); // h1是一个左值
Holder h2(std::move(h1)); // 调用移动构造函数
//Holder h3(std::move(h1)); // 此时h1已无数据,被转移到了h2 (1)
}
std::move的作用就是返回传入参数的右值引用, 总而言之move语义是为了避免临时对象的无意义的复制。