目录
左值引用和右值引用:
首先,来分析以下左值引用:
int main()
{
int a = 10;
int &b = a; // 定义一个左值引用变量
b = 20; // 通过左值引用修改引用内存的值
return 0;
}
反汇编如下:
int a = 10;
// 这条mov指令把10放到a的内存中
00354218 mov dword ptr [a],0Ah
int &b = a;
/* 下面的lea指令把a的地址放入eax寄存器
mov指令把eax的内容放入b内存里面
*/
0035421F lea eax,[a]
00354222 mov dword ptr [b],eax
b = 20;
/* 下面的mov指令把b内存的值放入eax寄存器(就是a的地址)
mov指令再把20放入eax记录的地址的内存里面(就是把20赋值给a)
*/
00354225 mov eax,dword ptr [b]
00354228 mov dword ptr [eax],14h
仔细比较其实可以发现,定义一个左值引用和定义一个指针在反汇编中是没有任何区别的。
考虑这句代码:
int &b = 20;
发现是不能通过编译的。但是改成下面的形式就可以通过编译了。
const int& b =20;
类比前面说的,左值引用和取地址是没有区别的,上述第一句代码编译不能通过可以理解理解为20是不能取地址的,第二句代码通过就可以认为加了一个const,20就可以去抵制了????当然不可以这样认为。是因为20产生了一个临时变量,临时变量给b进行赋值的,相当于下面的操作:
/*
这里temp是在内存上产生的临时量
const int temp = 20;
const int &b = temp;
*/
const int &b = 20;
通过反汇编可以进行进一步的确认:
const int &b = 20;
010517C8 mov dword ptr [ebp-14h],14h 《= ebp-14h就是内存栈上产生的临时量的内存地址
010517CF lea eax,[ebp-14h] 《= 取临时量的内存地址放入寄存器eax
010517D2 mov dword ptr [b],eax 《= 再把eax寄存器的值(放的是临时量地址)存入b中
通过上述的关于左值引用的分析我们可以得到这样的一些结论:左值引用必须要求右边的表达式能够取地址,如果无法取地址,就需要用常引用,但这样以来,就无法对右边表达式的值进行修改。
解决的办法就是采用右值引用。首先有下面的一些代码
int &&b = 20; // 通过指令可以看到,原来const int &b=20和int &&b=20一模一样!!!
这里mov指令相当于是产生了临时量,起始地址ebp-14h
00CA18B8 mov dword ptr [ebp-14h],14h
把临时量的地址放入eax寄存器当中
00CA18BF lea eax,[ebp-14h]
再把eax的值(临时量的地址)放入b内存中(一个指针大小的内存)
00CA18C2 mov dword ptr [b],eax
b = 40;
00CA18C5 mov eax,dword ptr [b]
00CA18C8 mov dword ptr [eax],28h
通过上述的反汇编代码我们可以得出:const int &b =20;和 int &&b =20;在底层是没有任何区别滴,只是int &&b = 20可以对数据进行修改。
所以,有地址的用左值引用,没有地址的用右值引用;有名字的用左值引用,没有名字的(如临时变量)用右值引用。
右值引用:面对对象的效率问题
class Stack
{
public:
// size表示栈初始的内存大小
Stack(int size = 1000)
:msize(size), mtop(0)
{
cout << "Stack(int)" << endl;
mpstack = new int[size];
}
// 栈的析构函数
~Stack()
{
cout << "~Stack()" << endl;
delete[]mpstack;
mpstack = nullptr;
}
// 栈的拷贝构造函数
Stack(const Stack &src)
:msize(src.msize), mtop(src.mtop)
{
cout << "Stack(const Stack&)" << endl;
mpstack = new int[src.msize];
memcpy(mpstack, src.mpstack, sizeof(int)*mtop);
}
// 栈的赋值重载函数
Stack& operator=(const Stack &src)
{
cout << "operator=" << endl;
if (this == &src)
return *this;
delete[]mpstack;
msize = src.msize;
mtop = src.mtop;
mpstack = new int[src.msize];
memcpy(mpstack, src.mpstack, sizeof(int)*mtop);
return *this;
}
// 返回栈的长度
int getSize()const { return msize; }
private:
int *mpstack;
int mtop;
int msize;
};
Stack GetStack(Stack &stack)
{
// 这里构造新的局部对象tmp
Stack tmp(stack.getSize());
/*
因为tmp是函数的局部对象,不能出函数作用域,
所以这里tmp需要拷贝构造生成在main函数栈帧上
的临时对象,因此这里会调用拷贝构造函数,完成
后进行tmp局部对象的析构操作
*/
return tmp;
}
int main()
{
Stack s;
/*
GetStack返回的临时对象给s赋值,该语句结束,临时对象
析构,所以此处调用operator=赋值重载函数,然后调用
析构函数
*/
s = GetStack(s);
return 0;
}
程序运行结果:
右值引用的拷贝构造和赋值重载:
可以在上述代码中添加右值引用的拷贝构造和赋值重载:
// 带右值引用参数的拷贝构造函数
Stack(Stack &&src)
:msize(src.msize), mtop(src.mtop)
{
cout << "Stack(Stack&&)" << endl;
/*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
mpstack = src.mpstack;
src.mpstack = nullptr;
}
// 带右值引用参数的赋值重载函数
Stack& operator=(Stack &&src)
{
cout << "operator=(Stack&&)" << endl;
if(this == &src)
return *this;
delete[]mpstack;
msize = src.msize;
mtop = src.mtop;
/*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
mpstack = src.mpstack;
src.mpstack = nullptr;
return *this;
}
代码运行结果会变为:
Stack(int)
Stack(int)
Stack(Stack&&) =》对应return tmp; 自动调用带右值引用参数版本的拷贝构造
~Stack()
operator=(Stack&&) =》 s = GetStack(s); 自动调用带右值引用参数的赋值重载函数
~Stack()
~Stack()
从上面打印的函数可以清楚的看到:上面的拷贝构造函数和赋值函数都调用了右值引用参数的版本。效率大大的提升,因为没有涉及到任何内存的开辟和数据的拷贝。因为临时对象马上就要析构了,直接把临时对象的资源拿过来用就可以了。
带右值引用参数的拷贝构造函数和复值函数又叫移动构造函数和移动赋值函数,这里的移动是指把临时量的资源移动给了当前的对象,没有发生任何新内存的开辟和数据的拷贝。
面试题:
#include <iostream>
#include <vector>
using namespace std;
class A
{
public:
A(int data=10):ptr(new int(data)) {}
~A() { delete ptr; ptr = nullptr; }
A(const A &src)
{
cout << "A(const A&)" << endl;
ptr = new int(*src.ptr);
}
A(A &&src)
{
cout << "A(A&&)" << endl;
ptr = src.ptr;
src.ptr = nullptr;
}
private:
int *ptr;
};
int main()
{
vector<A> vec;
vec.reserve(2);
A a;
vec.push_back(a); // 调用哪个构造函数?
vec.push_back(A(20)); // 调用哪个构造函数?
return 0;
}
很明显 vec.push_back(a)调用的是带左值引用参数的构造函数,而vec.push_back(A(20))调用的是带右值引用参数的拷贝构造函数。
代码打印结果如下:
A(const A&)
A(A&&)
函数返回容器:
vector<A> getVector()
{
vector<A> vec;
vec.reserve(3);
vec.push_back(A(20));
vec.push_back(A(30));
vec.push_back(A(40));
cout << "————————" << endl;
/*
这里返回vec时,会调用vector容器的带右值引用参数的拷贝构造函数,
类似vector(vector &&src),直接把这里vec的资源移动给main函数
中的v,效率很高,也就是说函数在返回容器的过程中,没有做任何的内存和
数据开销
*/
return vec;
}
int main()
{
vector<A> v = getVector();
return 0;
}
打印结果如下:
A(A&&)
A(A&&)
A(A&&)
可以得到,vector<A> v getVector()没有做任何数据拷贝的事情,因此大大的提高了效率。
引用折叠:
以下一段代码有助于更加深刻的理解右值引用:
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;
}
引用折叠的概念主要用在函数模板参数的推导之中;
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©: //T tmp = val; T是int&
01 val:10
02 val:11 tmp:11
代码解析:int&& + &&折叠成int&&,除此之外,其余的都折叠成int&.
std::move移动语义
class A
{
public:
A(int data=10):ptr(new int(data)) {}
~A() { delete ptr; ptr = nullptr; }
A(const A &src)
{
cout << "A(const A&)" << endl;
ptr = new int(*src.ptr);
}
A(A &&src)
{
cout << "A(A&&)" << endl;
ptr = src.ptr;
src.ptr = nullptr;
}
private:
int *ptr;
};
int main()
{
vector<A> vec;
vec.reserve(10);
for (int i = 0; i < 10; ++i)
{
A a(i);
/*
这里a是一个左值,因此vec.push_back(a)会调用左值的
拷贝构造函数,用a拷贝构造vector底层数组中的对象
*/
vec.push_back(a);
}
return 0;
}
代码运行如下:
A(const A&)
A(const A&)
A(const A&)
A(const A&)
A(const A&)
A(const A&)
A(const A&)
A(const A&)
A(const A&)
A(const A&)
右运行结果分析可得,调用了10次左值的构造函数。有 A a(i)是局部对象,当进行一次push_back后,它所持有的资源就会被释放掉了,因此这里不如用右值引用,直接将所持有的资源移动到vector下面即可。
可以将上述的部分代码修改如下:
int main()
{
vector<A> vec;
vec.reserve(10);
for (int i = 0; i < 10; ++i)
{
A a(i);
/*
由于a马上就会销毁,因此这里应该用右值引用参数
的拷贝构造函数,效率会更高
*/
vec.push_back(std::move(a));
}
return 0;
}
代码运行结果为:
A(A&&)
A(A&&)
A(A&&)
A(A&&)
A(A&&)
A(A&&)
A(A&&)
A(A&&)
A(A&&)
A(A&&)
vec.push_back(std::move(a))会应用到对象的右值引用的拷贝构造函数。std::move函数的作用救赎返回传入参数的右值引用。
从std::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 完美转发
下面的代码,实现了一个简单的vector,用来描述forward的应用场景,示例代码如下:
// 容器里面元素的类型
class A
{
public:
A(){}
// 带左值引用参数的赋值函数
A& operator=(const A &src)
{
cout << "operator=" << endl;
return *this;
}
// 带右值引用参数的赋值函数
A& operator=(A &&src)
{
cout << "operator=(A&&)" << endl;
return *this;
}
};
// 容器的类型
template<typename _Ty>
class Vector
{
public:
// 引用左值的push_back函数
void push_back(const _Ty &val)
{
addBack(val);
}
// 引用右值的push_back函数
void push_back(_Ty &&val)
{
// 这里传递val时,要用move转换成右值引用类型,
// 因为val本身是左值,有名字有地址,见前面引用折叠部分的说明
addBack(std::move(val));
}
private:
enum { VEC_SIZE = 10 };
_Ty mvec[VEC_SIZE];
int mcur;
template<typename _Ty>
void addBack(_Ty &&val)
{
/*
这里val本身永远是左值,所以不可能调用
容器内部对象的右值引用参数的operator=赋值函数
*/
mvec[mcur++] = val;
}
};
int main()
{
Vector<A> vec;
A a;
vec.push_back(a); // 调用A的左值引用的赋值函数
vec.push_back(A()); // 理应调用A的右值引用参数的赋值函数,却调用了左值引用的赋值函数
return 0;
}
代码运行结果如下:
operator=
operator=
vec.push_back(A())传入的是临时对象,最终却没有调用右值版本的赋值函数,是因为在addBack函数中val被永远的当作左值了,引起了折叠问题,所以就没有办法保证它传入参数的类型了,因此就不能保证右值引用和左值引用了。此时,std::forward就要起作用了,它称为完美转发,它可以保证实参的左值引用和右值引用类型:
template<typename _Ty>
void addBack(_Ty &&val)
{
/*
这里使用std::forward,可以获取val引用的实参的引用类型,
是左引用,还是右引用,原理就是根据“引用折叠规则”
int&+&&->int& int&&+&&->int&&
*/
mvec[mcur++] = std::forward<_Ty>(val);
}
再次运行上述代码可得:
operator=
operator=(A&&)
最终,vec.push_back(A())最终调用了对象的右值引用赋值函数,符合预期,因为在addBack中添加了完美转发机制。