上一章我们学习了多态的形式和如何使用多态,这一章我们将来讲一讲多态的原理,搬好小板凳准备开讲啦…
前情回顾: 认识多态 + 多态的条件及其性质
先来一道笔试题:sizeof(Base)是多少?
class Base
{
public:
void Func1()
{
cout << "Func1()" << endl;
}
virtual void Func2()
{
cout << "Func3()" << endl;
}
virtual void Func3()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
char _ch;
};
//有了虚函数之后对象中就有了一个表 -- 虚表(虚函数表)
int main()
{
Base b;
cout << sizeof(Base) << endl;
return 0;
}
先看运行结果:(不同位数的系统下大小是不一样的)
按照内存对齐的结果应该是 - 8,而现在答案确实12,这是怎么回事?
如图所示:
解释:
先看代码:
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
void Func3()
{
cout << "Derive::Func3()" << endl;
}
private:
int _d = 2;
};
先来看一下有虚函数的类实例化对象的大小:
结果:子类的大小是12。
综上小结:
普通调用和多态调用:
多态能够实现的依赖基础是:虚表完成了覆盖:
注意:
- 这些地址不是直接存在对象里的,是间接存的,对象里存的是一个指针,这个指针指向的表是虚表,虚表中存的是虚函数的地址。
- 不要和继承中菱形虚拟继承中的虚基表弄混了,虚基表中存的是偏移量。
1.父类赋值给子类对象,也可以切片,为什么实现不了多态?
(1)从汇编语法编译器的角度分析:
(2)从最底层来分析为什么不支持对象实现多态:
2.问题来了,切片拷贝的时候,父类那一部分的虚表指针要拷贝过去吗?
3.为什么呢?
4.如果虚表拷贝了的话:
同一个类型都是指向一张表的,同一个类型的不同对象它们的虚表都是一样的。(重点)
5.显然编译器也没有拷贝虚表:
动静态多态:
下看代码:
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
void Func3()
{
cout << "Derive::Func3()" << endl;
}
virtual void Func4()
{
cout << "Derive::Func4()" << endl;
}
private:
int _d = 2;
};
int main()
{
Derive d;
return 0;
}
通过监视窗口看:
我们先来看新增的虚函数Func4是否在虚表中:
通过内存窗口看:
下面我们来验证一下:(目的是确认Func4的指针在不在虚表)
补充:
打印子类的虚表,见下述代码:
//正确定义:
typedef void(*V_FUNC)();
//void PrintVFTable(V_FUNC a[]) -- 数组在传参的时候都会退化成了指针
//void PrintVFTable(void(**a)())-- 不用typedef的写法
void PrintVFTable(V_FUNC* a)
{
printf("vfptr:%p\n", a);
//**切记这里要记得清理解决方案** -- 不然会有非法访问
//g++的话在这里就要写死,因为它的虚表中不存在空指针
for (size_t i = 0; a[i] != nullptr; i++)
{
//printf("[%d]:%p\n", i, a[i]);
printf("[%d]:%p->", i, a[i]);
//用函数的地址直接去调用函数 -- 通过函数打印出结果便于观察
V_FUNC f = a[i];
f();
}
cout << endl;
}
我们该如何取对象头上【四个字节】呢?
解决办法(重点理解):
int main()
{
Base b;
Derive d;
//函数指针数组的地址指针
PrintVFTable((V_FUNC*)(*((int*)&d)));//取到对象头4byte的虚表指针
return 0;
}
运行窗口,监视和内存窗口看一看:
虚表存在哪个区域?
我们来验证一下:
int main()
{
Base b1;
Base b2;
Base b3;
Base b4;
//打印虚表
PrintVFTable((V_FUNC*)(*((int*)&b1)));
PrintVFTable((V_FUNC*)(*((int*)&b2)));
PrintVFTable((V_FUNC*)(*((int*)&b3)));
PrintVFTable((V_FUNC*)(*((int*)&b4)));
return 0;
}
深入理解:(重点)
按理来说在编译的时候就建好了虚表,对象在构造的时候才初始化虚表,其实不是初始化虚表,而是把这个类型的虚表找到,虚表的地址放在对象的头四个字节上。而是在对象初始化列表的时候挨个给vfptr。
首先我们排除虚表是存在栈上的:
其次我们再排除虚表是存在堆上的:
盲猜放在 常量区/代码段 更合理,因为 常量区/代码段 放的是全局数据和静态数据,因为函数指针数组放在静态区不正常,放在 常量区/代码段 相对来说就很合理。
我们写个程序反向验证一下:
int c = 2;
int main()
{
Base b1;
Base b2;
Base b3;
Base b4;
//打印虚表
PrintVFTable((V_FUNC*)(*((int*)&b1)));
PrintVFTable((V_FUNC*)(*((int*)&b2)));
PrintVFTable((V_FUNC*)(*((int*)&b3)));
PrintVFTable((V_FUNC*)(*((int*)&b4)));
//方向验证 -- 对比验证
int a = 0;
static int b = 1;
const char* str = "hello world";
int* p = new int[10];
printf("栈:%p\n", &a);
printf("静态区/数据段:%p\n", &b);
printf("静态区/数据段:%p\n", &c);
printf("常量区/代码段:%p\n", str);
printf("堆:%p\n", p);
cout << endl;
printf("虚表:%p\n", (*((int*)&b4)));
cout << endl;
//成员函数取地址都得这么玩
//函数编译完了是一段指令,第一句指令的地址就可以认为是函数的地址
printf("函数地址:%p\n", &Derive::Func3);
printf("函数地址:%p\n", &Derive::Func2);
printf("函数地址:%p\n", &Derive::Func1);
return 0;
}
运行结果:
结合上图几个地址来看,充分的说明了虚表是存在 常量区/代码段 中的:
多继承代码:
class Base1
{
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2
{
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
我们再来通过监视窗口看一下子类对象:
在之前多继承和菱形继承的基础上,我们再来理解这里:
问题来了,Derive中的fucn3放在哪一张虚表中呢?
我们来打印一下子类中的两个虚表:
Base2的虚表我们该如何打印呢?
- 根据Derive对象中内存布局,我们可以知道,Base1中的vfptr后面是Base1的成员变量b1,紧接着就是Base2对象中的vfptr,然后紧接着的是Base2的成员变量b2。
- 所以我们只需要跳过Base1对象中vfptr指针之后的成员,就可以找到Base2对象中的vfptr了
见去下代码:
typedef void(*VFPTR)();
//vTable是指向函数指针数组首元素的指针
void PrintVFTable(VFPTR* a)
{
printf("vfptr:%p\n", a);
for (size_t i = 0; a[i] != nullptr; i++)
{
printf("[%d]:%p->", i, a[i]);
//有函数的地址调用函数 -- 通过函数打印出结果便于观察
VFPTR f = a[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
PrintVFTable((VFPTR*)(*((int*)&d)));
PrintVFTable((VFPTR*)(*(int*)((char*)&d + sizeof(Base1))));
return 0;
}
我们直接将Derive中虚函数func1的地址打印出来:
原因:
我们通过汇编逐步探索整个过程:
Base1虚表调用Derive::func1:
Base2虚表调用Derive::func1:
我们上述过程是直接取到虚表中的内容直接通过虚表中存储的函数指针去调用(函数名就是函数地址),我们直接搞一个多态调用如下图:
经过本人验证,和上面两个图的过程一样!!
为什么在调用Base2::func1()的时候会比调用Base1::func1()的时候多跳了几层?(重点)
我们首先来看一下普通的菱形继承:
class A
{
public:
virtual void func()
{}
public:
int _a;
};
class B : public A
{
public:
virtual void func()
{}
public:
int _b;
};
class C : public A
{
public:
virtual void func()
{}
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
但是一旦加上虚继承就会编译不通过:
解决办法:
—— 在D类中重写虚函数func
那么B类和C类的虚表指针都放在哪里?
下面我们通过内存窗口来看一下(先给B类增加一个虚函数):
这里为之前讲的菱形虚拟继承对象模型中,虚基表中,第一行为0做了解释:
重写D中的func之后,就解决了之前的分不清楚的问题,但是B类指针和C类指针都是调用的D重写的虚函数…
别搞多继承,上面那么多繁琐的事情都是多继承之后可能出现的后果…