首先说下我对完全释放的理解。
完全释放是指经过了这系列的操作, 没有内存泄露。
三,四能够完全释放内存, 一,二由于条件不足, 不能确定。
但是四个使用的方式都是错误的, 这四种情况都不会导致运行错误, 可以算是一种巧合。
一点背景知识:
//////////////////////////////////////////////////
Class A;
A *pa = new A;
首先调用operator new(size_t) 来分配A所占据的内存空间, 然后在这个空间上调用一个函数,记做函数FA(注意不是A::A()):
{
构造基类 // 如果有
构造成员对象变量 // 如果有
调用自己的构造函数 // 如果有 A::A()
}
这是一个递归的过程, 既基类和成员变量的构造也有自己的FX函数。
如果基类和成员变量的FX都不存在,并且A::A()没写, 那么FA则不存在
最后吧这个空间给变量pa
Class A;
Class B : public A;
A *pa = new B;
delete pa;
这里分两种情况, 如果有virtual destructor和没有virtual destructor的情况
A或者A的任何层次继承的基类具有virtual destructor, 那么A则具有virtual destructor,否则则不具有。
有virtual destructor:
直接调用这个虚函数记做~FA (注意不是A::~A()), 因为虚函数的多态特性, 保证了
“对一个具有虚析构函数的基类指针调用delete, 能完全释放内存, 即便这个指针可能是派生类所产生的”
这是设计一个基类必须需要做的一件事, 比如你需要让别人继承你的话, 最起码要写一个空的虚析构函数。
c++的这个特性的实现一般采用这个过程。 这个在B的虚表中的~FA一般是这样的.
~FA thiscall (A *this, int flag)
{
// 注意这里传递过来的this是A级别的, 因为这个函数的产生是因为A导致的!!
调用B的析构函数B::~B() // 如果存在的话, 这里需要有一个修正this的过程, 即编译器根据A和B的层次
// 关系来修正, 对于单重派生来说, 是一样的。
析构B中的其他成员类对象
析构基类
if (flag & 1)
operator delete (void*) // 释放内存
}
注: 这也是个递归的过程, 其中 "析构B中的其他成员类对象" 和 "析构基类" 分别有自己的~FX函数
关于这个flag是用来区别delete pa 和 pa->~A() 的.
只要是destructor是virtual的, 这个~FA必然存在, 因为他需要占据虚表项.
没有virtual的destructor
编译器会生成一个函数记做~FA2, 看起来像这个样子
~FA2 thiscall (A *this)
{
调用A的析构函数A::~A();
析构A的其他成员类对象
析构A的基类
}
这也是个递归的过程, 同样, 还存在一个函数, 记做~FB2, 看起来是这个样子
~FB2 thiscall (B *this)
{
调用B的析构函数B::~B();
析构B的其他成员类对象
析构B的基类 // 即函数 ~FA2
}
执行delete pa的时候, 编译器
调用~FA2(), 然后在吧A层次的空间去调用operator delete(void *)
~FA2的不存在的条件是,A::~A()没有写并且他所有的基类和成员类对象的~FX都不存在。
A的其他成员类对象的则根据是否是virtual destructor来按照这两种法则析构.
如果是法则1则flag传递的是0.
这里有2个问题:
1: 没有经过FB2, 所以B的析构函数B::~B()和B的其他成员类对象没可能释放。
2: A层次空间的地址肯能不是B层次空间的地址, 强制operator delete可能会出错,
虽然大多数情况下是一样的
好, 接下来最重要的是
Class A;
A *pa = new A[2];
delete []pa;
第一次分配了多少内存?
可能是sizeof(A)*2 也可能是sizeof(A)*2 + sizeof(size_t)
第一种情况吧operator []new(size_t)的地址给pa
第二种情况吧operator []new(size_t) + sizeof(size_t)的地址给pa
这完全看这个~FA或者~FA2是否存在!
为什么这么说, 看看delete[]pa吧,
[]告诉编译器这是一个数组对象的析构, 我需要吧析构的过程应用到
所有的对象上, 所以我必须知道对象的个数, 然后吧从这个地址开始的每个
对象都去析构一把, 这个个数则记录在pa这个地址-sizeof(size_t)的地方!
但是, 如果析构是无动作的, 则没有记录这个size的必要
一点小的细节就是A如果是virtual destructor则循环调用那个虚函数
否则编译器可能生成一个 vector_~FA2(A *this, int flag)来释放
if (flag & 2) 则是一个vector析构,需要循环, 否则是就是~FA2(A *this, int flag)
对照上述不难发现
一:
class A;
class B : public A
//假设类B比类A要大,则:
A* pA = new B;
delete pA;
B或者A有虚析构, 一切工作正常,
否则B的析构函数不被调用, B的成员类对象不被析构, 但是
operator delete的时候能够pa占据的地址释放, 因为单继承是同一地址
二:
class A;
void* p = new A;
delete p;
因为是原生类型, 所以直接operator delete掉了, 地址是A的地址,能够正确释放
但是~FA或者~FA2都不会被调用, 可能会A锁包含的指针, 对象, 基类等占据的各种资源不被释放
三:
struct s
{
int i;
int j;
};
LPBYTE pData = new BYTE[100];
struct s* ps = (struct s*) pData;
delete ps;
显然没加[]的是一个单对象析构, 但是~Fs2是不存在的, 所以不被调用,
只是operator delete, 地址也对上了.
四:
class A
{
void Release(){delete this;}
};
class B : public A
{
int i;
};
B* pb = new B;
pb->Release();
这个Release是因为A生成的, 所以这个delete this其实是删除A,
由于A和B的地址相同, 所以空间能释放, 也能正确的调用~FA2, 但是~FB2没机会被调用了
庆幸的是他们都不存在.
关于__stdcall的说明,作为一个微软特有的调用约定限制符, msdn有详细的说明
Microsoft Specific
The __stdcall calling convention is used to call Win32 API functions. The callee cleans the stack, so the compiler makes vararg functions __cdecl. Functions that use this calling convention require a function prototype