C++三大特性(1)——继承

news2024/11/23 0:14:45

一.继承的概念及定义

概念

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

#include<iostream>
using namespace std;

//父类
class Person
{
public:
    void print()
    {
        cout << "name:" << _name << endl;
        cout << "age:" << _age << endl;
    }
protected:
    string _name = "bear";
    int _age = 20;
};

//子类
class Student : public Person
{
protected:
    int _stuid;
};


//子类
class Teacher : public Person
{
protected:
    int _jobid;
};



int main()
{
    Student s;
    Teacher t;
    s.print();
    t.print();
    return 0;
}

 在上述代码中, Student类与Teacher类都复用了Person的成员,继承后的父类的Person成员(成员函数+成员变量)都会变成子类的一部分。在监视窗口中可以看到s与都有一个Person类

 Student与Teacher都称为派生类或者子类,Person也称为基类或者父类。

 继承格式

继承关系和访问限定符

访问限定符有三种:

1.public访问

2.protected访问

3.private访问

继承方式也有三种:

1.public继承

2.protected继承

3.private继承

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

当基类的不同访问限定符的成员以不同的继承方式继承到派生类当中后,该成员在派生类的访问方式也会发生变化

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

例如Student类继承了Person类,但是无法在Student类中访问Person类中的private中的成员

 默认继承方式

假如我们不写基类的继承方式的话,那么:

class中,访问方式为private

struct中,访问方式为public

归纳总结五点

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

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

3. 基类成员访问方式的变化规则也不是无迹可寻的,我们可以认为三种访问限定符的权限大小为:public > protected > private。

4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式

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

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

派生类对象可以赋值给基类的对象,基类的指针以及基类的引用,因为在这个过程中,会发生基类和派生类对象之间的赋值转换

也可以看成切片,将派生类中基类的部分切出来赋值过去,如下图:

 派生类对象赋值给基类指针:

派生类对象赋值给基类引用:

 

例如下列代码:

//基类
class Person
{
protected:
	string _name; 
	string _sex;  
	int _age;     
};
//派生类
class Student : public Person
{
protected:
	int _stuid;   
};

 在该代码中,可以出现下列的情况:

Student s;
Person p = s;     //派生类对象赋值给基类对象
Person* ptr = &s; //派生类对象赋值给基类指针
Person& ref = s;  //派生类对象赋值给基类引用

 但是,基类对象不能赋值给派生类对象,但是基类的指针可以通过强制类型转换赋值给派生类的指针,但是此时基类的指针必须是指向派生类的对象才是安全的。

三.继承中的作用域

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

2.子类与父类中有同名成员的话,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)

3.需要注意的是,成员函数的隐藏中,只要子类与派生类中的函数名相同就会构成隐藏

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

例如下述代码:

#include<iostream>
using namespace std;

//父类
class Person
{
public:
    
protected:
    int _age = 100;
};

//子类
class Student : public Person
{
public:
    void print()
    {
        cout << _age << endl;
    }
protected:
    int _age = 20;
};



int main()
{
    Student s;
    s.print();//20
    return 0;
}

子类中有自身的_age,也有父类中的_age,那么在访问时将自动隐藏掉父类的_age,访问子类本身的_age。

假如我们需要访问父类中的_age,那么就要使用作用域限定符进行指定访问

例如下述代码:

#include<iostream>
using namespace std;

//父类
class Person
{
public:
    
protected:
    int _age = 100;
};

//子类
class Student : public Person
{
public:
    void print()
    {
        cout << Person::_age << endl;
    }
protected:
    int _age = 20;
};



int main()
{
    Student s;
    s.print();//100
    return 0;
}

四.派生类的默认成员函数

正常情况下,默认成员函数,即使我们不写,编译器也会自动生成,在类中,有六个默认成员函数。

但是,派生类的默认成员函数与类的默认成员函数不同

例如,我们创建了一个基类Person

#include <iostream>
#include <string>
using namespace std;

//基类
class Person
{
public:

	//构造函数
	Person(const string& name = "bear")
		:_name(name)
	{
		cout << "Person()" << endl;
	}

	//拷贝构造函数
	Person(const Person& p)
		:_name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}

	//赋值运算符重载函数
	Person& operator=(const Person& p)
	{
		cout << "Person& operator=(const Person& p)" << endl;
		if (this != &p)
		{
			_name = p._name;
		}
		return *this;
	}
	//析构函数
	~Person()
	{
		cout << "~Person()" << endl;
	}
private:
	string _name; //姓名
};

 当我们使用该基类派生出Student类时,其默认成员函数的逻辑如下:

//子类
class Student : public Person
{
public:
	//构造函数
	Student(const string& name, int id)
		:Person(name)//调用基类的构造函数初始化基类的那一部分成员
		, _id(id)//初始化派生类的成员
	{
		cout << "Student()" << endl;
	}

