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 规则以及它如何影响编程。读者可以了解三大法则的必要性和重要性。

除此之外,还解释了一些新概念,例如深拷贝和浅拷贝,它们有几种实现方式。