左值引用
int a = 4;
int &la = a; //定义一个引用变量 -- 就是左值引用
la = 100; //通过引用改变内存空间中变量a的值
左值引用在汇编层面其实和普通的指针是一样的;定义引用变量必须初始化,因为引用其实就是一个别名,&就是告诉编译器定义的是谁的引用。
int &a = 100; //这样是错误的
在C++中这样无法编译通过的,因为100无法进行取地址操作。可以通过下述方法解决:
const int temp = 10;
const int &val = temp;
结论
左值引用(C++ 98标准) 要求右边的值必须能取地址;如果无法取地址,可以用常引用;
但使用常引用后,我们只能通过引用来读取数据,无法修改,因为其被const修饰成常量引用了。这样多少有点不方便
为了解决上面的问题,以自C++11 开始就有了右值引用。
右值引用
C++对于左值和右值没有标准定义。
可以这样来理解
- 可以取地址的,有名字的,非临时的就是左值;
- 不能取地址的,没有名字的,临时的就是右值;
函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值。
定义右值引用的格式:
类型 && 引用名 = 右值表达式;
右值引用是C++ 11新增的特性,所以C++ 98的引用为左值引用。右值引用用来绑定到右值,绑定到右值以后,本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期。
int &&var = 10;
在汇编层面右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是,右值引用可以进行读写操作,而常引用只能进行读操作。
右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的构造来减少对象构造和析构操作以达到提高效率的目的。
class Stack
{
public:
// 构造
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];
for (int i = 0; i < mtop; ++i) {
mpstack[i] = src.mpstack[i];
}
}
// 赋值重载
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];
for (int i = 0; i < mtop; ++i) {
mpstack[i] = src.mpstack[i];
}
return *this;
}
int getSize()
{
return msize;
}
private:
int *mpstack;
int mtop;
int msize;
};
Stack GetStack(Stack &stack)
{
Stack tmp(stack.getSize());
return tmp;
}
int main()
{
Stack s;
s = GetStack(s);
return 0;
}
输出:
Stack(int) // 构造s Stack(int) // 构造tmp Stack(const Stack&) // tmp拷贝构造main函数栈帧上的临时对象 ~Stack() // tmp析构 operator= // 临时对象赋值给s ~Stack() // 临时对象析构 ~Stack() // s析构
由此可见:
执行 s = GetStack(s); 时,包含了两个操作,先把tmp拷贝给一个临时变量,然后再把这个变量赋值给s。
为了解决浅拷贝问题,为类提供了自定义的拷贝构造函数和赋值运算符重载函数,并且这两个函数内部实现都是非常的耗费时间和资源(首先开辟较大的空间,然后将数据逐个复制),我们通过上述运行结果发现了两处使用了拷贝构造和赋值重载,分别是tmp拷贝构造main函数栈帧上的临时对象、临时对象赋值给s,其中tmp和临时对象都在各自的操作结束后便销毁了,使得程序效率非常低下。
那么我们为了提高效率,是否可以把tmp持有的内存资源直接给临时对象?是否可以把临时对象的资源直接给s?
在C++11中,我们可以解决上述问题,方式是提供带右值引用参数的拷贝构造函数和赋值运算符重载函数.
// 带右值引用参数的拷贝构造函数
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) // 构造s Stack(int) // 构造tmp Stack(Stack&&) // 调用带右值引用的拷贝构造函数,直接将tmp的资源给临时对象 ~Stack() // tmp析构 operator=(Stack&&) // 调用带右值引用的赋值运算符重载函数,直接将临时对象资源给s ~Stack() // 临时对象析构 ~Stack() // s析构
可以直接赋值的原因是临时对象即将销毁,不会出现浅拷贝的问题,我们直接把临时对象持有的资源赋给新对象就可以了。
所以,临时量都会自动匹配右值引用版本的成员方法,旨在提高内存资源使用效率。