【问题标题】:Order of calling virtual destructors in C++C++中调用虚析构函数的顺序
【发布时间】:2020-04-08 14:40:13
【问题描述】:

所以我一直试图通过 C++ 理解 OOP 概念,但是我无法获得虚拟析构函数的某些部分。

我写了一个小sn-p:

class A{
    int x;
public: 
    virtual void show(){
        cout << " In A\n"; 
    }
    virtual ~A(){
        cout << "~A\n";
    };
};

class B: public A{
    int y;
public: 
    virtual void show(){
        cout << " In B\n"; 
    }
    virtual ~B(){
        cout << "~B\n";
    };
};

class C: public A{
    int z;
public: 
    virtual void show(){
        cout << " In C\n"; 
    }
    virtual ~C(){
        cout << "~C\n";
    };
};
class E: public A{
    int z;
public: 
    virtual void show(){
        cout << " In E\n"; 
    }
    virtual ~E(){
        cout << "~E\n";
    };
};

class D: public B , public C , public E{
    int z1;
public: 
    virtual void show(){
        cout << " In D\n"; 
    }
    virtual ~D(){
        cout << "~D\n";
    };
};

signed main(){
    // A * a = new A();
    // B *b = new B();
    D *d = new D();
    B *b = d;
    C *c = d;
    E * e = d;
    A * a = new A();
    cout << d << "\n";
    cout << b  << "\n";
    cout  << c << "\n";
    cout << e << "\n";
    delete b;
    // a -> show();

}

在运行代码时,我得到的结果是:

0x7f8c5e500000
0x7f8c5e500000
0x7f8c5e500018
0x7f8c5e500030
~D
~E
~A
~C
~A
~B
~A

现在三个问题:

  • 根据维基百科文章virtual_table,提到对象 c 的地址比 d 和 b 的地址 +8 个字节,如果是 e 会发生什么。
  • 当我调用delete b而不是delete d时,也得到相同顺序的虚析构函数,那为什么派生类析构函数叫
  • 只有当我删除一个对象时才会调用虚拟析构函数,然后在程序结束时如何删除 vtable 和 vpointers(当我在没有 delete d 的情况下运行代码时,执行只是停止而不打印任何内容)。李>

【问题讨论】:

  • 是关于虚拟继承的吗?

标签: c++ oop destructor vtable virtual-destructor


【解决方案1】:

根据维基百科文章 virtual_table ,提到对象 c 获得的地址比 d 和 b 的地址 +8 个字节,在 e 的情况下会发生什么。

地址通常依赖于编译器,因此非常冒险。我不会指望它们有什么特别的价值。

当我调用delete b而不是delete d时,也得到相同顺序的虚析构函数,那么为什么要调用派生类析构函数

指针的类型无关紧要。底层对象是用new D() 创建的,所以这些是被调用的析构函数。这是因为否则可能很难正确删除对象 - 如果您有一个创建各种子类的工厂,您如何知道将其删除为哪种类型?

(这里实际发生的是(指向)析构函数的(指针)存储在对象的 vtable 中。)

只有在我删除一个对象时才会调用虚拟析构函数,然后如何在程序结束时删除 vtable 和 vpointers(当我在没有 delete d 的情况下运行代码时,执行只是停止而不打印任何内容)。

如果你从不删除某些东西,它就永远不会被清理干净。程序结束时不会从堆中释放该内存。这是“内存泄漏”。当程序结束时,操作系统会一次性清理整个程序的堆(而不关心里面有什么)。

【讨论】:

  • 可能值得指出的是,“所以那些是被调用的析构函数”是正确的,因为析构函数是虚拟的。如果不是,则 delete b 是未定义的行为(但可能会调用错误的析构函数)。
【解决方案2】:

您的问题按顺序排列:

(1) 是的,与指向最派生类型的指针相比,指向具有多重继承的派生类对象的基类指针可能会更改其数值。原因是基类是派生类的一部分,很像一个成员,位于偏移处。仅对于多继承中的第一个派生类,此偏移量可以为 0。这就是无法使用简单的 reinterpret_cast() 强制转换此类指针的原因。

