C++11之array数组基础学习
[TOC]
概述
文章参考:https://zhuanlan.zhihu.com/p/138210501
**左值(lvalue, left value)**,顾名思义就是赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象。
**右值(rvalue, right value)**,右边的值,是指表达式结束后就不再存在的临时对象。
区分左值和右值是很重要的,这是使用C++11 move语义的基础。c++11标准基本上是通过举例来说明一个表达式是否是一个lvalue还是rvalue的。
1 | lvalue = rvalue; |
对于以上的语句,lvalue是我们要赋值的对象。表示了一个占据内存中某个可识别的位置(也就是一个地址)的对象。它是一个变量,存在于内存中,它的值可以被改变,可以被取地址。任何可以通过它的名字,指针或者引用来接触的变量都是lvalue,例如定义的某个变量和函数的参数, 对一个表达式取地址。
rvalue则是一个临时变量,不存在于内存中,存在于CPU的寄存器或者指令的立即数中(immediate number),因此我们不能改变它的值,不能取地址。它们通常是一个直接的数值,运算符返回的数值,或是函数的返回值,或者通过隐式类型转换得到的对象。也可以使用排除法来定义。一个表达式不是 左值 就是 右值 。 那么,右值是一个 不 表示内存中某个可识别位置的对象的表达式。
反面实例
我们在 C/C++ 编程中并不会经常用到 左值 (lvalue) 和 右值 (rvalue) 两个术语。然而一旦遇见,又常常不清楚它们的含义。最可能出现两这个术语的地方是在编译错误或警告的信息中。例如,使用 gcc
编译以下代码时:
1 | int foo() {return 2;} |
你会得到:
1 | test.c: In function 'main': |
没错,这个例子有点夸张,不像是你能写出来的代码。不过错误信息中提到了左值 (lvalue)。另一个例子是当你用 g++
编译以下代码:
1 | int& foo() { |
现在错误信息是:
1 | testcpp.cpp: In function 'int& foo()': |
同样的,错误信息中提到了术语右值 (rvalue)。那么,在 C 和 C++ 中,左值 和 右值 到底是什么意思呢?我这篇文章将会详细解释。
举例
上面的术语定义显得有些模糊,这时候我们就需要马上看一些例子。我们假设定义并赋值了一个整形变量:
1 | int a; |
那么,函数是不是就只可以作为右值呢?其实不是。考虑一个我们司空见惯的例子:
1 | vector<int> vec = {1,2,3,4,5}; |
我们看到,其实operator[]
是一个函数,其返回值依然可以作为左值。
而 C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。
**纯右值(prvalue, pure rvalue)**,纯粹的右值,没有标识符、不可以取地址的表达式, 要么是纯粹的字面量,例如 10, true; 要么是求值结果相当于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。
**将亡值(xvalue, expiring value)**,是 C++11 为了引入右值引用而提出的概念(因此在传统 C++中, 纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值。
xvalue可能稍有些难以理解,我们来看这样的代码:
1 | std::vector<int> foo() { |
可修改的左值
左值引用和右值引用
在明确了左值和右值的关系之后,对于左值的引用就是左值引用,而对于右值的引用就是右值引用。如果一个表达式的类型是一个lvalue reference (例如, T& 或 const T&, 等.),那这个表达式就是一个lvalue。其它情况,这个表达式就是一个rvalue。
C++11之前的引用,我们指的是左值引用(T&),即:
1 | int a = 3; |
C++11引入了右值引用的概念,使我们可以取一个右值的引用(T&&):
1 | int&& a = 3; //正确 |
C++11 提供了std::move
这个方法将左值参数无条件的转换为右值, 有了它我们就能够方便的获得一个右值临时对象,对对象类型右值引用的转换。
1 | int main() |
需要拿到一个将亡值,就需要用到右值引用的申明:T &&
,其中T
是类型。 右值引用的声明让这个临时值的生命周期得以延长、只要变量还活着,那么将亡值将继续存活。更多细节可参考:
move语义
传统的 C++ 没有区分『移动』和『拷贝』的概念,造成了大量的数据拷贝,浪费时间和空间。 右值引用的出现恰好就解决了这两个概念的混淆问题,为了结合左值引用来轻易完成move语义的实现。什么是move语义,为什么需要move语义,我们来举一个std::vector的栗子。我们在执行v2=v1时,需要先完成一次函数调用,即调用拷贝赋值运算符,然后执行内存分配,最后循环逐个元素。
如果v1和v2我们都需要的话,生成两份拷贝自然是没有问题的,但多数情况下我们只希望使用v2,那么我们就只希望生成一份拷贝,减少不必要又麻烦的拷贝过程:设想如果它包含10000个元素,要增加多大的开销?例如下面的swap函数:
1 | template <class T> swap(T& a, T& b){ |
move函数所做的只是拿到一个左值或右值参数,然后都将其返回为右值同时不触发任何拷贝函数。它的作用就是就是相当于把参数的值剪切到目标对象的值,move可以说是一种具有破坏性的读操作。
1 | std::string str = "Hello"; |
放在v2=v1里,我们能让参数v1的体积为0,只生成v2,避免生成额外拷贝。要实现这样移值的move语义,右值引用的好处就来了,它使我们能轻松快速地完成这个功能:
1 | template <class T> |
C++11也引入了使用move语义来实现的移动构造函数的概念。相比C++98的拷贝构造函数,其区别就相当于剪切粘贴和复制粘贴,就像上面的例子一样。
1 | void someFunc(Widget w); |
比如:
1 |
|