簡體   English   中英

我們何時必須使用復制構造函數?

[英]When do we have to use copy constructors?

我知道C ++編譯器為類創建了一個復制構造函數。 在這種情況下,我們必須編寫用戶定義的復制構造函數嗎? 你能舉一些例子嗎?

編譯器生成的復制構造函數執行成員復制。 有時這還不夠。 例如:

class Class {
public:
    Class( const char* str );
    ~Class();
private:
    char* stored;
};

Class::Class( const char* str )
{
    stored = new char[srtlen( str ) + 1 ];
    strcpy( stored, str );
}

Class::~Class()
{
    delete[] stored;
}

在這種情況下,成員方式復制stored成員不會復制緩沖區(只會復制指針),因此第一個被銷毀的副本共享緩沖區將成功調用delete[] ,第二個將運行到未定義的行為。 您需要深度復制復制構造函數(以及賦值運算符)。

Class::Class( const Class& another )
{
    stored = new char[strlen(another.stored) + 1];
    strcpy( stored, another.stored );
}

void Class::operator = ( const Class& another )
{
    char* temp = new char[strlen(another.stored) + 1];
    strcpy( temp, another.stored);
    delete[] stored;
    stored = temp;
}

我有點惱火的是,沒有引用Rule of Five法則的Rule of Five

這個規則很簡單:

五規則
無論何時編寫析構函數,復制構造函數,復制賦值運算符,移動構造函數或移動賦值運算符,您可能需要編寫其他四個。

但是有一個更一般的指導原則,你應該遵循,這源於編寫異常安全代碼的需要:

每個資源都應由專用對象管理

在這里@sharptooth的代碼仍然(大部分)都很好,但是如果他要在他的類中添加第二個屬性則不會。 考慮以下課程:

class Erroneous
{
public:
  Erroneous();
  // ... others
private:
  Foo* mFoo;
  Bar* mBar;
};

Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}

如果new Bar投擲怎么辦? 你如何刪除mFoo指向的對象? 有解決方案(功能級別try / catch ...),它們只是不擴展。

處理這種情況的正確方法是使用適當的類而不是原始指針。

class Righteous
{
public:
private:
  std::unique_ptr<Foo> mFoo;
  std::unique_ptr<Bar> mBar;
};

使用相同的構造函數實現(或實際上,使用make_unique ),我現在免費獲得異常安全! 這不是很令人興奮嗎? 最重要的是,我不再需要擔心正確的析構函數! 我確實需要編寫自己的Copy ConstructorAssignment Operator ,因為unique_ptr沒有定義這些操作......但是這里並不重要;)

因此, sharptooth的課程重新審視:

class Class
{
public:
  Class(char const* str): mData(str) {}
private:
  std::string mData;
};

我不了解你,但我發現我更容易;)

我可以回想一下我的實踐,並在必須處理明確聲明/定義復制構造函數時考慮以下情況。 我將案例分為兩類

  • 正確性/語義 - 如果您不提供用戶定義的復制構造函數,則使用該類型的程序可能無法編譯,或者可能無法正常工作。
  • 優化 - 為編譯器生成的復制構造函數提供了一個很好的替代方法,可以使程序更快。


正確/語義

我在本節中介紹了使用該類型正確操作程序所需的聲明/定義復制構造函數的情況。

閱讀完本節后,您將了解允許編譯器自行生成復制構造函數的幾個缺陷。 因此, seand在他指出的答案 ,它始終是安全地關閉復制能力的一個新的類, 故意使之后來真正需要的時候。

如何在C ++ 03中創建一個不可復制的類

聲明一個私有的復制構造函數,並且不為它提供一個實現(這樣即使該類的對象在類本身或其朋友中被復制,構建也會在鏈接階段失敗)。

如何使類在C ++ 11或更高版本中不可復制

在結尾處使用=delete聲明復制構造函數。


淺與深拷貝

這是最容易理解的案例,實際上是其他答案中提到的唯一案例。 shaprtooth已經很好地覆蓋了它。 我只想補充說,深度復制應該由對象專有的資源可以應用於任何類型的資源,其中動態分配的內存只是一種。 如果需要,可能還需要深度復制對象

  • 復制磁盤上的臨時文件
  • 打開一個單獨的網絡連接
  • 創建一個單獨的工作線程
  • 分配一個單獨的OpenGL幀緩沖區
  • 等等

