C++继承与多态
一,继承
1,继承的基本特性
- 子类继承父类所有方法和属性
- 子类是特殊的父类
- 子类对象可以当作父类对象使用
- 子类可以拥有父类没有的方法和属性
2,访问级别
- public:可以被外界访问
- private:只能在类中被访问
- protected:可以在当前类和子类中访问
限制级别:private>protected>public
3,继承方式和访问级别
父类成员在子类对外访问属性:= Max{ 继承方式, 父类成员访问级别 }
4,多继承和多重继承
多继承:
class man
{
};
class girl
{
};
class student:public man,public girl
{
};
多重继承:
class man
{
};
class girl:public man
{
};
class student:public girl
{
};
多重继承任意的父子对象,都满足赋值兼容性原则
5,赋值兼容性原则
子类对象可以当作父类对象使用
子类对象可以直接赋值给父类对象
子类对象可以直接初始化父类对象
父类指针可以指向子类对象
父类引用可以直接引用子类对象
6,继承和构造,析构
子类对象在创建时会首先调用父类的构造函数
父类构造函数执行结束后,执行子类的构造函数
当父类的构造函数有参数时,需要在子类的初始化列表中显示调用
析构函数调用的先后顺序与构造函数相反
7,继承和组合
口诀:先父母,后客人,再自己。
如果前面两个没有默认构造函数,则需要在初始化列表进行初始化
8,同名成员
当子类成员变量与父类成员变量同名时
- 子类依然从父类继承同名成员
- 在子类中通过作用域分别符::进行同名成员区分
- 同名成员存储在内存中的不同位置
- 如果不用::则子类将隐藏父类成员变量
二,函数重写
1,函数重写
- 子类定义了与父类原型相同的函数
- 函数的重写只能发生在子类与父类之间
- 父类的被重写的函数依然被子类继承了
- 默认情况下父类的被重写的函数会被子类隐藏
- 在子类中可以用作用域分辨符::调用父类被重写的函数
2,函数重写与赋值兼容性
父类指针或者引用指向子类对象时,此时调用的函数是父类的函数
原因:c++是静态编译语言。由于程序没有运行,所以不知道父类指针具体指向的是父类对象还是子类对象,从编译器的安全角度出发,编译器假设父类的指针指向的是父类对象。
3,多态
同样的调用语句有多种不同的表现形态
virtual关键字是C++支持多态的唯一方式
三,虚函数与多态
1,重载与重写的区别
重载
- 必须在同一个类中进行
- 重载由函数名,参数个数,参数类型,参数顺序决定。
- 子类无法重载父类的函数,父类同名函数(参数不同)会被隐藏
- 重载时在编译期根据参数的类型与个数决定调用函数
重写
- 必须发生在父类与子类之间
- 并且父类与之类的中的函数原型相同
- 使用virtual声明后会产生多态
- 多态是运行期间根据具体对象类型,决定调用函数。
2,虚函数的实现原理
a,什么是虚函数表
- 虚函数表是一个存储类成员函数指针的数据结构
- 虚函数表是由编译器自动生成与维护的
- virtual成员函数会被编译器放入虚函数表中
b,虚函数表(VTABLE)与虚函数指针(VPTR)
当类中申明虚函数时,类中都会生成一个虚函数表,每一个对象都有一个指向虚函数表的指针vptr。虚函数指针vptr是一个对象数据成员的第一个。
构造D的虚函数表的过程
- 拷贝基类的虚函数表
- 替换已经重写的虚函数指针
- 追加子类自己的虚函数指针
c,对象的vptr指针什么时候初始化的
- 对象在创建的时候由编译器对VPTR指针进行初始化
- 只有当对象的构造完全结束后VPTR的指向才最终确定
- 父类对象的VPTR指向父类虚函数表
- 子类对象的VPTR指向子类虚函数表
d,多继承与多态
当一个类继承多个类,且多个基类都有虚函数时,子类对象中将包含多个虚函数表的指针(即多个vptr)
D和B共用一个虚函数表,B称为主基类。把D的虚函数放入B中虚函数表中
3,抽象类和纯虚函数
- 纯虚函数是只声明函数原型,而故意不定义函数体的虚函数。
class shape//抽象类
{
public:virtual double area()=0;//纯虚函数
};
- 含有纯虚函数的类叫作抽象类
- 抽象类不能用于定义对象
- 抽象类只能用于定义指针和引用
- 抽象中的纯虚函数必须被子类重写
4,虚继承和接口类
虚继承:为了解决从不同途径继承来的同名数据成员造成的二义性问题, 可以将共同基类设置为虚基类,这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝。
class Child :virtual public Parent
接口类:成员函数全部是纯虚函数的类
5,多态和对象数组
不要将多态应用于数组
- 指针运算是通过指针的类型进行的 (编译期)
- 多态通过虚函数表实现的 (运行期)
Parent* p = NULL;
Child* c = NULL;
Child ca[3] = {Child(1, 2), Child(3, 4), Child(5, 6)};
p = ca;
c = ca;
p->f();
c->f();
p++;//这里的++是Parent对象大小
c++;//这里的++是Child对象的大小
p->f();//这里会发生段错误,此时的p指向的不是一个合法的对象
c->f();
6,一些问题总结
a,多态发生的条件
- 具有继承关系
- 父子类之间发生函数重写
- 有virtual关键字修饰
- 父类指针指向父类或者子类对象
b,不能为虚函数的情况
- 普通函数
- 静态成员函数
- 内联函数
- 构造函数
- 友员函数
c,为什么不能在构造函数里无法实现多态
先调用父类的构造函数,里面的如果有虚函数,有可能发生多态。就有可能调用子类的函数。但是此时的子类是没有构造的。成员变量没有初始化。会发生未定义行为
d,为什么不能在析构函数中里无法实现多态
先调用子类的析构函数,使数据成员被销毁,再调用父类析构函数时,里面有虚函数,就有可能再调用子类的函数,这就没有意义了。(可能编译器做优化,不会进行动态绑定)
e,是否可以将类的每个成员函数都声明为虚函数?
通过虚函数表指针VPTR调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数。而普通成员函数是在编译时就确定了调用的函数。在效率上,虚函数的效率要低很多。
f,为什么析构函数可以为虚函数
A* p=new B;A是父类,B是子类。
如果父类的析构函数是虚函数,当基类指针指向子类对象时,delete父类指针时,由于多态先会调用子类析构函数,然后再调用父类的析构函数。如果不是虚函数,只会调用父类的析构函数。子类的虚函数就没有被调用,可能会导致内存泄露
h,为什么构造函数不能为虚函数
虚函数表指针vptr是在对象完全构造后,才初始化的的,也就是指向虚函数表。在构造函数里,还没构造完成,所以无法确定vptr指针指向哪个虚函数表。