C++左值右值
左值右值
什么是左值右值
左值右值的概念我们一般理解为赋值号左边或者右边的值,这样理解也是有一定的道理的,但是实际上,左值右值绝不只是简简单单的定义,用好它绝对会大大提高编程效率。首先,严格意义上的左值右值取决于其是否能取地址,上代码:
#include <iostream>
int main() {
int a = 5;
std::cout << &a << std::endl; // 输出:0x7ffe7580b204
std::cout << &5 << std::endl; // 报错:lvalue required as unary ‘&’ operand
}
报错内容就是:不能将&
运算符应用于右值。之所以称呼为左值右值,是因为左值大多可以用赋值号修改(除了用const
修饰的)。当然,除了这些最简单的变量,还有很多比较奇怪的左值右值,这里也列举一下:
#include <iostream>
int x;
int& func_ref() {
return x;
}
class obj{
double member;
}
int main() {
int a = 10; // a 是左值
int* p = &a; // 正确,a 可寻址
a = 20; // 正确,左值可修改
int& ref = a; // ref是左值
int array[3] = {1, 2, 3}; // 数组元素是左值
obj.member = 5; // 类成员是左值
int b = a + 5; // a 作为右值使用(隐式转换)
func_ref() = 30; // 正确,func_ref() 返回左值引用,实际上func_ref()就是变量x的引用
std::cout << func_ref() << std::endl; // 输出30
const int c = 40;
// c = 50; // 错误,c 是 const 左值,不可修改
const char *ptr = &"hello"[0]; // 正确,“hello”是左值
}
这里有一个比较难以理解,就是hello
竟然是左值,而且可以取地址,这将在下面特性
介绍中提到。
左值右值的特性
首先,就是我们上面提的,左值可以使用取地址符&
,我们称之为可寻址性,那么右值也就具有不可寻址性。其次,左值的生存周期通常超出当前语句的作用域,这句话也很好理解,比如int x = 5
,那么在这句代码后仍然可以使用变量x
,而这里的右值5
,超过这句话后,就不存在了,下一次即使出现相同的5
也和它时不一个量了。
除此之外,还有他们的储存位置也不尽相同。对于左值而言,它通常存放在栈
中,比如int x = 5
,这里的x
就存放在栈中,除此之外,还有可能存放在堆
、静态区
等位置,而右值会存放在寄存器
、栈临时空间
等位置,甚至无储存,很明显就是右值没有可以持久的储存位置。
这里我们解释一下上面提到的hello
为什么是左值,作为C++字符串而言,它的设计继承了C语言的特性,C语言中的设计也是如此,当我们使用字符串比如hello
时,它通常是具有静态存储期的 const char[N]
数组对象,与其他类型的字面量如5
不同。对于这一点我觉得没有必要深究原因,留个心眼即可。
右值的引入
实际上,C++98/03是只有左值,没有右值这个概念的,之所以C++11引入这个概念是出于对性能的考虑,这也就是我在文首说的,它绝不仅仅只是个定义这么简单。上代码,注意下面的std::move()
函数:
#include <iostream>
#include <vector>
#include <chrono>
int main() {
const int DATA_SIZE = 10000000; // 一个包含 1000 万个 double 的向量
// 创建一个大型 vector(模拟需要拷贝的数据)
std::vector<double> src(DATA_SIZE, 3.14);
// 方法 1:深拷贝(拷贝构造函数)
auto start_copy = std::chrono::high_resolution_clock::now();
std::vector<double> dest_copy = src; // 深拷贝
auto end_copy = std::chrono::high_resolution_clock::now();
auto duration_copy = std::chrono::duration_cast<std::chrono::microseconds>(end_copy - start_copy).count();
// 方法 2:移动语义(std::move)
auto start_move = std::chrono::high_resolution_clock::now();
std::vector<double> dest_move = std::move(src); // 移动语义
auto end_move = std::chrono::high_resolution_clock::now();
auto duration_move = std::chrono::duration_cast<std::chrono::microseconds>(end_move - start_move).count();
// 输出结果
std::cout << "深拷贝耗时: " << duration_copy << " 微秒" << std::endl;
std::cout << "移动语义耗时: " << duration_move << " 微秒" << std::endl;
return 0;
}
这里我们定义了一个包含1000万个double
类型的向量,并用传统的赋值方式拷贝至另一个向量,记录下拷贝所需时间,同时用移动语义的方法拷贝相同的数组,记录下所需时间,并输出,输出结果如下:
深拷贝耗时: 41368 微秒
移动语义耗时: 0 微秒
性能差别就是这么大,我们解释一下。这里的深拷贝其实就是一个一个复制这个数据,转到需要拷贝的向量中,而移动语以就是只偷这块内存的指针,为我所用,所以其时间复杂度为O(1)
,当然,在这之后,原本的数据就为空了(但并不违法),如果我们在上面的代码中分别输出拷贝后的向量大小,就会发现,第一个仍然是一千万个变量,而移动后就归零了。
举个例子就是需要将一个快递寄给另一个地方,第一种做法是,把快递拿出来,按照原样再做一个,再装进另一个快递箱中,再贴上一个新的快递单,第二种做法就是撕掉之前的快递单,贴上新的快递单,(之前的快递单贴在了一个空箱子上)。
那么,这和左值右值有什么关系呢?事实上,向量src
在使用函数std::move()
后,它就从左值变为右值了,因为只有右值存在移动语义,因此这里的std::move()
函数并不是真的移动,它只是将左值变为右值。因此,对于一些容器的拷贝操作,我们应该能想到右值的存在。
附录
附录一
填坑:右值引用
上篇博客中我们提到了右值引用的概念,并且给出了定义方式&&
,这里,我们详细说说右值引用究竟有什么用。实际上,我们不太会去定义一个右值引用,当然,如果你强行使用一个右值引用一个常量,也并不会有什么问题,而且还会将这个右值的生命周期延长到引用的作用域,但是这着实是tuokuzifangpi
。右值引用的作用也正是上面提到的拷贝中的优点,至于&&
符号,别忘了,函数参数可以使用得到,这里上一个简单的代码尝尝鲜:
#include <iostream>
// 定义一个函数,接收 int 的右值引用参数
void processInt(int&& x) {
std::cout << "Received rvalue: " << x << std::endl;
}
int main() {
int a = 100;
processInt(std::move(a));
// 直接传递一个右值字面量也可以
processInt(200);
return 0;
}