这里不妨说下C++内存分布结构,我们来看看编译器是怎么处理类成员内存分布的,特别是在继承、虚函数存在的情况下。
工欲善其事,必先利其器,我们先用好Visual Studio工具,像下面这样一步一步来:
先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存布局,如果写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局。近期的VS版本都支持这样配置。
下面可以定义一个类,像下面这样:
                        
1 class Base2 {3     int a;4     int b;5 public:6     void CommonFunction();7 };
                        
然后编译一下,可以看到输出框里面有这样的排布:
这里不想花精力在内存对齐因素上,所以成员变量都设为int型。
从这里可以看到普通类的排布方式,成员变量依据声明的顺序进行排列(类内偏移为0开始),成员函数不占内存空间。
再看下继承,往后面添加如下代码:
                        
1 class DerivedClass: public Base2 {3     int c;4 public:5     void DerivedCommonFunction();6 };
                        
编译,然后看到如下的内存分布(父类的内存分布不变,这里只讨论子类成员变量的内存分布):
可以看到子类继承了父类的成员变量,在内存排布上,先是排布了父类的成员变量,接着排布子类的成员变量,同样,成员函数不占字节。
下面给基类加上虚函数,暂时注释掉DerivedClass,看一下这时的内存排布:
                        
1 class Base2 {3     int a;4     int b;5 public:6     void CommonFunction();7     void virtual VirtualFunction();8 };
                        
这个内存结构图分成了两个部分,上面是内存分布,下面是虚表,我们逐个看。VS所带编译器是把虚表指针放在了内存的开始处(0地址偏移),然后再是成员变量;下面生成了虚表,紧跟在&Base1_meta后面的0表示,这张虚表对应的虚指针在内存中的分布,下面列出了虚函数,左侧的0是这个虚函数的序号,这里只有一个虚函数,所以只有一项,如果有多个虚函数,会有序号为1,为2的虚函数列出来。
编译器是在构造函数创建这个虚表指针以及虚表的。
那么编译器是如何利用虚表指针与虚表来实现多态的呢?是这样的,当创建一个含有虚函数的父类的对象时,编译器在对象构造时将虚表指针指向父类的虚函数;同样,当创建子类的对象时,编译器在构造函数里将虚表指针(子类只有一个虚表指针,它来自父类)指向子类的虚表(这个虚表里面的虚函数入口地址是子类的)。
所以,如果是调用Base *p = new Derived();生成的是子类的对象,在构造时,子类对象的虚指针指向的是子类的虚表,接着由Derived*到Base*的转换并没有改变虚表指针,所以这时候p->VirtualFunction,实际上是p->vfptr->VirtualFunction,它在构造的时候就已经指向了子类的VirtualFunction,所以调用的是子类的虚函数,这就是多态了。
下面加上子类,并在子类中添加虚函数,像下面这样:
                        
1 class DerivedClass: public Base2 {3     int c;4 public:5     void DerivedCommonFunction();6     void virtual VirtualFunction();7 };
                        
可以看到子类内存的排布如下:
上半部是内存分布,可以看到,虚表指针被继承了,且仍位于内存排布的起始处,下面是父类的成员变量a和b,最后是子类的成员变量c,注意虚表指针只有一个,子类并没有再生成虚表指针了;下半部的虚表情况与父类是一样的。
我们把子类换个代码,像这样:
                        
1 class DerivedClass1 : public Base2 {3     int c;4 public:5     void DerivedCommonFunction();6     void virtual VirtualFunction2();7 };
                        
注意到这时我们并没有覆写父类的虚方法,而是重声明了一个新的子类虚方法,内存分布如下:
还是只有一个虚表指针,但是下方虚表的内容变化了,虚表的0号是父类的VirtualFunction,而1号放的是子类的VirtualFunction2。也就是说,如果定义了DerivedClass的对象,那么在构造时,虚表指针就会指向这个虚表,以后如果调用的是VirtualFunction,那么会从父类中寻找对应的虚函数,如果调用的是VirtualFunction2,那么会从子类中寻找对应的虚函数。
我们再改造一下子类,像这样:
                        
1 class DerivedClass1 : public Base2 {3     int c;4 public:5     void DerivedCommonFunction();6     void virtual VirtualFunction();7     void virtual VirtualFunction2();8 };
                        
