继承-类设计层次复用切片转换与菱形继承的死结

继承:类设计层次的复用、切片式转换与菱形继承的死结

我的github:(https://github.com/xcx55/ubuntu-linux-project)

感谢各位大佬参观我的github!!

一、继承是什么:从函数复用到类复用

之前所有代码复用都是函数层次 的------写个函数到处调。继承把复用抬到了类设计层次 :Student 和 Teacher 都有姓名/地址/电话/年龄、都有身份认证函数,分别写两遍就是冗余------把公共部分抽成 Person,让 Student/Teacher 继承它:

cpp 复制代码
class Person { ... };

class Student : public Person {   // 派生类 : 继承方式 基类
    int _stuid;   // 学号
    int _major;   // 专业
};

继承一个类的全部,不用自己再写一个了 。这就是面向对象里"由简单到复杂"的认知路径:以前接触的是函数层次的复用,继承是类设计层次的复用。

和类模板对比一下更有意思------类模板出来的类型是不一样的 (Stack 和 Stack 是两个类),而类继承出来的类型是一脉相承的(Student 也是一个 Person)。两个都是复用,但复用的方向不同。

复用的两大方式也因此凑齐了:组合(container = vector 那种"有一个")和继承("是一个")------组合和继承,是 C++ 复用的两条正路。

二、继承方式与权限:min 规则

继承方式有 public/protected/private 三种,配合基类成员的三种访问属性,规则一句话:取两者中更严的那个 (min 规则)。基类的 private 成员无论以什么方式继承,在派生类里都是不可见的------看得见摸不着,"继承了一份家产但锁在保险柜里"。

实际工程只记住一条:都用 public。protected/private 继承破坏"是一个"的关系,基本不用。

三、切片:为什么派生类赋给基类不用加 const

继承体系里最精妙的一块是类型转换。一般来说,派生类和基类是不一样的类型,不同类型赋值要生成临时对象、要加 const("临时变量具有常性"那章讲过)。但这里有个特殊处理的例外:

派生类对象可以赋值给基类的指针/基类的引用,而不需要加 const:

cpp 复制代码
Student s;
Person* p = &s;    // OK
Person& r = s;     // OK
Person obj = s;    // 也 OK,但发生了切片

原因看内存布局就懂:Student 对象的前半部分就是一整个 Person (_name/_sex/_age 在前,_No 在后)------指针和引用绑定的是派生类对象中的基类部分,类型改变了,但指向的就是父类那块东西,没有任何"临时对象"参与,所以不需要 const。

而 Person obj = s 走的是切割(切片) :用 s 的基类部分拷贝构造出一个全新的 Person,s 特有的 _No 被切掉了。大体都是派生类赋值给基类(父 → 子方向畅通),反过来不行------基类里没有派生类的部分,硬要转只能强制转换且不安全。

你笔记里那句推理很到位:"this 指针和引用,子类变为父类,因为子类必然嵌着父类,所以直接就有上下文联系" ------继承体系内有严格的类划分,但转换的通道已经由内存布局铺好了。

四、隐藏:两个域,先子后父

基类和派生类是两个独立作用域 ------但继承让它们有了查找顺序:访问成员,先在子类找,再到继承的父类找。

由此产生"隐藏"规则:无论是变量还是函数(不管参数),只要名字相同,子类的就会隐藏父类的 。注意这和重载无关------函数重载要求在同一作用域 ,而父子类是两个域,所以子类的 fun(int) 会把父类的 fun()/fun(int,int) 全部藏起来,想调父类的要显式 Person::fun()。

继承总结里你自己那句也保留了:变量声明直接继承;函数用的是同一套函数,两个域都有查找权限------除非你重写函数、声明变量 。顺带一条:static 变量在继承体系里也是独一份的------无论派生多少个类,那个 static 成员只有一份,所有对象共享。

五、派生类的默认成员函数:把父类"当一个整体"

六大默认成员函数在继承体系下的行为,核心一句:派生类的继承部分,直接调用父类对应的函数 ------"自己类的自己整":内置类型字节处理、自定义类型调它自己的、父子类把父类成员当做一个整体去调父类的。

构造函数细节最多:

  • 你可以自己写派生类的构造函数,但父类那部分要视为一个整体(就是一个类) ------所以要在初始化列表里写父类的匿名对象 Person("张三") 来初始化父类部分;父类有默认构造时可以不写,没有就必须在初始化列表里手动写;
  • 派生类不能单独构造父类成员 ------想造一个"只有父类部分"的对象?做不到,这就是实现"不能被继承的类"的钥匙:
    • C++98 方法:基类构造函数私有化------派生类看不见就调不了,无法实例化;
    • C++11 方法 :final 关键字修饰基类,直接禁止继承。
  • 拷贝构造/赋值重载同理:基类的部分调基类的拷贝/赋值;析构函数析构时自动调父类析构(保证先子后父的顺序由编译器机制兜底)。

六、多继承与菱形:C++ 设计的缺陷

多继承 :一个派生类有两个基类------从需求来说多需求很正常,但多继承是个大坑 。两个基类如果又继承自同一个祖先,就是菱形继承:

复制代码
      Person
     /      \
 Student    Teacher
     \      /
     Assistant

Assistant 对象里会有两份 Person 成员 ------数据冗余 + 访问二义性 (你访问 _name,走 Student 那条还是 Teacher 那条?)。支持多继承就一定会有菱形继承 ,Java 干脆不支持多继承、规避掉了这个问题,实践中也不建议设计出菱形继承这样的模型。

C++ 的补丁是虚继承(virtual 继承) :让 Person 在 Assistant 里只存一份。能解决,但代价是底层要引入偏移量表之类的间接层,复杂度陡增。标准库自己也有菱形虚拟继承的真实案例------IO 库:ios_base → basic_ios → basic_ostream/basic_istream → basic_iostream,iostream 就是菱形汇聚点,用的就是虚继承。库可以这么玩,业务代码里能绕则绕。

收个尾

继承这一章把前面所有章的积累又串了一遍:

  • 复用从函数层(继承1 之前的一切)升到类层;
  • 切片转换没有临时对象,因为子类内存里本来就嵌着完整的父类------"临时变量具有常性"在这里有了精确的例外;
  • 两个域、先子后父的查找规则,是"域论"在继承体系下的延伸;
  • 默认成员函数把父类当一个整体------"构造/拷贝/赋值分三个函数"的规矩原样生效;
  • final 的原理就是"派生类不能单独构造父类成员"------一个语法关键字背后是构造函数的调用链。

下一个大章:多态------虚函数、虚表指针,以及"指向谁调谁"的运行时决议。

相关推荐
架构师伯子明1 小时前
FFmpeg C++学习(一):编译和demo
开发语言·c++·学习
纪念 2291 小时前
C++算法(六)
开发语言·c++·算法
ShineWinsu2 小时前
对于Qt:QCheckBox常见属性的详细解析
c++·qt·面试·笔试·gui·qcheckbox·多选框
AI的探索之旅2 小时前
97 个 OpenCV 实例(三十七):二维码识别,一张图四个码的陷阱
c++·opencv·计算机视觉
UIU1143 小时前
P16237 [蓝桥杯 2026 省 B] 应急布线
数据结构·c++·算法·蓝桥杯
raindayinrain3 小时前
c++设计模式
开发语言·c++·设计模式
旖旎夜光3 小时前
LeetCode 315: 算右侧小于当前元素的个数(归并排序) —— 题解
c++·学习·算法·leetcode·排序算法
D e re k ZH3 小时前
保序回归:从 PAVA 到可并堆的单调约束优化
c++·算法·数据挖掘·回归·kotlin
青少儿编程课堂3 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:字典树前缀树与单词检索统计
c++·python·算法·bfs·信息学竞赛