C++ 中的三大法則

Jay Shaw 2023年10月12日
  1. 瞭解 C++ 中的三大法則
  2. C++ 中建構函式的隱式定義
  3. C++ 中建構函式的顯式定義
  4. まとめ
C++ 中的三大法則

三大法則是全球最流行的編碼習慣之一。法律規定 C++ 有一些必須一起宣告的特殊功能,即使是必需的。

這些函式是複製建構函式、複製賦值運算子和解構函式。

法律規定,如果在程式中宣告瞭三個特殊功能之一,則其他兩個也必須遵循。如果沒有,程式就會遇到嚴重的記憶體洩漏。

本文將詳細解釋三大法則以及如何繞過它。

瞭解 C++ 中的三大法則

必須考慮到當動態分配的資源被新增到一個類中時,會發生什麼情況,以瞭解大三大法則。

在下面的示例中,建立了一個建構函式類,它為指標分配一些記憶體空間。

class Big3 {
  int* x;

 public:
  Big3() : x(new int()) { std::cout << "Resource allocated \n"; }
};

如上所示,記憶體被分配給指標。現在,它也必須使用解構函式釋放。

~Big3() {
  std::cout << "Resource is released \n";
  delete x;
}

普遍的看法是,工作到此為止。但現實還很遙遠。

在此類中啟動複製建構函式時會發生一些嚴重的記憶體洩漏。

這個問題背後的原因是建構函式的隱式定義。當一個物件在沒有定義建構函式的情況下被複制時,編譯器會隱式建立一個複製建構函式,該建構函式不會建立克隆,而只是同一物件的影子。

預設情況下,複製建構函式對物件執行淺拷貝。程式使用這種複製方法執行良好,直到某些資源被動態分配給一個物件。

例如,在下面的程式碼中,建立了一個物件 p1。另一個物件 p2p1 的複製物件。

int main() {
  Big3 p1;
  Big3 p2(p1);
}

在這段程式碼中,當解構函式銷燬物件 p1 時,p2 成為一個懸空指標。這是因為物件 p2 指向物件 p1 的引用。

為了更好地理解,下面給出了完整的程式碼。

#include <iostream>

class Big3 {
  int* x;

 public:
  Big3() : x(new int()) { std::cout << "Resource allocated \n"; }
  ~Big3() {
    std::cout << "Resource is released \n";
    delete x;
  }
};

int main() {
  Big3 p1;
  Big3 p2(p1);
}

為了避免像懸空指標這樣的問題,程式設計師需要顯式宣告所有必需的建構函式,這就是三大規則的內容。

C++ 中建構函式的隱式定義

有兩種方法可以製作物件的副本。

  1. 淺拷貝——使用建構函式拷貝一個物件的地址,並將其儲存在新的物件中。
  2. 深度複製 - 使用類似的建構函式,將儲存在該地址內的值複製到新地址中。

通常,當為物件分配一些記憶體時,複製建構函式的隱式版本會複製指標 x 的引用,而不是建立具有自己的記憶體分配集的新物件。

下面是如何隱式定義特殊成員函式的表示。

book(const book& that) : name(that.name), slno(that.slno) {}

book& operator=(const book& that) {
  name = that.name;
  slno = that.slno;
  return *this;
}

~book() {}

在這個例子中,定義了三個特殊的成員函式。第一個是複製建構函式,它通過成員初始化器列表分配成員變數:name(that.name), slno(that.slno)

第二個建構函式是建立類物件副本的複製賦值建構函式。在這裡,運算子過載用於建立物件的副本。

最後,解構函式保持為空,因為沒有分配任何資源。該程式碼不會引發錯誤,因為物件不需要任何記憶體分配。

為什麼隱式定義在資源管理中失敗

假設類的成員指標接收記憶體分配。當使用預設賦值運算子和複製函式建構函式複製此類的物件時,此成員指標的引用將被複制到新物件。

結果,對一個物件所做的任何更改也會影響另一個物件,因為新物件和舊物件都將指向相同的記憶體位置。第二個物件將繼續嘗試使用它。

class person {
  char* name;
  int age;

 public:
  // constructor acquires a resource
  // 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;
  }

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

上面的例子複製了 name,它複製了指標,將值儲存在裡面。

宣告解構函式時,它只是刪除原始物件的例項。但是複製的物件一直指向同一個引用,現在該引用已被破壞。

這是記憶體洩漏的主要原因。它們出現在解構函式刪除原始物件及其引用時,而複製建構函式建立的物件繼續懸空,也稱為懸空指標。

類似地,如果懸空指標未選中,那麼該記憶體引用將在未來造成多個記憶體洩漏。

這個問題的唯一解決方案是顯式宣告建構函式,或者簡單地說,建立自己的複製建構函式和賦值運算子模型來解決這個問題。

自定義建構函式複製初始指標指向的值而不是其地址,為新物件分配單獨的記憶體。

C++ 中建構函式的顯式定義

