C++面向对象特性之继承篇

news2025/4/24 11:04:55

C语音是面向过程的语言,而C++在其之上多了面向对象的特性,面向对象三大特性:封装性、继承性、多态性。今天主包来讲讲自己学到的关于C++继承特性的知识。

一、继承是什么

        继承是提高代码复用的一种重要手段。正如C++的模版、泛型编程等等都是为了实现代码复用,避免我们重复写一些代码使得整体结构冗余。比如我们定义了一个类:Person,那么Person类该有的一些成员比如姓名,身份证号、年龄等等。那么现在我们想要有一个学生类Student,但是我们发现学生也有和Person类共有的一些特征比如姓名年龄等,但是学生类跟Person类不同肯定有其独特的地方,比如学生有学号、年级、成绩等等特性。 这时候为了避免重复写一些共有的成员,我们就使用继承的方式来使代码更加简洁。

        我们定义的Student类要继承Person类,那么我们就说Student是Person的子类或者派生类。而Person类是被Student继承了,那么这时候我们成Person类为父类基类。

二、怎么实现继承

1、继承的格式

        我们就以上面提到的Person类与Student类来举例说明

class Student: public Person
{
public:
//学生特有
    int _id;  //学号
    float _score; //考试成绩
    int grade; //年级
    //...
}

        首先,Student为派生类,Person为基类, 定义派生类时后面加上":",表示继承,而public表示继承方式,相应地,共有public、protected、private三种继承方式,,当然实际使用我们一般以public继承方式为主每种继承方式都会有着不同的特性,这个我们下个模块再讲。

2、一个完整的继承案例

        我们还是以上面的Person类与Student类为例。

        首先定义一个基类Person

class Person
{
public:
	//查询姓名
	void FindName()
	{
		cout << _name << endl;
	}

	//查询年龄
	void FindAge()
	{
		cout << _age << endl;
	}
protected:
	string _name = "张三"; //姓名
	int _age = 18;	       //年龄
	string _id;            //身份证号
	string _email;         //电子邮箱
};

        接着我们要搞一个派生类Student,Student类相比Person类多了学号、年级、分数等信息。

class Student :public Person
{
public:
	//查询学号
	void FindStuid()
	{
		cout << _stuid << endl;
	}

	//查询年级
	void FindGrade()
	{
		cout << _grade << endl;
	}
protected:
	int _stuid;               //学号
	float _score = 89.5;	  //分数
	string _grade = "大二";   //年级
};

        我们发现在子类中没有出现父类中的成员变量及成员函数,但是由于我们已经继承了Person类所以我们可以在某些情况下访问父类中的成员变量及成员函数,可以看到通过继承的方式明细提高了我们的代码复用,整体看着也更加清晰明了。

        通过测试,我们发现子类中虽然没有明确写出其关于姓名、年龄等成员变量及访问方式,但通过继承的方式,子类也具有与其父类相同的成员变量及成员函数。

三、继承的一些规则

1、继承基类成员访问方式的变化

        上面我们提到,继承方式有public、protected、private三种。那么这三种不同的继承方式当然会有区别了。

类成员\继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

        那么,这个表格什么意思呢,我们一一来看。

        首先看最后一行,基类中的成员都是private的,那么这时候对于派生类对象,他虽然继承了基类的成员,但他是不能够访问基类的成员,无论是在类里面还是在类外面。基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。

        例如,当基类成员访问限定符是protected时,在派生类中进行测试,发现能访问。

        当将基类成员访问限定符改为private后,就会出现错误,当然我们非要想访问也不是不可以,在基类中写一个public类型的get函数来间接访问。

        而当基类的成员是public类型限定时,那么派生类继承后这个成员在派生类中的访问限定方式就是继承方式,比如基类成员是public类型,你使用private的继承方式,那么在派生类中他就是private成员。同样对于基类的protected成员,我们发现继承方式是public时,在派生类中就是public限定,继承方式是protected,在派生类中就是protected限定,继承方式是private,在派生类中就是private。我们要知道,访问限定符权限由大到小是:public、protected、private。struct定义类时默认继承方式是public,class定义类时默认继承方式是private。

        于是我们就得出了一个结论,派生类的成员的访问限定符是基类成员访问限定符和继承方式中权限最小的那一个,基类的其他成员 在派⽣类的访问⽅式 == Min(成员在基类的访问限定符,继承⽅式)。

