C++11前没有右值引用的说法,涉及到拷贝构造和赋值时,过程如下:
#include<iostream>
#include <string.h>
using namespace std;
class String
{
public:
//构造函数
String(const char *ptr = nullptr){
std::cout << "String(const char *ptr)" << std::endl;
if (ptr == nullptr){
mpstr = new char[1];
mpstr[0] = '\0';
}
else{
mpstr = new char[strlen(ptr) + 1];
strcpy(mpstr, ptr);
}
}
//拷贝构造函数
String(const String &src){
std::cout << "String(const String &src)" << std::endl;
//delete[]mpstr;
mpstr = new char[strlen(src.mpstr) + 1];
strcpy(mpstr, src.mpstr);
}
//赋值运算符的重载函数
String& operator=(const String &src){
std::cout << "String& operator=(const String &src)" << std::endl;
if (this == &src)
return *this;
delete[]mpstr;
mpstr = new char[strlen(src.mpstr) + 1];
strcpy(mpstr, src.mpstr);
return *this;
}
~String(){
std::cout << "~String()" << std::endl;
delete[]mpstr;
mpstr = nullptr;
}
private:
char *mpstr;
};
String GetString(){
String tmp = "9999999";
return tmp;
}
int main(){
String str1 = "112345";
String str2;
str2 = GetString();
return 0;
}
在Visual Studio 2013运行的结果如下,在不同编译器结果可能有所差异:
String(const char *ptr)
String(const char *ptr)
String(const char *ptr)
String(const String &src)
~String()
String& operator=(const String &src)
~String()
~String()
~String()
请按任意键继续. . .
梳理一下上面main函数调用过程中的构造函数和赋值函数的过程:
- 调用构造函数构造出str1对象;
String(const char *ptr)
- 调用构造函数构造出str2对象;
String(const char *ptr)
- 调用构造函数构造出GetString函数栈帧tmp对象,这里需要注意,由于函数返回自定义类型,编译器会提前在main函数栈帧开辟一份空间,用来存放将来函数返回到main函数栈帧的临时对象,该空间的内存地址通过GetString参数的形式传递给GetString,只不过我们开发者不用关心;
String(const char *ptr)
- GetString函数return处,将会调用拷贝构造函数构造main函数提前为临时对象开辟的那一块内存,生成对象;
String(const String &src)
- GetString函数调用完成,栈帧回退,tmp对象析构;
~String()
- main函数栈帧上的临时对象赋值给已经存在的对象str2;
String& operator=(const String &src)
- 赋值语句结束,临时对象生命周期到,被析构;
~String()
- main函数出函数右括号,栈帧上的对象生命周期将结束,分别析构str1,
~String()
,再析构str2,~String()
.
分析拷贝构造过程:
上述过程中,涉及GetString拷贝构造main函数栈帧上临时对象时有一个很大的缺点,那就是实际上我们可以直接将GetString栈帧上对象外部引用的堆内存数据直接引用过来,不需要我们在main函数栈帧上重新开辟内存,然后再把GetString上的tmp对象中的数据挨个拷贝到main函数,最后GetString函数回退又把tmp对象析构掉,是不是多此一举了,干嘛不直接将数据给将要构造的新对象,我们最终的目的是想将临时对象所引用的堆内存去构造其他新对象,避免多次数据的拷贝,空间的开辟等开销,临时对象的任务也就完成了:
//我们希望拷贝构造时是这样的
mpstr = tmpobject.mpstr;
//直接将临时对象引用数据交给目标对象即可,避免重复内存开辟和数据的copy
tmpobject.mpstr = nullptr;
//GetString上的tmp对象出作用域析构时:析构函数什么都不做,因为他的资源已经
//交给了拷贝构造生成的新对象,并且自己的指针被置为nullptr.
分析赋值运算时的过程:
删除原来的str2空间,然后重新开辟一块与tmp object一样size的堆空间,然后再挨个拷贝数据到str2的新空间上去,然后main函数栈帧上的tmp临时对象被析构,空间被释放:
delete[]mpstr;//赋值需要删除原先引用的堆内存
mpstr = new char[strlen(src.mpstr) + 1];//src指的是main函数栈帧上临时对象
strcpy(mpstr, src.mpstr);
实际上我们更希望,删除释放掉str2原来的堆上的数据,然后str2直接将临时对象的堆数据拿来引用,将临时对象的指针置为nullptr,并且临时对象析构时什么也不做:
delete[]mpstr;
mpstr = src.mpstr;
src.mpstr = nullptr;
//临时对象析构什么也不要做
你觉得效率哪个高?
临时对象:马上结束生命周期的对象,旧的做法是根据临时对象的size等开辟构造新对象或者复制已存在的对象,完事以后临时对象马上被析构。
C++11意识到这个问题,支持带右值引用的拷贝构造和赋值运算重载:
- 右值引用:右值的引用
int v = 20;
int &vv = v; //左值的引用
const int &a = 20;//常引用
int &&b = 20; //右值的引用,但是b是一个左值,有自己空间和命名
int &c = b;//ok
//int &&d = b;//error
- 临时量,临时对象属于右值,一个右值引用变量本身是一个左值
String &s = String("1111");
在旧一点的编译器,这样写是没有问题的,但是实际上String("1111")是一个临时量,
也就是一个右值,语句结束生命周期也就结束,*正确的写法* 应该用右值引用引用他,或者用常引用引用他:
String &&ss = String("222222");
const String &sss = String("1111");
添加带右值引用参数的拷贝构造以及赋值运算符重载函数的String类:
#include<iostream>
#include <string.h>
using namespace std;
class String
{
public:
//构造函数
String(const char *ptr = nullptr){
std::cout << "String(const char *ptr)" << std::endl;
if (ptr == nullptr){
mpstr = new char[1];
mpstr[0] = '\0';
}else{
mpstr = new char[strlen(ptr) + 1];
strcpy(mpstr, ptr);
}
}
//左值引用参数的拷贝构造函数
String(const String &src){ //src引用的是一个左值
std::cout << "String(const String &src)" << std::endl;
mpstr = new char[strlen(src.mpstr) + 1];
strcpy(mpstr, src.mpstr);
}
//右值引用参数的拷贝构造函数
String(String &&src){//src引用的是一个临时对象
std::cout << "String(const String &&src)" << std::endl;
mpstr = src.mpstr;
src.mpstr = nullptr;
}
//左值引用参数赋值运算符的重载函数
String& operator=(const String &src){//src引用的是一个左值
std::cout << "String& operator=(const String &src)" << std::endl;
if (this == &src)
return *this;
delete[]mpstr;
mpstr = new char[strlen(src.mpstr) + 1];
strcpy(mpstr, src.mpstr);
return *this;
}
//右值引用参数赋值运算符的重载函数
String& operator=(String &&src){ //src引用的是一个临时对象
std::cout << "String& operator=(const String &&src)" << std::endl;
if (this == &src)
return *this;
delete[]mpstr;
mpstr = src.mpstr;
src.mpstr = nullptr;
return *this;
}
~String(){
std::cout << "~String()" << std::endl;
if (mpstr != nullptr){
delete mpstr;
mpstr = nullptr;
}
}
bool operator>(const String &src){
if (strcmp(mpstr, src.mpstr) > 0)
return true;
return false;
}
bool operator<(const String &src){
if (strcmp(mpstr, src.mpstr) < 0)
return true;
return false;
}
bool operator==(const String &src){
if (strcmp(mpstr, src.mpstr) == 0)
return true;
return false;
}
//获取字符串的长度
int length()const{ return strlen(mpstr); }
//根据下标返回对应的字符
char& operator[](int index){ return mpstr[index]; }
//返回该字符串
const char* c_str()const{ return mpstr; }
private:
char *mpstr;
friend String operator+(const String &lhs, const String &rhs);
friend ostream& operator<<(ostream &out, const String &src);
};
//operator+
String operator+(const String &lhs, const String &rhs){
String str;
char* temp = new char[lhs.length() + rhs.length() + 1];
strcpy(str.mpstr, lhs.mpstr);
strcat(str.mpstr, rhs.mpstr);
return str;
}
ostream& operator<<(ostream &out, const String &src){
out << src.mpstr;
return out;
}
String GetString(){
String tmp = "9999999";
return tmp;
}
int main(){
String str1 = "112345";
String str2;
str2 = GetString();
return 0;
}
编译运行结果:
String(const char *ptr)
String(const char *ptr) String(const char *ptr)
String(const String &&src)
~String()
String& operator=(const String &&src)
~String()
~String()
~String()
请按任意键继续. . .
出现临时对象时,会自动匹配到到右值的拷贝构造和赋值运算重载方法,减少无效内存的开辟释放以及数据的拷贝。