C++虚函数表与类的内存分布深入分析理解
不可定义为虚函数的函数类的静态函数和构造函数不可以定义为虚函数:静态函数的目的是通过类名函数名访问类的static变量或者通过对象调用staic函数实现对static成员变量的读写要求内存中只有一份数据。而虚函数在子类中重写并且通过多态机制实现动态调用在内存中需要保存不同的重写版本。构造函数的作用是构造对象而虚函数的调用是在对象已经构造完成并且通过调用时动态绑定。动态绑定是因为每个类对象内部都有一个指针指向虚函数表的首地址。而且虚函数类的成员函数static成员函数都不是存储在类对象中而是在内存中只保留一份。将析构函数定义为虚函数的作用类的构造函数不能定义为虚函数析构函数可以定义为虚函数这样当我们delete一个指向子类对象的基类指针时可以达到调用子类析构函数的作用从而动态释放内存。如下我们先定义一个基类和子类12345678910111213141516171819202122232425classVirtualTableA{public:virtual~VirtualTableA(){cout Desturct Virtual Table A endl;}virtualvoidprint(){cout print virtual table A endl;}};classVirtualTableB :publicVirtualTableA{public:virtual~VirtualTableB(){cout Desturct Virtual Table B endl;}virtualvoidprint();};voidVirtualTableB::print(){cout this is virtual table B endl;}我们写一个函数做测试1234567891011voiddestructVirtualTable(){VirtualTableA *pa newVirtualTableB();useTable(pa);deletepa;}voiduseTable(VirtualTableA *pa){//实现动态调用pa-print();}程序输出this is virtual table BDesturct Virtual Table BDesturct Virtual Table A在上面的例子中我们先在destructVirtualTable函数中new了一个VirtualTableB类型对象并用基类VirtualTableA的指针指向了这个对象。然后将基类指针对象pa传递给useTable函数这样会根据多态原理调用VirtualTableB的print函数然后再执行delete pa操作。此时如果pa的析构函数不写成虚函数那么就只会调用VirtualTableA的析构函数不会调用子类VirtualTableB的析构函数导致内存泄露。而我们将析构函数写成虚析构之后可以看到先调用了子类VirtualTableB的析构函数再调用了基类VirtualTableA的析构函数达到了释放子类空间的目的。有人会问将析构函数不写为虚函数直接delete子类对象VirtualTableB调用子类的析构函数不可以吗比如如下的调用12VirtualTableB *pb newVirtualTableB();deletepa;上述调用没有问题无论析构函数是否为虚析构都可以成功释放子类空间。但是项目编程中常常会编写一些通用接口比如上面的useTable函数它只接受VirtualTableA类型的指针所以我们常常会用基类指针接受子类对象来通过多态的方式调用子类函数为了方便delete基类指针也要释放子类空间就要将析构函数设置为虚函数。虚函数表原理为了介绍虚函数表原理我们先实现一个基类和子类1234567891011121314151617classBaseclass{public:Baseclass() : a(1024) {}virtualvoidf() { cout Base::f endl; }virtualvoidg() { cout Base::g endl; }virtualvoidh() { cout Base::h endl; }inta;};// 0 1 2 3 4 5 6 7(虚函数表空间) 8 9 10 11 12 13 14 15(存储的是a)classDeriveClass :publicBaseclass{public:virtualvoidf() { cout Derive::f endl; }virtualvoidg2() { cout Derive::g2 endl; }virtualvoidh3() { cout Derive::h3 endl; }};一个类对象其内存分布的基本结构为虚函数表地址非静态成员变量类的成员函数不占用类对象的空间他们分布在一片属于类的共有区域。类的静态成员函数喝成员变量不占用类对象的空间他们分配在静态区。虚函数表的地址存储在类对象的起始位置。所以我们利用这个原理通过寻址的方式访问虚函数表里的函数123456789101112131415161718192021voiduseVitualTable(){Baseclass b;b.a 1024;cout sizeof b is sizeof(b) endl;int*p (int*)(b);cout pointer address of vitural table p endl;cout address of b is b endl;cout address of a is p 2 endl;cout address of p1 is p 1 endl;cout value of a is *(p 2) endl;cout address of vitural table (int*)(*p) endl;cout sizeof int is sizeof(int) endl;cout sizeof p is sizeof(p) sizeof(int*) is sizeof(int*) endl;Func pFun (Func)(*(int*)(*p));pFun();pFun (Func) * ((int*)(*p) 2);pFun();pFun (Func)(*((int*)(*p) 4));pFun();}上面的程序输出sizeof b is 16pointer address of vitural table 0xb6fdd0address of b is 0xb6fdd0address of a is 0xb6fdd8address of p1 is 0xb6fdd4value of a is 1024address of vitural table0x46d890sizeof int is 4sizeof p is 8 sizeof(int*) is 8Base::fBase::gBase::h可以看到b的大小为16字节因为我的机器是64位的所以指针类型都占用8字节int 占用4字节但是要遵循补齐原则结构体的大小要为最大成员大小的整数倍所以要补齐4字节那么844 16 字节关于类对象对齐和补齐原则稍后再详述。b的内存分布如下图这个根据不同的机器所占的字节数不一样在32位机器上int为4字节虚函数表地址为4字节44 8字节这个再之后再说明对齐和补齐的原则。b表示取b的地址因为虚函数表地址存储在b的起始地址所以b也是虚函数表的地址的地址我们通过int*强转是方便存储b的地址因为64位机器指针都是8字节32位机器指针是4字节。p为虚函数表的地址的地址p1具体移动了4个字节因为p1移动多少个字节取决于p所指向的数据类型int,int为4字节所以p1在p的地址移动四个字节p2在p的地址移动8个字节。p只想虚函数表的地址换句话说p存储的是虚函数表的地址虚函数表地址占用8字节p2就是从p向后移动8字节这样刚好找到a的地址。那么*(p2)就是取a的数值。int*(*p)就是取虚函数表的地址转为int*是方便读写。我们将b的内存分布以及虚函数表结构画出来上图中可以看到虚函数表中存储的是虚函数的地址所以通过不断位移虚函数表的指针就可以达到指向不同虚函数的目的。12Func pFun (Func)(*(int*)(*p));pFun();*(int *)(*p)就是取出虚函数表首地址指向的虚函数再通过Func转化为函数类型然后调用pFun即可调用虚函数f。所以想调用第二个虚函数g将(int*)(*p)加2 位移8个字节即可12pFun (Func) * ((int*)(*p) 2);pFun();同样的道理调用h就不赘述了。继承关系中虚函数表结构DeriveClass继承了BaseTest类子类如果重写了虚函数则子类的虚函数表中存储的虚函数为子类重写的否则为基类的。我们画一下DeriveClass的虚函数表结构因为函数f被DeriveClass重写所以DeriveClass的虚函数表存储的是自己重写的f。而虚函数g和h没有被DeriveClass重写所以DeriveClass虚函数表存储的是基类的g和h。另外DeriveClass虚函数表里也存储了自己特有的虚函数g2和h3.