C++删除运算符如何查找多态对象的内存位置
How does the C++ delete operator find the memory location of a polymorphic object?
我想知道当给delete操作符一个基类指针时,它是如何计算出需要释放的内存位置的,该指针与对象的真实内存位置不同。
我想在我自己的自定义分配器/解除分配器中复制这种行为。
考虑以下层次结构:
struct A
{
unsigned a;
virtual ~A() { }
};
struct B
{
unsigned b;
virtual ~B() { }
};
struct C : public A, public B
{
unsigned c;
};
我想分配一个C类型的对象,并通过B类型的指针删除它。据我所知,这是运算符delete的有效使用,它在Linux/GCC:下工作
C* c = new C;
B* b = c;
delete b;
有趣的是,指针"b"answers"c"实际上指向不同的地址,因为对象在内存中的布局方式不同,而删除运算符"知道"如何找到并释放正确的内存位置。
我知道,一般来说,在给定基类指针的情况下,不可能找到多态对象的大小:找出多态对象的尺寸。我怀疑,通常也不可能找到对象的真实内存位置。
注:
- 我的问题与新建[]和删除[]的工作方式无关。我对单对象分配的情况感兴趣。如何删除[]";知道";操作数数组的大小
- 我也不关心析构函数是如何调用的。我对内存本身的释放感兴趣。如何';删除';删除基类的指针时有效
- 我使用-fno-rtti和-fno异常进行了测试,因此G++不应该访问运行时类型信息
这显然是特定于实现的。在实践中,实现事物的合理方法数量相对较少。从概念上讲,这里有几个问题:
-
您需要能够获得指向派生最多的对象的指针,即(概念上)包含所有其他类型的对象。
在标准C++中,您可以使用
dynamic_cast
:来完成此操作void *derrived = dynamic_cast<void*>(some_ptr);
它只从
B*
中获取C*
,例如:#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; }
在我的系统上给出了以下内容
0x912c0080x912c0100x912c008
-
一旦完成,它就变成了一个标准的内存分配跟踪问题。通常,这可以通过以下两种方式之一来完成: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"; }
这些信息可以类似地释放/删除,并用于确定如何处理返回的内存块。
实现malloc_useable_size
的确切细节在libc源代码malloc/malloc.c:中给出
(以下是Colin Plumb编辑的解释。)
使用"边界标记"方法来维护内存块,如下所示在例如Knuth或Standish中描述。(见Paul Wilson的论文ftp://ftp.cs.utexas.edu/pub/garbage/allocsrv.ps调查技术。)空闲块的大小存储在每个块和末尾。这使得整合零散的区块非常快地变成大块。大小字段也包含位表示块是空闲的还是在使用中。
分配的区块如下所示:
chunk->+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|上一个区块的大小(如果已分配)||+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|区块大小,以字节为单位|M|P|mem->+-+-+-+-+/-+-+-+-+++++++++-+-+-++|用户数据从这里开始。。。(malloc_usable_size()字节)。.|nextchunk->+-+-+-+-+++++++++-+-+-++-+-+-+/-+-+-+-++|区块大小|+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
调用的第一个析构函数将是由虚拟机制(vtable)确定的派生对象最多的析构函数。这个析构函数知道对象的大小!它可以把这些信息藏在某个地方,或者把它传递到销毁器的链条上。
它的实现是定义的,但一种常见的实现技术是operator delete
实际上是由析构函数调用的(而不是包含delete
的代码),并且析构函数有一个隐藏的参数来控制是否调用operator delete
。
使用此实现,大多数对析构函数的调用(所有显式dtor调用、对自动和静态变量的调用,以及从派生析构函数对基析构函数调用)都会将该额外的隐藏参数设置为false(因此不会调用运算符delete)。然而,当存在delete表达式时,它会调用隐藏参数为true的对象的顶级析构函数。在您的示例中,这将是C::~C(),因此它将知道回收整个对象的内存
通常的实现(理论上可能还有其他实现,我怀疑在实践中是否有)是每个基对象都有一个vtable(如果没有,则基对象不是多态的,不能用于删除)。vtable不仅包含指向虚拟函数的指针,还包含整个RTTI所需的内容,包括从当前对象到最派生对象的偏移量。
为了解释(在任何真正的实现中都可能存在差异,我可能犯了一些错误),以下是真正使用的:
struct A_VTable_Desc {
int offset;
void* (destructor)();
} AVTable = { 0, A::~A };
struct A_impl {
unsigned a;
A_VTable_Desc* vptr;
};
struct B_VTable_Desc {
int offset;
void* (destructor)();
} BVtable = { 0, &B::~B };
struct B_impl {
unsigned b;
B_VTable_Desc* __vptr;
};
A_VTable_Desc CAVtable = { 0, &C::~C_as_A };
B_VTable_Desc CBVtable = { -8, &C::~C_as_B };
struct C {
A_impl __aimpl;
B_impl __bimpl;
unsigned c;
};
而C的构造函数隐含地做一些类似的事情
this->__aimpl->__vptr = &CAVtable;
this->__bimpl->__vptr = &CBVtable;
编译delete
运算符时,编译器需要确定在执行析构函数后要调用的"deallocation"函数。请注意,析构函数与解除分配调用没有任何直接关系,但它确实会影响编译器如何查找解除分配函数。
在通常的情况下,对象没有特定于类型的解除分配函数,在这种情况下使用全局解除分配函数并且总是隐式声明(C++03 3.7.3/2):
void operator delete(void*) throw();
请注意,此函数甚至不接受大小参数。它只需要根据指针的值来确定分配大小。这可以通过在地址之前存储分配的大小来实现(是否有其他实现方式?)。
但是,在决定使用该释放函数之前,编译器会执行一次查找,看看是否应该使用特定类型的释放函数。该函数可以具有单个参数(void*
)或两个参数(void*
和size_t
)。
在查找解除分配函数时,如果用作delete
操作数的指针的静态类型具有虚拟析构函数,则(C++03 12.5/4):
deallocation函数是在动态的定义中通过查找找到的函数类型的虚拟析构函数
实际上,对于具有虚拟析构函数的类型,任何operator delete()
释放函数都是虚拟的,即使实际函数必须是static
(标准在12.5/7中对此进行了说明)。在这种情况下,如果需要,编译器可以传递对象的大小,因为它可以访问对象的动态类型(对对象指针的任何必要调整都可以以相同的方式找到)。
如果delete
的操作数的静态类型是静态的,则operator delete()
释放函数的查找遵循通常的规则。同样,如果编译器选择了一个需要大小参数的释放函数,它可以这样做,因为它在编译时知道对象的静态类型。
最后一种情况是导致未定义行为:如果指针的静态类型没有虚拟析构函数,而是指向派生类型对象,那么编译器可能会查找错误的释放函数并传递错误的大小。但既然这是未定义行为的结果,那也没关系。
指向多态对象的指针通常被实现为指向对象和虚拟表的指针,其中包含关于对象的底层类的信息。delete将知道这些实现细节,并找到正确的析构函数
它可以像malloc一样做到这一点。一些malloc只记录对象本身之前的大小。大多数现代malloc都要复杂得多。请参阅tcmalloc,这是一个快速分配器,它将页面上相同大小的对象保持在一起,因此只需要保持页面粒度上的大小信息。
- 通过switch和static_cast访问多态对象的运行时类型
- 转身多态对象
- 多态对象的数组
- 了解C++中多态对象的序列化
- 如何复制多态对象
- 分配分配器为多态对象分配内存
- 将多态对象存储在unordered_set中
- 包含多态对象的向量:静态断言误差
- 是否可以在不手动在C 中的每个派生类中添加覆盖的克隆方法来克隆多态对象
- 如何正确地使一个对象拥有另一个多态对象
- 复制多态对象
- 是否可以使用放置新来更改多态对象的类型?
- 多态对象的容器
- C++在运行时确定多态对象的类型
- 多态对象成员
- 识别多态对象的替代方法
- 多态对象销毁和并发性
- 如何在堆栈上创建多态对象
- 多态对象复制
- C++删除运算符如何查找多态对象的内存位置