跟风C++主题年:从虚析构函数想到的内存基本模型

来源:互联网 发布:怎样安装app软件 编辑:程序博客网 时间:2024/05/01 07:11

 跟风C++主题年:从虚析构函数想到的内存基本模型

最近上公司强化班的课程,老师讲mfc编程的C++基础,讲多态性的时候引入了一个例子:

#include<iostream>
using namespace std;
 
class animal
{
public:
       animal()
       {
              cout<<"aniaml live!"<<endl;
       }
 
       ~animal()
       {
              cout<<"aniaml death!"<<endl;
       }
 
       virtual void breath()
       {
              cout<<"aniaml breath!"<<endl;
       }
};
 
class fish:public animal
{
public:
       fish()
       {
              cout<<"fish live!"<<endl;
       }
 
       ~fish()
       {
              cout<<"fish death!"<<endl;
       }
 
       void breath()//没有关键字virtual但是他也是virtual
       {
              cout<<"fish breath!"<<endl;
       }
};
 
class cat:public animal
{
public:
       cat()
       {
              cout<<"cat live!"<<endl;
       }
 
       ~cat()
       {
              cout<<"cat death!"<<endl;
       }
 
       void breath()//没有关键字virtual但是他也是virtual
       {
              cout<<"cat breath!"<<endl;
       }
};
 
int main()
{
       animal *pa3,*pa4;
       pa3 = new fish();
       pa4 = new cat();
 
       cout<<endl;
 
       pa3->breath();
       pa4->breath();
 
       delete pa3;
       delete pa4;
 
       system("pause");
       return 0;
}
这个例子恐怕大家想都不用想就能打出拉,但是却暴露了一个很重要的信息
看结果:
Animal live
Fish live
Animal live
Cat live
 
Fish breath
Cat breath
Animal death
Animal death

P3p4指针生成的是子对象fishcat为什么只有animal的析构被调用?为什么没有调用子类的析构函数?到底继承体系下的内存布局是什么样子的?

 

第一个想法是《effictive C++》上的条款7,这本书的第二版和第三版我都看过,不过因为没有什么实际做项目的经验,只是对其有一个大概的印象,但是这次老师给的代码所引出的问题让我马上意识到了这个问题的严重:既然声明的是子类型,那么析构的时候不调用子类型析构,就是说明这样会造成子类型那部分的内存泄露!
pa3和pa4指向子类型,而在结束时delete的却是一个父类型的对象!而这里的父类型只有一个非虚析构函数!
C++指出:当子类型对象经由一个父类型对象指针被删除,而父类型析构函数非虚,则结果没有定义!实际情况就是想结果里所显示的那样——子类型的那部分内存没有被销毁!
当然解决问题的方法很简单,给父类型的析构函数加上virtual关键字就ok了,结果也会有相应的显示:
Fish death
Animal death
Cat death
Animal death
 
只有当一个类型想要被派生时,虚析构函数才是必要的,原因也很简单:vptr占地方!他指向一个虚表vtbl。(详细说明看《effective C++(3e)》条款7)这里主要想明确一下c++的对象模型的基本结构。
Vptr的存在这个例子可以简略的证明,
class tree
{
public:
       tree()
       {
              cout<<"tree live"<<endl;
       }
       ~tree()
       {
              cout<<"tree death"<<endl;
       }
       void leaf()
       {
              cout<<"tree leaf"<<endl;
       }
};
在main函数里写:sizeof(tree)和sizeof(animal)结果是什么?1和4!tree和animal不同之处仅在于虚函数上,他们都没有属性,他们都只有方法,而c++的对象模型中有一点需要明确:类的大小最小是1!这时候的类没有虚函数没有属性。那么证明虚函数是占有空间的。
再来:在fish类中加入一个int型的属性weight,然后分别sizeof三种类型的对象tree的、fish的和cat的,就会发现结果是:1,8,4。还是那样:tree对象大小是1这是必须的,fish和cat的区别仅在于一个有int属性一个没有int的大小是4那么剩下的就是vptr了。
指针是没有类型的区别的,不同的是他指向的对象不同!如:
       double * pd;
       double * pi;
 
       cout<<sizeof(pd)<<endl;
       cout<<sizeof(pi)<<endl;
结果都是4!这也证明了vptr指针的存在的形式。
那么类型的函数呢?为什么只有属性?
C++把属性和方法分开考虑:
每个对象实例只包含两样东西:非静态成员的属性,虚拟函数的指针。前者就是weight这样的属性,后者vptr指向的类型的vtbl,vtbl中又包含了用于RTTI的type_info object和虚函数这样动态连编的东西。
每个类型的函数、静态成员等存放于另外的内存中,这些东西在内存中只有独一份的拷贝,调用时依靠this调用函数,用类型使用静态成员。 
为了达到多态的效果,继承体系中不同类型vptr里指向的东西的内容是不同的,这也就能实现动态连编的效果了。
 
由于不能画图,不能形象明确的表示出来一个子类实例的基本内存布局,其实可以把它理解成:
Base class
Derived class
两层结构,父类型中的属性在这块内存的上面,子类型的一些属性在下边,但是vptr在哪是根据具体编译器来定的,可能在最上边,高于base class的所有属性,也可能在最下边低于derived class的所有属性。
 
以上就是C++对象模型的基本结构
 
C++对象模型是一个太深奥的东西,就像《深入探索C++对象模型》的一则评论所说的那样:他不是婴幼儿奶粉、也不是较大婴儿的奶粉,而是成人专用的低脂高钙特殊奶粉。的确C++的复杂型很大一部分体现在了他的类机制中,他的复杂超出了我的想象也超出了一般编译原理书籍介绍的范畴,但是如果我们不了解C++编译器为我们构造了什么,那么怎么能够自信的说我们了解他呢。正像侯捷老师所说:“浮沙之上勿筑高台”。
当然也正是侯捷老师的这句话让我为了知其然而且知其所以然,心甘情愿的掏出崭新的红色毛爷爷。
 
原创粉丝点击