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

时间:2014-03-18 21:12:38

标签: 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?
}

5 个答案:

答案 0 :(得分:27)

三规则和零规则

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

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

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

如何将其应用于您的代码

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

字符串

如果您注意到brandname实际上是一个&#34;动态字符串&#34;,标准库不仅可以保存您的C风格字符串,还可以使用{{3}自动管理字符串的内存}。

动态分配的B

第二个资源似乎是动态分​​配的B。如果你是动态分配除了&#34之外的其他原因;我想要一个可选的成员&#34;你应该使用std::string来自动处理资源(在适当的时候解除分配)。另一方面,如果您希望它是可选成员,则可以使用std::unique_ptr代替。

Bs

的集合

最后一个资源只是B的数组。使用std::optional可以轻松管理。标准库允许您根据不同需求选择各种不同的容器;只需提及其中一些:std::vectorstd::dequestd::list

结论

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

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

这既安全又可读。

答案 1 :(得分:11)

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

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

另一个例子是Scott Meyers in Effective C++

指出的那个例子
  

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

然后他继续

  

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

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

  

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

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

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

答案 2 :(得分:2)

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

  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;

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


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

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

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

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

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

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

答案 3 :(得分:1)

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

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

在许多情况下,施工期间的动态分配或资源获取具有清理阶段。例如,可能需要删除动态分配的内存。

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

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

总之,如果班级获得资源或需要专门的清理(让我们按照确定的顺序说),那么应该有析构函数。

答案 4 :(得分:1)

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

对象可以&#34;终止&#34;有两种方式:

  1. 如果它是静态分配的,那么它就会终止&#34;隐含地(由编译器)。
  2. 如果它是动态分配的,那么它就会终止&#34;明确地(通过调用delete)。
  3. &#34;终止&#34;显式使用基类类型的指针,析构函数必须是virtual