覆盖析构函数 C++

11

来自C++ FAQ:

[11.4] 我可以为我的类重载析构函数吗?不可以。

我知道这意味着您不能更改返回类型、参数类型或参数数量。我可能会在单词的语法上钻牛角尖,但是覆盖父类的析构函数是否可能?


class Child : public Parent {
public:
    virtual Parent::~Parent() {
        // New definition
    }
};

而且就此事而言,需要递归地完成吗?

class Grandchild : public Child {
public:
    Child::Parent::~Parent() {
        // An even newer definition
    }
};

我阅读了这篇文章相关帖子,这让我想到了一个问题,因为析构函数不会被继承,所以它们不能被覆盖,但我从来没有看到过明确的说明。

编辑:我更改了这个问题以反映出我想要重写父类的析构函数,注意子类和孙子类都要重写~Parent()。

我这么做的主要原因是为了保持 Parent 的接口,同时改变它被销毁的方式(这也是子类存在的原因)。另外,我将有其他东西来管理所有创建的 Parent,而且会在稍后选择的时间显式地调用它们的析构函数。


@yngum 当然,析构函数是可以继承的。如果你在子类中没有提供一个覆盖父类的析构函数,实际上你就继承了基类的析构函数。 在派生类中重写父类的析构函数是有很多意义的,就像你可以在下面给出的一些示例中看到的那样。 - Philip Stuyck
3
析构函数不会被继承。如果您未声明一个析构函数,编译器会自动生成一个。在隐式析构函数执行之后,基类的析构函数会自动调用。请参见我回答中的标准引用。 - John Dibling
2
如果一个基类的析构函数被派生类继承,那么派生类的成员将如何被销毁? - John Dibling
2
@PhilipStuyck:“……因此它们被继承。” 不,析构函数不会被继承。12.4/3:“如果一个类没有用户声明的析构函数,则会隐式声明一个析构函数。” - John Dibling
@John http://publib.boulder.ibm.com/infocenter/lnxpcomp/v8v101/index.jsp?topic=%2Fcom.ibm.xlcpp8l.doc%2Flanguage%2Fref%2Fcplr380.htm 显示你是正确的,而我是错误的。当然,析构函数的行为方式是从子类到基类一个接一个地进行,这使它看起来像是继承。隐式创建的析构函数是空的,在调用基类析构函数之后立即执行。 - Philip Stuyck
显示剩余4条评论
4个回答

10

我可能在词语的语法上纠缠小节,

但事实并非如此——它们是两个非常不同的概念。

然而,你可以覆盖析构函数吗?

是的,在许多情况下你实际上必须这样做。但是,为了使它适用于多态对象,你需要将基类析构函数声明为virtual

Parent const& p = Child();

因为Parent::~Parent是虚函数,所以将在作用域结束时正确地调用p.~Child()


@yngum 啊,显然是“析构函数”。 - Konrad Rudolph
1
虚拟构造函数并不存在。而且你甚至不能在构造函数中调用虚拟方法。 - Philip Stuyck
@KonradRudolph 但是这回答了问题吗?我想让Child重新定义对~Parent()的调用。 - Danny A
1
@DannyA 很遗憾,你不能这样做,每个类只能有一个析构函数定义。 - yngccc
@PhilipStuyck - 您确实可以在构造函数中调用虚函数。调用将转到当前正在构建的类,这不一定是最派生的类。 - Pete Becker
我的意思是,如果你创建一个带有虚拟抽象init方法的基类,并在构造函数中调用它,那么你将遇到严重的问题。如果在基类中实现了虚拟方法并且这正是你想要做的,那么你可以调用基类的虚拟方法。我会非常小心处理这样的事情。关键是,只有当构造函数完成后,类才被完全构建。 - Philip Stuyck

