> 技术文档 > 《C++进阶之继承多态》【final + 继承与友元 + 继承与静态成员 + 继承模型 + 继承和组合】

《C++进阶之继承多态》【final + 继承与友元 + 继承与静态成员 + 继承模型 + 继承和组合】


【final + 继承与友元 + 继承与静态成员 + 继承模型 + 继承和组合】目录

  • 前言:
  • ------------------------
  • 一、final关键字——不能被继承的类
    • 1. 怎么实现不能被继承的类?
  • ------------------------
  • 二、继承与友元
    • 1. 父类友元访问子类成员的限制
    • 2. 子类无法继承父类友元的权限
  • ------------------------
  • 三、继承与静态成员
    • 1. 所有派生类共享同一实例
    • 2. 可通过类名直接调用
  • ------------------------
  • 四、继承模型
  • 1. 继承模型有哪些?
    • ① 单继承模型
    • ② 多继承模型
      • 菱形继承
      • 虚继承
  • 2. IO库中的虚继承长什么样?
  • 3. 关于多继承中指针偏移的一道面试题?
  • ------------------------
  • 五、继承和组合
    • 1. 什么是继承/组合?
    • 2. 继承和组合的区别是什么?
    • 3. 继承和组合怎么进行选择?
    • 4. 继承和组合的使用案例

在这里插入图片描述

往期《C++初阶》回顾:

《C++初阶》目录导航


往期《C++进阶》回顾:
/------------ 继承多态 ------------/
【普通类/模板类的继承 + 父类&子类的转换 + 继承的作用域 + 子类的默认成员函数】

前言:

嗨✧(≖ ◡ ≖✿) ,小伙伴们大家好呀!今天是平平无奇的一天,哦不对,今天其实是阳光明媚的一天呢。 (●°u°●)​ 」

嗯,在这么美好的日子里,我们要继续学习 【final + 继承与友元 + 继承与静态成员 + 继承模型 + 继承和组合】 的内容啦。想必大家现在已经满怀期待了吧(◔◡◔✿),那我们就开始学习吧!✲゚。⋆٩(◕‿◕。)۶⋆。゚✲゚*

------------------------

一、final关键字——不能被继承的类

1. 怎么实现不能被继承的类?

通过将类的构造函数设为私有使用C++11 引入的final关键字 实现,两种方式原理不同,但都能阻止继承。


