封装->继承->多态
- 普通虚函数 check
- 虚析构函数 check
- 纯虚函数
- 抽象类
- 接口类
- RTTI
- 异常处理
- 隐藏 覆盖 check
- 早绑定 晚绑定 check
- 虚函数表 check
虚函数实现原理
虚函数实现原理是
函数指针:
- 对象指针—>指针指向对象
- 函数指针—>指针指向函数
函数存在内存中,可以通过指针指向这段代码的开头,那么函数就会从头一直向下执行。如图:
比如可以通过Func1_ptr
得到fun3()
函数入口,并开始执行。函数指针与普通指针一样,存储着内存的地址,这个地址就是函数的首地址。
虚函数实现原理是虚函数表指针。假如有两个类Shape
和Circle
类:
父类Shape
:
1 | class Shape{ |
此时,当实例化一个Shape
对象时,此Shape
对象中包含如下左侧内容:
此Shape对象
中,除了m_iEdge
外,还有一个成员vftable_ptr,称为虚函数表指针。
- 这个指针指向一个虚函数表,此虚函数表与Shape类的定义同时出现。
- 此表占空间,从起始位置
0xCCFF
处开始,也就是vftable_ptr
这里存储内容为0xCCFF
。 - 此表只有一个,通过Shape实例化出的所有对象都指向同一个虚函数表,即所有通过
Shape
实例化的对象,其vftable_ptr
都存储0xCCFF
。也就是说每一个实例的vftable_ptr
都指向Shape类的虚函数表。言外之意,虚函数表属于类,而非类对象。
在父类Shape
的虚函数表中一定定义了一个函数指针:calcArea_ptr
,它是calcArea
函数入口地址,即calcArea_ptr
中存储0x3355
(0x3355
是Shape类
中calcArea
函数地址)。调用calcArea
时,先通过vftable_ptr
找到虚函数表,再通过位置偏移找到虚函数的入口地址,从而最终找到calcArea
计算面积。
上述过程实例化Shape父类对象。
当定义了子类Circle:
1 | class Circle : public Shape{ // 共有继承Shape |
注意Circle中并没有calcArea()
函数,也就说,Circle使用Shape类的calcArea
计算面积。
当实例化Circle
子类对象后。此Circle
对象中存储的内容如下:
因为Circle
中没有定义虚函数,但它从父类中继承了虚函数calcArea
,所以在实例化一个Circle
对象时,也会产生一个虚函数表(这个虚拟性是从父类继承过来的)。注意此虚函数表是Circle类自己的虚函数表,起始地址为0x6688
,而Shape类的虚函数表起始地址是0xCCFF
。但是Circle
虚函数表中计算面积的指针calcArea_ptr
是一样的,都存储0x3355
(因为是继承过来的)。
这就能够保证在Circle
中访问父类Shape的calcArea
时,也能够通过虚函数表指针找到自己的虚函数表,从而找到父类Shape的calcArea
。
如果子类Circle定义中定义了自己的calcArea
函数,即子类的calcArea
有自己的函数地址0x4B2C
:
1 | class Circle : public Shape{ // 共有继承Shape |
当实例化一个Circle类对象时,Shape类没有任何变化(当然,父类不会因为子类的变化而改变呀!),但是Circle会有变化:如图:
Circle
虚函数表是一样的(即表地址是一样的),但是因为Circle
自己定义了自己的calcArea
方法,所以calcArea_ptr
所指向的也是Circle自己的calcArea
地址0x4B2C
,换句话说,原先calcArea_ptr
中父类的calcArea
地址被Circle自己的calcArea
地址覆盖。所以,此时如果使用Shape的指针指向Circle的对象,执行子类的虚函数calcArea
。这便是virtual的功能
上述过程就是多态原理。
函数的覆盖与隐藏
覆盖即上述过程:
当Circle没有自己的
calcArea()
时,Circle的虚函数表中calcArea_ptr
存的是0x3355
,即父类calcArea()
的地址。当Circle有自己的
calcArea()
时,Circle的虚函数表中calcArea_ptr
存的是0x4B2C
,即子类自己的calcArea()
的地址。
此时虚函数表中,子类的虚函数地址覆盖父类虚函数地址。隐藏与多态无关:
与多态(virtual)无关,即当父类子类中没有
virtual
虚函数时,也就是说这个类我不希望他有多塔的性质,父类子类出现了同名函数,且父类指针分别指向子类对象,父类同名函数隐藏了子类同名函数。(毕竟这个子类对象类型是父类,回顾继承篇)
敲黑板首先判断需不需要这个父类有多态的性质,如果需要,这个父类中一定要有virtual函数,才会有虚函数表。虚函数表属于类,而非对象。
虚析构函数原理
回顾上一篇笔记,当父类中析构函数都为virtual析构函数时,通过父类指针指向子类对象,最后通过delete
接父类指针就可以,先执行子类析构函数紧接着执行父类析构函数。这个过程与虚函数表有关。
强调一下前提:先执行子类析构函数紧接着执行父类析构函数(也就是说通过父类指针执行到了子类的析构函数,这就是为什么先执行子类析构函数)
在父类Shape中加上虚虚析构函数:
1 | class Shape{ |
定义子类,写上子类虚析构函数:
1 | class Circle : public Shape{ // |
此时用父类指针指向子类对象,且delete接父类指针:
1 | int mian(){ |
虚函数表如何工作:
当在Shape中定义了虚析构函数,Shape类的虚函数表中就会有一个Shape类的虚函数指针~Shape_ptr
(指向父类析构函数),
同时在Circle类中也会产生一个子类析构函数的函数指针~Circle_ptr
(指向子类析构函数)。注意上述两个析构函数指针同时出现。
如果使用Shape的指针指向Circle的对象,执行子类的虚函数calcArea。当delete接Shape指针时,通过Shape找到Circle类的vftable_ptr(Shape类指针指向Circle类对象),进而找到虚函数表,最后找到Circle类的析构函数,从而使得Circle类析构函数得意执行。最后执行Shape类析构函数。
{有疑惑,父类指针指向子类对象,会执行子类虚函数?(delete父类的指针时,程序会去找父类的指针指向的地址,该地址就是子类头部虚函数表指针的地址,进而找到子类虚函数表,最后执行子类析构函数) delete时,发生了什么?回顾继承篇}
现实中的虚函数
明确概念:
- 对象的大小:实例化对象中数据成员所占内存大小,(不包括成员函数)。
- 对象的地址:通过类实例化一个对象,这个对象的所占的内存单元的首地址。
- 对象成员的地址:一个对象中每一个成员所占据的地址。因为每个成员的数据类型不同,所以占用不同大小的内存。
- 虚函数表指针:当实例化一个对象后,这个对象的第一个内存中所存储的指针,这个指针就是虚函数表的指针。就是上述所有的
vftable_ptr
。可以根据这个特点,通过计算对象的大小来证明虚函数表示的存在。
当没有virtual时
假如有两个类:
父类Shape:
1 | class Shape{ |
子类Circle:
1 | class Circle : public Shape{ |
执行1:
1 | Shape shape; // 实例化一个对象 |
结果1:
1 | 1 // 对于一个数据成员都没有的类对象,c++ 用一个内存单元来标记它。 |
执行2:
1 | Circle circle(100); |
结果2:
1 | 4 // int 型数据占4个内存单元 |
当有virtual时
父类与子类:
1 | class Shape{ |
执行:
1 | Shape shape; |
返回:
1 | 8 |
shape对象大小是8
,Shape类对象地址中第一个内容是虚函数表地址4198160
;
Circle类对象地址中的第一个内容是虚函数表地址4198120
,之后移动指针2次,便是存储数据成员100的位置。