【C++进阶】Cyber骇客的赛博血统上传——【面向对象之 继承 】一文带你搞懂面向对象编程的三要素之————继承

⚡ CYBER_PROFILE ⚡
/// SYSTEM READY ///


WARNING \]: DETECTING HIGH ENERGY **🌊 🌉 🌊 心手合一 · 水到渠成** ![分隔符](https://i-blog.csdnimg.cn/direct/60a3de2294e9439abad47378e657b337.gif) |------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------| | **\>\>\> ACCESS TERMINAL \<\<\<** || | [**\[ 🦾 作者主页 \]**](https://blog.csdn.net/fengtinghuqu520?spm=1000.2115.3001.5343) | [**\[ 🔥 C语言核心 \]**](https://blog.csdn.net/fengtinghuqu520/category_12955956.html) | | [**\[ 💾 编程百度 \]**](https://blog.csdn.net/fengtinghuqu520/category_13083835.html) | [**\[ 📡 代码仓库 \]**](https://blog.csdn.net/fengtinghuqu520/article/details/147275999?spm=1001.2014.3001.5502) | --------------------------------------- Running Process: 100% \| Latency: 0ms *** ** * ** *** #### 索引与导读 * [1. 继承的核心概念](#1. 继承的核心概念) * [2. 继承的基本语法](#2. 继承的基本语法) * * [2)继承方式(权限继承矩阵表)](#2)继承方式(权限继承矩阵表)) * [3. 继承与模板的结合](#3. 继承与模板的结合) * * [3.1)类 继承 类模板](#3.1)类 继承 类模板) * [3.2)类模板 继承 类模板](#3.2)类模板 继承 类模板) * [3.3)模板继承的语法坑](#3.3)模板继承的语法坑) * * [如何破解?(三大解法)](#如何破解?(三大解法)) * [4. 基类 和 派生类 之间的转换](#4. 基类 和 派生类 之间的转换) * * [4.1)普通类型转换:必须加 const](#4.1)普通类型转换:必须加 const) * [4.2)继承体系的特殊处理:不需要 const](#4.2)继承体系的特殊处理:不需要 const) * [4.3)对象切片](#4.3)对象切片) * [4.4)基类绝对不能赋值给派生类](#4.4)基类绝对不能赋值给派生类) * [4.5)向下转型的安全隐患与 RTTI](#4.5)向下转型的安全隐患与 RTTI) * * [1)强制类型转换(极其危险)](#1)强制类型转换(极其危险)) * [2)安全的向下转型(dynamic_cast 安全转换)](#2)安全的向下转型(dynamic_cast 安全转换)) * [总结](#总结) * [5. 继承中的作用域](#5. 继承中的作用域) * * [5.1)基类和派生类都有独立的作用域](#5.1)基类和派生类都有独立的作用域) * [5.2)同名成员的"隐藏"与 基类:: 显式访问](#5.2)同名成员的“隐藏”与 基类:: 显式访问) * [5.3)基类与派生类中关于 函数重载 的潜规则](#5.3)基类与派生类中关于 函数重载 的潜规则) * [6. 派生类的默认成员函数](#6. 派生类的默认成员函数) * * [6.1)❗我们写这几个默认成员函数,首先思考几个原则性的问题](#6.1)❗我们写这几个默认成员函数,首先思考几个原则性的问题) * [6.2)代码级剖析](#6.2)代码级剖析) * [6.3)图表解析](#6.3)图表解析) * [7. 实现一个不能被继承的类](#7. 实现一个不能被继承的类) * * [方法 1:利用构造函数的私有性(C++98 时代的"黑科技")](#方法 1:利用构造函数的私有性(C++98 时代的“黑科技”)) * [方法 2:使用 final 关键字(C++11 现代标准,最推荐)](#方法 2:使用 final 关键字(C++11 现代标准,最推荐)) * [8. 继承与友元](#8. 继承与友元) * [9. 继承与静态成员](#9. 继承与静态成员) * [⭕10. 单继承、多继承 及其 菱形继承问题](#⭕10. 单继承、多继承 及其 菱形继承问题) * * [10.1)单继承](#10.1)单继承) * [10.2)多继承](#10.2)多继承) * [10.3)菱形继承问题](#10.3)菱形继承问题) * [10.4)虚继承](#10.4)虚继承) * [10.5)多继承中的指针偏移问题](#10.5)多继承中的指针偏移问题) * [11. 继承与组合](#11. 继承与组合) * * [为什么优先使用组合(Has-A)?](#为什么优先使用组合(Has-A)?) * [什么时候必须用继承](#什么时候必须用继承) * [💻结尾--- 核心连接协议](#💻结尾— 核心连接协议) ## 1. 继承的核心概念 在`C++`中,继承是面向对象编程`(OOP)`的**三大基石之一(封装、继承、多态)** * **基类 (Base Class / Parent Class)**:被继承的类,包含通用的属性和方法。 * **派生类 (Derived Class / Child Class)**:继承基类的类,可以添加新成员或修改(重写)基类的行为。 *** ** * ** *** ## 2. 继承的基本语法 ```cpp class Base { /* ... */ }; class Derived : [继承方式] Base { /* ... */ }; ``` ### 2)继承方式(权限继承矩阵表) 无论哪种继承方式,基类的 `private` 成员在派生类中**永远不可见**(不可直接访问) * **继承方式改变的是基类的 `public` 和 `protected` 成员在派生类中的"降级"规则:** | 继承方式 | 基类成员访问级别 | 在派生类中的访问级别 | |------------------|-------------|-----------------| | **public 继承** | `public` | 保持为 `public` | | | `protected` | 保持为 `protected` | | | `private` | 不可见(存在但无法直接访问) | | **protected 继承** | `public` | 降级为 `protected` | | | `protected` | 降级为 `protected` | | | `private` | 不可见(存在但无法直接访问) | | **private 继承** | `public` | 降级为 `private` | | | `protected` | 降级为 `private` | | | `private` | 不可见(存在但无法直接访问) | *** ** * ** *** ## 3. 继承与模板的结合 ### 3.1)类 继承 类模板 基类是一个模板,但派生类是一个普通的类 * **核心逻辑:** 在继承时,你必须给基类添加具体的类型(实例化) ```cpp template class Base { public: T data; void print() { cout << "Base" << endl; } }; // 派生类是普通类,但必须把基类模板的具体类型写死(比如 int) class Derived : public Base { public: void doSomething() { data = 10; // 这里的 data 已经被确定为 int 类型 print(); } }; ``` **总结:** 编译器把 Base 当作一个已经完全确定的类来对待 *** ** * ** *** ### 3.2)类模板 继承 类模板 * **核心逻辑:** 派生类本身也是一个模板。派生类需要接收类型参数,并且通常要把这个参数继续传递给基类 ```cpp template class Base { public: T value; }; template class Derived :public Base { public: T getValue() { return this->value; // 稍后解释为什么这里推荐加 this-> } }; Derived d; ``` `return this->value;` ------------ **稍后解释为什么这里推荐加 `this->`** *** ** * ** *** ### 3.3)模板继承的语法坑 **在普通的继承中** ,子类可以直接使用父类的 public 和 protected 成员 但在模板继承中,如果你直接使用父类的成员变量或函数,编译器会无情地报错:"**找不到该标识符**"! ```cpp template class Base { public: void foo() {} }; template class Derived : public Base { public: void bar() { foo(); // ❌ 编译报错!找不到 foo()! } }; ``` **为什么会这样?(底层原理)** `C++` 编译器在解析模板时使用**两阶段名字查找** 1. **第一阶段(定义时)** :编译器检查模板的语法。此时,它不知道 `T` 是什么,因此它也不知道 `Base` 到底是个什么东西(因为你以后可能会写一个针对特定类型的模板特化,而那个特化版本里可能根本没有 `foo()` 函数)。所以,编译器拒绝去 `Base` 里查找名字 2. **第二阶段(实例化时)** :只有当你真正写出 `Derived d;` 时,编译器才开始把具体的类型代入 *** ** * ** *** #### 如何破解?(三大解法) 为了在第一阶段安抚编译器,你需要显式地告诉编译器:"这个名字不是凭空出现的,它依赖于模板参数,去父类里找!" 1. **使用 `this->`(最推荐、最优雅):** ```cpp void bar() { this->foo(); } // 告诉编译器,foo 是属于当前对象(派生自 Base)的成员函数 ``` 2. **使用 `Base::` 作用域解析:** ```cpp void bar() { Base::foo(); } // 显式指明是从父类来的 // ▲ 警告:如果 foo() 是虚函数,这种写法会关闭多态机制(静态绑定),要小心使用! ``` 3. **使用 `using` 声明** ```cpp using Base::foo; void bar() { foo(); } ``` *** ** * ** *** ## 4. 基类 和 派生类 之间的转换 * 我们先定义一个基础的继承体系,作为后面所有代码的演示平台: ```cpp #include #include using namespace std; // 基类 class Base { public: int base_data = 10; virtual ~Base() = default; // 为了支持后面的 dynamic_cast }; // 派生类 class Derived : public Base { public: int derived_data = 20; }; ``` `virtual ~Base() = default;` 只要你的类打算被别人继承,它的析构函数就必须是 `virtual` 的 这个我们后期会讲虚函数的作用 *** ** * ** *** ### 4.1)普通类型转换:必须加 const ```cpp int a = 1; // double& d = a; // ❌ 编译报错! const double& d = a; // ✅ 正确。因为 int 转 double 会生成一个匿名的 double 临时变量(右值) ``` **临时变量为常量** *** ** * ** *** ### 4.2)继承体系的特殊处理:不需要 const ```cpp Derived derived_obj; // ✅ 正确且安全!没有产生任何临时对象! Base& base_ref = derived_obj; Base* base_ptr = &derived_obj; // 验证:它们操作的是同一块内存 base_ref.base_data = 999; cout << derived_obj.base_data << endl; // 输出 999 ``` 当编译器看到 `Base& = derived_obj` 时,它没有去创建一个新的 `Base` 临时对象。**它只是在底层做了一个指针偏移,让 `base_ref `直接指向 `derived_obj` 内存布局中属于 `Base` 的那一部分** 。既然直接指向了原对象,当然可以直接修改,不需要 `const` 限制。 *** ** * ** *** ### 4.3)对象切片 ```cpp void test_object_slicing() { Derived d; d.base_data = 100; d.derived_data = 200; // ⚠️ 发生对象切片!这里调用的是 Base::Base(const Base& other) Base b = d; cout << b.base_data << endl; // 输出 100,基类数据被成功拷贝 // cout << b.derived_data << endl; // ❌ 编译报错!b 对象里根本没有 derived_data } ``` **按值赋值** * `Base b = d;`迫使编译器调用`Base`的拷贝构造函数 * 而`Base`的拷贝构造函数只认识`Base`自己的成员变量(`base_data`) * 它对 `Derived` 特有的 `derived_data` 视而不见 *** ** * ** *** ### 4.4)基类绝对不能赋值给派生类 ```cpp void test_base_to_derived_assignment() { Base b; // Derived d = b; // ❌ 编译直接报错!不允许这种操作! } ``` *** ** * ** *** ### 4.5)向下转型的安全隐患与 RTTI * **披着 `Base` 外衣的 `Derived`** ```cpp Base* true_derived = new Derived(); ``` * **纯正的`Base`** ```cpp Base* pure_base = new Base(); ``` *** ** * ** *** #### 1)强制类型转换(极其危险) ```cpp Derived* p1 = (Derived*)pure_base; ``` **`p1->derived_data = 10;`** ------------\>编译通过,但运行时极其容易错误,**因为内存里根本没有`derived_data`** #### 2)安全的向下转型(dynamic_cast 安全转换) ```cpp Derived* unsafe_ptr = dynamic_cast(pure_base); if (unsafe_ptr == nullptr) { cout << "转换失败!被 RTTI 拦截,安全着陆。" << endl; } ``` `pure_base`本体就是个 `Base` #### 总结 当使用 `dynamic_cast` 时,`C++` 运行时系统会去查询对象虚函数表`(vtable)`旁的 `type_info` 信息(这就是 `RTTI`) *** ** * ** *** ## 5. 继承中的作用域 ### 5.1)基类和派生类都有独立的作用域 在 `C++` 编译器的眼里,全局作用域是最外层,基类作用域在里面,派生类作用域在最里面 **编译器找名字的顺序:先在 领地 B 找 -\> 找不到再去 领地 A 找 -\> 再找不到去全局找** ```cpp class Base { // 领地 A:Base 的作用域 }; class Derived : public Base { // 领地 B:Derived 的作用域 }; ``` *** ** * ** *** ### 5.2)同名成员的"隐藏"与 基类:: 显式访问 **因为编译器找名字是从内向外找(先找派生类,再找基类)** 一旦在派生类的作用域里找到了你要的名字,它就会立刻停止搜索。 这就导致基类里那个同名的成员被"屏蔽"了 ```cpp #include using namespace std; class Base { public: int value = 100; // 基类的 value }; class Derived : public Base { public: int value = 200; // 派生类的 value,名字相同,触发隐藏! void print() { // 1. 直接访问,编译器在当前作用域找到了 value,直接用 cout << "直接访问 value: " << value << endl; // 输出 200 // 2. 破除隐藏:必须加上域作用限定符 Base:: cout << "显式访问 Base::value: " << Base::value << endl; // 输出 100 } }; ``` *** ** * ** *** ### 5.3)基类与派生类中关于 函数重载 的潜规则 由于基类和派生类是两个独立的作用域,所以在派生类中只要写了一个同名函数,不管参数列表是什么,基类中所有的同名函数都会被瞬间"秒杀"(隐藏),对派生类对象不再直接可见 * 我们先定义一个基类 ```cpp class Base { public: void func() { cout << "Base 的无参 func()" << endl; } void func(int x) { cout << "Base 的带参 func(int)" << endl; } }; ``` * 再定义一个派生类 ```cpp class Derived : public Base { public: // 只要名字叫 func,基类里的所有 func 统统被屏蔽! void func(string s) { cout << "Derived 的 func(string)" << endl; } }; ``` * 接着我们测试 ```cpp void test_function_hiding() { Derived d; ``` ❌ 编译报错!找不到无参的 func,因为被隐藏了 ```cpp d.func(); ``` ❌ 编译报错!找不到接受 int 的 func,即使你传了 10,它也只认 string 版本的 ```cpp d.func(10); ``` ✅ 成功调用 Derived 的版本 ```cpp d.func("hello"); ``` **破除隐藏的唯一方法:指名道姓** ```cpp d.Base::func(); d.Base::func(10); ``` *** ** * ** *** ## 6. 派生类的默认成员函数 默认成员函数通常会有6个 **但是我们通常只关注前四个:构造函数、析构函数、拷贝构造函数、赋值重载** *** ** * ** *** ### 6.1)❗我们写这几个默认成员函数,首先思考几个原则性的问题 1. 我们不写,默认生成的函数行为是什么?是否符合要求? 2. 不符合,我们要自己实现吗?如何实现? > **问题1:** > > * **默认行为(编译器的铁律)**:编译器为你生成的默认函数,对待这两部分有不同的策略。 > * **对待基类部分**:无条件调用基类对应的默认函数(默认构造、默认析构、拷贝构造函数、赋值重载)。 > * **对待派生类自己的成员** : > * 对于内置类型(`int`,指针,`char` 等):构造时不初始化(随机值);拷贝/赋值时进行无脑的字节拷贝(**浅拷贝**);析构时不作任何处理。 > * 对于自定义类型(如 `std::string`):调用它们自己的默认函数。 > * **是否符合要求?(分水岭)** > * **符合** :如果你的派生类里只有普通的 `int`,或者像 `std::string` 这种自己会管理内存的智能对象,编译器的默认行为堪称完美,你千万不要自己手写。 > * **不符合(灾难降临)** :如果你的派生类里有**裸指针** ,并且在堆区 `new` 了一块内存(比如 `int* arr = new int[100]`)。默认的"浅拷贝"会让两个对象的指针指向同一块内存,析构时会导致 **Double Free** (重复释放内存)导致程序直接崩溃 > > **问题2** > > * **结论** :必须自己实现(遵守 C++ 的 **Rule of Three / Rule of Five**:析构、拷贝构造、赋值重载,要写就一起写)。 > * **如何实现的"生死线"** :在你自己手写派生类的这些函数时,你必须**显式地**去处理"基类部分"!如果你忘了,编译器绝不会帮你填坑,它会给你塞一个默认的过去,导致极其隐蔽的 Bug。 *** ** * ** *** ### 6.2)代码级剖析 假设我们有这样一个场景:**基类 `Person`,派生类 `Student`** ```cpp #include #include using namespace std; class Person { protected: int m_age; public: Person(int age = 0) : m_age(age) { cout << "Person 构造" << endl; } Person(const Person& p) : m_age(p.m_age) { cout << "Person 拷贝构造" << endl; } Person& operator=(const Person& p) { if (this != &p) m_age = p.m_age; cout << "Person 赋值" << endl; return *this; } virtual ~Person() { cout << "Person 析构" << endl; } }; ``` *** ** * ** *** **接下来,我们手撕`Student`的四大默认函数** 1. 派生类的构造函数(先造老子,再造儿子) * 如何实现: 必须在派生类的初始化列表中,显式调用基类的构造函数来初始化基类部分 **如果你不写,编译器会强行调用基类的无参默认构造函数(如果基类没有无参构造,直接编译报错)** ```cpp class Student : public Person { private: char* m_name; public: // ✅ 正确姿势:通过初始化列表,把 age 传给 Person,同时初始化自己的 m_name Student(int age, const char* name) : Person(age) { m_name = new char[strlen(name) + 1]; strcpy(m_name, name); cout << "Student 构造" << endl; } }; ``` *** ** * ** *** 2. 派生类的拷贝构造函数 * **如何实现:** 很多人自己写深拷贝时,光顾着拷贝自己的指针,忘了基类! 如果你不显式调用基类的拷贝构造,**编译器会去调用基类的默认无参构造函数!导致你新对象的 `m_age` 变成了 `0` 或者`随机值`** ❌ 错误姿势:忘了基类!导致 Person 部分被默认初始化,产生数据切片 ```cpp Student(const Student& other) { ... } ``` ✅ 正确姿势:必须在初始化列表中显式调用 `Person` 的拷贝构造,传入 `other!` 💡 魔法点:这里把 Student 引用传给 Person 的引用参数,发生了我们之前讲过的"绝对安全"的向上转型! ```cpp Student(const Student& other) : Person(other) { // 自己动手做深拷贝 m_name = new char[strlen(other.m_name) + 1]; strcpy(m_name, other.m_name); cout << "Student 拷贝构造" << endl; } ``` *** ** * ** *** 3. 派生类的赋值运算符重载 * 如何实现: 赋值重载同样需要处理两层逻辑 * 一要**防止自己赋值给自己** * 二要**显式调用基类的赋值重载,最后再处理自己的深拷贝** ```cpp // ✅ 正确姿势 Student& operator=(const Student& other) { cout << "Student 赋值" << endl; // 1. 防身术:防止自己赋值给自己(极度危险,会导致自己的内存先被删掉) if (this == &other) { return *this; } // 2. 显式调用基类的赋值重载,处理基类部分的数据(age) // 💡 魔法点:显式指明作用域 Person::operator=(other); // 3. 处理派生类自己的深拷贝(先释放旧的,再分配新的) delete[] m_name; m_name = new char[strlen(other.m_name) + 1]; strcpy(m_name, other.m_name); return *this; } ``` *** ** * ** *** 4. 派生类的析构函数 * **如何实现:** 在析构函数里,你只需要打扫派生类自己弄脏的屋子(释放 `m_name`)。绝对不要去显式调用 `~Person()` * **底层机制:** 编译器在执行完派生类的析构函数体后,会自动在底层插入对基类析构函数的调用。这就保证了**先灭儿子,再灭老子**的绝对顺序。如果你手贱调用了,会导致基类被析构两次 ```cpp // ✅ 正确姿势:只管好自己,剩下的交给编译器 ~Student() { // 记得基类的析构函数一定要是 virtual 的! delete[] m_name; cout << "Student 析构" << endl; // 编译器会自动在这里偷偷加上:Person::~Person(); } ``` *** ** * ** *** ### 6.3)图表解析 **构造时先造老子后造儿子,析构时先灭儿子后灭老子** ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c16aa119364c4e3bb7edab03758dcbd6.png) *** ** * ** *** ## 7. 实现一个不能被继承的类 ### 方法 1:利用构造函数的私有性(C++98 时代的"黑科技") **派生类的构造函数必须调用基类的构造函数** 如果你把基类的构造函数设为 `private`,那么派生类在实例化时,就会因为"看不见、调不动"父类的构造函数而导致编译报错 ```cpp class Base { private: Base() {} // 1. 构造函数私有化:只有我自己能用,别人(包括儿子)都调不到 }; class Derived : public Base { public: // ❌ 编译报错:'Base::Base()' is private // 因为 Derived 的构造函数必须先调用 Base 的构造函数,但 Base 把它藏起来了 Derived() {} }; int main() { // Base b; // ❌ 连基类自己也没法直接在外部实例化了 // Derived d; // ❌ 派生类彻底废了 return 0; } ``` *** ** * ** *** ### 方法 2:使用 final 关键字(C++11 现代标准,最推荐) `C++11` 引入了`final`关键字,直接从语法层面封死了继承的路 **在类名后面直接加 final** ```cpp // 在类名后面直接加 final class Base final { public: Base() {} }; // ❌ 编译报错:cannot derive from 'final' base 'Base' in derived type 'Derived' class Derived : public Base { }; ``` *** ** * ** *** ## 8. 继承与友元 友元关系不能继承,也就是说**基类友元不能访问派生类私有和保护成员** > 通俗地说:"**你父亲的朋友(基类的友元),不一定是你的朋友(派生类的友元)** 。"即便 `Display` 函数在 `Person` 里被声明为友元,它也只能看到 `Student` 对象中属于 `Person` 的那部分,而看不到 `Student` 自己特有的保护成员 ```cpp class Student; class Person { public: friend void Display(const Person& p, const Student& s); protected: string _name; // 姓名 }; class Student : public Person { protected: int _stuNum; // 学号 }; void Display(const Person& p, const Student& s) { cout << p._name << endl; cout << s._stuNum << endl; // 编译报错:无法访问 protected 成员 } int main() { Person p; Student s; // 编译报错: error C2248: "Student::_stuNum": 无法访问 protected 成员 // 解决方案: Display 也要成为 Student 的友元即可 Display(p, s); return 0; } ``` 1. `Display` 是 `Person` 的友元 → 它可以访问 `Person` 的 `_name` 2. `Student` 继承自 `Person` 3. `Display` 试图访问 `Student` 特有的 `_stuNum` 4. **结局:编译器拒绝** 因为 `Display` 并没有在 `Student` 类里获得"好友位"。 *** ** * ** *** ## 9. 继承与静态成员 基类定义了 `static` 静态成员,则整个继承体系里面**只有一个** 这样的成员。无论派生出多少个派生类,都只有一个 `static` 成员实例 ```cpp class Person { public: string _name; static int _count; }; int Person::_count = 0; class Student : public Person { protected: int _stuNum; public: int main() { Person p; Student s; // 非静态成员 _name 的地址是不一样的 // 说明派生类继承下来了,父类和派生类对象各有一份 cout << &p._name << endl; cout << &s._name << endl; // 静态成员 _count 的地址是一样的 // 说明派生类和基类共用同一份静态成员 cout << &p._count << endl; cout << &s._count << endl; // 公有的情况下,通过父类或派生类类域都可以访问静态成员 cout << Person::_count << endl; cout << Student::_count << endl; return 0; } }; ``` *** ** * ** *** ## ⭕10. 单继承、多继承 及其 菱形继承问题 ### 10.1)单继承 一个派生类只从一个基类继承 ```cpp class Animal { public: void eat() { cout << "正在进食..." << endl; } }; class Dog : public Animal { public: void bark() { cout << "汪汪汪!" << endl; } }; ``` **内存模型** * **布局:** 内存中先排布 `Animal` 的成员,紧接着排布 `Dog` 的成员 * **关系:** 是一条直线:Animal -\> Dog *** ** * ** *** ### 10.2)多继承 多继承允许一个派生类同时从两个或多个基类继承 ```cpp //基类 A:沙发 class Sofa { public: void sit() { cout << "坐着很舒服..." << endl; } }; //基类 B:床 class Bed { public: void sleep() { cout << "躺着很安稳..." << endl; } }; //派生类:沙发床 class SofaBed : public Sofa, public Bed { public: void fold() { cout << "正在折叠成沙发..." << endl; } }; ``` **内存模型** * **布局** :内存中会按照继承声明的顺序,先排布 `Sofa` 的成员,再排布 `Bed` 的成员,最后是 `SofaBed` 自己的成员。 * **关系** :是一个多对一的结构: `{Sofa, Bed} -> SofaBed` *** ** * ** *** ### 10.3)菱形继承问题 **菱形继承** :菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有**数据冗余** 和**二义性** 的问题,在 `Assistant` 的对象中 `Person` 成员会有两份 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/cd813ecbcabd40bb8277825254996896.png) *** ** * ** *** ### 10.4)虚继承 `C++`语法复杂,多继承就是一个体现。 有了多继承,就存在菱形继承; 有了菱形继承就会有菱形虚拟继承 **我们观察下面的代码** ```cpp class Person { public: string _name; // 姓名 /*int _tel; int _age; string _gender; string _address;*/ }; //使用虚继承 Person 类 class Student : virtual public Person { protected: int _num; // 学号 }; //使用虚继承 Person 类 class Teacher : virtual public Person { protected: int _id; // 职工编号 }; //教授助理 class Assistant : public Student, public Teacher { protected: string _majorCourse; // 主修课程 }; int main() { //使用虚继承,可以解决数据冗余和二义性 Assistant a; a._name = "peter"; return 0; } ``` **`virtual` 必须加在产生"分叉"的地方,而不是"汇合"的地方** *** ** * ** *** ### 10.5)多继承中的指针偏移问题 ```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; } ``` | 指针变量 | 指向类型 | 实际地址值 | 说明 | |------|-----------|---------|----------------------------| | `p3` | `Derive*` | `0x100` | 指向对象开头 | | `p1` | `Base1*` | `0x100` | 因为 `Base1` 在开头,所以和 `p3` 相等 | | `p2` | `Base2*` | `0x104` | 发生了偏移!跳过了 `Base1` 的空间 | *** ** * ** *** ## 11. 继承与组合 **继承是 `is-a` 关系** **组合是 `has-a` 关系** ```cpp class Engine { public: void start() { /* 引擎启动逻辑 */ } }; class Car { private: Engine _eng; // 组合:引擎是 Car 的一个成员 public: void run() { _eng.start(); // ✅ 黑箱复用:Car 只管用 Engine 的公开接口,不关心其内部怎么喷油的 cout << "汽车行驶中..." << endl; } }; ``` `Engine` 内部怎么改,只要 `start()` 接口不变,`Car` 就不受影响。每个类都保持了良好的封装性 *** ** * ** *** ### 为什么优先使用组合(Has-A)? * **组合** :宿主类只知道成员类的 `public` 接口。成员类内部怎么改(甚至改了成员变量名),只要接口不变,宿主类代码一行都不用动。 * **继承** :派生类可以看到基类的 `protected` 成员。基类一旦改动内部逻辑,往往会引发派生类的连锁崩溃。这被称为\*\*"脆弱的基类问题"\*\* *** ** * ** *** ### 什么时候必须用继承 C++ 的灵魂------**多态**,必须依赖继承 | 场景需求 | 推荐方案 | 理由 | |-----------------|--------|----------------------------| | 我想借用另一个类的功能代码 | **组合** | 借用工具而已,没必要认爹,保持代码独立性。 | | 我想在运行时更换某个组件 | **组合** | 插件式设计,极其灵活。 | | 我需要实现接口的一致性(多态) | **继承** | 只有继承能让你在父类指针下操作不同子类。 | | 两个类有严格的层级和分类关系 | **继承** | 符合人类直觉,逻辑清晰。 | | 我想隐藏基类的复杂实现细节 | **组合** | 继承会暴露太多 `protected` 内容给子类。 | *** ** * ** *** ## 💻结尾--- 核心连接协议 **警告:** 🌠🌠正在接入底层技术矩阵。如果你已成功破解学习中的逻辑断层,请执行以下指令序列以同步数据:🌠🌠 *** ** * ** *** **【📡】 建立深度链接:** **关注**本终端。在赛博丛林中深耕底层架构,从原始代码到进阶协议,同步见证每一次系统升级。 **【⚡】 能量过载分发:** 执行**点赞**操作。通过高带宽分发,让优质模组在信息流中高亮显示,赋予知识跨维度的传播力。 **【💾】 离线缓存核心:** 将本页加入**收藏**。把这些高频实战逻辑存入你的离线存储器,在遭遇系统崩溃或需要离线检索时,实现瞬时读取。 **【💬】 协议加密解密:** 在**评论区**留下你的散列码。分享你曾遭遇的代码冲突或系统漏洞(那些年踩过的坑),通过交互式编译共同绕过技术陷阱。 **【🛰️】 信号频率投票:** 通过**投票**发射你的选择。你的每一次点击都在重新定义矩阵的进化方向,决定下一个被全量拆解的技术节点。 *** ** * ** *** ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/57b03915c54b43a7a03fa92dbbfe57c3.gif) ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/0905dc972de8414bb602715de3f866ee.gif)

相关推荐
Tanecious.2 小时前
蓝桥杯备赛:Day2-B3612 求区间和
c++·蓝桥杯
C+++Python2 小时前
Linux/C++多进程
linux·运维·c++
stolentime2 小时前
通信题:洛谷P15942 [JOI Final 2026] 赌场 / Casino题解
c++·算法·洛谷·joi·通信题
XZHOUMIN2 小时前
【生成pdf格式的报告】
c++·pdf·mfc
elseif1232 小时前
浅谈 C++ 学习
开发语言·c++·学习
沛沛rh453 小时前
深入并发编程:从 C++ 到 Rust 的学习笔记
c++·笔记·学习·算法·rust
小CC吃豆子3 小时前
C/C++中 int 的最大最小值
c语言·开发语言·c++
欧米欧3 小时前
C++模板初阶
开发语言·c++
CheerWWW3 小时前
C++学习笔记——初始化列表、创建和实例化对象、new 关键字、隐式构造与 explicit 关键字、运算符与运算符重载
c++·笔记·学习