1. 私有构造函数 + 静态创建(传统技巧,C++11 前常用)

  • 把类的构造函数设为 private ,外部无法直接创建对象

  • 再通过 静态成员函数 提供对象创建入口

    class NonInheritable{private:NonInheritable() // 私有构造函数,外部无法直接调用{ } NonInheritable(const NonInheritable&) = delete; // 若需要拷贝构造,也设为私有(可选) public:static NonInheritable create() // 静态函数,提供创建对象的唯一入口{return NonInheritable();}};// 错误:派生类 Sub 构造时,需调用基类 NonInheritable 的构造函数,但基类构造函数私有,无法访问class Sub : public NonInheritable{};

原理

  • C++ 规定,派生类构造时必须调用基类构造函数初始化基类部分。

  • 若基类构造函数是 private ,派生类无法访问该构造函数,编译器直接报错,达到 “禁止继承” 效果。


2. 利用 final 关键字(推荐,简洁直观)

  • 类名虚函数后加 final ,可限制继承重写

    class FinalClass final { // ... };// 错误:编译报错,无法继承 final 修饰的类class SubClass : public FinalClass {}; 

原理final 是 C++11 为限制继承设计的关键字,编译器会直接拦截派生操作,强制保证类 “不可被继承”。

------------------------

二、继承与友元

继承:用于构建类的层次关系、实现功能复用与扩展。

友元:用于突破封装、让特定 函数/类 访问私有成员。

二者的关系主要体现在 友元关系无法被继承 这一核心规则上。

  • 父类的友元,不会自动成为子类的友元 。
  • 可类比生活场景理解:“父亲的朋友,不一定是儿子的朋友” ,子类无法 “继承” 父类与其他类的友元权限。

友元关系不能继承具体分两种情况:

  1. 父类友元访问子类成员的限制
  2. 子类无法继承父类友元的权限

1. 父类友元访问子类成员的限制

父类的友元函数/类,仅能访问:

  • 父类自身的私有成员
  • 子类从父类继承的成员(因为这些成员本质属于父类的 “基因” )

无法访问子类:新增的私有/保护成员(子类自己扩展的 “独特内容”,父类友元没权限触及 )

#include using namespace std;/*---------------------定义:“基类:Base类”---------------------*/class Base{ friend class FriendClass; //声明 FriendClass 为友元类,允许其访问 Base 的私有成员private: int _baseData = 10; // 基类私有成员};/*---------------------定义:“派生类:Derived类”---------------------*/class Derived : public Base{private: int _derivedData = 20; // 派生类新增私有成员};/*---------------------定义:“友元类:FriendClass类”---------------------*/class FriendClass{public: void access(Base& b) { cout << \"访问Base::_baseData: \" << b._baseData << endl; //可访问 Base 对象的私有成员 _baseData } void access(Derived& d) { cout << \"访问Derived::_baseData (inherited): \" << d._baseData << endl; //可访问 Derived 对象中从 Base 继承的私有成员 _baseData cout << \"访问Derived::_derivedData: \" << d._derivedData << endl; //但无法访问 Derived 自身新增的私有成员 _derivedData // 错误:FriendClass 不是 Derived 的友元,无法访问 _derivedData }};int main(){ //1.创建:“基类 + 派生类 + 基类的友元类”的对象 Base b; Derived d; FriendClass fc; //2.调用友元函数访问 Base 对象的私有成员 fc.access(b); //3.调用友元函数访问 Derived 对象的私有成员 fc.access(d); // 只能访问从 Base 继承的部分,无法访问 Derived 自身新增的私有成员 return 0;}

在这里插入图片描述

2. 子类无法继承父类友元的权限

若子类想让某个类/函数访问自己的私有成员,必须自己重新声明友元 ,父类的友元关系不会 “传递” 给子类。

代码示例1

#include using namespace std;/*---------------------定义:“基类:Base类”---------------------*/class Base{ friend void friendFunc(Base& b); //声明 friendFunc 为友元函数,允许其访问 Base 的私有成员private: int _baseData = 10; // 基类私有成员};/*---------------------定义:“派生类:Derived类”---------------------*/class Derived : public Base{private: int _derivedData = 20; // 派生类新增私有成员 // friend void friendFunc(Derived& d); //注意:若不单独声明,friendFunc 无法访问 Derived 的私有成员};/*---------------------定义:“友元函数:friendFunc函数”---------------------*/void friendFunc(Base& b){ cout << \"访问Base::_baseData: \" << b._baseData << endl; //可访问 Base 对象的私有成员}// 测试函数:演示友元关系的非继承性void test(){ //1.创建派生类的对象d Derived d; //2.调用友元函数 // friendFunc(d); // 错误:friendFunc 不是 Derived 的友元,无法访问其私有成员 friendFunc(static_cast<Base&>(d)); // 正确:可将 Derived 对象隐式转换为 Base&,但只能访问 Base 部分 //注意:friendFunc 接受 Base& 参数,Derived 对象可隐式转换为 Base&}int main(){ //1.创建:“基类 + 派生类”的对象 Base b; Derived d; //2.调用友元函数访问 Base 对象的私有成员(合法) friendFunc(b); //3.调用测试函数,验证对 Derived 对象的访问限制 test(); return 0;}

在这里插入图片描述

代码示例2

#include #include using namespace std;// 前向声明class Student;/*---------------------定义:“基类:Person类”---------------------*/class Person{public:friend void Display(const Person& p, const Student& s); // 声明 Display 为友元函数,允许访问 Person 的 protected 成员protected:string _name; // 姓名};/*---------------------定义:“派生类:Student类”---------------------*/class Student : public Person{protected:int _num; // 学号};/*---------------------定义:“友元函数:Display函数”---------------------*/void Display(const Person& p, const Student& s){//1.访问 Person 的 protected 成员 _namecout << p._name << endl;//2.尝试访问 Student 的 protected 成员 _num(此处会触发编译错误)cout << s._num << endl;}int main(){//1.创建:“基类 + 派生类”的对象Person p;Student s;//2.调用友元函数,触发访问权限检查Display(p, s);return 0;}

在这里插入图片描述

------------------------

三、继承与静态成员

在 C++ 中,继承静态成员的关系主要体现在静态成员的 全局唯一性可继承性 上。

核心规则静态成员被所有派生类共享

  • 全局唯一性
    • 基类的静态成员(静态 变量/函数)在整个继承体系中只有一份实例
    • 无论派生多少个子类,所有对象共享该静态成员
  • 继承但不复制
    • 派生类会继承基类的静态成员,但不会为每个派生类单独创建副本
    • 静态成员的内存位置由基类确定,所有派生类共享同一地址

1. 所有派生类共享同一实例

代码示例1:所有派生类共享同一实例

#include using namespace std;/*---------------------定义:“基类:Base类”---------------------*/class Base{public://1.声明基类的静态变量static int s_value;};//2.初始化基类的静态变量int Base::s_value = 10;/*---------------------定义:“派生类:Derived1类”---------------------*/class Derived1 : public Base{};/*---------------------定义:“派生类:Derived2类”---------------------*/class Derived2 : public Base{};int main(){//1.输出“修改前”基类和派生类的静态变量s_value ---> 所有类共享同一静态变量cout << \"输出“修改前”基类和派生类的静态变量s_value\" << endl;cout << \"Base::s_value=\" << Base::s_value << endl;cout << \"Derived1::s_value=\" << Derived1::s_value << endl;cout << \"Derived2::s_value=\" << Derived2::s_value << endl << endl;//2.输出“修改后”基类和派生类的静态变量s_value ---> 修改静态变量会影响所有类cout << \"输出“修改后”基类和派生类的静态变量s_value\" << endl;Derived1::s_value = 20; //s_value 在内存中只有一份,无论通过基类还是派生类访问,操作的都是同一个变量。cout << \"Base::s_value=\" << Base::s_value << endl;cout << \"Derived2::s_value=\" << Derived2::s_value << endl;return 0;}

在这里插入图片描述

代码示例2:所有派生类共享同一实例

#include #include using namespace std;/*---------------------定义:“基类:Base类”---------------------*/class Person{public: string _name; //非静态成员变量的类内定义 static int _count; //静态成员变量的类内声明};//静态成员变量类外初始化int Person::_count = 0;/*---------------------定义:“派生类:Derived类”---------------------*/class Student : public Person{protected: int _stuNum;};int main(){ /*-----------------创建对象-----------------*/ //1.创建:“基类 + 派生类”的对象 Person p; Student s; /*-----------------打印验证-----------------*/ //1.验证非静态成员 _name:派生类对象和基类对象各有一份,地址不同 cout << \"验证非静态成员 _name\" << endl; cout << &p._name << endl; cout << &s._name << endl << endl; //2.验证静态成员 _count:派生类和基类共用同一份,地址相同 cout << \"验证静态成员 _count\" << endl; cout << &p._count << endl; cout << &s._count << endl << endl; /*-----------------类名访问-----------------*/ //3.公有静态成员,基类和派生类通过类作用域访问 cout << \"通过类名访问静态成员变量 _count\" << endl; cout << Person::_count << endl; cout << Student::_count << endl; return 0;}

在这里插入图片描述

2. 可通过类名直接调用

#include using namespace std;/*---------------------定义:“基类:Base类”---------------------*/class Base {public: static void print() { cout << \"Base::staticPrint()\" << endl; }};/*---------------------定义:“派生类:Derived类”---------------------*/class Derived : public Base {};int main() { //1.直接通过类名调用静态函数 Base::print(); Derived::print(); //2.也可通过对象调用(但不推荐,易混淆) Derived d; d.print(); return 0;}

在这里插入图片描述

------------------------

四、继承模型

1. 继承模型有哪些?

继承模型:指的是 面向对象编程(OOP)中,子类如何从父类继承成员(属性、方法等),以及这些成员在内存中如何布局访问规则如何生效 底层机制

  • 它决定了继承关系中数据和行为的传递、复用方式,是理解 C++ 继承特性的核心基础。

常见继承模型分类单继承模型多继承模型,二者模型差异显著。

① 单继承模型

单继承模型派生类仅从一个基类继承。

  • :这种模型结构简单、逻辑清晰,是构建类层次的核心方式。

以下从基本语法内存布局访问规则角度展开解析:


一、单继承的基本语法

/*-----------------------------语法-----------------------------*/class 基类 { // 基类成员};class 派生类 : 继承方式 基类 { // 派生类新增成员};/*-----------------------------示例-----------------------------*/class Person { /* ... */ };class Student : public Person // 单继承{ /* ... */ }; 

在这里插入图片描述


二、单继承的内存布局

  • 单继承下,派生类对象的内存布局遵循 “基类成员在前,派生类新增成员在后” 的规则。
class Base{ int _baseData; // 基类成员};class Derived : public Base{ int _derivedData; // 派生类新增成员};

内存布局示意:(逻辑上)

Derived 对象内存:+-------------------+| Base 部分 | | _baseData | // 基类成员,先存储+-------------------+| Derived 部分 || _derivedData | // 派生类新增成员,后存储+-------------------+

关键点

  • 派生类对象的起始地址与基类部分的地址相同(&d == &(d.Base部分)
  • 若基类有虚函数,对象开头会包含一个 虚函数表指针(vptr),指向该类的虚函数表

三、单继承的访问规则

单继承中,public继承基类的(public/protected/private)成员的访问,规则如下:

基类成员权限 派生类内部能否访问 类外部(通过对象)能否访问 public 能 能 protected 能 不能 private 不能(需通过基类接口) 不能

示例验证

class Base{public:int publicData;protected:int protectedData;private:int privateData;};class Derived : public Base{public:void test(){publicData = 1; // 允许:基类 public 成员protectedData = 2; // 允许:基类 protected 成员// privateData = 3; // 错误:基类 private 成员不可访问}};int main(){Derived d;d.publicData = 10; // 允许:public 成员可通过对象访问// d.protectedData = 20; // 错误:protected 成员不可通过对象访问return 0;}

② 多继承模型

多继承模型派生类同时从多个基类继承class A : public B, public C {}

  • :这种模型提供了更高的灵活性,但也引入了复杂性和潜在问题。

以下从基本语法内存布局角度展开解析:


一、多继承的基本语法

/*-----------------------------语法-----------------------------*/class 基类1 { /* ... */ };class 基类2 { /* ... */ };class 派生类 : 继承方式1 基类1, 继承方式2 基类2 { // 派生类新增成员};/*-----------------------------示例-----------------------------*/class Student{ /* 基类1 */ };class Teacher{ /* 基类2 */ };class Assistant : public Student, public Teacher { /* 派生类 */ }

在这里插入图片描述


二、多继承的内存布局

  • 多继承下,派生类对象的内存布局遵循 “按基类声明顺序排列各基类部分,最后是派生类新增成员” 的规则。
class Base1{ int _data1;};class Base2{ int _data2;};class Derived : public Base1, public Base2{ int _derivedData;};

内存布局示意:(逻辑上)

Derived 对象内存:+-------------------+| Base1 部分 | | _data1 | // 第一个基类,先存储+-------------------+| Base2 部分 || _data2 | // 第二个基类,后存储+-------------------+| Derived 部分 || _derivedData | // 派生类新增成员+-------------------+

关键点

  • 派生类对象的起始地址与第一个基类(Base1)的地址相同
  • 不同基类部分的地址可能不连续(取决于编译器优化)

菱形继承

菱形继承:是多继承体系下容易出现的一种特殊继承结构,因继承关系形似菱形而得名,会引发 数据冗余访问二义性 等问题。


一、菱形继承的基本语法

菱形继承是多继承的一种特殊情况,典型结构为:

  • 存在一个公共基类
  • 两个中间派生类,都继承自该公共基类
  • 最终有一个派生类,同时继承这两个中间派生类

此时,继承关系形成一个菱形(或钻石形)结构,示例如下:

/*-----------------------------语法-----------------------------*/class Person{ /* 公共基类 */ };class Student : public Person { /* 中间类1 */ };class Teacher : public Person { /* 中间类2 */ };class Assistant : public Student, public Teacher { /* 最终派生类 */ }

在这里插入图片描述

特别注意:上述这种结构只是菱形继承的典型表现形式,实际上,判断是否为菱形继承,并非看继承的形式一定得是严格的 “菱形” 或 “钻石形” 外观才叫菱形继承。

简单来说,只要继承结构满足下面的条件,就属于菱形继承。

  1. 存在一个公共基类被多次继承
  2. 最终派生类通过不同路径继承了同一个基类

比如说下面的这种继承结构,就是一种菱形继承,其会导致最终派生类中包含多个相同的公共基类的子对象。

在这里插入图片描述


二、菱形继承的内存布局

示例代码

class Person{public: string _name;};class Student : public Person{ /* ... */};class Teacher : public Person{ /* ... */};class Assistant : public Student, public Teacher{ /* ... */};

内存布局

Assistant 对象内存:+-------------------+| Student 部分 | | Person::_name | // 第一份 _name+-------------------+| Teacher 部分 || Person::_name | // 第二份 _name+-------------------+| Assistant 部分 |+-------------------+

在这里插入图片描述

访问歧义

Assistant ta;ta._name = \"Alice\"; // 错误:哪份 _name?Student 的还是 Teacher 的?ta.Student::_name = \"Alice\"; // 显式指定路径,可解决歧义

三、菱形继承的核心问题

1. 数据冗余

  • 最终派生类 Assistan 的对象中,会包含多份公共基类 Person 的成员
    • 比如,Person 有成员 _name
    • 那么 Assistant 对象中会通过 Student 继承一份 _name
    • 又通过 Teacher 继承一份 _name,造成内存浪费

2. 访问二义性

  • 当访问公共基类 Person 的成员时,编译器无法确定到底该访问哪一份(是 Student 继承来的,还是 Teacher 继承来的 )
class Person{public:string _name;};class Student : public Person{/* ... */};class Teacher : public Person{/* ... */};class Assistant : public Student, public Teacher{/* ... */};int main(){Assistant ta;// 错误:编译器不知道访问 Student::_name 还是 Teacher::_nameta._name = \"jack\";return 0;}

虚继承

虚继承:是 C++ 中解决多继承问题的核心机制,尤其用于处理菱形继承带来的 数据冗余访问二义性 问题。

  • 通过在派生类定义时使用 virtual 关键字,确保多个派生路径中公共基类的成员仅在最终派生类中保留一份

一、虚继承的基本语法

  • 虚继承的典型应用场景:菱形继承
class Person{public: string _name;};class Student : virtual public Person // Student 虚继承 Person{ /* ... */};class Teacher : virtual public Person // Teacher 虚继承 Person{ /* ... */};class Assistant : public Student, public Teacher // Assistant 继承 Student 和 Teacher{ /* ... */};

对比:

  • 未用虚继承时Assistant 对象包含两份 Person_name 成员,访问 ta._name 会报错(二义性)
  • 使用虚继承后Assistant 对象仅包含一份 Person_name 成员,访问 ta._name 明确且唯一

二、虚继承的底层原理

虚继承通过虚基类指针(vbptr虚基表(vbtable 实现。


1. 内存布局变化(以菱形继承为例)

假设类结构为 Assistant继承Student和Teacher,Student和Teacher虚继承Person,则各对象的内存布局:

  • Person

    • 公共基类 Person 的成员被统一存放在最终派生类 Assistant 对象内存的最下方,仅一份
  • StudentTeacher

    • 中间派生类 StudentTeacher 的对象中,会新增一个虚基类指针(vbptr,指向虚基表(vbtable
    • 虚基表中存储了当前类到公共基类 Person 成员的偏移量,通过偏移量可找到唯一的 Person 成员
  • Assistant

    • 包含 StudentTeacher 的子对象(各含一个 vbptr

示例解析(简化理解)

  • 假设 Person 有成员 _name
  • StudentTeacher 虚继承 Person
  • Assistant 继承 StudentTeacher

Assistant 对象内存布局大致为:

Assistant 对象内存:+------------------------+| Student 部分(含 vbptr) | +------------------------+| Teacher 部分(含 vbptr) | +------------------------+| Person 部分(_name) | // 仅一份+------------------------+| Assistant 新增成员 | +------------------------+

在这里插入图片描述


2. 访问虚基类成员的过程

当访问 Assistant 对象的 _name 时:

  1. Assistant 通过 StudentTeachervbptr 找到对应的虚基表
  2. 从虚基表中获取到 Person::_nameAssistant 对象中的偏移量
  3. 通过偏移量直接访问唯一的 Person::_name 成员

三、虚继承的构造顺序

虚继承会改变类构造函数的调用顺序:

  1. 虚基类的构造函数最终派生类直接调用,而非中间派生类。
  2. 构造顺序为:虚基类 → 非虚基类 → 派生类自身
#include #include  using namespace std;/*---------------------定义:“基类:Person类”---------------------*/class Person{public: Person(const string& name) : _name(name) { cout << \"Person 构造函数,name = \" << _name << endl; } string _name; // 姓名};/*---------------------定义:“中间派生类:Student类”---------------------*/class Student : virtual public Person{public: Student(const string& name) : Person(name) { cout << \"Student 构造函数\" << endl; }};/*---------------------定义:“中间派生类:Teacher类”---------------------*/class Teacher : virtual public Person{public: Teacher(const string& name) : Person(name) { cout << \"Teacher 构造函数\" << endl; }};/*---------------------定义:“最终派生类:Assistant类”---------------------*/class Assistant : public Student, public Teacher{public: Assistant(const string& name) :Person(name) ,Student(name) ,Teacher(name) { cout << \"Assistant 构造函数\" << endl; } /* 构造函数: * * 1.显式初始化虚基类 Person(这是必要的,否则会调用 Person 的默认构造函数) * 2.调用 Student 和 Teacher 的构造函数(但它们对 Person 的初始化会被忽略) */};int main(){ cout << \"=== 创建助教对象 ===\" << endl; Assistant assistant(\"张三\"); /* 创建 Assistant 对象时的构造顺序: * * 1.虚基类 Person(由 Assistant 直接初始化) * 2.非虚基类 Student(其对 Person 的初始化被忽略) * 3.非虚基类 Teacher(其对 Person 的初始化被忽略) * 4.Assistant 自身 */ cout << \"\\n=== 访问姓名信息 ===\" << endl; cout << \"姓名: \" << assistant._name << endl; // 由于虚继承,_name 仅存在一份实例,无需指定作用域,直接访问 cout << \"学生姓名: \" << assistant.Student::_name << endl; cout << \"教师姓名: \" << assistant.Teacher::_name << endl; // 以上两种方式通过作用域限定符访问,但实际上指向同一内存位置 cout << \"\\n=== 程序结束 ===\" << endl; return 0;}

在这里插入图片描述

:若最终派生类未显式调用虚基类构造函数,编译器会自动调用其默认构造函数。

#include #include  using namespace std;/*---------------------定义:“公共基类:Person类”---------------------*/class Person {public: //1.实现:“构造函数”---> 用 C 风格字符串初始化姓名 Person(const char* name) : _name(name) // 初始化列表初始化成员 _name { cout << \"Person 构造函数调用,姓名:\" << _name << endl; } string _name; // 姓名};/*---------------------定义:“中间派生类:Student类”---------------------*/class Student : virtual public Person {public: //1.实现:“构造函数”---> 初始化 Person 基类、学号 Student(const char* name, int num) : Person(name) // 调用 Person 构造函数初始化从公共基类继承的部分 , _num(num) // 初始化学号成员 { cout << \"Student 构造函数调用,学号:\" << _num << endl; }protected: int _num; // 学号};/*---------------------定义:“中间派生类:Teacher类”---------------------*/class Teacher : virtual public Person {public: //1.实现:“构造函数”---> 初始化 Person 基类、职工编号 Teacher(const char* name, int id) : Person(name) // 调用 Person 构造函数初始化公共基类部分 , _id(id) // 初始化职工编号成员 { cout << \"Teacher 构造函数调用,职工编号:\" << _id << endl; }protected: int _id; // 职工编号};/*---------------------定义:“最终派生类:Assistant类”---------------------*/class Assistant : public Student, public Teacher {public: //1.实现:“构造函数”---> 需显式初始化公共基类 Person,再初始化 Student、Teacher Assistant(const char* name1, const char* name2, const char* name3) : Person(name3) // 直接初始化公共基类 Person,这是虚继承的关键要求 , Student(name1, 1) // 调用 Student 构造函数,学号固定传 1(示例逻辑) , Teacher(name2, 2) // 调用 Teacher 构造函数,职工编号固定传 2(示例逻辑) { cout << \"Assistant 构造函数调用\" << endl; }protected: string _majorCourse; // 主修课程};int main() { cout << \"----------创建 Assistant 对象:----------\" << endl; Assistant a(\"张三\", \"李四\", \"王五\"); cout << \"----------打印Assistant 对象中 Person 部分的姓名:----------\" << endl; cout << a._name << endl; //注意:由于虚继承,Person 的 _name 由 Assistant 构造函数中 Person(name3) 决定 //所以 _name 的值是 \"王五\" return 0;}

在这里插入图片描述


虚继承与非虚继承的对比:

特性 非虚继承(普通继承) 虚继承 基类成员数量 每个派生路径均保留一份基类成员 最终派生类仅保留一份基类成员 二义性问题 存在(如:菱形继承) 解决(仅一份基类成员) 构造函数调用 由直接派生类调用基类构造函数 由最终派生类直接调用虚基类构造函数 内存布局 简单(无虚基类指针) 复杂(含虚基类指针和虚基表) 适用场景 单继承或无公共基类的多继承 菱形继承或需要共享基类成员的场景

2. IO库中的虚继承长什么样?

在这里插入图片描述

在 C++ 标准库的 IO 类模板继承体系里:

  • 我们能直观看到:大部分类采用 单继承 设计
  • basic_iostream 是特殊的 —— 它多继承basic_ostreambasic_istream

那我们都知道的一件事情就是:当一个继承关系中先是进行一些单继承,然后又进行了一个多继承的情况的话,就会出现:菱形继承的问题

那下面我们就来看一看,标准的IO库是怎么解决菱形继承问题的!!!

//basic_ostream 类模板:表示基本输出流template<class CharT, class Traits = std::char_traits<CharT>>class basic_ostream : virtual public std::basic_ios<CharT, Traits>{};//basic_istream 类模板:表示基本输入流template<class CharT, class Traits = std::char_traits<CharT>>class basic_istream : virtual public std::basic_ios<CharT, Traits>{};/* 注意事项:*1.CharT 表示字符类型(如:char、wchar_t 等)*2.Traits 表示字符特性,默认使用标准库的 char_traits,用于提供字符相关的基本操作(如:比较、复制等)** 作用:*1.虚继承自 basic_ios,目的是在多重继承场景下(如:basic_iostream)避免基类 basic_ios 的成员重复*2.同样虚继承自 basic_ios,和 basic_ostream 配合解决多重继承时的基类成员冗余问题*/

3. 关于多继承中指针偏移的一道面试题?

关于上面的代码,下面说法正确的是( )

A. p1 == p2 == p3 B. p1 < p2 < p3

C. p1 == p3 != p2 D. p1 != p2 != p3

#include  using namespace std; class Base1 {public: int _b1;};class Base2 {public: int _b2;};class Derive : public Base1, public Base2 {public: int _d;};int main(){ // 创建 Derive 类的对象 d,该对象包含 Base1、Base2 以及自身的成员 Derive d; // 定义 Base1 类型指针 p1,指向 Derive 对象 d 中从 Base1 继承的部分 // 因为 Derive 公有继承 Base1,所以可以安全地将 Derive 对象指针转换为 Base1 指针 Base1* p1 = &d; // 定义 Base2 类型指针 p2,指向 Derive 对象 d 中从 Base2 继承的部分 // 同理,Derive 公有继承 Base2,可转换为 Base2 指针 Base2* p2 = &d; // 定义 Derive 类型指针 p3,直接指向 Derive 对象 d 的起始地址 Derive* p3 = &d; return 0;}

解析:

在多继承场景中,派生类 Derive 的对象 d 内存布局会包含:

  • 基类 Base1Base2 的成员
  • 以及自身成员

大致如下(简化示意 ):

Derive 对象 d 的内存:+-------------------+| Base1 部分 | // 包含 _b1+-------------------+| Base2 部分 | // 包含 _b2+-------------------+| Derive 自身 _d |+-------------------+
  • p1
    • Base1* 类型指针,指向 dBase1 部分的起始地址
    • d 的起始地址相同(因为 Base1 是第一个基类 )
  • p2
    • Base2* 类型指针,指向 dBase2 部分的起始地址
    • 由于 Base2 排在 Base1 之后,其地址比 d 的起始地址大一个 Base1 的大小(即:偏移了 sizeof(Base1)
  • p3Derive* 类型指针,指向 d起始地址

因此

  • p1p3 地址相同(都指向 d 起始 )
  • p2 因偏移,地址与 p1p3 不同

在这里插入图片描述

答案C

------------------------

五、继承和组合

1. 什么是继承/组合?

在 C++ 面向对象设计中,继承(Inheritance)组合(Composition) 是实现代码复用、构建复杂类结构的两种核心手段。

  • 它们各有特点,适用场景不同,理解二者关系对设计灵活、可维护的程序至关重要。

1. 继承(“是一个” 关系,is-a )

  • 含义派生类(子类)直接继承基类(父类)的成员(属性、方法),可复用基类逻辑并扩展新功能。

  • 关系Derived 是一个 BaseStudent 是一个 Person

  • 语法

    class Base{/* 基类成员 */};class Derived : public Base{/* 派生类成员,可复用 Base 的成员 */};

2. 组合(“有一个” 关系,has-a )

  • 含义一个类(宿主类)通过包含其他类的对象来复用功能,被包含的类(成员对象)是宿主类的 “组件”。

  • 关系Host 有一个 ComponentCar 有一个 Engine

  • 语法

    class Component{/* 组件类成员 */};class Host{Component comp; // 组合 Component 对象};

2. 继承和组合的区别是什么?

继承与组合的两种复用模式对比

继承:白箱复用

继承的核心是基于基类实现派生出新类,让派生类复用基类的功能。这种复用模式被称为 “白箱复用”,关键特点是:

  • 从 “可见性” 角度,基类的内部细节(如:protected 成员、实现逻辑 )对派生类是 “透明可见” 的。
  • 继承一定程度上破坏了基类的封装性:若基类的实现细节(如:成员变量、函数逻辑 )发生改变,很可能直接影响派生类的行为,甚至导致编译或运行错误。
  • 最终表现为 派生类与基类的依赖关系极强,耦合度很高—— 基类的修改会 “牵一发而动全身”,增加了代码维护的风险。

组合:黑箱复用

组合的核心是通过 “组装 / 组合” 已有对象,构建出更复杂的功能。这种复用模式被称为 “黑箱复用”,关键特点是:

  • 被组合的对象(成员对象)仅需暴露清晰、稳定的接口,其内部实现细节对组合类是 “不可见” 的(类似 “黑箱” )。
  • 组合类与成员对象之间依赖关系弱,耦合度低:只要成员对象的接口不变,组合类无需关心其内部逻辑如何修改,也不会被成员对象的变化影响。
  • 由于组合严格依赖 “接口” 而非 “实现”,它天然有助于保持每个类的封装性,让代码更易维护、扩展。
特性 继承(Inheritance) 组合(Composition) 复用方式 直接继承基类的成员,派生类与基类强耦合 包含其他类的对象,宿主类与成员对象弱耦合 关系语义 is-a(派生类是基类的特殊化) has-a(宿主类包含成员对象作为组件) 成员访问 派生类可直接访问基类的 protected 成员 宿主类需通过成员对象的接口访问其成员 生命周期 派生类对象创建时,基类子对象先构造 成员对象的生命周期由宿主类对象管理

继承和组合的优缺点:

1. 继承的优缺点

  • 优点
    • 直接复用逻辑:无需额外代码,派生类可直接使用基类的属性和方法
    • 支持多态:通过虚函数,派生类可重写基类行为,实现运行时多态
  • 缺点
    • 强耦合:派生类依赖基类的实现细节,基类修改可能破坏派生类
    • 菱形继承问题:多继承易导致成员冗余、访问二义性(需虚继承解决)

2. 组合的优缺点

  • 优点
    • 弱耦合:宿主类与成员对象接口解耦,成员对象修改不影响宿主类
    • 灵活复用:可动态替换成员对象(若用指针/引用),适配不同场景
    • 避免菱形继承:不涉及继承层次,天然无多继承的复杂问题
  • 缺点
    • 间接访问:需通过成员对象的接口访问其功能,代码可能更繁琐
    • 不支持多态:默认无法直接重写成员对象的行为(需结合指针 + 多态实现)

3. 继承和组合怎么进行选择?

在设计模式中,“组合优于继承”(Composition over Inheritance) 是重要原则,核心思想是:优先用组合实现复用,减少继承带来的强耦合。


继承与组合的选择依据:

  1. 关系判断
    • 若类间是 is-a 关系(StudentPerson ),优先用继承
    • 若类间是 has-a 关系(CarEngine ),优先用组合
  2. 耦合与维护
    • 需强复用基类逻辑且基类稳定时,继承更简洁
    • 需解耦、动态替换功能时,组合更灵活
  3. 多态需求
    • 需通过虚函数重写实现多态时,继承是直接方案
    • 组合也可结合接口 + 多态实现,但稍复杂

总结:

  • 实际开发中,应遵循 “组合优于继承” 原则,优先用组合降低耦合
  • 仅在明确 is-a 关系且需多态时,合理使用继承

二者并非互斥,复杂类设计中常结合使用(:继承实现接口,组合实现功能复用 )

4. 继承和组合的使用案例

代码案例1:STL中的stack容器适配器的实现方式

// 以下演示 stack 与 vector 的两种关系(实际标准库中 stack 通常用组合,这里对比说明)template<class T>class vector { };// 错误示范:stack 公有继承 vector,强行让 stack \"是一个\" vector(is-a)// 但 stack 语义上更适合 \"有一个\" vector(has-a),此写法会暴露 vector 所有接口,不符合栈的设计template<class T>class stack : public vector<T> { };// 正确示范:stack 组合 vector,体现 has-a 关系(stack \"有一个\" vector 作为底层容器)template<class T>class stack {public: vector<T> _v; // 组合 vector 对象,stack 通过 _v 实现底层存储};

代码案例2:汽车的继承和组合

#include  #include  #include  using namespace std; /*---------------------定义:“基类:Tire类”---------------------*/class Tire //注:后续会被 Car 组合,体现 (has-a 关系){protected: string _brand = \"Michelin\"; // 轮胎品牌,默认米其林 size_t _size = 17;  // 轮胎尺寸,默认 17 寸};/*---------------------定义:“基类:Car类”---------------------*/class Car //注:后续被 BMW、Benz 继承,体现(is-a 关系){protected: string _colour = \"白色\"; // 车颜色,默认白色 string _num = \"京00001\"; // 车牌号,默认京00001 Tire _t1; // 第一个轮胎 ---> 组合轮胎对象,体现 Car \"有一个\" Tire(has-a 关系) Tire _t2; // 第二个轮胎 Tire _t3; // 第三个轮胎 Tire _t4; // 第四个轮胎};/*---------------------定义:“派生类:BMW类”---------------------*/class BMW : public Car //注:公有继承 Car 类,体现 is-a 关系(BMW \"是一个\" Car){public: void Drive() { cout << \"好开-操控\" << endl; // BMW 车型的驾驶体验描述 }};/*---------------------定义:“派生类:Benz类”---------------------*/class Benz : public Car //注:公有继承 Car 类,体现 is-a 关系(Benz \"是一个\" Car){public: void Drive() { cout << \"好坐-舒适\" << endl; // Benz 车型的驾驶体验描述 }};int main() { BMW bmwCar; bmwCar.Drive(); // 调用 BMW 的 Drive 方法 return 0;}

在这里插入图片描述

在这里插入图片描述