C++ 继承入门(下):友元、静态成员与菱形继承的底层逻辑

C++ 继承入门(下):友元、静态成员与菱形继承的底层逻辑

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》

《C++入门到进阶&自我学习过程记录》《算法题讲解指南》--从优选到贪心

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

一. 友元 —— 友元关系不可继承

  1、错误版本

  2、正确版本

二. 静态成员 —— 继承体系中静态成员的共享性

三. 多继承及菱形继承问题:本质特点与解决方案

  1、单继承与多继承模型

  2、菱形继承:虚继承解决“数据冗余”与“二义性”

    2.1 菱形继承出现的坑(解决二义性问题)

    2.2 虚继承:彻底解决菱形继承问题

    3、多继承中指针偏移问题

友元,静态成员,菱形继承总结表:

四、继承与组合:C++ 代码复用的核心方式对比

结束语


前言

      在上次C++ 继承入门(上):从基础概念定义到默认成员函数,吃透类复用的核心逻辑文章中我们对继承的基础类复用进行了讲解,但除此之外友元、静态成员、菱形继承这些特殊场景往往是学习“继承”的理解难点。本篇文章将逐一讲解这些场景的底层逻辑,帮你彻底掌握继承的 “隐藏规则”。

一. 友元 —— 友元关系不可继承

      C++中,基类的友元函数/类无法直接访问派生类的私有成员。这就像"你父亲的朋友,不等同就是你的朋友",友元关系不具有继承性。如果需要让友元访问派生类成员,必须在派生类中重新声明一下友元

具体示例:(附错误版本及解决方案)

  1、错误版本

#include<iostream> #include<vector> using namespace std; //友元——友元不能被继承 class Person { public: friend void Display(const Person& p, const Student& s); protected: string _name = "张三"; //姓名 }; class Student : public Person { protected: int _stuid = 123; //学号 }; void Display(const Person& p, const Student& s) { cout << p._name << endl; cout << s._stuid << endl; } void Test1() { Person p; Student s; Display(p, s); } int main() { Test1(); return 0; }

      我们会发现出现了上面两个报错,第二个报错我们倒是好理解,但是第一个报错原因可能很多人会不理解是什么意思。

      通常如果出现这种缺少“,”这种报错,但是检查的时候没有漏掉逗号, 有一种情况就是类型出现了问题(没有类型),但这是为什么呢?
      原因就在于编译器当看到一个类型时是向上查找,所以当编译器走到友元函数的代码时看到了Student 这个类型,就会向上查找,但是上面却没有这个类型的说明就会报错。
      那有些人就会说了:直接把 Student 放到 Person 上面不就行了,但是 Student 是 Person 的继承类,如果把 Student 放到 Person 上面,当编译器走到 Student 时向上查找又找不到 Person 了。这就是典型的“相互依赖”。解决方法就是在最开始前置声明一下 Student

      而第二个报错就说明了:基类的友元是不能被派生类继承的,所以解决方法就是在派生类也进行友元声明。

  2、正确版本

//友元——友元不能被继承 //前置声明Student(为了让编译器走到友元函数时能向上查找到 Student) class Student; class Person { public: friend void Display(const Person& p, const Student& s); protected: string _name = "张三"; //姓名 }; class Student : public Person { friend void Display(const Person& p, const Student& s); protected: int _stuid = 123; //学号 }; void Display(const Person& p, const Student& s) { cout << p._name << endl; cout << s._stuid << endl; //error C2248 : “Student::_stuid” : 无法访问 protected 成员(在“Student”类中声明) //说明基类的友元不能被派生类继承,要使用就需要在派生类也进行友元声明 } void Test1() { Person p; Student s; Display(p, s); } int main() { Test1(); return 0; }

核心结论

  • 基类友元仅能访问基类的 private/protected 成员
  • 若需访问派生类成员,必须在派生类中重新声明友元;
  • 友元关系是"一对一的"不能继承自动传递

二. 静态成员 —— 继承体系中静态成员的共享性

      基类的静态成员(静态变量/静态函数)在整个继承体系中仅存在一份,派生类和基类共享该成员,不会因为继承而产生多个。这与非静态成员不同 —— 非静态成员每个对象独一份。
      也就是说对于基类的静态成员而言,对其进行初始化则说所有派生类的该成员都会被初始化成相同值,并且一个类的静态成员进行修改也会影响其他所有相关的派生类和基类。

