首页 > 编程语言 >[C++] 继承详解

[C++] 继承详解

时间:2024-12-12 17:32:16浏览次数:8  
标签:继承 成员 基类 C++ 详解 Student 派生类 public

目录

前言

演示用编译器及其标准

Dev C++ 6.7.5 Red panda  C++14

                                                  先   赞   后   看    养   成   习   惯  

正文

1、继承的概念与意义

2、继承的使用 

2.1继承的定义及语法

2.2基类与派生类间的转换

2.3继承中的作用域

 2.4派生类的默认成员函数

<1>构造函数

<2>赋值重载函数

3、继承与友元

4、继承与静态变量

5、继承与组合

有关继承的经典面试题

<1>C++有多继承,为什么java等语言没有?

<2>什么是菱形继承?多继承的问题是什么?

<3>继承和组合的区别?什么时候用继承?什么时候用组合?

后记


前言

今天zty带来的是继承详解,搞了5个小时,大家给个赞呗,zty还要上学,发作品会少一点

                                                  先   赞   后   看    养   成   习   惯 

                                                  先   赞   后   看    养   成   习   惯  

演示用编译器及其标准
Dev C++ 6.7.5 Red panda  C++14

                                                  先   赞   后   看    养   成   习   惯  

正文

1、继承的概念与意义

什么是继承?

答:继承就是继承

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。

通过继承联系在一起的类构成了一种层次关系,在这种关系中有一个基类(base class),其他类则是直接或间接地从基类继承过来的,这些继承来的类可以称为派生类(drived class)。基类通常有着层次关系中所有类共同拥有维护的成员,而每个派生类也有着自己各自特定的成员。 

一个简单的例子:一个学习管理系统,那么成员必定有学生,老师等等,这些是身份,归根到底是个人(基类)包含着名字、年龄、地址等基础信息。这些需要共同维护的就是基类的成员。

//共同维护的成员部分->基类
class Person
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证 
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
protected:
	string _name = "qsy"; // 姓名 
	string _address; // 地址 
	string _tel; // 电话 
	int _age = 18; // 年龄 
};
 
class Student : public Person
{
public:
	// 学习 
	void study()
	{
		// ...
	}
protected:
	int _stuid; // 学号 
};
class Teacher : public Person
{
public:
	// 授课 
	void teaching()
	{
		//...
	}
protected:
	string title; // 职称 
};
int main()
{
	Student s;
	Teacher t;
	s.identity();
	t.identity();
	return 0;
}

可以看到派生类可以访问基类成员

如果没有继承这种结构关系的话 Student和Teacher 都有姓名/地址/ 电话/年龄等成员变量,都有identity身份认证的成员函数,设计到两个类里面就是冗余的。更好地体现了继承是类设计层次的复用。

2、继承的使用 

2.1继承的定义及语法

这就是继承的语法格式

继承方式与访问限定符号一样有着三种,不同的继承方式与不同的类成员组合会是不同的情况 

总结一下规律:

<1>基类 private 成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。

将年龄变为私有验证一下是否继承到了派生类对象

<2>如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为 protected。可以看出保护成员限定符是因继承才出现的。

如果想要访问 private 成员可以在基类中成员函数访问,这样派生类可以间接访问到 private成员

<3>基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承方式),public  > protected > private。

Tip:class默认继承方式是 private,struct默认继承方式是public。最好显示写出继承方式

<4>在实际运用中⼀般使用都是 public 继承,几乎很少使用 protetced/private 继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实 际中扩展维护性不强。

2.2基类与派生类间的转换

基类与派生类之间是否有着类型的转换呢?

答案是可以的! public继承中有一个 is-a 概念:每个派生类都是一个特殊的基类对象

• public继承的派生类对象 可以赋值给 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。

• 基类对象不能赋值给派生类对象

• 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type  Information)的dynamic_cast 来进行识别后进行安全转换。

2.3继承中的作用域

继承体现中也有各自的作用域规则并且引出来一个隐藏概念,隐藏影响的只是编译器查找规则

1. 在继承体系中基类和派生类都有独立的作用域。

2. 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。 (在派生类成员函数中,可以使用 基类::基类成员显示访问)

3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。(区分重载)

4. 注意在实际中在继承体系里面最好不要定义同名的成员。

// Student的_num 和 Person的_num 构成隐藏关系,可以看出这样代码虽然能跑,但是⾮常容易混淆 
class Person
{
protected:
	string _name = "zty"; // 姓名 
	int _num = 114514; // ⾝份证号 
};
class Student : public Person
{
public:
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		cout << " 身份证号:" << Person::_num << endl;
		cout << " 学号:" << _num << endl;
	}
protected:
	int _num = 520250502; // 学号 
};
int main()
{
	Student s1;
	s1.Print();
	return 0;
};

访问的是哪个 _num 呢? 

可以看到派生类成员隐藏了基类的同名成员,直接访问了派生类的 _num 

同理,函数也有隐藏的现象

 2.4派生类的默认成员函数

