目录
左值引用:
左值引用使用&
符号进行声明,例如int&
或const T&
,其中T
是任意类型。
1. 左值引用可以做函数参数:
void increment(int& num) { num++; } int main() { int value = 10; increment(value); // 传递左值给函数 // 现在value的值变为11 return 0; }
在这个示例中,
increment
函数接受一个左值引用参数,并对其进行递增操作。2. 左值引用可以用于对象成员变量:
class MyClass { public: int& getValue() { return value; } private: int value = 10; }; int main() { MyClass obj; obj.getValue() = 20; // 使用左值引用返回的对象成员变量进行赋值 // 现在obj中的value变为20 return 0; }
在这个示例中,
getValue
函数返回一个左值引用,允许我们直接修改对象的成员变量。3. 用于修改被引用对象,除非引用本身是const的
int main() { int value = 10; int& ref = value; // 引用类型的变量ref绑定到value的左值 ref = 20; // 修改ref的值,也会修改value的值 // 现在value的值变为20 return 0; }
在这个示例中,我们声明了一个引用类型的变量
ref
,将其绑定到value
的左值,并可以通过引用修改value
的值。
在汇编指令上看,定义一个左值引用在汇编指令上和定义一个指针是没有任何区别的,定义一个引用变量int& ref = value, 是必须初始化的,因为指令上需要把右边value的地址放到左边的ref的内存里(相当于定义了一个指针的内存),当给引用变量ref赋值时,指令从ref里面取出value的地址,并更改内容。所以在汇编指令层面,引用和指针的操作没有任何区别!
int &b = 20; //无法执行
上述代码无法执行,因为需要从20取地址放到b的地址中,但是20是数字没有在内存上的存储,因此无法取地址,但是可以:
const int &b = 20;
用常引用可以解决,这时内存上产生了一个临时变量保存了20,b现在引用的是这个临时变量,所以可行,但是这样就只能读取数据,无法修改数据。
综上所述,左值引用必须要求右边的值必须能够取地址,如果无法取地址,可以用常引用,但是用了常引用只能读取无法修改,这时需要右值引用。
const int &b=20和int &&b=20在底层指令上是一模一样的,都需要产生临时量,然后保存临时量的地址,没有任何区别,不同的是,通过右值引用变量,可以进行读操作,也可以进行写操作。有地址的用左值引用,没有地址的用右值引用;有变量名字的用左值引用,没有变量名字的(比如临时量没有名字)用右值引用。
右值引用:
C++11提供带右值引用参数的拷贝构造函数和operator=赋值重载函数。使用这两个函数不涉及内存的开辟和数据拷贝,临时对象马上要析构了,直接把临时对象持有的资源拿过来就行了。临时量都会自动匹配右值版本的成员方法,提高内存的资源使用效率。
带右值引用参数的拷贝构造和赋值重载函数,又叫移动构造函数和移动赋值函数,这里的移动指的是把临时量的资源移动给了当前对象,临时对象就不持有资源,为nullptr了,实际上没有进行任何的数据移动,没发生任何的内存开辟和数据拷贝。
引用折叠:
在模板编程中,处理参数传递和类型推导
int main()
{
int a = 10;
int &b = a;
//int &&c = a; // 错误,无法将左值a绑定到右值引用c
//int &&d = b; // 错误,无法将左值b绑定到右值引用d
int &&e = 20; // 正确,20是一个右值(没地址没名字),可以绑定到右值引用e上
//int &&f = e; // 错误,无法将左值e绑定到右值引用f,因为e有名字,有地址,本身也是左值
int &g = e; // 正确,e本身有名字,有地址,是一个左值,可以被g引用
return 0;
}
右值引用变量e本身是一个左值
template<typename T> void func(T&& val) { cout << "01 val:" << val << endl; T tmp = val; tmp++; cout << "02 val:" << val << " tmp:" << tmp << endl; } int main() { int a = 10; int &b = a; int &&c = 10; cout << "func(10):" << endl; func(10);// 10是右值,引用类型是int&&,T&&推导过程是int&&+&&折叠成int&&,所以T是int,下同 cout << "func(a):" << endl; func(a);// a是左值,不可能用右值引用来引用,所以func推导T为int&,那么T&&->int&+&&折叠成int& cout << "func(std::move(a)):" << endl; func(std::move(a)); // std::move(a)是把a转成右值类型,右值引用类型是int&&,所以func推导T为int cout << "func(b):" << endl; func(b);// b是左值,不可能用右值引用来引用,所以func推导T为int&,那么T&&->int&+&&折叠成int& cout << "func(c):" << endl; func(c);// c是左值,不可能用右值引用来引用,所以func推导T为int&,那么T&&->int&+&&折叠成int& return 0; }
代码运行打印如下:
func(10): //T tmp = val; T是int
01 val:10
02 val:10 tmp:11
func(a): //T tmp = val; T是int&
01 val:10
02 val:11 tmp:11
func(std::move(a)): //T tmp = val; T是int
01 val:11
02 val:11 tmp:12
func(b): //T tmp = val; T是int&
01 val:11
02 val:12 tmp:12
func(c): //T tmp = val; T是int&
01 val:10
02 val:11 tmp:11引用折叠,就是int && + &&折叠成int&&,除此之外,都折叠成int&,如int& + &&折叠成int&.
std::move移动语义
move就是返回传入的实参的右值引用类型,做了一个类型强转,move代码:
template<class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&&
move(_Ty&& _Arg) noexcept
{ // forward _Arg as movable
return (static_cast<remove_reference_t<_Ty>&&>(_Arg));
}
std::forward完美转发
如果直接将参数传递给其他函数,参数的值类别(左值还是右值)可能会发生改变,导致无法实现完美传递。
template<typename _Ty> void addBack(_Ty &&val) { /* 这里使用std::forward,可以获取val引用的实参的引用类型, 是左引用,还是右引用,原理就是根据“引用折叠规则” int&+&&->int& int&&+&&->int&& */ mvec[mcur++] = std::forward<_Ty>(val); } int main() { Vector<A> vec; A a; vec.push_back(a); // 调用A的左值引用的赋值函数 vec.push_back(A()); // 理应调用A的右值引用参数的赋值函数,却调用了左值引用的赋值函数 return 0; }
//打印结果: operator= operator=(A&&)
std::forward函数的作用就是根据输入参数的值类别,将参数以相同的值类别转发给其他函数。如果输入参数是左值,那么转发时将保持左值,如果输入是右值,那么转发时是右值引用。
C++库里面提供的两个forward重载函数,分别接收左值和右值引用类型,进行一个类型强转,(static_cast<_Ty&&>(_Arg)), 如果实参类型是int& + && -> int&就保持了实参的左值引用类型,如果实参类型是int&& + && -> int&&就保持了实参的右值引用类型。
【总结】:std::move是获取实参的右值引用类型;std::forward是在代码实现过程中,保持实参的原有的引用类型(左引用或者右引用类型)。