【C++】 继承与多态(中)


继承(知识点下)

一、继承与静态成员

  • 子类不能 "重写" 静态成员,只能 "隐藏"
  • 父类和子类共享同一个静态变量(如果子类没有重新定义)
  • 静态方法不具备多态性
  • 调用规则:编译看左边,运行也看左边(和普通方法多态完全相反

二、多继承及其菱形继承问题

1.继承模型

单继承**:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承**

多继承**:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。**

菱形继承**:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。**

两大致命问题
  1. 数据冗余最顶层父类 Animal 成员变量存两份,浪费空间
  2. 访问二义性子类直接访问顶层父类成员,编译器不知道走哪条继承路径

2.虚继承

cpp 复制代码
class A{ public: int a; };
// 中间类加 virtual 虚继承
class B:virtual public A{};
class C:virtual public A{};
class D:public B,public C{};

1. 作用

专门解决菱形继承两大问题:

  1. 顶层父类成员重复拷贝、数据冗余
  2. 顶层父类成员访问二义性

2. 底层原理

  1. 虚继承依靠 虚基类表 + 虚基类指针
  2. 派生类不直接拷贝父类成员,只存偏移地址
  3. 所有派生类共享同一份虚基类成员

3. 构造函数执行顺序

虚基类最先构造 → 普通父类 → 子类

  • 虚基类只会构造一次
  • 必须由最派生类调用虚基类构造

4. 核心特点

  1. virtual 只用于继承,不是成员
  2. 虚继承不产生虚函数表,是虚基类表
  3. 减少内存占用,解决菱形冲突
  4. 不能用来实现多态

5. 易混区分

  • 虚继承 virtual public:解决菱形继承数据冗余
  • 虚函数 virtual 函数:实现运行时多态

6.题目练习

cpp 复制代码
class 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)直接负责调用,中间派生类(StudentTeacher)中对虚基类的构造调用会被编译器忽略。

内存视角

当程序执行 Assistant a("张三", "李四", "王五");时:

  1. 系统首先为最底层的派生类 Assistant分配内存。

  2. 在初始化阶段,编译器会先找到最顶层的虚基类 Person

  3. 它看到 Assistant的初始化列表中写了 Person(name3),于是将 "王五" 传递给 Person的构造函数来初始化那份唯一的 _name成员。

  4. 随后再去执行 StudentTeacher构造函数中除了虚基类以外的其他部分(如初始化 _num_id

多继承中指针偏移问题?下⾯说法正确的是( C )
A:p1 == p2 == p3 B:p1 < p2 < p3
C:p1 == p3 != p2****D:p1 != p2 != p3

cpp 复制代码
class 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库中的菱形虚拟继承

最核心考点

  1. C++ IO 库是菱形虚拟继承的标准实例
  2. 顶层基类:ios_base
  3. 中间层:istream、ostream(虚继承自 ios_base)
  4. 最终类:iostream(多继承自 istream + ostream)
  5. 目的:让状态、标志、缓冲区只保留一份
  6. 虚继承只加在中间层: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) + 父类指针 / 引用指向子类对象
  • 核心特点:灵活性极高,是面向对象设计的核心
  • 满足条件(缺一不可):
    1. 子类继承父类
    2. 子类重写(override)父类的虚函数
    3. 父类指针 / 引用指向子类对象


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: 以上都不正确

cpp 复制代码
class 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.协变(了解)

派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引 ⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际意义并不⼤,所以我们 了解⼀下即可。

cpp 复制代码
class 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.析构函数重写

普通成员函数重写要求:函数名、参数、返回值完全一致但析构函数是特例:

  1. 基类析构声明为 virtual
  2. 派生类哪怕不加 virtual、析构函数名看起来不同(~A() / ~B()
  3. 编译器底层统一把所有析构函数名处理成 destructor
  4. 自动构成虚函数重写,满足动态多态

简单记:只要基类析构是虚析构,子类析构天然重写

一句话面试答题话术:

因为编译器会将所有析构函数统一命名,基类虚析构后子类析构自动完成重写,使用父类指针释放子类对象时实现动态绑定,先调用子类析构释放子类独有资源,再调用基类析构,彻底避免内存泄漏,所以含有继承体系的基类析构必须设计为虚函数。

3.override 和 final关键字

cpp 复制代码
void func() override; // 只能加在子类虚函数后面
virtual void func() final;
关键字 作用 使用位置
override 强制检查是否重写,避免写错 子类重写函数后
final 禁止重写 或 禁止继承 虚函数 / 类

五、纯虚函数和抽象类

1. 纯虚函数定义

在虚函数声明末尾加上 = 0,即为纯虚函数

复制代码
virtual 返回值 函数名(参数列表) = 0;

核心特点

  1. 仅做接口声明,可以不写函数实现
  2. 语法上允许手动写实现,但业务层面一般没必要
  3. 纯虚函数没有默认实现,目的就是强制子类重写

2. 抽象类

包含至少一个纯虚函数的类,称为抽象类

抽象类硬性规则

  1. 无法实例化对象

    复制代码
    Base b;    // 编译报错,抽象类不能创建对象
    Base* p;   // 允许定义指针、引用
  2. 派生类继承抽象类后:

    • 必须重写所有纯虚函数,派生类才能成为普通类、正常实例化
    • 只要有一个纯虚函数没重写,派生类依旧是抽象类,依旧不能实例化
  3. 抽象类可以拥有普通成员函数、成员变量、构造、析构函数

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;
}

相关推荐
Aurorar0rua6 小时前
CS50 x 2024 Notes C -14
c语言·开发语言·学习方法
小短腿的代码世界7 小时前
从.qrc到rcc编译器:Qt资源系统的隐秘运作机制与大型项目性能突围
开发语言·qt
MY_TEUCK7 小时前
【2026最新Python+AI学习基础】Python 入门笔记篇
笔记·python·学习
2401_833269308 小时前
Java网络编程入门
java·开发语言
青瓦梦滋8 小时前
C++的IO流与STL的空间配置器
开发语言·c++
五月君_8 小时前
Bun v1.3.14 发布,Rust 版即将进 Claude Code 内测,下一版可能就告别 Zig
开发语言·后端·rust
鱼很腾apoc9 小时前
【学习篇】第20期 超详解 C++ 多态:从语法规则到底层原理
java·c语言·开发语言·c++·学习·算法·青少年编程
不吃土豆的马铃薯10 小时前
4.SGI STL 二级空间配置器 allocate 与_S_refill 源码解析
c语言·开发语言·c++·dreamweaver·内存池