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

suh*_*han 43 c++ arrays polymorphism inheritance dynamic-memory-allocation

我有一个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;
}
Run Code Online (Sandbox Code Playgroud)

这给出了输出:

动物构造函数
猫构造函数
动物析构函数

我不明白Cat当我的基类析构函数是虚拟的时,为什么不调用 的析构函数?

Mik*_*ine 42

请注意,虽然 aCat是 an ,但sAnimal的数组不是s的数组。换句话说,数组在 C++ 中是不变的,而不是像其他语言中那样是协变的。CatAnimal

所以你要向上转换这个数组,这会让编译器感到困惑。delete[]在这种情况下,您必须在正确的原始类型 - 上执行数组Cat*

Cat请注意,如果您分配了 2 个或更多s 的数组,将其转换为 anAnimal*然后尝试使用第二个或后续的 Animal,您会出于同样的原因遇到类似的问题。

  • `std::vector` 来救援 (4认同)
  • 如果您在“Animal”中有一个数据成员并向“Cat”添加了第二个数据成员,那么这一点会变得更加清晰。从那时起 `sizeof(Animal)!=sizeof(Cat)`,数组索引将完全关闭。指向“Animal”的“智能指针”数组将按预期工作,因为将为每个指针调用析构函数,并且指针是真正的多态性。 (3认同)

AnA*_*ons 10

根据我的理解,这是未定义的行为,因为(在 7.6.2.9 删除,p2 中,强调我的):

单对象删除表达式中,delete 操作数的值可以是空指针值、由先前的非数组 new 表达式产生的指针值,或者指向由以下方法创建的对象的基类子对象的指针:如此新的表达方式。如果不是,则行为未定义。在数组删除表达式中,删除操作数的值可以是空指针值 或由先前数组新表达式产生的指针值...

这基本上意味着该delete[]类型必须与来自的类型完全相同new[](不允许有基类子对象,例如delete)。

因此,出于这样的原因 - 在我看来,这次是显而易见的 - 实现需要知道完整对象大小是多少,以便它可以迭代到下一个数组元素。

我写了反驳论据来解释为什么这可能会有所不同 - 但经过一番思考(并阅读评论) - 我意识到这样的解决方案正在解决 XY 问题。

  • 但这会鼓励危险行为。如果你允许这样做,那么你基本上是在说“Animal* a = new Cat[2];” 删除[] a;`就可以了。这隐含着“a”本身很有用,而实际上它却没有什么用处。此时,“a[1]”是未定义的行为,因为数组索引使用类型(“a[i]”定义为“(a+i)”),该类型在“a”上移动“sizeof(Animal)”字节并且_not_它必须执行的`sizeof(Cat)`字节。因此,通过不允许“a”全面有用来避免整个问题可能是正确的做法。 (2认同)

Oer*_*ted 9

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

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

https://en.cppreference.com/w/cpp/language/reinterpret_cast#Type_aliasing

非正式地说,如果忽略顶级简历资格,则两种类型是相似的:

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

据我了解,继承不是相似性......

  • 继承确实不可能是相似的。请注意,该数组包含值,而不是引用,并且“Cat”值大于“Animal”值,因此它不适合放在那里。这使得从“Cat *”到“Animal *”的转换在指向数组时无效,但由于该类型并不表明它指向数组并且对于单个实例来说它是有效的,因此程序员必须注意这一点。 (5认同)
  • 啊,是的 - 请注意,如果您分配不止一只猫,那么例如 `j[1]` 会为第二只猫计算错误的地址,因为它使用 sizeof(Animal) 而不是 sizeof(Cat) (3认同)

Jor*_*mse 7

我对其他答案的唯一批评是它们太温和了。数组之所以有效,是因为所有条目都具有相同的大小,因此可以轻松计算出索引为 5 的条目的起始地址。将 Derived 数组转换为 Base 数组(这就是您在这里所做的)可能会产生比您所显示的更糟糕的后果。(如果数组恰好只有一个条目或者 Derived 没有新的数据成员,那么您可能没问题。) A[1].method() 可能会做一些奇怪的事情,因为 (Base*)(A)+1 甚至不是任何对象的起始地址。该方法将读取错误的数据,并且任何更改的影响都将是高度不可预测的。对虚函数的任何调用都将使用完全无意义的 vtable 指针,因此谁知道将执行什么“代码”。即使该调用不会立即造成灾难,您也可能会覆盖 vtable 指针。销毁数组几乎肯定会删除一些无意义的“指针”。

指向单个对象的指针(在没有“[]”的情况下分配和删除)是可以的。您可以转换为 Base*、dynamic_cast 返回,并调用虚函数。这就是 Ayxan Haqverdili 的建议有效的原因。X 的数组不应该包含除运行时类型 X 的对象之外的任何内容,并且 new[] (甚至 new[1])创建数组。