我们什么时候需要定义析构函数?

use*_*009 24 c++ destructor

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

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?
}
Run Code Online (Sandbox Code Playgroud)

Sho*_*hoe 28

三规则与零规则

处理资源的良好方式是使用三规则(现在由于移动语义而成为五条规则),但最近另一条规则正在接管:零规则.

这个想法,但你真的应该阅读这篇文章,是资源管理应该留给其他特定的类.

在这方面的标准库提供了一套很好的工具一样:std::vector,std::string,std::unique_ptrstd::shared_ptr,有效地消除了自定义析构函数,移动/拷贝构造函数,移动/复制分配和默认构造的需要.

如何将其应用于您的代码

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

字符串

如果你注意到brandname它实际上是一个"动态字符串",标准库不仅可以保存你的C风格字符串,还可以自动管理字符串的内存std::string.

动态分配的B.

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

Bs的集合

最后一个资源只是一个Bs 数组.这很容易管理std::vector.标准库允许您根据不同需求选择各种不同的容器; 只提其中的一些:std::deque,std::liststd::array.

结论

要添加所有建议,您最终会得到:

class A {
private:
    std::string brandname;
    std::unique_ptr<B> b;
    std::vector<B> vec;
public:
    virtual void something(){} = 0;
};
Run Code Online (Sandbox Code Playgroud)

这既安全又可读.

  • 好的,但这很难回答这个问题.问:"我什么时候定义析构函数?" 答:"使用'矢量'." 咦? (7认同)
  • @EdS.,答案是隐含的:"从不,使用'vector`".:) (6认同)
  • 好吧,我认为这不是一个很好的答案。理解从来都不是一件坏事,你不能真的相信除了标准库实现者之外没有人需要定义他们自己的析构函数。 (5认同)
  • @Jeffrey零的统治是很棒的人,非常感谢,我以前没有听说过 (3认同)
  • 我认为答案是正确理解*零法则*和*三法则*.因此你的回答和@Claudiordgz相得益彰.其余只是我认为的哲学问题.两个+1. (2认同)

Cla*_*dgz 11

正如@nonsensickle指出的那样,问题太广了......所以我会尝试用我所知道的一切来解决它...

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

另一个例子是Scott Meyers在Effective C++中指出的一个例子

当您尝试通过基类指针删除派生类对象并且基类具有非虚拟析构函数时,结果是未定义的.

然后他继续说道

如果一个类不包含任何虚函数,那通常表明它不打算用作基类.当一个类不打算用作基类时,将析构函数设置为虚拟通常是一个坏主意.

他对虚拟析构函数的结论是

最重要的是,无偿宣布虚拟所有析构函数与从未声明虚拟版本一样错误.事实上,许多人用这种方式总结了这种情况:当且仅当该类包含至少一个虚函数时,才在类中声明虚拟析构函数.

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

请务必查看@ Jefffrey关于零度规则的答案


cma*_*ter 6

正好有两件事需要定义析构函数:

  1. 当您的对象被破坏时,您需要执行一些操作而不是破坏所有类成员。

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

  2. 当您需要通过基类指针销毁对象时。

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

    #include <iostream>
    
    class Foo {
        public:
            ~Foo() {
                std::cerr << "Foo::~Foo()\n";
            };
    };
    
    class Bar : public Foo {
        public:
            ~Bar() {
                std::cerr << "Bar::~Bar()\n";
            };
    };
    
    int main() {
        Foo* bar = new Bar();
        delete bar;
    }
    
    Run Code Online (Sandbox Code Playgroud)

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

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


现在到您的示例代码:
当您的成员是指向某物的指针(作为指针或引用)时,编译器不知道......

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

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

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

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

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