继承(知识点下)
一、继承与静态成员
- 子类不能 "重写" 静态成员,只能 "隐藏"
- 父类和子类共享同一个静态变量(如果子类没有重新定义)
- 静态方法不具备多态性
- 调用规则:编译看左边,运行也看左边(和普通方法多态完全相反
二、多继承及其菱形继承问题
1.继承模型
单继承**:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承**
多继承**:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。**
菱形继承**:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。**
两大致命问题
- 数据冗余最顶层父类
Animal成员变量存两份,浪费空间- 访问二义性子类直接访问顶层父类成员,编译器不知道走哪条继承路径
2.虚继承
cppclass A{ public: int a; }; // 中间类加 virtual 虚继承 class B:virtual public A{}; class C:virtual public A{}; class D:public B,public C{};1. 作用
专门解决菱形继承两大问题:
- 顶层父类成员重复拷贝、数据冗余
- 顶层父类成员访问二义性
2. 底层原理
- 虚继承依靠 虚基类表 + 虚基类指针
- 派生类不直接拷贝父类成员,只存偏移地址
- 所有派生类共享同一份虚基类成员
3. 构造函数执行顺序
虚基类最先构造 → 普通父类 → 子类
- 虚基类只会构造一次
- 必须由最派生类调用虚基类构造
4. 核心特点
virtual只用于继承,不是成员- 虚继承不产生虚函数表,是虚基类表
- 减少内存占用,解决菱形冲突
- 不能用来实现多态
5. 易混区分
- 虚继承 virtual public:解决菱形继承数据冗余
- 虚函数 virtual 函数:实现运行时多态
6.题目练习
cppclass Person { public: Person(const char* name) :_name(name) { } string _name; // 姓名 }; class Student : virtual public Person { public: Student(const char* name, int num) :Person(name) , _num(num) { } protected: int _num; //学号 }; class Teacher : virtual public Person { public: Teacher(const char* name, int id) :Person(name) , _id(id) { } protected: int _id; // 职⼯编号 }; // 不要去玩菱形继承 class Assistant : public Student, public Teacher { public: Assistant(const char* name1, const char* name2, const char* name3) :Person(name3) , Student(name1, 1) , Teacher(name2, 2) { } protected: string _majorCourse; // 主修课程 }; int main() { // 思考⼀下这⾥a对象中_name是"张三", "李四", "王五"中的哪⼀个? Assistant a("张三", "李四", "王五"); cout << a._name; return 0; }在 C++ 的多重继承中,当存在虚基类时,有一个特殊的构造规则:虚基类的构造函数由最终的派生类(在这里是
Assistant)直接负责调用,中间派生类(Student和Teacher)中对虚基类的构造调用会被编译器忽略。内存视角
当程序执行
Assistant a("张三", "李四", "王五");时:
系统首先为最底层的派生类
Assistant分配内存。在初始化阶段,编译器会先找到最顶层的虚基类
Person。它看到
Assistant的初始化列表中写了Person(name3),于是将 "王五" 传递给Person的构造函数来初始化那份唯一的_name成员。随后再去执行
Student和Teacher构造函数中除了虚基类以外的其他部分(如初始化_num和_id)多继承中指针偏移问题?下⾯说法正确的是( C )
A:p1 == p2 == p3 B:p1 < p2 < p3
C:p1 == p3 != p2****D:p1 != p2 != p3
cppclass Base1 { public: int _b1; }; class Base2 { public: int _b2; }; class Derive : public Base1, public Base2 { public: int _d; }; int main() { Derive d; Base1* p1 = &d; Base2* p2 = &d; Derive* p3 = &d; return 0; }
三、IO库中的菱形虚拟继承
最核心考点
- C++ IO 库是菱形虚拟继承的标准实例
- 顶层基类:ios_base
- 中间层:istream、ostream(虚继承自 ios_base)
- 最终类:iostream(多继承自 istream + ostream)
- 目的:让状态、标志、缓冲区只保留一份
- 虚继承只加在中间层:istream /ostream
四、继承和组合
重点概要
• public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。
• 组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。
• 继承允许你根据基类的实现来定义派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为**⽩箱复用(white-box reuse)。术语"⽩箱"是相对可视性⽽⾔:在继承⽅式中,基类的内部细节对派⽣类可⻅ 。继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依赖关系很强,耦合度⾼。**
• 对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对 象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为**⿊箱复⽤(black-box reuse), 因为对象的内部细节是不可⻅的。对象只以"⿊箱"**的形式出现。 组合类之间没有很强的依赖关 系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。
• 优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的 关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合。
继承 组合 is-a 是一个 has-a 有一个 代码复用:继承父类所有成员 代码复用:调用内部对象功能 破坏封装,父类私有也能间接访问 封装性强,低耦合 支持多态、重写 不支持多态 紧耦合,父类一改子类全变 松耦合,灵活易改
多态
1. 什么是多态?
多态的本质:父类指针 / 引用指向子类对象,调用同名函数时,执行子类的实现逻辑。
2. 实现多态两个必须重要条件:
• 必须是基类的指针或者引⽤调⽤虚函数
• 被调⽤的函数必须是虚函数,并且完成了虚函数重写/覆盖。3.C++ 多态的两大分类
C++ 多态分为静态多态(编译期多态)和动态多态(运行期多态),二者的触发时机、实现原理完全不同。
1. 静态多态(编译期确定)
- 触发时机:程序编译时就确定调用哪个函数
- 实现方式:函数重载、运算符重载、模板
- 核心特点:效率高,无运行时开销
cpp#include <iostream> using namespace std; // 函数重载:静态多态 class Calc { public: int add(int a, int b) { return a + b; } double add(double a, double b) { return a + b; } }; int main() { Calc c; c.add(1, 2); // 编译期确定调用int版本 c.add(1.1, 2.2); // 编译期确定调用double版本 return 0; }
2. 动态多态(运行期确定)⭐⭐⭐
- 触发时机:程序运行时根据对象实际类型确定调用逻辑
- **实现方式:**继承 + 虚函数(virtual) + 父类指针 / 引用指向子类对象
- 核心特点:灵活性极高,是面向对象设计的核心
- 满足条件(缺一不可):
- 子类继承父类
- 子类重写(override)父类的虚函数
- 父类指针 / 引用指向子类对象
4. 动态多态核心:虚函数与重写
1. 虚函数语法
在父类成员函数前加
virtual关键字,子类重写时可省略virtual(建议保留,可读性更强)。
cpp#include <iostream> using namespace std; // 父类:动物 class Animal { public: // 虚函数:标记为多态接口 virtual void shout() { cout << "动物发出叫声" << endl; } }; // 子类:狗 class Dog : public Animal { public: // 重写父类虚函数 void shout() override { // C++11推荐加override,强制检查重写 cout << "小狗:汪汪汪" << endl; } }; // 子类:猫 class Cat : public Animal { public: void shout() override { cout << "小猫:喵喵喵" << endl; } }; // 统一接口:接收任意Animal子类对象 void doShout(Animal& animal) { animal.shout(); // 多态调用:运行时确定执行哪个子类函数 } int main() { Dog dog; Cat cat; doShout(dog); // 输出:小狗:汪汪汪 doShout(cat); // 输出:小猫:喵喵喵 return 0; }2. 虚函数的重写/覆盖
虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。
3.多态场景的⼀个选择题
以下程序输出结果是什么( B )
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
cppclass A { public: virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;} virtual void test(){ func();} }; class B : public A { public: void func(int val = 0){ std::cout<<"B->"<< val <<std::endl; } }; int main(int argc ,char* argv[]) { B*p = new B; p->test(); return 0; }关键:func () 调用发生了什么?
① 函数体:动态绑定(多态生效)
func()是虚函数,所以真正调用的是 B::func ()→ 所以输出前缀是 B->② 默认参数:静态绑定(编译期确定)
默认参数不参与多态!编译器在编译时,只看当前所在类的默认参数
4.虚函数重写的⼀些其他问题
1.协变(了解)
派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引 ⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际意义并不⼤,所以我们 了解⼀下即可。
cppclass A {}; class B : public A {}; class Base { public: virtual A* f() { return new A; } }; class Derived : public Base { public: // 协变:返回 B*,而不是 A* B* f() override { return new B; } };2.析构函数重写
普通成员函数重写要求:函数名、参数、返回值完全一致但析构函数是特例:
- 基类析构声明为
virtual- 派生类哪怕不加 virtual、析构函数名看起来不同(
~A()/~B())- 编译器底层统一把所有析构函数名处理成
destructor- 自动构成虚函数重写,满足动态多态
简单记:只要基类析构是虚析构,子类析构天然重写
一句话面试答题话术:
因为编译器会将所有析构函数统一命名,基类虚析构后子类析构自动完成重写,使用父类指针释放子类对象时实现动态绑定,先调用子类析构释放子类独有资源,再调用基类析构,彻底避免内存泄漏,所以含有继承体系的基类析构必须设计为虚函数。
3.override 和 final关键字
cppvoid func() override; // 只能加在子类虚函数后面 virtual void func() final;
关键字 作用 使用位置 override 强制检查是否重写,避免写错 子类重写函数后 final 禁止重写 或 禁止继承 虚函数 / 类
五、纯虚函数和抽象类
1. 纯虚函数定义
在虚函数声明末尾加上
= 0,即为纯虚函数
virtual 返回值 函数名(参数列表) = 0;核心特点
- 仅做接口声明,可以不写函数实现
- 语法上允许手动写实现,但业务层面一般没必要
- 纯虚函数没有默认实现,目的就是强制子类重写
2. 抽象类
包含至少一个纯虚函数的类,称为抽象类
抽象类硬性规则
无法实例化对象
Base b; // 编译报错,抽象类不能创建对象 Base* p; // 允许定义指针、引用派生类继承抽象类后:
- 必须重写所有纯虚函数,派生类才能成为普通类、正常实例化
- 只要有一个纯虚函数没重写,派生类依旧是抽象类,依旧不能实例化
抽象类可以拥有普通成员函数、成员变量、构造、析构函数
3. 完整代码演示
cpp#include <iostream> using namespace std; // 抽象类 class Animal { public: // 纯虚函数:统一接口规范 virtual void speak() = 0; virtual ~Animal() = default; // 抽象类建议虚析构 }; class Dog : public Animal { public: // 重写纯虚函数,必须实现 void speak() override { cout << "汪汪汪" << endl; } }; class Cat : public Animal { // 未重写 speak(),Cat 仍是抽象类 }; int main() { // Animal a; 报错,抽象类不能实例化 Dog d; d.speak(); // 多态用法 Animal* p = new Dog; p->speak(); delete p; // Cat c; 报错,Cat是抽象类,无法实例化 return 0; }









