目录
1 左值与右值
C++ 增加了一个新的类型,右值引用,记作“&&”
- 左值
是指在内存中有明确的地址,我们可以找到这块地址的数据(可取地址)
- 右值
只提供数据,无法找到地址(不可取地址)
- 所有有名字的都是左值,而右值是匿名的
- 一般情况下,位于等号左边的是左值,位于等号右边的是右值,但是也可以出现左值给左值赋值的情况。
2 右值
C++11 中右值分为两种情况:一个是将亡值,另一个是纯右值:
- 将亡值
非引用返回的临时变量,运算表达式产生的临时变量,原始字面量,lambda 表达式等。
- 纯右值
与右值引用相关的表达式,比如:T&& 类型函数的返回值,std::move() 的返回值等。
3 右值引用
右值引用就是对右值引用的类型。因为右值是匿名的,所以我们只能通过引用的方式找到它。无论是左值引用还是右值引用,都必须初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用,该右值所占的内存又可以被使用。
#include<iostream>
#include<vector>
using namespace std;
int&& value = 520;//右值引用,520是字面值,是右值
class Test
{
public:
Test()
{
cout << "构造函数" << endl;
}
//const是万能引用,即可以接收左值,又能接收右值
Test(const Test& other)//常量左值引用
{
cout << "拷贝构造函数" << endl;
}
};
Test getObj()
{
return Test();
}
int main()
{
int a1;
//int &&a2 = a1; //报错,右值引用不能被左值初始化
//Test& t1 = getObj(); //右值不能初始化左值引用
Test&& t2 = getObj(); //函数返回的临时对象是右值,可以被引用
const Test& t3 = getObj(); //常量左值引用是万能引用,可以接收左值、右值、常量左值、常量右值
const int& t3 = a1; //被左值初始化
return 0;
}
4 右值引用的用处
在 C++ 用对象初始化时,会调用拷贝构造,如果这个对象占用堆内存很大,那么这个拷贝的代价就是非常大的,在某些情况,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。
#include<iostream>
#include<vector>
using namespace std;
class Test
{
public:
Test() : m_num(new int(100))
{
cout << "构造函数" << endl;
}
//const是万能引用,即可以接收左值,又能接收右值
Test(const Test& other) : m_num(new int(*other.m_num))//常量左值引用
{
cout << "拷贝构造函数" << endl;
}
~Test()
{
delete m_num;
}
int* m_num;
};
Test getObj()
{
Test t;
return t;
}
int main()
{
Test t = getObj();
cout << "t.m_num = " << *t.m_num << endl;
return 0;
}
//输出
/*
构造函数
拷贝构造函数
t.m_num = 100
*/
这段代码在调用 Test t = getObj();的时候,调用了拷贝构造函数,对返回的临时对象进行了深拷贝得到了对象 t,在 getObj 函数中创建的对象虽然进行了内存申请操作,但是没有使用就被释放掉了。如果我们在函数结束后,仍然可以利用在函数里面申请的空间,就极大的节省了创建对象和释放对象的空间,这个操作就需要我们的右值引用来完成。
右值引用具有移动语义,移动语义可以将堆区资源,通过浅拷贝从一个对象转移到另一个对象,这样就能减少不必要的临时对象的创建,拷贝以及销毁,大幅度提高性能。
#include<iostream>
#include<vector>
using namespace std;
class Test
{
public:
Test() : m_num(new int(100))
{
cout << "构造函数" << endl;
}
//const是万能引用,即可以接收左值,又能接收右值
Test(const Test& other) : m_num(new int(*other.m_num))//常量左值引用
{
cout << "拷贝构造函数" << endl;
}
//添加移动构造函数,参数是右值引用
Test(Test&& a) :m_num(a.m_num)
{
a.m_num = nullptr;
cout << "移动构造函数" << endl;
}
~Test()
{
delete m_num;
}
int* m_num;
};
Test getObj()
{
Test t;
return t;
}
int main()
{
Test t = getObj();// 因为getObj 返回的是右值,所以调用移动构造函数
cout << "t.m_num = " << *t.m_num << endl;
return 0;
}
//输出
/*
构造函数
移动构造函数
t.m_num = 100
*/
在上面的代码中添加了移动构造函数(参数为右值引用类型),这样在进行 Test t = getObj();并没有调用构造函数进行深拷贝,而是调用的(浅拷贝)移动构造,提高了性能。
本例子中,getObj() 返回值是一个右值,在进行赋值操作的时候,如果等号右边是一个右值,那么移动构造函数就会被调用。
结论:
需要动态申请大量的资源的类,应该设计移动构造,提高程序的效率。需要注意的是在提供移动构造的同时,一般也会提供左值引用拷贝构造函数,左值初始化新对象时会走拷贝构造函数。
5 move左值转右值
C++11 添加了右值引用,却不能左值初始化右值引用,在一些特定的情况下免不了需要左值初始化右值引用(用左值调用移动构造),如果想要用左值初始化一个右值引用,想要借助 std::move() 函数,move() 函数可以将左值转换为右值。
#include<iostream>
#include<vector>
using namespace std;
class Test
{
public:
int a = 3;
int* m_num = &a;
Test() : m_num(new int(100))
{
cout << "构造函数" << endl;
}
Test(const Test& other) : m_num(new int(*other.m_num))//常量左值引用
{
cout << "拷贝构造函数" << endl;
}
//添加移动构造函数,参数是右值引用
Test(Test&& a) :m_num(a.m_num)
{
a.m_num = nullptr;
cout << "移动构造函数" << endl;
}
~Test()
{
delete m_num;
cout << "析构函数" << endl;
}
};
Test getObj()
{
Test t;
return t;
}
int main()
{
Test t = getObj();
Test t1 = move(t);//此处调用移动构造,因为move是一个右值
cout << "t1.m_num = " << *t1.m_num << endl;
return 0;
}
//输出
/*
构造函数
移动构造函数
析构函数
移动构造函数
t1.m_num = 100
析构函数
析构函数
*/
6 引用折叠
C++中,并不是所有情况下 && 都代表右值引用,在模板和自动类型推导(auto)中,如果是模板参数,想要指定为 T&&,如果是自动类型推导,需要指定为 auto&&,这两种情况下,&& 被称作“未定的引用类型”。另外 const T&& 表示一个右值引用,不是未定引用类型。
template<typename T>
void fun(T&& param)
{
work(forward<T>(param))
}
int main()
{
fun(10); //对于 f(10) 来说传入的实参 10 是右值,因此 T&& 表示右值引用
int x = 1;
fun(x);//对于 f(x) 来说传入的实参是 x 是左值,因此 T&& 表示左值引用
return 0;
}
因为 T&& 或者 auto&& 这种未定引用类型作为参数时,有可能被推导成右值引用,也有可能被推导为左值引用,在进行类型推导时,右值引用会发生变化,这种变化被称作引用折叠。折叠规则如下:
- 提供右值推导 T&& 或者 auto&& 得到的是一个右值引用类型,const T&& 表示一个右值引用。
- 通过非右值(右值引用、左值、左值引用、常量右值引用、常量左值引用)推导 T&& 绘制 auto&& 得到的是一个左值引用类型。
int main()
{
int&& a1 = 1; //右值 推导为 右值引用
auto&& bb = a1; //右值引用 推导为 左值引用
auto&& bb1 = 2; //右值 推导为 右值引用
int a2 = 1;
int& a3 = a2; //左值 推导为 左值引用
auto&& cc = a3; //左值引用 推导为 左值引用
auto&& cc1 = a2; //左值 推导为 左值引用
const int& s1 = 1; //常量左值引用
const int&& s2 = 1; //常量右值引用
auto&& dd = s1; //常量左值引用 推导为 左值引用
auto&& ee = s2; //常量右值引用 推导为 左值引用
return 0;
}
7 forward完美转发
右值引用类型是独立于值的,一个右值引用作为函数的形参时,在函数内部转发该参数给内部其他参数时,他就变成了一个左值(当右值被命名是编译器认为他是个左值),并不是原来的类型了。如果按照参数原来的类型转发到另一个函数,可以使用 C++11 的 std::forward() 函数,该函数实现的功能称之为完美转发。
// 函数原型
template <class T> T&& forward(typename remove_reference<T>::type& t) noexcept;
template <class T> T&& forward(typenameremove_reference<T>::type&& t) noexcept;
// 精简之后的样子
std::forward<T>(t);
- std::forward<T>(t);
- 当 T 为左值引用类型时,t 将会被转换为左值
- 当 T 不是左值引用类型时,t 将会被转换为 T 类型的右值
#include <iostream>
using namespace std;
template<typename T>
void printValue(T& t)
{
cout << "左值引用: " << t << endl;
}
template<typename T>
void printValue(T&& t)
{
cout << "右值引用:" << t << endl;
}
template<typename T>
void testForward(T && v)
{
printValue(v);
printValue(move(v));
printValue(forward<T>(v));
cout << endl;
}
int main()
{
testForward(520);
int num = 1314;
testForward(num);
testForward(forward<int>(num));//int不是左值引用,则num被推导为右值
testForward(forward<int&>(num));//int&是左值引用,则num被推导为左值
testForward(forward<int&&>(num));
return 0;
}
//输出
/*
左值引用: 520
右值引用:520
右值引用:520
左值引用: 1314
右值引用:1314
左值引用: 1314
左值引用: 1314
右值引用:1314
右值引用:1314
左值引用: 1314
右值引用:1314
左值引用: 1314
左值引用: 1314
右值引用:1314
右值引用:1314
*/