目录
一、左值右值
1.1 定义
在C++中有几个晦涩的概念:左值、右值、泛左值、纯右值、将亡值
这5种值类别我总结了一张表格,如下:
首先需要区分两个概念。在C++中,表达式有两种属性:
- 类型(type)
- 值类别(value category)
例如有int a=1, b=2; 那么"a+b"这个表达式的【类型】是int,而【值类别】是右值,合起来就称为【int类型的右值表达式】。
左值右值描述的是一个表达式的值类别。
在C++中,一切表达式都必然属于左值、将亡值、纯右值中的一种。而右值=将亡值+纯右值,所以我们又可以说一切C++表达式都必然要么是左值要么是右值。
泛左值=左值+将亡值; 右值=纯右值+将亡值; (将亡值既属于泛左值又属于右值)。
这些分类是由于它们具有不同性质而命名的,但其实我们只需要掌握左值和右值的概念就已经足够了。
从性质上来看:
- 只有左值可以取地址、可以作为等号左边的操作数;不具有该性质的就必然是右值。
- 左值和将亡值都具有多态性质;不具有该性质的就是纯右值了。
1.2 举例
用是否可以取地址来验证表达式是左值还是右值:
#include <utility>
#include <string>
using namespace std;
string &f1(string &a) {
return a;
}
string &&f2() {
string a;
return move(a);
}
string f3() {
return "";
}
int main() {
string s("hello");
printf("%p\n", &f1(s)); //f1(s)是左值,可以取地址
// printf("%p\n", &f2()); //f2()是将亡值,不能取地址
// printf("%p\n", &f3()); //f3()是纯右值,不能取地址
int a = 1;
printf("%p\n", &++a); //前置自增是左值,可以取地址
// printf("%p\n", &a++); 后置自增是右值,不能取地址
printf("%p\n", &"hello"); //字符串字面值是左值,可以取地址
// printf("%p\n", &42); //数字字面值是右值,不能取地址
}
验证将亡值的多态性
#include <iostream>
struct A {
virtual void f() {
printf("classA\n");
}
};
struct B : A {
void f() override {
printf("classB\n");
}
};
int main() {
static_cast<A&&>(B()).f(); //打印classB, static_cast表达式是将亡值,静态类型为A,动态类型为B,发生多态
static_cast<A>(B()).f(); //打印classA, static_cast表达式是纯右值,静态类型为A,动态类型为A,未发生多态
}
二、左值引用 右值引用
2.1 定义
在C++中引用类型与表达式之间的绑定关系有语法上的规定,如下表:
- T&只能绑定非常量左值
- const T&是万能引用类型,可以绑定任意值类别的表达式
- T&&只能绑定非常量右值
- const T&&能绑定右值类型的表达式,无论是否常量
代码验证绑定规则:
#include <utility>
#include <string>
using namespace std;
int main() {
string s1; //非常量左值
const string s2; //常量左值
string &r1 = s1; //【非常量左值引用】可以绑定【非常量左值】
// string &r2 = s2; //【非常量左值引用】不能绑定【常量左值】
const string &r3 = s1; //【常量左值引用】可以绑定【非常量左值】
const string &r4 = s2; //【常量左值引用】可以绑定【常量左值】
const string &r5 = move(s1); //【常量左值引用】可以绑定【非常量右值】
const string &r6 = move(s2); //【常量左值引用】可以绑定【常量右值】
string &&r7 = move(s1); //【非常量右值引用】可以绑定【非常量右值】
// string &&r8 = move(s2); //【非常量右值引用】不能绑定【常量右值】
const string &&r9 = move(s1); //【常量右值引用】可以绑定【非常量右值】
const string &&r10 = move(s2); //【常量右值引用】可以绑定【常量右值】
}
回过头看一下C++的拷贝构造和移动构造
拷贝构造T(const T&)接受一个常量左值引用,可以绑定T类型的任意值类别表达式;
移动构造T(T&&)接受一个右值引用,只能绑定T类型的右值表达式。
这使得当没有定义移动构造函数时,右值表达式也能匹配上拷贝构造函数的形参。但同时也没法享受移动语义带来的高效率了。
2.2 引用的作用
左值本身就是具名变量,当它被绑定给左值引用时只是起了个别名,生命周期并不会变化。
而右值本身的生命周期会在表达式结束后立即被销毁。但当右值被绑定给引用时,该右值会“重获新生”,它的生命周期会延长,与该引用的生命周期相同。
代码举例:
#include <iostream>
using namespace std;
class A {
public:
A(int a) : mVal(a) {
printf("A(), mVal=%d\n", mVal);
}
~A() {
printf("~A(), mVal=%d\n", mVal);
}
private:
int mVal;
};
A &f() {
A a = A(10);
return a; // a是局部变量,是左值。该值在离开此函数时即被销毁,绑定给A&并不会延长它的生命周期
}
int main() {
A &ref = f(); // 此处的ref指向一个非法值
A(0); // 右值。生命周期只有该行。被创建后立即被析构
A &&ref1 = A(1); // 将右值绑定给右值引用ref1,其生命周期被延长至与ref1相同
A &&ref2 = A(2); // 将右值绑定给右值引用ref2,其生命周期被延长至与ref2相同
} // 离开函数作用域,依次销毁ref2、ref1
2.3 需要注意的点
※ 右值引用是个左值。(是不是很绕口?)
准确的来说,所有的引用,无论是左值引用还是右值引用,都是左值表达式。
这是因为右值引用延长了右值的生命周期,我们可以去取右值引用的地址,因此它是个左值。
代码举例:
#include <iostream>
int main() {
int &&a = 3; //3是右值 a是右值引用
printf("address of a: %p", &a); //我们不能直接对3取地址,但可以对右值引用a取地址。
}
※ move虽然将左值转换成了右值,但其生命周期并不会像右值一样立即结束。
代码举例:
#include <iostream>
using namespace std;
class A {
public:
A(int a) : m(a) {
printf("A(), m=%d\n", m);
}
A(const A &) = default;
A(A &&) = default;
~A() {
printf("~A(), m=%d\n", m);
}
private:
int m;
};
int main() {
A a1(1);
move(a1); //move将a1从左值转换为右值,但它并不会在离开此行后被析构
A a2(2);
}