C++动态对象。在运行时如何确定对象大小?

5

我不明白一件事。比如,我声明了类A和类B,其中类B是类A的子类:

class A {
    public:
        int a;
}

class B : public A {
    public:
        int b;
}

显然,如果我创建A或B的实例,它们在内存中的大小可以由类型确定。
A instanceA; // size of this will probably be the size of int (property a)
B instanceB; // size of this will probably be twice the size of int (properties a and b)

但是如果我创建动态实例,然后稍后释放它们呢?

A * instanceAPointer = new A();
A * instanceBPointer = new B();

这些是不同类的实例,但程序会将它们视为类A的实例。在使用它们时没有问题,但是如何释放它们呢?为了释放已分配的内存,程序必须知道要释放的内存大小,对吗?

所以如果我写

delete instanceAPointer;
delete isntanceBPointer;

程序如何知道每个指针所指的地址开始,应该释放多少内存?因为显然,对象的大小不同,但程序将它们视为类型A。谢谢。

1
也许这个链接会有帮助?http://www.openrce.org/articles/files/jangrayhood.pdf - OldProgrammer
1
实际上,第二个可能会导致内存泄漏,因为它不是一个多态类。如果该类是多态的,编译器就能够根据动态类型对其进行解分配,无论是使用运行时类型信息还是其他方法,并自动释放实际分配的相同数量的内存。但如果该类不是多态的,我认为不能保证处理这种情况的正确性,所以您应该始终通过正确类型的指针来删除它。 - Justin Time - Reinstate Monica
@JustinTime 你所说的多态类是指带有虚析构函数的类吗? - Michal Artazov
1
@Justin:delete 永远不会使用 RTTI。虚析构函数的存在通过 vtable 处理了标识问题。 - rubenvb
@JustinTime 是的,这就是人们为什么总是建议在客户端可能通过多态指针删除的任何类中实现虚析构函数的原因,这样就可以查找并执行正确的派生 Dtor(s)。 - underscore_d
显示剩余9条评论
3个回答

7
我将假设您已经知道删除操作的工作原理。
至于如何使delete知道如何清理继承实例,这就是为什么在继承上下文中使用虚拟析构函数,否则您将会产生未定义的行为。基本上,像每个其他的虚拟函数一样,析构函数是通过vtable调用的。
还要记住:C++编译器隐式地在您的析构函数中销毁父类。
class A {
    public:
        int a;
    virtual ~A(){}
}

class B : public A {
    public:
        int b;
    ~B() { /* The compiler will call ~A() at the very end of this scope */ }
}

这就是为什么这会起作用的原因;

A* a = new B();
delete a;

通过vtabledelete将调用析构函数~B()。由于编译器隐式插入了派生类中基类的析构函数调用,因此~B()中将调用A的析构函数。

如果我不声明虚析构函数,那么程序可能会崩溃,这取决于编译器的实现,是吗? - Michal Artazov

3

如果您通过指向基类子对象的指针删除对象,并且子对象的类没有虚析构函数,则其行为是未定义的。

另一方面,如果它有虚析构函数,则虚分派机制会负责为正确的地址(即完整的、最派生的对象)释放正确量的内存。

您可以通过将dynamic_cast<void*>应用于任何适当的基类子对象指针来自己发现最派生对象的地址。(也可参见this question。)


3
这在未定义行为方面是正确的,但在内存释放方面是不正确的。与实际对象大小相等的字节数将始终被释放,即使没有虚析构函数。然而,当然不会调用适当的析构函数。 - SergeyA
没有虚析构函数,你甚至找不到正确的地址来释放内存,更别提大小了... - Kerrek SB
Kerrek,为什么?您释放整个内存块,大小已知于释放例程(通常因为它是块的前缀),并且您从指针中给出的地址开始。 - SergeyA
@SergeyA 所以如果我理解正确的话,指针指向的内容并不重要,因为引用的内存块隐含地包含了它的大小信息? - Michal Artazov
1
@SergeyA:你只能通过从分配函数返回的指针来释放内存。某些随机子对象的地址通常不是这样的指针。大小也很重要,因为它可能会被传递给释放函数(作为第二个参数),请参见[expr.delete]/(10.1)。 - Kerrek SB
显示剩余4条评论

2
为了释放已分配的内存,程序必须知道要释放的内存大小,对吧?
如果你考虑C库中的malloc和free,你会发现在调用free时不需要指定要释放的内存数量,即使在这种情况下,由于free被提供了一个void *,因此无法推断出它。相反,分配库通常要么记录足够的有关所提供的内存的信息,以便仅使用指针即可完成解除分配。
这在C++解除分配例程中仍然是正确的:如果基类提供其自己的static void operator delete(void*,std::size_t)和基类析构函数是virtual,则将传递动态类型的大小。默认情况下,解除分配结束于::operator delete(void*),它不会给出任何大小:分配例程本身必须知道足够的信息才能操作。
分配例程可能工作的方式有多种,包括:
- 存储分配的大小 - 从同一大小块的池中分配类似大小的对象,使得该池中的任何指针隐含地与该块大小相关

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接