当使用非虚拟析构函数删除基类时,Clang和GCC做什么? [英] What do Clang and GCC do when `delete`ing base classes with non-virtual destructors?

查看:162
本文介绍了当使用非虚拟析构函数删除基类时,Clang和GCC做什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

已有



在典型情况下,第一个基类将在派生对象的开始,所以使用派生的地址作为指向第一个基类对象的工作与单继承情况相同 - 我们得到等效的静态绑定/静态分派。 p>

如果我们尝试使用任何其他基类,指向该派生类的指针不会指向该基类的对象。



有了一个指针,它可以用来指向第二个(或后续的)基类。非虚拟析构函数,通常会发生的情况是,代码将基本上占用第一个基类对象的地址,大致相当于 reinterpret_cast 尝试使用该内存,就像它是由指针指定的基类的对象(例如,base2)。例如,让我们假设base2在偏移14有一个指针,base2的析构函数试图删除它指向的一块内存块。使用非虚拟析构函数,它可能会接收到一个指向base1主体的指针 - 但它仍然会从那里看偏移量14,并尝试将它作为一个指针,并将其传递给 delete 。它可能是base1包含一个指针在该偏移量,它实际上指向一些动态分配的内存,在这种情况下,这可能实际上似乎成功。再次,它也可能是一个完全不同的东西,程序死了一个错误信息(例如)试图释放一个无效的指针。



可能base1在大小上小于14个字节,所以这实际上是在base2中操作(说)offset 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\\\
;删除数据; }
};

class base2 {
char * data2;
public:
base2():data2(new char){}
〜base2(){std :: cout< 〜base2 \\\
;删除数据2; }
};

类派生:public base,public base2 {
char * more_data;

public:
derived():more_data(new char){}
〜derived(){std :: cout< 〜derived\\\
;删除more_data; }
};

int main(){
base2 * b = new derived;
delete b;
}

g ++ / Linux:分段错误

clang / Linux :分段错误

VC ++ / Windows:Popup:foo.exe已停止工作一个问题导致程序停止正常工作。请关闭程序。



如果我们将指针改为 base 而不是 base2 ,我们得到〜base (如果我们只从一个基类派生,并使用指向该基类的指针,我们得到相同的结果:只有那个基类的析构函数运行) p>

There is already a question asking about the "real-world" behavior of deleteing a pointer to a base class that lacks a virtual destructor, but the question is restricted to a very limited case (the derived class has no members with non-trivial destructors), and the accepted answer just says there's no way to know without checking the behavior of every compiler.

....but that isn't actually very helpful; knowing that every compiler might behave differently doesn't tell us anything about the behavior of any particular compiler. So, what do Clang and G++ do in this case? I would assume they would simply call the base-class destructor, then deallocate the memory (for the entire derived class). Is this the case?

Or, if it's not possible to determine this for all versions of GCC and Clang, how about GCC 4.9 and 5.1, and Clang 3.5 through 3.7?

解决方案

First, the standard disclaimer: this is undefined behavior, so even with one specific compiler, changing the compiler flags, the day of the week, or the way you look at the computer could change the behavior.

The following all assumes you have some sort of at least slightly non-trivial destruction happening in your destructors (e.g., the objects delete some memory, or contain object others that themselves delete some memory).

In the simple case (single inheritance) you typically get something roughly equivalent to static binding--that is, if you destroy a derived object via a pointer to a base object, only the base constructor is invoked so the object isn't destroyed properly.

If you use multiple inheritance, and you destroy an object of derived class via the "first" base class, it'll typically be about the same as if you used single inheritance--the base class destructor will be invoked, but the derived class destructor won't be.

If you have multiple inheritance and destroy a derived object via a pointer to the second (or subsequent) base class, your program will typically crash. With multiple inheritance, you have multiple base class objects at multiple offsets in the derived object.

In the typical case, the first base class will be at the beginning of the derived object, so using the address of derived as a pointer to the first base class object works about the same as in the single inheritance case--we get the equivalent of static binding/static dispatch.

If we try this with any of the other base classes, a pointer to the derived doesn't point to an object of that base class. The pointer needs to be adjusted to point to the second (or subsequent) base class before it can be used as a pointer to that type of object at all.

With a non-virtual destructor, what'll typically happen is that the code will basically take that address of that first base class object, do roughly the equivalent of a reinterpret_cast on it, and try to use that memory as if it were an object of the base class specified by the pointer (e.g., base2). For example, let's assume base2 has a pointer at offset 14, and base2's destructor attempts to delete a block of memory it points at. With a non-virtual destructor, it'll probably receive a pointer to the base1 subject--but it'll still look at offset 14 from there, and try to treat that as a pointer, and pass it to delete. It could be that base1 contains a pointer at that offset, and it's actually pointing at some dynamically allocated memory, in which case this might actually appear to succeed. Then again, it could also be that it's something entirely different, and the program dies with an error message about (for example) attempting to free an invalid pointer.

It's also possible that base1 is smaller that 14 bytes in size, so this ends up actually manipulating (say) offset 4 in base2.

Bottom line: for a case like this, things get really ugly in a hurry. The very best you can hope for is that the program dies quickly and loudly.

Just for kicks, quick demo code:

#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: Segmentation fault
clang/Linux: Segmentation fault
VC++/Windows: Popup: "foo.exe has stopped working" "A problem caused the program to stop working correctly. Please close the program."

If we change the pointer to base instead of base2, we get ~base from all the compilers (and if we derive only from one base class, and use a pointer to that base class, we get the same: only that base class' destructor runs).

这篇关于当使用非虚拟析构函数删除基类时,Clang和GCC做什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