(2) b 指向一个E,它也是is-an A

这正是virtual 对成员函数的意义:编译器生成的代码检查在运行时指向的对象并调用为对象的实际类型定义的函数(E),而不是用于访问该对象的表达式类型(B)。表达式的类型在编译时完全确定;实际完整对象的类型不是。

如果你没有声明一个虚拟的析构函数,程序可能会像你预期的那样运行:编译器将创建代码,它只调用为表达式类型定义的函数(对于B),没有任何运行时查看-UPS。非虚成员函数调用效率稍高;但是在析构函数的情况下,在您的情况下,当通过基类表达式进行破坏时,行为是 undefined 。如果您的析构函数是公共的,它应该是虚拟的,因为这种情况可能会发生。

Herb Sutter 编写了 an article about virtual functions,其中包括值得一读的虚拟析构函数。

(3) 内存,包括动态分配的内存,在程序退出时被现代标准操作系统释放并再次用于其他用途。 (在旧操作系统或独立实现中,如果它们提供动态分配,则可能不是这种情况。)但是,动态分配对象的 析构函数 不会被调用,如果它们持有,这可能是个问题最好释放数据库或网络连接等资源。

【讨论】:

  • 如果AB的析构函数不是虚拟的,那么表达式delete b(其中b是一个指向B的指针,但是指向一个类的实例源自 OP 代码中的 B) 具有未定义的行为。如果它表现得像 OP“可能预期的那样”,那是偶然的 - 并且不能保证。
  • @Peter 我以为我说过...为清楚起见进行编辑。
  • @Peter 我认为即使B 的析构函数(在您的示例中)是虚拟的,它也是未定义的。调用A *pa = new B{}; delete pa; 仍然会被静态解析(尽管如果现代编译器在那里警告我不会感到惊讶,因为它可以静态证明是错误的)。
【解决方案3】:

关于对象的地址。正如在另一个答案中已经解释的那样,这取决于编译器。不过还是可以解释的。

多重继承中的对象地址 (一种可能的编译器实现)

这是一个可能的内存图,假设指向虚拟表的指针是8字节,int是4字节。

D 类首先有它指向虚拟表(vtbl_ptr 或 vptr)的指针,然后是没有自己的 vtbl_ptr 的 B 类,因为它可以与 D 共享相同的 vtbl。

类 C 和 E 必须带有它们自己的嵌入式 vtbl_ptr。它将指向 D 的 vtbl(几乎......,有一个 thunk 问题需要处理,但让我们忽略它,您可以在下面的链接中阅读有关 thunk 的信息,但是这不会影响对额外 vtbl_ptr 的需求)。

每个附加基类的附加 vptr 是必需的,所以当我们查看 C 或 E 时,vptr 的位置总是在相同的位置,即在对象的顶部,无论它是否实际上是一个具体的 C或者它是作为 C 持有的 D。对于 E 和任何其他不是第一个继承基类的基类也是如此。

根据上面我们可能看到的地址:

D d; // sitting at some address X
B* b = &d; // same address
C* c = &d; // jumps over vtbl_ptr (8 bytes) + B without vtbl_ptr (8 bytes)
           // thus X + 16 -- or X + 10 in hexa
E* e = &d; // jumps in addition over C part including vtbl_ptr (16 bytes)
           // thus X + 32 -- or X + 20 in hexa

请注意,问题中出现的地址的数学运算可能会有所不同,因为上述内容取决于编译器。 int 的大小可能不同,填充可能不同,vtbl 和 vptr 的排列方式也取决于编译器。


要了解有关对象布局和地址计算的更多信息,请参阅:

以及关于该主题的以下 SO 条目:

【讨论】:

    猜你喜欢
    • 2018-05-08
    • 1970-01-01
    • 2021-05-18
    • 2010-10-13
    • 1970-01-01
    • 2013-06-24
    • 1970-01-01
    • 2023-03-31
    相关资源
    最近更新 更多