【问题标题】:When do we need to define destructors? [duplicate]我们什么时候需要定义析构函数? [复制]
【发布时间】:2014-04-24 19:56:35
【问题描述】:

我读到当我们有指针成员和定义基类时需要定义析构函数,但我不确定我是否完全理解。我不确定的一件事是定义默认构造函数是否无用,因为默认情况下总是给我们一个默认构造函数。另外,我不确定我们是否需要定义默认构造函数来实现 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?
}

【问题讨论】:

  • 虽然答案可能是相关的,但问题并不相同。不是重复的。我认为这是一个很好的问题,我想亲自听到答案。
  • 你的第二句话有点混乱。我想你的意思是你写构造函数的地方的析构函数?

标签: c++ destructor


【解决方案1】:

三法则和零法则

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

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

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

如何将其应用到您的代码中

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

字符串

如果您注意到brandname 实际上是一个“动态字符串”,标准库不仅可以将您从 C 样式字符串中解救出来,而且会自动使用std::string 管理字符串的内存。

动态分配的B

第二个资源似乎是动态分​​配的B。如果您出于“我想要一个可选成员”以外的其他原因进行动态分配,您绝对应该使用std::unique_ptr,它将自动处理资源(在适当时取消分配)。另一方面,如果您希望它成为可选成员,您可以改用std::optional

Bs的集合

最后一个资源只是Bs 的数组。这可以通过std::vector 轻松管理。标准库允许您从各种不同的容器中进行选择,以满足您的不同需求;仅提及其中一些:std::dequestd::liststd::array

结论

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

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

既安全又可读。

【讨论】:

  • 好的,但这很难回答问题。问:“我什么时候定义析构函数?”答:“使用vector。”嗯?
  • @EdS.,答案是含蓄的:“永远不要,使用vector”。 :)
  • 嗯,我认为这不是一个很好的答案。理解从来都不是一件坏事,你不能真的相信除了标准库的实现者之外没有人需要定义自己的析构函数。
  • 我认为答案在于正确理解零法则三法则。因此,您的回答和@Claudiordgz 相得益彰。在我看来,其余的只是哲学问题。两者都 +1。
  • @Jeffrey 零规则太棒了,非常感谢,我以前没听说过
【解决方案2】:

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

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

另一个例子是Scott Meyers in Effective C++指出的那个

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

然后他继续

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

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

底线是,无缘无故地将所有析构函数声明为虚拟与从不将它们声明为虚拟一样是错误的。事实上,很多人这样总结这种情况:当且仅当该类包含至少一个虚函数时,才在一个类中声明一个虚析构函数。

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

一定要查看@Jefffrey 对零规则的回答

【讨论】:

  • 虽然我发现你的回答很有见地,但我认为这个问题比这更广泛。他想知道他何时打算覆盖默认构造函数/析构函数,而我在问题的任何地方都没有看到virtual 的提及。这不是一个答案,但可以在实际答案之外给出,所以请标记它。在那之前-1。
  • 你认为编辑是更多的实际答案吗?
  • 是的,这是一个很大的改进,因此 +1。
  • 谢谢,我正在想另一个原因,但我现在真的想不出。
  • 我认为您和@Jeffriey 在问题允许的范围内已经涵盖了它。
【解决方案3】:

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

  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;
    }
    

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

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


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

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

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

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

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

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

【讨论】:

    【解决方案4】:

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

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

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

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

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

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

    【讨论】:

      【解决方案5】:

      如果您动态分配内存,并且您希望仅在对象本身“终止”时才释放此内存,那么您需要一个析构函数。

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

      1. 如果它是静态分配的,那么它会被隐式“终止”(由编译器)。
      2. 如果它是动态分配的,那么它会被显式“终止”(通过调用delete)。

      当显式使用基类类型的指针“终止”时,析构函数必须是virtual

      【讨论】:

        猜你喜欢
        • 2011-07-26
        • 2011-12-13
        • 2011-09-20
        • 2011-03-30
        • 1970-01-01
        • 2016-10-18
        • 2020-10-19
        • 2014-08-01
        • 2011-09-27
        相关资源
        最近更新 更多