C++中的“虚函数但没有虚析构函数”

49

我有一个基类Media和几个派生类,分别是DVDBook等。 基类的代码如下:

class Media{
    private:
        int id;
        string title;
        int year;
    public:
        Media(){ id = year = 0; title = ""; }
        Media(int _id, string _title, int _year): id(_id), title(_title), year(_year) {}
//      virtual ~Media() = 0;
        void changeID(int newID){ id = newID; }
        virtual void print(ostream &out);
};

问题在于:如果没有析构函数,GCC会给我一堆警告信息,但程序仍能编译通过并正常工作。现在我想摆脱这些烦人的警告信息,因此我加了一个虚析构函数来满足编译器,结果是:程序不能编译,并出现以下错误:

undefined reference to `Media::~Media()`

把析构函数声明为纯虚函数并不能解决这个问题。 那么出了什么问题?


1
你为什么要使用纯虚构造函数呢?一个空的虚构造函数同样可以胜任(而且它所占用的非空格字符数量相同)。 - Gunther Piez
1
@LuchianGrigore 那不是我想表达的意思。我为你重新表述一下:你为什么想让Media类成为抽象类? - Gunther Piez
1
@drhirsch 通常是因为你想禁止它的创建。也许媒体类型的对象没有意义。 - Luchian Grigore
2
如果有人只需要禁用“类具有虚函数但非虚析构函数”的警告,则是-Wno-non-virtual-dtor。不过我并不建议将其作为这个问题的答案。 - Raptor007
5个回答

53

你需要定义虚析构函数,而不仅仅是添加它。

//Media.h
class Media{
    //....
    virtual ~Media() = 0;
};

//Media.cpp
#include "Media.h"
//....
Media::~Media() {};

你收到警告的原因是,所有将要被继承的类都应该有虚拟或受保护的(credit @Steve)析构函数,否则通过指向基类的指针删除实例会导致未定义的行为。

注意,即使它们是纯虚析构函数,你也必须提供析构函数的定义。


4
所有派生自该类的类都应该有一个虚析构函数,或者一个受保护的析构函数,以防止外部使用错误的指针删除对象。这适用于那些外部不应该直接删除对象的情况(例如,由某个工厂创建并返回智能指针的对象)。 - Steve Jessop
不敢相信我必须实现一个空的析构函数。 问题解决了,谢谢大家! - Max
6
如果您的编译器足够新,您也可以使用= default(C++11功能)。 - MSalters
1
仅定义空构造函数 virtual ~Media() {} 就足够了吗?(Jerry Coffin 的回答) - pevik
1
如果可以使用= default,就应该使用;即使在实践中实现与默认相同,用户提供特殊成员函数也可能会阻止或妨碍其他功能。 - underscore_d

33

问题在于:如果没有析构函数,GCC会给我一堆警告“类具有虚拟函数但没有虚拟析构函数”,但仍然编译并且我的程序正常工作

这是现代C++中一个令人烦恼的警告,但在旧的面向对象的C++中通常是正确的。

问题在于您的对象销毁方式。进行一个简单的测试:

#include <iostream>

class Base {};
class Derived: public Base { public: ~Derived() { std::cout << "Aargh\n"; } };

int main() {
  Base* b = new Derived();
  Derived* d = new Derived();

  delete d;
  delete b;
}

这会打印出:

Aargh

是的,只被调用一次。

问题在于,在对类型为Base*的变量调用delete时,会调用Base::~Base()方法。如果它是virtual的,则调用会动态分派到最终的方法(基于动态类型),在本例中为Derived::~Derived(),但如果它不是,则Derived::~Derived()永远不会被调用,因此永远不会被执行。

因此,如果您希望在基类上调用delete(或使用为您完成此操作的智能指针),则需要在其类定义中添加virtual ~Base() {}。这就是为什么当您创建一个没有虚拟析构函数的多态类时,gcc会发出警告。


注意:时间已经改变,自那以后我在Clang中实现了-Wdelete-non-virtual-dtor,并且也在gcc中复制了它。

-Wnon-virtual-dtor对库编写者有用(因为它在基类上发出警报),但可能存在更高的误报率;另一方面-Wdelete-non-virtual-dtor在调用点触发,并且具有更低的误报率(通常可以通过添加final来消除类的“多态”属性以解决这个问题)。


但请注意,至少从官方来说,这大部分都是猜测。官方上来说,通过指向基类的指针/引用销毁派生对象需要一个虚析构函数,否则行为将变得不确定。 - Jerry Coffin
你应该给每个至少有一个虚函数的类提供一个虚析构函数,这不仅是因为其他原因。 - Spencer
@Spencer:你能给它们起个名字吗?我想不出其他方法,除了正确地销毁对象。 - Violet Giraffe

11

你所注释的是一个纯虚析构函数的声明。这意味着必须在派生类中重写该函数才能实例化该类的对象。

你需要的只是将析构函数定义为虚函数:

virtual ~Media() {}

在C++11或更新版本中,通常更倾向于将其定义为默认值,而不是使用空的函数体:

virtual ~Media() = default;

7
你应该实现虚析构函数,而不是将其变成纯虚函数。
可以查看这个类似的问题(从虚析构函数错误的角度来看可能是相同的,但不是警告)获取更多信息。
编辑:更通用的解决方案,回复LuchianGrigore的评论(感谢指出)。
你也可以将析构函数设为纯虚函数,并按照上述提到的问题进行实现。
在你的类中使用虚析构函数应该是为了防止基类实例化(即当你没有其他纯虚函数使类抽象时)。

1
纯虚析构函数有其作用。但即使声明为纯虚,仍然需要定义。 - Luchian Grigore
是的,我的回答只涉及错误。我会在我的回答中添加关于类中使用纯虚析构函数以及实现它们的方法。+1 - lucian.pantelimon

0

先取消声明,然后尝试在类声明后添加以下行

Media::~Media(){}

1
在类声明之后添加此内容将导致链接期间出现多个定义。定义必须在一个翻译单元中。 - Luchian Grigore
1
那还是不正确的。在类声明之后,在头文件中添加它会导致错误。 - Luchian Grigore
@LuchianGrigore,你能告诉我如果我在.h文件中添加定义,编译器会给出什么样的错误吗?在Visual Studio上它可以正常工作。 - Hiren
如果您将其放在一个头文件中,并将该头文件包含在多个C++源文件中,则会发生链接错误,因为定义将针对每个.cpp文件复制一次。 - Al.G.

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