//静态成员 class Person { public: string _name; static int _count; }; int Person::_count = 1; class Student : public Person { protected: int _stuid; }; void Test2() { Person p; Student s; // 这里的运行结果可以看到非静态成员的_name地址是不一样的 // 说明非静态成员派生类继承下来了,基类和派生类对象各有一份不一样的 cout << &p._name << endl; cout << &s._name << endl; cout << endl; // 这里的运行结果可以看到静态成员的_count地址是一样的 // 说明派生类和基类共用同一份静态成员,所以对其中一个修改就会影响所有相关基类和派生类的静态成员 cout << &p._count << endl; cout << &s._count << endl; cout << endl; cout << p._count << endl; cout << s._count << endl; cout << endl; Person::_count++; // 公有的情况下,基类派生类指定类域都可以访问静态成员 //原因在前面的学习已经讲解过了,静态成员不属于任何一个对象,是存在静态区的 //可以把静态成员看成全局变量只是被类域所限制而已,所以要访问就需要使用域作用限定符来突破类域访问 cout << Person::_count << endl; cout << Student::_count << endl; cout << endl; } int main() { Test2(); return 0; }

核心结论(前三个在前面的学习已经讲过):

  • 静态成员变量必须在类外初始化,否则会触发链接错误;
  • 静态成员函数只能访问静态成员变量,无法访问非静态成员;
  • 公有的情况下,基类派生类指定类域也可以访问静态成员
  • 继承体系中所有类(基类,派生类)共享同一份静态成员,修改一处会影响全局。

三. 多继承及菱形继承问题:本质特点与解决方案

  1、单继承与多继承模型

      单继承:一个派生类只有⼀个直接基类时称这个继承关系为单继承
      多继承
:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是:先继承的基类在前面后继承的基类在后面派生类成员在放到最后面

//多继承 class Student { protected: string _name; //姓名 }; class Teacher { protected: int _id = 123; //职工编号 }; class Assistant : public Student, public Teacher { protected: string _majorCourse; //主修课程 }; void Test3() { Assistant a; } int main() { Test3(); return 0; }

  2、菱形继承:虚继承解决“数据冗余”与“二义性”

      菱形继承是指“一个派生类同时继承两个基类,而这两个基类又共同继承自一个顶层基类”的结构(并非一定是个菱形结构的图)。这种结构会导致两个核心问题:

  • 数据冗余:顶层基类的成员被最后的派生类继承了两次
  • 二义性:访问成员时无法确定到底属于哪个基类

      支持多继承就一定会有菱形继承,像 Java 就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。

    2.1 菱形继承出现的坑(解决二义性问题)

//菱形继承 // 顶层基类 class Person { public: string _name; //会被Assistant继承两次 }; // 中间基类1 class Student : public Person { protected: int _num; //学号 }; // 中间基类2 class Teacher : public Person { protected: int _id; //职工编号 }; class Assistant : public Student, public Teacher { protected: string _majorCourse; //主修课程 }; void Test3() { Assistant a; //a._name = "张三"; //error C2385: 对“_name”的访问不明确 // (到底是Student::_name还是Teacher::_name呢?) a.Student::_name = "李四"; a.Teacher::_name = "王五"; // 只能显式指定,但数据冗余仍存在,没有解决 cout << a.Student::_name << endl; // 输出李四 cout << a.Teacher::_name << endl; // 输出王五 } int main() { Test3(); return 0; }

    2.2 虚继承:彻底解决菱形继承问题

