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 <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
。
该类具有三个私有变量——静态 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 规则以及它如何影响编程。读者可以了解三大法则的必要性和重要性。
除此之外,还解释了一些新概念,例如深拷贝和浅拷贝,它们有几种实现方式。