关于c ++:我们什么时候需要定义析构函数?

When do we need to define destructors?

本问题已经有最佳答案,请猛点这里访问。

我读到当我们有指针成员和定义基类时,析构函数需要被定义,但我不确定我是否完全理解。我不确定的一件事是,定义一个默认的构造函数是否无用,因为在默认情况下,我们总是被赋予一个默认的构造函数。另外,我不确定是否需要定义默认的构造函数来实现RAII原则(我们是否只需要将资源分配放到构造函数中,而不定义任何析构函数?).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class A
{

public:
    ~Account()
    {
        delete [] brandname;
        delete b;

        //do we need to define it?

    };

    something(){} =0; //virtual function (reason #1: base class)

private:
    char *brandname; //c-style string, which is a pointer member (reason #2: has a pointer member)
    B* b; //instance of class B, which is a pointer member (reason #2)
    vector<B*> vec; //what about this?



}

class B: public A
{
    public something()
    {
    cout <<"nothing" << endl;
    }

    //in all other cases we don't need to define the destructor, nor declare it?
}


三的法则和零的法则

处理资源的好方法是使用三规则(由于移动语义,现在是五规则),但最近另一个规则正在接管:零规则。

但您应该真正阅读本文的想法是,资源管理应该留给其他特定的类。

在这方面,标准库提供了一套很好的工具,如:std::vectorstd::stringstd::unique_ptrstd::shared_ptr,有效地消除了对自定义析构函数、移动/复制构造函数、移动/复制分配和默认构造函数的需要。

如何将其应用于代码

在代码中,您有许多不同的资源,这是一个很好的例子。

如果您注意到brandname实际上是一个"动态字符串",那么标准库不仅可以将您从C样式的字符串中保存下来,而且还可以使用std::string自动管理字符串的内存。

动态分配的B

第二个资源似乎是动态分配的B。如果您是因为"我想要一个可选成员"以外的其他原因而动态分配的,那么您一定要使用std::unique_ptr,它将自动处理资源(适当时解除分配)。另一方面,如果您希望它是可选成员,则可以使用std::optional

BS系列

最后一个资源只是一个B的数组,使用std::vector很容易管理。标准库允许您根据不同的需要从不同的容器中进行选择,只需提到其中一些:std::dequestd::liststd::array

结论

要添加所有建议,您将得到:

1
2
3
4
5
6
7
8
class A {
private:
    std::string brandname;
    std::unique_ptr b;
    std::vector vec;
public:
    virtual void something(){} = 0;
};

既安全又可读。


正如"胡说八道"所指出的,问题太广泛了…所以我会用我所知道的一切来解决…

重新定义析构函数的第一个原因是在三的规则中,这是Scott Meyers有效C++中的项目6的一部分,但不是完全的。三个规则表示,如果您重新定义了析构函数、复制构造函数或复制赋值操作,那么这意味着您应该重写所有三个操作。原因是,如果必须重写自己的版本,那么编译器默认值将不再对其余版本有效。

另一个例子是Scott Meyers在有效C++中指出的例子。

When you try to delete a derived class object through a base class pointer and the base class has a non virtual destructor, the results are undefined.

然后他继续说

If a class does not contain any virtual functions, that is often an indication that it is not meant to be used as a base class. When a class is not intended to be used as a base class, making the destructor virtual is usually a bad idea.

他关于虚拟的析构函数的结论是

The bottom line is that gratuitously declaring all destructors virtual is just as wrong as never declaring them virtual. In fact, many people summarize the situation this way: declare a virtual destructor in a class if and only if that class contains at least one virtual function.

如果不是三种情况的规则,那么可能你的对象中有一个指针成员,或者你在对象中为它分配了内存,那么你需要在析构函数中管理内存,这是他书中的第6项

一定要查看@jeffrey的答案


有两件事需要定义一个析构函数:

  • 当对象被销毁时,需要执行一些操作,而不是销毁所有类成员。

    这些操作中的绝大多数曾经是释放内存,根据raii原则,这些操作已经转移到raii容器的析构函数中,编译器负责调用这些容器。但是这些操作可以是任何操作,比如关闭一个文件,或者将一些数据写入日志,或者……如果您严格遵循RAII原则,您将为所有其他操作编写RAII容器,以便只有RAII容器定义了析构函数。

  • 当需要通过基类指针销毁对象时。

    当需要这样做时,必须将析构函数定义为基类中的virtual。否则,派生的析构函数将不会被调用,与它们是否被定义以及它们是否是virtual无关。下面是一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <iostream>

    class Foo {
        public:
            ~Foo() {
                std::cerr <<"Foo::~Foo()
    "
    ;
            };
    };

    class Bar : public Foo {
        public:
            ~Bar() {
                std::cerr <<"Bar::~Bar()
    "
    ;
            };
    };

    int main() {
        Foo* bar = new Bar();
        delete bar;
    }

    该程序只打印Foo::~Foo(),不调用Bar的析构函数。没有警告或错误消息。只有部分破坏的对象,以及所有的后果。因此,请确保您在出现这种情况时自己发现它(或者在您定义的每个非限定类中添加virtual ~Foo() = default;)。

  • 如果这两个条件都不满足,则不需要定义析构函数,默认的构造函数就足够了。

    下面是您的示例代码:当成员是指向某个对象的指针(作为指针或引用)时,编译器不知道…

    • …是否有指向此对象的其他指针。

    • …指针是指向一个对象,还是指向一个数组。

    因此,编译器无法推断是否或如何销毁指针指向的内容。所以默认的析构函数永远不会在指针后面销毁任何东西。

    这既适用于brandname也适用于b。因此,您需要一个析构函数,因为您需要自己进行释放。或者,您可以为它们使用raii容器(std::string和智能指针变体)。

    这种推理不适用于vec,因为这个变量直接包括对象中的std::vector<>。因此,编译器知道vec必须被销毁,而这反过来又会销毁它的所有元素(毕竟它是一个raii容器)。


    如果动态分配内存,并且只希望在对象本身被"终止"时释放该内存,那么需要有一个析构函数。

    对象可以通过两种方式"终止":

  • 如果它是静态分配的,那么它将被(编译器)隐式"终止"。
  • 如果是动态分配的,则显式地"终止"(通过调用delete)。
  • 当使用基类类型的指针显式"终止"时,析构函数必须是virtual


    我们知道如果没有提供析构函数,编译器将生成一个。

    这意味着除了简单清理之外的任何东西,如基元类型,都需要一个析构函数。

    在许多情况下,施工过程中的动态分配或资源获取都有一个清理阶段。例如,可能需要删除动态分配的内存。

    如果类表示硬件元素,则可能需要关闭该元素,或将其置于安全状态。

    容器可能需要删除其所有元素。

    总之,如果类获取资源或需要专门的清理(比如按确定的顺序),那么应该有析构函数。