《C++ 继承》三大面向对象编程——继承:派生类构造、多继承、菱形虚拟继承概要

🔥个人主页:Cx330🌸
❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》
《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔
🌟心向往之行必能至
🎥Cx330🌸的简介:

目录
1.1.1 间接实现:【C++98】构造函数私有的类不能被继承
前言:
在面向对象编程的世界里,“避免重复” 与 “灵活扩展” 是开发者始终追求的目标,而 C++ 的继承机制正是实现这两个目标的核心工具。它让我们能够从已有的类(基类)中 “继承” 成熟的成员变量与成员函数,无需重新编写重复代码;同时又能在新类(派生类)中添加专属成员、重写原有函数,让类的功能随需求自然延伸。无论是模拟现实世界中 “动物与猫、狗” 的层级关系,还是开发中 “基础组件与定制组件” 的复用场景,继承都为代码的组织与维护提供了清晰的逻辑框架。理解继承,便是掌握 C++ 面向对象编程的关键一步

一、派生类的默认成员函数专题
1.1实现一个不可继承类实现
1.1.1 间接实现:【C++98】构造函数私有的类不能被继承
基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象

运行结果:

这里必须调用基类的构造,但是基类这里是私有的,看不见,所以就不能再调用了。
1.1.2 直接实现:final关键字修改基类
C++11新增了一个final关键字,final修改基类,派生类就不能继承了

1.1.3 代码实现
//设计一个不能被继承的类 //class Base class Base final { public: void func5() { cout << "Base::func5" << endl; } protected: int a = 1; private: //构造函数私有的类不能被继承 Base() {} }; class Derive :Base { }; int main() { Derive d; return 0; }4.4.4 final关键字
在本文博主不展开讲,下篇博客,博主会介绍C++进阶中又一个重要的模块——【多态】,在【多态】中,博主会介绍两个涉及到【多态】中的重写相关知识点的关键字:override和final。
也就是说,final充当了两个作用
(1)直接实现一个不能被继承的类(【继承】篇涉及知识点);
(2)不让重写基类虚函数(【多态】(下一篇博客)篇即将涉及的知识点)。
二、继承体系中的友元关系
2.1 友元与继承的关系特性
友元关系不能继承。
也就是说基类友元不能访问派生类私有和保护成员
2.2 解决方案
把派生类也变成基类的友元的友元即可
2.3 实战
2.3.1 正确代码演示
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 _stuNum; // 学号 }; void Display(const Person& p, const Student& s) { cout << p._name << endl; cout << s._stuNum << endl; } int main() { Person p; Student s; // 编译报错:error C2248: “Student::_stuNum”: ⽆法访问 protected 成员 // 解决⽅案:Display也变成Student 的友元即可 Display(p, s); return 0; }运行结果:

这段代码是能顺利运行的,但是,我们看下面这段代码
class Person { // 友元关系不能被子类继承 friend void Display(const Person& p, const Student& s); public: 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; } int main() { Person p; Student s; // 编译报错:error C2248:“Student::_stuNum”:无法访问 protected 成员 // 解决方案:Display也变成Student 的友元即可 Display(p, s); return 0; }2.3.2 前置声明的必要性
不加前置声明会报下面的错

2.3.3 友元关系不能继承

因为友元关系不能继承,因此我们要给派生类也变成基类友元的友元
三、静态成员在继承中的特性
3.1 静态成员共享机制:父子共用同一份
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个派生类,都只有一个static成员实例
3.2 静态与非静态成员对比
3.2.1 非静态成员:实例独立
非静态成员的继承是父类和子类各一份,地址不一样
3.2.2 静态成员:类间共享
静态成员的继承是父类和子类共用同一份,地址也一样
3.3 实践出真知:静态成员继承实践案例
class Person { public: string _name; static int _count;//存放在静态区 }; int Person::_count = 0; class Student : public Person { protected: int _stuNum; }; 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; }运行结果:

四、单继承 vs 多继承(以及菱形继承问题详解)
事先说明:多继承是个大坑!!!
4.1 单继承 vs 多继承

4.1.1 概念对比

4.1.2 实战
class Person { public: string _name; // 姓名 }; class Student : virtual public Person //virtual虚拟继承在腰部 { protected: int _num; //学号 }; class Teacher : virtual public Person { protected: int _id; // 职⼯编号 }; class Assistant : public Student, public Teacher { protected: string _majorCourse; // 主修课程 };4.2 菱形继承问题详解
4.2.1 菱形继承的概念

4.2.2 菱形继承的数据冗余与二义性问题
基类数据越多,这两个问题越严重
数据冗余:如下图所示,Person有两个
二义性:访问不明确~>指定类域勉强解决

4.2.3 虚继承解决方案
菱形继承——多继承延伸的坑

多继承不是问题,多继承实现的菱形继承才是问题

因此设计了“菱形虚拟继承”来解决,下面我们会介绍虚继承
4.2.4 虚继承机制与virtual关键字
关键词virtual加在腰部位置,如下图所示

都加上virtual可不可以?——当然不行。
换个说法,药能多吃吗?会影响底层的空间模型,能编译通过但底层空间会乱

虚继承太复杂了,无论是使用还是底层,都太复杂
不要玩菱形继承!!!当然,菱形继承也是有应用的,库里面的IO库就是搞成菱形继承的,IO库的使用会专门在IO库讲
4.2.5 菱形继承的问题

4.2.6 实战
class Person { public: Person(const char* name) :_name(name) { } string _name; // 姓名 }; class Student : virtual public Person { public: Student(const char* name, int num) :Person(name) , _num(num) { } protected: int _num; //学号 }; class Teacher : virtual public Person { public: Teacher(const char* name, int id) :Person(name) , _id(id) { } protected: int _id; // 职⼯编号 }; // 不要去玩菱形继承 class Assistant : public Student, public Teacher { public: Assistant(const char* name1, const char* name2, const char* name3) :Person(name3) ,Student(name1, 1) ,Teacher(name2, 2) { } protected: string _majorCourse; // 主修课程 }; int main() { // 思考⼀下这⾥a对象中_name是"张三", "李四", "王五"中的哪⼀个? Assistant a("张三", "李四", "王五"); return 0; }运行结果:

4.2.7 可以设计出多继承,不建议设计出菱形继承
我们可以设计出多继承,但是不建议设计出菱形继承,因为菱形虚拟继承以后,无论是使用还是底层都会复杂很多。当然有多继承语法支持,就一定存在会设计出菱形继承,像Java是不支持多继承的,就避开了菱形继承
4.3 IO库中的菱形虚拟继承



4.4 多继承中的指针偏移问题
4.4.1 题目
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; }下面说法正确的是( )
A. p1 == p2 == p3
B. p1 < p2 < p3
C. p1 == p3 != p2
D. p1 != p2 != p3
4.4.2 答案解析
正确答案:C

五、继承与组合设计模式对比
5.1 基本概念:is-a vs has-a
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象
- 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-boxreuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对派生类可见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-boxreuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装
- 先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),就用组合
5.2 继承与组合关系对比

5.3 实践
// 继承和组合 // Tire(轮胎)和Car(⻋)更符合has-a的关系 class Tire { protected: string _brand = "Michelin"; // 品牌 size_t _size = 17; // 尺寸 }; class Car { protected: string _colour = "白色"; // 颜色 string _num = "陕ABIT00"; // 车牌号 Tire _t1; // 轮胎 Tire _t2; // 轮胎 Tire _t3; // 轮胎 Tire _t4; // 轮胎 }; class BMW : public Car { public: void Drive() { cout << "好开-操控" << endl; } }; // Car和BMW/Benz更符合is-a的关系 class Benz : public Car { public: void Drive() { cout << "好坐-舒适" << endl; } }; template<class T> class vector {}; // stack和vector的关系,既符合is-a,也符合has-a template<class T> class stack : public vector<T> {}; template<class T> class stack { public: vector<T> _v; }; int main() { return 0; }is-a用继承,has-a用组合
// 继承和组合 // Tire(轮胎)和Car(⻋)更符合has-a的关系 // 轮胎类 class Tire { protected: string _brand = "Michelin"; size_t _size = 17; }; // 汽车基类 class Car { protected: string _colour = "白色"; string _num = "陕ABIT00"; Tire _tires[4]; // 使用数组更合适 }; // 派生类 class BMW : public Car { public: void Drive() { cout << "好开-操控" << endl; } }; class Benz : public Car { public: void Drive() { cout << "好坐-舒适" << endl; } }; // 正确的stack实现 template<class T> class Stack { private: vector<T> _v; // 组合关系 public: void push(const T& x) { _v.push_back(x); } void pop() { if (!_v.empty()) _v.pop_back(); } T& top() { if (!_v.empty()) return _v.back(); throw std::out_of_range("Stack is empty"); } bool empty() const { return _v.empty(); } size_t size() const { return _v.size(); } }; int main() { BMW bmw; bmw.Drive(); Stack<int> s; s.push(1); s.push(2); cout << s.top() << endl; // 输出2 return 0; }5.4 继承 vs 组合
5.4.1 白盒复用与黑盒复用
白盒测试:更加难,一般由研发人员写并且测试,看得见、透明——保护、私有都可使用;
黑盒测试:看不见,不透明;
白盒 / 黑盒好坏的依据是从软件设计角度出发的
8.4.2 软件设计中的选择策略
高内聚,低耦合——可维护性(其中一个修改,另一个不受影响)
8.4.3 模块
打成一个个模块,哪个出问题改哪个,不受影响
组件:静态库、动态库——不可执行的二进制文件
1、编译时间降低;
2、看不到源码(二进制编译)
8.4.4继承和组合哪个更好?
实践的角度:优先使用组合;既符合继承也符合组合,我们使用组合;但是要注意:是“优先使用组合”,不是必须使用,但是像多态这些需要继承的地方还是要用继承
完整代码演示
#include<iostream> using namespace std; //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 _stuNum; // 学号 //}; //void Display(const Person& p, const Student& s) //{ // cout << p._name << endl; // cout << s._stuNum << endl; //} // //int main() //{ // Person p; // Student s; // // 编译报错:error C2248: “Student::_stuNum”: ⽆法访问 protected 成员 // // 解决⽅案:Display也变成Student 的友元即可 // Display(p, s); // // return 0; //} //class Person //{ //public: // string _name; // static int _count;//存放在静态区 //}; // //int Person::_count = 0; // //class Student : public Person //{ //protected: // int _stuNum; //}; // //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; //} //菱形继承 //菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以 //看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就 //⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议 //设计出菱形继承这样的模型的 //class Person //{ //public: // string _name; // 姓名 //}; //// //class Student : virtual public Person //virtual虚拟继承在腰部 //{ //protected: // int _num; //学号 //}; //class Teacher : virtual public Person //{ //protected: // int _id; // 职⼯编号 //}; // //class Assistant : public Student, public Teacher //{ //protected: // string _majorCourse; // 主修课程 //}; // //int main() //{ // // 编译报错:error C2385: 对“_name”的访问不明确 // Assistant a; // a._name = "peter"; // // 需要显⽰指定访问哪个基类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决 // a.Student::_name = "xxx"; // a.Teacher::_name = "yyy"; // // return 0; //} //class Person //{ //public: // Person(const char* name) // :_name(name) // { } // string _name; // 姓名 //}; // //class Student : virtual public Person //{ //public: // Student(const char* name, int num) // :Person(name) // , _num(num) // { // } //protected: // int _num; //学号 //}; // //class Teacher : virtual public Person //{ //public: // Teacher(const char* name, int id) // :Person(name) // , _id(id) // { // } //protected: // int _id; // 职⼯编号 //}; // //// 不要去玩菱形继承 //class Assistant : public Student, public Teacher //{ //public: // Assistant(const char* name1, const char* name2, const char* name3) // :Person(name3) // ,Student(name1, 1) // ,Teacher(name2, 2) // { // } //protected: // string _majorCourse; // 主修课程 //}; // //int main() //{ // // 思考⼀下这⾥a对象中_name是"张三", "李四", "王五"中的哪⼀个? // Assistant a("张三", "李四", "王五"); // return 0; //} // 继承和组合 // Tire(轮胎)和Car(⻋)更符合has-a的关系 // 轮胎类 //class Tire { //protected: // string _brand = "Michelin"; // size_t _size = 17; //}; // //// 汽车基类 //class Car { //protected: // string _colour = "白色"; // string _num = "陕ABIT00"; // Tire _tires[4]; // 使用数组更合适 //}; // //// 派生类 //class BMW : public Car { //public: // void Drive() { cout << "好开-操控" << endl; } //}; // //class Benz : public Car { //public: // void Drive() { cout << "好坐-舒适" << endl; } //}; // //// 正确的stack实现 //template<class T> //class Stack { //private: // vector<T> _v; // 组合关系 //public: // void push(const T& x) { _v.push_back(x); } // void pop() { // if (!_v.empty()) // _v.pop_back(); // } // T& top() { // if (!_v.empty()) // return _v.back(); // throw std::out_of_range("Stack is empty"); // } // bool empty() const { return _v.empty(); } // size_t size() const { return _v.size(); } //}; // //int main() { // BMW bmw; // bmw.Drive(); // // Stack<int> s; // s.push(1); // s.push(2); // cout << s.top() << endl; // 输出2 // // return 0; //} //实现多态的两个重要条件 // 必须是基类的指针或者引⽤调⽤虚函数 // 被调⽤的函数必须是虚函数,并且完成了虚函数重写 / 覆盖。 // 虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数 // (即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同), // 称派⽣类的虚函数重写了基类的虚函数 //class Person //{ //public: // virtual void BuyTicket()//虚函数 // { // cout << "买票-全价" << endl; // } //}; // //class Student :public Person //{ //public: // virtual void BuyTicket() // { // cout << "买票-打折" << endl; // } //}; // //void Func(Person* ptr) //{ // // 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket // // 但是跟ptr没关系,⽽是由ptr指向的对象决定的。 // ptr->BuyTicket(); //} //int main() //{ // Person ps; // Student st; // // Func(&ps); // Func(&st); // // return 0; //} //class Animal //{ //public: // virtual void talk() const // { } //}; // //class Dog : public Animal //{ //public: // //重写实现,可以不加virtual // virtual void talk() const // { // std::cout << "汪汪" << std::endl; // } //}; // //class Cat : public Animal //{ //public: // virtual void talk() const // { // std::cout << "(>^ω^<)喵" << std::endl; // } //}; // ////必须是指针或者引用 //void letsHear(const Animal& animal) //{ // animal.talk(); //} // //int main() //{ // Cat cat; // Dog dog; // // letsHear(cat); // letsHear(dog); // // return 0; //} //class A {}; //class B : public A {}; // //class Person { //public: // //协变(了解) // // 派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤, // // 派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变 // virtual A* BuyTicket() // { // cout << "买票-全价" << endl; // return nullptr; // } //}; // //class Student : public Person { //public: // virtual B* BuyTicket() // { // cout << "买票-打折" << endl; // return nullptr; // } //}; // //void Func(Person* ptr) //{ // ptr->BuyTicket(); //} // //int main() //{ // Person ps; // Student st; // // Func(&ps); // Func(&st); // // return 0; //} //class A //{ //public: // virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; } // virtual void test() { func(); } //}; //class B : public A //{ //public: // //多态是:不加virtual重写是重写虚函数的实现部分 // //相当于是基类的函数声明部分+派生类的函数实现部分 // //即 virtual void func(int val = 1)+{ std::cout << "B->" << val << std::endl; } // void func(int val = 0) { std::cout << "B->" << val << std::endl; } //}; //int main(int argc, char* argv[]) //{ // B* p = new B; // //多态调用 // p->test(); // //普通调用 // p->func(); // // return 0; //} //class A //{ //public: // virtual ~A() // { // cout << "~A()" << endl; // } //}; // //class B : public A { //public: // //建议加上virtual // //virtual ~B() // ~B() // { // cout << "~B()->delete:" << _p << endl; // delete _p; // } //protected: // int* _p = new int[10]; //}; // //// 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能 ////构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。 // ////基类只要保证析构函数是虚函数,下面这下些场景就不会存在内存泄露 //int main() //{ // A* ptr1 = new B; // delete ptr1; // // A* ptr2 = new A; // delete ptr2; // // return 0; //} //override检查虚函数 //class Car { //public: // //virtual void Dirve()//函数名写错、参数写错等导致⽆法构成重写 // virtual void Drive() // { } // //不想让派⽣类重写这个虚函数,那么可以⽤final去修饰 // virtual void Drive() final // { } //}; //class Benz :public Car { //public: // virtual void Drive() override { cout << "Benz-舒适" << endl; } //}; //int main() //{ // return 0; //} //设计一个不能被继承的类 //class Base //class Base final //{ //public: // void func5() { cout << "Base::func5" << endl; } //protected: // int a = 1; //private: // //构造函数私有的类不能被继承 // Base() // {} //}; // //class Derive :Base //{ // //}; //int main() //{ // Derive d; // // return 0; //}结尾
往期回顾:
《C++ 继承》三大面向对象编程——继承:代码复用与功能扩展的核心机制
结语:继承作为 C++ 面向对象编程的三大特性(封装、继承、多态)之一,是连接 “通用类” 与 “专用类” 的桥梁。它通过代码复用减少重复开发,通过功能扩展满足个性化需求,同时又通过访问控制和继承方式保障代码的安全性与灵活性。掌握继承的概念、关键要素与使用原则,不仅能提升代码效率,更能帮助我们构建逻辑清晰、易于维护的面向对象系统