據觀察,隱式定義的建構函式導致物件的淺拷貝。為了解決這個問題,程式設計師明確定義了一個複製建構函式,它有助於在 C++ 中深度複製物件。

深度複製是一種分配新記憶體塊來儲存指標值的方法,而不僅僅是儲存記憶體引用。

此方法需要顯式定義所有三個特殊成員方法,以便編譯器在複製物件時分配新記憶體。

解構函式在 C++ 中顯式定義的建構函式中的作用

必須建立解構函式來擦除分配給函式物件的記憶體。如果你不這樣做,這可能會導致記憶體洩漏。

在隱式建構函式中,即使宣告瞭解構函式,問題仍然存在。出現問題是因為如果複製分配的物件記憶體,則複製的物件將指向與原始物件相同的記憶體。

當一個刪除其解構函式中的記憶體時,另一個將有一個指向無效記憶體的指標,當它嘗試使用它時事情會變得複雜。

因此,必須建立顯式定義的複製建構函式,為新物件提供要清除的記憶體碎片。

下面的程式顯示了一個遵守三大法則的演示類。

class name {
 private:
  int* variable;  // pointer variable
 public:
  // Constructor
  name() { variable = new int; }

  void input(int var1)  // Parameterized method to take input
  {
    *variable = var1;
  }

  // Copy Constructor
  name(name& sample) {
    variable = new int;
    *variable = *(sample.variable);

    // destructor
    ~name() { delete variable; }
  };

在 C++ 中實現複製建構函式

該程式有一個類 Book,它帶有一個預設的引數化建構函式和一個解構函式。當沒有提供輸入時,預設建構函式返回空值,而引數化建構函式初始化這些值並複製它們。

這裡包含了一個異常處理方法(try-catch),當變數 m_Name 無法分配資源時,它會丟擲異常。

在解構函式之後,會建立一個複製建構函式,用於複製原始物件。

#include <cstring>
#include <exception>
#include <iostream>

using namespace std;

class Book {
  int m_Slno;
  char* m_Name;

 public:
  // Default Constructor
  Book() : m_Slno(0), m_Name(nullptr) {}

  // Parametarized Constructor
  Book(int slNumber, char* name) {
    m_Slno = slNumber;
    unsigned int len = strlen(name) + 1;
    try {
      m_Name = new char[len];
    } catch (std::bad_alloc e) {
      cout << "Exception received: " << e.what() << endl;
      return;
    }
    memset(m_Name, 0, len);
    strcpy(m_Name, name);
  }

  // Destructor
  ~Book() {
    if (m_Name) {
      delete[] m_Name;
      m_Name = nullptr;
    }
  }

  friend ostream& operator<<(ostream& os, const Book& s);
};

ostream& operator<<(ostream& os, const Book& s) {
  os << s.m_Slno << ", " << s.m_Name << endl;
  return os;
}

int main() {
  Book s1(124546, "Digital Marketing 101");
  Book s2(134645, "Fault in our stars");

  s2 = s1;

  cout << s1;
  cout << s2;

  s1.~Book();
  cout << s2;

  return 0;
}

在主函式中,當物件 s1 被銷燬時,s2 不會丟失它的動態物件,即物件 s 的字串變數。

下面的另一個示例演示瞭如何使用複製建構函式深度複製物件。在下面的程式碼中,建立了一個建構函式類 design

該類具有三個私有變數——靜態 lh 中的兩個,以及一個動態物件 w

#include <iostream>
using namespace std;

// A class design
class design {
 private:
  int l;
  int* w;
  int h;

 public:
  // Constructor
  design() { w = new int; }

  // Method to take input
  void set_dimension(int len, int brea, int heig) {
    l = len;
    *w = brea;
    h = heig;
  }

  // Display Function
  void show_data() {
    cout << "The Length is = " << l << "\n"
         << "Breadth  of the design = " << *w << "\n"
         << "Height = " << h << "\n"
         << endl;
  }

  // Deep copy is initialized here
  design(design& sample) {
    l = sample.l;
    w = new int;
    *w = *(sample.w);
    h = sample.h;
  }

  // Destructor
  ~design() { delete w; }
};

// Driver Code
int main() {
  // Object of class first
  design first;

  // Passing Parameters
  first.set_dimension(13, 19, 26);

  // Calling display method
  first.show_data();

  // Copying the data of 'first' object to 'second'
  design second = first;

  // Calling display method again to show the 'second' object
  second.show_data();

  return 0;
}

輸出:

The Length is = 13
Breadth  of the design = 19
Height = 26

The Length is = 13
Breadth  of the design = 19
Height = 26
--------------------------------
Process exited after 0.2031 seconds with return value 0
Press any key to continue . . .

まとめ

本文全面而詳細地解釋了 big 3 規則以及它如何影響程式設計。讀者可以瞭解三大法則的必要性和重要性。

除此之外,還解釋了一些新概念,例如深拷貝和淺拷貝,它們有幾種實現方式。