2、继承类模版

        这是一个stack继承类模版vector的例子

namespace xiaoli
{
	template<class T>
	class stack : public std::vector<T>
	{
	public:
		void push(const T& x)
		{
			vector<T>::push_back(x);
		}
		void pop()
		{
			vector<T>::pop_back();
		}
		const T& top()
		{
			return vector<T>::back();
		}
		bool empty()
		{
			return vector<T>::empty();
		}
	};
}

        需要注意,当我们的派生类对应的基类是类模版时,在成员中访问时需要指定类域。否则就会编译报错,找不到标识符。这是因为模版在需要时才会推导其类型进行实例化,直接访问会导致找不到。

3、基类和派生类之间的转换

        public继承方式的派生类对象可以将其赋值给基类的指针/基类的引用。但是按照我们的认识,派生类会在基类的基础上多一些成员,那么这个赋值是怎么实现的呢。有个形象的说法就是切片,派生类将属于基类的那部分切出来,因此基类的指针和引用指向的是派生类中切出来的基类部分。

        但是基类对象却不能赋值给派生类对象。而基类的指针或基类的引用可以通过强制类型转换赋给派生类的指针或引用,但是要求该基类的指针或基类的引用指向的是派生类对象时这个行为才是安全的。

int main()
{
	Student s1;
	// 派⽣类对象可以赋值给基类的指针/引⽤
	Person* p1 = &s1;
	Person& p2 = s1;
	// 派⽣类对象可以赋值给基类的对象是通过调⽤基类的拷⻉构造完成的
	Person p3 = s1;
	// 基类对象不能赋值给派⽣类对象,这⾥会编译报错
	s1 = p3;
	return 0;
}

        我们发现将基类对象赋给派生类对象时会发生报错。

4、继承中的隐藏

        在继承中,基类和派生类都有其对应的独立的作用域。

        如果基类和派生类中有同名成员,那么派生类成员会屏蔽对基类同名成员的直接访问,这就是隐藏。当然如果你非要想访问,可以通过指定类域显示访问。而对于成员函数,只要函数名相同就会构成隐藏,因此尽量不要定义同名的成员或成员函数。        

5、默认成员函数

        我们都知道类中有一些默认的成员函数,自己不显示实现,编译器就会自动生成一个,比如构造函数、析构函数、拷贝构造函数等等。那么在继承体系中,这几个函数的行为是怎样的呢。   

(1)构造函数     

        派生类的构造函数需要调用基类的构造函数来初始化基类的那一部分成员。如果基类没有默认构造函数,则需要在派生类构造函数的初始化列表中显示调用.。派生类对象初始化时先调用基类构造函数再调用派生类构造函数

(2)析构函数

        派生类的析构函数在被调用完成后会自动调用基类的析构函数清理基类成员,这样保证了先清理派生类成员再清理基类成员的顺序。派生类对象先调用派生类的析构函数再调用基类的析构函数。在不是虚继承的情况下,派生类析构函数和基类析构函数构成隐藏关系。

(3)拷贝构造函数

        派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类的拷贝初始化。

(4)赋值运算符重载函数

        派生类的赋值运算符重载函数operator=需要调用基类的operator=完成基类成员的赋值。要注意在重载赋值运算符时,派生类的operator=隐藏了基类的operator=,因此要指定类域使用。

6、实现一个不能被继承的类

方法一:

        将基类的构造函数私有化,派生类的构造函数需要调用基类的构造函数,当基类的构造函数使用private修饰后,无论以何种方式继承,派生类都不能调用,因此派生类无法实例化出对象。

方法二:

        使用final关键字,使用final关键字修饰基类,则其就不能被继承了。

