当前位置: 首页 > news >正文

《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 <iostream>
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 继承的私有成员 _baseDatacout << "访问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 <iostream>
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.创建派生类的对象dDerived 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 <iostream>
#include <string>
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 <iostream>
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 <iostream>
#include <string>
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 <iostream>
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 <iostream>
#include <string>  
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 <iostream>
#include <string>  
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、TeacherAssistant(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<CharT, Traits>,目的是在多重继承场景下(如:basic_iostream)避免基类 basic_ios 的成员重复
*		2.同样虚继承自 basic_ios<CharT, Traits>,和 basic_ostream 配合解决多重继承时的基类成员冗余问题
*/

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

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

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

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

#include <iostream>  
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 <iostream> 
#include <string>    
#include <vector>   
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";  // 车牌号,默认京00001Tire _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;
}

在这里插入图片描述

在这里插入图片描述

http://www.lryc.cn/news/619284.html

相关文章:

  • HTML第三次作业
  • 腾讯位置商业授权微信小程序关键词输入提示
  • Flink DataStream 按分钟或日期统计数据量
  • 深度学习——03 神经网络(3)-网络优化方法
  • 基于Apache Flink的实时数据处理架构设计与高可用性实战经验分享
  • 搜索引擎核心机制解析
  • 美团搜索推荐统一Agent之性能优化与系统集成
  • 云计算-OpenStack 实战运维:从组件配置到故障排查(含 RAID、模板、存储管理,网络、存储、镜像、容器等)
  • Flink中的窗口
  • HTML5 Canvas实现数组时钟代码,适用于wordpress侧边栏显示
  • 方法论基础。
  • 设计秒杀系统从哪些方面考虑
  • 从零开始:用PyTorch实现线性回归模型
  • 比特币与区块链:去中心化的技术革命
  • VUE2连接USB打印机
  • Pytorch FSDP权重分片保存与合并
  • 【C语言强化训练16天】--从基础到进阶的蜕变之旅:Day3
  • 【Qt开发】常用控件(三) -> geometry
  • 疏老师-python训练营-Day44预训练模型
  • php7 太空船运算符
  • Linux 软件编程:文件IO、目录IO、时间函数
  • 适配安卓15(对应的sdk是35)
  • RxJava 在 Android 中的深入解析:使用、原理与最佳实践
  • 大牌点餐接口api对接全流程
  • 《吃透 C++ 类和对象(中):构造函数与析构函数的核心逻辑》
  • Ubuntu22.04轻松安装Qt与OpenCV库
  • 药房智能盘库系统的Python编程分析与实现—基于计算机视觉与时间序列预测的智能库存管理方案
  • 基于大数据spark的医用消耗选品采集数据可视化分析系统【Hadoop、spark、python】
  • 分段锁和限流的间接实现
  • 通信中间件 Fast DDS(一) :编译、安装和测试