我有一个像这样的简单程序:
#include "stdafx.h"
#include <iostream>
using namespace std;
int main()
{
class B {
protected:
int data = 0;
public:
B() { cout << "B() ctor\n";}
virtual ~B() { cout << "~B()\n"; }
virtual void method() { cout << "data in B: " << data << "\n"; }
};
class A : public B
{
int dataA = 2;
public:
A() { cout << "A() ctor\n"; }
~A() { cout << "~A()\n"; }
void method() { cout << "data in A: " << dataA << "\n"; }
};
{
B* fptrList[]{ &B{}, &A{}};
for (auto& itr : fptrList)
itr->method();
}
cin.get();
return 0;
}
这是我期望的结果:
B() ctor
B() ctor
A() ctor
data in B: 0
data in A: 2
~A()
~B()
~B()
以下是我运行此程序时的实际结果:
B() ctor
~B()
B() ctor
A() ctor
~A()
~B()
data in B: 0
data in B: 0
我的问题是:
答案 0 :(得分:0)
好的,这是未定义的行为,但问题仍然很有趣,为什么这个未定义的行为。
为什么按此顺序调用构造函数/析构函数?如前所述,创建一个接一个地创建/销毁的临时对象。
为什么我可以调用已经不存在的对象的方法?你的临时对象存在于堆栈中,因此内存只会在main
的末尾释放功能,因此您仍然可以访问此内存,并且不会因调用其他功能(例如打印到终端)而受到破坏。如果您使用new
创建对象,而不是删除它而不是尝试使用它 - 可能会更高,系统已经回收了此内存并且您会收到分段错误。
为什么我看到B调用方法的2倍?这个很有趣。为了调用对象的虚函数,编译器将决定应该调用哪个方法的决策委托给虚拟表(它的地址占据了这个对象的前8个字节(至少对于我的编译器和64位))。关于虚方法的一个众所周知的细节是,在析构函数调用期间,所有虚拟方法都被调用,就好像它们不是虚拟方法一样。但是你的代码有什么用呢?您会看到它的副作用:通过使用当前类的虚拟表覆盖当前对象的虚拟表,可以在析构函数中确保非虚拟行为。因此,在调用B
的析构函数之后,内存中包含您可以看到的类B
的虚拟表,因为B::method
被调用了两次。
让我们跟踪程序中虚拟表的值:
A{}
:首先调用超类B
- 构造函数 - (尚未完全完成的)对象具有类B
的虚拟表(此地址被移入对象占用的前8个字节),而不是A
- 构造函数被调用 - 现在该对象具有类A
的虚拟表。~A()
的调用:执行后,A
的析构函数会自动调用B
的析构函数。 B
的析构函数所做的第一件事就是使用B类的虚拟表覆盖对象的虚拟表。B
的虚拟表。itr->method();
在地址B
处找到类itr
的虚拟表,并指向B::method()
。