【C++高阶】C++继承学习手册:全面解析继承的各个方面

📝个人主页🌹:Eternity._
⏩收录专栏⏪:C++ “ 登神长阶 ”
🤡往期回顾🤡:模板进阶
🌹🌹期待您的关注 🌹🌹

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

继承

  • 📖1. 继承的概念及定义
    • ⛰️继承的概念
    • 🌄继承定义
  • 📙2. 基类和派生类对象赋值转换
  • 📕3. 继承中的作用域
    • 🎩成员变量隐藏
    • 🎈成员函数隐藏
  • 📚4. 派生类的默认成员函数
    • 🧩默认成员函数
    • 🧩派生类默认函数特征
  • 📒5. 友元与静态成员变量
    • 🍂友元
    • 🍁静态成员
  • 📜6. 多继承
    • 🌞菱形继承
    • 🌙虚拟继承
    • ⭐虚拟继承解决数据冗余和二义性的原理
  • 🔥7. 总结


前言: 当我们踏上C++编程的旅程时,继承无疑是一个无法回避且至关重要的概念。作为面向对象编程的三大特性之一,继承不仅让我们能够创建出层次清晰、结构合理的代码,还极大地提高了代码的可重用性和可维护性。在本文中,我们将一起深入探讨C++继承的奥秘,从基础概念到高级应用,逐步揭开它的神秘面纱

C++继承允许我们定义一个基类(或称为父类),并从这个基类中派生出新的类(称为派生类、子类)。派生类会继承基类的成员和成员函数,同时还可以添加自己的成员和成员函数。这种能力使得我们能够构建出复杂的类层次结构,实现代码的模块化和复用

在本文的学习中我们不仅仅要了解继承的基本概念。在实际编程中,我们还需要掌握如何正确使用继承、如何避免常见的继承陷阱、以及如何利用继承来优化我们的代码结构。因此,本文将带领大家从多个角度全面学习C++继承,包括继承的语法规则、访问控制、构造函数与析构函数的调用、多重继承与菱形继承等问题

让我们一起踏上学习C++继承的旅程吧!


📖1. 继承的概念及定义

⛰️继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用

继承代码示例

class A 
{
public:
	void func()
	{
		cout << "A::func()" << endl;
	}
protected:
	int _a = 10;
};

// 继承后父类A的成员_a(成员函数+成员变量)都会变成子类的一部分
class B : public A
{
public:
	// ......
protected:
	int _b = 100;
};

int main()
{
	A a ;
	B b;
	a.func();
	b.func(); // b可以调用A中的成员函数
	return 0;
}

🌄继承定义

我们从刚刚的代码示例可以看到A是基类(父类),B是派生类(子类)

定义格式
在这里插入图片描述
注意:在定义继承的时候继承方式可以省略不写,如果不写则是根据基类的定义来决定默认继承方式,但是建议定义时带上继承方式

class定义的类默认private继承,struct定义的类默认public继承

继承关系和访问限定符
在这里插入图片描述
在这里插入图片描述


继承基类成员访问方式的变化
继承方式和访问限定符都有三种,虽然它们组合一共有9中能使用的方法,但是我们最常用的只有红色框里面的两种用法
在这里插入图片描述

这里我们有以下几点需要注意:

  • 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
  • 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的
  • 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected> private
  • 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式
  • 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强

📙2. 基类和派生类对象赋值转换

关于赋值规则这里我们先提两点:

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去
  • 基类对象不能赋值给派生类对象

在这里插入图片描述


我们在讲C++入门知识的时候讲过,引用类型不同的变量时,会产生一个临时变量,临时变量具有常性,需要const修饰,但是在继承中就不需要const修饰

代码示例

int main()
{
	int c = 1;
	double d = 1.1;
	const int& r = d; // 中间产生了一个临时变量,临时变量具有常性,需要const修饰
	
	B b;
	A a = b; // 子类可以赋值给基类
	// b = a; // false, 基类不可以赋值给子类
	
	A& ra = b; // is-a 的关系中间不会产生临时对象,父子类的赋值兼容规则(切割/切片)
	return 0;
}

