文章目录
右值引用(&&)提供了一个函数重载的概念。
void fun(int &&a)
void fun(int b)
void fun(const b)
左值和右值的区别:
- 右值不可以取地址,左值可以
- 不可以直接修改,可以通过右值引用来修改,左值可以
- 右值只能放在括号的右边,左值两边都能放
- 右值不具名 ,左值具名
首先:
1.什么是左值和右值
左值:
- 能够用&取地址的表达式是左值表达式
- 左值可以修改
- 左值可以放在等号的左右两边。(int a = 10;int b = a)
a就是左值。
如:函数名和变量名,返回左值引用的函数调用,前置自增、自减(++i,–i)由赋值运算符或符合运算符连接的表达式(a=b,a+=b,a%=b),解引用表达式*p,字符串字面值"abc"
右值:(纯右值和将亡值)
- 右值不可以取地址
- 不可以直接修改,可以通过右值引用来修改
- 右值只能放在括号的右边
- 右值不具名
如:
字面值,(3,false,12.23)
求值结果相当于字面值或者不具名的临时对象(值传递,函数的返回值,)
- i++/i–就是右值,举个例子,i = 6;i++得到结果为6,这个6是i+1之前的一个副本,没有名字,i不是它的名字,i的值此时是7.
- 解引用表达式*p是左值,而取地址表达式&a是纯右值。
&(*p)
是对的,因为 *p的p指向一个实体,&就是取这个实体的地址。
而对于&a
而言,本身就是地址,相当于一个字面值,不能再取地址的地址,就是纯右值。 - a+b,a&&b,a==b都是纯右值。 a+b得到的是一个不具名的临时对象来存储它的值,而其余返回的是true和false,相当于字面值。
将亡值:
【c11之前右值和纯右值等价,c11中将亡值随着右值引用的引入,而新引入的。】
换句话说:“将亡值”产生的概念是因为右值引用的产生而引起的。
- 将亡值表达式:
<1> 返回右值引用的函数的调用表达式
如:int a = f(3);f(3)的返回值是右值,副本拷贝给a,然后消失
<2> 转换为右值引用的转换函数的表达式。
表达式(x+y),(x+y)是右值
详细来说:
在C11中,我们用左值去初始化一个对象或者给一个已有对象赋值的时候,会调用拷贝构造函数或者拷贝赋值运算符来拷贝资源(所谓资源就是new出来的空间),而**当我们用右值来初始化或赋值,**调动的是移动构造函数和移动赋值运算符来移动资源,避免了拷贝。提高了效率。(下面还会提到)。
当该右值准备初始化和赋值时,他的资源已经转移给被初始化和被赋值者,同时将该右值马上析构。
从另一个角度说:当一个右值准备完成初始化和赋值这个动作的时候,已经处于将亡的状态了。上面两个表达式的结果都是不具名的右值引用,因此属于右值。
且:
1>这种右值是C11的新生事物——“右值引用”相关的“新右值”。
2>这种右值常用来完成移动构造和移动赋值的特殊任务,且扮演“将亡”的角色,因此被称为将亡值。
如:
int fun()
{
int a =10;
return a;
}
int main()
{
int x = 0;
x = fun();
}
对于fun()函数,在return时,并不是返回a本身,a在fun()函数结束时,生存期便结束了,a的值会被存储到临时量中,这个临时量就是将亡值。
左右值使用的原则:
- 右值可以赋给左值,左值不能赋给右值(左值权限更大)
如;
int a = 3;//a是左值,3是右值
int d = a; //d和a都是左值,左值可以作为右值
int &&d = a 错的//右值引用左值不可以
int &&d = 10 //右值引用右值可以
int &&d = f(10) //右值引用右值可以
- 右值无法被修改:
如:
int a = 10;// 10是右值常数,无法修改
- 编译允许左值建立引用,不允许右值建立引用
如:
int num = 10;
int &b = num; //正确
int &b = 10; 错误
右值引用:
既然左值可以修改,那么想要对右值修改怎么办?
----- 采用右值引用
(右值引用就是对一个右值进行引用的类型。)
(右值是不具名的,所以我们必须通过引用才能找到。)
左值右值必须初始化
无论声明左值引用还是右值引用都必须进行初始化,因为引用类型本身不拥有绑定对象的内存,只是该对象的别名。
如:
int &&a; 错误的,没有初始化
不能使用左值对右值引用进行初始化。
int num =10;
int && a= num; 错误
int&&a = 10; 正确
右值引用可以对右值进行修改,
int &&a =10;
a = 100; 将右值10修改为100;
可以看到,通过右值引用,既可以修改右值的值,换可以修改地址,从功能上,上升为左值。
右值引用的本质就是不用拷贝的左值
生存期延长:
通过右值引用的声明,可以让右值的声明周期延长,只要该变量还或者,该右值临时变量一会一直活下去。
右值引用的好处
函数传参 分为值传递和引用传递(不谈指针传递)。
两者相比,引用传递的优势就是通过传递地址,来减少一次拷贝,值传递需要将实参的值放到一个临时变量中,再将这个临时变量传给形参。
- 如果是传递对象(函数传参),换需要调用拷贝构造函数,来创建一个临时对象,进行传值。而引用传递,直接相当于给要传递的对象起了个别名,通过这个别名直接操作这个对象。就不需要调用拷贝构造函数。
- 如果是返回一个对象,如果是值的方式返回,也需要调用拷贝构造函数,创建一个临时对象空间,将该对象暂存,在返回到调用点处(调用了一次拷贝构造函数)
- 函数传参:int f(int &a)
- 函数返回值:int &f();
如果返回的是一个临时对象:
如下:
int &fun()
{
String s1;
return s1;
}
当返回对象s1的地址时,其时s1存在一个临时对象空间中,返回给调用点左值,就会被析构,如果外界对s1的地址进行访问,显然是访问不到的。这也是左值引用的一个弊端,而右值引用就是来解决这个问题。那么右值引用怎么解决临时的对象空间被析构呢?
当返回值为右值引用时,会把返回的临时对象中的内存据为己用,仍然保持有效性,避免了拷贝。
移动构造函数
一个函数返回一个class对象时,把本来调用拷贝构造函数,但经过编译器优化之后,没有调用拷贝构造函数?
如何优化:
不调用拷贝构造函数,而是调用移动拷贝构造函数。
语法:
类名(类名 && other)
{
…
}
拷贝构造函数
String(const String& other)
{
len = other.len;
str = new char[len + 1];
strcpy_s(str, len+1,other.str);
cout << "copy construct" << endl;
}
移动构造函数
//移动构造函数
String(String&& s)
{
cout << "move copy construct:" << endl;
str = s.str;
s.str = NULL;
}
普通赋值运算符重载
String &operator=(const String& s)//运算符重载
{
if (this == &s)
return *this;
// return;
delete[] str;
len = s.len;
str = new char[len + 1];
strcpy_s(str,len+1, s.str);
return *this;
// return;
}
移动赋值运算符
//移动赋值运算符
String& operator=(String&& s) //引用方式返回
{
if (this != &s)
{
delete[]str;
str = s.str;
s.str = NULL;
}
cout << this << "move operator=:" << &s << endl;
return *this;
}
在整个程序执行的过程中,只对堆内存空间new了一次,
- 首先s2调用构造函数完成对str的初始化,也就是s2指向为字符串“baiU”开辟的堆空间。
- return s2时,(调用的是移动构造函数)创建将亡值对象,指向 s2申请的空间,
- s2析构。
- 将亡值移动赋值给s1过程。
拷贝构造函数和移动构造函数对于指针的处理是不一样的
移动构造和拷贝构造和区别:
拷贝构造函数所做的是深拷贝,就是a拷贝到b中,需要在b中首先开辟一片空间在将a中的内容复制过去
移动构造函数干的是浅拷贝,就是将a中的指针直接复制到b中,同时要将a中的指针指向的位置改变。