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

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

目录
前言:
一、派生类的默认成员函数专题
1.1实现一个不可继承类实现
1.1.1 间接实现:【C++98】构造函数私有的类不能被继承
1.1.2 直接实现:final关键字修改基类
1.1.3 代码实现
4.4.4 final关键字
二、继承体系中的友元关系
2.1 友元与继承的关系特性
2.2 解决方案
2.3 实战
2.3.1 正确代码演示
2.3.2 前置声明的必要性
2.3.3 友元关系不能继承
三、静态成员在继承中的特性
3.1 静态成员共享机制:父子共用同一份
3.2 静态与非静态成员对比
3.2.1 非静态成员:实例独立
3.2.2 静态成员:类间共享
3.3 实践出真知:静态成员继承实践案例
四、单继承 vs 多继承(以及菱形继承问题详解)
4.1 单继承 vs 多继承
4.1.1 概念对比
4.1.2 实战
4.2 菱形继承问题详解
4.2.1 菱形继承的概念
4.2.2 菱形继承的数据冗余与二义性问题
4.2.3 虚继承解决方案
4.2.4 虚继承机制与virtual关键字
4.2.5 菱形继承的问题
4.2.6 实战
4.2.7 可以设计出多继承,不建议设计出菱形继承
4.3 IO库中的菱形虚拟继承
4.4 多继承中的指针偏移问题
4.4.1 题目
4.4.2 答案解析
五、继承与组合设计模式对比
5.1 基本概念:is-a vs has-a
5.2 继承与组合关系对比
5.3 实践
5.4 继承 vs 组合
5.4.1 白盒复用与黑盒复用
8.4.2 软件设计中的选择策略
8.4.3 模块
8.4.4继承和组合哪个更好?
完整代码演示
结尾
前言:
在面向对象编程的世界里,“避免重复” 与 “灵活扩展” 是开发者始终追求的目标,而 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; // 输出2return 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++ 面向对象编程的三大特性(封装、继承、多态)之一,是连接 “通用类” 与 “专用类” 的桥梁。它通过代码复用减少重复开发,通过功能扩展满足个性化需求,同时又通过访问控制和继承方式保障代码的安全性与灵活性。掌握继承的概念、关键要素与使用原则,不仅能提升代码效率,更能帮助我们构建逻辑清晰、易于维护的面向对象系统
