为什么通过基指针删除[]一组派生对象是未定义的行为?

时间:2011-05-30 02:35:40

标签: c++ undefined-behavior dynamic-arrays delete-operator

我在5.3.5 [expr.delete] p3下的C ++ 03标准中找到了以下代码段:

  

在第一个备选方案(删除对象)中,如果要删除的对象的静态类型与其动态类型不同,则静态类型应为操作数的动态类型的基类,静态类型应具有虚拟析构函数或行为未定义。 在第二种方法(删除数组)中,如果要删除的对象的动态类型与其静态类型不同,则行为未定义。


快速审查静态和动态类型:

struct B{ virtual ~B(){} };
struct D : B{};

B* p = new D();

p的静态类型为B**p的动态类型为D1.3.7 [defns.dynamic.type]

  

[示例:如果指针p的静态类型是“指向class B的指针”指向class D的对象,派生自{{ 1}},表达式B的动态类型为“*p。”]


现在,再次查看顶部的引用,这意味着如果我正确的话,以下代码会调用未定义的行为,无论是否存在D析构函数:

virtual

我是否以某种方式误解了标准中的措辞?我忽略了什么吗?为什么标准将此指定为未定义的行为?

5 个答案:

答案 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的大小来计算要访问的内存位置,如果BD的大小不同,则不会导致预期结果

就像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以正确的顺序调用析构函数。