	//拷贝构造函数
	Student(const Student& s)
		:Person(s)//调用基类的拷贝构造函数完成基类成员的拷贝构造
		, _id(s._id)//拷贝构造派生类的成员
	{
		cout << "Student(const Student& s)" << endl;
	}

	//赋值重载运算符
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);//调用基类的operator=完成基类的成员赋值
			_id = s._id;//完成派生类的成员赋值
		}
		return *this;
	}

	//析构函数
	~Student()
	{
		cout << "~Student()" << endl;//调用完成后自动调用基类的析构函数
	}
private:
	int _id;
};

 可以看到,在派生类的默认成员函数中,都会先调用基类的默认成员函数

但是析构函数会先对派生类析构完再自动调用基类的析构函数,原因是:假如先调用基类的析构函数,那么此时派生类里的基类成员可能会出现问题。

总结如下:

1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须再派生类构造函数的初始化列表阶段显示调用。

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

3.派生类的operator=必须要调用基类的operator=完成基类的赋值。

4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

5.派生类对象初始化先调用基类构造再调派生类构造。

6.派生类对象析构清理先调用派生类的析构再调用基类的析构。

7.因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(后续会讲到)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),即父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

五.继承与友元

在类的讲解中,出现了一个友元的概念,在继承体系中也有这个概念,。但是,友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员,但是可以访问基类自身的私有和保护成员。

例如下列代码:
 

#include <iostream>
#include <string>
using namespace std;
//基类
class Person
{
public:
	//声明Display是Person的友元
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; //姓名
};

//子类
class Student : public Person
{
protected:
	int _id; //学号
};

//显示
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl; //可以访问
	cout << s._id << endl; //无法访问
}
int main()
{
	Person p;
	Student s;
	Display(p, s);
	return 0;
}

 假如想让Display函数也能够访问派生类Student的私有和保护成员,只能在派生类Student中进行友元声明。

class Student : public Person
{
public:
	friend void Display(const Person& p, const Student& s);//友元声明
protected:
	int _id; //学号
};

六.继承与静态成员

假如我们在基类定义了static静态成员,那么整个继承体系里只有一个这样的成员。无论有多少个派生类都只有一个static成员实例化。

//基类
class Person
{
public:
	Person()
	{
		++_count;
	}
	Person(const Person& p)
	{
		++_count;
	}

public:
	static int _count;//定义静态成员变量
};
int Person:: _count = 0;//静态成员变量在类外初始化

//派生类1
class Student : public Person
{
protected:
	string _stuid;
};

//派生类2
class Graduate : public Person
{
protected:
	string _cpp;
};

int main()
{
	Student s1;
	Student s2(s1);
	Student s3;
	Graduate s4;
	cout << Person::_count << endl;//4
	cout << Student::_count << endl;//4
}

 可以看到打印的结果都为4,还可以通过内存窗口查看_count的地址来确定

cout << &Person::_count << endl; //0x00000004
cout << &Student::_count << endl; //0x00000004

 查看地址也可以证明这两个看似不同的_count实际都指向同一个地址。

七.复杂的菱形继承及菱形虚拟继承

继承方式

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

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

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

 从菱形的继承模型可以看到,Assistant对象里包含了Student与Teacher,它们两个各自又包含了一个Person,那么假如Person里有成员变量,那么Student里也有,Teacher里也有,最后到Assistant里就会有两份Person成员,那么这时候就会出现数据冗余和二义性问题。

通过显示指定访问父类的成员可以解决二义性问题,但是数据冗余的问题还是无法解决。

那么这时候为了同时解决这两个问题,就引出了一个虚拟继承的概念,如果上述的继承关系,在Student与Teacher继承Person的时候使用虚拟继承,那么就可以解决了,但是需要注意的是,虚拟继承最好不要在别的场景下使用,否则会出现很多问题。

菱形虚拟继承

例如下述的虚拟继承代码:

#include <iostream>
using namespace std;
class A
{
public:
	int _a;
};
class B : virtual public A
{
public:
	int _b;
};
class C : virtual public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

 在继承后,可以在内存窗口看到二义性与数据冗余的问题都可以解决

菱形虚拟继承的原理

 通过内存窗口,我们可以看到D类对象当中各个成员在内存当中的分布情况:

 

 上图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中A的成员_a放到了最后面,原本重复放置的_a成员的位置变成两个指针,这两个指针叫虚基表指针,而他们指向一个虚基表,虚基表中第二个数据存着偏移量,第一个数据是预留的位置(不需要关心这个),通过偏移量就可以找到下面A的成员_a。

所以,这两个指针通过一系列计算,最终都会找到成员_a的位置。

 

 我们若是将D类对象赋值给B类对象,在这个切片过程中,就需要通过虚基表中的第二个数据找到公共虚基类A的成员,得到切片后该B类对象在内存中仍然保持这种分布情况。