class Person final
{
public:
    void func()
    {
        cout << _age << endl;
    }

protected:
    string _name;
    string _id;
    int _age;
};

7、友元与继承

        友元关系不能被继承,即基类的友元不能访问派生类的私有和保护成员。

class Student;
class Person
{
public:
friend void Print(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
friend void Print(const Person& p, const Student& s);
protected:
int _stuid;   // 学号
};
void Print(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuid << endl;
}

        解决方式就是使其也成为派生类的友元。

8、static静态成员与继承

        基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样static修饰的成员。⽆论派⽣出多少个派⽣类,都 只有⼀个static成员实例。

class Person
{
public:  
    //为了后面打印地址便于观察
	string _name;
	static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:
	int _stuid;
};


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

通过观察打印其地址我们成功验证了上面的结论,static修饰的成员在整个继承体系中只有一份。

四、多继承及菱形继承问题

1、多继承

        我们上面常见的一个派生类直接继承一个基类,这个继承关系称为单继承。那么在C++中,一个派生类也可以有两个或多个基类,这种继承关系就叫做多继承。

2、菱形继承

        由多继承就引发了菱形继承问题,菱形继承是多继承的⼀种特殊情况。下面为网上随便找的一张图用来说明。

可以看到,类A和类B分别继承了类Base,而类C又继承了类A和类B,这就是典型的一种菱形继承。

可以看出菱形继承有数据冗余和⼆义性的问题。对于基类Base的成员,在C中会有两份,那么首先就造成了数据冗余,因为多了重复的一份,其次最主要的是二义性问题,如果类C想要访问类Base中的成员,有两份,就会发生冲突。这时候为了能够明确访问对象,就需要指明访问哪个基类的成员。
//错误示例
C c_tmp;
c_tmp._name = "Tom";
//正确示例
C c_tmp;
c_tmp.A::_name = "Jone";
C_tmp.B::_name = "Peter";

这种方式虽然解决了二义性的问题,但数据冗余的问题仍然没有解决。

3、虚继承

        上面我们可以看到,由于C++多继承的原因,出现了菱形继承的问题。为了解决这种问题,C++又提出了虚继承。通过在继承方式前加一个virtual的方式实现虚继承

// 使⽤虚继承Person类
class Student : virtual public Person
{
protected:
int _stuid; //学号
};

// 使⽤虚继承Person类
class Teacher : virtual public Person
{
protected:
int _teachid; //教职工编号
};

       

//博士
class Doctor : public Student, public Teacher
{
protected:
int _count; //论文数量
};

int main()
{
// 使⽤虚继承,可以解决数据冗余和⼆义性
Doctor a;
a._name = "Peter";
return 0;
}

我们在使用多继承时,尽量避免使用菱形继承,会使得代码耦合度变高,变得更复杂,不利于我们的维护代码,可以看到多继承也是有一定缺陷的,因此java就取消了多继承这个东西。

五、继承和组合的讨论

        通过继承,每个派生类对象都是一个基类对象。而对于组合,假设B组合了A,那么每个B对象中都有一个A对象。这两种方式会带来很多差别。在前面使用容器适配器模拟实现stack时,使用vector作为container,这就是一种组合关系。

        在继承⽅式中,基类的内部细节对派⽣类可⻅。继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依赖关系很强,耦合度⾼。

        组合方式是继承之外另一种复用代码的选择。组合类之间没有很强的依赖关系,耦合度低,代码维护性好。

        在实际使用中,我们尽量多使用组合而非继承,当然这并不是绝对的,要依据代码适配哪种方式来决定,比如类之间的关系更适合继承或者需要实现多态,那么就需要继承。在既适合继承又适合组合时,我们尽量选用组合方式实现代码复用。

        

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【AI News | 20250423】每日AI进展

AI Repos 1、suna Suna是一款完全开源的AI助手&#xff0c;旨在通过自然对话帮助用户轻松完成现实世界的任务。它作为您的数字伙伴&#xff0c;提供研究、数据分析和日常问题解决等功能&#xff0c;并结合强大的能力与直观的界面&#xff0c;理解您的需求并交付成果。Suna的工…

【学习准备】算法和开发知识大纲

1 缘起 今年&#xff08;2025年&#xff09;的职业升级结果&#xff1a;不通过。没办法升职加薪了。 需要开始完善学习&#xff0c;以应对不同的发展趋势&#xff0c;为了督促自己学习&#xff0c;梳理出相关学习大纲。 分为算法和开发两部分。 算法&#xff0c;包括基础算法和…

第七篇:linux之基本权限、进程管理、系统服务

第七篇&#xff1a;linux之基本权限、进程管理、系统服务 文章目录 第七篇&#xff1a;linux之基本权限、进程管理、系统服务一、基本权限1、什么是权限&#xff1f;2、为什么要有权限&#xff1f;3、权限与用户之间的关系&#xff1f;4、权限对应的数字含义5、使用chmod设定权…

爬虫案例-爬取某企数据

文章目录 1、准备要爬取企业名称数据表2、爬取代码3、查看效果 1、准备要爬取企业名称数据表 企业名称绍兴市袍江王新国家庭农场绍兴市郑杜粮油专业合作社绍兴市越城区兴华家庭农场绍兴市越城区锐意家庭农场绍兴市越城区青甸畈家庭农场绍兴市袍江王新国家庭农场绍兴市袍江月明…

学习笔记—C++—string(一)

目录 string 为什么学习string的类 string类的常用接口 string类对象的常见构造 string类对象的访问及遍历操作 operator[] 迭代器 范围for auto 迭代器&#xff08;二&#xff09; string类对象的容量操作 size,length,max_size,capacity,clear基本用法 reserve 提…

GPLT-2025年第十届团体程序设计天梯赛总决赛题解(共计266分)

今天偶然发现天梯赛的代码还保存着&#xff0c;于是决定写下这篇题解&#xff0c;也算是复盘一下了 L1本来是打算写的稳妥点&#xff0c;最后在L1-6又想省时间&#xff0c;又忘记了insert&#xff0c;replace这些方法怎么用&#xff0c;也不想花时间写一个文件测试&#xff0c…

MySQL数据库精研之旅第十期:打造高效联合查询的实战宝典(一)

专栏&#xff1a;MySQL数据库成长记 个人主页&#xff1a;手握风云 目录 一、简介 1.1. 为什么要使用联合查询 1.2. 多表联合查询时的计算 1.3. 示例 二、内连接 2.1. 语法 2.2. 示例 三、外连接 4.1. 语法 4.2. 示例 一、简介 1.1. 为什么要使用联合查询 一次查询需…

15.FineReport动态展示需要的列

1.首先连接自带的sqlite数据库&#xff0c;具体方法参考下面的链接 点击查看连接sqlite数据库 2.文件 – 新建普通报表 3.新建数据库查询 4.查询自带的销售明细表 5.把数据添加到格子中&#xff0c;并设置边框颜色等格式 6.查询新的数据集&#xff1a;column 7.点笔 8.全部添…

Windows云主机远程连接提示“出现了内部错误”

今天有人反馈说有个服务器突然连不上了&#xff0c;让我看下什么问题&#xff0c;我根据他给的账号密码试了下发现提示“出现了内部错误”&#xff0c;然后就是一通排查 先是查看安全组&#xff0c;没发现特别的问题&#xff0c;因为也没有调过这块的配置 然后通过控制台登录进…

最新扣子(Coze)案例教程:Excel数据生成统计图表,自动清洗数据+转换可视化图表+零代码,完全免费教程

大家好&#xff0c;我是斜杠君。 知识星球群有同学和我说每天的工作涉及很多数据表的重复操作&#xff0c;想学习Excel数据表通过大模型自动转数据图片的功能。 今天斜杠君就带大家一起搭建一个智能体&#xff0c;以一个销售行业数据为例&#xff0c;可以快速实现自动清洗Exc…

如何安装Visio(win10)

首先下载下面这些文件 HomeStudent2021Retail.img officedeploymenttool_17531-20046.exe office中文语言包.exe 确保这些文件都在一个文件夹内&#xff08;我已经上传这些资源&#xff0c;这些资源都是官网下载的&#xff09; 官网资源下载教程 1.下载Office镜像&#xff0…

建筑安全员 A 证与 C 证:差异决定职业方向

在建筑行业的职业发展道路上&#xff0c;安全员 A 证和 C 证就像两条不同的岔路&#xff0c;它们之间的差异&#xff0c;在很大程度上决定了从业者的职业方向。 从证书性质和用途来看&#xff0c;A 证是从业资格证书&#xff0c;更像是一把开启安全管理高层岗位的 “金钥匙”。…

(19)VTK C++开发示例 --- 分隔文本读取器

文章目录 1. 概述2. CMake链接VTK3. main.cpp文件4. 演示效果 更多精彩内容&#x1f449;内容导航 &#x1f448;&#x1f449;VTK开发 &#x1f448; 1. 概述 本例采用坐标和法线&#xff08;x y z nx ny nz&#xff09;的纯文本文件&#xff0c;并将它们读入vtkPolyData并显示…

Redis从入门到实战先导篇

前言&#xff1a;本节内容包括虚拟机VMware的安装&#xff0c;Linux系统的配置&#xff0c;FinalShell的下载与配置&#xff0c;Redis与其桌面客户端的安装指导,便于后续黑马Redis从入门到实战的课程学习 目录 主要内容 0.相关资源 1.VMware安装 2.Linux与CentOS安装 3.Fi…

JavaScript 防抖和节流

方法一&#xff1a;使用lodash库的debounce方法 方法二&#xff1a;手写防抖函数 function debounce(fn,t){// 1.声明一个定时器变量 因为需要多次赋值 使用let声明let timer // 返回一个匿名函数return function(){if(timer){// 如果定时器存在清除之前的定时器 clearTimeout(…

Spring Boot 启动时 `converting PropertySource ... to ...` 日志详解

Spring Boot 启动时 converting PropertySource ... to ... 日志详解 1. 日志背景 在 Spring Boot 应用启动过程中&#xff0c;会加载并处理多种 配置源&#xff08;如 application.properties、系统环境变量、命令行参数等&#xff09;。这些配置源会被封装为 PropertySource…

分割数据集中.json格式标签转化成伪彩图图像

一、前言 图像分割任务中&#xff0c;分割数据集的转换和表示方式对于模型训练至关重要。目前主要有两种常见的分割结果表示方法&#xff1a; 1. 转化为TXT文件 这种方式通常使用一系列的点&#xff08;坐标&#xff09;来表示图像中每个像素的类别标签。每个点通常包含像素…

Linux之彻底掌握防火墙-----安全管理详解

—— 小 峰 编 程 目录&#xff1a; 一、防火墙作用 二、防火墙分类 1、逻辑上划分&#xff1a;大体分为 主机防火墙 和 网络防火墙 2、物理上划分&#xff1a; 硬件防火墙 和 软件防火墙 三、硬件防火墙 四、软件防火墙 五、iptables 1、iptables的介绍 2、netfilter/…

# 构建和训练一个简单的CBOW词嵌入模型

构建和训练一个简单的CBOW词嵌入模型 在自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;词嵌入是一种将词汇映射到连续向量空间的技术&#xff0c;这些向量能够捕捉词汇之间的语义关系。在这篇文章中&#xff0c;我们将构建和训练一个简单的Continuous Bag of Words…

Collection集合,List集合,set集合,Map集合

文章目录 集合框架认识集合集合体系结构Collection的功能常用功能三种遍历方式三种遍历方式的区别 List集合List的特点、特有功能ArrayList底层原理LinkedList底层原理LinkedList的应用场list:电影信息管理模块案例 Set集合set集合使用哈希值红黑树HashSet底层原理HashSet集合元…