GeekBand-secondweek-c++的多态和虚函数
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了GeekBand-secondweek-c++的多态和虚函数相关的知识,希望对你有一定的参考价值。
多态与虚函数
13章的简单继承只是实现了对已有对象的实现的重定义和直接调用,但是向上映射导致的对象切割仍然是个缺陷;
1、延续13章的向上映射
简单继承中,派生类重定义了基类的成员函数,此时,向上映射的结果是很明显的,它使用了基类实现的函数版本,这显然并不是我们想要的效果;为什么会有这样的结果发生,我们先探讨几个问题:
函数调用绑定:函数调用确定目标函数体称为捆绑,编译期绑定称为早绑定,上面的问题就是早绑定引起的,因为编译器只知道基类对象的类型,调用函数也会绑定基类实现的函数版本,要解决这一问题,就要引入晚绑定,即运行时绑定或动态绑定,当一种语言要实现动态绑定时,必须有一种机制能保证运行时可以确定对象的类型和并找到合适的调用函数,当然这由编译器增加部分代码做这样的判断和查找;
2、虚函数
虚函数要求基类声明这个函数时用上virtual关键字,晚绑定只对virtual起作用,虚函数不可能内联,必须有地址;基类的成员函数声明为virtual,任何派生类函数重写时都不要重复加上virtual,虽然这么做没什么错误,这称为越位?
带有虚函数的类对象,不仅包含着成员变量,此对象还维护着一个虚函数表指针,指针指向着当前对象所属的类所实现版本的虚函数表;
扩展性:通过在基类中定义为成员函数为virtual,不用改变以基类对象为参数的全局函数就可以在系统中随意增加新功能。在一个设计好的oop程序中,大多数或所有的函数都沿用这种以基类对象为参数、传递派生类对象的模型,它只与基类接口通信,却使用着派生类的成员变量和自己实现的虚函数版本。这样的程序是可扩展的,因为可以通过从公共基类继承新数据类型而增加新功能,操作基类接口的函数完全不需要改变就可以使用这些新类。
3、晚绑定的实现
3.1 可以看到虚函数确实实现了晚绑定,实际上编译器从virtual知道我们希望晚绑定,当然这样的函数就不能当成普通的函数来处理,编译器对每个包含虚函数的类创建一个虚函数表(Vtable),然后在对象实例中放置放入一个指针,保存该类的虚函数表地址,称为Vpointer,并指向Vtable,当通过基类对象调用虚函数时,实际动态传入的对象的指针Vpointer会找到应该指向的虚函数表,然后调用目标版本的虚函数,从而实现晚绑定;
3.2 看到这个虚函数表指针
通过sizeof可以看到,有虚函数的类,类大小要多出一个void指针的大小,它就是存放的Vpointer,指向一个虚函数表
引入“哑”成员:一个类内部什么成员变量都没有时,编译器会放一个哑成员占位,保证类的大小不能是0,Vpointer也是一个成员,所以有虚函数的类不需要哑成员;
3.3 要明确向上映射仅处理地址,即地址意味着信息不完全,编译器不能擅自绑定哪一个函数,如果编译器明确知道对象的类型,像值传递,那么对任何函数的调用都不会用晚绑定,
3.4 晚绑定牺牲了效率和代码量,这也是为什么c++不默认全是虚函数而是让用户有选择地使用;
4、抽象基类和纯虚函数
含有虚函数的基类称为抽象基类,有纯虚函数的基类称为纯抽象基类;纯虚函数在基类虚函数表里只占位,没有任何指向,包含纯虚函数的类不能创建对象;
纯虚函数非常有用,既然不能创建对象,也就不能对基类对象采用值传递,这样保证了向上迎射期间只能使用指针或引用。纯虚函数仍然可以在基类中定义,但即使这样做也不影响它占位虚函数表而无指向的特性,也仍不可创建基类对象;
5、向下映射
编译器不允许声明为基类的引用和指针调用派生类特有的成员函数,除非显性地映射,强制转换类型,否则编译器认为是不安全的行为,编译器无法知道这个特有函数的编译信息;向上映射是默认的安全的,向下映射必须显性映射;
将在18章讨论c++提供运行时类型信息的方法;
对象切割:传地址或引用意味着传递的内容(就是对象地址)不论基类对象还是派生类对象,长度是相同的;而对于传值,编译器会限定压栈出栈的都是一个基类对象大小内存,编译器接受一个派生类对象,但是只拷贝基类拥有的部分,而切掉派生部分,编译器保证这个对象正安全地以值传递,因为这时编译器认为它知道这个对象的确切的类型(这个对象的额外特征有用的任何信息都已经失去)。另外,用值传递时,它对基类对象使用拷贝构造函数,该构造函数初始化Vptr指向基类Vtable,并且只拷贝这个对象的基类部分。这里没有显式的拷贝构造函数,所以编译器自动地为我们合成一个。由于上述诸原因,这个对象在切片期间变成了一个base对象;
避免切片发生的有效方式就是基类声明纯虚函数;
6、虚函数和构造函数
创建派生类对象,编译器在构造函数进入函数体前加入了初始化Vptr和构造基类的部分,默认情况下,调用无参的基类构造函数,如没有在初始化列表显性使用基类其它版本构造函数并且基类也没有一个默认构造函数,就会编译报错;
注意:不能在初始化列表中初始化基类的成员变量,此时基类还没构造完成;
构造函数体内如果使用了虚函数,那么被调用的只是这个函数的本地版本,虚机制在构造函数中并不起作用;
为了解决析构时对派生类对象调用了基类析构函数,必须对析构函数声明虚函数,保证析构函数也加入到虚函数表并正确被调用,同样的虚机制在析构函数中也不起作用;
关于构造函数和析构函数屏蔽虚机制的原因见20页的分析
以上是关于GeekBand-secondweek-c++的多态和虚函数的主要内容,如果未能解决你的问题,请参考以下文章
GeekBand-secondweek-c++的多态和虚函数
javascript 一个后台UI的多片实现(模仿浏览器的多标签)
Delphi的基于接口(IInterface)的多播监听器模式(观察者模式 ),利用RTTI实现Delphi的多播事件代理研究