左值和右值的定义以及区别:
先来看在C语言中的左值和右值 :
左值:可赋值的值为左值
右值:不可赋值的值为右值
再来看C++中左值右值的定义如下:
左值:可以取地址的值(有名字的值、非临时量)
右值:不可取地址的值(没有名字的值、临时量)
左值引用:
int a = 10; //整型a = 10
int& b = a; //可以编译通过 左值引用就相当于变量的别名 const int a = 10; //左值引用字面常量的时候 改为常引用const就可以引用了//左值引用要求右值必须能够取地址,如果不能取地址,则必须为常引用右值引用:
int&& a = 10; //字面常量10为纯右值,可以编译通过
int&& b = a; //不可以编译通过 因为a是字面常量10的右值引用 a以及有名字了
说完了上面的定义来看下面这些代码:
//我们下面的String是一个了类
String fun()
{String s2("hello");return s2;
}String & fun()
{String s2("hello");return s2;
}String && fun()
{String s2("hello");return s2;
}
int main()
{ String s1;s1 = fun();
}
上面分别是用String,String &,String &&作为返回,通过下面的主函数去调用fun函数的时候哪些可以通过编译呢?
移动拷贝构造函数、移动赋值函数
#include<iostream>
#include<cstring>
using namespace std;class String
{char* str;public:String(const char* p = NULL) :str(NULL) //构造函数{cout << "construct :" << this << endl;if (p != NULL){str = new char[strlen(p) + 1];strcpy(str, p);}else{str = new char[1];*str = '\0';}}~String() //析构函数{cout << "destroy : " << this << endl;if (str != NULL){delete[] str;}str = NULL;}String(const String& s) :str(NULL) //拷贝构造函数{cout << "copy construct : " << this << endl;str = new char[strlen(s.str) + 1];strcpy(str, s.str);}char* Relese(char* p){char* old = str;str = p;return old;}String(String&& s){cout << " move copy construcgt :" << this << endl;str = s.str;s.str = NULL;}String& operator=(String&& s){if (this != &s){s.str = Relese(s.str);}cout << this << " move operator=: " << &s << endl;return *this;}
};String fun()
{String s2("hello");return s2;
}int main()
{String s1;s1 = fun();return 0;
}
先来看上述代码,创建s1对象,s1对象堆区申请空间,进入fun函数中,创建s2对象,s2堆区申请空间,return时将s2通过拷贝构造函数创建临时对象,临时对象申请空间,s2释放堆区空间,临时空间释放堆区空间,到main程序结束时,释放s1对象空间,下面来看上述代码执行结果:
上述代码表明了刚刚所推断的都是正确的,但是简单的几行代码对堆区进行多次操作,而我们想要达到的目的只是将对象s2中的字符串转移到对象s1中,于是乎移动拷贝构造函数、移动赋值函数就有了,顾名思义,它的作用是移动对象中的资源,并不是移动对象
结果如下:
上述调用移动拷贝构造和移动赋值具体如下:
1.s1对象创建,堆区申请空间给个'\0'
2.调用fun函数
3.调用拷贝构造函数创建s2对象,堆区申请空间
4.return时调用移动拷贝构造(这里为什么会调用移动拷贝构造呢,本来临时对象通过拷贝构造函数传参s2来构建对象,但是现在有移动拷贝构造函数(参数为右值引用),因为我们前面说过右值引用可以是临时量,所以这块就是临时对象调用移动拷贝构造将临时对象指针指向hello堆区,s2对象指针置为空,临时对象调用完移动拷贝构造之后,s2对象指针已经为NULL,s2对象被析构不会释放掉堆区内存)。----》对了,return s2底层调用了rerun std::move(s2),将s2强转为右值对象!
5.这一步相当于右值引用传参来重载等号运算符,也是一样的,因为临时对象给s1赋值,所以临时对象右值引用来进行传参,进入到移动赋值函数时,将s1对象指针指向hello堆区,并将临时对象指针置为NULL。
tips:这块为什么临时对象就会调用移动拷贝构造函数、移动赋值函数?因为临时对象符合右值引用的条件,如果你没有写出移动拷贝构造函数、移动赋值函数的话,就会调用普通的哪些函数!
还有一点上述那个代码中的s1对象指向堆区的'\0'会造成内存泄漏,所以得写一个Relese函数释放s1堆区空间!
移动拷贝构造函数、移动赋值函数就是为了减少对堆区的操作次数,仅仅移动对象的资源,并不是移动对象!