我在5.3.5 [expr.delete] p3
下的C ++ 03标准中找到了以下代码段:
在第一个备选方案(删除对象)中,如果要删除的对象的静态类型与其动态类型不同,则静态类型应为操作数的动态类型的基类,静态类型应具有虚拟析构函数或行为未定义。 在第二种方法(删除数组)中,如果要删除的对象的动态类型与其静态类型不同,则行为未定义。
快速审查静态和动态类型:
struct B{ virtual ~B(){} };
struct D : B{};
B* p = new D();
p
的静态类型为B*
,*p
的动态类型为D
,1.3.7 [defns.dynamic.type]
:
[示例:如果指针
p
的静态类型是“指向class B
的指针”指向class D
的对象,派生自{{ 1}},表达式B
的动态类型为“*p
。”]
现在,再次查看顶部的引用,这意味着如果我正确的话,以下代码会调用未定义的行为,无论是否存在D
析构函数:
virtual
我是否以某种方式误解了标准中的措辞?我忽略了什么吗?为什么标准将此指定为未定义的行为?
答案 0 :(得分:32)
Base* p = new Base[n]
创建一个n
大小的Base
元素数组,其中p
指向第一个元素。但是,Base* p = new Derived[n]
会创建一个n
大小的Derived
元素数组。 p
然后指向第一个元素的Base
子对象。 p
不然后引用数组的第一个元素,这是有效的delete[] p
表达式所需要的。
当然有可能在这种情况下授权(然后实施)delete [] p
做正确的事情。但它需要什么?实现必须注意以某种方式检索数组的元素类型,然后在道德上dynamic_cast
p
到此类型。然后就像我们已经做的那样做一个简单的delete[]
。
问题在于,无论是否使用多态,都需要每次多态元素类型的数组。在我看来,这不符合C ++的理念,即不为你不使用的东西付费。但更糟糕的是:启用多态的delete[] p
根本没用,因为p
在您的问题中几乎无用。 p
是一个指向元素子对象的指针,不再有;否则它与阵列完全无关。你当然不能p[i]
(对于i > 0
)。因此delete[] p
不起作用并不合理。
总结一下:
数组已经有很多合法用途。通过不允许数组以多态方式运行(无论是作为整体还是仅作为delete[]
),这意味着具有多态元素类型的数组不会因这些合法用途而受到惩罚,这符合C ++的理念。
如果另一方面需要具有多态行为的数组,则可以根据我们已经实现的数组来实现。
答案 1 :(得分:12)
将一个派生数组作为基数数组处理是错误的,而不仅仅是在删除项目时。例如,即使只访问元素也会导致灾难:
B *b = new D[10];
b[5].foo();
b[5]
将使用B
的大小来计算要访问的内存位置,如果B
和D
的大小不同,则不会导致预期结果
就像std::vector<D>
无法转换为std::vector<B>
一样,指向D[]
的指针不应转换为B*
,但出于历史原因,无论如何编译。如果改为使用std::vector
,则会产生编译时错误。
C++ FAQ Lite answer on this topic中也对此进行了解释。
因此delete
会导致未定义的行为,因为以这种方式处理数组已经是错误的,即使类型系统无法捕获错误。
答案 2 :(得分:1)
只是为了增加 某些 的优秀答案 - 我写了一个简短的例子,用不同的偏移来说明这个问题。
请注意,如果您注释掉Derived类的m_c成员,则删除操作将正常运行。
干杯,
盖。
#include <iostream>
using namespace std;
class Base
{
public:
Base(int a, int b)
: m_a(a)
, m_b(b)
{
cout << "Base::Base - setting m_a:" << m_a << " m_b:" << m_b << endl;
}
virtual ~Base()
{
cout << "Base::~Base" << endl;
}
protected:
int m_a;
int m_b;
};
class Derived : public Base
{
public:
Derived()
: Base(1, 2) , m_c(3)
{
}
virtual ~Derived()
{
cout << "Derived::Derived" << endl;
}
private:
int m_c;
};
int main(int argc, char** argv)
{
// create an array of Derived object and point them with a Base pointer
Base* pArr = new Derived [3];
// now go ahead and delete the array using the "usual" delete notation for an array
delete [] pArr;
return 0;
}
答案 3 :(得分:0)
我认为这一切都归结为零开销原则。即该语言不允许存储有关阵列元素的动态类型的信息。
答案 4 :(得分:0)
恕我直言,这与数组的限制有关,以处理构造函数/析构函数。请注意,当调用new[]
时,编译器强制仅实例化默认构造函数。以同样的方式调用delete[]
时,编译器可能只查找调用指针静态类型的析构函数。
现在在virtual
析构函数的情况下,应首先调用Derived类析构函数,然后调用Base类。因为对于数组编译器可能看到调用对象的静态类型(这里是Base)类型,它最终可能只调用Base析构函数;这是UB。
话虽如此,它并不一定是所有编译器的UB;说for example gcc以正确的顺序调用析构函数。