目录
思维导图:
一· 继承
1.1 继承的定义
继承就是在保持原有类的特性基础上,进行功能的扩展,功能的增加;产生新的的一个类,称为子
类或者是派生类。
继承的本质是类设计层次的复用,继承呈现了面向对象设计的层次结构
1.2 继承的使用
此时类 Student ,Teacher 是对类 Person 的继承。
借助调试,发现:此时Student 这个类不仅仅有一个自己类型的成员 _stuid 还有父类的所有成
员。这就体现了继承的作用:复用
二·对象赋值兼容转换的规则
2.1隐式类型转换
2.2显示类型转换
显示类型转换也是借助一个临时对象进行赋值的
注意这里编译报错:不是因为类型不同造成的,而是引用造成权限放大问题
此时可能会编译报错(具体情况取决于编译器):编译器会借助一个临时对象来进行对 f 这个对象
的拷贝,此时这个临时对象具有const 属性,r 又是这个临时对象的引用,但是r不具有const 属
性,因此造成权限放大
解决:
2.3 赋值兼容转换(切割 / 切片)
2.3.1 赋值兼容转换
思考一个问题:子类对象赋值给父类对象的时候,是否需要进行拷贝,生成一个临时对象?当被赋
值的对象是一个父类引用的时候,并且这个父类引用不加const 修饰,是否可以编译通过?
首先子类对象赋值给父类对象是天然的,当父类对象使用引用的形式并且没有const 修饰也是可以
编译通过的。
对于父类对象引用的赋值操作:通过对子类对象进行划分切割,让Person&p 指向子类对象;这也就是所谓的切割或者切片(注意只是一种形象化的理解)
2.3.2 使用细节
1)子类对象可以赋值给父类的对象,引用,指针
2)父类对象不能赋值给子类对象,引用,指针
3)对父类指针进行强制类型转换可以赋值给子类指针,可能存在越界访问
三·继承的作用域
3.1 同名的成员
在继承关系里面,父类和子类的成员是允许同名的
当在子类的类域里面进行对同名成员的访问时,优先子类(和局部优先的原则一样),要是对父类
的同名成员进行访问,需要指定类域
3.2 函数隐藏
函数隐藏:在继承关系里面,只要是函数名字相同,就构成函数隐藏,不管函数参数类型是否一
样,函数返回类型是否一样。
看看以下程序运行结果是啥?
A 2个fun 函数构成函数重载
B 2个fun 函数构成函隐藏
C 2个fun 函数构成虚函数
D 编译报错
E 运行错误
分析:
函数重载的首要条件就是在同一个作用域里面,所以A不对
B正确:函数名字一样,符合继承关系
D: 对于父子类的同名成员是支持同时存在的
四·派生类的默认成员函数
4.1构造函数
在继承关系里面,在调用子类的构造函数之前,必须先调用父类的构造,以确保父类的成员被整及
时准确的初始化。注意调用构造函数的先后顺序不能颠倒!!!
假设是先调用子类的构造函数再调用父类的构造函数:对于一个继承关系我们知道是先有父类后有
子类的,当我们在父类的作用域里面先调用子类的构造函数是有问题的,此时子类还不存在,而且
父类并不知道子类里面到底有什么样的成员以及成员对应的访问权限……所以说,只能先调用父类
再调用子类的构造函数,遵从“先父后子”
4.2 析构函数
对于析构函数调用的先后问题,是不是也是“先父后子”。
析构函数调用必须满足:“先子后父”的顺序。
假设先调用父类的析构函数,当父类析构调用结束后,子类的析构函数可能会涉及到对父类成员的
访问,此时就出现野指针的问题。
先子后父:保证的资源可以正确合理的释放,当子类析构函数调用结束,编译器自动调用父类的
析构函数
完整代码:
class Person { public://构造函数Person(const char* name = "xiaoli"): _name(name){cout << "Person()" << endl;}//拷贝构造函数Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}// 赋值重载函数Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}// 析构函数~Person(){cout << "~Person()" << endl;}protected:string _name; // 姓名};class Student : public Person{public:// 构造需要注意:此时子类里面不仅仅有自己的成员还有父类的成员,这时候需要调用父类构造函数// 对于初始化列表里面成员初始化先后顺序:与列表里面出现的先后无关,只与成员声明先后顺序有关(先对父类成员进行初始化)Student( const char* name,int num) : _num(num), Person(name){cout << "Student()" << endl;}Student(const Student& s): Person(s), _num(s._num){cout << "Student(const Student& s)" << endl;}Student& operator = (const Student& s){cout << "Student& operator= (const Student& s)" << endl;if (this != &s){Person::operator =(s);_num = s._num;}return *this;}~Student(){cout << "~Student()" << endl;}protected:int _num; //学号};
4.3 拷贝构造函数
继承关系里面,先调用父类的拷贝构造函数,再调用子类的拷贝构造函数。在拷贝 的过程中,涉
及到对父类的 拷贝,所以和先调用父类的构造函数一样的道理。“先父后子”
4.4 赋值重载函数
一样的问题:先调用父类的赋值重载函数再调用子类的赋值重载函数,“先父后子”
但是注意一个问题:此时父类里面的赋值重载函数的函数名字是 operator =() 而子类里面 的赋值重
载函数名字也是 operator=() ,此时父子类里面法函数名字相同,构成函数隐藏的关系,函数隐藏
关系只需要满足:符合继承关系,函数名字一样即可,编译器会调用子类的赋值重载函数,因此造
成栈溢出的问题。
解决:在函数调用 的前面,指定作用域即可保证先调用父类的赋值重载函数
五·继承与友元的关系
友元关系不能被继承;父类的友元函数不能在类外对子类的私有和保护成员进行访问。
六· 继承与静态成员的关系
在整个继承的体系里面只有一个静态成员,与派生子类 的个数没有关系,子类对这个静态成员继
承的是使用权
七·多继承
1. 单继承
单继承:一个子类只有一个直接的父类
2.多继承
当一个子类的直接父类个数大于等于2的时候,就是一个多继承的关系
菱形继承是多继承的一个特例
3·菱形继承存在的问题
1)二义性问题
2)数据冗余问题
通过这个Assistant 对象,我们发现Person 这个类存储了2份数据,同时当对_name 进行访问的
时候,发生歧义。
4·菱形的虚继承
为更好解决数据冗余和二义性问题,采用菱形的虚继承。
这也是一个菱形继承关系,此时的 virtual 关键字需要添加在哪里?
对于虚继承的关键字virtual ,添加在与父类直接相连的直系子类上即可。因为此时最下面的子类
里面是对最上面的类进行的数据的多存储。
5.菱形虚继承的原理
接下来进行调试:借助监视和内存来观察。
我们发现B,C 里面不仅仅存储了对应的成员,还多存了一个数据,这个数据又是什么?
再次打开一个内存窗口:输入 00 e1 7b dc, 就能得到以下结果
此时通过对0x 0095F9AC ,14000000,0x 0095F9C0 进行分析我们发现
0x 0095F9AC + 14000000 = 0x 0095F9C0,也就是当前_a 的地址
其实14000000 ,是一个偏移量的大小:表示当前指针距离A 指针偏移量的多少,这样我们就能访
问到 A 里面的成员,从而也避免了数据冗余问题。
总体来说:虚继承原理借助了虚基表指针和虚基表(存的是一个偏移量)来解决了二义性和
数据冗余问题。
注意:对于多继承而言,能不用菱形的虚继承就不要使用,底层比较复杂,稍不注意,自己可能就
会绕不出来。
八·继承的总结
1)继承在语法和底层方面都比较复杂,尤其是菱形虚继承。
2)对于public 继承是 is-a 的关系:每一个子类对象都是一个父类对象
3)对于组合是has-a 的关系:
4)组合关系更符合“低耦合,高内聚”
九·相关的面试题
1. 什么是菱形继承?菱形继承的问题是什么? 是多继承的一种特殊情况:直系的父类个数大于等于2个 问题:二义性;数据冗余 2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的? 在多继承中,使用关键字 virtual ,确保父类只有一个共享实例。 采用 虚继承:借助虚基类指针和虚基表 3. 继承和组合的区别?什么时候用继承?什么时候用组合? 区别: 1) 对于public 继承是 is-a 的关系:每一个子类对象都是一个父类对象对于组合是has-a 的关系:
2)继承的耦合度更高,组合耦合度较低,受其他接口影响较小
3)对于继承,子类是可以继承父类的所有属性和方法的(私有成员除外)
使用继承:在多态里面;行为或者功能的复用……
使用组合:隐藏实现的细节,只把接口给外部使用;耦合度要求较低……