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()
請按任意鍵繼續. . .
出現臨時對象時,會自動匹配到到右值的拷貝構造和賦值運算重載方法,減少無效內存的開闢釋放以及數據的拷貝。