【学习篇】第19期 一文彻底搞懂C++继承
目录
开头:
ok了,如标题所见,我们这一期的学习篇进入到了一个全新的板块——继承,在前面我们已经系统的学习过C++中类和对象部分,我们会把功能函数封装到一个类中,这正是C++三大特性的第一种——封装、继承、多态,下面我们就来学习下继承这个特性
一.,继承的概念级定义
(1)继承的概念
在 C++ 面向对象编程(OOP)中,继承是三大核心特性(封装、继承、多态)之一,也是实现代码复用、构建类层次结构的核心手段
继承是指一个类(派生类 / 子类)可以复用另一个类(基类 / 父类)的成员变量和成员函数,同时可扩展自身独有特性的机制。
简单说:继承就是 “复用 + 扩展”,是类设计层面的代码复用(区别于函数层面复用)。
假如现在我们要完成一个学校教务系统,要有老师和学生这两个主要对象:

如上图,我们需要设计出两个不同的类,但是仔细看可以发现,这两个类的成员变量有很多重复的部分,比如:姓名、年龄、性别、电话,这些信息不管是老师还是学生都具有的,既然有很多重复的部分,像这样写两个不同的类代码就很冗余,接下来就要引出继承来解决这个问题了
(2)继承的定义
1.定义格式

如上图,这就是继承的用法
Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。
可以把继承想象成生物学的遗传现象,子类可以从父类继承一些成员函数和成员变量
继承的核心价值:
• 代码复用:避免重复编写公共代码,减少冗余
• 层次清晰:体现 “一般→特殊” 的逻辑(人→学生 / 老师)
• 易于扩展:新增子类时,直接继承基类,无需修改原有代码
• 支撑多态:继承是实现多态的基础(后续章节详解)

2.三种继承方式:权限变化规则
C++ 提供public、protected、private三种继承方式,核心区别是基类成员在派生类中的访问权限变化。

关键结论:
- 基类private 成员:无论用哪种继承方式继承,派生类都无法直接访问(仅继承到内存,语法隐藏)
这⾥的不可⻅是指基类的私有成员还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问 - 基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类
中能访问,就定义为protected。可以看出保护成员限定符 protected 是因继承才出现的。 - 权限简化公式:派生类权限 = min (基类成员权限,继承方式)(public > protected > private)
- 默认继承:class默认private 继承,struct默认public 继承(建议显式写继承方式)
- 在实际运⽤中⼀般使⽤都是public继承,很少且不建议使用 protected / private 继承
二.基类和派生类间的转换
public继承的子类对象 可以赋值给 父类的指针 / 父类的引⽤
这里引入 **切片(slice)**的概念:
• 子类对象赋值给父类指针 / 引用时,仅保留父类部分,子 类独有成员被 “切掉”,称为切片

要注意的是:父类对象不能赋值给子类对象


三.继承中的作用域:隐藏(Shadow)规则
隐藏规则:
• 基类和派生类有独立作用域
• 派生类与基类有同名成员时,派生类成员隐藏基类同名成员,以派生类为主(就近原则)
• 成员函数隐藏:仅函数名相同即可(参数不同也隐藏,区别于重载);
• 建议:实际开发避免同名成员,减少混淆。

问题(1):A和B类中的两个func构成什么关系()
A. 重载 B. 隐藏 C.没关系
问题(2):下⾯程序的编译运⾏结果是什么()
A. 编译报错 B. 运⾏报错 C. 正常运⾏
答案是: B、A
解析:就是上面隐藏规则的第三条,只要子类和父类的两个函数的函数名相同,两个函数就是隐藏关系,不管两个函数的参数是否相同,另外也会编译报错,此时父类的 fun 函数被隐藏了,要想要调用,要指定作用域
结论:函数名相同即构成隐藏,与参数无关
四.派生类的默认成员函数:构造、拷贝、赋值、析构
编译器默认生成:构造、析构、拷贝构造、赋值重载、取地址重载、const 取地址重载。
派生类默认成员函数的核心规则:必须先处理基类成员,再处理派生类成员

- 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员,如果基类没有默认的构造
函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。
这里要特别注意:不能这样写,只能走初始化列表
为什么????
因为C++规定:任何对象,在能被使用、能被读写、能进入函数体代码修改前,必须先执行构造完成初始化,变成合法对象。为什么不能将构造函数写在函数体内部?因为在进入 {} 之前,编译器会自动的调用默认的无参构造来进行对象的初始化,但是 person 作为基类,编译器不能之前调用默认的无参构造,只能显示的写,但是我们没有给编译器提供默认的无参构造,所以它会报错
但有的同学会说:为什么之前写构造函数我走初始化列表和在函数体进行初始化都可以完成对变量的初始化工作,到这却不行了??
因为之前我们写在函数 {} 内部的,本质上是赋值,编译器在进入 {} 前,就已经自动的完成了对对象的初始化工作

总结:必须走初始化列表的5种情况
- 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。

== person(st) 是上面关于’'切片“的用法==
- 派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的
operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域

- 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派
⽣类对象先清理派⽣类成员再清理基类成员的顺序。

实现⼀个不能被继承的类
⽅法1:把基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。
⽅法2:C++11新增了⼀个final关键字,final修改基类,派⽣类就不能继承了。

五.继承与友元、静态成员
(1)继承与友元
友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员 ,友元关系不传递,不继承

(2)继承与静态成员:整个体系共享一份
基类静态成员被所有派生类共享,整个继承体系只有一份静态成员实例

六.多继承与菱形继承:C++ 的经典陷阱
(1)继承模型

单继承:
多继承:
菱形继承
菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从上面的对象成员模型构造,可以看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的

(2)虚继承:解决菱形继承问题
核心方案:使用virtual关键字虚继承公共基类,让公共基类在派生类中只保留一份

虚继承的构造规则:公共基类由最派生类直接初始化


Assistant 是继承链中 “最底层” 的派生类,它必须在构造函数的初始化列表里,直接调用 Person(n1) 来初始化公共基类。
扩展:IO 库的菱形虚继承
C++ 的 iostream 库就是典型的菱形虚继承结构

• iostream 同时继承 istream 和 ostream,而它们又都虚继承自 ios
• 这样 ios 中的状态信息(如错误标志、缓冲区指针)只会有一份,避免了冗余和冲突
问题:多继承中指针偏移问题?下⾯说法正确的是( )
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3

答案: C
七.继承与组合:is-a vs has-a
核心区别
• 继承(is-a):表示 “是一种” 关系(Student 是一种 Person),白箱复用(基类细节对派生类可见,耦合度高);
• 组合(has-a):表示 “包含” 关系(Car 包含 Tire),黑箱复用(仅依赖接口,耦合度低)。
继承允许你根据基类的实现来定义派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤(white-box reuse)。术语“⽩箱”是相对可视性⽽⾔:在继承⽅式中,基类的内部细节对派⽣类可⻅ 。继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依赖关系很强,耦合度⾼。
对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-box reuse),因为对象的内部细节是不可⻅的。对象只以“⿊箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。
优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。
结尾
这一期对C++继承知识的学习就结束了,希望通过本文的学习,你能够彻底理解C++继承。如果本文对你有帮助,欢迎点赞、收藏、关注!如果有任何问题,也欢迎在评论区留言交流。我会持续更新更多的C++技术文章,敬请期待!
往期回顾
更多推荐






所有评论(0)