1. 程式人生 > 其它 >C++複習——降低拷貝代價(深/淺拷貝,左值/右值引用,拷貝構造和移動操作)

C++複習——降低拷貝代價(深/淺拷貝,左值/右值引用,拷貝構造和移動操作)

為什麼關注拷貝

將某種資訊/資料從一個位置傳遞到另一個位置是程式中的常用操作,這一過程可以被視為(廣義的)拷貝。在編寫程式中,通常會涉及到兩種形式的拷貝:

  • 深拷貝:真正對原始的資料進行復制,得到一個原始資料的副本;
  • 淺拷貝:複製了變數的位元組內容,但對於指標變數而言,在新的位置上仍然是通過訪問資料的地址來間接獲取資料,原始資料只有一份。

可以看出,深淺拷貝的區分主要是針對指標而言。顯然,後一種方法沒有實現真正意義的複製,在進行淺拷貝後,對於後面資料的修改會影響到之前的原始資料,帶來難以發現的影響。

在編寫程式中,根據實際需要,這兩種方式都會被用到,合理的使用拷貝可以有效利用資源。但在實際場景中,由於不清楚這兩種操作的界限,在許多情況下會造成複雜的問題,C++設計了一些機制來約束自定義類中的拷貝行為。

拷貝基礎

在正式開始介紹拷貝之前,需要關注幾個與之相關的概念:

什麼時候發生拷貝

  • 使用=操作定義變數
  • 將一個實參傳遞給非引用的形參
  • 從函式返回非引用的變數

可以看出,第一種情況是較為引起注意的拷貝,但是後兩種情況拷貝也會發生,並且很容易被忽略。

拷貝與引用

引用可以認為是原有資料的一種別名,它並不佔用新的資料空間(此處是指不會佔用與被引用資料一樣大的空間)。引用可以通過一種類似常量指標的方式實現的,在引用被宣告的時候就需要進行初始化。

通常不認為傳遞/返回引用的過程中進行了拷貝,即引用是我們所說的pass-by-reference,而拷貝指的是pass-by-value。

拷貝與移動

在許多時候,一個物件被拷貝之後很快就被銷燬或不再使用了,那麼這個拷貝操作顯得有些浪費資源。在這種情況下,可以使用移動操作避免拷貝。為了定義移動操作,需要在程式中區分右值和左值。

左值與右值

左右值的區別主要在於所表示物件的生命週期不同,左值是一個具有持久狀態的物件(變數),右值是一個字面量或者臨時物件。該右值物件與其他物件毫無聯絡,可以被銷燬或者進行變動,不會帶來其他負面影響。因此,移動操作主要是針對右值進行的。

我們所說的普通引用可以被視為左值引用,在引入右值的概念後,相對應的也有右值引用,右值引用只能繫結到字面量或者返回右值的表示式上,而不能繫結到左值上。(但是反過來是可以的)

哪些表示式返回左值

  • 返回左值引用的函式
  • 賦值操作
  • 下標操作
  • 解引用
  • 前置遞增遞減

哪些表示式返回右值

  • 返回非引用型別的函式
  • 算術運算
  • 後置遞增遞減

移動的目的,就是將一個左值轉化為右值引用從而避免拷貝。在這種情況下,需要符合之前的承諾,即完全切斷源物件與其他物件的聯絡,當作真正的右值來處理。此後源物件只能銷燬或者賦新值,這一操作應當由程式設計者保證。

自定義類中的拷貝控制

在一個自定義類中,有以下幾個特殊的成員函式與拷貝行為有關:

  1. 建構函式
  2. 解構函式
  3. 拷貝建構函式
  4. 拷貝賦值函式
  5. 移動建構函式
  6. 移動賦值函式

在預設情況下,C++會為自定義類合成這幾個特殊的成員函式,而根據類功能和設計不同,以上一些預設方法合成的函式可能會帶來意外危害。

三五法則是一個C++中對以上成員函式設計策略的建議。首先這裡要說明,Rule of Three/Five並不是法則的內容有三條或者五條,而是為了說明這個法則針對的物件是三個函式(以上2-4)還是五個函式(2-6),後者是C++11後開始支援的。三五法則的實質就是為了對拷貝行為進行控制,避免潛在的問題。

法則具體內容可以參考:https://smartkeyerror.oss-cn-shenzhen.aliyuncs.com/Phyduck/c%2B%2B/copy-control/4.%20%E4%B8%89%E4%BA%94%E6%B3%95%E5%88%99.pdf

總結

簡要梳理一下要做的事,實際上就是區分幾種操作,以及認識三種操作應當在什麼場景下進行。

傳遞資訊的操作分別為:

  • 使用引用(pass-by-reference)
  • 使用拷貝(pass-by-value)
    • 使用淺拷貝(預設拷貝建構函式)
      • 但是在使用淺拷貝的時候需要注意某些隱含的情況,比如指標拷貝後帶來的兩次記憶體釋放問題
      • 如果不能確定,最好是將拷貝構造定義為預設刪除,或者使用智慧指標
      • 自己定義一個合理的拷貝建構函式最好
    • 對於不會在其他處使用的變數,可以使用移動操作配合移動構造/移動賦值函式來實現拷貝功能
      • 需要確保這個被移動的變數之後處於待銷燬的狀態,後續程式不再修改它,否則會帶來錯誤
    • 使用深拷貝

不同情況需要不同的場景支援,這個並不是絕對的,需要個人結合情況分析。