C++ 中的三大法則

Jay Shaw 2023年1月30日 2022年7月18日
  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 <iostream>
#include <exception>
#include <cstring>

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 規則以及它如何影響程式設計。讀者可以瞭解三大法則的必要性和重要性。

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