为什么输出与我所期望的不同?
我有一个简单的程序是这样的:为什么输出与我所期望的不同?
#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
我问题是:
- 为什么输出与我所期望的不同?
- 在调用〜A()和〜B()后,如何调用method()方法?
- 为什么类B的method()被调用两次?
该程序无法解释,因为它展示未定义的行为。
翻译:它的越野车。这是临时对象的地址,然后在临时对象被破坏后尝试对它们进行解引用。
一个很好的C++编译器,甚至会告诉你该程序被打破,将拒绝参加这场灾难:
t.C: In function ‘int main()’:
t.C:26:27: error: taking address of temporary [-fpermissive]
B* fptrList[]{ &B{}, &A{}};
^
t.C:26:33: error: taking address of temporary [-fpermissive]
B* fptrList[]{ &B{}, &A{}};
^
该程序的任何输出是毫无意义的垃圾。
这是怎么回事:
- 初始化
fptrList
临时变量A
和B
- 临时变量被摧毁的将它们的地址之后的地址,让你的代码是未定义行为。
- 正确的做法是尝试使用运算符
new
和智能指针,或者在初始化程序之外创建实例。
这里是一个可能的解决办法:
{
B b;
A a;
B* fptrList[]{ &b, &a };
for (auto& itr : fptrList)
itr->method();
}
快速提问:为什么在他们的地址被采取后,而不是在声明结束后,临时销毁? – Rakete1111
@ Rakete1111由于在获取地址后不需要临时对象,因此编译器可以*决定何时调用析构函数。看起来你的编译器决定马上销毁它们。 – dasblinkenlight
我知道你的“修复”是正确的方法。我只是试了一下,看到它有多奇怪。 –
好吧,这是不确定的行为,但问题仍然是有趣的,为什么它是这个不确定的行为。
为什么按此顺序调用构造函数/析构函数?如已经建立,您创建临时对象,它们是相继创建/销毁的。
为什么我可以调用已经不存在的对象的方法?您的临时对象位于堆栈上,因此内存将仅在
main
函数结束时释放,因此您仍然可以访问此内存,并且不会因调用其他函数而遭到破坏(例如,打印到终端)。如果您要创建new
的对象,那么要删除它并尝试使用它 - 机会会更高,系统已经回收了此内存,并且会出现分段错误。为什么我看到2次B的方法叫?这个很有趣。为了调用一个对象的虚函数,编译器委托决定哪个方法应该被调用到一个虚拟表(它的地址占用了这个对象的前8个字节(至少对于我的编译器和64位))。关于虚拟方法的众所周知的细节是,在析构函数调用期间,所有虚拟方法都被调用,就好像它们不是虚拟的。但是你的代码有什么用处呢?您会看到它的副作用:在析构函数中通过用当前类的虚拟表覆盖当前对象的虚拟表来确保非虚拟行为。因此,在调用
B
的析构函数后,内存中将包含类B
的虚拟表,您可以看到它,因为B::method
被调用了两次。
让我们跟踪虚表它的价值在你的程序:
通话A{}
- :起初,超
B
-constructor被称为 - 的(尚未完全完成)对象有类B
的虚拟表(该地址被移入对象所占用的前8个字节),而不是A
-constructor被调用 - 现在对象具有类A
的虚拟表。 - 调用
~A()
:执行后,A
的析构函数自动调用B
的析构函数。B
的析构函数所做的第一件事是用类B的虚拟表覆盖对象的虚拟表。 - 因此,在销毁之后,内存仍然存在并且被解释为对象将具有虚拟表类
B
。 -
itr->method();
找到类B
的虚拟表itr
指向并呼叫B::method()
。
这只是UB,试图解释行为是没有意义的。 – songyuanyao
我不这么认为。您看到对method()的调用获得成功并打印出正确的数据值。如果这些对象实际上被销毁,那么调用访问“data”成员的method()应该会引发一个运行时错误。 –
这是UB,没有保证。它可能会引起运行时错误,可能会运行良好(不幸的是)。 – songyuanyao