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
。另一個物件 p2
是 p1
的複製物件。
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++ 中建構函式的隱式定義
有兩種方法可以製作物件的副本。
- 淺拷貝——使用建構函式拷貝一個物件的地址,並將其儲存在新的物件中。
- 深度複製 - 使用類似的建構函式,將儲存在該地址內的值複製到新地址中。
通常,當為物件分配一些記憶體時,複製建構函式的隱式版本會複製指標 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
。
該類具有三個私有變數——靜態 l
和 h
中的兩個,以及一個動態物件 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 規則以及它如何影響程式設計。讀者可以瞭解三大法則的必要性和重要性。
除此之外,還解釋了一些新概念,例如深拷貝和淺拷貝,它們有幾種實現方式。