C++中的new与delete与虚析构函数的关系的实验研究

 

这是我在进行内部培训《C++程序编译与运行期间存储资源的分配》期间,为了保证培训内容的准确性而做的实验,最初想把它写成一篇介绍C++编译器如何解释动态内存分配的文档,考虑到各个编译器之间的差异,以及我个人对编译器的了解十分有限,在这里我没有下定论,而只给出个人的初步分析结果。

以下实验是在VC7.1,缺省的调试模式下进行的。作为一个惯例,我对做实验的类重载new与delete操作符,实现方式是调用全局的new 与delete分配内存,我个人增加的内容是打印出分配内存的地址,用来观察指针的值是否发生变化。每个实验的类都包含了成员变量,这样是为了防止子类与父类对象大小一致,从而影响实验结果。我先从一个正常的例子开始

 

知识点: newdelete始终是静态函数,不能够被继承

 

1.           一个正常的例子

#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)  newdelete的地址一致,没有内存泄漏。

2.转换new出来的指针,delete会发生什么?

2.1 转换成void*

稍微修改上面例子中的main函数,

 

int main(){

     MyBase* p = new MyBase;

     void* pv = p;

     delete pv;  //delete void*类型的指针

}

 

 

 

 

 

 

输出结果:

 

MyBase address by new : 3409880

 

 

我的分析:

1)  MyBase类的析构函数没有得到执行。

2)  newdelete的地址一致,一般不会造成内存泄漏。

 

 

2.2强制转换成其他对象的指针

我们在上面的例子中,增加一个新的类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)  newdelete的地址一致,一般不会造成内存泄漏

 

 

点评:

new出来的指针转换成其他类型的指针(父子类关系除外),会导致构造与析构函数之间的成对关系得到破坏,如果析构函数不做任何清理操作,不会出什么问题,如果析造函数需要清理内存或其他重要资源,结果将是未知的。

 

 

3.   基类的析构函数不是虚的,在动态分配与释放内存中会发生些什么

前面的实验没有考虑继承关系。在共有继承时,基类的析构函数必须是虚的,违反这条原则会如何呢?我们接着实验。

 

3.1 单继承中的动态内存释放

 

我们编写一个新的类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)  newdelete的地址一致,一般不会造成内存泄漏

 

问题:在继承结构中,基类的析构函数不是虚的,释放内存真的没有问题么??我们用下面的实验证明它只在单继承结构中有效。

 

知识点:

1)  在单继承中,子类对象与父类对象占用相同的起始空间,子类向父类转换时不需要调整偏移(或偏移为0),在多重继承与虚继承中却需要做这种指针调整。

2)  编译器在编译一个类的析构函数时,为了能够正确释放内存,需要做一些额外工作,在

Modern C++ Design Generic Programming and Design Patterns Applied 4.7节中有所阐述

 

3.2 多重继承中的动态内存释放

 

我们增加另外一个类MyBase2MyBase2只有一个成员变量(注意:编译器会生成一个非虚的析构函数),然后让类MyNewMyBaseMyBase2继承,相应的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

 

 

 

 

4.数组类型New[]delete[]

4.1 析构函数不为虚,对数组对象内存释放的影响

同样我们来做数组对象的实验,我们在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编译器在对数组对象分配内存时,保存了对象的个数,由于父类与子类对象大小不一样,导致分配与释放内存时的内存长度计算结果不同,是否会内存泄漏取决于编译器的实现。

 

4.2 数组对象与一般对象的new delete不匹配

数组对象与一般对象的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一个数组对象,释放数组中单个对象的情况,比较简单在此不做实验与分析

 

5. 基本结论

在C++中类型转换(非父子类之间),基类析构函数不为虚,new与delete之间不匹配,除了可能导致内存泄漏,还可能干扰编译器对类型的正确解释以及调用错误的析构函数,如果在程序中不加控制使用,会导致不可预知的后果。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值