我们既覆写父类的虚函数,也有新添的虚函数,那么可以料想的到,是下面的这种内存分布:
下面来讨论多重继承,代码如下:
                        
 1 class Base 2 { 3     int a; 4     int b; 5 public: 6     void CommonFunction(); 7     void virtual VirtualFunction(); 8 }; 9 10 11 class DerivedClass1: public Base12 {13     int c;14 public:15     void DerivedCommonFunction();16     void virtual VirtualFunction();17 };18 19 class DerivedClass2 : public Base20 {21     int d;22 public:23     void DerivedCommonFunction();24     void virtual VirtualFunction();25 };26 27 class DerivedDerivedClass : public DerivedClass1, public DerivedClass228 {29     int e;30 public:31     void DerivedDerivedCommonFunction();32     void virtual VirtualFunction();33 };
                        
内存分布从父类到子类,依次如下:
Base中有一个虚表指针,地址偏移为0
DerivedClass1继承了Base,内存排布是先父类后子类。
DerivedClass2的情况是类似于DerivedClass1的。
下面我们重点看看这个类DerivedDerivedClass,由外向内看,它并列地排布着继承而来的两个父类DerivedClass1与DerivedClass2,还有自身的成员变量e。DerivedClass1包含了它的成员变量c,以及Base,Base有一个0地址偏移的虚表指针,然后是成员变量a和b;DerivedClass2的内存排布类似于DerivedClass1,注意到DerivedClass2里面竟然也有一份Base。
这里有两份虚表了,分别针对DerivedClass1与DerivedClass2,在&DerivedDericedClass_meta下方的数字是首地址偏移量,靠下面的虚表的那个-16表示指向这个虚表的虚指针的内存偏移,这正是DerivedClass2中的{vfptr}在DerivedDerivedClass的内存偏移。
如果采用虚继承,像下面这样:
                        
 1 class DerivedClass1: virtual public Base 2 { 3     int c; 4 public: 5     void DerivedCommonFunction(); 6     void virtual VirtualFunction(); 7 }; 8  9 class DerivedClass2 : virtual public Base10 {11     int d;12 public:13     void DerivedCommonFunction();14     void virtual VirtualFunction();15 };16 17 class DerivedDerivedClass :  public DerivedClass1, public DerivedClass218 {19     int e;20 public:21     void DerivedDerivedCommonFunction();22     void virtual VirtualFunction();23 };
                        