D d;
B b = d; //切片行为

 得到切片后该B类对象当中各个成员在内存当中的分布情况如下:

 可以看到,_a对象还是存储在B对象的最后。

八.继承的总结与反思

在C++的第一个特效继承里,我们就看到了相当复杂的语法,其中多继承就是一个体现。有了多继承,就可能存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出菱形继承,否则代码在复杂度及性能上都容易出现问题,当菱形继承出问题时难以分析,并且会有一定的效率影响。

多继承可以认为是C++的缺陷之一,很多后来的OO(Object Oriented)语言都没有多继承,如Java。

所以后来,就有了继承与组合。

继承是一种is-a的关系,也就是说每个派生类对象都是一个基类对象;

而组合是一种has-a的关系,若是B组合了A,那么每个B对象中都有一个A对象。

例如手机和小米手机就是一个 is—a的关系,他们之间适合使用继承:

class Phone
{
protected:
	string _colour; //颜色
	string _num; //系列
};
class MiPhone : public Phone
{
public:
	void Use()
	{
		cout << "this is MiPhone" << endl;
	}
};

 而手机和摄像头就是has—a的关系,它们之间适合使用组合

class Camera
{
protected:
	string _brand; //品牌
	size_t _size; //尺寸
};
class Phone
{
protected:
	string _colour; //颜色
	string _num; //系列
	Tire _c; //摄像头
};

假如两个类之间可以看成is—a的关系又可以看作has—a的关系,那么优先使用组合。

原因如下:

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


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


3.实际中尽量多使用组合,组合的耦合度低,代码维护性好。不过继承也是有用武之地的,有些关系就适合用继承,另外要实现多态也必须要继承。若是类之间的关系既可以用继承,又可以用组合,则优先使用组合。

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

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

相关文章

set用法

ES6中的Set是一种新的数据结构&#xff0c;类似于数组&#xff0c;用于存储有序的数据。Set没有随机访问的能力&#xff0c;不能通过索引来获取具体的某个元素Set中的元素具有唯一性&#xff0c;不允许存储相同的元素。 Set本身是一个构造函数&#xff0c;可以用来实例化Set对…

计算机网络—HTTPS协议详解:工作原理、安全性及应用实践

&#x1f3ac;慕斯主页&#xff1a;修仙—别有洞天 ♈️今日夜电波&#xff1a;ヒューマノイド—ずっと真夜中でいいのに。 1:03━━━━━━️&#x1f49f;──────── 5:06 &#x1f504; ◀️ ⏸…

陆面、生态、水文模拟与多源遥感数据同化

原文链接&#xff1a;陆面、生态、水文模拟与多源遥感数据同化https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247601198&idx6&sn51b9b26b75c9df1f11dcb9a187878261&chksmfa820dc9cdf584df9ac3b997c767d63fef263d79d30238a6523db94f68aec621e1f91df85f6…

VMware 安装配置 Ubuntu(最新版、超详细)

1. 下载安装 VMware ➡️➡️➡️来源&#xff1a;VMware Docs VMware Workstation Pro™ 使专业技术人员能够在同一台 PC 上同时运行多个基于 x86 的 Windows、Linux 和其他操作系统&#xff0c;从而开发、测试、演示和部署软件。 [Step 1]&#xff1a; 点击 VMware Workstati…

【Java探索之旅】从输入输出到猜数字游戏

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; Java编程秘籍 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言一、输入输出1.1 输出到控制台1.2 从键盘输入 二、猜数字游戏2.1 所需知识&#xff1a…

【TI毫米波雷达】I2C初始化配置和主机数据收发,用SDA来模拟UART数据输出,可直接连接IWR6843AOP开发板引脚

【TI毫米波雷达】I2C初始化配置和主机数据收发&#xff0c;用SDA来模拟UART数据输出&#xff0c;可直接连接IWR6843AOP开发板引脚 文章目录 导入库引脚复用初始化I2C配置数据发送用SDA来模拟UART数据输出附录&#xff1a;结构框架雷达基本原理叙述雷达天线排列位置芯片框架Demo…

数据结构-----枚举、泛型进阶(通配符?)

文章目录 枚举1 背景及定义2 使用3 枚举优点缺点4 枚举和反射4.1 枚举是否可以通过反射&#xff0c;拿到实例对象呢&#xff1f; 5 总结 泛型进阶1 通配符 ?1.1 通配符解决什么问题1.2 通配符上界1.3 通配符下界 枚举 1 背景及定义 枚举是在JDK1.5以后引入的。主要用途是&am…

【Linux】进程通信之匿名管道通信

