36

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

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


快速回顾静态和动态类型:

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

B* p = new D();

的静态类型pB*,而动态类型*pD1.3.7 [defns.dynamic.type]

[示例:如果p静态类型为“指向 ”class B的指针指向 的对象class D,从 派生B,则表达式的动态类型*p为“<code>D.”]


现在,再次查看顶部的引用,这意味着如果我做对了,下面的代码会调用未定义的行为,而不管是否存在virtual析构函数:

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

B* p = new D[20];
delete [] p; // undefined behaviour here

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

4

5 回答 5

32

Base* p = new Base[n]创建一个-size 的元素n数组,然后指向第一个元素。但是,会创建一个-size 的元素数组。然后指向第一个元素的子对象。然而,它并不引用数组的第一个元素,这是有效表达式所需要的。BasepBase* p = new Derived[n]nDerivedpBase pdelete[] p

当然,delete [] p在这种情况下,可以强制(然后实施)Does The Right Thing™。但这需要什么?实现必须小心以某种方式检索数组的元素类型,然后在道德上dynamic_cast p对这种类型进行检索。delete[]然后就像我们已经做的那样做一个简单的问题。

这样做的问题是,每次多态元素类型的数组时都需要这样做,无论是否使用多态性。在我看来,这不符合不为不使用的东西付费的 C++ 理念。但更糟糕的是:启用多态的功能delete[] p根本没用,因为p在您的问题中几乎没用。p是指向元素子对象的指针,仅此而已;否则它与数组完全无关。您当然不能p[i](for i > 0) 使用它。所以delete[] p不工作也不是没有道理的。

总结一下:

  • 数组已经有很多合法用途。通过不允许数组表现出多态性(作为一个整体或仅用于delete[]),这意味着具有多态元素类型的数组不会因这些合法用途而受到惩罚,这符合 C++ 的哲学。

  • 另一方面,如果需要具有多态行为的数组,则可以根据我们已有的实现一个。

于 2011-05-30T03:17:45.303 回答
12

将派生数组视为基数组是错误的,不仅在删除项目时。例如,即使只是访问元素通常也会导致灾难:

B *b = new D[10];
b[5].foo();

b[5]将使用 的大小B来计算要访问的内存位置,如果BD具有不同的大小,这将不会导致预期的结果。

就像 astd::vector<D>不能转换为 a一样std::vector<B>,指向的指针D[]也不应该转换为 a B*,但由于历史原因,它无论如何都会编译。如果std::vector改为使用 a ,则会产生编译时错误。

C++ FAQ Lite answer on this topic也对此进行了解释。

所以delete在这种情况下会导致未定义的行为,因为以这种方式处理数组已经是错误的,即使类型系统无法捕获错误。

于 2011-05-30T03:26:04.040 回答
1

只是为了添加sth的出色答案- 我写了一个简短的例子来说明这个问题与不同的偏移量。

注意,如果注释掉 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;
}
于 2017-10-16T07:30:58.230 回答
0

认为这一切都归结为零开销原则。即该语言不允许存储有关数组元素的动态类型的信息。

于 2011-05-30T02:57:12.987 回答
0

恕我直言,这与处理构造函数/析构函数的数组限制有关。请注意,当new[]被调用时,编译器强制仅实例化默认构造函数。与调用时相同delete[],编译器可能只查找调用指针的静态类型的析构函数。

现在在析构函数的情况下virtual,应首先调用派生类析构函数,然后调用基类。由于对于数组编译器可能会看到调用对象的静态类型(此处为 Base)类型,因此它可能最终只调用 Base 析构函数;这是UB。

话虽如此,它不一定适用于所有编译器;比如说gcc以正确的顺序调用析构函数。

于 2011-05-30T03:02:06.293 回答