【C++:继承】C++面向对象继承全面解析:派生类构造、多继承、菱形虚拟继承与设计模式实践

🔥艾莉丝努力练剑:个人主页

专栏传送门:《C语言》《数据结构与算法》C/C++干货分享&学习过程记录Linux操作系统编程详解笔试/面试常见算法:从基础到进阶

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬艾莉丝的简介:


🎬艾莉丝的C++专栏简介:

​​


目录

C++的两个参考文档

[4 ~> 派生类的默认成员函数专题](#4 ~> 派生类的默认成员函数专题)

[4.4 实现一个不可继承类实现](#4.4 实现一个不可继承类实现)

[4.4.1 间接实现:【C++98】构造函数私有的类不能被继承](#4.4.1 间接实现:【C++98】构造函数私有的类不能被继承)

[4.4.2 直接实现:final关键字修改基类](#4.4.2 直接实现:final关键字修改基类)

[4.4.3 代码实现](#4.4.3 代码实现)

[4.4.4 final关键字](#4.4.4 final关键字)

[5 ~> 继承体系中的友元关系](#5 ~> 继承体系中的友元关系)

[5.1 友元与继承的关系特性](#5.1 友元与继承的关系特性)

[5.2 解决方案](#5.2 解决方案)

[5.3 最佳实践](#5.3 最佳实践)

[5.3.1 正确代码演示](#5.3.1 正确代码演示)

[5.3.2 前置声明的必要性](#5.3.2 前置声明的必要性)

[5.3.3 友元关系不能继承](#5.3.3 友元关系不能继承)

[6 ~> 静态成员在继承中的特性](#6 ~> 静态成员在继承中的特性)

[6.1 静态成员共享机制:父子共用同一份](#6.1 静态成员共享机制:父子共用同一份)

[6.2 静态与非静态成员对比](#6.2 静态与非静态成员对比)

[6.2.1 非静态成员:实例独立](#6.2.1 非静态成员:实例独立)

[6.2.2 静态成员:类间共享](#6.2.2 静态成员:类间共享)

[6.3 实践出真知:静态成员继承实践案例](#6.3 实践出真知:静态成员继承实践案例)

[6.4 共用一份的体现](#6.4 共用一份的体现)

[7 ~> 单继承 vs 多继承(以及菱形继承问题详解)](#7 ~> 单继承 vs 多继承(以及菱形继承问题详解))

[7.1 单继承 vs 多继承](#7.1 单继承 vs 多继承)

[7.1.1 概念对比](#7.1.1 概念对比)

[7.1.2 最佳实践](#7.1.2 最佳实践)

[7.2 菱形继承问题详解](#7.2 菱形继承问题详解)

[7.2.1 菱形继承的概念](#7.2.1 菱形继承的概念)

[7.2.2 菱形继承的数据冗余与二义性问题](#7.2.2 菱形继承的数据冗余与二义性问题)

[7.2.3 虚继承解决方案](#7.2.3 虚继承解决方案)

[7.2.4 虚继承机制与virtual关键字](#7.2.4 虚继承机制与virtual关键字)

[7.2.5 菱形继承的问题](#7.2.5 菱形继承的问题)

[7.2.6 实践](#7.2.6 实践)

[7.2.7 可以设计出多继承,不建议设计出菱形继承](#7.2.7 可以设计出多继承,不建议设计出菱形继承)

[7.3 IO库中的菱形虚拟继承](#7.3 IO库中的菱形虚拟继承)

[7.4 多继承中的指针偏移问题](#7.4 多继承中的指针偏移问题)

[7.4.1 题目](#7.4.1 题目)

[7.4.2 答案解析](#7.4.2 答案解析)

[7.4.3 运行](#7.4.3 运行)

[8 ~> 继承与组合设计模式对比](#8 ~> 继承与组合设计模式对比)

[8.1 基本概念:is-a vs has-a](#8.1 基本概念:is-a vs has-a)

[8.2 继承与组合关系对比](#8.2 继承与组合关系对比)

​编辑

[8.3 最佳实践](#8.3 最佳实践)

[8.4 继承 vs 组合](#8.4 继承 vs 组合)

[8.4.1 白盒复用与黑盒复用](#8.4.1 白盒复用与黑盒复用)

[8.4.2 软件设计中的选择策略](#8.4.2 软件设计中的选择策略)

[8.4.3 模块](#8.4.3 模块)

[8.4.4 博主手记](#8.4.4 博主手记)

[8.5 继承与组合最佳实践指南:继承和组合哪个更好?](#8.5 继承与组合最佳实践指南:继承和组合哪个更好?)

完整代码示例与实践演示

Test.cpp:

结尾


C++的两个参考文档

老朋友(非官方文档):cplusplus

官方文档(同步更新):cppreference



4 ~> 派生类的默认成员函数专题

4.4 实现一个不可继承类实现

4.4.1 间接实现:【C++98】构造函数私有的类不能被继承

基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。

运行一下------

这里必须调用基类的构造,但是基类这里是私有的,看不见,所以就不能再调用了。

4.4.2 直接实现:final关键字修改基类

C++11新增了一个final关键字,final修改基类,派生类就不能继承了。

我们同样运行一下------

4.4.3 代码实现

cpp 复制代码
// C++11的方法 
class Base final
{
public:
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;
private:
	// C++98的方法
	/*Base()
	{}*/
};

class Derive :public Base
{
	void func4() { cout << "Derive::func4" << endl; }
protected:
	int b = 2;
};

int main()
{
	Base b;
	Derive d;

	return 0;
}

4.4.4 final关键字

在本文博主不展开讲,下篇博客,博主会介绍C++进阶中又一个重要的模块------【多态】,在【多态】中,博主会介绍两个涉及到【多态】中的重写相关知识点的关键字:override和final

也就是说,final充当了两个作用------

(1)直接实现一个不能被继承的类(【继承】篇涉及知识点);

(2)不让重写基类虚函数(【多态】篇即将涉及的知识点)。


5 ~> 继承体系中的友元关系

5.1 友元与继承的关系特性

友元关系不能继承。

这句话 也就是说基类友元不能访问派生类私有和保护成员

5.2 解决方案

把派生类也变成基类的友元的友元即可。

5.3 最佳实践

5.3.1 正确代码演示

cpp 复制代码
// 前置声明
class Student;

class Person
{
	// 友元关系不能被子类继承
	friend void Display(const Person& p, const Student& s);
public:
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;
}

运行一下------

这段代码是能顺利运行的,但是,我们看下面这段代码------

cpp 复制代码
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;
}

5.3.2前置声明的必要性

不加前置声明会报下面的错------

5.3.3 友元关系不能继承

因为友元关系不能继承,因此我们要给派生类也变成基类友元的友元。


6 ~> 静态成员在继承中的特性

6.1 静态成员共享机制:父子共用同一份

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个派生类,都只有一个static成员实例。

6.2 静态与非静态成员对比

6.2.1 非静态成员:实例独立

非静态成员的继承是父类和子类各一份,地址不一样。

6.2.2 静态成员:类间共享

静态成员的继承是父类和子类共用同一份,地址也一样。

**6.3 实践出真知:**静态成员继承实践案例

cpp 复制代码
class Person
{
public:
	string _name;
	static int _count;
};

// 静态成员会被继承
int Person::_count = 10;

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

运行一下------

如上图所示。

6.4 共用一份的体现


7 ~> 单继承 vs 多继承(以及菱形继承问题详解)

事先说明:多继承是个大坑!!!

7.1 单继承 vs 多继承

7.1.1 概念对比

7.1.2 最佳实践

cpp 复制代码
class Person
{
public:
	Person(const char* name)
		:_name(name)
	{ }

public:
	string _name; // 姓名
	//int* _age; // 年龄
	//int _tel; // 电话
	//string _address; // 地址
};

//class Student : public Person
class Student : virtual public Person // 虚继承
{
public:
	Student(const char* name,int num)
		:Person(name)
		,_num(num)
	{ }
protected:
	int _num; // 学号
};

//class Teacher : public Person
class Teacher : virtual public Person
{
public:
	Teacher(const char* name, int id)
		:Person(name)
		,_id(id)
	{ }
protected:
	int _id; // 教职工编号
};

7.2 菱形继承问题详解

7.2.1 菱形继承的概念

7.2.2 菱形继承的数据冗余与二义性问题

基类数据越多,这两个问题越严重。

数据冗余:如下图所示,Person有两个------

**二义性:访问不明确~>**指定类域勉强解决

7.2.3 虚继承解决方案

菱形继承------多继承延伸的坑

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

**因此设计了"菱形虚拟继承"来解决,**下面我们会介绍虚继承。

7.2.4 虚继承机制与virtual关键字

关键词virtual加在腰部位置,如下图所示------

都加上virtual可不可以?------当然不行。

换个说法,药能多吃吗?会影响底层的空间模型,能编译通过但底层空间会乱

虚继承太复杂了,无论是使用还是底层,都太复杂。

不要玩菱形继承!!!当然,菱形继承也是有应用的,库里面的IO库就是搞成菱形继承的,IO库的使用会专门在IO库讲。

7.2.5 菱形继承的问题

7.2.6 实践

cpp 复制代码
class Person
{
public:
	Person(const char* name)
		:_name(name)
	{ }

public:
	string _name; // 姓名
	//int* _age; // 年龄
	//int _tel; // 电话
	//string _address; // 地址
};

//class Student : public Person
class Student : virtual public Person // 虚继承
{
public:
	Student(const char* name,int num)
		:Person(name)
		,_num(num)
	{ }
protected:
	int _num; // 学号
};

//class Teacher : public Person
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(name1)
		,Student(name2,1)
		,Teacher(name3,2)
		,_majorCourse("计算机")
	{ }

protected:
	string _majorCourse; // 主修课程
};

int main()
{
	// 思考一下这里a对象中_name是"张三","李四","王五"中的哪一个?
	Assistant a("张三", "李四", "王五");

	// 编译报错:error C2385:对"_name"的访问不明确
	/*Assistant a;*/
	a._name = "peter";

	// 需要显示指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题无法解决 
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";

	// 再比如
	a.Student::_name = "小黄";
	a.Teacher::_name = "黄老师";

	cout << endl;

	return 0;
}

运行一下------

7.2.7 可以设计出多继承,不建议设计出菱形继承

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

7.3 IO库中的菱形虚拟继承

7.4 多继承中的指针偏移问题

7.4.1 题目

下面说法正确的是( )

A. p1 == p2 == p3 B. p1 < p2 < p3
C. p1 == p3 != p2 D. p1 != p2 != p3

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

7.4.2 答案解析

正确答案:C

7.4.3 运行

运行一下------


8 ~> 继承与组合设计模式对比

8.1 基本概念:is-a vs has-a

1、public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。

2、组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。

3、继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-boxreuse)。术语"白箱"是相对可视性而言:在继承方式中,基类的内部细节对派生类可见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。

4、对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-boxreuse),因为对象的内部细节是不可见的。对象只以"黑箱"的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

5、优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),就用组合。

8.2 继承与组合关系对比

8.3 最佳实践

cpp 复制代码
// 继承和组合
// 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用组合。

cpp 复制代码
// 继承和组合
// 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;
}

运行一下------

8.4 继承 vs 组合

8.4.1 白盒复用与黑盒复用

白盒测试:更加难,一般由研发人员写并且测试,看得见、透明------保护、私有都可使用;

黑盒测试:看不见,不透明;

白盒 / 黑盒好坏的依据是从软件设计角度出发的。

8.4.2 软件设计中的选择策略

高内聚,低耦合------可维护性(其中一个修改,另一个不受影响)。

8.4.3 模块

打成一个个模块,哪个出问题改哪个,不受影响。

组件:静态库、动态库------不可执行的二进制文件------

(1)编译时间降低;

(2)看不到源码(二进制编译)。

8.4.4 博主手记

如下图所示------

8.5 继承与组合最佳实践指南:继承和组合哪个更好?

**实践的角度:优先使用组合;既符合继承也符合组合,我们使用组合;**但是要注意:是"优先使用组合",不是必须使用,但是像多态这些需要继承的地方还是要用继承。


完整代码示例与实践演示

Test.cpp:

cpp 复制代码
#define  _CRT_SECURE_NO_WARNINGS  1
#include<iostream>
#include<vector>
#include<list>
using namespace std;

//// 前置声明
//class Student;
//
//class Person
//{
//	// 友元关系不能被子类继承
//	friend void Display(const Person& p, const Student& s);
//public:
//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 = 10;
//
//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;
//}

//class Person
//{
//public:
//	Person(const char* name)
//		:_name(name)
//	{ }
//
//public:
//	string _name; // 姓名
//	//int* _age; // 年龄
//	//int _tel; // 电话
//	//string _address; // 地址
//};

////class Student : public Person
//class Student : virtual public Person // 虚继承
//{
//public:
//	Student(const char* name,int num)
//		:Person(name)
//		,_num(num)
//	{ }
//protected:
//	int _num; // 学号
//};
//
////class Teacher : public Person
//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(name1)
//		,Student(name2,1)
//		,Teacher(name3,2)
//		,_majorCourse("计算机")
//	{ }
//
//protected:
//	string _majorCourse; // 主修课程
//};
//
//int main()
//{
//	// 思考一下这里a对象中_name是"张三","李四","王五"中的哪一个?
//	Assistant a("张三", "李四", "王五");
//
//	// 编译报错:error C2385:对"_name"的访问不明确
//	/*Assistant a;*/
//	a._name = "peter";
//
//	a.Student::_name = "小黄";
//	a.Teacher::_name = "黄老师";
//
//	cout << endl;
//
//	return 0;
//}

//// 继承 is-a
//class stack : public vector
//{ };
//
//// 组合 has-a
//class stack
//{
//	vector _v;
//};

//// 写一个不能被继承的类
//
//class Base
//{
//public:
//	void func5() { cout << "Base::func5" << endl; }
//protected:
//	int a = 1;
//private:
//	// C++98的办法:构造函数私有的类不能被继承 ------ 间接
//	Base()
//	{}
//};

//class Derive : Base
//{};

class Base final
{
public:
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;
};

class Derive : Base // 修饰基类,派生类不能再被继承了 ------ 直接
{};

int main()
{
	Derive d;

	return 0;
}

结尾

往期回顾:

【C++:继承】面向对象编程精要:C++继承机制深度解析与最佳实践

结语:都看到这里啦!那请大佬不要忘记给博主来个"一键四连"哦!

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡

૮₍ ˶ ˊ ᴥ ˋ˶₎ა

相关推荐
微露清风3 小时前
系统性学习C++-第七讲-string类
java·c++·学习
ezreal_pan3 小时前
架构权衡与实践:基于“约束大于规范”的缓存组件封装
redis·cache·1024程序员节
少年码客3 小时前
英文 PDF 文档翻译成中文的优质应用
人工智能·1024程序员节
m0_748233643 小时前
C++开发中的常用设计模式:深入解析与应用场景
javascript·c++·设计模式
勇闯逆流河3 小时前
【C++】哈希表:除留余散法和哈希桶的实现
c++·哈希算法·散列表
hweiyu003 小时前
Gradle 增量构建与构建缓存:自定义 Task 如何实现 “只构建变化内容”?
gradle·1024程序员节
报错小能手3 小时前
项目——基于C/S架构的预约系统平台(3)
linux·开发语言·笔记·学习·架构·1024程序员节
cxr8283 小时前
涌现的架构:集体智能框架构建解析
人工智能·语言模型·架构·1024程序员节·ai智能体·ai赋能
lsx2024063 小时前
jQuery Mobile 实例
开发语言