9
是的,可以重载类的析构函数。事实上,在定义使用多态性的类层次结构时,您必须在基类中声明虚拟析构函数。
与普通成员函数的覆盖相同,析构函数的覆盖方式也完全相同。当您通过指向基类的指针使用delete删除对象时,派生类的析构函数会被正确调用。这就是为什么对于多态层次结构,必须在基类中具有虚拟析构函数的原因。
然而,虚拟析构函数和虚拟成员函数之间存在一个差异,这与析构函数的虚拟属性无关。也就是说,在执行以下代码时:
class A
{
public:  
  virtual void Foo() {}
  virtual ~A() {};
};

class B : public A
{
public:
  void Foo() {};
  ~B() {}
};

int main()
{
  A* a = new B;
  a->Foo();  // B::Foo() is called
  delete a;  // B is destroyed via B::~B()
}

当你调用a->Foo()时,会调用类B中的方法Foo()。由于B::Foo()没有明确调用A::Foo(),因此A::Foo()不会被调用。
但是,当通过delete a;销毁对象时,首先会调用析构函数B::~B(),然后在该函数结束之后但在控制返回到程序之前,还会调用基类析构函数A::~A()
当然,在你思考这个问题时,这是显而易见的,再次强调,这与析构函数的虚拟性质无关,但它确实与普通的虚拟方法调用行为不同,所以我想指出这一点。
《C++03》标准引用:
在执行析构函数的内容并销毁其中分配的任何自动对象后,类X的析构函数调用X的直接成员的析构函数、X的直接基类的析构函数,以及如果X是最派生类的类型(12.6.2),则它的析构函数调用X的虚基类的析构函数。所有析构函数都被调用,就好像它们被一个限定名引用一样,也就是忽略任何可能存在于更派生类中的虚拟覆盖析构函数。基类和成员按照它们构造完毕的相反顺序被销毁(见12.6.2)。析构函数中的返回语句(6.6.3)可能不会直接返回给调用者;在转移控制权到调用者之前,将调用成员和基类的析构函数。数组元素的析构函数按照它们构造的相反顺序被调用(见12.6)。

3

是的:你可以有虚拟析构函数,唯一的原因是在派生类中重写它们。

它的样子像这样:

class Parent {
public:
    virtual ~Parent();
};

class Child : public Parent {
public:
    virtual ~Child();
};

class Grandchild : public Child {
public:
    ~Grandchild(); // virtual is inherited here
};

请注意,析构函数不像普通函数一样被name重写,因为该名称始终是您要销毁的实例所属的类的名称。
还要注意,父类的析构函数总是被调用的,因此您不需要复制其清除代码:有关详细信息,请阅读成员对象和基类子对象的构造和析构顺序。

术语

  • 覆盖一个函数意味着在派生类中实现基类virtual函数。您不能改变签名(除了使用协变返回类型)。因此,override与继承的虚函数具有相同的签名。
  • 重载一个函数意味着使用相同的名称(在某种程度上也是相同的作用域)实现多个函数。因此,overload与其他具有相同名称的函数具有不同的签名,不直接涉及虚拟分配,并且不一定继承。

虚拟的Child析构函数不是必需的,因为派生类“继承”了Parent的虚拟方面。参考 - Luke Heytens
“virtual”关键字并非必需,这正是我编写Grandchild类的目的。虽然它仍然是一个虚析构函数,但我不确定你想表达什么意思。 - Useless
是的,我想说的是Child类的“virtual”关键字是不必要的。Child和Grandchild都已经是虚拟的了(就像你为Grandchild所示的那样)。 - Luke Heytens

1

是的,每当您有一个可能使用基类引用被销毁的子类时,您可以并且应该使析构函数成为虚函数。静态代码分析工具甚至会抱怨如果您没有提供虚析构函数。

考虑以下示例:

class A
{
public:
    A() { a = new int; }
    virtual ~A() { delete a; }

private:
    int *a;
};

class B final : public A
{
public:
    B() { b = new int; }
    ~B() { delete b; }

private:
    int *b;
};

int main()
{
    A *a = new B();
    delete a;
}

如果A的析构函数不是虚函数,那么delete a只会调用A的析构函数,最终导致内存泄漏。但由于它是虚函数,两个析构函数都会被调用,按照顺序为~B() -> ~A()。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接