Base类没有变化,但往下看:
DerivedClass1就已经有变化了,原来是先排虚表指针与Base成员变量,vfptr位于0地址偏移处;但现在有两个虚表指针了,一个是vbptr,另一个是vfptr。vbptr是这个DerivedClass1对应的虚表指针,它指向DerivedClass1的虚表vbtable,另一个vfptr是虚基类表对应的虚指针,它指向vftable。
下面列出了两张虚表,第一张表是vbptr指向的表,8表示{vbptr}与{vfptr}的偏移;第二张表是vfptr指向的表,-8指明了这张表所对应的虚指针位于内存的偏移量。
DerivedClass2的内存分布类似于DerivedClass1,同样会有两个虚指针,分别指向两张虚表(第二张是虚基类表)。
下面来仔细看一下DerivedDerivedClass的内存分布,这里面有三个虚指针了,但base却只有一份。第一张虚表是内含DerivedClass1的,20表示它的虚指针{vbptr}离虚基表指针{vfptr}的距离,第二张虚表是内含DerivedClass2的,12表示它的虚指针{vbptr}离虚基表指针{vfptr}的距离,最后一张表是虚基表,-20指明了它对应的虚指针{vfptr}在内存中的偏移。
虚继承的作用是减少了对基类的重复,代价是增加了虚表指针的负担(更多的虚表指针)。
下面总结一下(当基类有虚函数时):
这里是一个简单的内存泄漏自动检查机制。只适用于单线程情况下。工作原理就是将已经分配的内存用一个双向链表串联起来,释放内存时从链表里删除。在程序退出时,将链表中未释放的内存打印出来。同时在内存块中保存文件名和行号,用于定位内存分配地址。
  001 // placement_new.cpp : Defines the entry point for the console application.
  002 //
  003
  004 #include "stdafx.h"
  005 #include <iostream>
  006 #include <cassert>
  007
  008 #define ASSERT assert
  009 char buff[1024];
  010
  011 using namespace std;
  012
  013 struct MEMORY_TAG
  014 {
  015 int nSize;
  016 const char* szFile;
  017 int nLine;
  018 MEMORY_TAG* pPrev;
  019 MEMORY_TAG* pNext;
  020 };
  021
  022 MEMORY_TAG g_header = {0, 0, 0, 0, 0};
  023 // 打印出所有未释放的内存
  024 void DumpUnAllocatedMem()
  025 {
  026 for(MEMORY_TAG* pEntry = g_header.pNext; pEntry; pEntry = pEntry->pNext)
  027 {
  028 printf("%s(%d) : leak %d bytes ", pEntry->szFile ? pEntry->szFile : "", pEntry->nLine, pEntry->nSize);
  029 }
  030 }
  031 // 统计已经分配的内存块数和字节数
  032 int CountOfAllocatedMem(void* pnSize= NULL)
  033 {
  034 int nCount = 0;
  035 size_t allocated = 0;
  036 for(MEMORY_TAG* pEntry = g_header.pNext; pEntry; pEntry = pEntry->pNext)
  037 {
  038 allocated += pEntry->nSize;
  039 nCount++;
  040 }
  041 printf("%d count, %d total ", nCount, allocated);
  042 return nCount;
  043 }
  044
  045 // 实现一个全局operator new可以有文件名和行号作为参数
  046 void* operator new(size_t size, const char* pszFile, int line)
  047 {
  048 size_t nRealSize = size + sizeof(MEMORY_TAG);
  049 MEMORY_TAG* pTag = (MEMORY_TAG*)malloc(nRealSize);
  050
  051 pTag->nSize = nRealSize;
  052 pTag->szFile = pszFile;
  053 pTag->nLine = line;
  054 // 插入队列头部
  055 if (g_header.pNext)
  056 {
  057 g_header.pNext->pPrev = pTag;
  058 }
  059 pTag->pNext = g_header.pNext;
  060 g_header.pNext = pTag;
  061 pTag->pPrev = &g_header;
  062
  063 return pTag + 1;
  064 }
  065
066 void* operator new(size_t size)
  067 {
  068 return (operator new(size, __FILE__, __LINE__));
  069 }
  070
  071
  072 void operator delete(void* p, const char*, int line)
  073 {
  074 delete p;
  075 }
  076
  077 void operator delete(void* p)
  078 {
  079 MEMORY_TAG* pTag = (MEMORY_TAG*)(((char*)p)-sizeof(MEMORY_TAG));
  080 // 从队列中删除
  081 pTag->pPrev->pNext = pTag->pNext;
  082 if (pTag->pNext)
  083 {
  084 pTag->pNext->pPrev = pTag->pPrev;
  085 }
  086 free(pTag);
  087 }
  088
  089 class Object
  090 {
  091 public:
  092 Object()
  093 {
  094 cout << "Object's contructor." << endl;
  095 }
  096
  097 ~Object()
  098 {
  099 cout << "Object's destructor." << endl;
  100 }
  101
  102 char data[1024];
  103 };
  104
  105 #define NEW new(__FILE__, __LINE__)
  106
  107 int _tmain(int argc, _TCHAR* argv[])
  108 {
  109 ASSERT(CountOfAllocatedMem() == 0);
  110 Object* pObj = new(buff) Object;
  111
  112 pObj->~Object();
  113
  114 pObj = NEW(Object);
  115 ASSERT(CountOfAllocatedMem() == 1);
  116
  117 delete pObj;
  118 ASSERT(CountOfAllocatedMem() == 0);
  119
  120 pObj = NEW Object;
  121 ASSERT(CountOfAllocatedMem() == 1);
  122 delete pObj;
  123 ASSERT(CountOfAllocatedMem() == 0);
  124
  125 pObj = NEW Object;
  126
  127 char* p = new char[968];
  128 ASSERT(CountOfAllocatedMem() == 2);
  129
  130 DumpUnAllocatedMem();
  131 return 0;
  132 }