继承多态
继承的本质:代码的复用。
类和类之间的三种关系:
- 代理:被代理类的接口的功能子集
- 组合:a part of/has a,一个类是一个类的一部分
- 继承:is a/a kind of,一个类是另一个类的一种
class Base//基类
{
public:
int ma;
protected:
int mb;
private:
int mc;
};
class Derive:public Base//派生类
{
public:
int md;
protected:
int me;
private:
int mf;
};
派生类的内存布局:
派生类继承了基类的:
- 所有成员变量,私有的成员也会继承,但是无法访问。
- 除基类的构造函数和析构函数之外的所有成员方法
- 由于成员方法不占用对象的内存,因此派生类的内存布局如下:
继承的访问限定:
派生类对象的构造方式:
- 先调用基类的构造函数构造从基类继承的数据再构造派生类,基类的构造必须写在初始化列表中。
- 初始化列表中的代码先于构造函数执行。
- 初始化列表中不会定义构造顺序。
有如图一段代码时
#include<iostream>
using namespace std;
class Base
{
public:
Base(int a):ma(a){cout<<"Base(int a)"<<endl;}
~Base(){cout<<"~Base()"<<endl;}
protected:
int ma;
};
class Derive:public Base
{
public:
Derive(int data):Base(data),mb(data){cout<<"Derive(int data)"<<endl;}
~Derive(){cout<<"~Derive()"<<endl;}
private:
int mb;
};
int main()
{
Derive(10);
return 0;
}
我的运行结果如下:
基类和派生类之间同名成员方法之间的关系:
- 重载:函数名相同,参数列表不同,作用域相同的函数构成重载,基类和派生类的方法不能构成重载,作用域不同。
- 隐藏:如果派生类和基类存在同名的方法,我们就说派生类的方法把基类的方法隐藏了。发生于派生类和基类之间,一个类内不存在隐藏关系。
- 覆盖:派生类和基类有同名同参数列表同返回值的方法,并且该方法在基类中被定义为虚方法时,派生类中的方法会将基类中的方法覆盖。
如图,用红色线划起来的两个show()方法构成重载,而用黑色线划起来的show()方法构成隐藏(派生类和基类之间)。
那如果我们需要调用基类中被隐藏的show()方法时,我们需要在调用时加上该方法的作用域。
int mian()
{
Derive d(10);
d.show();
d.Base::show();
return 0;
}
如图的运行结果如下:
注意:基于上面的类代码,如果我们在主函数中使用基类对象调用一个带参数的show()方法是不能成功的。因为我们派生类中有一个无参的show()方法,将基类中的show()方法都隐藏起来了,而我们使用派生类的调用有参show()方法时,编译器会发现派生类中只有一个无参的show()方法,因此会报错。在调用时加上基类的作用域就会正确执行,因为编译器会明白要调用的是从基类继承而来的有参show()方法。
C++支持的四种类型强转
- Const_cast:去掉变量的const属性
- Static_cast:编译器认为可以支持的强转,安全性略高
- Reinterpret_cast:类似c语言的无条件强制转换
- Dynamic_cast:RTTI强制转换(run time type information)
- 调用方式:类似于类模板的调用方式。
基类和派生类之间的相互赋值
- 基类对象赋给派生类对象:不可以,派生类对象中派生类那部分空间无法赋值。
- 派生类对象赋给基类对象:可以,只能赋给从基类中继承来的成员。派生类自己的成员无法使用。
- 基类指针指向派生类对象:可以,但是指针解引用只能访问基类的成员。
- 派生类指针指向基类对象:不可以。
基于上面的类代码,我们将main函数改写成下面这样
int main()
{
Derive der(10);
Base *p = &der;
p->show();
cout<<sizeof(Base)<<endl;
cout<<sizeof(sizeof(Derive))<<endl;
cout<<typeid(p).name()<<endl;
cout<<typeid(*p).name()<<endl;
return 0;
}
运行结果如图:
我们可以看到
- 当我们使用基类指针指向派生类对象再调用基类和派生类中都有的方法时,默认调用的是基类的方法。这是因为我们指针p的类型在编译时已经确定为基类的指针,当它去调用show方法时,直接去基类的作用域中调用show方法。
- 基类的大小为4字节,派生类的大小为8字节,
- 指针p是Base*类型的指针,*P的类型是Base。
可是当我们将基类中的show方法改成一个加了virtual关键字的虚函数后,会发生怎样的变化呢??
运行结果如图:
- 调用了派生类的show方法
- 基类大小变成8个字节,派生类大小变成12个字节
- 指针p是Base*类型,*p的类型却是Derive类型
- 当我们基类中存在虚函数时,基类和派生类的大小都会增加4个字节。
这是为什么?
原来,当我们类中存在虚函数时,我们的对象内存布局中就会多出一个名为vfptr的指针,在32位系统上指针的大小为4字节。该指针指向一个名为vftable的结构,被叫做虚表,虚标里面存储着虚函数的地址。
注意:
- 一个类只有一个虚表,在编译期间产生。
- 虚表存放在只读数据段,可见范围为整个程序的运行过程。
- 每个对象都会有一个vfptr指针。
- Vfptr指针的排列优先级非常高,通常都排在对象的前四个字节。
- 虚表中有一个RTTI指针,保存着运行时的类型信息,我们打印*p的类型时,编译器无法确定,只能去虚函数表中访问RTTI,下面的0是代表vfptr在对象内存中相对于首地址的偏移量。
当我们派生类继承含有虚函数的基类时,也会继承该类的vfptr指针,将自己的vfptr和基类的vfptr合并,因此,派生类中也只含有一个vfptr(什么类型的对象就指向什么类型的表)。如下图:
我们可以看到在派生类的虚表中有两个虚方法,一个是基类的有参show方法,另一个是派生类无参的show方法。而基类中的和派生类中同名同返回值同参数列表的无参show方法却不在虚表中,这时因为派生类的同名同参数列表同返回值的show方法将基类中的show方法覆盖。而当我们用指针p访问show方法时,我们发现该方法是一个虚方法,因此首先去虚表中查找该方法,而在虚表中基类的无参show方法已经被派生类同名同参数同返回值的虚方法覆盖了,因此我们只能调用派生类的虚方法。这也就是我们所说的动多态,在运行时才能确定的多态。
注意:覆盖指的是覆盖虚函数表中的show。
由以上特性我们可知,成为虚函数要满足以下几点:
- 能取地址,还得现有对象
- 构造函数不能被定义为虚函数,没有完整的对象
- 析构函数可以被定义成虚函数
- Inline函数不能被定义成虚函数,因为他不能取地址
- Static成员函数不能成为虚函数,它的调用不依赖对象
什么情况下产生多态的编译/调用:
测试用例:
- 对象本身调用:在编译器确定,不会产生多态
- 基类指针指向基类对象调用虚函数:会产生多态
- 基类指针指向派生类对象调用虚函数:会产生多态
- 基类引用引用派生类对象:会产生多态
- 积累应用引用基类对象:会产生多态
- 派生类指针指向派生类对象:会产生多态
- 派生类引用引用派生类对象:会产生多态
结论:
- 指针或者引用调用
- 调用的对象是虚方法
纯虚函数和抽象类
- 纯虚函数:只有定义,没有实现并且在函数名括号后面加上‘=0’表示该函数是一个纯虚函数。
- 抽象类:含有纯虚函数的类就是抽象类。专门为了继承而写,将其作为基类。
- 特点:
- 抽象类不能实例化对象,但可以定义指针,可以定义引用。
- 当我们继承抽象类的成员方法时,必须在派生类中实现抽象方法,不然派生类也就成了抽象类。