继承中的对象是is-a 的关系,它们中间并不会产生临时对象,这就是父子类的赋值兼容规则(切割/切片)


📕3. 继承中的作用域

关于作用域的注意事项:

  • 在继承体系中基类和派生类都有独立的作用域。
  • 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  • 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  • 注意在实际中在继承体系里面最好不要定义同名的成员

🎩成员变量隐藏

当继承的基类与子类有同名的成员变量时,不指定的话,会调用子类的成员变量

代码示例

class A 
{
protected:
	int _a = 10;
};

class B : public A
{
public:
	void Print()
	{
		cout << "_a:" << _a << endl;
		// cout << "A: _a:" << A::_a << endl; // 要想成功打印A类的元素必须要指定
		cout << "_b:" << _b << endl;
	}
protected:
	int _a = 99;
	int _b = 100;
};

int main()
{
	B b;
	// 成员变量同名
	// A 和 B中的 _a 构成隐藏
	b.Print(); // // _a = 99 , _b = 100; 就近原则
	return 0;
}

🎈成员函数隐藏

在继承中,同名函数并不会构成函数重载,因为他们在不同的作用域,每个类都是独立的,成员函数满足函数名相同就构成隐藏

代码示例

class A 
{
public:
	void func()
	{
		cout << "func()" << endl;
	}
protected:
	int _a = 10;
};

class B : public A
{
public:
	// 
	void func(int b)
	{
		cout << "func(int b)" << endl;
	}
protected:
	int _b = 100;
};

int main()
{
	B b;
	// 成员函数同名
	// A 和 B中的 func() 构成隐藏
	b.func(); // 打印“func(int b)”
}

📚4. 派生类的默认成员函数

🧩默认成员函数

默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个
在这里插入图片描述

相关文章:默认成员函数


🧩派生类默认函数特征

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
  • 派生类的operator=必须要调用基类的operator=完成基类的复制
  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序
  • 派生类对象初始化先调用基类构造再调派生类构造
  • 派生类对象析构清理先调用派生类析构再调基类的析构
  • 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系

在这里插入图片描述
综上所述:关于基类和子类的调用顺序,一般情况都是先父后子,但是析构必须先子后父,来避免析构完父类之后,子类出错

继承默认函数的实现代码示例

class A 
{
public:
	A()
	{}

	A(int a)
		:_a(a)
	{
		cout << "A()" << endl;
	}

	A(const A& a)
		:_a(a._a)
	{
		cout << "A(const A& a)" << endl;
	}

	A& operator=(const A& a)
	{
		cout << "A& operator=(const A& a)" << endl;
		if (&a != this)
		{
			_a = a._a;
		}
		return *this;
	}

	~A()
	{
		cout << "~A()" << endl;
	}
protected:
	int _a = 10;
};

class B : public A
{
public:
	B()
	{}

	B(int a, int b)
		// :_a(a) // _a不是基类成员不能这样初始化
		:A(a)
		,_b(b)
	{
		cout << "B()" << endl;
	}

	B(const B& b)
		// :_a(a) // _a不是基类成员不能这样初始化
		:A(b)
		, _b(b._b)
	{
		cout << "B(const A& a, const B& b)" << endl;
	}

	B& operator=(const B& b)
	{
		cout << "B& operator=(const B& b)" << endl;
		if (&b != this)
		{
			// 需要调用A类的 operator=
			A::operator=(b);
			_b = b._b;
		}
		return *this;
	}

	// 析构函数会先析构父类,而有时候先析构父类,子类会出事
	// 不需要显式调用父类析构
	~B()
	{
		cout << "~B()" << endl;
	}
protected:
	int _b = 100;
};
int main()
{
	B b1(1, 100);
	B b2(b1);

	B b3(2, 200);
	b1 = b3;
	return 0;
}

