剖析深拷貝與淺拷貝,探究重載返回引用還是對象

  • 2019 年 10 月 6 日
  • 筆記

剖析深拷貝與淺拷貝,探究重載返回引用還是對象

導論

今天在研究STL源碼中發現這麼一段有意義的程式碼:

// 重載前置++操作符  ++i  _Self& operator++() _GLIBCXX_NOEXCEPT  {      _M_node = _M_node->_M_next;      return *this;  }    // 重載後置++操作符 i++  _Self operator++(int) _GLIBCXX_NOEXCEPT  {      _Self __tmp = *this;             // 記錄原值  *調用的是拷貝構造函數      _M_node = _M_node->_M_next;      // 進行操作      return __tmp;                    // 返回原值  }

上述分別是前置++重載操作符與後置++操作符重載,可以有個疑惑,為何前置返回的是引用而後置返回的是對象呢?

這裡先給出解釋,後面一步步分析!

總結:不妨嘗試一下,連續的前置++不會出問題,但是連續的後置++就報錯了~

對於STL源碼設計也是考慮了模仿內置類型的行為,後置的++需要返回增加之前的對象,不需要返回新對象,所以直接不返回對象的引用.

前置的++返回的是增加後的對象,這個對象是需要保留的,不是臨時對象,返回引用就不需要拷貝對象,效率高.

上面這句話說的真的稀里糊塗的,第一次看到這句話,肯定一臉懵逼,實際上將上述話差分開就是解決兩個問題:

(1) 深拷貝和淺拷貝?

(2) c++中有些重載運算符為什麼要返回引用?

為了理解第(2)點,我們需要知道什麼是深拷貝,什麼是淺拷貝,分配記憶體是在堆上,還是在棧上!

1.深拷貝和淺拷貝

這裡先闡述一下C++對象中的兩個概念,分別是拷貝操作與賦值操作!

  • 拷貝操作

如果對象在申明的同時馬上進行的初始化操作,則稱之為拷貝運算

String A("hello");  String B=A;

此時其實際調用的是B(A)這樣的淺拷貝操作。調用的是String類的拷貝構造函數!

  • 賦值操作

如果對象在申明之後,在進行的賦值運算,我們稱之為賦值運算

String A("hello");  String B;  B=A;

此時實際調用的類的預設賦值函數B.operator=(A),調用的是=操作符重載函數.

不管是淺拷貝還是賦值運算,其都有預設的定義。也就是說,即使我們不overload這兩種operation,仍然可以運行。那麼,我們到底需不需要overload這兩種operation 呢?答案就是:一般,我們我們需要手動編寫析構函數的類,都需要overload 拷貝函數和賦值運算符。

現在有如下例子:

using namespace std;  class String  {  private:      char *str;      int len;  public:      String(const char* s);//構造函數聲明      void show()      {          cout << "value = " << str << endl;      }        /*copy construct*/      String(const String& other)      {          len = other.len;          str = new char[len + 1];          strcpy(str, other.str);          cout << "copy construct" << endl;      }  };    String::String(const char* s)//構造函數定義  {      len = strlen(s);      str = new char[len + 1];      strcpy(str, s);      cout<<"construct"<<endl;  }

調用:

String str1("abc");  String str2("123");  str1.show();  str2.show();

如果在上述調用後面加一句:

str2=str1

此時的記憶體分配如下圖所示:

因此,對於預設的賦值運算,如果對象域內沒有heap上的空間,其不會產生任何問題。但是,如果對象域內需要申請heap上的空間,那麼在析構對象的時候,就會連續兩次釋放heap上的同一塊記憶體區域,從而導致異常。

因此,對於對象的域在heap上分配記憶體的情況,我們必須重載賦值運算符。當對象間進行拷貝的時候,我們必須讓不同對象的成員域指向其不同的heap地址–如果成員域屬於heap的話。

其餘程式碼一致,在上述加上運算符=重載函數:

String& String::operator=(const String &other)//運算符重載  {      cout<<"= operator"<<endl;      if (this == &other)          return *this;  //        return;      delete[] str;      len = other.len;      str = new char[len + 1];      strcpy(str, other.str);      return *this;  //    return;  }

此時記憶體分配示意圖如下:

這樣,在對象str1,str2退出相應的作用域,其調用相應的析構函數,然後釋放分別屬於不同heap空間的記憶體,程式正常結束。

上述的運算符重載就是深拷貝!我們也可以這樣重載賦值運算符 void operator=(A &a);即不返回任何值。如果這樣的話,他將不支援客戶鏈式賦值 ,例如a=b=c will be prohibited!

上述另一個寫法:

void String::operator=(const String &other)//運算符重載  {      cout << "= operator" << endl;      if (str!=NULL)          delete[] str;      len = other.len;      str = new char[len + 1];      strcpy(str, other.str);  //    return;  }

後面調用如果是str3=str2=str1,就會出現下面錯誤:

error: no match for 『operator=』 (operand types are 『String』 and 『void』) str3 = str2 = str1;//str3.operator=(str1.operator=(str2))

從上可以看出,賦值運算符和拷貝函數很相似。只不過賦值函數最好有返回值(進行鏈式賦值),返回也最好是對象的引用, 而拷貝函數不需要返回任何。同時,賦值函數首先要釋放掉對象自身的堆空間(如果需要的話),然後進行其他的operation.而拷貝函數不需要如此,因為對象此時還沒有分配堆空間。

2.C++中有些重載運算符為什麼要返回引用?

假設不返回引用,如下面程式碼:

class String  {  private:      char *str;      int len;  public:      String(const char* s);//構造函數聲明      String operator=(const String& another);//運算符重載,此時返回的是對象      void show()      {          cout << "value = " << str << endl;      }        /*copy construct*/      String(const String& other)      {      }        ~String()      {          delete[] str;          cout << "deconstruct" << endl;      }  };    String::String(const char* s)//構造函數定義  {      len = strlen(s);      str = new char[len + 1];      strcpy(str, s);      cout<<"construct"<<endl;  }    String String::operator=(const String &other)//運算符重載  {      cout<<"= operator"<<endl;      if (this == &other)          return *this;  //        return;      delete[] str;      len = other.len;      str = new char[len + 1];      strcpy(str, other.str);      return *this;  //    return;  }  int main()  {      String str1("abc");      String str2("123");      str2=str1;      str1.show();      str2.show();      return 0;  }

此時運行報錯:

free(): invalid size

上述出錯是因為,操作符=重載返回的不是引用,對於上述的操作符重載返回的是對象,此時對象是臨時對象,並且會多調用一次拷貝構造與析構函數,當調用拷貝構造函數的時候,並沒有在堆上分配記憶體,而此時free調的其實就是臨時對象,而在後面str1與str2任務完成後,str1會因為前面釋放臨時對象被free掉了,所以此時進入str1的析構函數,會出現錯誤!解決這種問題將返回改為引用即可.

對比一下返回對象與返回引用:

第一種:返回對象

String String::operator=(const String &other)//運算符重載  {      cout<<"= operator"<<endl;      if (this == &other)          return *this;  //        return;      delete[] str;      len = other.len;      str = new char[len + 1];      strcpy(str, other.str);      return *this;  //    return;  }

調用:

String str1("abc");  String str2("123");  String str3("456");    (str3 = str2) = str1;  str1.show();  str2.show();  str3.show();

輸出:

construct  construct  construct  = operator  copy construct  = operator  copy construct  deconstruct  deconstruct  value = abc  value = 123  value = 123  deconstruct  deconstruct  deconstruct

第二:返回引用:

construct  construct  construct  = operator  = operator  value = abc  value = abc  value = abc  deconstruct  deconstruct  deconstruct

將上述結果對比放在表格中:

(注:可以往右拖)

construct

construct

construct

construct

construct

construct

= operator

= operator

copy construct

= operator

= operator

value = abc

copy construct

value = abc

deconstruct

value = abc

deconstruct

deconstruct

value = abc

deconstruct

value = 123

deconstruct

value = 123

deconstruct

deconstruct

deconstruct

  • 區別1:會發現使用引用返回後少了四行,原因返回的如果是對象,這個對象是臨時對象,返回後會調用一次拷貝構造函數,結束後會調用析構函數,上面使用了兩次=,所以第一種情況會多四行.
  • 區別2:結果不同,我們期待的結果是將str1也拷貝進str3,可是第一種情況並沒有實現這種效果,str3隻得到了str2的內容,並沒有得到str1的內容,這是因為執行(str3=str2)後,因為返回的是對象(一個臨時對象,str3的一個拷貝),不是引用,所以此時str3不在後面的=str1的操作中,而是str1對一個臨時對象賦值,所以str3的內容保持不變(等於str2)。

總結

那麼什麼情況下要返回對象的引用呢?

原因有兩個:

  • 允許進行連續賦值
  • 防止返回對象(返回對象也可以進行連續賦值(常規的情況,如a = b = c,而不是(a = b) = c))的時候調用拷貝構造函數和析構函數導致不必要的開銷,降低賦值運算符的效率。

最後,我們回到我們最前面解釋:

對於STL源碼設計也是考慮了模仿內置類型的行為,後置的++需要返回增加之前的對象,不需要返回新對象,所以直接不返回對象的引用.

前置的++返回的是增加後的對象,這個對象是需要保留的,不是臨時對象,返回引用就不需要拷貝對象,效率高.

相信大家對這句話認識更加深刻!

學習資料:

https://www.cnblogs.com/winston/archive/2008/06/03/1212700.html

https://www.cnblogs.com/codingmengmeng/p/5871254.html