C++ delete运算符如何找到多态对象的内存位置?

Ant*_*ton 28 c++ memory-management g++

我想知道delete操作符如何在给出与对象的真实内存位置不同的基类指针时计算出需要释放的内存位置.

我想在我自己的自定义分配器/解除分配器中复制此行为.

考虑以下层次结构:

struct A
{
    unsigned a;
    virtual ~A() { }
};

struct B
{
    unsigned b;
    virtual ~B() { }
};

struct C : public A, public B
{
    unsigned c;
};
Run Code Online (Sandbox Code Playgroud)

我想分配一个C类型的对象,并通过类型B的指针删除它.据我所知,这是一个有效的使用operator delete,它在Linux/GCC下工作:

C* c = new C;
B* b = c;

delete b;
Run Code Online (Sandbox Code Playgroud)

有趣的是,指针'b'和'c'实际上指向不同的地址,因为对象在内存中的布局方式,而删除操作符"知道"如何查找和释放正确的内存位置.

我知道,一般来说,在给定基类指针的情况下找不到多态对象的大小是不可能的:找出多态对象的大小.我怀疑通常不可能找到对象的真实内存位置.

笔记:

Fle*_*exo 13

这显然是特定于实现的.在实践中,实现事物的方法相对较少.从概念上讲,这里存在一些问题:

  1. 您需要能够获得指向最派生对象的指针,即(概念上)包含所有其他类型的对象.

    在标准C++中,您可以使用以下命令执行此操作dynamic_cast:

    void *derrived = dynamic_cast<void*>(some_ptr);
    
    Run Code Online (Sandbox Code Playgroud)

    例如,C*它只从a 获得了回报B*:

    #include <iostream>
    
    struct A
    {
        unsigned a;
        virtual ~A() { }
    };
    
    struct B
    {
        unsigned b;
        virtual ~B() { }
    };
    
    struct C : public A, public B
    {
        unsigned c;
    };
    
    int main() {
      C* c = new C;
      std::cout << static_cast<void*>(c) << "\n";
      B* b = c;
      std::cout << static_cast<void*>(b) << "\n";
      std::cout << dynamic_cast<void*>(b) << "\n";
    
      delete b;
    }
    
    Run Code Online (Sandbox Code Playgroud)

    在我的系统上提供以下内容

    0x912c008
    0x912c010
    0x912c008
    
  2. 一旦完成,它就成了标准的内存分配跟踪问题.通常以两种方式之一完成,a)在分配的内存之前记录分配的大小,然后找到大小只是指针减法或b)在某种数据结构中记录分配和空闲内存.有关详细信息,请参阅此问题,该问题有很好的参考.

    使用glibc,您可以非常合理地查询给定分配的大小:

    #include <iostream>
    #include <stdlib.h>
    #include <malloc.h>
    
    int main() {
      char *test = (char*)malloc(50);
      std::cout << malloc_usable_size(test) << "\n";
    }
    
    Run Code Online (Sandbox Code Playgroud)

    该信息可用于免费/删除,并用于弄清楚如何处理返回的内存块.

实现的确切细节malloc_useable_size在libc源代码中给出,在malloc/malloc.c中:

(以下内容包括Colin Plumb的轻微编辑说明.)

使用例如Knuth或Standish中描述的"边界标记"方法来维护存储块.(参见Paul Wilson的论文 ftp://ftp.cs.utexas.edu/pub/garbage/allocsrv.ps,了解这些技术的调查.)免费块的大小存储在每个块的前面和结束.这使得碎片块更快地整合成更大的块.大小字段还包含表示块是空闲还是正在使用的位.

分配的块看起来像这样:

    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of previous chunk, if allocated            | |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of chunk, in bytes                       |M|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
            |             User data starts here...                          .  
            .                                                               .  
            .             (malloc_usable_size() bytes)                      .  
            .                                                               |   
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+     
            |             Size of chunk                                     |  
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  

  • @antonm - `-fno-rtti`是非标准的C++,所以当你明确禁用标准功能时,我不认为调用标准C++是不可移植的.在我的测试中,我展示的例子仍然有效.手册页说"*dynamic_cast运算符仍然可以用于不需要运行时类型信息的强制转换,即转换为"void*"或明确的基类."*我包含了分配位,因为它似乎很重要需要发生的概念性事物的细节. (2认同)

Mar*_*som 10

销毁基类指针需要您实现了一个虚拟析构函数.如果你不这样做,所有的赌注都会被取消.

调用的第一个析构函数将是由虚拟机制(vtable)确定的最派生对象的析构函数.这个析构函数知道对象的大小!它可以将这些信息放在某个地方,或者将它传递给析构函数链.


Chr*_*odd 7

它的实现定义了,但是一种常见的实现技术operator delete实际上是由析构函数调用的(而不是带有其中的代码delete),并且析构函数中有一个隐藏的参数来控制是否operator delete调用它.

通过这种实现,大多数对析构函数的调用(所有显式dtor调用,调用auto和static变量,以及从派生析构函数调用base析构函数)都会将额外隐藏的arg设置为false(因此不会调用operator delete).但是,当有一个删除表达式时,它会调用具有隐藏arg true的对象的顶级析构函数.在你的例子中,这将是C :: ~C(),因此它将知道回收整个对象的内存