📒5. 友元与静态成员变量

🍂友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员,因为朋友的朋友不一定也是自己的朋友,如果基类,子类都想使用必须都在各自的域里面声明

代码示例

class A 
{
public:
	friend void Print(const A& a, const B& b);
protected:
	int _a = 10;
};

class B : public A
{
public:
	// 
protected:
	int _b = 100;
};

void Print(const A& a, const B& b)
{
	cout << a._a << endl;
	cout << b._b << endl;
}

int main()
{
	A a;
	B b;
	Print(a, b);
}

🍁静态成员

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

代码示例

class A 
{
public:
	A()
	{
		++_count;
	}
	static int _count;
protected:
	int _a = 10;
};
int A::_count = 0;
class B : public A
{
public:
	// 
protected:
	int _b = 100;
};

int main()
{
	A a;
	B b;
	cout << A::_count << endl;
}

📜6. 多继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

class B : public A

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

class D : public B , public C

菱形继承:菱形继承是多继承的一种特殊情况。

class B : public A
{......};
class C : public A
{......};
class D : public B , public C

🌞菱形继承

在这里插入图片描述

class A
{
protected:
	int _a = 1;
};
class B :public A
{
protected:
	int _b = 2;
};
class C :public A
{
protected:
	int _c = 3;
};
class D :public B, public A
{
protected:
	int _d = 4;
};

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在D的对象中_a成员会有两份,我们在访问的时候无法明确知道访问的是哪一个,必须要显示指定访问哪个父类的成员,但是数据冗余任然无法解决!
在这里插入图片描述


🌙虚拟继承

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在A和B的继承A时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用

代码示例

class A
{
protected:
	int _a = 1;
};
class B : virtual public A
{
protected:
	int _b = 2;
};
class C : virtual public A
{
protected:
	int _c = 3;
};
class D : public B, public A
{
protected:
	int _d = 4;
};

⭐虚拟继承解决数据冗余和二义性的原理

  • 虚拟继承通过将共同的祖先类(即虚基类)的拷贝在派生类对象中只保留一份,来解决这个问题。具体来说,虚拟继承会在内存中创建一个虚基表,并在派生类对象中存储一个指向这个虚基表的指针(即虚基表指针)。虚基表中存的偏移量。通过偏移量可以找到下面的A,而无需在派生类对象中多次存储这些数据成员。因此,虚拟继承通过减少重复存储的数据成员来消除数据冗余
  • 虚拟继承通过改变派生类访问虚基类成员的方式来解决这个问题。在虚拟继承中,派生类对象通过虚基表指针来访问虚基类(即共同祖先类)的成员。由于虚基表中存储了虚基类成员的地址,因此派生类对象可以明确地知道应该访问哪个虚基类成员,从而消除了二义性。

在这里插入图片描述

加上表中偏移量可以找到最底下的A


🔥7. 总结

回顾学习过程,我们学会了如何定义基类与派生类,掌握了访问控制规则,理解了构造函数与析构函数在继承中的作用,还探讨了多重继承及其带来的挑战。这些知识不仅丰富了我们的编程技能,更为我们解决实际问题提供了有力的工具

在结束对C++继承的学习之旅后,我们不禁感叹其强大的功能和灵活性。通过深入探究继承的基本概念、语法规则以及高级应用,我们逐渐揭开了其背后的奥秘,并体验到了它在面向对象编程中的独特价值

学习C++继承并非一蹴而就的过程。它需要我们不断地实践、思考、总结和创新。在未来的编程之路上,我们将继续深化对继承的理解,探索其更多的应用场景和高级特性,如虚继承、接口继承等,我们也要认识到继承并非万能的。在使用继承时,我们需要权衡其带来的好处和潜在的风险,避免过度使用导致代码结构复杂、难以维护。我们应该根据具体的需求和场景,选择最合适的编程范式和工具!!!
在这里插入图片描述

希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行!
谢谢大家支持本篇到这里就结束了,祝大家天天开心!