//虚继承 //顶层基类 class Person { public: Person(const char* name) :_name(name) { } public: string _name; // 姓名 }; // 中间基类1:虚继承Person(添加virtual) //virtual,谁(Person)被继承多次就在继承谁(Person)的那些子类(Student)加 class Student : virtual public Person { public: Student(const char* name, int num) :Person(name)// 在虚继承下,中间基类会暂时不初始化顶层基类 , _num(num) // 只会初始化自己的成员变量 { } protected: int _num; //学号 }; // 中间基2:虚继承Person(添加virtual) //virtual,谁(Person)被继承多次就在继承谁(Person)的那些子类(Teacher)加 class Teacher : virtual public Person { public: Teacher(const char* name, int id) :Person(name)// 在虚继承下,中间基类会暂时不初始化顶层基类 , _id(id) // 只会初始化自己的成员变量 { } protected: int _id; // 职工编号 }; // 最终派生类:菱形继承(Person成员会被合并成仅一份) class Assistant : public Student, public Teacher { public: // 关键:虚继承下,顶层基类的构造由最终派生类显式调用 Assistant(const char* name1, const char* name2, const char* name3) :Person(name1)// 直接初始化顶层基类,下面两个初始化不会进去 , Student(name2, 1) , Teacher(name3, 2) , _majorCourse("计算机") { } protected: string _majorCourse; // 主修课程 }; void Test4() { Assistant a("张三", "李四", "王五"); //思考一下这里 a 对象中 _name 是"张三", "李四", "王五"中的哪一个? //上面有三次Person(name),但其实就只有在Assistant里一次,其它两次会跳过。 //所以是张三 } int main() { Test4(); return 0; }

      虚继承的关键细节:

  • virtual 仅需添加在中间基类继承顶层基类时,最终派生类继承中间基类不需要添加
  • 虚继承下,顶层基类的构造函数由最终派生类负责调用中间基类的构造函数不再初始化顶层基类(但还是需要写出来的)。
  • 虚继承时会增加底层复杂度(虚基表),因此尽量避免设计菱形继承结构,除非业务逻辑必须如此。

    3、多继承中指针偏移问题

      下面说法正确的是( C )
      A:p1 == p2 == p3  B:p1 < p2 < p3  C:p1 == p3 != p2  D:p1 != p2 != p3

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; }

图解如下

友元,静态成员,菱形继承总结表

场景核心特性避坑避坑指南
友元友元关系不随继承传递,若需访问派生类私有成员,必须在派生类中重新声明友元控制友元使用范围,避免因过度开放访问破坏类的封装性
静态成员全继承体系共享唯一实例,需在类外初始化;静态函数仅能访问静态成员变量关注静态成员的“全局共享”特性,多线程场景需加锁保护,避免并发冲突
菱形继承因间接继承共同基类导致数据冗余和访问二义性,需通过虚继承解决;虚继承下顶层基类由最终派生类初始化设计阶段优先规避菱形结构,确需使用时再通过虚继承处理,避免过度依赖增加代码复杂度

四、继承与组合:C++ 代码复用的核心方式对比

  • 继承( is-a 关系 ):体现 “子类是父类的一种” 的逻辑,例如 “Student 是 Person 的一种” “BMW 是 Car 的一种”。派生类直接继承基类的成员(属性 / 方法),可扩展自身独有功能,属于 “白箱复用” —— 子类能访问基类非私有成员了解其内部实现细节
  • 组合( has-a 关系 ):体现 “一个类包含另一个类的对象” 的逻辑,例如 “Car 包含 Tire”“Computer 包含 CPU”。组合类通过调用被包含对象的公开接口实现复用被包含类的内部细节对组合类隐藏,属于 “黑箱复用”

选择原则

  • 优先使用组合:组合的低耦合特性更符合“高内聚、低耦合”的设计原则,代码可维护性更强,尤其在复杂系统中,能减少类间依赖带来的修改风险。不过也不太那么绝对,类之间的关系就只适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),就用组合。
  • 必要时使用继承:当类间明确存在 “is-a” 关系,或需要通过继承实现多态(如基类指针指向派生类对象)时,选择继承;避免为了复用少量代码而强行使用继承,导致耦合度升高。
结束语

      到此,C++继承入门就全部讲解完了。C++ 继承的核心价值在于实现类级别的代码复用,但友元、静态成员、菱形继承这些特殊场景,恰恰是理解继承机制 “深度” 的关键。从友元关系的 “不可继承性”,到静态成员的 “全局共享特性”,再到菱形继承中虚继承对数据冗余与二义性的解决,都表现出 C++ 对 “封装”“复用” 与 “安全性” 的平衡设计。希望对大家学习C++能有所收获!

C++参考文档:
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/

Read more

区块链 Web3 系统的开发

