从C开始的CPP学习生活04-多态与虚函数
多态是一种泛型编程思想,虚函数是实现这个思想的语法基础,即父类的指针调用子类的函数
1、虚函数
1 |
|
这段代码运行结果是打印class CSon
这就是虚函数的作用,可以让父类指针调用子类的函数,原因就是父类函数声明是virtual,而子类对这一函数实现了重写
- 形式: virtual void fun()
- 子类的函数要和父类函数名称相同
- 多个子类,换子类就调用相应子类的函数 CFather* fa = new CSon;如果把CSon更换为另一个子类,另一个子类也重写了show函数,则会调用另一个子类的show函数
- 多态指针对指针对象
- 重写针对虚函数,覆盖针对普通函数
2、虚函数的特点
- 子类重写的函数,默认是虚函数,可以显示的加virtual,也可以不加
- 返回值和函数体的内容必须完全相同才能构成重载
- 但是存在一种特殊情况,即子类的函数是父类函数的协变。
1 |
|
- 虚函数不能是内联函数,但并不是编译出错,而是编译过后内联无效
- 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联
- 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时不可以内联
- 只有在编译器知道所调用的对象时哪个类的时候才能进行内联
- 构造函数不能是虚函数,内联是构造函数唯一的合法存储类
3、虚表
多态与虚函数的实现原理
每个包含了虚函数的类都包含一个虚表
当一个类A继承另一个类B时,A同样会继承类B的虚表,但不是使用类B的虚表,而是复制了一份;
若A未重写B的虚函数,则A类的对象调用该函数时访问的是继承自B的虚表,所以调用的是B的函数,而当A重写了B的虚函数之后,A的虚表内容被替换成A所重写的函数地址,所以A类对象调用该函数的时候自然使用的是A类的函数;
3.1、虚表指针
虚表是一个指针数组,其元素是虚函数的函数指针,也就是存储着虚函数的地址;
普通的函数不是虚函数,调用的时候不需要经过虚表;
虚表在程序编译阶段就可以构造出来;
当然,虚表不是只能有一个,一个类如果继承了多个存在虚函数的类,则会存在多个虚表,每个虚表对应一个父类;
为了指定虚表的对象,编译器会给类添加一个*__vptr
指针,用来指向虚表;
3.2、虚表存放位置
c/c++程序所占用内存一共分为五种
栈区、堆区、程序代码区、全局数据区、文字常量区
虚表存储在全局数据区
3.3、upcasting
1 |
|
CSon类继承自CFather,上面这段代码地址空间是CSon类型的,我们使用父类指针指向这段地址,则父类指针会用父类指针的解释方式看待这段内存。但同时,父类指针获得的v__ptr
是指向子类CSon的虚表的,所以我们在调用虚函数的时候,会调用子类的虚函数
这里面涉及upcasting的问题,即把子类对象当作父类来看待
3.4、找到虚表的地址
虚表指针在对象内存的首4个字节,虚表里面每4个字节存储一个函数指针,最后4字节存储0x0
1 |
|
上述代码分别打印 fun class CSon 以及 0
4、虚析构
正常情况下,使用父类的指针指向子类的对象,在进行析构时调用的是父类的析构函数
如果父类的析构函数的虚析构,则子类的函数默认也是虚析构
在进行析构时会先调用子类的析构函数,然后调用父类的析构函数
delete指向哪种类型的指针,就调用哪种类型的析构函数。
5、纯虚函数
形式:
1 |
|
特点:
- 没有函数实现
- 无法进行对象声明,即有纯虚函数的类不能实例化对象
- 相实现对象需要使用子类继承该类,并且子类要实现纯虚函数
抽象类:有纯虚函数的类
接口类:只有纯虚函数的类,可以有成员,可以有构造函数
1 |
|
6、虚继承
有这样一个问题 类B继承类A,类C继承类A,然后类D继承类A和类B,这会发生什么问题?
1 |
|
此时编译会出现问题:"D::a" is ambiguous
ambiguous access of 'a'
注意,使用命名空间的访问方法时,只能使用d.B::a以及d.C::a,无法使用d.A::a
解决此类问题的方法是使用虚继承
1 |
|
解决了多继承中访问不明确的问题
不建议用,结构复杂,内存开销比较大
7、联编
将模块或者函数合并在一起生成可执行代码的处理过程
按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编
对于非虚的方法,编译器使用静态联编
对于含有虚函数的类,由于需要到运行阶段才能知道具体要调用哪个函数,所以编译器要使用动态联编
8、单例模式
思想:一个类只能创造一个对象
- 构造函数:private/protected
- 通过静态成员函数申请对象空间,并返回地址
- 定义一个静态标记,记录对象个数,并控制
- 析构函数,将标记清空,以达到重复申请对象的目的
- 该类可以被继承,但是继承后的类无法实例化对象(因为实例化对象需要调用父类的构造函数)
1 |
|