在这里插入图片描述
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=1m2qp8pe3h4nz

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/713394.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

每日一练——用队列实现栈

225. 用队列实现栈 - 力扣&#xff08;LeetCode&#xff09; Queue.h #pragma once #include<stdlib.h> #include<assert.h> #include<stdbool.h>typedef int QDataType;typedef struct QNode {QDataType data;struct QNode* next; } QNode;typedef struct …

案例 采用Springboot默认的缓存方案Simple在三层架构中完成一个手机验证码生成校验的程序

案例 Cacheable 是 Spring Framework 提供的一个注解&#xff0c;用于在方法执行前先检查缓存&#xff0c;如果缓存中已存在对应的值&#xff0c;则直接返回缓存中的值&#xff0c;而不执行该方法体。如果缓存中不存在对应的值&#xff0c;则执行方法体&#xff0c;并将方法的…

hugo 博客写作流程 (二)

写作工具 推荐两款软件,本人都使用过: typora和obsidian typora: https://github.com/woniu336/typora-007 新建文章 在博客目录找到shell/quickstart.sh双击打开即可 发布到Github 前提: 魔法上网在Github新建一个仓库 找到shell/quickstart.sh双击打开,选择7.SSH 管理…

Android采用Scroller实现底部二楼效果

需求 在移动应用开发中&#xff0c;有时我们希望实现一种特殊的布局效果&#xff0c;即“底部二楼”效果。这个效果类似于在列表底部拖动时出现额外的内容区域&#xff0c;用户可以继续向上拖动查看更多内容。这种效果可以用于展示广告、推荐内容或其他信息。 效果 实现后的…

代理设计模式,框架AOP思想

文章目录 引言&#x1f92a;代理对象(Proxy)如何开发一个代理对象开发中的业务层代码冗余问题开发静态代理类动态代理 引言&#x1f92a; 代理 (proxy) &#xff0c;举个生活中常见的现象&#xff0c;在之前网路还未走进大众的时代里&#xff0c;如果我们想买一些东西&#xf…

Java——构造器(构造方法)和 this

一、什么是构造器 构造器&#xff08;Constructor&#xff09;是Java类的一种特殊方法&#xff0c;用于初始化对象的状态。构造器在创建对象时被调用&#xff0c;可以对对象的成员变量进行初始化。 我之前的文章《Java——类和对象-CSDN博客》中也提到了构造器。 二、构造器…

pc repair

pc repair 修理电脑&#xff0c;换配件

【猫狗分类】Pytorch VGG16 实现猫狗分类1-数据清洗+制作标签文件

Pytorch 猫狗分类 用Pytorch框架&#xff0c;实现分类问题&#xff0c;好像是学习了一些基础知识后的一个小项目阶段&#xff0c;通过这个分类问题&#xff0c;可以知道整个pytorch的工作流程是什么&#xff0c;会了一个分类&#xff0c;那就可以解决其他的分类问题&#xff0…

马斯克在2024年特斯拉股东大会上的年度发言

马斯克表示&#xff0c;“如果市盈率是20或25倍&#xff0c;那就意味着&#xff0c;光是Optimus就能带来20万亿美元的市值。而自动驾驶汽车的市值可能在5到10万亿美元之间。因此&#xff0c;特斯拉的市值达到当今市值最高公司的10倍&#xff0c;是可以想象的&#xff0c;也是有…

一个在C#中集成Python的例子

一个在C#中集成Python的例子。在C#中可以执行Python脚本&#xff0c;在Python中也可以调用C#宿主中的功能&#xff08;clr.AddReference(Business)&#xff09;。 文件说明 Debug为执行目录 Mgr.exe为执行文件 Py\init.py为python初始化脚本 Py\Lib.zip为python需要的模块&…

大数据实训项目(小麦种子)-02、实训项目整体功能介绍与演示

文章目录 前言界面及功能描述实现功能描述技术选型界面展示首页界面功能1&#xff1a;HDFS&#xff0c;选择文件上传文件详细步骤 功能2&#xff1a;MapReduce预处理数据功能3&#xff1a;Hbase存储小麦种子数据并查询前10条记录功能4&#xff1a;Hive分析原始csv文件数据并ech…

【GO-OpenCV】go-cv快速配置

最近对golang实现目标检测心血来潮&#xff0c;尝试在没有sudo权限的平台配置go-cv,有所发现&#xff0c;索性多个平台都做尝试 安装Go语言&#xff08;Golang&#xff09; 通过包管理器安装&#xff08;适用于Debian/Ubuntu&#xff09;(有点慢) 更新包列表&#xff1a; sud…

简单的基于小波变换的图像压缩(Python)

2023 沃尔夫数学奖得主&#xff0c;给了杜克大学的Ingrid Daubechies&#xff08;多贝西&#xff09;教授 以色列沃尔夫基金会理事会成员 Michael Lin 教授在周二宣布: “Ingrid Daubechies is awarded the Wolf Prize for her work in the creation and development of wavel…

搭建k8s集群报错unknown command “\u00a0“ for “kubeadm init“

搭建k8s报错unknown command “\u00a0” for “kubeadm init” 网上搜了一下&#xff0c;是因为复制过来的命令前面包含了空格&#xff0c;将复制的命令放到idea可以清楚看到几个命令前面有空格&#xff0c;删除掉就好了&#xff0c;记录一下

设计模式-享元模式Flyweight(结构型)

享元模式(Flyweight) 享元模式是一种结构型模式&#xff0c;它主要用于减少创建对象的数量&#xff0c;减少内存占用。通过重用现有对象的方式&#xff0c;如果未找到匹配对象则新建对象。线程池、数据库连接池、常量池等池化的思想就是享元模式的一种应用。 图解 角色 享元工…

【团队成长】2024-24周周报-第一次组会人员分工48期推文预告

大家好&#xff01;我们是IndustryOR 团队&#xff0c;致力于分享业界落地的算法技术。欢迎关注微信公众号/知乎/CSDN【运筹匠心】 。 记录人&#xff1a;张哲铭&#xff0c;算法专家&#xff0c;某互联网大厂 【团队成长/个人成长】系列的推文会以 【工作周报】 的方式记录Ind…

【机器学习】人工智能与气候变化:利用深度学习与机器学习算法预测和缓解环境影响

&#x1f4dd;个人主页&#xff1a;哈__ 期待您的关注 目录 &#x1f525;引言 1.1 背景介绍 1.2 人工智能与机器学习的崛起 1.3 本文内容概述 &#x1f528;气候变化的挑战 2.1 现今气候变化带来的影响和挑战 2.2 引发关注的气候变化趋势和数据 &#x1f916;人工智能…

使用SpringBoot对接Kafka

Kafka是什么&#xff0c;以及如何使用SpringBoot对接Kafka 一、Kafka与流处理 我们先来看看比较正式的介绍&#xff1a;Kafka是一种流处理平台&#xff0c;由LinkedIn公司创建&#xff0c;现在是Apache下的开源项目。Kafka通过发布/订阅机制实现消息的异步传输和处理。它具有高…

VMware Workstation安装及使用详细教程

如何安装VMware Workstation的详细教程 一、准备工作 1. 下载VMware Workstation&#xff1a; 访问VMware官方网站&#xff0c;找到VMware Workstation的下载页面。根据您的操作系统&#xff08;Windows或macOS&#xff09;选择相应的版本进行下载。确保您的计算机满足VMwar…

牛客小白月赛96 解题报告 | 珂学家

前言 题解 A. 最少胜利题数 签到 n1 len(set(input())) n2 len(set(input()))if n1 < n2:n1, n2 n2, n1print (-1 if n1 6 else n1 - n2 1)B. 最少操作次数 思路: 分类讨论 只有-1,0,1,2这四种结果 特判 01, 10 n int(input()) s input()# 枚举 from collectio…