好吧,我一直试图通过C ++理解OOP概念,但是我无法获得虚拟析构函数的某些部分。
我写了一个小片段:
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
现在有三个问题:
delete d
的情况下运行代码时,执行只是停止而没有打印任何内容)。 答案 0 :(得分:2)
b
指向is-an E
的{{1}}。
这正是A
对成员函数的含义:编译器生成的代码检查运行时指向的对象,并调用为实际类型定义的函数对象(即virtual
),而不是用于访问该对象(即E
)的表达式类型。表达式的类型在编译时完全确定;实际完整对象的类型不是。
如果您未声明虚拟的析构函数,则程序可能会按预期运行:编译器将创建代码,该代码仅调用针对表达式类型(对于B
)定义的函数,而无需运行-时间查询。非虚拟成员函数的调用效率更高。但是对于像您这样的析构函数,通过基类表达式进行销毁时,行为是未定义。如果您的析构函数是公共的,则应该是虚拟的,因为这种情况可能会发生。
Herb Sutter编写了an article about virtual functions,其中包括值得阅读的虚拟析构函数。
答案 1 :(得分:0)
根据Wikipedia文章virtual_table,对象c的地址比d和b的地址多8个字节,这在e情况下会发生。
地址通常与编译器相关,因此非常容易。我不会依赖于它们具有任何特定的价值。
当我调用delete b而不是delete d时,也会得到相同顺序的虚拟析构函数,所以为什么派生类析构函数称为
指针的类型无关紧要。基础对象是使用new D()
创建的,因此这些是被调用的析构函数。这是因为否则可能很难正确删除对象-如果您有一个创建各种子类的工厂,您如何知道将其删除为哪种类型?
仅当我删除对象时,才调用虚拟析构函数,然后在程序结束时删除vtable和vpointers(当我运行不带delete d的代码时,执行就停止了,而没有打印任何内容)。
如果您从不删除任何内容,则永远不会对其进行清理。程序结束时没有从堆中释放该内存。这是“内存泄漏”。程序结束后,操作系统将一次性清理整个程序堆(无需关心其中的内容)。
答案 2 :(得分:0)
关于对象的地址。正如另一个答案中已经解释的那样,这取决于编译器。但是仍然可以解释。
多重继承中的对象地址 (可能的编译器实现)
这里是一个可能的内存图,假设指向虚拟表的指针是8个字节,而int是4个字节。
类D 首先具有指向虚拟表(vtbl_ptr或vptr)的指针,然后进入类B 而没有自己的vtbl_ptr,因为它可以与D共享相同的vtbl。
C类和E类必须带有自己的嵌入式vtbl_ptr。它将指向D的vtbl(几乎...,有一个 thunk 问题要处理,但是让我们忽略它,您可以在下面的链接中阅读有关 thunk 的信息,但这不会影响对其他vtbl_ptr的需求。
每个附加基类都需要附加vptr,因此当我们查看C或E时,vptr的位置始终位于同一位置,即在对象的顶部,无论它实际上是否是具体的C还是D保留为C。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条目: