《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. 父类友元访问子类成员的限制
父类的友元函数/类,仅能访问:
父类自身的私有成员
子类从父类继承的成员
(因为这些成员本质属于父类的 “基因” )
无法访问子类:新增的私有/保护成员
(子类自己扩展的 “独特内容”,父类友元没权限触及 )
#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
{ /* 最终派生类 */
}
特别注意:上述这种结构只是菱形继承的典型表现形式,实际上,判断是否为菱形继承,并非看继承的形式一定得是严格的 “菱形” 或 “钻石形” 外观才叫菱形继承。
简单来说,只要继承结构满足下面的条件,就属于菱形继承。
存在一个公共基类被多次继承
最终派生类通过不同路径继承了同一个基类
比如说下面的这种继承结构,就是一种菱形继承,其会导致最终派生类中包含多个相同的公共基类的子对象。
二、菱形继承的内存布局
示例代码
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
对象内存的最下方,仅一份
Student
和Teacher
类:
- 中间派生类
Student
、Teacher
的对象中,会新增一个虚基类指针(vbptr
),指向虚基表(vbtable
)- 虚基表中存储了当前类到公共基类
Person
成员的偏移量,通过偏移量可找到唯一的Person
成员
Assistant
类:
- 包含
Student
和Teacher
的子对象(各含一个vbptr
)
示例解析(简化理解)
- 假设
Person
有成员_name
Student
、Teacher
虚继承Person
Assistant
继承Student
、Teacher
则
Assistant
对象内存布局大致为:Assistant 对象内存: +------------------------+ | Student 部分(含 vbptr) | +------------------------+ | Teacher 部分(含 vbptr) | +------------------------+ | Person 部分(_name) | // 仅一份 +------------------------+ | Assistant 新增成员 | +------------------------+
2. 访问虚基类成员的过程
当访问
Assistant
对象的_name
时:
Assistant
通过Student
或Teacher
的vbptr
找到对应的虚基表- 从虚基表中获取到
Person::_name
在Assistant
对象中的偏移量- 通过偏移量直接访问唯一的
Person::_name
成员
三、虚继承的构造顺序
虚继承会改变类构造函数的调用顺序:
- 虚基类的构造函数由最终派生类直接调用,而非中间派生类。
- 构造顺序为:虚基类 → 非虚基类 → 派生类自身
#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_ostream
和basic_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
内存布局会包含:
- 基类
Base1
、Base2
的成员- 以及自身成员
大致如下(简化示意 ):
Derive 对象 d 的内存: +-------------------+ | Base1 部分 | // 包含 _b1 +-------------------+ | Base2 部分 | // 包含 _b2 +-------------------+ | Derive 自身 _d | +-------------------+
p1
:
Base1*
类型指针,指向d
中Base1
部分的起始地址- 与
d
的起始地址相同(因为Base1
是第一个基类 )p2
:
Base2*
类型指针,指向d
中Base2
部分的起始地址- 由于
Base2
排在Base1
之后,其地址比d
的起始地址大一个Base1
的大小(即:偏移了sizeof(Base1)
)p3
:Derive*
类型指针,指向d
的起始地址
因此:
p1
和p3
地址相同(都指向d
起始 )p2
因偏移,地址与p1
、p3
不同
答案【C】
------------------------
五、继承和组合
1. 什么是继承/组合?
在 C++ 面向对象设计中,继承(Inheritance) 和 组合(Composition) 是实现代码复用、构建复杂类结构的两种核心手段。
- 它们各有特点,适用场景不同,理解二者关系对设计灵活、可维护的程序至关重要。
1. 继承(“是一个” 关系,is-a )
含义:派生类(子类)直接继承基类(父类)的成员(属性、方法),可复用基类逻辑并扩展新功能。
关系:
Derived
是一个Base
(如:Student
是一个Person
)语法:
class Base {/* 基类成员 */ };class Derived : public Base {/* 派生类成员,可复用 Base 的成员 */ };
2. 组合(“有一个” 关系,has-a )
含义:一个类(宿主类)通过包含其他类的对象来复用功能,被包含的类(成员对象)是宿主类的 “组件”。
关系:
Host
有一个Component
(如:Car
有一个Engine
)语法:
class Component {/* 组件类成员 */ };class Host {Component comp; // 组合 Component 对象 };
2. 继承和组合的区别是什么?
继承与组合的两种复用模式对比
继承:白箱复用
继承的核心是:基于基类实现派生出新类,让派生类复用基类的功能。这种复用模式被称为
“白箱复用”
,关键特点是:
- 从 “可见性” 角度,基类的内部细节(如:
protected
成员、实现逻辑 )对派生类是 “透明可见” 的。- 继承一定程度上破坏了基类的封装性:若基类的实现细节(如:成员变量、函数逻辑 )发生改变,很可能直接影响派生类的行为,甚至导致编译或运行错误。
- 最终表现为 派生类与基类的依赖关系极强,耦合度很高—— 基类的修改会 “牵一发而动全身”,增加了代码维护的风险。
组合:黑箱复用
组合的核心是:通过 “组装 / 组合” 已有对象,构建出更复杂的功能。这种复用模式被称为
“黑箱复用”
,关键特点是:
- 被组合的对象(成员对象)仅需暴露清晰、稳定的接口,其内部实现细节对组合类是 “不可见” 的(类似 “黑箱” )。
- 组合类与成员对象之间依赖关系弱,耦合度低:只要成员对象的接口不变,组合类无需关心其内部逻辑如何修改,也不会被成员对象的变化影响。
- 由于组合严格依赖 “接口” 而非 “实现”,它天然有助于保持每个类的封装性,让代码更易维护、扩展。
特性 | 继承(Inheritance) | 组合(Composition) |
---|---|---|
复用方式 | 直接继承基类的成员,派生类与基类强耦合 | 包含其他类的对象,宿主类与成员对象弱耦合 |
关系语义 | is-a (派生类是基类的特殊化) | has-a (宿主类包含成员对象作为组件) |
成员访问 | 派生类可直接访问基类的 protected 成员 | 宿主类需通过成员对象的接口访问其成员 |
生命周期 | 派生类对象创建时,基类子对象先构造 | 成员对象的生命周期由宿主类对象管理 |
继承和组合的优缺点:
1. 继承的优缺点
- 优点:
直接复用逻辑
:无需额外代码,派生类可直接使用基类的属性和方法支持多态
:通过虚函数,派生类可重写基类行为,实现运行时多态- 缺点:
强耦合
:派生类依赖基类的实现细节,基类修改可能破坏派生类菱形继承问题
:多继承易导致成员冗余、访问二义性(需虚继承解决)
2. 组合的优缺点
- 优点:
弱耦合
:宿主类与成员对象接口解耦,成员对象修改不影响宿主类灵活复用
:可动态替换成员对象(若用指针/引用),适配不同场景避免菱形继承
:不涉及继承层次,天然无多继承的复杂问题- 缺点:
间接访问
:需通过成员对象的接口访问其功能,代码可能更繁琐不支持多态
:默认无法直接重写成员对象的行为(需结合指针 + 多态实现)
3. 继承和组合怎么进行选择?
在设计模式中,“组合优于继承”(Composition over Inheritance) 是重要原则,核心思想是:优先用组合实现复用,减少继承带来的强耦合。
继承与组合的选择依据:
关系判断
:
- 若类间是
is-a
关系(如:Student
是Person
),优先用继承
- 若类间是
has-a
关系(如:Car
有Engine
),优先用组合
耦合与维护
:
- 需强复用基类逻辑且基类稳定时,继承更简洁
- 需解耦、动态替换功能时,组合更灵活
多态需求
:
- 需通过虚函数重写实现多态时,继承是直接方案
- 组合也可结合接口 + 多态实现,但稍复杂
总结:
- 实际开发中,应遵循 “组合优于继承” 原则,优先用组合降低耦合
- 仅在明确
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;
}