C++中虚析构函数的调用顺序
Order of calling virtual destructors in C++
嗯,所以我一直试图通过 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
现在三个问题:
- 根据维基百科文章,virtual_table,提到对象 c 的地址比 d 和 b 的地址多 8 个字节,在 e 的情况下会发生什么。
- 当我调用 delete b 而不是 delete d 时,也得到相同顺序的虚拟析构函数,那么为什么派生的 class 析构函数称为
- 仅当我删除一个对象时才调用虚拟析构函数,然后程序结束时如何删除 vtable 和 vpointers(当我 运行 没有
delete d
的代码时,执行只是不打印任何东西就停止了)。
According to the wikipedia article , virtual_table , it was referred that object c gets an address +8 bytes than that of d and b , what happens in case of e.
地址通常依赖于编译器,因此很冒险。我不会指望它们具有任何特定的价值。
When i call delete b instead of delete d , also get the same order sequence of virtual destructors , so why is the derived class destructor called
指针的类型无关紧要。底层对象是用 new D()
创建的,所以这些是被调用的析构函数。这是因为否则可能很难正确删除对象 -- 如果您有一个创建各种子类的工厂,您如何知道将其删除为哪种类型?
(这里实际发生的事情是(指向)析构函数存储在对象的 vtable 中。)
The virtual destructors are called only when i delete an object , then how are the vtable and vpointers gets deleted when the program ends ( when i run the code without the delete d the execution just stops without printing anything ).
如果你从不删除某些东西,它就永远不会被清理干净。程序结束时没有从堆中释放该内存。这是一个"memory leak"。当程序结束时,OS 一次性清理整个程序的堆(不管里面有什么)。
您的问题顺序:
(1) 是的,与指向最派生类型的指针相比,指向指向具有多重继承的派生 classes 对象的基的指针可能会改变它们的数值。原因是基础 class 是派生 class 的一部分,很像一个成员,位于偏移量处。仅对于多重继承中的第一个派生 class,此偏移量可以为 0。这就是为什么不能使用简单的 reinterpret_cast()
.
转换此类指针的原因
(2) b
指向一个 E
也 is-an A
。
这正是 virtual
对成员函数的意义:编译器生成的代码检查指向 运行 时间 的对象,并且调用为对象的实际类型(E
)定义的函数,而不是用于访问该对象的表达式类型(B
)。表达式的类型在编译时完全确定;实际完整对象的类型不是。
如果您不声明析构函数为虚函数,程序可能会像您预期的那样运行:编译器将创建仅调用为表达式类型定义的函数(对于 B
)的代码,而无需任何运行-时间查找。非虚拟成员函数调用效率稍高;但是在析构函数的情况下,就像你的情况一样,当通过基本 class 表达式进行销毁时,行为是 undefined 。如果你的析构函数是 public 它应该是虚拟的,因为这种情况可能会发生。
Herb Sutter 撰写了 an article about virtual functions,包括值得一读的虚拟析构函数。
(3) 内存,包括动态分配的内存,在程序退出时被释放并再次可供现代标准操作系统用于其他用途。 (如果旧操作系统或独立实现提供动态分配,则情况可能并非如此。)然而,动态分配对象的 析构函数 不会被调用,这可能是个问题如果他们持有数据库或网络连接等资源,最好将其释放。
关于对象的地址。正如另一个答案中已经解释的那样,这取决于编译器。不过还是可以解释的。
多重继承对象的地址
(一个可能的编译器实现)
这里是一个可能的内存图,假设指向virtual的指针table是8个字节,int是4个字节。
Class D 首先有指向虚拟 table (vtbl_ptr 或 vptr)的指针然后是 class B 没有自己的 vtbl_ptr,因为它可以与 D 共享相同的 vtbl。
Classes C 和 E 必须自带嵌入式 vtbl_ptr。它将指向 D 的 vtbl(几乎......,有一个 thunk 问题需要处理但让我们忽略它,你可以阅读 thunk 在下面的链接中,但这并不影响对额外 vtbl_ptr).
的需求
每个附加基数的附加 vptr class 是必需的,所以当我们查看 C 或 E 时,vptr 的位置总是在同一位置,即在对象的顶部,无论是否它实际上是一个具体的 C 或者它是一个作为 C 持有的 D。对于 E 和任何其他不是第一个继承基数的基数 class 也是如此。
我们根据上面可能看到的地址:
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 的排列方式也取决于编译器。
要了解有关对象布局和地址计算的更多信息,请参阅:
- C++: Under the Hood by Jan Gray(旧但仍然相关)
以及关于该主题的以下 SO 条目:
- Object layout in case of virtual functions and multiple inheritance
- Understanding virtual table in multiple inheritance
嗯,所以我一直试图通过 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
现在三个问题:
- 根据维基百科文章,virtual_table,提到对象 c 的地址比 d 和 b 的地址多 8 个字节,在 e 的情况下会发生什么。
- 当我调用 delete b 而不是 delete d 时,也得到相同顺序的虚拟析构函数,那么为什么派生的 class 析构函数称为
- 仅当我删除一个对象时才调用虚拟析构函数,然后程序结束时如何删除 vtable 和 vpointers(当我 运行 没有
delete d
的代码时,执行只是不打印任何东西就停止了)。
According to the wikipedia article , virtual_table , it was referred that object c gets an address +8 bytes than that of d and b , what happens in case of e.
地址通常依赖于编译器,因此很冒险。我不会指望它们具有任何特定的价值。
When i call delete b instead of delete d , also get the same order sequence of virtual destructors , so why is the derived class destructor called
指针的类型无关紧要。底层对象是用 new D()
创建的,所以这些是被调用的析构函数。这是因为否则可能很难正确删除对象 -- 如果您有一个创建各种子类的工厂,您如何知道将其删除为哪种类型?
(这里实际发生的事情是(指向)析构函数存储在对象的 vtable 中。)
The virtual destructors are called only when i delete an object , then how are the vtable and vpointers gets deleted when the program ends ( when i run the code without the delete d the execution just stops without printing anything ).
如果你从不删除某些东西,它就永远不会被清理干净。程序结束时没有从堆中释放该内存。这是一个"memory leak"。当程序结束时,OS 一次性清理整个程序的堆(不管里面有什么)。
您的问题顺序:
(1) 是的,与指向最派生类型的指针相比,指向指向具有多重继承的派生 classes 对象的基的指针可能会改变它们的数值。原因是基础 class 是派生 class 的一部分,很像一个成员,位于偏移量处。仅对于多重继承中的第一个派生 class,此偏移量可以为 0。这就是为什么不能使用简单的 reinterpret_cast()
.
(2) b
指向一个 E
也 is-an A
。
这正是 virtual
对成员函数的意义:编译器生成的代码检查指向 运行 时间 的对象,并且调用为对象的实际类型(E
)定义的函数,而不是用于访问该对象的表达式类型(B
)。表达式的类型在编译时完全确定;实际完整对象的类型不是。
如果您不声明析构函数为虚函数,程序可能会像您预期的那样运行:编译器将创建仅调用为表达式类型定义的函数(对于 B
)的代码,而无需任何运行-时间查找。非虚拟成员函数调用效率稍高;但是在析构函数的情况下,就像你的情况一样,当通过基本 class 表达式进行销毁时,行为是 undefined 。如果你的析构函数是 public 它应该是虚拟的,因为这种情况可能会发生。
Herb Sutter 撰写了 an article about virtual functions,包括值得一读的虚拟析构函数。
(3) 内存,包括动态分配的内存,在程序退出时被释放并再次可供现代标准操作系统用于其他用途。 (如果旧操作系统或独立实现提供动态分配,则情况可能并非如此。)然而,动态分配对象的 析构函数 不会被调用,这可能是个问题如果他们持有数据库或网络连接等资源,最好将其释放。
关于对象的地址。正如另一个答案中已经解释的那样,这取决于编译器。不过还是可以解释的。
多重继承对象的地址 (一个可能的编译器实现)
这里是一个可能的内存图,假设指向virtual的指针table是8个字节,int是4个字节。
Class D 首先有指向虚拟 table (vtbl_ptr 或 vptr)的指针然后是 class B 没有自己的 vtbl_ptr,因为它可以与 D 共享相同的 vtbl。
Classes C 和 E 必须自带嵌入式 vtbl_ptr。它将指向 D 的 vtbl(几乎......,有一个 thunk 问题需要处理但让我们忽略它,你可以阅读 thunk 在下面的链接中,但这并不影响对额外 vtbl_ptr).
的需求每个附加基数的附加 vptr class 是必需的,所以当我们查看 C 或 E 时,vptr 的位置总是在同一位置,即在对象的顶部,无论是否它实际上是一个具体的 C 或者它是一个作为 C 持有的 D。对于 E 和任何其他不是第一个继承基数的基数 class 也是如此。
我们根据上面可能看到的地址:
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 的排列方式也取决于编译器。
要了解有关对象布局和地址计算的更多信息,请参阅:
- C++: Under the Hood by Jan Gray(旧但仍然相关)
以及关于该主题的以下 SO 条目:
- Object layout in case of virtual functions and multiple inheritance
- Understanding virtual table in multiple inheritance