首先,标准免责声明:这是未定义的行为,因此即使使用一个特定的编译器,更改编译器标志、星期几或您查看计算机的方式也可能会改变行为。
以下所有内容都假设您的析构函数中发生了某种至少稍微不平凡的破坏(例如,对象删除了一些内存,或者包含其他对象本身删除了一些内存)。
在简单的情况下(单继承),您通常会得到大致相当于静态绑定的东西 - 也就是说,如果您通过指向基对象的指针销毁派生对象,则仅调用基构造函数,因此该对象不会正确销毁。
如果您使用多重继承,并通过“第一个”基类销毁派生类的对象,则通常与使用单继承大致相同 - 将调用基类析构函数,但派生类析构函数不会。
如果您具有多重继承并通过指向第二个(或后续)基类的指针销毁派生对象,则您的程序通常会崩溃。通过多重继承,您可以在派生对象中的多个偏移处拥有多个基类对象。
![enter image description here](https://i.stack.imgur.com/tL5QL.png)
在典型情况下,第一个基类将位于派生对象的开头,因此使用派生对象的地址作为指向第一个基类对象的指针,其工作方式与单继承情况大致相同 - 我们得到等价的静态绑定/静态调度。
如果我们对任何其他基类尝试此操作,则指向派生类的指针不会指向该基类的对象。需要将指针调整为指向第二个(或后续)基类,然后才能将其用作指向该类型对象的指针。
对于非虚拟析构函数,通常会发生的情况是代码基本上会采用第一个基类对象的地址,大致相当于reinterpret_cast
并尝试使用该内存,就好像它是指针指定的基类的对象(例如,base2)。例如,假设 base2 在偏移量 14 处有一个指针,并且 base2 的析构函数尝试删除它指向的内存块。对于非虚拟析构函数,它可能会收到一个指向 base1 主题的指针——但它仍然会从那里查看偏移量 14,并尝试将其视为指针,并将其传递给delete
。可能是 base1 包含一个位于该偏移量的指针,并且它实际上指向一些动态分配的内存,在这种情况下,这实际上可能看起来成功。话又说回来,也可能是完全不同的东西,并且程序终止并显示一条有关(例如)尝试释放无效指针的错误消息。
也有可能 base1 是smaller大小为 14 个字节,因此最终实际上操作了(例如)base2 中的偏移量 4。
底线:对于这样的情况,事情很快就会变得非常糟糕。您所能期望的最好结果就是程序快速而响亮地终止。
只是为了好玩,快速演示代码:
#include <iostream>
#include <string>
#include <vector>
class base{
char *data;
std::string s;
std::vector<int> v;
public:
base() { data = new char; v.push_back(1); s.push_back('a'); }
~base() { std::cout << "~base\n"; delete data; }
};
class base2 {
char *data2;
public:
base2() : data2(new char) {}
~base2() { std::cout << "~base2\n"; delete data2; }
};
class derived : public base, public base2 {
char *more_data;
public:
derived() : more_data(new char) {}
~derived() { std::cout << "~derived\n"; delete more_data; }
};
int main() {
base2 *b = new derived;
delete b;
}
g++/Linux:分段错误
clang/Linux:分段错误
VC++/Windows:弹出窗口:“foo.exe 已停止工作”“出现问题导致程序停止正常工作。请关闭程序。”
如果我们将指针更改为base
代替base2
,我们得到~base
来自所有编译器(如果我们仅从一个基类派生,并使用指向该基类的指针,我们会得到相同的结果:仅运行该基类的析构函数)。