Bootstrap

C++虚函数表和虚基类表

概述

  • 虚函数表是C++动态多态的具体实现手段,其中构造函数不能是虚函数析构可以是虚函数,且常作为虚函数
  • 虚基类表解决了多重继承、多继承,基类在内存有多份拷贝的以及二义性的问题

两者原理一样

虚函数表

  • 为什么构造函数不能是虚函数?
    (1)具有继承关系的类的构造顺序是:先基类后派生类
    (2)具有继承关系的类的析构顺序是:先派生类后基类
class A
{
public:
	virtual A() {	//virtual不能修饰构造函数,这里编译器不通过
		OutputDebugStringW(_T("--A构造\n"));
	};
	~A() {
		OutputDebugStringW(_T("--A析构\n"));
	};
};

class B : public A
{
public:
	B() {
		OutputDebugStringW(_T("--B构造\n"));
	};
	~B() {
		OutputDebugStringW(_T("--B析构\n"));
	};
};

如上面代码所示,当A* pA = new B()时,根据(1)先去构造A,A构造完后,因为是虚函数,所以会去调用B的构造函数,但是B实例都还没有完成,B实例的vptr仍未初始化,因此找不到B的构造函数的,所以构造函数不是能虚函数

  • 虚析构函数的作用?
    当A* pA = new B(); delete pA时,会去调用A的析构,如果A的析构不是虚函数,那么就不会多态地去调用B的析构函数,~B()就没有调用机会,析构我们经常用来释放资源和内存,如果B有需要手动释放的资源和内存就不会得到释放,虚析构函数主要解决该种情况
    如果是B b入栈的情况,构造时:A的构造->B的构造,析构时:B的析构->A的析构,这跟栈FILO规则符合,上面讲的是delete一个指向派生类的基类指针的时候
    网上详细解说

虚基类表

网上详细解说

总结:
一个类仅有一个vbptr属于自身的,因此vbptr第0元素是指归属类相对于vbptr的偏移值

class Base {
public:
	int base;
};

class ChildA : virtual public Base {
public:
	int a;
	virtual void vfuna() {};
};

class ChildB : virtual public Base {
public:
	int b;
	virtual void vfunb() {};
};

class ChildC : public ChildA, public ChildB {
public:
	int c;
	//void vfuna() {};
};

4和16的vbptr分别属于父类ChildA和ChildB的,因此第0元素分别是-4,-4,而不是基于ChildC

对ChildC的继承方式修改一下,virtual public ChildA:

class ChildC : virtual public ChildA, public ChildB {
public:
	int c;
	//void vfuna() {};
};

在这里插入图片描述

ChildA的内存布局发生变化,但是ChildA的vbptr第0元素还是-4,因为vbptr还是归属ChildA

///
虚函数指针vfptr则是覆盖的,即原来基类有的vfptr,继承过来后,就变成当前子类来计算偏移值,还是如上图两种情况
在这里插入图片描述
virtual public ChildA方式
在这里插入图片描述

vs查看类内存布局

本人浅试了一下网上“微软的Visual Studio提供给用户显示C++对象在内存中的布局的选项:/d1reportSingleClassLayout。使用方法很简单,直接在[工具(T)]选项下找到“Visual Studio命令提示©”后点击即可。切换到cpp文件所在目录下输入如下的命令即可”

于是另外寻找了方法,开始菜单栏->vs2015开发人员命令提示->cl /d1 reportSingleClassLayout类名 绝对路径.cpp

cl /d1 reportSingleClassLayout类名 文件名.cpp

二级指针

//d是D实例首址,(int*)d转成int*指针,(*(int*)d)取第一个int的内容,虚基类表(首址),(int*)(*(int*)d)
//将虚基类表(首地址)转成int*指针,*(int*)(*(int*)d)取第一个int的内容,即虚基类表第一个int内容,继承
//类的偏移
std::cout << "    [0] => " << *(int*)(*(int*)d) << std::endl;


// (((int*)(*(int*)d)) + 1)虚基类表(首址)+1,即偏移一个int,得到虚基类表的第二个元素
std::cout << "    [1] => " << *(((int*)(*(int*)d)) + 1) << std::endl;   

指针加减一个整数

指针的加减实际偏移是按照该指针指向的数据类型的大小(字节)为单位

  • 大端存储模式:数据的低位保存在内存中的高地址中,数据的高位保存在内存中的低地址中;
  • 小端存储模式:数据的低位保存在内存中的低地址中,数据的高位保存在内存中的高地址中;

例子:

char *p = new char(4);		//假设首地址0x10
short*pShort = (short*)p;		//强转成short
*p = 'a';					//地址0x10		对应的ascii码0x61
*(++p) = 'b';				//地址0x11		对应的ascii码0x62
*(++p) = 'c';				//地址0x12		对应的ascii码0x63
*(++p) = 'd';				//地址0x13		对应的ascii码0x64


cout << *pShort << endl;			//地址0x10		值0x6261	因为小端存储,61是低地址,62高地址
cout << *(pShort + 1) << endl;			//地址0x12		值0x6463
int iVect[5] = { 1,2,3,4};		//假设首地址0x10
cout << *iVect << endl;			//地址0x10		值0x0001
cout << *(iVect + 1) << endl;	//地址0x14,iVect+1实际地址偏移了4个字节,即一个int大小		值0x0002
cout << *(iVect + 2) << endl;	//地址0x18		值0x0003
cout << *(iVect + 3) << endl;	//地址0x1c		值0x0004
;