继承的概念:
继承机制:可以利用已有的数据类型来定义新的数据类型,所定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。
OOP强调软件的可重用性(software reuseablility).C++提供类的继承机制,解决了软件中代码重用的问题:代码复用
继承方式
继承方式 | 基类的访问限定 | 派生类的访问限定 | 外部的访问限定 |
Public: |
|
|
|
| public | public | Yes |
| protected | protected | No |
| Private | 不可见 | No |
Protect: |
|
|
|
| public | protected | No |
| protected | protected | No |
| Private: | 不可见 | No |
Private: |
|
|
|
| public | Private: | No |
| protected | Private: | No |
| Private: | 不可见 | No |
派生类对象的构造过程是什么?
在派生类构造函数的初始化列表中,指定基类的构造方式(否则找默认构造函数构造)
构造:基类部分成员=====》派生类的部分
析构:派生类的成员=====》基类的成员析构
1.基类对象首先被创建
2.派生类构造函数应通过成员初始化列表将基类构造信息传递给基类构造函数构造
派生类如何处理和基类同名的成员?
派生类对象访问的时候,直接访问的是派生类自己的;
访问基类的同名成员,需要加上基类的作用域即可。派生类和基类同名的函数中,会出现三种不同的情况,覆盖,重载,隐藏。
单继承:
单继承是一般的单一继承,一个子类只有一个直接父类时称这个继承关系为单继承,这种关系比较简单是一对一的关系。
B内存中包含A类数据成员:
多继承:
一个子类有两个或以上直接父类时称这个继承关系为多继承。
这种继承方式使一个子类可以继承多个父类的特性。
多继承可以看作是单继承的扩展,因为派生类具有多个基类,派生类与每个基类之间的关系仍可看作是一个单继承。
多继承下派生类的构造函数与单继承下派生类构造函数相似,它必须同时负责该派生类所有基类构造函数的调用。
C内存中同时包含B和A
同时,派生类的参数个数必须包含完成所有基类初始化所需的参数个数。
多继承派生类的内存模型与继承列表类的顺序是有关的
菱形继承:
VS上对以上的内存模型进行实际测试:这里的每个类只有一个整形变量
解决菱形继承中出现的数据冗余和数据的二义性问题
根本原因:
B类和C类都同时继承了A类,所以在B类和C类中都含有A类,造成了数据的冗余和访问时的数据二义性
有两种解决方法:
一、 访问时标识作用域:使用A数据成员前应该标识是C::中的A还是B::中的A。
二、 用到一种新的继承方法:虚继承--解决菱形继承的二义性和数据冗余的问题
虚继承的提出就是为了解决多重继承时会保存两份间接基类数据的问题,也就是说虚继承机制就只保留了一份副本,但是这个副本是被多重继承的基类所共享的,编译器怎么实现这个机制的?
虚继承:
在虚继承机制下继承出一个D类,关系如图所示:
以上继承关系的构造析构顺序
内存模型:
先通过VS在内存中查看D类的内存模型。
对上图中红色框出来的数据换种方式进行显示
会发现这是两个相同的地址,也就是说在菱形虚继承下,编译器在虚继承下的派生类中添加了一个指针来帮我们解决了数据的冗余和二义性问题。
实际上这个vbptr指针为指向虚基类表起始地址的指针:
虚基类表有八个字节,每四个字节表示一个偏移地址。
前四个字节:
表示该对象的数据成员起始地址相对于虚函数表指针vfptr的偏移量
后四个字节:
表示该对象所继承过来的基类对象相对于vbptr指针起始地址的偏移量
Is a继承和has_a继承的区别和联系
Is a 继承:
整体来看,is-a表示了一种“是的”关系。比如白马是马,香蕉是水果,老师是人这种关系。
public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象并且 Is a关系是不可逆的。
Has a继承:
has-a体现了有这个思想。
比如,午餐有香蕉。但是午餐不是香蕉。
其实私有跟保护继承体现了has-a原则。是因为私有跟保护继承是实现继承。
什么是实现继承呢?
实现继承的主要目标是代码重用,我们发现类B和类C存在同样的代码,因此我们设计了一个类 A,用于存放通用的代码,基于这种思路的继承称为实现继承。
我们可以说,午餐中存在香蕉。
protetced/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分,是 has-a 的关系原则。
那么基类的友元函数能不能被派生类继承呢?
1.友元函数
答案是:不能!
友元只是能访问指定类的私有和保护成员的自定义函数,不是被指定类的成员,自然不能继承。
并且使用友元类时注意:
(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
(3)友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明
注意事项:
1.友元可以访问类的私有成员。
2.友元只能出现在类定义内部,友元声明可以在类中的任何地方,一般放在类定义的开始或结尾。
3.友元可以是普通的非成员函数,或前面定义的其他类的成员函数,或整个类。
4.类必须将重载函数集中每一个希望设为友元的函数都声明为友元。
5.友元关系不能继承,基类的友元对派生类的成员没有特殊的访问权限。如果基类被授予友元关系,则只有基类具有特殊的访问权限。该基类的派生类不能访问授予友元关系的类。
静态成员函数的继承
基类与派生类的静态成员函数与静态成员是共用一段空间的,即静态成员和静态成员函数是可以继承的。
父类的static变量和函数在派生类中依然可用,但是受访问性控制(比如,父类的private域中的就不可访问)。而且对static变量来说,派生类和父类中的static变量是共用内存空间的,这点在利用static变量进行引用计数的时候要特别注意
派生类的friend函数可以访问派生类本身的一切变量,包括从父类继承下来的protected域中的变量。但是对父类来说,他并不是friend的。
基类和派生类同名方法的三种关系是什么?
函数覆盖、函数隐藏、函数重载
先分别讲一下函数覆盖,隐藏和重载分别是什么:
1.成员函数被重载的特征
(1)相同的作用域(必须在同一个类中)。这点很重要
(2)函数名字相同
(3)参数不同
(4)virtual 关键字可有可无。
2.覆盖/重写是指派生类函数覆盖基类函数,特征是
(1)不同的作用域(分别位于派生类与基类);
(2)函数名字相同;
(3)参数列表(不考虑this指针)相同,返回值相同
(4)基类函数必须有virtual关键字。
当派生类对象调用子类中该同名函数时会自动调用子类中的覆盖版本,而不是父类中的被覆盖函数版本,这种机制就叫做覆盖。
派生类对象调用的是派生类的覆盖函数
指向派生类的基类指针调用的也是派生类的覆盖函数
基类的对象调用基类的函数
问题:覆盖函数的访问权限问题?
看基类的访问权限,因为多态是通过基类指针根据指针类型去调用派生类的覆盖函数,编译的时候是检查你基类指针的访问限定符,也说在编译时检查访问限定符的。
问题:覆盖函数拥有形参默认值会发生什么问题?
形参默认值是在编译的时候确定,也就是说在压栈的时候是压入基类的默认值参数,因为你是通过基类指针去调用派生类的具体重写函数。这个时候你在派生类函数参数中写默认值是没有意义的
3.隐藏是指派生类的函数屏蔽了与其同名的基类函数,规则如下
(1) 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2) 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
(3) 如果想调用被隐藏的函数,调用处则加上基类的作用域。
(4) 如果想调用被隐藏的函数,派生类加上using::fun() 声明到该类里面。这样就破坏隐藏的特征,也就破坏了隐藏的语法。
6.基类和派生类之间的转换关系是什么?
将派生类引用或指针转换为基类引用或指针被称为向上强制转换
编译器默认支持继承结构下到上的转换关系,不支持上到下的转换关系,从内存上来看,基类和派生类的内存大小可能不同。如果不使用显示类型转换,从上到下的转换编译器是不允许的,因为is-a关系是不可逆的。
基类对象 赋值==》 派生类对象 no
派生类指针(引用) 赋值==》 基类对象 no
派生类对象 赋值==》 基类对象 ok
基类指针(引用) 赋值==》 派生类对象 ok