开发一个区块链 Web3 系统(通常指去中心化应用 dApp)是一个跨学科的过程,涉及智能合约逻辑、去中心化存储和传统 Web 前端技术的结合。 以下是开发 Web3 系统的核心步骤: 1. 确定应用场景与架构设计 在动手写代码前,首先要明确业务逻辑中哪些部分需要“去中心化”。 * 链上逻辑:涉及资产、权属、治理或关键业务规则的部分,需编写为智能合约。 * 离线数据:大容量文件(如图片、视频)不建议存放在链上,通常使用 IPFS 或 Arweave。 * 技术栈选择:根据需求选择公链(如 Ethereum, Solana, Polygon)或联盟链。 2. 环境搭建与工具准备 Web3 开发有一套专门的工具链。 * 开发框架:Hardhat 或 Foundry(目前最主流,用于编译、测试、

By Ne0inhk
基于2-RSS-1U的双足机器人并联踝关节分析与实现

基于2-RSS-1U的双足机器人并联踝关节分析与实现

"当你的机器人开始像人类一样思考如何走路时,你会发现,原来最复杂的不是大脑,而是脚踝。"这句话在机器人学界越来越成为共识。论文ASAP中的研究也证实,在sim2real中,偏差最大的正是踝关节控制。 参考文献:On the Comprehensive Kinematics Analysis of a Humanoid Parallel Ankle Mechanism 结构变体:Structural design and motion analysis of parallel ankle joints for humanoid robots 脚踝革命:深入解析人形机器人高性能并联踝关节 传统的单轴踝关节设计,就像给机器人穿了一双"高跟鞋"——虽然能走,但走得很僵硬,很危险。我们需要的是像人类脚踝一样的灵活性:既能前后摆动(pitch),又能左右倾斜(roll)

By Ne0inhk

ClawdBot保姆级部署指南:零配置运行Telegram全能翻译机器人

ClawdBot保姆级部署指南:零配置运行Telegram全能翻译机器人 你是否试过在 Telegram 群里发一条中文消息,想让外国朋友立刻看懂,却要先复制、切到翻译 App、再粘贴、再发回群?又或者收到一张模糊的菜单图,想快速知道价格和菜品,却得手动打字识别?更别说语音留言听不清、汇率临时查不到、天气预报找不到入口……这些日常小痛点,本不该消耗你的时间。 ClawdBot 不是另一个“概念型”AI项目。它是一个真正能装进你本地设备、开箱即用、不依赖云服务、不上传隐私数据的个人 AI 助手。而它的核心能力,正由 vLLM 高效驱动——这意味着你在树莓派上也能跑起 4B 级别大模型,响应快、显存省、推理稳。它不追求参数堆砌,只专注一件事:把复杂技术藏在背后,把简单好用交到你手上。 但今天这篇文章,我们不聊架构设计,也不讲模型微调。我们要一起完成一件非常实在的事:5 分钟内,在你自己的机器上,

By Ne0inhk
Neo4j图谱可视化-告别单调灰色、掌握色彩定制的艺术

Neo4j图谱可视化-告别单调灰色、掌握色彩定制的艺术

摘要 本文旨在系统地介绍在 Neo4j 中为知识图谱定制颜色的多种方法与最佳实践。从最基础的手动界面操作,到通过修改数据结构实现持久化着色,再到基于节点属性的高级动态着色技巧,本文将为读者提供一套完整的图谱可视化解决方案,帮助读者将复杂的数据网络转化为直观、清晰、富有洞察力的彩色图谱。 引言:当知识图谱遇上 “色盲” 当您第一次在 Neo4j Browser 中执行查询,满怀期待地切换到图形视图时,可能会遇到一个令人沮丧的场景:一个由无数灰色节点和线条构成的杂乱网络。这种单调的视觉呈现,使得数据中蕴含的丰富结构和关系模式难以被快速识别,极大地削弱了知识图谱作为数据分析工具的价值。 幸运的是,Neo4j Browser 提供了强大而灵活的样式定制功能。通过为不同类型的节点和关系应用恰当的颜色,我们可以将数据的内在逻辑和层次结构直观地呈现出来,让知识图谱真正 “活” 起来,成为洞察数据的有力武器。 本文将从核心原理出发,详细讲解三种主流的颜色定制方法,并通过具体的医药和情感分析实例,帮助您掌握这门 “图谱着色” 的艺术。 核心概念:颜色与 “标签(Label)” 的绑定

By Ne0inhk