一、进程间进行通信的目的 我们往往需要多个进程协同&#xff0c;共同完成一些事情。 数据传输&#xff1a;一个进程需要将它的数据发送给另一个进程资源共享&#xff1a;多个进程之间共享同样的资源。通知事件&#xff1a;一个进程需要向另一个或一组进程发送消息&#xff0c…

Netty NioEventLoop详解

文章目录 前言类图主要功能NioEventLoop如何实现事件循环NioEventLoop如何处理多路复用Netty如何管理Channel和Selector管理Channel管理Selector注意事项 前言 Netty通过事件循环机制(EventLoop)处理IO事件和异步任务&#xff0c;简单来说&#xff0c;就是通过一个死循环&…

23年坚守,只为打造高品质立秀膨体,索康让品质为中国说话

2024年3月23日&#xff0c;第二十三届上海国际整形美容外科大会&#xff08;以下简称“大会”&#xff09;在上海召开&#xff0c;本次大会由张涤生整形外科发展基金会主办&#xff0c;上海交通大学附属第九人民医院整复外科、Chinese Journal of Plastic and Reconstructive S…

性能优化 - 你知道CSS有哪些优化方案吗

难度级别:中高级及以上 提问概率:70% CSS是前端开发工作中必不可少的技能之一,同时也是网页开发中必不可少的重要元素之一。但很多人所开发的项目本身对性能要求并不高,再加上项目周期紧张,久而久之,也就容易养成不考虑细节的习惯,觉得C…

NzN的数据结构--二叉树part2

上一章我们介绍了二叉树入门的一些内容&#xff0c;本章我们就要正式开始学习二叉树的实现方法&#xff0c;先三连后看是好习惯&#xff01;&#xff01;&#xff01; 目录 一、二叉树的顺序结构及实现 1. 二叉树的顺序结构 2. 堆的概念及结构 3. 堆的实现 3.1 堆的创建 …

04-12 周五基于VS code + Python实现CSDN发布文章的自动生成

简介 之前曾经说过&#xff0c;在撰写文章之后&#xff0c;需要&#xff0c;同样需要将外链的图像转换为的形式&#xff0c;因此&#xff0c;可以参考 04-12 周五 基于VS Code Python 实现单词的自动提取 配置步骤 配置task 在vscode的命令面板configure task。配置如下的任…

python知识点汇总(十一)

python知识点总结 1、当Python退出时&#xff0c;是否会清除所有分配的内存&#xff1f;2、Python的优势有哪些&#xff1f;3、什么是元组的解封装4、Python中如何动态获取和设置对象的属性&#xff1f;5、创建删除操作系统上的文件6、主动抛出异常7、help() 函数和 dir() 函数…

人工智能基础部分26-基于知识推理引擎KIE算法的原理介绍与实际应用

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下人工智能基础部分26-基于知识推理引擎KIE算法的原理介绍与实际应用。知识推理引擎(Knowledge Inference Engine, KIE)是一种人工智能技术&#xff0c;其核心原理是基于规则和逻辑的方法来处理复杂的问题。它构建在业…

从前端角度防范XSS攻击的策略与实践

XSS&#xff08;Cross-Site Scripting&#xff0c;跨站脚本攻击&#xff09;是一种常见的网络安全威胁&#xff0c;它允许攻击者将恶意脚本注入到正常的网页中&#xff0c;从而在其他用户的浏览器上执行这些脚本。这可能导致数据泄露、会话劫持、甚至是对受害者计算机的完全控制…

探新路建“枢纽” 湖南深耕中非经贸合作“试验田”

湖南作为中国与非洲经贸合作的重要窗口&#xff0c;积极推动中非经贸关系的发展和深化。通过构建覆盖全产业链的高效运作模式&#xff0c;湖南企业能够在一周内将肯尼亚干制鳀鱼加工成为麻辣鲜香的劲仔深海小鱼并投入中国市场。此外&#xff0c;湖南还致力于推动非洲优质农产品…

【vue】watchEffect 自动侦听器

watchEffect&#xff1a;自动监听值的变化 获取旧值时&#xff0c;不是很方便&#xff0c;建议用watch <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevic…

数学基础:深度学习的语言

数学基础&#xff1a;深度学习的语言 概述 在深度学习的世界里&#xff0c;数学不仅仅是一套工具&#xff0c;它是构建、理解和优化深度学习模型的基石。从向量空间的概念到复杂的优化算法&#xff0c;数学的每一个分支都在深度学习的发展中扮演着关键角色。本文的目标是通过深…

解决cmd输入py文件路径不能执行,使用anaconda prompt 能执行

究其原因&#xff0c;是因为没有配置环境&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 第一步&#xff1a;配置环境变量 操作步骤如下&#xff1a; 1、右击此电脑 ---->属性 2、高级系统设置 3、点击环境变量 4、选择 …