我是否需要为第二个接口类添加虚析构函数?

18
我有叫做“Base”和“Derived”的类。
struct Base {
    Base() = default;
    virtual ~Base() = default;
    Base(const Base&) = delete;
    Base& operator=(const Base&) = delete;

    virtual void DoStuff() = 0;
};

“Base”类需要虚析构函数,这是可以理解的。此外,我不允许复制这个类。

struct Derived : Base {
    Derived() = default;
    ~Derived() override = default;

    void DoStuff() override { /*...*/ }
};

int main()
{
    std::shared_ptr<Base> a = std::make_shared<Derived>();
    a->DoStuff();
    return 0;
}

现在让我们介绍另外两个类,它们分别被称为CallableDerivedCallable
struct Callable
{
    virtual void Call() = 0;
};

struct DerivedCallable : Base, Callable
{
    DerivedCallable() = default;
    ~DerivedCallable() override = default;

    void DoStuff() override { /*...*/ }
    void Call() override { /*...*/ }
};

int main()
{
    std::shared_ptr<Base> a = std::make_shared<Derived>();
    a->DoStuff();

    {
        auto callableA = std::dynamic_pointer_cast<DerivedCallable>(a);
        if(callableA) {
            callableA->Call();
        }
    }

    std::shared_ptr<Base> b = std::make_shared<DerivedCallable>();
    b->DoStuff();
    
    {
        auto callableB = std::dynamic_pointer_cast<DerivedCallable>(b);
        if(callableB) {
            callableB->Call();
        }
    }

    return 0;
}

Derived 没有继承 Callable,所以 callableA 是 nullptr,因此 if 语句不会执行 Call() 函数。

另一方面,DerivedCallable 继承自 Callable,而 std::dynamic_pointer_cast 将增加对象的引用计数到 2,因此当 callableB 超出作用域时,对象不会被释放,只有引用计数会减少到 1,然后主函数将释放 b

Callable 需要有虚析构函数吗?


2
看看这个链接:https://en.cppreference.com/w/cpp/types/is_polymorphic。在你的类型上使用它。顺便说一句,好问题,加一。 - Bathsheba
1
@Edziju 请问下面这两行代码的含义是什么:Base& operator(const Base&) = delete; 和 Derived() = default; Derived() override = default;? - Vlad from Moscow
我的错,应该是 operator=~Derived() override = default - Edziju
2个回答

19
只有当您通过基类指针删除派生类对象时,才需要虚析构函数。 由于您正在使用std::shared_ptr,因此不需要任何虚析构函数,因为shared_ptr存储了一个正确类型的删除器(无论您如何转换它)。 如果您将拥有DerivedCallable对象并带有Callable指针(std::unique_ptr或其他调用delete on a Callable*的东西),那么它应该有一个虚析构函数。但是,如果您只有带有Callable*的非拥有引用,则不一定需要虚析构函数。 在类已经具有其他虚成员函数的情况下添加虚析构函数非常便宜,因此添加它是可以接受的,这样您就不必担心意外地删除它。

3
当将原始指针转换为shared_ptr时,存在异常形式的shared_ptr持有适当的删除器。如果任何其他成员函数是虚拟的,则最好具有虚拟析构函数。 - Marek R
4
@MarekR 将派生类型的原始指针转换为基类类型的 shared_ptr(例如 std::shared_ptr<Base>(new Derived))可以正确存储派生类型的析构函数。如果将其作为原始指针转换为基类类型,则会得到一个拥有基类类型的所有权指针,因此需要一个虚析构函数。我同意即使在最后一点上也应该有一个虚析构函数。 - Artyer
1
@Artyer 我的意思是当原始指针指向基类时更复杂的例子。在这种情况下,它将无法检测到正确的析构函数。 这是一个极端案例,但确实可能发生。同样最好使用适用于 std::unique_ptr 的类。 - Marek R
1
你确定吗?Callable有一个纯虚方法。 - BЈовић
由于您正在使用std::shared_ptr,因此您不需要任何虚析构函数,因为shared_ptr存储了一个正确类型的删除器(无论您如何转换它)。这并不完全正确(只有90%的真实性)。是make_shared使其安全,并且/或通过指向可以安全删除的类型的指针进行初始构造,而不是shared ptr本身。提及这一点将改善答案(除了注释之外)。 - Yakk - Adam Nevraumont
显示剩余2条评论

7

这要看情况。理论上说,Base 不需要一个虚析构函数。当您携带的对象的动态类型与其静态类型不同时,需要将析构函数设计为虚函数。

在您的例子中,您有一个指向实际上指向 Derivied 对象的 Base 指针。如果您没有将 ~Base() 设计成虚函数,那么销毁该对象将表现出未定义的行为,可能导致无法销毁对象的 Derived 部分。

因此,只要您不打算通过特定基类拥有指向对象的指针,那么该基类的析构函数就不需要是虚函数。


1
"无法销毁对象的派生部分。即使在实践中可能是这种行为,但这将是未定义的行为。" - Jarod42
这正是我想的。只是需要确认一下:D。谢谢。 - Edziju

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