【问题标题】:Why does polymorphism not apply on arrays in C++? [duplicate]为什么多态性不适用于 C++ 中的数组? [复制]
【发布时间】:2017-04-28 01:33:32
【问题描述】:
#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;
}

输出如下:(Visual Studio 2015 with Clang 3.8)

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

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

【问题讨论】:

  • 多态只能通过指针起作用,数组的元素不是指针,它们是值类型。您将需要一个指针数组并分别删除它们。 (或智能指针向量)
  • 我同意@Galik 的评论,但它对我来说很好,正确的多态行为。 sh-4.2$ g++ -std=c++11 -o main *.cpp ~Derived(): 2~Base(): 1 ~Derived(): 2~Base(): 1~Derived(): 2~Base (): 1 ~Derived(): 2~Base(): 1
  • 这不起作用,因为数组使用指针算法来访问数组 els,在你的情况下是 sizeof(Base) 而另一方面,你的数组中的一个元素是 sizeof(Derived) 访问时的结果数组 el 你会在错误的地址结束。
  • 在这里检查了相同的代码:- tutorialspoint.com/compile_cpp11_online.php ......工作正常吗?为什么?
  • 这段代码没有意义。想想sizeof(*p)

标签: c++ polymorphism standards virtual destructor


【解决方案1】:

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

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

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

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

This Q&A 更详细地说明了标准有此要求的原因。

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

【讨论】:

  • 它实际上是一个“删除数组表达式”,它依赖于静态类型和动态类型相同。 operator delete[] 是别的东西(尽管删除表达式使用的是别的东西)。
  • 或者一个智能指针向量来进一步简化它。
【解决方案2】:

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

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

产生对象的东西,比如打印机,是协变的。假设你的朋友想借一台 3D 打印机,拿出它生产的十个物体,然后把它们粘在一个盒子里。你可以给他们一台2D打印机;它会打印十页,你的朋友会把它们粘在一个盒子里。但是,如果那个朋友想拿十个 2D 对象并将它们粘在一个文件夹中,你就不能给他们一台 3D 打印机。

消耗物体的东西,比如碎纸机,是逆变的。你的朋友有一个文件夹,里面有它打印的页面,但它没有卖掉,所以他想把它撕碎。你可以给他们一台 3D 碎纸机,就像工业用的碎纸机一样,它可以很好地工作;喂它页面根本不会造成任何困难。另一方面,如果他想切碎他之前得到的那盒物体,你不能给他二维切碎机,因为这些物体可能不适合插槽。

数组是不变的;它们都使用对象(通过赋值)并产生对象(通过数组访问)。作为这种关系的一个例子,以某种传真机为例。如果您的朋友想要一台传真机,他们需要的正是他们想要的那种;它们不能有超类或子类,因为您不能将 3D 对象粘贴到 2D 纸张的插槽中,也不能将 3D 传真机生成的对象绑定到书籍中。

【讨论】:

    【解决方案3】:

    虽然其他两个答案(herehere)已经从技术方面解决了这个问题,并很好地解释了为什么编译器在尝试编译你给它的代码时会有一项不可能完成的任务,但他们确实做到了不要用你的问题解释概念上的问题。

    多态性在我们谈论类-子类关系时才起作用。因此,当我们遇到您编码的情况时,我们有:

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

    但是你正在尝试做一些不同的事情:

    这里有一个问题。虽然在集合论中我们可以说一组子类也是一组超类,但在编程中却不是这样。差异“只有两个字符”这一事实在一定程度上增加了问题。但是元素数组在某种程度上与这些元素中的任何一个都完全不同。

    也许如果您使用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;
    }
    

    这段代码显然不能编译,模板不会根据它们的参数成为另一个的子类。在某种程度上,这就像期望一个指向一个类的指针变成一个指向一个完全不相关的类的指针,这根本行不通。

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

    【讨论】:

      【解决方案4】:

      给定,

      Base* p = Derived[4];
      

      C++11 标准制定

      delete [] p;
      

      是未定义的行为。

      5.3.5 删除

      ...

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

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

      如果sizeof(Derived)Nnew Derived[4] 分配的内存类似于:

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

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

      使用时:

      Base* p = new Derived[4];
      

      你有:

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

      p+1 指向自sizeof(Base) &lt; 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.
      }
      

      【讨论】:

      • 解释清楚。
      • 从设计的角度来看,我看不出为什么删除不能使用数组指针中的信息。在 Fortran 中,多态数组最终确定得很好。
      • @VladimirF,我没有答案。
      猜你喜欢
      • 1970-01-01
      • 2011-07-01
      • 2014-01-21
      • 2021-10-31
      • 2023-03-09
      • 1970-01-01
      • 1970-01-01
      • 2011-05-22
      • 2019-08-18
      相关资源
      最近更新 更多