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()
請按任意鍵繼續. . .  

出現臨時對象時,會自動匹配到到右值的拷貝構造和賦值運算重載方法,減少無效內存的開闢釋放以及數據的拷貝。

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