虚函数详解第一篇:对象内存模型浅析

C++中的虚函数的内部实现机制到底是怎样的呢?
    鉴于涉及到的内容有点多,我将分三篇文章来介绍。
    第一篇:对象内存模型浅析,这里我将对对象的内存模型进行简单的实验和总结。
    第二篇:继承对象的构造和析构浅析,这里我将对存在继承关系的对象的构造和析构进行简单的实验和总结。
    第三篇:虚函数的内部机制浅析,这里我将对虚函数内部的实现机制进行实验总结。
    我使用的编译器是VS2008,有不足或者不准确的地方,欢迎大家拍砖(我个人非常迫切的希望得到大家的指正),我会及时修正相关内容。
 
    开始正题:对象内存模型浅析:
    
  1. #include <tchar.h>
  2. #include <iostream>
  3. using namespace std;
  4. #pragma pack (1)
  5. class Person
  6. {
  7. private:
  8. int m_nAge;
  9. };
  10. class Man : public Person
  11. {
  12. private:
  13. double m_dHeight;
  14. };
  15. int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
  16. {
  17. Person Jack;
  18. Man Mike;
  19. cout << sizeof(Jack) << endl;
  20. cout << sizeof(Mike) << endl;
  21. return 1;
  22. }
    首先解释一下#pragma pack(1)这条语句的作用,它要求编译器将字节对齐的最小单位设定为1个字节。
    关于字节对齐,简单的解释就是,假定一个32位的CPU,读取一个存储在内存中的int型的变量,如果该int变量存放在内存中的首地址是偶地址,那么CPU一个周期就能读出这32bit的数据,如果该int变量存放在内存中的首地址是奇地址,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。所以,如果我们将字节对齐设置为4个字节,那么理论上,CPU执行我们代码的速度要比将字节对齐设置为1个字节的速度要快。
    所以,如果我们将字节对齐设置为8个字节,那么
    int nNum1;
    double dNum2;
将会占用16个字节的大小,而如果我们将字节对齐设置为1个字节,那么它将会占用12个字节的大小,上述代码将字节对齐设置为1个字节,是为了防止字节对齐干扰了我们对于对象内存模型的实验。
    回到主题,上述代码的执行结果如下:
    4
    12
    我们看到,Person类对象占用了4个字节大小的内存空间,Man类对象占用了12个字节的大小的内存空间,所以,Man类中实际上有两个成员变量,int m_nAge和double m_dHeight,所以可以得出一个结论:派生类对象中同时包含基类的成员变量。
    那么它们在内存中的位置是怎样的呢?
  1. #include <tchar.h>
  2. #include <iostream>
  3. using namespace std;
  4. #pragma pack (1)
  5. class Person
  6. {
  7. private:
  8. int m_nAge;
  9. };
  10. class Man : public Person
  11. {
  12. private:
  13. double m_dHeight;
  14. };
  15. class Woman : public Person
  16. {
  17. private:
  18. double m_dWigth;
  19. };
  20. int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
  21. {
  22. Person Jack;
  23. Man Mike;
  24. Woman Susan;
  25. cout << &Jack << endl;
  26. cout << &Mike << endl;
  27. cout << &Susan << endl;
  28. return 1;
  29. }
上述代码输出了Person类对象和Man类对象的地址,执行结果如下:
0012FF60
0012FF4C
0012FF38
我们知道,0012FF60和0012FF4C之间有14个字节的内存空间,0012FF38和0012FF4C之间有14个字节的内存空间,我们将Man类对象分成Person基类部分(int m_nAge)和Man派生类部分(double m_dHeight),将Woman类对象分成Person基类部分(int m_nAge)和Woman派生类部分(double m_dWeight)。那么,Man类和Woman类的对象内存模型如下:
 
 
所以,
  • Person Jack;
  • Man Mike;
  • Woman Susan;
这三行代码实际上产生了3个Person基类部分、一个Man派生类部分和一个Woman派生类部分,而非像代码中写的表意那样,有一个Person基类部分,一个Man派生类部分和一个Woman派生类部分。
类的继承和派生只是简化方便我们程序员编写代码,并不会简化派生类对象占用的内存大小。
 
C++中的虚函数的内部实现机制到底是怎样的呢?
    鉴于涉及到的内容有点多,我将分三篇文章来介绍。
    第一篇:对象内存模型浅析,这里我将对对象的内存模型进行简单的实验和总结。
    第二篇:继承对象的构造和析构浅析,这里我将对存在继承关系的对象的构造和析构进行简单的实验和总结。
    第三篇:虚函数的内部机制浅析,这里我将对虚函数内部的实现机制进行实验总结。
    我使用的编译器是VS2008,有不足或者不准确的地方,欢迎大家拍砖(我个人非常迫切的希望得到大家的指正),我会及时修正相关内容。
 

