引用变量
概念
简单理解就是对一个已存在的变量起别名,与那个已存在的变量共用一块内存空间。
用法:已存在变量的类型 & 引用变量名 = (引用实体)已存在变量
int main()
{
int a = 1;
int& b = a;
return 0;
}
在上面这个示例代码中,b是a的引用变量,我们可以通过a去输出1,也可以通过b来输出1,相当于给张三这个人起了个绰号叫张山,无论是叫张山还是张三代指都是一个人。
语法规定
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
引用与指针的区别
虽然在语法概念上引用就是一个,没有独立空间,和其引用实体共用同一块空间,但是底层实现是有空间,因为引用是按照指针方式来实现的
int main()
{
int a = 1;
int& ra = a;
int b = 2;
ra = b;
//这里只是把b的值赋值给ra,不是改变引用对象
//&ra = &b;//引用一旦给定初值无法改变方向
cout << b << endl;
cout << ra << endl;
//而指针可以改变指向
int* pa = &a;
pa = &b;
(*pa)++;
cout << b << endl;
return 0;
}
引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
引用的使用场景
传参
swap1是传值传参,在swap1中,a和b 不过是main函数的拷贝,所以改变swap1的a和b,不能改变main函数的a和b。而swap2是传址传参,在swap2中,a和b是main函数中a和b的别名,所以能够改变他,而且传引用传参不会发生拷贝构造,也会提高效率
常引用参数、临时变量
其实由上面的例子来看,swap1的传值引用会产生临时匿名对象,然后再拷贝到函数参数中,会调用该类的拷贝构造。
但是在某些传引用传参也会产生临时匿名对象(在被const修饰的前提之下,为什么?因为临时匿名对象具有常性,所以只有加const才能引用他)
- 实参的类型正确,但实参是右值
- 发生隐式类型转换
比如:
double refcube(const double &ra)
{
return ra*ra*ra;
}
double side = 3.0;
long edge = 5L;
double c1 = refcube(side); // ra is side
double c2 = refcube(edge);// edge是long类型,发生隐式类型转换
double c3 = refcube(7.0);// 7.0是字面量是右值
double c7 = refcube(side + 10.0); // side + 10.0是将亡值 也是右值的一种
总结:哪几种场景会产生临时变量??
- 隐形类型转换 比如:intb=0 float a=b b在赋值到a时候编译器会产生临时变量再去赋值给a
- 函数传值传参
- 函数返回值
函数返回
int& Add1(int a, int b)
{
int c = a + b;
return c;
}
int Add2(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret1 = Add1(1, 2);//ret1是Add1中c的引用
int ret2 = Add1(1, 2);//隐式类型转换
int ret3 = Add2(1, 2);//返回的时候把Add2的临时里面对象给拷贝到ret3
const int& ret4 = Add2(1, 2);//把他的临时匿名对象给引用了
cout << "ret1 is :" << ret1 << endl;
cout << "ret2 is :" << ret2 << endl;
cout << "ret3 is :" << ret3 << endl;
cout << "ret4 is :" << ret4 << endl;
return 0;
}
为什么得出以上结果?
ret1是Add1中c的引用,c是add函数中的变量,生命周期会在离开add函数的时候销毁,所以引用的虽然是c,但是c已经在函数返回的时候还给内存了,所以是随机值。
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
ret2是3 本质上其实是 int c = 3 返回变量 int temporary& = c; ret2 = temporary。其实也是拷贝构造
ret3是典型的传值返回,返回的时候把Add2的临时里面对象给拷贝到ret3
ret4是引用类型,它把Add2的返回值引用了,因为临时匿名对象具有常性,所以要用const修饰
右值引用
字面量、常量、变量
字面量:顾名思义就是我们人在读这个变量的,可以里面懂的他代指的含义。const int a = 16
16就是字面量,而被const修饰的只读的变量就是常量。如果a没有被const修饰那就是变量。
左值、右值、将亡值
左值:是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
右值:也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
将亡值:即将还给内存的值,一些临时匿名对象。
概念
之前学习的引用都是左值引用,而右值引用就是对右值起别名
右值引用语法: 引用类型 && 引用名 = 右值
右值引用的作用
其实左值引用也可以对右值取别名,比如:a+b
表达式是右值,我们可以这样写const int& ret2 = (a + b);
因为临时匿名对象具有常性,我们加上const就可以对他引用了,也可以使用右值引用 int&& ret2 = (a + b);
如果你要使用右值引用引用左值的话可以使用move函数,他会返回一个右值给引用变量int a = 1; int&& b = move(a);
其实对于已经存在左值引用的情况下,为什么要弄一个右值引用呢?
void func(const int& a)
{
cout << "void func(int& a)" << endl;
}
//void func(int&& a)
//{
// cout << "void func(int&& a)" << endl;
//}
int main()
{
int a = 0;
int b = 1;
func(a);
func(a + b);
}
void func(int& a)
{
cout << "void func(int& a)" << endl;
}
void func(int&& a)
{
cout << "void func(int&& a)" << endl;
}
int main()
{
int a = 0;
int b = 1;
func(a);
func(a + b);
}
右值引用的第一个意义是在之前只有左值引用的时候,如果我们对右值引用要在前面+const 去修饰参数,可读性不强,难区分左右值传参,而现在多了右值引用我们可以利用传参不同构成函数重载,来增加明确的可读性。
再看一段代码:
int main()
{
Jaxsen::string s1("hello");
Jaxsen::string ret1 = s1;
Jaxsen::string ret2 = (s1+'!');
return 0;
}
在ret1 = s1
上,本质是s1调用了拷贝构造把ret1深拷贝到ret1中,然后我们再去看ret2 = (s1 + '!' )
,首先s1 +'!'
是一个右值,对于这个右值我们是否有必要对他进行深拷贝呢?在ret1 = s1
我们之所以要做深拷贝是因为避免同一个变量指向同一块内存,导致我改变ret1,s1也会跟着改变。但是在s1 +'!'
这个右值表达式中产生出来的结果是一个将亡值,它的生命周期将会在这个表达式运算完就还给内存了,我们根本不用去担心访问冲突的关系。所以移动拷贝就出来了。
移动拷贝
什么是移动拷贝?
简单的理解就是把那些临时匿名对象,没有名字的变量,用右值引用变量管理那块要还给操作系统的内存。省去深拷贝,提高了效率。
注意:右值引用的根本作用不是减少拷贝,大部分的减少拷贝左值引用已经完成了
string operator+(char ch)
{
string tmp(*this);
tmp += ch;
return tmp;
}
在上面代码执行的时候,可以观察出ret2的地址和operator+中的tmp是一个地址。
左值引用解决直接减少拷贝,相当于传指针,不过没有指针那么复杂。而左值引用没有解决的是函数内的局部对象不能用引用返回的问题。
看下面场景:
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv;
vv.resize(numRows);//给定开辟有多少个vector<int>
//给每一个vector<int>开辟空间
for(int i = 0;i<vv.size();i++)
{
vv[i].resize(i+1);
}
//把杨辉三角的每一行的首元素和尾元素置为1
for(int i = 0;i<numRows;i++)
{
vv[i][0] = 1;
vv[i][vv[i].size()-1] =1;
}
//执行逻辑:上一行的前一位+上一行的当前位 =当前位
for(int i = 2;i < numRows;i++)
{
for(int j = 1;j < vv[i].size() - 1;j++)
{
vv[i][j] = vv[i-1][j-1]+vv[i-1][j];
}
}
return vv;
}
};
在这样的一个杨辉三角问题中,我们需要在generate函数中构建临时对象vv,然后还传值返回,在传值返回的时候深拷贝构建临时匿名对象,去复制给外面的接收又要深拷贝,而且这返回值根本不用担心访问冲突的问题,没有必要做深拷贝。所以右值这就是引用的第二个意义。
move
int main()
{
string s1("hello");
cout << "第一次赋值" << endl;
string s2 = s1;
cout << s1 << endl;
cout << s2 << endl;
cout << "第二次赋值" << endl;
string&& s3 = move(s1);
cout << s1 << endl;
cout << s3 << endl;
cout << "第三次赋值" << endl;
string s4 = move(s1);
cout << s1 << endl;
cout << s4 << endl;
return 0;
}
为什么会有这样的输出,请解释赋值语句的含义?
第一次赋值语句是将对象s1赋值给s2,发生深拷贝把s1的值拷贝一份给s2,相当于在内存中开多一块空间存放与s1一样的值,不过是s2管。
第二次赋值语句是将对象s1move成了右值,然后用右值引用变量去给该值起别名,s3和s1都有管理这片内存的权限,和左值引用没有什么区别。
第三次是s1变成右值交给s4去管理,发生了移动拷贝,把s1原本管理的内存交给s4去管理,s1被置为了null。所以不能轻易的把左值move右值然后又赋值给其他变量,这样原本的那个左值会失去对该内存的管理。
完美转发
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 万能引用(引用折叠):既可以引用左值,也可以引用右值
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
为什么是这个输出结果?
再看一个案例
int main()
{
int&& rr1 = 10;
cout << &rr1 << endl;
rr1++;
cout << rr1 << endl;
return 0;
}
10是字面量而rr1右值引用变量引用之后就可以取地址和做++运算,得出结论:当右值被右值引用变量引用之后就转化为了左值,或者说右值引用变量是左值。
为什么要这样设计呢?因为只有这样才可以对右值进行操作(移动构造)。
我们再回到上面那一题目,我们可以得出结论,在我们将右值作为函数参数传递给右值引用的时候,那个参数就变成了左值,所以输出的就是左值。
forward
含义:在传参的过程中保留对象原生类型属性
使用方法:forward<原生类型>(变量)
上面修改后:
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 万能引用(引用折叠):既可以引用左值,也可以引用右值
template<typename T>
void PerfectForward(T&& t)
{
Fun(forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
注意:在使用forward()的时候,要从最开始层层往下写下来。
移动赋值
和移动构造一样的原理:右值传参对该参数做移动,把这个生命周期极短的临时变量交给右值引用变量管理。
总结
右值引用解决了,左值引用中函数内变量不能引用返回需要强行深拷贝的场景和传右值作为参数,仍然使用深拷贝的场景。使用右值引用减少了很多没有必要的深拷贝,大大提高了效率