c++ 为什么通过指向基类的指针删除数组时不调用派生类的(虚)析构函数?

myzjeezk  于 2023-05-20  发布在  其他
关注(0)|答案(6)|浏览(125)

我有一个带有虚析构函数的Animal类和一个派生类Cat

#include <iostream>

struct Animal
{
    Animal() { std::cout << "Animal constructor" << std::endl; }
    virtual ~Animal() { std::cout << "Animal destructor" << std::endl; }
};

struct Cat : public Animal
{
    Cat() { std::cout << "Cat constructor" << std::endl; }
    ~Cat() override { std::cout << "Cat destructor" << std::endl; }
};

int main()
{
    const Animal *j = new Cat[1];
    delete[] j;
}

这给出了输出:
动物构造器
Cat构造器
动物毁灭者
我不明白为什么不调用Cat的析构函数,当我的基类析构函数是虚的时?

ekqde3dh

ekqde3dh1#

注意,虽然CatAnimal,但Cat s的数组 * 不是 * Animal s的数组。换句话说,数组在C++中是 * 不变的 *,而不是像在其他语言中那样的 * 协变的 *。
所以你向上转换了这个数组,这以后会混淆编译器。在这种情况下,必须在正确的原始类型-Cat*上执行数组delete[]
请注意,如果您分配了一个包含2个或更多Cat的数组,将其转换为Animal*,然后尝试使用第二个或后续Animal,则会出现类似的问题。

oxf4rvwz

oxf4rvwz2#

我回答我自己的评论:https://en.cppreference.com/w/cpp/language/delete(强调我的)

对于第二种(数组)形式,expression必须是空指针值或先前由分配函数不是非分配形式的new-expression的数组形式(即,过载(10))。表达式的指向类型必须与数组对象的元素类型相似。如果expression是其他任何东西,包括如果它是通过new-expression的非数组形式获得的指针,则行为未定义。

https://en.cppreference.com/w/cpp/language/reinterpret_cast#Type_aliasing
非正式地说,如果忽略顶级cv限定,两种类型是相似的:

  • 它们是同一类型;或
  • 它们都是指针,指向的类型相似;或
  • 它们都是指向同一个类的成员的指针,并且被指向的成员的类型相似;或
  • 它们都是相同大小的数组或都是未知边界的数组,并且数组元素类型相似。(C++20之前)
  • 它们都是相同大小的数组或至少其中之一是未知边界的数组,并且数组元素类型相似。

据我所知,遗传不是相似性…

but5z9lq

but5z9lq3#

在我的理解中,这是未定义的行为,因为(在7.6.2.9中删除,p2,强调我的):
单对象delete表达式中,delete的操作数的值可以是空指针值,也可以是前一个非数组new表达式的指针值,或者是指向由这样的new表达式创建的对象的基类子对象的指针。如果不是,则行为未定义。在数组delete表达式中,delete的操作数的值可以是空指针值****,也可以是前一个数组new-expression...的指针值。
这基本上意味着,对于delete[],类型必须与new[]完全相同(不允许像delete这样的基类子对象)。
因此,原因是这样的-在我看来,这一次是显而易见的-实现需要知道完整的对象大小是多少,以便它可以迭代到下一个数组元素。

  • 我写了反驳为什么这可能是不同的-但经过一些思考(并阅读评论)-我意识到这样的解决方案是解决X-Y问题。
rkkpypqq

rkkpypqq4#

我对其他答案唯一的批评是它们是两个驯服的。数组之所以有效,是因为所有条目都具有相同的大小,所以索引为5的条目的起始地址可以很容易地计算出来。将Derived数组转换为Base数组(这就是您在这里所做的)可能会产生比您所展示的更糟糕的结果。(如果数组碰巧只有一个条目或Derived没有新的数据成员,你 * 可能 * 没问题。)A[1].method()可能会做一些奇怪的事情,因为(Base*)(A)+1甚至不是任何对象的起始地址。该方法将读取错误的数据,并且任何更改的影响都将是高度不可预测的。任何对虚函数的调用都将使用一个完全没有意义的vtable指针,所以谁知道会执行什么“代码”。即使该调用不是立即的灾难,您也可以覆盖vtable指针。销毁数组几乎肯定会删除一些无意义的“指针”。
指向单个对象的指针(分配和删除时不带“[]”)正常。可以强制转换为Base*、dynamic_cast back和调用虚函数。这就是为什么Ayxan Haqverdili的建议有效。X的数组除了运行时类型X的对象之外不应该有任何东西,并且new[](甚至new[1])创建数组。

wswtfjt7

wswtfjt75#

其他人已经解释了这个问题,但是如果你想要一个多态对象数组,你需要一个指针数组。这里有一个方法:

vector<unique_ptr<Animal>> animals;
animals.push_back(std::make_unique<Cat>());
animals.push_back(std::make_unique<Dog>());
animals.push_back(std::make_unique<Monkey>());
lo8azlld

lo8azlld6#

使Cat析构函数也是虚拟的,代码就可以按预期工作了。

相关问题