引用与指针
指针一方面是为了兼容C的程序,另一方面指针的功能非常强大,这里不做过多介绍了,指针就是变量的地址。
左右值
int a = 1; // 这里 a 写在左边,就是左值,1写在右边,就是右值
int b = a; // 左值:b,右值:a
基于上述例子,可以看出,大部分变量其实是左值,变量也可以当右值,但是显然数字这种量不能当左值,比如不能写1=a
这样的语法。
左右值很大一个区别就是,左值可以取地址&a
,右值是不能取地址的&1
。
右值还有一个名称,叫做将亡值,即生命周期马上就没了的。
但是经常会有使用右值的情况,因此右值已经会被移动了,可以把右值看成无人认领的物品,如果有人认领,右值就会被移动,没人认领就消失了。
引用
c++11之前只有一种引用,也就是现在的左值引用,不妨把左值引用叫做绑定吧。
int a = 1;
int& b = a; // 把左值引用 b 绑定到 a 上,对b修改就会修改a
b = 2; // a也变成了2,事实上,引用底层实现就是用了地址。
左右值引用
- 左值引用只能绑定左值,不能绑定右值。
const
左值引用可以绑定左值或右值。- 右值引用只能绑定右值,不可以绑定左值。
move
c++给出了一个函数,move
函数,可以把左值更换成右值的属性
#include <move.h>
int b = 1;
// int&& c = b; // 报错
int&& c = std::move(b); // 不报错,可运行
int&& d = c; // 也报错,右值引用变量其实是左值
拷贝/移动 构造/赋值
常常定义类的时候,会有拷贝构造和移动构造,拷贝赋值运算符重载和移动赋值运算符重载等。
- 拷贝构造/赋值参数是:
const T& t
,能够接收左值和右值 - 移动构造/赋值参数是:
T&& t() noexcept
,只能接收右值,并推荐加上noexcept
来提高运行速度 - 如果传入的是右值,编译器会更加倾向于调用移动构造/赋值,除非没有定义移动赋值和构造。
如果一个对象(包括函数的形参声明)声明的时候用另一个对象赋值,就会调用构造函数而不是赋值函数。如果一个变量已经声明且定义了,在后续赋值则会调用赋值运算符。
在现代编译器上,一般都会有优化,所以不用特别担心,总之,相信编译器的优化吧。
forward
万能引用-forward的引子
void function(MyClass&& a); // 这是右值引用
template<class T>
void function(T&& t); // 这里其实叫做万能引用,因为T的类型需要等调用才确定
template <class T>
class A{
void function_2(T&& t); // 这里其实是右值引用,可不是万能引用,因为实例化对象就已经确定T的类型
template<class U> // 模板嵌套相当于 template <class T> template<class U> void fun();
void function_3(U&& u); // 万能引用,因为U的类型要等到调用才知道
};
万能引用中,如果传入左值,那么是左值引用,传入右值就是右值引用。
保持右值属性
前面提到,右值引用的变量其实是左值,那如何保证其右值属性好为我们所用呢?答:std::forward<T>()
转发函数
#include <move.h>
int a = 1;
int& b = a;
int&& c = move(b);
int&& d = forward<int>(c); // 其实 forward 返回的就是右值罢了
万能引用的应用
void Fun(int& a) {
cout << "左值引用,a = " << a << endl;
}
void Fun(int&& a) {
cout << "右值引用,a = " << a << endl;
}
void Fun(const int& a) {
cout << "const左值引用,a = " << a << endl;
}
void Fun(const int&& a) {
cout << "const右值引用,a = " << a << endl;
}
template<class T>
void F(T&& t) { // 这是一个万能引用
Fun(std::forward<T>(t)); // 保持右值属性
Fun(t); // 如果不使用转发的话,t就一定是个左值,因为右值引用,左值引用都是左值,这样我们一旦进入深层函数就没法保持右值属性了
}
int a = 10;
const int b = 11;
const int c = 12;
// 调用
F(a);
F(b);
F(std::move(c));
F(13);
结果如下:
左值引用,a = 10 // a
const左值引用,a = 11 // b
const右值引用,a = 12 // c
右值引用,a = 13 // 13
分析:
- a显然是左值,调用的也是左值引用
- b是常左值,调用的也是常左值引用
- move(c)是右值引用,然后前面还有const修饰,所以是常右值引用
- 13是典型的右值,所有是右值引用
STL库应用参考
上面是特性,下面给出STL库中怎么使用forward
的呢?
template<class T>
class vector{
void push_back(const T& t){ // 左值引用
insert(end(), t);
}
void push_back(T&& t){ // 右值引用
insert(end(), std::forward<T>(t)); // 完美转发保持右值属性
// 解释:因为 这个函数里面的t是一个右值引用的左值量,如果直接传入insert的话,就变成左值了
// 因此,这里使用forward保证它的右值属性
}
}
// 这里的insert是形如下面的定义
template<class T>
void insert(Iterator it, T&& t); // 用的万能引用
也就是说,如果我们希望传入右值引用还保持右值属性的话,就需要使用forward
转发
拓展
上面的结论是forward可以保持右值引用的属性,但是看了下面定义,发现还可以干点其他的
// 下面是forward的定义
template<typename _Tp>
constexpr _Tp&& // 返回类型是<T>里面T的右值引用
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
template<typename _Tp>
struct remove_reference
{ using type = __remove_reference(_Tp); }; // __remove_reference找不到具体定义,应该是编译器的,但是我们根据名字可以直到是去除引用属性的
有了上面的可以干嘛呢?其实引用符号是可以消掉的,比如&&&
等价于&
(但是明着这么写是会报错的,编译器处理的逻辑确实这样),也就是,奇数个引用都是左值引用,偶数个引用(不包括0)都是右值引用。
int a = 10;
// 下面两个是赋值,都不会报错
int b = forward<int&>(a);
int b = forward<int>(a);
int& b = forward<int>(a); // 报错的
int& b = std::forward<int&>(a); // 这个却不报错
int&& b = forward<int>(a); // 不报错
int&& b = forward<int&>(a); // 报错
int&& b = forward<int&&>(a); // 不报错
// 在代码中是不允许连续写多个 & 的,但是编译器转换其实又是采用这种方式
// 规律
// b前面的 & 数量的奇偶性需要和 <&> 里面的奇偶性保持一致,<&>里面为0也算是偶数
继续拓展
回到上面的万能函数
template<class T>
void F(T&& t);
F<int&>(a); // 不报错,编译器处理 T=int&, T&&=int&&&=int&
F<int&&>(1); // 也不报错,编译器处理 T=int&&, T&&=int&&&&=int&&
总结
量分为左值量和右值量,引用分为左值引用和右值引用。
- 左值量绑定到左值引用,可以
- 右值量绑定到左值引用,不可以(需要常左值引用修饰才可以,其实是为了兼容c++11以前的代码采取的措施)
- 左值量绑定到右值引用,不可以(要使用
move
转换,其作用是将左值变成右值) - 右值量绑定到右值引用,可以
- 右值引用也是一个左值量,需要使用
forward<T>(T t)
来将它转换成右值。
对于一些函数的写法:
template<class T>
class A{
void function(T&& t); // 右值引用
template<class U> void function_2(U&& u); // 嵌套模板,里层仍然是未知类型,还是万能引用
};
template<class T> void function(T&& t); // 万能引用
使用forward
的情景是:希望保持右值属性的时候使用它即可