为什么多态不适用于 C++ 中的数组?
Why does polymorphism not apply on arrays in C++?
#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++ 中的数组?
你会得到未定义的行为,因为运算符 delete[]
不知道数组中存储的是哪种对象,因此它信任静态类型来决定各个对象的偏移量。标准如下:
In the second alternative (delete array) if the dynamic type of the object to be deleted differs from its static type, the behavior is undefined.
数组的静态类型需要与您用于分配的元素类型相匹配:
Derived* p = new Derived[4]; // Obviously, this works
This Q&A 详细说明了标准有此要求的原因。
就修复此行为而言,您需要创建一个指针数组,常规或智能指针(最好是智能指针以简化内存管理)。
给定,
Base* p = Derived[4];
C++11 标准使得
delete [] p;
成为未定义的行为。
5.3.5 Delete
...
2 ... In the second alternative (delete array) if the dynamic type of the object to be deleted differs from its static type, the behavior is undefined.
从内存布局的角度来看,delete [] p;
会导致未定义的行为也是有道理的。
如果 sizeof(Derived)
是 N
,new 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) < sizeof(Derived)
.
以来第一个对象中间的某个位置
p+1
|
V
+--------+--------+--------+--------+
| N | N | N | N |
+--------+--------+--------+--------+
在 p+1
上调用析构函数时,指针不指向对象的开头。因此,该程序表现出未定义行为的症状。
相关问题
由于 Base
和 Derived
的大小不同,您不能使用 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.
}
虽然其他两个答案 ( and ) 从技术方面解决了问题,并且很好地解释了为什么编译器在尝试编译您提供的代码时会遇到不可能完成的任务,但他们确实做到了没有用你的问题解释概念上的问题。
当我们谈论 Class-Subclass 关系时,多态性 只有 起作用。因此,当我们遇到您所编码的情况时,我们有:
我们说 "All instances of the Derived
are also instances of Base
"。请注意,这个 必须 成立,否则我们甚至不能开始谈论多态性。在这种情况下,它成立,因此我们可以使用指向 Derived
的指针,其中代码需要指向 Base
.
的指针
但是你正在尝试做一些不同的事情:
这里有一个问题。虽然在集合论中我们可以说 subclass 的集合也是 superclass 的集合,但在编程中却不是这样。由于差异为 "only two characters",问题在某种程度上有所增加。但是元素数组在某种程度上与这些元素中的任何一个完全不同。
也许如果您使用 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;
}
此代码显然无法编译,模板不会根据其参数成为彼此的子class。在某种程度上,这就像期望指向一个 class 的指针变成指向完全不相关的 class 的指针一样,这根本行不通。
希望这能帮助一些想要更直观地了解正在发生的事情而不是技术解释的人。
跟covariance and contravariance的概念有关。我将举一个例子,希望能澄清一些事情。
我们将从一个简单的层次结构开始。假设我们正在处理现实世界中对象的创建和销毁。我们有 3D 对象(例如木块)和 2D 对象(例如纸张)。 2D 对象可以像 3D 对象一样对待,除了高度可以忽略不计。这是正确的子类化方向,因为反过来是不正确的; 2D 对象可以平放在彼此之上,这对于任意 3D 对象来说是非常困难的。
产生对象的东西,比如打印机,是协变的。假设你的朋友想借一台 3D 打印机,用它打印十个物体,然后把它们粘在一个盒子里。你可以给他们一台二维打印机;它会打印十页,你的朋友会把它们贴在一个盒子里。但是,如果那个朋友想拿十个 2D 对象并将它们粘贴到一个文件夹中,你不能给他们一台 3D 打印机。
消耗物体的东西,比如碎纸机,是逆变。您的朋友有一个文件夹,上面印有页面,但卖不出去,所以他想把它撕碎。你可以给他们一台 3D 粉碎机,就像用于粉碎汽车等东西的工业粉碎机,它会很好用;给它喂食页面不会给它带来任何困难。另一方面,如果他想粉碎他之前得到的物品箱,你不能给他2D粉碎机,因为物品可能不适合插槽。
数组是不变的;它们都使用对象(通过赋值)并生成对象(通过数组访问)。作为这种关系的一个例子,以某种传真机为例。如果你的朋友想要一台传真机,他们需要的正是他们想要的那种;它们不能有超类或子类,因为您不能将 3D 对象粘贴到 2D 纸的插槽中,也不能将 3D 传真机生成的对象绑定到一本书中。
#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++ 中的数组?
你会得到未定义的行为,因为运算符 delete[]
不知道数组中存储的是哪种对象,因此它信任静态类型来决定各个对象的偏移量。标准如下:
In the second alternative (delete array) if the dynamic type of the object to be deleted differs from its static type, the behavior is undefined.
数组的静态类型需要与您用于分配的元素类型相匹配:
Derived* p = new Derived[4]; // Obviously, this works
This Q&A 详细说明了标准有此要求的原因。
就修复此行为而言,您需要创建一个指针数组,常规或智能指针(最好是智能指针以简化内存管理)。
给定,
Base* p = Derived[4];
C++11 标准使得
delete [] p;
成为未定义的行为。
5.3.5 Delete
...
2 ... In the second alternative (delete array) if the dynamic type of the object to be deleted differs from its static type, the behavior is undefined.
从内存布局的角度来看,delete [] p;
会导致未定义的行为也是有道理的。
如果 sizeof(Derived)
是 N
,new 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) < sizeof(Derived)
.
p+1
|
V
+--------+--------+--------+--------+
| N | N | N | N |
+--------+--------+--------+--------+
在 p+1
上调用析构函数时,指针不指向对象的开头。因此,该程序表现出未定义行为的症状。
相关问题
由于 Base
和 Derived
的大小不同,您不能使用 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.
}
虽然其他两个答案 (
当我们谈论 Class-Subclass 关系时,多态性 只有 起作用。因此,当我们遇到您所编码的情况时,我们有:
我们说 "All instances of the Derived
are also instances of Base
"。请注意,这个 必须 成立,否则我们甚至不能开始谈论多态性。在这种情况下,它成立,因此我们可以使用指向 Derived
的指针,其中代码需要指向 Base
.
但是你正在尝试做一些不同的事情:
这里有一个问题。虽然在集合论中我们可以说 subclass 的集合也是 superclass 的集合,但在编程中却不是这样。由于差异为 "only two characters",问题在某种程度上有所增加。但是元素数组在某种程度上与这些元素中的任何一个完全不同。
也许如果您使用 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;
}
此代码显然无法编译,模板不会根据其参数成为彼此的子class。在某种程度上,这就像期望指向一个 class 的指针变成指向完全不相关的 class 的指针一样,这根本行不通。
希望这能帮助一些想要更直观地了解正在发生的事情而不是技术解释的人。
跟covariance and contravariance的概念有关。我将举一个例子,希望能澄清一些事情。
我们将从一个简单的层次结构开始。假设我们正在处理现实世界中对象的创建和销毁。我们有 3D 对象(例如木块)和 2D 对象(例如纸张)。 2D 对象可以像 3D 对象一样对待,除了高度可以忽略不计。这是正确的子类化方向,因为反过来是不正确的; 2D 对象可以平放在彼此之上,这对于任意 3D 对象来说是非常困难的。
产生对象的东西,比如打印机,是协变的。假设你的朋友想借一台 3D 打印机,用它打印十个物体,然后把它们粘在一个盒子里。你可以给他们一台二维打印机;它会打印十页,你的朋友会把它们贴在一个盒子里。但是,如果那个朋友想拿十个 2D 对象并将它们粘贴到一个文件夹中,你不能给他们一台 3D 打印机。
消耗物体的东西,比如碎纸机,是逆变。您的朋友有一个文件夹,上面印有页面,但卖不出去,所以他想把它撕碎。你可以给他们一台 3D 粉碎机,就像用于粉碎汽车等东西的工业粉碎机,它会很好用;给它喂食页面不会给它带来任何困难。另一方面,如果他想粉碎他之前得到的物品箱,你不能给他2D粉碎机,因为物品可能不适合插槽。
数组是不变的;它们都使用对象(通过赋值)并生成对象(通过数组访问)。作为这种关系的一个例子,以某种传真机为例。如果你的朋友想要一台传真机,他们需要的正是他们想要的那种;它们不能有超类或子类,因为您不能将 3D 对象粘贴到 2D 纸的插槽中,也不能将 3D 传真机生成的对象绑定到一本书中。