这是我在进行内部培训《C++程序编译与运行期间存储资源的分配》期间,为了保证培训内容的准确性而做的实验,最初想把它写成一篇介绍C++编译器如何解释动态内存分配的文档,考虑到各个编译器之间的差异,以及我个人对编译器的了解十分有限,在这里我没有下定论,而只给出个人的初步分析结果。
以下实验是在VC7.1,缺省的调试模式下进行的。作为一个惯例,我对做实验的类重载new与delete操作符,实现方式是调用全局的new 与delete分配内存,我个人增加的内容是打印出分配内存的地址,用来观察指针的值是否发生变化。每个实验的类都包含了成员变量,这样是为了防止子类与父类对象大小一致,从而影响实验结果。我先从一个正常的例子开始
知识点: new与delete始终是静态函数,不能够被继承
#include <iostream> class MyBase{ int i; public: void* operator new(size_t s){ void* p = ::operator new (s); std::cout << "MyBase address by new : " << int(p) << std::endl; return p; } void operator delete(void* p){ std::cout << "MyBase address by delete: " << int(p) << std::endl; ::delete p; } MyBase(){ i=1; } ~MyBase(){ std::cout << "i=" << i <<std::endl; std::cout << "~MyBase()" <<std::endl; } }; int main(){ MyBase* p = new MyBase; delete p; } |
我编写一个类MyBase,重载了new与delete操作符,在main()中验证动态分配与释放MyBase对象。
运行结果如下:
MyBase address by new : 3409880 i=1 ~MyBase() MyBase address by delete: 3409880 |
我的分析:
1) MyBase类的对象的构造函数,析构函数都得到了执行
2) new与delete的地址一致,没有内存泄漏。
稍微修改上面例子中的main函数,
int main(){ MyBase* p = new MyBase; void* pv = p; delete pv; //delete void*类型的指针 } |
输出结果:
MyBase address by new : 3409880 |
我的分析:
1) MyBase类的析构函数没有得到执行。
2) new与delete的地址一致,一般不会造成内存泄漏。
我们在上面的例子中,增加一个新的类NoRelation,将上面new出来对象的指针强制转换成NoRelation对象的指针,然后delete掉这个NoRelation对象的指针。
class NoRelation{ public: void* operator new(size_t s){ void* p = ::operator new(s); std::cout << "NoRelation address by new : " << int(p) << std::endl; return p; } void operator delete(void* p){ std::cout << "NoRelation address by delete: " << int(p) << std::endl; ::delete p; } NoRelation(){ } ~NoRelation(){ std::cout << "~NoRelation()" <<std::endl; } }; int main(){ MyBase* p = new MyBase; NoRelation* pn = (NoRelation*)p; delete pn; } |
输出结果:
MyBase address by new : 3409880 ~NoRelation() NoRelation address by delete: 3409880 |
我的分析:
1) 调用了其他类型的析构函数,MyBase类的构造/析构函数成对关系的得到破坏
2) new与delete的地址一致,一般不会造成内存泄漏
点评:
将new出来的指针转换成其他类型的指针(父子类关系除外),会导致构造与析构函数之间的成对关系得到破坏,如果析构函数不做任何清理操作,不会出什么问题,如果析造函数需要清理内存或其他重要资源,结果将是未知的。
前面的实验没有考虑继承关系。在共有继承时,基类的析构函数必须是虚的,违反这条原则会如何呢?我们接着实验。
我们编写一个新的类MyNew继承MyBase, MyBase的析构函数不是虚的,我们在main()中动态获取MyNew类型的对象指针,却释放MyBase类型的指针。
#include <iostream> class MyBase{ int i; public: void* operator new(size_t s){ void* p = ::operator new (s); std::cout << "MyBase address by new : " << int(p) << std::endl; return p; } void operator delete(void* p){ std::cout << "MyBase address by delete: " << int(p) << std::endl; ::delete p; } MyBase(){ i=1; } ~MyBase(){ std::cout << "i=" << i <<std::endl; std::cout << "~MyBase()" <<std::endl; } };
class MyNew: public MyBase{ int j; public: void* operator new(size_t s){ void* p = ::operator new (s); std::cout << "MyNew address by new : " << int(p) << std::endl; return p; } void operator delete(void* p){ std::cout << "MyNew address by delete: " << int(p) << std::endl; ::delete p; } MyNew(){ j = 2; std::cout << "MyNew()" <<std::endl; } ~MyNew(){ std::cout << "j=" << j <<std::endl; std::cout << "~MyNew()" <<std::endl; } }; int main(){ MyBase* p = new MyNew; delete p; } |
输出结果:
MyNew address by new : 3409880 MyNew() i=1 ~MyBase() MyBase address by delete: 3409880 |
初步分析:
1) 子类的析构函数没有执行
2) new与delete的地址一致,一般不会造成内存泄漏
问题:在继承结构中,基类的析构函数不是虚的,释放内存真的没有问题么??我们用下面的实验证明它只在单继承结构中有效。
知识点:
1) 在单继承中,子类对象与父类对象占用相同的起始空间,子类向父类转换时不需要调整偏移(或偏移为0),在多重继承与虚继承中却需要做这种指针调整。
2) 编译器在编译一个类的析构函数时,为了能够正确释放内存,需要做一些额外工作,在
《Modern C++ Design Generic Programming and Design Patterns Applied》 4.7节中有所阐述
我们增加另外一个类MyBase2,MyBase2只有一个成员变量(注意:编译器会生成一个非虚的析构函数),然后让类MyNew从MyBase与MyBase2继承,相应的main函数也作相应的修改。
class MyBase2 { int i; }; class MyNew: public MyBase, public MyBase2{ int j; public: void* operator new(size_t s){ void* p = ::operator new (s); std::cout << "MyNew address by new : " << int(p) << std::endl; return p; }
void operator delete(void* p){ std::cout << "MyNew address by delete: " << int(p) << std::endl; ::delete p; } MyNew(){ j = 2; std::cout << "MyNew()" <<std::endl; } ~MyNew(){ std::cout << "j=" << j <<std::endl; std::cout << "~MyNew()" <<std::endl; } };
int main(){ MyBase2* p = new MyNew; delete p; } |
测试结果:
初步分析:
1) 基类的析构函数不是虚的,会导致子类的析构函数不会被执行
2) 释放内存的指针很可能不指向获取内存时的地址,在调试版可能会抛出异常,在发行版本会造成内存泄漏
如果将上面基类修改为虚析构函数,编译器会做指针调整,保证程序运行正确
virtual ~MyBase() virtual ~MyBase2()
修改后的测试结果: MyNew address by new : 3409880 MyNew() j=2 ~MyNew() i=10 ~MyBase2() i=1 ~MyBase() MyNew address by delete: 3409880 |
同样我们来做数组对象的实验,我们在main()中,动态分配与释放数组。
#include <iostream> class MyBase{ int i; public: void* operator new(size_t s){ void* p = ::operator new (s); std::cout << "MyBase address by new : " << int(p) << std::endl; return p; } void operator delete(void* p){ std::cout << "MyBase address by delete: " << int(p) << std::endl; ::delete p; } MyBase(){ i=1; } ~MyBase(){ std::cout << "i=" << i <<std::endl; std::cout << "~MyBase()" <<std::endl; } }; class MyNew: public MyBase{ int j; public: MyNew(){ j = 2; }
~MyNew(){ std::cout << "j=" << j <<std::endl; } };
int main(){ MyBase* p = new MyNew[2]; delete[] p; } |
测试结果:
MyBase address by new : 3409880 i=2 i=1 MyBase address by delete: 3409880 |
初步分析:
1)基类析构函数不是虚的,会造成编译器对对象地址的解析错误,在本例中将一个MyNew对象当成了两个MyBase对象。
2)由于调用析构函数的个数与分配时一致,可以认为VC7.1编译器在对数组对象分配内存时,保存了对象的个数,由于父类与子类对象大小不一样,导致分配与释放内存时的内存长度计算结果不同,是否会内存泄漏取决于编译器的实现。
数组对象与一般对象的new 与delete不匹配的后果是什么呢?大部分C++的书籍给出的结果都是“后果是未知的”,是什么样的原因导致了这样的结论了,我们修改main()接着做实验。
#include <iostream> class MyBase{ int i; public: void* operator new[](size_t s){ void* p = ::operator new (s); std::cout << "MyBase address by new : " << int(p) << std::endl; return p; } void operator delete[](void* p){ std::cout << "MyBase address by delete: " << int(p) << std::endl; ::delete[] p; } void* operator new(size_t s){ void* p = ::operator new (s); std::cout << "MyBase address by new : " << int(p) << std::endl; return p; } void operator delete(void* p){ std::cout << "MyBase address by delete: " << int(p) << std::endl; ::delete p; } MyBase(){ i=1; } ~MyBase(){
std::cout << "i=" << i <<std::endl; } };
int main(){ MyBase* p = new MyBase[2]; delete p; } |
测试结果:
初步分析:
1) VC7.1编译器对于数组对象,返回内存地址的前4个字节保存了数组对象的个数
2) VC7.1编译器new与delete不匹配,会造成释放内存的指针很可能不指向获取内存时的地址,在调试版可能会抛出异常,在发行版本会造成内存泄漏
至于new一个数组对象,释放数组中单个对象的情况,比较简单在此不做实验与分析
在C++中类型转换(非父子类之间),基类析构函数不为虚,new与delete之间不匹配,除了可能导致内存泄漏,还可能干扰编译器对类型的正确解释以及调用错误的析构函数,如果在程序中不加控制使用,会导致不可预知的后果。