c++ 鐵人三項順序 什麼是三項規則?




鐵人三項順序 (7)

複製對象意味著什麼? 什麼是複制構造函數復制賦值運算符 ? 我什麼時候需要自己申報? 我怎樣才能防止我的對像被複製?


介紹

C ++用值語義處理用戶定義類型的變量。 這意味著對像被隱式複製到各種上下文中,我們應該理解“複製對象”實際上意味著什麼。

讓我們考慮一個簡單的例子:

class person
{
    std::string name;
    int age;

public:

    person(const std::string& name, int age) : name(name), age(age)
    {
    }
};

int main()
{
    person a("Bjarne Stroustrup", 60);
    person b(a);   // What happens here?
    b = a;         // And here?
}

(如果您對name(name), age(age)部分感到困惑,這稱為成員初始化程序列表 。)

特殊會員功能

複製person對象意味著什麼? main功能顯示兩種不同的複制方案。 初始化person b(a);複制構造函數執行。 它的工作是根據現有對象的狀態構建一個新的對象。 賦值b = a複制賦值操作符執行 。 它的工作通常稍微複雜一點,因為目標對像已經處於某種需要處理的有效狀態。

由於我們自己並沒有聲明拷貝構造函數和賦值運算符(也不是析構函數),所以這些都是為我們隱式定義的。 標準報價:

複製構造函數和復制賦值運算符,以及析構函數都是特殊的成員函數。 [ 注意當程序沒有明確聲明它們時,實現會隱式地為一些類類型聲明這些成員函數。 如果使用它們,實現將隱含地定義它們。 [...] 結束說明 ] [n3126.pdf第12節§1]

默認情況下,複製對象意味著複製其成員:

非聯合類X的隱式定義的複制構造函數執行其子對象的成員副本。 [n3126.pdf第12.8節§16]

非聯合類X的隱式定義的複制賦值運算符執行其子對象的成員複製分配。 [n3126.pdf第12.8節30節]

隱式定義

隱式定義的特殊成員函數如下所示:

// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    name = that.name;
    age = that.age;
    return *this;
}

// 3. destructor
~person()
{
}

在這種情況下,成員複製正是我們想要的:複製nameage ,因此我們得到一個獨立的獨立person對象。 隱式定義的析構函數總是空的。 在這種情況下這也很好,因為我們沒有在構造函數中獲取任何資源。 成員的析構函數在析構函數完成後隱式調用:

在執行析構函數的主體並銷毀在主體內分配的任何自動對象之後,類X的析構函數調用X的直接成員的析構函數[n3126.pdf 12.4§6]

管理資源

那麼我們何時應該明確地聲明這些特殊的成員函數呢? 當我們的班級管理資源時 ,也就是說,班級的某個對象負責該資源時。 這通常意味著資源在構造函數中獲得 (或傳入構造函數)並在析構函數中釋放

讓我們回顧一下預標準的C ++。 沒有std::string這樣的東西,程序員也愛上了指針。 person類可能看起來像這樣:

class person
{
    char* name;
    int age;

public:

    // the constructor acquires a resource:
    // in this case, dynamic memory obtained via new[]
    person(const char* the_name, int the_age)
    {
        name = new char[strlen(the_name) + 1];
        strcpy(name, the_name);
        age = the_age;
    }

    // the destructor must release this resource via delete[]
    ~person()
    {
        delete[] name;
    }
};

即使在今天,人們仍然以這種風格編寫課程並陷入困境:“ 我將一個人推到了一個向量中,現在我發現了瘋狂的內存錯誤! ”請記住,默認情況下,複製對象意味著複製它的成員,但複製name成員只是複制一個指針, 而不是它指向的字符數組! 這有幾個不愉快的效果:

  1. 通過a變化可以通過b觀察。
  2. 一旦b被銷毀, a.name就是一個懸掛指針。
  3. 如果a被銷毀,刪除懸掛指針會產生未定義的行為
  4. 由於作業沒有考慮作業前指定的name ,遲早你會在整個地方發生內存洩漏。

明確的定義

由於成員複製不具備所需的效果,因此我們必須明確定義復制構造函數和復制賦值運算符以製作字符數組的深層副本:

// 1. copy constructor
person(const person& that)
{
    name = new char[strlen(that.name) + 1];
    strcpy(name, that.name);
    age = that.age;
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    if (this != &that)
    {
        delete[] name;
        // This is a dangerous point in the flow of execution!
        // We have temporarily invalidated the class invariants,
        // and the next statement might throw an exception,
        // leaving the object in an invalid state :(
        name = new char[strlen(that.name) + 1];
        strcpy(name, that.name);
        age = that.age;
    }
    return *this;
}

