C++11中使用带右值引用参数的拷贝构造函数和赋值运算符重载函数来提升OOP效率

C++11前没有右值引用的说法,涉及到拷贝构造和赋值时,过程如下:

简单实现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){
		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函数调用过程中的构造函数和赋值函数的过程:

  1. 调用构造函数构造出str1对象; String(const char *ptr)
  2. 调用构造函数构造出str2对象;String(const char *ptr)
  3. 调用构造函数构造出GetString函数栈帧tmp对象,这里需要注意,由于函数返回自定义类型,编译器会提前在main函数栈帧开辟一份空间,用来存放将来函数返回到main函数栈帧的临时对象,该空间的内存地址通过GetString参数的形式传递给GetString,只不过我们开发者不用关心String(const char *ptr)
  4. GetString函数return处,将会调用拷贝构造函数构造main函数提前为临时对象开辟的那一块内存,生成对象;String(const String &src)
  5. GetString函数调用完成,栈帧回退,tmp对象析构;~String()
  6. main函数栈帧上的临时对象赋值给已经存在的对象str2;String& operator=(const String &src)
  7. 赋值语句结束,临时对象生命周期到,被析构;~String()
  8. 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()
请按任意键继续. . .  

出现临时对象时,会自动匹配到到右值的拷贝构造和赋值运算重载方法,减少无效内存的开辟释放以及数据的拷贝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章