当使用非虚拟析构函数“删除”基 类 时,Clang 和 GCC 会做什么?

What do Clang and GCC do when `delete`ing base classes with non-virtual destructors?

已经有 a question 询问 delete 指向缺少虚拟析构函数的基 class 的指针的 "real-world" 行为,但问题受到限制在非常有限的情况下(派生的 class 没有具有非平凡析构函数的成员),并且接受的答案只是说如果不检查每个编译器的行为就无法知道。

.....但这实际上并不是很有帮助;知道每个编译器 可能 的行为不同并不能告诉我们关于任何 特定 编译器的行为的任何信息。那么,在这种情况下,Clang 和 G++ 做了什么?我假设他们会简单地调用 base-class 析构函数,然后释放内存(对于整个派生的 class)。是这样吗?

或者,如果无法确定所有版本的 GCC 和 Clang,GCC 4.9 和 5.1,以及 Clang 3.5 到 3.7 怎么样?

如果你删除一个没有虚析构函数的对象,编译器可能会假定删除的地址是最派生对象的地址。

除非您使用主基 class 删除对象,否则情况不会如此,因此编译器将使用不正确的地址调用 operator delete

当然编译器不会调用派生class的析构函数,或者派生class的operator delete(如果有的话)。

首先,标准免责声明:这是未定义的行为,因此即使使用一个特定的编译器,更改编译器标志、星期几或您查看计算机的方式也可能会改变行为。

以下所有内容均假设您在析构函数中发生了某种至少是轻微的非平凡破坏(例如,对象删除了一些内存,或包含其他对象本身删除了一些内存)。

在简单的情况下(单继承),您通常会得到大致等同于静态绑定的东西——也就是说,如果您通过指向基对象的指针销毁派生对象,则只会调用基构造函数,因此对象没有被正确销毁。

如果您使用多重继承,并且您通过 "first" 基础 class 销毁派生 class 的对象,它通常与您使用单一继承大致相同继承--将调用基 class 析构函数,但不会调用派生的 class 析构函数。

如果您有多重继承并通过指向第二个(或后续)基 class 的指针销毁派生对象,您的程序通常会崩溃。对于多重继承,您在派生对象的多个偏移处有多个基础 class 对象。

在典型情况下,第一个基数 class 将位于派生对象的开头,因此使用派生对象的地址作为指向第一个基数 class 对象的指针适用于与单继承情况相同——我们得到等效于 static binding/static dispatch.

如果我们用任何其他基 class 尝试此操作,指向派生的指针不会指向该基 class 的对象。指针需要调整为指向第二个(或后续)基class,然后才能用作指向该类型对象的指针。

对于非虚拟析构函数,通常会发生的是代码基本上会获取第一个基 class 对象的地址,大致相当于 reinterpret_cast ,并尝试使用该内存,就好像它是指针指定的基 class 的对象(例如 base2)。例如,假设 base2 在偏移量 14 处有一个指针,并且 base2 的析构函数试图删除它指向的一块内存。对于非虚拟析构函数,它可能会收到一个指向 base1 主题的指针——但它仍会从那里查看偏移量 14,并尝试将其视为指针,并将其传递给 delete .可能是 base1 在该偏移处包含一个指针,它实际上指向一些动态分配的内存,在这种情况下,这实际上可能看起来成功了。话又说回来,也可能是完全不同的东西,程序死了,并显示一条关于(例如)试图释放无效指针的错误消息。

也有可能 base1 更小,大小为 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 has stopped working" "A problem caused the program to stop working correctly. Please close the program."

如果我们将指针更改为 base 而不是 base2,我们会从所有编译器得到 ~base(如果我们只从一个基派生 class,并使用指向该基 class 的指针,我们得到相同的结果:只有该基 class 的析构函数运行)。