c++旧标准中有引用类型和指针类型,从c++11标准开始引入了右值引用,原来的引用被称之为左值引用,今天我们就来看一下右值引用是什么?右值引用是用来解决什么问题的?
1. 什么是右值引用
1.1左值和右值
c++的表达式可以是左值或者右值。左值和右值的概念是从c语言继承而来的,原本的含义是:左值可以放在赋值语句的左侧,右值则不能。
但是在c++中,左值和右值的区别没那么简单。例如常量表达式为代表的一些左值实际上不能作为赋值语句的左侧运算对象。
当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象本身(内存中的位置)。
需要使用左值的地方:
- 赋值运算符需要一个左值作为左侧运算对象,得到一个左值。
- 取地址符(&)作用左值运算对象,得到这个对象的指针(右值)。
- 解引用(*)和下标运算符([])求值结果为左值。
- 内置类型和迭代器的递增递减运算符(++,–)需要作用于左值,前置自增自减返回左值,后置自增自减返回右值。
- 字符串字面量如 “hello world”
右值是没有标识符、不可以取地址的表达式,一般也称之为“临时对象”。最常见的情况有:
- 返回非引用类型的表达式,如 x++、x + 1、make_shared(42)
- 除字符串字面量之外的字面量,如 42、true
1.2左值引用和右值引用
在旧标准中的引用是对表达式的一个别名,c++11中将旧标准中的引用称之为左值引用,使用&表示。与其相对的是右值引用,只能绑定到右值,使用&&表示。右值引用只能绑定到一个即将销毁的变量。
2. 移动
在c++11中,虽然不能将一个右值引用绑定到一个左值上,但是可以显示地将一个左值转换为对应的右值引用。使用标准库提供的std::move()函数来实现,调用了move意味着这个左值除了销毁和重新赋值以为,我们不再使用它。
3. 右值引用的使用场景
3.1 移动构造函数
提供移动构造函数可以高效拷贝一些右值对象或不再使用的左值对象。
class TestDemo
{
public:
TestDemo(int num):num(num){
std::cout << "调用构造函数" <<"," << num << endl;
}
TestDemo(const TestDemo& other) :num(other.num) {
std::cout << "调用拷贝构造函数" <<"," << num << endl;
}
TestDemo(TestDemo&& other) noexcept :num(other.num) {
std::cout << "调用移动构造函数" <<"," << num << endl;
}
private:
int num;
};
int main() {
TestDemo test(1);
TestDemo test2(std::move(test));
}
运行输出
调用构造函数,1
调用移动构造函数,1
在上面的示例中调用构造函数构造了一个对象test, test对象如果在后面不再使用,则可以通过std::move获取它的右值引用进而调用移动构造函数,避免了数据的拷贝。
3.2 移动赋值
除了移动构造函数,我们还可以定义移动赋值操作符,可以将一个右值移动到左值上,避免数据拷贝。
TestDemo &operator=(TestDemo&& other) noexcept {
if(this != &other) {
//todo::释放资源
num = other.num;
}
std::cout << "调用移动赋值" << endl;
return *this;
}
TestDemo test(1);
TestDemo test2(2);
test2 = std::move(test);
运行输出结果:
调用构造函数,1
调用构造函数,2
调用移动赋值
3.3 push_back
class TestDemo
{
public:
TestDemo(int num):num(num){
std::cout << "调用构造函数" <<"," << num << endl;
}
TestDemo(const TestDemo& other) :num(other.num) {
std::cout << "调用拷贝构造函数" <<"," << num << endl;
}
TestDemo(TestDemo&& other) noexcept :num(other.num) {
std::cout << "调用移动构造函数" <<"," << num << endl;
}
private:
int num;
};
int main() {
std::vector<TestDemo> demo1{2,3};
TestDemo test{1};
demo1.push_back(test);
}
初始化一个有两个元素的vector,然后再插入一个左值。我们知道vector超过容量之后要扩容然后拷贝原有数据。但如果类定义了一个noexcept的移动构造函数,原始数据将不用拷贝构造函数拷贝,而使用移动构造,大大提高效率。以上代码输出:
调用构造函数,2
调用构造函数,3
调用拷贝构造函数,2
调用拷贝构造函数,3
调用构造函数,1
调用拷贝构造函数,1
调用移动构造函数,3
调用移动构造函数,2
但是如果没有定义移动构造函数,那就需要拷贝了,如下示例:
class TestDemo
{
public:
TestDemo(int num):num(num){
std::cout << "调用构造函数" <<"," << num << endl;
}
TestDemo(const TestDemo& other) :num(other.num) {
std::cout << "调用拷贝构造函数" <<"," << num << endl;
}
// TestDemo(TestDemo&& other) noexcept :num(other.num) {
// std::cout << "调用移动构造函数" <<"," << num << endl;
// }
private:
int num;
};
int main() {
std::vector<TestDemo> demo1{2,3};
TestDemo test{1};
demo1.push_back(test);
}
运行输出:
调用构造函数,2
调用构造函数,3
调用拷贝构造函数,2
调用拷贝构造函数,3
调用构造函数,1
调用拷贝构造函数,1
调用拷贝构造函数,3
调用拷贝构造函数,2