程序员成长之旅——继承和多态
C++的三大特性有: 封装、继承、多态。
今天我们来探索一下继承和多态。
继承和多态
先简单理解一下继承和多态:继承相当于子类继承了父类的数据和方法,子类父类我们也称为派生类和基类,继承一般我们在子类中添加的是父类没有的成员。而多态是建立在继承之上的,它使用了C++编译器最核心的技术,即动态绑定技术。其核心思想是父类对象调用子类对象的方法。
接下来我们通过一些问题加深这块的理解:
C++类继承的三种关系
C++中继承主要有三种关系:public、protected、private。
public继承子类也是public,可以替代父类完成父类接口所声明的行为;
protected继承子类也是protected,子类不能自动转换成为父类的接口。从语法来说,子类还是可以调用父类的protected成员,也就是只能在内部访问,不能在外部访问;
private继承子类是private,虽然子类还可以调用父类中的public和private成员,但是子类的子类就不可以调用了。
私有继承和组合有什么相同点和不同点
相同点:都可以表示“有一个”关系
不用点:私有继承中派生类能访问基类的protected成员,并且可以重写基类的虚函数,甚至当基类是抽象类的情况。组合不具有这些功能。
什么是多态
多态性的定义:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。有两种类型的多态性:
(1)编译时的多态性。编译时的多态性是通过重载来实现的。对于非虚的成员来说,系统在编译的时候,根据传递的参数,返回的类型等信息决定实现何种操作。
(2)运行时的多态性。运行时的多态性就是指直到系统运行时,才根据实际情况决定何种操作。C++中,运行时的多态性是通过虚成员实现。
虚函数是怎么实现的
简单的来说的话,虚函数是通过虚函数表来实现的。
事实上,如果一个类中含有虚函数,则系统会为这个类分配一个指针成员指向一张虚函数表,表中每一项指向一个虚函数的地址,实现上就是一个函数指针的数组。
构造函数调用虚函数
在构造函数中,虚拟机制不会发生作用,因为基类的构造函数在派生类构造函数之前执行,当基类构造函数运行时,派生类数据成员还没有被初始化。
为什么需要多重继承,优缺点是什么
实际生活中,一些事物往往会拥有两个或两个以上事物的属性,为了解决这个问题,C++引入了多重继承的概念。
优点:对象可以调用多个基类中的接口
缺点:容易出现继承向上的二义性
多重继承二义性的消除
(1)加上全局符确定调用那一份的拷贝。
(2)使用虚拟继承就可以。
多重继承和虚拟继承
(1)任何虚拟基类的构造函数按照它们被继承的顺序构造
(2)任何非虚拟基类的构造函数按照它们被构造的顺序构造
(3)任何成员对象的构造按照它们声明的顺序调用
(4)类自身的构造函数
继承和组合的区别?什么时候用继承?什么时候用组合
(1)public继承是一个is_a的关系,也就是每个派生类对象都是一个基类对象
(2)组合是一个has_a的关系,假设B组合了A,每个B对象中都有一个A对象
(3)继承一定程度会破坏基类的封装,依赖关系强,耦合度高
(4)组合是继承之外另一种复用选择,它的依赖关系不强,耦合度低。
(5)实际尽量去用组合。它的维护性高,但是要呈现多态的话就必须用继承。类之间的关系的换一般用组合就好。
(6)私有继承中派生类能够访问基类的protected成员,并且可以重写基类的虚函数,甚至当基类是抽象类的情况。组合不具有这些功能。
为什么要引入抽象基类和纯虚函数
(1)为了方便使用多态特性
(2)在很多情况下,基类本身生成对象时不合情理的。例如:动物可以作为一个基类派生出老虎、狮子等子类,但动物本身生成对象明显不合常理。抽象基类不能够实例化,它定义的纯虚函数相当于接口,能把派生类的共同行为提取出来。
虚函数和纯虚函数有什么区别
(1)类中如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,这样编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
(2)虚函数在子类里面也可以不重写,但是纯虚函数必须在子类中实现,它就是一个接口。
(3)虚函数的类用于“实作继承”,也就是继承接口的同时也继承了父类的实现。当然,大家也可以完成自己的实现。纯虚函数的类用于“介面继承”,即纯虚函数关注的是接口的统一性,实现由子类完成。
(4)但纯虚函数的类叫做虚基类,这种基类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。这样的类也叫做抽象类。
虚函数存在哪?虚表存在哪?
虚函数和普通函数一样,都是存在代码段的,只是它的指针存在了虚表中。另外对象存的不是虚表,而是虚表指针。验证VS下存的也是代码段的。
inline函数可以是虚函数吗?
不能,因为inline函数没有地址,无法将地址放到虚函数表中。
静态成员可以是虚函数吗?
1.static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。
2.静态与非静态成员函数之间有一个主要的区别。那就是静态成员函数没有this指针。
虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable.
对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual.
虚函数的调用关系:this -> vptr -> vtable ->virtual function
构造函数可以是虚函数吗?
不能,因为对象的虚函数指针是在构造函数初始化列表阶段才初始化的。
析构函数可以是虚函数吗?
可以,并且最好把析构函数定义成虚函数。原因:将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
对象访问普通函数快还是虚函数快
首先如果是普通对象,是一样快的。如果是指针对象或者是
引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
虚函数表是什么阶段生成的,一般存在哪
虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
什么是DLL HELL
DLL HELL 主要是指DLL(动态链接库)版本冲突的问题。一般情况下,DLL新版本会覆盖旧版本,那么原来的旧版本的DLL的应用程序就不能继续正常工作了。
虚函数表
大家都知道,虚函数是通过一张虚函数表来实现的。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,其内容真实反映了实际的函数。这样,在有虚函数的类的实例化中,这个表被分配在了这个实例的内存中,所以当用父类的指针来操作一个子类的时候,这个虚函数表就非常重要了,它就像一个地图一样,指明了实际所应该调用的函数。
C++的标准规格说明书中说到,编译器必须保证虚函数表的指针存在于对象实例中最前面的位置(这个是为了保证正确取到虚函数的偏移量)。这意味着通过实例地址得到这张虚函数表,然后就可以遍历其中的函数指针,并调用相应的函数。
举个例子大家就懂了
#include<iostream>
using namespace std;
class Base
{
public:
virtual void fun1() {cout << "Base::fun1" << endl;}
virtual void fun2() {cout << "Base::fun2" << endl;}
virtual void fun3() {cout << "Base::fun3" << endl;}
private:
int num1;
int num2;
};
typedef void (*fun)(void);
int main()
{
Base b;
Fun pFun;
pFun = (Fun)*((int*)*(int*)(&b)+0);
pFun();
pFun = (Fun)*((int*)*(int*)(&b)+1);
pFun();
pFun = (Fun)*((int*)*(int*)(&b)+2);
pFun();
}
执行结果是:
Base::fun1
Base::fun2
Base::fun3
一个类中会有多少张虚函数表呢?
对于一个单继承的类,如果它有虚拟函数,则只有一张虚函数表。对于多重继承的类,它可能有多张虚函数表。