开始正题:继承对象的构造和析构浅析:

    在虚函数详解第一篇中,我简单的介绍了C++对象内存模型。我们了解到派生类对象是由基类部分和派生部分构成的,那么该派生类对象是如何被构造和析构的呢?
    
  1. #include <tchar.h>
  2. #include <iostream>
  3. using namespace std;
  4. class Person
  5. {
  6. public:
  7. Person()
  8. {
  9. cout << _T("基类的构造函数被调用") << endl;
  10. }
  11. ~Person()
  12. {
  13. cout << _T("基类的析构函数被调用") << endl;
  14. }
  15. };
  16. class Man : public Person
  17. {
  18. public:
  19. Man()
  20. {
  21. cout << _T("派生类的构造函数被调用") << endl;
  22. }
  23. ~Man()
  24. {
  25. cout << _T("派生类的析构函数被调用") << endl;
  26. }
  27. };
  28. int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
  29. {
  30. Man Mike;
  31. return 1;
  32. }
上述代码的执行结果如下:
我们可以看到:构造一个派生类对象的时候,先调用基类的构造函数,再调用派生类的构造函数,析构一个派生类对象的时候,先调用派生类的析构函数,再调用基类的析构函数。
 
    上述内容讲述的是普通派生类的构造和析构过程,对于具有虚函数的派生类的构造和析构过程是怎样的呢?
    
  1. #include <tchar.h>
  2. #include <iostream>
  3. using namespace std;
  4. class Person
  5. {
  6. public:
  7. Person()
  8. {
  9. cout << _T("基类的构造函数被调用") << endl;
  10. }
  11. virtual void Height()
  12. {
  13. cout << _T("人类具有身高属性") << endl;
  14. }
  15. virtual ~Person()
  16. {
  17. cout << _T("基类的析构函数被调用") << endl;
  18. }
  19. };
  20. class Man : public Person
  21. {
  22. public:
  23. Man()
  24. {
  25. cout << _T("派生类的构造函数被调用") << endl;
  26. }
  27. virtual void Height()
  28. {
  29. cout << _T("男人具有身高属性") << endl;
  30. }
  31. virtual ~Man()
  32. {
  33. cout << _T("派生类的析构函数被调用") << endl;
  34. }
  35. private:
  36. double m_dHeight;
  37. double m_dWeight;
  38. };
  39. int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
  40. {
  41. Person* pPersonObj = new Man;
  42. delete pPersonObj;
  43. return 1;
  44. }
上述代码的执行结果如下:
大家可能注意到了,上述代码中基类和派生类的析构函数都采用虚析构函数,而在_tmain函数中的调用方式也采用了Person* pPersonObj = new Man这种多态调用方式。当delete pPersonObj被执行来释放派生类对象的时候,实际上调用的是派生类对象的虚析构函数,而派生类对象的虚析构函数会调用基类的析构函数,这样就能将派生类对象完美的析构,如果这里不采用虚析构函数,会是什么结果呢?
 
  • #include <tchar.h>
  • #include <iostream>
  • using namespace std;
  • class Person
  • {
  • public:
  • Person()
  • {
  • cout << _T("基类的构造函数被调用") << endl;
  • }
  • virtual void Height()
  • {
  • cout << _T("人类具有身高属性") << endl;
  • }
  • ~Person()
  • {
  • cout << _T("基类的析构函数被调用") << endl;
  • }
  • };
  • class Man : public Person
  • {
  • public:
  • Man()
  • {
  • cout << _T("派生类的构造函数被调用") << endl;
  • }
  • virtual void Height()
  • {
  • cout << _T("男人具有身高属性") << endl;
  • }
  • virtual ~Man()
  • {
  • cout << _T("派生类的析构函数被调用") << endl;
  • }
  • private:
  • double m_dHeight;
  • double m_dWeight;
  • };
  • int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
  • {
  • Person* pPersonObj = new Man;
  • delete pPersonObj;
  • return 1;
  • }
上述代码执行结果如下:
 
我们可以看到,当delete pPersonObj被执行的时候,只调用了基类的析构函数,并没有调用派生类的析构函数,所以这个对象的派生部分的内存并没有被释放,从而造成内存泄露。
所以:当基类中包含有虚函数的时候,析构函数一定要写成虚析构函数,否则会造成内存泄露。
为什么一定要这么做呢?我们在第三篇的内容里寻找答案。
 
第三篇:自己打算写,待续。。。

