一、类的继承
1、在解决问题时,先查看当前现在的类能否解决部分问题,如果可以则把旧的继承后再拓展来缩短解决问题的时间,降低解决问题的难度,继承就是为了让代码能够重复使用。
2、当面临一个复杂问题时,可以先把问题进行分层,每层的类解决一部分问题,然后通过继承进行汇总,最终得到一个解决问题的类。
二、继承的基本语法
1、继承表
一个子类可以继承多个父类,每个类的继承方式可以不同。
class 子类:继承方式 父类1,继承方式 父类2,... //继承表
{
}
2、继承方式
public 公有继承
private 私有继承
protected 保护继承
3、继承方式的影响
父类 内部 子类 外部 友元
public yes yes yes yes
protected yes yes no yes
private yes no no yes
注意:继承方式并不影响父类中的成员在子类的访问的权限,而影响成员到子类中变成什么访问属性。
父类 public继承 protected继承 private继承
public public protecte private
protected protected protected private
private private private private
三、继承的基本特点
1、子类会继承父类的所有成员(公有、私有、保护)。
注意:子类对象完全可以当作父类对象使用,子类对象与父类对象没有本质上的区别。
2、向上和向下的类型转换
子类转换到父类:子类的指针或引用转换成父类的指针或引用,是一种缩小类型的转换,对于编译器来说是安全的(用父类指针或引用指向子类对象)。
父类转换成子类:父类的指针或引用转换成子类的指针可引用,是一种放大类型的转换,对于编译器来说是危险的(用子类指针或引用指向父类对象)。
注意:编译器仅仅是检查指针或引用的数据类型,而对实际引用的目标对象不关心(构成多态的基础)。
3、子类会隐藏父类的同名成员
1、子类依然会继承父类中的同名成员,但是会隐藏父中的同名成员,在子类中并不直接访问父中的成员。
2、可以通过域限制定符访问 obj.Base::成员名
3、可以使用父类的指针或引用来指向子类对象,编译器就会把子类对象当作父类对象来看待,访问的成员就是父类中的。
四、多重继承、钻石继承、虚继承
1、多重继承
在C++中子类可以有多个父类,按照继承表的顺序继承父类中的所有成员,并按照继承表调用父类的构造函数。
在子类中按照继承顺序排列父类,并且会标记每个父类的位置。
当父类的指针或引用指向子类对象时,编译器会自动计算出父类在子类中的位置。
2、钻石继承
假如一个子类继承了多个父类,而这多个父类有一个共同的父类,爷爷类中的成员会在孙子类中存在多份,这种继承叫钻石继承。
这种继承并不会产生错误,孙子类访问祖先的成员时必须使用 父类名::成员名,重点是这种继承方式会造成冗余浪费内存。
注意:在创建孙子类时会多次调用祖先类的构造函数,销毁函数类时也会多次调用祖先类的析构函数。
3、虚继承 virtual
在进行钻石结构继承时,如果父类继承祖先类时使用了virtual关键字,那么在孙子类中只保留一份祖先类的成员,也只会调用一次祖先类的析构、析构、拷贝函数。
注意:使用虚继承时子类中会多一个指针指向从父类中继承的成员。
五、子类的构造、析构、拷贝、赋值
1、子类在执行它的构造函数前会按照继承表的顺序先执行父类的构造函数,默认执行无参构造,如果父类没有无参构造需要在子类构造函数的初始化列表中显式调用。
2、子类的析构函数执行完成后会根据继承表地顺序逆序执行父类的析构函数,当使用父类指针或指向子类对象时,delete父类指针时只会调用父类的析构函数,而这种构造方式可能会造成内存泄漏或重复释放。
3、当使用旧的子类对象初始化新的子类对象时,会调用子类的拷贝构造函数,但在执行子类的拷贝构造前会按照继承表的顺序先执行父类的拷贝构造。
4、在执行赋值函数时,子类并不调用父类的赋值函数,如果需要可以显式调用。
六、虚函数、覆盖、多态
虚函数:类的成员函数在声明时当面加virtual关键字,这种函数就叫做虚函数。
覆盖:有虚函数的类被继承后,子类中的同函数名的函数签名与父类中的虚函数相同,则子类中的函数会覆盖父类中的同名函数。
多态:如果子类中的函数覆盖了父类中的函数,当使用父类指针指向类对象或都父类对象,通过该指针访问虚函数,它会根据实际的对象去调用函数,这就是类的多态(运行时、动态)。
七、函数覆盖和多态条件
1、函数覆盖的条件
必须是父子类之间,且父类中的函数是虚函数。
函数签名必须相同(函数名、参数列表完全相同)
返回值相同 或者 具有父子类关系的指针或引用(子类函数的返回值类型可以向向父类函数的返回做隐式转换)。
注意:访问属性不会影响覆盖,但常函数属性会影响。
2、重载、隐藏、覆盖(重写)的区别
重载:同一作用域下的同名函数,参数列表不同构造重载关系。
覆盖:符合一系列条件(父子类,虚函数,函数名、参数列表相同)。
隐藏:不同作用域下,不是覆盖关系的同名函数,就会构造隐藏关系。
3、多态
1、父子类之间有函数形成覆盖。
2、父类的指针或引用指向子类对象。
练习1:在子类的构造函数和析构函数中能否调用覆盖过的成员函数,为什么?
可以,在子类构造函数执行时函数覆盖已经完成,在析构函数执行时覆盖关系依然保持。
练习2:在父类的构造函数和析构函数中能否调用覆盖过的成员函数,为什么?
不可以,当父类构造执行时子类的函数覆盖还没有完成。
当父类析构执行时子类已经销毁,不可以。
练习3:类的构造函数和析构函数能否是虚函数,为什么?
假定构造可以是虚函数,当创建子类时子类的构造会先调用父类的构造函数,当构造函数执行时由于完成了覆盖,此时根据多态规则父类构造能名分辨出实际调用它的是子类对象,它会再调用子类的构造,这样就形成无限循环,因此构造函数不可以是虚函数。
析构函数没有问题。
注意:当子类有资源需要析构函数释放,如果使用多态机制那么父类的析构函数必须是虚函数。
八、纯虚函数和抽象类
1、纯虚函数 在声明虚函数的小括号后添加 =0; 这种函数就叫纯虚函数。
virtual 返回值 函数名(参数列表) = 0;
这种函数不需要定义,仅声明即可。
2、抽象类
成员函数中有纯虚函数的类叫抽象类,这种类不能创建对象。
抽象类必须被继承且纯虚函数被覆盖,然后由子类创建对象,如果子类没覆盖父类的纯虚函数,那么子类也将变成抽象类,不能创建对象。
3、纯抽象类
所有的成员函数都是纯虚函数的类叫纯抽象类。
这种类一般用来调用设计接口,类似C语言中函数说明的头文件。
九、虚函数表
虚函数表指针
当类中定义了虚函数,就会在类中增加一个指针,该指针指向类中虚函数表,该指针在每对象的前4个字节中。
虚函数表
虚函数表中存储着每个虚函数的地址,当子类继承父类后,也会继承父类的虚函数表指针(字节数增加4),子类的成员函数就会通过虚函数表指针对比每个虚函数,如果达到覆盖要求,就会把子类中成员函数地址替换虚函数表中的地址,该虚函数就被覆盖了。
对象多态的机制:
当通过对象访问虚函数时,通过对象中的虚函数表指针访问函数函数,如果该函数地址被替换过,那么执行自然就是替换后的函数,也就是子类中的成员函数。
练习:通过虚函数表指针,访问虚函数。
((void(*)(void))**(int**)&a)();
面向对象的四个特征:
抽象:解决问题先假想一个能够解决问题的对象,然后分析出该对象解决问题时需要的数据和功能,分析过程就是抽象。
封装:把抽象过程封装成类,然后根据需要给每个成员设置访问属性,该过程叫封装。
继承:当解决问题时先考虑现有的类能否有帮助,如果有则把旧的继承,然后再进行拓展,最终缩短解决问题的时间。
当面临一个复杂问题时,先把分析成若干层小问题,然后每一层封装一个类,再通过继承最终汇聚成一个类,以此来降低解决问题的难度。
多态:根据继承+虚函数机制,当通过指针或引用调用虚函数时,会根据对象的虚函数表指针执行相就的函数,这种执行机制就叫多态(动态多态或运行时多态)。
工厂类模式:
该模式是专门用来创建类对象,会根据传递的参数,有选择的创建对象。