左值引用(&)
左值引用的定义格式:
类型 & 引用名 =左值表达式;
int a = 10;
int &b = a; //定义一个左值引用变量
b = 20; //通过左值引用修改引用内存的值
左值引用在汇编层面其实和普通的指针是一样的;定义引用变量必须初始化,因为引用其实就是一个别名,需要告诉编译器定义的是谁的引用。
int &var = 10;//编译报错
上述代码是编译报错,因为10无法进行取地址操作,无法对一个立即数取地址,因为立即数并没有在内存中存储,而是存储在寄存器中,可以通过下述方法解决:
const int &var = 10;
使用常引用来引用常量数字10,因为此刻内存上产生了临时变量保存了10,这个临时变量是可以进行取地址操作的,因此var引用的其实是这个临时变量,相当于下面的操作:
const int temp = 10;
const int &var = temp;
根据上述分析,得出如下结论:
左值引用要求右边的值必须能够取地址,如果无法取地址,可以用常引用;但使用常引用后,我们只能通过引用来读取数据,无法去修改数据,因为其被const修饰成常量引用了。
右值引用(&&)
右值引用的定义格式:
类型 && 引用名 = 右值表达式;
C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:
- 可以取地址的,有名字的,非临时的就是左值;
- 不能取地址的,没有名字的,临时的就是右值;
从本质上理解,创建和销毁由编译器幕后控制,程序员只能确保在本行代码有效的,就是右值(如立即数、函数返回的值);而用户创建的,通过作用域规则可知其生存期的,就是左值(如非匿名对象、函数返回的局部变量的引用以及const对象)。
int &&var = 10;
右值引用用来绑定到右值,绑定到右值以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期。右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是,右值引用可以进行读写操作,而常引用只能进行读操作。
右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的构造来减少对象构造和析构操作以达到提高效率的目的,如:
#include<iostream>
using namespace std;
class Stack
{
private:
int *m_stack;
int m_size;
public:
int getSize(){return m_size;}
public:
// 构造
Stack(int size = 1000)
:m_size(size)
{
cout << "Stack(int size = 1000)" << endl;
m_stack = new int[size];
}
// 析构
~Stack()
{
cout << "~Stack()" << endl;
delete[] m_stack;
m_stack = nullptr;
}
// 拷贝构造
Stack(const Stack& src)
:m_size(src.m_size)
{
cout << "Stack(const Stack& src)" << endl;
m_stack = new int[src.m_size];
for (int i = 0; i < src.m_size; i++) {
m_stack[i] = src.m_stack[i];
}
}
// 赋值重载
Stack& operator=(const Stack& src)
{
cout << "Stack& operator=(const Stack& src)" << endl;
if (this == &src)
return *this;
delete[] m_stack;
m_size = src.m_size;
m_stack = new int[src.m_size];
for (int i = 0; i < src.m_size; ++i) {
m_stack[i] = src.m_stack[i];
}
return *this;
}
};
Stack GetStack(Stack& stack)
{
Stack temp(stack.getSize());
return temp;
}
int main()
{
Stack s;
s = GetStack(s);
return 0;
}
运行结果为:
Stack(int size = 1000)//构造s
Stack(int size = 1000)//构造temp
Stack(const Stack& src)//拷贝temp,构造临时对象
~Stack()//析构temp
Stack& operator=(const Stack& src)//临时对象赋值给s
~Stack()//析构临时对象
~Stack()//析构s
上述自定义的拷贝构造函数和赋值运算符重载函数调用时会先开辟较大的存储空间,然后将数据逐个复制,复制完毕后再将原始对象销毁,这需要耗费大量的时间和资源,使得程序效率非常低下。对此,是否可以把原始对象持有的内存资源直接给新的对象呢?答案是肯定的,右值引用可以很好的实现上述功能。
#include<iostream>
using namespace std;
class Stack
{
private:
int *m_stack;
int m_size;
public:
int getSize(){return m_size;}
public:
// 构造
Stack(int size = 1000)
:m_size(size)
{
cout << "Stack(int size = 1000)" << endl;
m_stack = new int[size];
}
// 析构
~Stack()
{
cout << "~Stack()" << endl;
delete[] m_stack;
m_stack = nullptr;
}
// 拷贝构造
Stack(Stack&& src) noexcept
:m_size(src.m_size)
{
cout << "Stack(Stack&& src)" << endl;
/*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
m_stack = src.m_stack;
src.m_stack = nullptr;
}
// 赋值重载
Stack& operator=(Stack&& src) noexcept
{
cout << "operator=(Stack&&)" << endl;
if (this == &src)
return *this;
delete[] m_stack;
m_size = src.m_size;
/*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
m_stack = src.m_stack;
src.m_stack = nullptr;
return *this;
}
};
Stack GetStack(Stack& stack)
{
Stack temp(stack.getSize());
return temp;
}
int main()
{
Stack s;
s = GetStack(s);
return 0;
}
Stack(int size = 1000)//构造s
Stack(int size = 1000)//构造temp
Stack(Stack&& src)//右值引用的拷贝,直接将temp的资源给临时对象
~Stack()//析构temp
Stack& operator=(Stack&& src)//右值引用的赋值,直接将临时对象资源赋值给s
~Stack()//析构临时对象
~Stack()//析构s
上述自定义的右值引用拷贝构造函数和右值引用赋值运算符重载函数调用时直接将原始对象资源给新对象,避免了重新开辟内存,拷贝数据,提高了程序的效率。
可能有的小伙伴会问,m_stack = src.m_stack为什么可以直接赋值,那是因为临时对象即将销毁,不会出现浅拷贝的问题。
带右值引用参数的拷贝构造和赋值重载函数,又叫移动构造函数和移动赋值函数,这里的移动指的是把临时量的资源移动给了当前对象,临时对象就不持有资源,为nullptr了,实际上没有进行任何的数据移动,没发生任何的内存开辟和数据拷贝。
引用的绑定对象
左值引用:
非常量左值引用只能绑定到非常量左值上;常量左值引用可以绑定到非常量左值、常量左值、非常量右值、常量右值等所有的值类型。
int num = 10;
int& a = num; //编译成功,非常量左值引用支持引用非常量左值
const int num2 = 100;
int& b = num2; //编译失败,非常量左值引用不支持引用常量左值
int& c = 10; //编译失败,非常量左值引用不支持引用右值
const int& d = num; //编译成功,常量左值引用支持引用非常量左值
const int& e = num2; //编译成功,常量左值引用支持引用常量左值
const int& f = 100; //编译成功,常量左值引用支持引用右值
右值引用:
非常量右值引用只能绑定到非常量右值上;常量右值引用可以绑定到非常量右值、常量右值上。
int num = 10;
const int num2 = 100;
int&& a = num; //编译失败,非常量右值引用不支持引用非常量左值
int&& b = num2; //编译失败,非常量右值引用不支持引用常量左值
int&& c =10; //编译成功,非常量右值引用支持引用非常量右值
const int&& d = num; //编译失败,常量右值引用不支持引用非常量左值
const int&& e = num2; //编译失败,常量右值引用不支持引用常量左值
const int&& f = 100; //编译成功,常量右值引用支持引用右值
move函数:
常量左值引用可以绑定到右值上,但右值引用不能绑定任何类型的左值,C++11中提供了一个标准库move函数获得绑定到左值上的右值引用,即直接调用std::move告诉编译器将左值像对待同类型右值一样处理,但是被调用后的左值将不能再被使用。
int n = 10;
int&& x = std::move(n); //编译成功
cout << x << endl; //输出结果为10;