自我注冊的對象

考慮一個類,其中所有對象 - 無論它們是如何構造的 - 必須以某種方式注冊。 一些例子:

  • 最簡單的示例:維護當前現有對象的總數。 對象注冊就是增加靜態計數器。

  • 一個更復雜的例子是擁有一個單例注冊表,其中存儲了對該類型的所有現有對象的引用(以便可以將通知傳遞給所有這些對象)。

  • 引用計數的智能指針只能被視為此類別中的一個特殊情況:新指針將“自身”注冊到共享資源而不是全局注冊表中。

這種自注冊操作必須由類型的任何構造函數執行,復制構造函數也不例外。


具有內部交叉引用的對象

一些對象可能具有非平凡的內部結構,在它們的不同子對象之間具有直接的交叉引用(事實上,只有一個這樣的內部交叉引用足以觸發這種情況)。 編譯器提供的復制構造函數將破壞內部對象內關聯,將它們轉換為對象間關聯。

一個例子:

struct MarriedMan;
struct MarriedWoman;

struct MarriedMan {
    // ...
    MarriedWoman* wife;   // association
};

struct MarriedWoman {
    // ...
    MarriedMan* husband;  // association
};

struct MarriedCouple {
    MarriedWoman wife;    // aggregation
    MarriedMan   husband; // aggregation

    MarriedCouple() {
        wife.husband = &husband;
        husband.wife = &wife;
    }
};

MarriedCouple couple1; // couple1.wife and couple1.husband are spouses

MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?

只允許復制符合特定條件的對象

在某些狀態下可能存在可以安全復制對象的類(例如,默認構造狀態),否則無法安全復制。 如果我們想允許復制安全復制對象,那么 - 如果編程是防御性的 - 我們需要在用戶定義的復制構造函數中進行運行時檢查。


不可復制的子對象

有時,應該可復制的類聚合不可復制的子對象。 通常,對於具有不可觀察狀態的對象會發生這種情況(這種情況將在下面的“優化”部分中詳細討論)。 編譯器只是幫助識別這種情況。


准可復制的子對象

應該是可復制的類可以聚合准可復制類型的子對象。 准可復制類型在嚴格意義上不提供復制構造函數,但具有另一個允許創建對象的概念副本的構造函數。 使類型准可復制的原因是當沒有關於該類型的復制語義的完全一致時。

例如,重新訪問對象自注冊案例,我們可以爭辯說,可能存在這樣的情況:只有當對象是完整的獨立對象時,才必須向全局對象管理器注冊該對象。 如果它是另一個對象的子對象,那么管理它的責任在於它的包含對象。

或者,必須支持淺層和深層復制(它們都不是默認值)。

然后最終決定留給該類型的用戶 - 在復制對象時,他們必須明確指定(通過附加參數)預期的復制方法。

在采用非防御性編程方法的情況下,也可能存在常規復制構造函數和准復制構造函數。 當在絕大多數情況下應該應用單一復制方法時,這是合理的,而在罕見但很好理解的情況下,應該使用替代復制方法。 然后編譯器不會抱怨它無法隱式定義復制構造函數; 用戶應自行負責記住並檢查是否應通過准復制構造函數復制該類型的子對象。


不要復制與對象標識密切相關的狀態

在極少數情況下,對象的可觀察狀態的子集可以構成(或被認為)對象身份的不可分割的部分,並且不應該轉移到其他對象(盡管這可能有些爭議)。

例子:

  • 對象的UID(但是這個也屬於上面的“自注冊”案例,因為id必須在自行注冊的行為中獲得)。

  • 在新對象不能繼承源對象的歷史記錄但是以單個歷史記錄項“ 從<OTHER_OBJECT_ID> <TIME>復制 ”的情況下,對象的歷史記錄(例如,撤銷/重做堆棧)。

在這種情況下,復制構造函數必須跳過復制相應的子對象。


強制執行復制構造函數的正確簽名

編譯器提供的復制構造函數的簽名取決於可用於子對象的復制構造函數。 如果至少有一個子對象沒有真正的復制構造函數 (通過常量引用獲取源對象),而是有一個變異的復制構造函數 (通過非常量引用獲取源對象),那么編譯器將別無選擇但要隱式聲明然后定義一個變異的復制構造函數。

