为什么多态不适用于C ++中的数组?

时间:2016-12-13 03:56:10

标签: c++ polymorphism standards virtual destructor

#include <iostream>

using namespace std;

struct Base
{
    virtual ~Base()
    {
        cout << "~Base(): " << b << endl;
    }

    int b = 1;
};

struct Derived : Base
{
    ~Derived() override
    {
        cout << "~Derived(): " << d << endl;
    }

    int d = 2;
};

int main()
{
    Base* p = new Derived[4];
    delete[] p;
}

输出如下:(带有Clang 3.8的Visual Studio 2015)

~Base(): 1
~Base(): 2
~Base(): -2071674928
~Base(): 1

为什么多态不适用于C ++中的数组?

4 个答案:

答案 0 :(得分:20)

鉴于,

Base* p = Derived[4];

C ++ 11标准制作

delete [] p;

是未定义的行为。

  

5.3.5删除

     

...

     

2 ...在第二种方法(删除数组)中,如果要删除的对象的动态类型与其静态类型不同,则行为未定义。

从内存布局的角度来看,为什么delete [] p;会导致未定义的行为也是有道理的。

如果sizeof(Derived)N,则new Derived[4]会分配如下内存:

+--------+--------+--------+--------+
|   N    |   N    |   N    |   N    |
+--------+--------+--------+--------+

一般来说,sizeof(Base)&lt; = sizeof(Derived)。在您的情况下,sizeof(Base)&lt; sizeof(Derived),因为Derived有一个额外的成员变量。

使用时:

Base* p = new Derived[4];
你有:

p
|
V
+--------+--------+--------+--------+
|   N    |   N    |   N    |   N    |
+--------+--------+--------+--------+

p+1指向sizeof(Base) < sizeof(Derived)以来第一个对象中间的某个位置。

       p+1
       |
       V
+--------+--------+--------+--------+
|   N    |   N    |   N    |   N    |
+--------+--------+--------+--------+

p+1上调用析构函数时,指针不指向对象的开头。因此,该程序表现出不明确行为的症状。

相关问题

由于BaseDerived的大小不同,您无法使用p迭代动态分配的数组的元素。

for ( int i = 0; i < 4; ++i )
{
   // Do something with p[i]
   // will not work since p+i does not necessary point to an object
   // boundary.
}

答案 1 :(得分:14)

您得到未定义的行为,因为运算符delete[]不知道数组中存储了哪种对象,因此它信任静态类型来决定单个对象的偏移量。标准说明如下:

  

在第二种方法(删除数组)中,如果要删除的对象的动态类型与其静态类型不同,则行为是未定义的。

数组的静态类型需要与您用于分配的元素类型相匹配:

Derived* p = new Derived[4]; // Obviously, this works

This Q&A详细介绍了标准有此要求的原因。

至于修复此行为,您需要创建一个指针数组,常规或智能(最好是智能以简化内存管理)。

答案 2 :(得分:4)

它与covariance and contravariance的概念有关。我将举一个例子,希望澄清事情。

我们将从一个简单的层次结构开始。假设我们正在处理在现实世界中创建和销毁对象的问题。我们有3D对象(如木块)和2D对象(如纸张)。可以将2D对象视为3D对象,但高度可忽略不计。这是正确的子类化方向,因为反之则不正确; 2D物体可以平放在彼此之上,这对于任意3D物体来说是非常困难的。

产生对象的东西,如打印机,是协变。假设你的朋友想要借用3D打印机,取出它产生的十个物体,并将它们粘在一个盒子里。你可以给他们一台2D打印机;它会打印十页,你的朋友会把它们放在一个盒子里。但是,如果该朋友想要拍摄十个2D对象并将它们粘贴在一个文件夹中,则无法为它们提供3D打印机。

消耗物体的东西,如碎纸机,是逆变。你的朋友有一个包含打印页面的文件夹,但它没有卖,所以他想要粉碎它。你可以给他们一台3D碎纸机,就像用来粉碎汽车之类的工业用的那样,它会很好用;喂它的页面使它没有任何困难。另一方面,如果他想要撕碎他早先得到的物品盒,你就不能给他2D碎纸机,因为物体可能不适合插槽。

数组不变;它们都使用对象(具有赋值)并生成对象(具有数组访问权限)。作为这种关系的一个例子,拿一台传真机。如果你的朋友想要一台传真机,他们需要他们要求的确切类型;它们不能有超级或子类,因为您不能将3D对象粘贴到2D纸张的插槽中,并且您无法将3D传真机生成的对象绑定到书中。

答案 3 :(得分:2)

虽然其他两个答案(herehere)已经从技术方面解决了问题并且很好地解释了为什么编译器会尝试编译您给出的代码时遇到一个不可能完成的任务它,他们没有解释你的问题的概念问题。

当我们讨论类 - 子类关系时,多态性只能工作。因此,当我们遇到您编码的情况时,我们有:

enter image description here

我们说“Derived的所有实例都是Base的实例”。请注意,必须持有,否则我们甚至无法开始讨论多态性。在这种情况下它保持,因此我们可以使用指向Derived的指针,其中代码需要指向Base的指针。

但是你试图做一些不同的事情:

enter image description here

我们遇到了问题。虽然在集合论中我们可以说一组子类也是一组超类,但在编程中却不是这样。由于差异是“仅两个字符”,所以问题有所增加。但是元素数组在某种程度上与任何一个元素完全不同。

也许如果您使用std::array模板重新编写代码,那将更加明确:

#include <iostream>
#include <array>

struct Base
{   
    virtual ~Base()
    {
        std::cout << "~Base()" << std::endl;
    }
};

struct Derived : Base
{   
    ~Derived() override
    {
        std::cout << "~Derived()" << std::endl;
    }
};

int main()
{
    std::array<Base, 4>* p = new std::array<Derived, 4>;
    delete[] p;
}

这段代码显然无法编译,模板根据其参数不会成为彼此的子类。在某种程度上,这就像期望指向一个类的指针成为指向完全不相关的类的指针,这根本不起作用。

希望这有助于一些想要更直观地了解正在发生的事情的人,而不是技术解释。