目录
一、概念
二、虚表工作/运行原理
1.虚函数在一个类内存储的大小
2.对虚函数的访问(一维数组)
3.单继承
(1)虚函数继承情况
(2)单继承存储的大小
(3)基类子类调用情况
4.多继承
(1)存储的大小
(2)继承情况
(3)对虚函数的访问(二维数组)
三、多态
1.多态的条件
2.联编/绑定/捆绑
3.使用
一、概念
多态的4种状态:重载多态、包含多态、参数多态、强制多态
- 重载多态:函数重载与运算符重载
- 包含多态:含有虚函数的多态
- 参数多态:模板——类模板、函数模板
- 强制多态:强制类型转化——static_cast,cost_cast...
二、虚表工作/运行原理
1.虚函数在一个类内存储的大小
如果一个类包含了虚函数,不管有多少的虚函数,则增加列一个指针的大小,该指针vptr叫虚指针,它指向一个虚表,该虚表中存放虚函数入口地址
- 64位:8字节(指针大小)
- 32位:4字节(指针大小)
如下A类,sizeof(A)= 4,无论几个f都是4个字节
普通函数不占类的空间大小 Class{ void fun()}; sizeof(A)=1
空类大小为1 Class A{}; sizeof(A)=1
2.对虚函数的访问(一维数组)
类内有虚指针vptr,指针指向虚表vtable,虚表内存储本类中函数入口地址,存储的函数叫虚函数vfptr(f:函数)。对虚函数的范围可以通过先查找到虚表,然后在表内以数组名+下标的形式查数得到函数。
- 存储:虚指针->虚表->虚函数
- 访问:虚表->数组+下标
以如下形式进行范围:
int main()
{
A a;
typedef void(*FUN)();
FUN pf=NULL;
FUN* (*((int*)(*((int*)&a)));
pf();
FUN* (*((int*)(*((int*)&a))+1);//对下一个的范围
pf();
}
解析:
- 函数指针:FUN
- pf函数指针类型,指向返回值为void 无参的函数FUN
- a是A类型的对象
- &a:取地址
- (int*)&a 原A*类型转换为int*类型
- *((int*)&a) 解引用,a的内容,虚表的地址
- (int*)(*((int*)&a)) 强制转换类型
- *((int*)(*((int*)&a)) 取内容,虚表首地址->第一个元素的地址——函数名
- FUN* (*((int*)(*((int*)&a)) )强转为FUN类型
3.单继承
(1)虚函数继承情况
继承情况与普通函数的一致,如下代码所示,B继承A,在B内对fa重写,gb是B自己的:
B继承流程:
- 全盘接收。B把A的虚表继承过来——内有三个x虚函数,A::fa,A::fb,A::bc
- 改写。同名同参虚函数fa被重写/覆盖,把内容修改为B::fa
- 添加。在后面添加B::gb,B::hb
=》B内总共5个函数:fa,fb,fc,gb,hb
也就是说,继承含有虚表时,该继承继承该改写改写与一般无虚表的继承无差别。
(2)单继承存储的大小
无论继承了多少个虚函数,还是一个指针的大小
(3)基类子类调用情况
B b;
b.fa;
👆输出的结果是B::fa
void test(A a)//子类对象不能接收基类,基类可接收子类(基类少,子类多)
{
a.fa();
}
void main()
{
B b;
test(b);//将子类对象传给基类
}
👆此时输出结果为A::fa,因为此处没有产生多态。(多态在下面讲)
4.多继承
(1)存储的大小
存储大小为:继承的类个数*一个指针的大小
如下代码,sizeof(D)=12:3*4
class A
{
public:
virtual void fa(){cout<<A::fa"<<endl;}
virtual void ha(){cout<<A::ha"<<endl;}
}
class B
{
public:
virtual void fb(){cout<<B::fb"<<endl;}
virtual void hb(){cout<<B::hb"<<endl;}
}
class C
{
public:
virtual void fc(){cout<<C::fc"<<endl;}
virtual void hc(){cout<<C::hc"<<endl;}
}
class D
{
public:
virtual void fd(){cout<<D::fd"<<endl;}
virtual void hd(){cout<<D::hd"<<endl;}
}
单继承与多继承的字节大小?
- 单继承时,无论内部几个虚表都是一个指针的大小
- 多继承时,继承n个类,大小为n*指针的大小。有几个类就有几个虚指针
(2)继承情况
①子类的虚表跟在哪个子类的虚表后面?
- 谁先继承就先跟在哪,D先继承A,就把D的虚表挂在A虚表后面,查看继承下来A的虚表内个数:
如下图所示:
- 其中A,B,C的虚表为:
- D的虚表为:
- 三个虚指针vptr指向三个虚表(基类有几个就有几个虚表)
因此,子类内有虚函数时,会把子类新添加的虚函数挂到第一个父类的虚函数后面
- 笔试题如果问:该例虚表的运行原理?
- 可以用上面代码+画图+一些语言描述回答
②如果基类们ABC有相同的函数f,D内重写f函数,重写了哪些类的?
基类的同名同参函数都会被改写
(3)对虚函数的访问(二维数组)
int main()
{
D d;
FUN pf=NULL;
FUN* (*((int*)(*((int*)&d)));//0行0列
pf();
FUN* (*((int*)(*((int*)&a))+1);//0行1列
pf();
FUN* (*((int*)(*((int*)&a))+2);//0行2列
pf();
FUN* (*((int*)(*((int*)&a))+3);//0行3列
pf();
FUN* (*((int*)(*((int*)&a)+1));//1行0列
pf();
FUN* (*((int*)(*((int*)&a)+1)+1);//1行1列
pf();
FUN* (*((int*)(*((int*)&a)+2));//2行0列
pf();
...
}
总结:
- 对于多继承,在子类的对象中,每个父类都有自己的虚表,将最终子类的虚函数放在第一个父类的虚表中,使得不同父类类型指针的指向清晰。
- 如果在子类中重写了父类们中的同名同参虚函数,那么虚表中同样修改。
- 单继承时,无论内部几个虚表都是一个指针的大小
- 多继承时,继承n个类,大小为n*指针的大小。有几个类就有几个虚指针
三、多态
1.多态的条件
- 覆盖/重写(两个类之间必须是父子关系、最少两个类)
- 同名同参虚函数
- 基类指针或者引用指向基类对象或者派生类对象
2.联编/绑定/捆绑
定义:将函数名和函数调用联系的过程
联编种类:
早捆绑、晚捆绑
早期联编、晚期联编
早——编译时(编译:检查语法错误)
晚——运行时(运行:检查逻辑错误)
3.使用
如下代码中 编译时输出:A::fn,B::fn
如下代码输出为: A::fn A::fn
因为参数类型A类,aa:A类,因此用aa调用fn就是A类内fn
这是在编译时确定了调用类型,而不是通过参数传递,也就是说这种情况下没有联编上。那么如何联编上?
不能通过值传递--拷贝构造复制,没有传递b参数本身,需要修改为引用与指针
①修改为引用:
②指针
这种不同对象调用不同函数的现象,就叫多态
如果不是虚函数,就没有多态,结果都是A::fn。B内有2个fn,调用的是隐藏的。
为什么呢?
因为只有是虚函数才查虚表,不是的话就直接调基类内的函数
在调用时,被覆盖就调用子类的,没覆盖就基类的