虚函数的意外行为?

3
当我在Visual Studio 2010中运行以下C++代码时,如果任何派生类函数被声明为虚函数,程序将在变量删除处卡住。有人能解释一下吗?
void testInheritance()
{
    class a
    {
        public :
            char x;
            void fn1()
            {
                std::cout<<"\n In Class A Function 1 : "<<x;
            }
            virtual void fn2()
            {
                std::cout<<"\n In Class A Function 2 : "<<x;
            }
            a()
            {
                x='A';
                std::cout<<"\n In A() : "<<x;
            }
            ~a()
            {
                std::cout<<"\n In ~A : "<<x;
            }
    };

    class b: public a
    {
        public :
            char y;
            virtual void fn1()
            {
                std::cout<<"\n In Class B Function 1 : "<<y;
            }
             void fn3()
            {
                std::cout<<"\n In Class B Function 3 : "<<y;
            }
            b()
            {
                y='B';
                std::cout<<"\n In B() : "<<y;
            }
            ~b()
            {
                std::cout<<"\n In ~B : "<<y;
            }
    };

    a* var = new b();
    delete var;
}

更多信息:
我知道要调用b::fn1和类b的析构函数,需要在基类即a类中声明它们为虚函数。但是如果我不这样做,也不声明b类(也不是a类)中的任何函数为虚函数,则应该调用a的fn1和析构函数,并且这确实发生了。但是当我将b类(但不是a类)的任何成员声明为虚函数,无论是新成员还是重载成员,使用VS2010编译时会挂起,使用linux上的gcc4.4.4编译时则会出现错误。它应该调用其中一个析构函数并正常工作,但我无法理解程序崩溃的原因。
进一步地,在使用Visual Studio 2010中的Intellitrace时,我试图在挂起代码的位置中断,会收到以下消息:
该进程似乎已死锁(或未运行任何用户模式代码)。 所有线程都已停止。

请注意,因为您在afn1声明之前没有放置virtual关键字,所以a* bb = new b; bb->fn1();将调用a::fn1 - Matthieu M.
5个回答

4
您期望出现“意外行为”,因为您在程序中创建了一个“未定义的行为”。使用指向具有“非虚析构函数”的“基类”的指针删除“派生类”对象会导致“未定义的行为”。未定义的行为意味着任何事情都可能发生。C++标准第1.3.24节规定:“允许的未定义行为范围从完全忽略具有不可预测结果的情况,到在环境特征下进行翻译或程序执行的记录方式(带或不带发出诊断消息),到终止翻译或执行(带发出诊断消息)。”如何解决这个问题?在基类中将析构函数设置为虚函数即可。

感谢Matthieu M.提供的信息,但如果我在类b中没有声明任何函数为虚函数,则会调用a的析构函数,并且可以正常工作而不会出现任何卡顿/崩溃。我可能希望将此函数虚拟化以便进一步派生类,但不是从a到b。在这种情况下,它不应该崩溃/阻塞。 - Archit Jain

2
您的析构函数不是虚函数,因此您不能使用基类指针删除变量。很可能您只是在存在其他虚函数时获得了两组行为。

2

您需要声明析构函数虚拟


如果在类b中我没有声明任何其他函数为虚函数,那它是如何工作的? - Archit Jain
无论你的类中有多少虚函数,析构函数都使用与其他成员函数相同的函数调用机制。 - Andrey Sidorov

1
如果“卡住”意味着b::~b()没有被调用,那么答案是,a::~a()需要是virtual
您正在使用一个基类(a)指针来持有class b的对象。当您执行delete var;时,它只调用了a::~a(),而这个函数不是virtual的;通过将其设置为virtual,可以按正确顺序调用ab的析构函数。
[注意:另一种情况是,如果您在某个地方设置了断点并且没有逐步执行,它也会被卡住。 :) ]

删除指向派生类对象的多态基类指针是未定义行为,因此技术上可以发生任何事情,尽管不太可能(程序也可能被卡住)。 - Alok Save
不,这里的“stuck”指的是阻塞。我已经在问题中添加了更多信息。 - Archit Jain

0

我实在是厌倦了在C++测试中看到这种问题,问在这种情况下的行为会是什么。他们希望你回答它将调用A的析构函数但不会调用B的。

这并不是保证的行为,你不能依赖它。未定义的行为意味着你无法确定会发生什么,这在这里也是如此。

这也是“不要这样做”的一个例子。在我的上一份工作中,我完全从系统中删除了一个测试,因为它与主题无关且不相关。

另一种选择是将a的析构函数设置为虚函数或将其设置为protected。这也可以保护你,因为main()将无法编译,因为你无法从那里调用delete var。你甚至不能通过像main一样做同样的事情来调用b的未定义行为,因为你可能会感到惊讶,但是对于a*delete在那里也是不可访问的。

boost::shared_ptr<a>( new b );

安全地创建一个b的删除器而不是 a 的删除器。

虽然 a 中有另一个虚函数,但您几乎肯定应该选择使其析构函数为虚函数的选项。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接