現在,如果子對象類型的“變異”復制構造函數實際上沒有改變源對象(並且只是由不了解const關鍵字的程序員編寫),該怎么辦? 如果我們不能通過添加缺少的const修復該代碼,那么另一個選項是使用正確的簽名聲明我們自己的用戶定義的復制構造函數並提交轉向const_cast


寫時復制(COW)

已經放棄直接引用其內部數據的COW容器必須在構造時進行深度復制,否則它可能表現為引用計數句柄。

盡管COW是一種優化技術,但復制構造函數中的這種邏輯對於其正確實現至關重要。 這就是為什么我把這個案例放在這里,而不是在“優化”部分,我們接下來。



優化

在以下情況下,您可能需要/需要根據優化問題定義自己的復制構造函數:


復制期間的結構優化

考慮一個支持元素刪除操作的容器,但可以通過簡單地將刪除的元素標記為已刪除,並稍后再循環其插槽來實現。 當制作這樣一個容器的副本時,壓縮幸存數據而不是按原樣保留“已刪除”的插槽可能是有意義的。


跳過復制不可觀察的狀態

對象可能包含不屬於其可觀察狀態的數據。 通常,這是在對象的生命周期內累積的緩存/記憶數據,以加速對象執行的某些慢速查詢操作。 跳過復制該數據是安全的,因為在執行相關操作時(以及如果!)將重新計算該數據。 復制此數據可能是不合理的,因為如果通過改變操作來修改對象的可觀察狀態(從中派生緩存數據),它可能會很快失效(如果我們不打算修改對象,為什么我們要創建一個深層然后復制?)

僅當輔助數據與表示可觀察狀態的數據相比較大時,才優化該優化。


禁用隱式復制

C ++允許通過explicit聲明復制構造函數來禁用隱式復制。 然后,該類的對象不能傳遞給函數和/或通過值從函數返回。 這個技巧可以用於看似輕量級但復制起來確實非常昂貴的類型(但是,使其成為可復制的可能是更好的選擇)。

在C ++ 03中聲明一個復制構造函數也需要定義它(當然,如果你打算使用它)。 因此,僅僅考慮所討論的問題,這樣的復制構造函數意味着您必須編寫編譯器為您自動生成的相同代碼。

C ++ 11和更新的標准允許使用顯式請求聲明特殊成員函數(默認和復制構造函數,復制賦值運算符和析構函數) 以使用默認實現 (只需使用=default結束聲明)。



待辦事項

這個答案可以改進如下:

  • 添加更多示例代碼
  • 說明“具有內部交叉引用的對象”的情況
  • 添加一些鏈接

如果您有一個動態分配內容的類。 例如,您將書籍的標題存儲為char *並使用new設置標題,副本將不起作用。

你必須編寫一個復制構造函數,它的title = new char[length+1]然后是strcpy(title, titleIn) 復制構造函數只會執行“淺”復制。

當對象按值傳遞,按值返回或顯式復制時,將調用復制構造函數。 如果沒有復制構造函數,c ++會創建一個默認的復制構造函數,它會生成一個淺復制。 如果對象沒有動態分配內存的指針,那么淺拷貝就可以了。

禁用copy ctor和operator =通常是一個好主意,除非該類特別需要它。 這可以防止低效率,例如在預期引用時通過值傳遞arg。 編譯器生成的方法也可能無效。

讓我們考慮下面的代碼片段:

class base{
    int a, *p;
public:
    base(){
        p = new int;
    }
    void SetData(int, int);
    void ShowData();
    base(const base& old_ref){
        //No coding present.
    }
};
void base :: ShowData(){
    cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
    this->a = a;
    *(this->p) = b;
}
int main(void)
{
    base b1;
    b1.SetData(2, 3);
    b1.ShowData();
    base b2 = b1; //!! Copy constructor called.
    b2.ShowData();
    return 0;
}

Output: 
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();

b2.ShowData(); 提供垃圾輸出,因為創建了一個用戶定義的復制構造函數,沒有編寫代碼來顯式復制數據。 所以編譯器不會創建相同的。

只是想與大家分享這些知識,盡管大多數人都已經知道了。

干杯......快樂的編碼!!!

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM