文章目录
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/3afc4d75bbf54666a0edd9802802d4b1.png)
面向对象语言的三大特性:封装、继承和多态
封装的思想:
- 将数据和方法封装到一个类中,通过访问限定符的修饰,想让外界访问的定义成public,不想让外界访问的定义成protected/private,比如class的定义
- 将一个类型/类封装到另一个类中,自主定义该类型的行为,或者通过typedef重命名类型,暴露给给用户,让用户同一使用,不用关注底层,比如
list/reverse_list
的迭代器
1. 继承的概念和定义
1.1 继承的概念
继承是面向对象语言在类设计上的一种代码复用手段
有这样的场景,你要完成一个学生管理系统,必然需要描述很多的对象,于是构建很多类,每个类代表不同的群体,比如学生类、老师类、宿管类…定义出来后,发现每个类中都有某些属性是相同的,比如大家都有名字、年龄、性别这样的属性,在每个类中都定义了一遍,很显然代码冗余,于是就有了继承
将每个类的公共属性提取出来,单独作为一个类,称为父类/基类;每个群体中持有它们独有的属性,叫做子类/派生类,通过继承的方式,将父类继承给子类,这样子类就有父类的属性和自身独有的属性
1.2 继承的定义
2. 父类成员变量在子类中的访问限定符
父类的成员变量有三种访问限定符,继承方式也有三种,父类成员变量在子类中的访问限定符是怎样的?
父类成员变量/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
父类public成员 | 子类的public成员 | 子类的protected成员 | 子类的private成员 |
父类protected成员 | 子类的protected成员 | 子类的protected成员 | 子类的private成员 |
父类private成员 | 子类中不可见 | 子类中不可见 | 子类中不可见 |
对于这张表,有个规律可以帮助记忆
无论是何种继承方式,只要是父类的private成员变量,在子类中不可见;这里的不可见是指无法在子类中无法直接访问父类成员,但可以间接访问
其余情况,按照public > protected > private 的顺序,取继承方式和成员访问限定符中较小的那个,就是父类的成员在子类的访问限定符;比如如果是父类的protected成员,public继承方式,那么在子类中该成员就是protected的
在实践中,大部分情况使用的都是public继承,同时用private修饰成员也很少见
3. 父类和子类的赋值转换
C语言中,相关类型之间可以发生隐式类型转换,中间会产生临时变量,C++中延续了这种语法
在public继承中,可以认为每一个子类对象都是一个特殊的父类对象,子类对象可以赋值给父类,中间不产生临时变量,这是一种赋值兼容
如果将子类赋给父类的指针或引用,该指针/引用指向子类中的父类那部分,我们也称为切割/切片
但是不能将父类赋值给子类
4. 继承中的作用域
- 父类和子类有着各自的作用域
- 如果父类和子类有同名变量,则这两个变量构成隐藏(重定义);在子类中访问时默认访问子类的同名变量,可以通过域作用限定符访问父类中的同名变量
- 如果父类和子类有同名成员函数,只要函数名相同,这两个函数就构成隐藏
5. 子类的默认成员函数
子类中,我们将成员分成两部分,父类的和自身的
子类的默认成员函数执行规则:
- 把父类的成员看成一个整体,去调用父类的默认成员函数
- 自身的成员不变,内置类型不做处理,自定义类型去调用自定义类型的默认成员函数
5.1 子类的构造函数
如果没有写子类的构造函数,默认构造函数会把父类的成员看成整体,去调用父类的默认构造,自身成员内置类型不做处理,自定义类型去调用自定义类型的构造函数
如果父类没有默认构造,则需要自己写子类的构造并在初始化列表中显示调用父类的构造
5.2 子类的拷贝构造
同理,如果没写子类的拷贝构造,父类成员去调用父类的拷贝构造,自身成员完成值拷贝
如果有深拷贝,需要自己写拷贝构造,则需要在子类的拷贝构造中显示调用父类的拷贝构造
5.3 子类的赋值重载
赋值重载同拷贝构造类似,有深拷贝则需要自己写赋值重载,在子类的赋值重载中调用父类的赋值重载,需要注意的是,父子赋值重载构成隐藏,在调用父类赋值重载时需要指定作用域
5.4 子类的析构函数
析构函数能够直接调用,但如果在子类析构函数中调用父类系统,会发现报错
由于多态的需要,编译器会将父类和子类的析构函数同一命名为destructor()
,于是父类和子类的析构函数构成隐藏,默认调用的是子类的析构
析构函数的析构顺序不同,在构造时,是先构造父类的部分,再构造子类的部分;而析构则相反,需要保证先析构子类的部分,再析构父类的部分
因此,子类析构在调用结束后会自动调用父类析构,这是为了防止在子类析构中使用父类成员后导致非法访问的问题
6. 继承与友元
父类的友元函数不是子类的友元函数
7. 继承与静态成员
父类的静态成员不仅属于当前父类,也属于所有子类,只有一份
利用该特性,可以计算当前父类一共有多少个子类(包括父类)
8. 多继承与菱形继承
- 单继承:一个子类只有一个直接父类
- 多继承:一个子类有两个及以上个直接父类
生活中,有些物品具有双重属性,比如西红柿,它既是水果,又是蔬菜,使用多继承来描述十分合理
多继承本身也没问题,但有多继承,就可能出现菱形继承
菱形继承导致最总的D对象中包含了两份/多份A对象,也就是数据冗余的问题
同时,访问D对象中的A类成员时,编译器不知道访问哪个,也就是二义性的问题
对于菱形继承所导致的问题,Bjarne Stroustrup给出的答案是使用虚继承,对继承最初父类的子类进行virtual
修饰
在菱形虚拟继承中,将B和C的公共父类A放到了D对象模型中的最下面,我们把A类叫做虚基类;B和C中都多了一个指针,该指针中存放的值是当前类到虚基类的偏移量
为什么要存放到虚基类的偏移量?如果我们想要通过父类的指针/引用,去访问虚基类中的对象,指针中的偏移量就能找到虚基类
同时,被virtual
修饰的类自身内存结构也发生变化,为什么这样做?拿B类举例,B对象的指针/引用可能是一个D对象,也可能是一个B对象,这样做就统一了访问虚基类的操作
9. 继承和组合
- 继承是一种is-a的关系
- 组合是一种has-a的关系
如果一个对象既能用继承描述,又能用组合描述,优先使用组合
继承是一种"白箱"复用,父类成员对子类可见,导致类和类,模块和模块之间耦合度高,相互影响大
组合是一种"黑箱"复用,被包含类的成员对包含类不可见,模块之间耦合度低,相互影响程度小
10.继承总结
关于多继承的面试题:
-
C++有多继承,为什么java没有?
C++比java先设计,在当时,多继承看起来十分合理,于是就设计了出来,但是没想到出现了菱形继承的问题,导致解决非常麻烦,而且生活中也很少使用;而java吸收了这个教训,在设计时就舍弃了多继承
-
多继承的问题是什么?
多继承本身没有任何问题,但有多继承就可能会写出棱形继承
-
棱形继承的问题?如何解决?
数据冗余,二义性;对第一级的子类使用虚拟继承
-
底层角度是如何解决数据冗余和二义性的?
在内存中,虚基类放到整个对象的最下面,并在虚继承类中添加一个指针,用来记录虚基类的偏移量