析构函数执行期间vptr是否会改变?

10

我在看这篇文章,里面说:“当进入基类析构函数时,对象成为一个基类对象,并且C++中的所有部分——虚函数、dynamic_cast等——都会以这种方式处理它。” 这是否意味着在销毁过程中vptr已经改变了?那是如何发生的呢?


2
这都是非常具体实现相关的。 - Fred Foo
5
除非考虑不使用虚表的实现,否则这很少是特定于实现的。在使用虚函数的所有实现中,都必须在析构期间更改 vptr - David Rodríguez - dribeas
4
@Larsmans,我认为可以安全地假设任何提到虚函数表和虚函数指针的问题都带有限定语“在使用虚函数表和虚函数指针的实现中……”。这样我们就不必一遍又一遍地看到吹毛求疵的评论指出虚函数表和虚函数指针是标准不需要的实现细节了。 - Rob Kennedy
2个回答

13
在所有使用虚函数表的实现中(即所有当前的C++实现),答案是肯定的,vptr会被更改为正在执行的析构函数类型的类型。原因是标准要求正在销毁的对象的类型与正在执行的析构函数的类型相同
如果您有一个三个类型B、D、MD(基类、派生类、最派生类)的层次结构,并且您实例化并销毁一个MD类型的对象,在执行MD::~MD()时对象的类型MD,但当隐式调用基类析构函数时,对象的运行时类型必须是D。这是通过更新vptr来实现的。

3
类型必须正确的原因是它是可观察的,通过在销毁期间调用虚函数来观察。必须调用正确的重载函数,并且它可能与销毁开始前将要调用的重载函数不同,这意味着vptr必须已更新。 - Jonathan Wakely
1
@JonathanWakely:没错,在析构期间不更新vptr的情况下,调用虚函数可能会被分派到一个类型的覆盖者中,而该类型的析构函数已经完成(这是其他语言(如Java)中的行为)。 - David Rodríguez - dribeas
这也适用于构造函数吗? - hl3mukkel
2
@hl3mukkel:没错,只不过方向相反(从基类到派生类)。 - David Rodríguez - dribeas

6

当然,严格的C++回答是:“标准并没有关于虚函数表或多态实现的规定。”

但实际上,在实践中,是的。在基类析构函数开始执行之前,vtbl已经被修改。

编辑:

以下是我使用MSVC10来展示这个过程的代码:

#include <string>
#include <iostream>
using namespace std;

class Poly
{
public:
    virtual ~Poly(); 
    virtual void Foo() const = 0;
    virtual void Test() const = 0 { cout << "PolyTest\n"; }
};

class Left : public Poly
{
public:
    ~Left() 
    { 
        cout << "~Left\n"; 
    }
    virtual void Foo() const {  cout << "Left\n"; }
    virtual void Test() const  { cout << "LeftTest\n"; }
};

class Right : public Poly
{
public:
    ~Right() { cout << "~Right\n"; }
    virtual void Foo() const { cout << "Right\n"; }
    virtual void Test() const { cout << "RightTest\n"; }
};

void DoTest(const Poly& poly)
{
    poly.Test();
}

Poly::~Poly() 
{  // <=== BKPT HERE
    DoTest(*this);
    cout << "~Poly\n"; 
}

void DoIt()
{
    Poly* poly = new Left;
    cout << "Constructed...\n";
    poly->Test();
    delete poly;
    cout << "Destroyed...\n";
}

int main()
{
    DoIt();
}

现在,在Poly析构函数的左花括号处设置一个断点。
当你运行这段代码并在左花括号处中断时(就在构造函数体开始执行之前),你可以看一眼vptr:enter image description here 此外,你可以查看Poly析构函数的反汇编代码:
Poly::~Poly() 
{ 
000000013FE33CF0  mov         qword ptr [rsp+8],rcx  
000000013FE33CF5  push        rdi  
000000013FE33CF6  sub         rsp,20h  
000000013FE33CFA  mov         rdi,rsp  
000000013FE33CFD  mov         ecx,8  
000000013FE33D02  mov         eax,0CCCCCCCCh  
000000013FE33D07  rep stos    dword ptr [rdi]  
000000013FE33D09  mov         rcx,qword ptr [rsp+30h]  
000000013FE33D0E  mov         rax,qword ptr [this]  
000000013FE33D13  lea         rcx,[Poly::`vftable' (13FE378B0h)]  
000000013FE33D1A  mov         qword ptr [rax],rcx  
    DoTest(*this);
000000013FE33D1D  mov         rcx,qword ptr [this]  
000000013FE33D22  call        DoTest (13FE31073h)  
    cout << "~Poly\n"; 
000000013FE33D27  lea         rdx,[std::_Iosb<int>::end+4 (13FE37888h)]  
000000013FE33D2E  mov         rcx,qword ptr [__imp_std::cout (13FE3C590h)]  
000000013FE33D35  call        std::operator<<<std::char_traits<char> > (13FE3104Bh)  
}
000000013FE33D3A  add         rsp,20h  
000000013FE33D3E  pop         rdi  
000000013FE33D3F  ret  

跨过下一行,进入析构函数体内,再次查看vptr:

enter image description here 现在当我们从析构函数体内调用DoTest时,vtbl已经被修改为指向purecall_,这将在调试器中生成运行时断言错误:


3
我认为这个测试并不具有决定性,即使它是确定的,也会指向完全的怪异,其中vtable机制将在析构函数中分派到一个从未在vtable中的函数指针(在析构开始之前)。这个特定测试的问题在于你正在直接从析构函数调用虚指针,在那时因为编译器知道对象的确切类型(Poly),它不使用动态分派,而是使用静态分派。要修复测试,您可以创建一个函数,该函数接受Poly&并对其调用Test,从而强制分派。 - David Rodríguez - dribeas
由于虚函数表是实现特定的,因此在析构函数中查看*reinterpret_cast<intptr_t *>(this)可能会更简单? :) - user396672

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