請注意初始化和賦值之間的區別:在分配name以防止內存洩漏之前,我們必須拆除舊狀態。 此外,我們必須防止x = x形式的自我分配。 沒有這個檢查, delete[] name會刪除包含字符串的數組,因為當你寫x = xthis->namethat.name都包含相同的指針。

例外安全

不幸的是,如果由於內存耗盡導致new char[...]拋出異常,這種解決方案將失敗。 一種可能的解決方案是引入一個局部變量並對語句進行重新排序:

// 2. copy assignment operator
person& operator=(const person& that)
{
    char* local_name = new char[strlen(that.name) + 1];
    // If the above statement throws,
    // the object is still in the same state as before.
    // None of the following statements will throw an exception :)
    strcpy(local_name, that.name);
    delete[] name;
    name = local_name;
    age = that.age;
    return *this;
}

這也可以在沒有明確檢查的情況下自行分配。 這個問題的更強大的解決方案是copy-and-swap成語 ,但我不會在這裡詳細討論異常安全。 我只提到例外來說明以下幾點: 編寫管理資源的類很困難。

非複制資源

一些資源不能或不應該被複製,例如文件句柄或互斥體。 在這種情況下,只需將復制構造函數和復制賦值運算符聲明為private而不給出定義:

private:

    person(const person& that);
    person& operator=(const person& that);

或者,您可以從boost::noncopyable繼承或將它們聲明為已刪除(C ++ 0x):

person(const person& that) = delete;
person& operator=(const person& that) = delete;

三條規則

有時你需要實現一個管理資源的類。 (永遠不要管理單個類中的多個資源,這只會導致痛苦。)在這種情況下,請記住三條規則

如果您需要自己顯式聲明析構函數,複製構造函數或複制賦值運算符,則可能需要明確聲明它們中的全部三個。

(不幸的是,這個“規則”不是由C ++標准或我知道的任何編譯器強制執行的。)

忠告

大多數時候,你不需要自己管理資源,因為現有的類如std::string已經為你做了。 只需將使用std::string成員的簡單代碼與使用char*的錯綜複雜且容易出錯的替代方法進行比較,就可以確信。 只要你遠離原始指針成員,三條規則不太可能涉及你自己的代碼。


C ++中的三條規則是設計和開發三個要求的基本原則,如果在下面的一個成員函數中有明確的定義,那麼程序員應該將另外兩個成員函數一起定義。 即以下三個成員函數是不可或缺的:析構函數,拷貝構造函數,複製賦值運算符。

在C ++中復制構造函數是一個特殊的構造函數。 它用於構建一個新對象,它是與現有對象副本等效的新對象。

複製賦值運算符是一個特殊的賦值運算符,通常用於將現有對象指定給同一類型對象的其他對象。

有很快的例子:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;

三巨頭的法律如上所述。

簡單的英語就是一個簡單的例子,它解決了這種問題:

非默認析構函數

你在你的構造函數中分配了內存,所以你需要編寫一個析構函數來刪除它。 否則你會造成內存洩漏。

你可能會認為這是工作完成。

問題在於,如果復制是由對象組成的,則副本將指向與原始對象相同的內存。

一旦它們中的一個刪除了析構函數中的內存,另一個將會有一個指向無效內存的指針(這被稱為懸掛指針),當它試圖使用它時,事情會變得多毛。

因此,你寫了一個拷貝構造函數,以便為它分配新的對象自己的內存塊來銷毀。

賦值運算符和復制構造函數

您將構造函數中的內存分配給您的類的成員指針。 當您複製此類的對象時,默認賦值運算符和復制構造函數會將此成員指針的值複製到新對象。

這意味著新對象和舊對象將指向同一片內存,因此當您在一個對像中更改它時,它也將被更改為其他對象。 如果一個對象刪除了這個內存,另一個將繼續嘗試使用它 - eek。

為了解決這個問題,你需要編寫自己的拷貝構造函數和賦值操作符。 您的版本為新對象分配單獨的內存,並複制第一個指針所指向的值而不是其地址。


三條法則是C ++的基本原則,基本上是這樣說的

如果你的課程需要任何

  • 一個拷貝構造函數
  • 一個賦值操作符
  • 析構函數

明確地定義,那麼它可能需要全部三個

原因是他們三個人通常都用來管理資源,如果你的班級管理資源,通常需要管理複製和釋放資源。

如果沒有良好的語義來複製類的管理資源,則考慮通過將復制構造函數和賦值運算符聲明(不defining )為private來禁止複制。

