《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 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 { /* 最终派生类 */ }

特别注意:上述这种结构只是菱形继承的典型表现形式,实际上,判断是否为菱形继承,并非看继承的形式一定得是严格的 “菱形” 或 “钻石形” 外观才叫菱形继承。
简单来说,只要继承结构满足下面的条件,就属于菱形继承。
存在一个公共基类被多次继承最终派生类通过不同路径继承了同一个基类
比如说下面的这种继承结构,就是一种菱形继承,其会导致最终派生类中包含多个相同的公共基类的子对象。

二、菱形继承的内存布局
示例代码
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有成员_nameStudent、Teacher虚继承PersonAssistant继承Student、Teacher则
Assistant对象内存布局大致为:Assistant 对象内存:+------------------------+| Student 部分(含 vbptr) | +------------------------+| Teacher 部分(含 vbptr) | +------------------------+| Person 部分(_name) | // 仅一份+------------------------+| Assistant 新增成员 | +------------------------+
2. 访问虚基类成员的过程
当访问
Assistant对象的_name时:
Assistant通过Student或Teacher的vbptr找到对应的虚基表- 从虚基表中获取到
Person::_name在Assistant对象中的偏移量- 通过偏移量直接访问唯一的
Person::_name成员
三、虚继承的构造顺序
虚继承会改变类构造函数的调用顺序:
- 虚基类的构造函数由最终派生类直接调用,而非中间派生类。
- 构造顺序为:虚基类 → 非虚基类 → 派生类自身
#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_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,目的是在多重继承场景下(如:basic_iostream)避免基类 basic_ios 的成员重复*2.同样虚继承自 basic_ios,和 basic_ostream 配合解决多重继承时的基类成员冗余问题*/
3. 关于多继承中指针偏移的一道面试题?
关于上面的代码,下面说法正确的是( )
A.
p1 == p2 == p3B.p1 < p2 < p3C.
p1 == p3 != p2D.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内存布局会包含:
- 基类
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成员、实现逻辑 )对派生类是 “透明可见” 的。- 继承一定程度上破坏了基类的封装性:若基类的实现细节(如:成员变量、函数逻辑 )发生改变,很可能直接影响派生类的行为,甚至导致编译或运行错误。
- 最终表现为 派生类与基类的依赖关系极强,耦合度很高—— 基类的修改会 “牵一发而动全身”,增加了代码维护的风险。
组合:黑箱复用
组合的核心是:通过 “组装 / 组合” 已有对象,构建出更复杂的功能。这种复用模式被称为
“黑箱复用”,关键特点是:
- 被组合的对象(成员对象)仅需暴露清晰、稳定的接口,其内部实现细节对组合类是 “不可见” 的(类似 “黑箱” )。
- 组合类与成员对象之间依赖关系弱,耦合度低:只要成员对象的接口不变,组合类无需关心其内部逻辑如何修改,也不会被成员对象的变化影响。
- 由于组合严格依赖 “接口” 而非 “实现”,它天然有助于保持每个类的封装性,让代码更易维护、扩展。
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 #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;}