C++虚函数解析(转载)的更多相关文章

  1. 构造函数为什么不能是虚函数 ( 转载自C/C++程序员之家)

    从存储空间角度,虚函数对应一个指向vtable虚函数表的指针,这大家都知道,可是这个指向vtable的指针其实是存储在对象的内存空间的.问题出来了,如果构造函数是虚的,就需要通过 vtable来调用, ...

  2. C++中的虚函数解析[The explanation for virtual function of CPlusPlus]

    1.什么是虚函数?                                                                                            ...

  3. 【转】C++虚函数解析

    本文转自陈皓大叔(左耳朵耗子)的博客www.coolshell.com. 文章是很久之前所写,去年还在写C++时有幸拜读,现在想起来还是相当有价值一转的,如果有一定C++基础(特别是读过<深度探 ...

  4. C++ 虚函数表解析(比较清楚,还可打印虚函数地址)

    C++ 虚函数表解析 陈皓 http://blog.csdn.net/haoel 前言 C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父 ...

  5. 【转载】 C++多继承中重写不同基类中相同原型的虚函数

    本篇随笔为转载,原文地址:C++多继承中重写不同基类中相同原型的虚函数. 在C++多继承体系当中,在派生类中可以重写不同基类中的虚函数.下面就是一个例子: class CBaseA { public: ...

  6. [转载]C++虚函数浅析

    原文:http://glgjing.github.io/blog/2015/01/03/c-plus-plus-xu-han-shu-qian-xi/ 感谢:单刀土豆 C++虚函数浅析 JAN 3RD ...

  7. C++析构函数定义为虚函数(转载)

    转载:http://blog.csdn.net/alane1986/article/details/6902233 析构函数执行时先调用派生类的析构函数,其次才调用基类的析构函数.如果析构函数不是虚函 ...

  8. (C/C++学习)5.C++中的虚继承-虚函数-多态解析

    说明:在C++学习的过程中,虚继承-虚函数经常是初学者容易产生误解的两个概念,它们与C++中多态形成的关系,也是很多初学者经常产生困惑的地方,这篇文章将依次分别对三者进行解析,并讲述其之间的联系与不同 ...

  9. C++虚函数及虚函数表解析

    一.背景知识(一些基本概念) 虚函数(Virtual Function):在基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数.纯虚函数(Pure Virtual Functio ...

随机推荐

  1. iOS:面向对象的思想使用sqlite数据库

    SQLite支持的常见数据类型如下所示. –INTEGER 有符号的整数类型 –REAL 浮点类型 –TEXT 字符串类型,采用UTF-8和UTF-16字符编码 –BLOB 二进制大对象类型,能够存放 ...

  2. QT源码之Qt信号槽机制与事件机制的联系

    QT源码之Qt信号槽机制与事件机制的联系是本文要介绍的内容,通过解决一个问题,从中分析出的理论,先来看内容. 本文就是来解决一个问题,就是当signal和slot的连接为Qt::QueuedConne ...

  3. 【Android UI】使用RelativeLayout与TableLayout实现登录界面

    使用RelativeLayout与TableLayout分别实现两种登录界面,学习RelativeLayout布局 中如何对齐与调整组件相对位置,使用TableLayout实现登录界面,学习如何设置列 ...

  4. 数据库插入数据返回当前自增主键ID值的方法

    当我们插入一条数据的时候,我们很多时候都想立刻获取当前插入的主键值返回以做它用.我们通常的做法有如下几种: 1. 先 select max(id) +1 ,然后将+1后的值作为主键插入数据库: 2. ...

  5. RaceWeb介绍(7):由500强公司数据高速生成百度地图——生成坐标字段及坐标数据

    接上篇. 一.生成X坐标.Y坐标两个字段. 我们须要为每一个公司建立X坐标和Y坐标字段,用来保存XY坐标. 既然为了突出"快",这一步就有程序来完毕吧. 右键单击"世界5 ...

  6. WCF 之 数据契约

    前面几篇讲的都只能传递string类型的简单参数,数据契约就是用来解决如传递一个带有多个属性的Class类型的对象的. WCF推荐使用数据契约的方式实现数据的序列化.这部分的内容很好理解但是很重要,先 ...

  7. ubuntu12.04下helloworld驱动从失败到成功过程

    最近在看linux的设备驱动程序,写一个简单的helloworld程序都花了我好久的时间,具体过程如下: 编写helloworld.c 编写Makefile 注意,makefile中的命令那里是一个t ...

  8. 〖Android〗巧用/system/etc/mkshrc文件,把busybox常用命令映射(链接)出来;

    在/system/etc/mkshrc文中尾部添加以下代码即可: # for busybox for n in $(busybox --list) do eval alias $n=\'busybox ...

  9. 一些Java面试题深入分析

    一.&和&&的差别? &和&&都能够用作逻辑与的运算符.表示逻辑与(and),当运算符两边的表达式的结果都为true时.整个运算结果才为true,否则, ...

  10. 携程实时大数据平台演进:1/3 Storm应用已迁到JStorm

    携程大数据平台负责人张翼分享携程的实时大数据平台的迭代,按照时间线介绍采用的技术以及踩过的坑.携程最初基于稳定和成熟度选择了Storm+Kafka,解决了数据共享.资源控制.监控告警.依赖管理等问题之 ...