6个默认成员函数,默认的意思就是指我们不写,编译器会自动生成⼀个,那么在派生类中,这 几个成员函数是如何生成的呢?

四个常见默认成员函数:

<1>构造函数

派生类的构造函数必须调用基类的构造函数初始化基类的那⼀部分成员。

class Person
{
public:
	Person(const char* name="zty") //全缺省函数,默认构造
		:_name(name)
	{
		cout << "Person()" << endl;
	}
protected:
	string _name;//姓名
};
 
class Student :public Person
{
public:
	//不显示实现默认构造,编译器生成的
	// 1. 内置类型->不确定
	// 2. 自定义类型->调用自定义类型的显示写的默认构造
	// 3. 基类成员看作一个整体,要求调用基类的默认构造
protected:
	int _num;//学号
	string _addrss;//地址
};
 
int main()
{
	Student s1;
	return 0;
}

如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。

如何实现一个不能被继承的类呢? 

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

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

<2>赋值重载函数

派生类的operator=必须要调用基类的operator=完成基类的复制。

赋值重载与拷贝构造类似一般编译器默认生成的就已经够用了,如果有资源申请的话才需要显示实现

Student& operator=(const Student& s)
{
	if (this != &s)
	{
		operator=(s);//派生类切片基类成员
		_num = s._num;
		_addrss = s._addrss;
	}
	return *this;
}

然后

<iframe allowfullscreen="true" data-mediaembed="csdn" frameborder="0" id="bH1nHqe3-1733739684956" src="https://live.csdn.net/v/embed/437928"></iframe>

bz

栈溢出,无限递归调用,我们不是想要调用基类的赋值函数吗?为什么调用了派生类的呢?

需要注意的是派生类的 operator= 隐藏了基类的operator= ,所以显示调用基类的operator= ,需要指定基类作用域

Student& operator=(const Student& s)
{
	if (this != &s)
	{
		//基类和派生类的赋值构成了隐藏关系 需要指定作用域
		Person::operator=(s);//派生类切片基类成员
		_num = s._num;
		_addrss = s._addrss;
	}
	return *this;
}

3、继承与友元

友元关系不能继承,也就是说基类友元不能访问派生类私有和保护成员。比如爸爸的朋友可以说是你的朋友吗?

 
class Student;//前置声明
class Person
{
public:
	friend void Display(const Person& p, const Student& s);//需要前置声明否则报错招不到 Student
protected:
	string _name; // 姓名 
};
class Student : public Person
{
protected:
	int _stuNum; // 学号 
};
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
}
int main()
{
	Person p;
	return 0;
}

问: 如果访问派生类的私有和保护成员呢? 

答: 报错

4、继承与静态变量

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

验证一下:

class A
{
public: 
	static int _a;
	int _aa;
};
class B :public A
{
public:
	int _b;
};
// static int _a = 1;报错
 int A::_a = 1;//注意定义的方式
int main()
{
	A  a;
	B b1;
	B b2;
	//这⾥的运行结果可以看到非静态成员_aa的地址是不⼀样的
	// 说明派生类继承下来了,⽗类派生类对象各有⼀份 
	cout << &a._aa << endl;
	cout << &b1._aa << endl;
	cout << endl;
	// 这⾥的运行结果可以看到静态成员 _a 的地址是⼀样的 
	//说明派生类和基类共用同⼀份静态成员 
	cout << &a._a << endl;
	cout << &b1._a << endl;
	cout << &b2._a << endl;
	cout << endl;
	//公有情况下 基类派生类都可以访问静态成员变量
	cout << a._a << endl;
	cout << b1._a << endl;
	cout << b2._a << endl;
	return 0;
}

也就说明他们共用一个_a变量,所以无论派生出多少个子类,都只有一个static成员实例

这个特性可以带来一种思路统计实例化类的数量个数,只需在构造函数中加入一个增加该静态变量的语句即可:

class Person
{
public:
	Person() { ++_count; }//子类的构造会调用父类构造
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。
};
int Person::_count = 0;
 
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
int main()
{
	Student s1;
	Student s2;
	Student s3;
	cout << " 人数 :" << Person::_count << endl;
	Student::_count = 0;
	cout << " 人数 :" << Person::_count << endl;
	return 0;
}

5、继承与组合

  • public继承是一种is-a(谁是什么)的关系。也就是说每个派生类对象都是一个基类对象。
  • 组合是一种has-a(谁有什么)的关系。假设B组合了A,每个B对象中都有一个A对象(也就是把A作为B的成员变量)

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


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

有关继承的经典面试题

<1>C++有多继承,为什么java等语言没有?

答:因为他善
历史原因!C++是先驱者(人的直觉认为多继承很合理,我感觉正常人都会想到多继承),并且c++中的多继承处理起来十分复杂,访问基类变量的过程就会很复杂!!!java等后来发展的语言见到c++中多继承的复杂,就干脆放弃了。

