概述
- 虚函数表是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