(請注意,即將推出的新版本的C ++標準(即C ++ 11)將語義添加到C ++中,這可能會改變三項規則。但是,我對此知之甚少,無法編寫C ++ 11部分關於三的規則。)


基本上如果你有一個析構函數(而不是默認的析構函數),這意味著你定義的類有一些內存分配。 假設這個類是由一些客戶代碼或由你使用的。

    MyClass x(a, b);
    MyClass y(c, d);
    x = y; // This is a shallow copy if assignment operator is not provided

如果MyClass只有一些原始類型成員,則默認賦值運算符可以工作,但如果它有一些指針成員和沒有賦值運算符的對象,結果將是不可預知的。 因此我們可以說,如果在類的析構函數中有某些東西需要刪除,我們可能需要一個深層複製操作符,這意味著我們應該提供一個拷貝構造函數和賦值操作符。


複製對象意味著什麼? 有幾種方法可以復制對象 - 讓我們來談談您最可能引用的兩種 - 深度複製和淺度複製。

由於我們使用面向對象的語言(或者至少假定是這樣),所以假設你分配了一塊內存。 由於它是一種OO語言,我們可以很容易地引用我們分配的內存塊,因為它們通常是我們定義的原始變量(int,chars,bytes)或類,它們是由我們自己的類型和基元組成的。 假設我們有一類Car,如下所示:

class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
{
private String sPrintColor;
private String sModel;
private String sMake;

public changePaint(String newColor)
{
   this.sPrintColor = newColor;
}

public Car(String model, String make, String color) //Constructor
{
   this.sPrintColor = color;
   this.sModel = model;
   this.sMake = make;
}

public ~Car() //Destructor
{
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
}

public Car(const Car &other) // Copy Constructor
{
   this.sPrintColor = other.sPrintColor;
   this.sModel = other.sModel;
   this.sMake = other.sMake;
}
public Car &operator =(const Car &other) // Assignment Operator
{
   if(this != &other)
   {
      this.sPrintColor = other.sPrintColor;
      this.sModel = other.sModel;
      this.sMake = other.sMake;
   }
   return *this;
}

}

如果我們聲明一個對象,然後創建一個完全獨立的對象副本,那麼深層副本就是我們最終得到2個完全集合的內存中的2個對象。

Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.

現在讓我們做一些奇怪的事情。 比方說car2要么編程錯誤,要么意圖分享car1的實際內存。 (這樣做通常是一個錯誤,而在課堂上通常是討論下的毯子。)假裝每次詢問car2時,都確實解析了car1內存空間的指針......這或多或少是淺拷貝是。

//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.

 Car car1 = new Car("ford", "mustang", "red"); 
 Car car2 = car1; 
 car2.changePaint("green");//car1 is also now green 
 delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve 
 the address of where car2 exists and delete the memory...which is also
 the memory associated with your car.*/
 car1.changePaint("red");/*program will likely crash because this area is
 no longer allocated to the program.*/

因此,無論您使用哪種語言編寫,在復制對象時都要非常小心,因為大部分時間您都需要進行深層複製。

什麼是複制構造函數和復制賦值運算符? 我已經在上面使用過它們。 當您輸入代碼時會調用複制構造函數,如Car car2 = car1; 本質上,如果你聲明一個變量並將其分配在一行中,那就是調用複制構造函數的時候。 賦值運算符是當您使用等號時發生的情況 - car2 = car1; 。 注意car2沒有在同一個語句中聲明。 您為這些操作編寫的兩段代碼可能非常相似。 實際上,典型的設計模式還有另一個功能,您可以調用它來設置所有內容,只要您滿意,初始復制/分配是合法的 - 如果您查看我編寫的代碼,函數幾乎完全相同。

我什麼時候需要自己申報? 如果您不是在編寫代碼來共享或以某種方式進行生產,您只需要在需要時聲明它們。 如果您選擇“無意中”使用它,並且沒有創建一個程序語言,那麼您需要了解程序語言的功能,即編譯器默認。 我很少使用複制構造函數作為實例,但賦值運算符覆蓋很常見。 你知道你可以重寫什麼加法,減法等等嗎?

我怎樣才能防止我的對像被複製? 覆蓋所有允許使用私有函數為對象分配內存的方式是一個合理的開始。 如果你真的不希望人們複製它們,你可以通過拋出異常並且不復制對象來公開並提醒程序員。


許多現有的答案已經觸及了拷貝構造函數,賦值運算符和析構函數。 但是,在C ++ 11之後,移動語義的引入可能會擴展到3以上。

最近Michael Claisse發表了一個涉及這個主題的演講: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class ://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class





rule-of-three