<2>什么是菱形继承?多继承的问题是什么?

答:是你跌
菱形继承如字面意思(两个父类的父类是同一个类就会发生菱形继承),多继承本身没什么问题,真正的问题是有多继承就可能发生菱形继承。菱形继承就有问题了:变量的二义性和继承冗杂。解决办法很简单就是虚拟继承,但是这样就会大大降低效率。

<3>继承和组合的区别?什么时候用继承?什么时候用组合?

答:前轱辘不转后轱辘转思密达
继承:通过扩展已有的类来获得新功能的代码复用方法
组合:新类由现有类的对象合并而成的类的构造方式

如果二者间存在一个“是”的关系,并且一个类要对另外一个类公开所有接口,那么继承是更好的选择
如果二者间存在一个“有”的关系,那么首选组合
!能用组合就用组合!!!能用组合就用组合!!!能用组合就用组合!!!

后记

作者:zty郑桐羽呀

联系方式:(不挂了,有事私信)

兄弟们给个赞呗

                                                  先   赞   后   看    养   成   习   惯  

标签:继承,成员,基类,C++,详解,Student,派生类,public
From: https://blog.csdn.net/zty20120913/article/details/144353976

相关文章

  • C++_运算符重载
    filesystemc++11在CMakeList.txtfind_package(BoostCOMPONENTSsystemfilesystemregexREQUIRED)include_directories(${Boost_INCLUDE_DIRS})target_link_libraries(projectname${Boost_LIBRARIES})程序#include<boost/filesystem.hpp>......
  • ubuntu网络配置工具netplan详解
     1.首先查看当前的 netplan配置文件:ls/etc/netplan/通常会有一个类似 01-netcfg.yaml 或 50-cloud-init.yaml 的文件。 2.编辑netplan配置文件firefly@firefly:~$vim/etc/netplan/01-netcfg.yamlnetwork:version:2renderer:networkdethernets:......
  • DGCRN模型数学原理及运算过程详解
    这是一份用于动态图卷积循环网络DGCRN模型理解的入门教程,采用论文公式与示例结合的方式阐述动态图的实现过程与图卷积GCN在RNN中的运用。本文关于数学原理部分不一定完全严谨,如有错误请在评论区指出。 模型来自论文:DynamicGraphConvolutionalRecurrentNetworkforTraf......
  • js 中的console使用详解
    console是JavaScript提供的一个全局对象,常用于调试和日志记录。它包含一组方法,用于在控制台中打印消息、显示数据以及调试程序。以下是console常见方法的详细介绍和用法。1.常用方法1.1console.log()作用:打印普通消息,最常用的日志方法。用法:console.log("Hello,wo......
  • C++ Boost库 tuple元组
    元组boost::tuple是Boost库中提供的允许程序员创建固定大小的元组,这些元组可以包含不同类型的元素。元组是一个数据结构,它可以存储多个值,这些值可以是不同类型的。boost::tuple是C++标准库中std::tuple的前身,后者在C++11标准中被引入。特点固定大小:一旦创建,boost::tuple的大小......
  • MyBatis映射文件配置:入参详解
    MyBatis映射文件配置:入参详解MyBatis是一个优秀的持久层框架,它简化了数据库操作的复杂性,尤其是在处理SQL语句和Java对象之间的映射时。在MyBatis中,映射文件的配置是非常重要的一部分,尤其是如何正确地处理SQL语句的入参。本文将详细介绍MyBatis映射文件中入参的配置方......
  • C++11 lock_guard和unique_lock
    一、RAII技术1、ResourceAcquisitionIsInitialization,资源获取即初始化,由C++之父提出。2、使用局部对象来管理资源的技术,称为资源获取即初始化。3、资源主要指操作系统中有限的东西,如内存、套接字等;局部对象指存储在栈里的对象,生命周期由操作系统来管理。4、资源......
  • Linux—软件包管理中APT工具详解
    本文为UbuntuLinux操作系统-第11弹~~今天接着讲昨天没讲完的知识点,主要内容是Linux软件包管理中的APT工具的详细运用知识~上期回顾:Linux—软件包管理概念及Deb软件包使用更多Linux相关内容点击......
  • PySide 信号与槽机制详解
    PySide信号与槽机制详解引言PySide是一个用于创建跨平台桌面应用程序的Python绑定库,它封装了Qt库的功能。Qt的核心特性之一是它的信号与槽(SignalsandSlots)机制,这是一种对象间通信的方法。在PySide中,这种机制允许开发者轻松地连接用户界面元素的行为和应用程序......
  • Java多线程与线程池技术详解(九)
    面对苦难的态度:《病隙碎笔》“不断的苦难才是不断地需要信心的原因,这是信心的原则,不可稍有更动。”孤独与心灵的成长:《我与地坛》“孤独的心必是充盈的心,充盈得要流溢出来要冲涌出去,便渴望有人呼应他、收留他、理解他。”目录上一篇博客习题讲解使用ReentrantLock实现......