C++ ---- 继承

news2024/11/28 1:29:31

 

目录

继承概念及定义

继承概念

继承定义

语法

继承关系和访问限定符

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

规律总结

以公有继承为例测试

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

继承中的作用域

派生类的默认成员函数 

构造函数

析构函数

拷贝构造

赋值重载

继承与友元

继承与静态成员

菱形继承以及菱形虚拟继承

单继承和多继承

菱形继承

虚继承解决数据冗余和二义性

虚继承的原理(什么是虚基表)

继承和组合


继承概念及定义

继承概念

●继承机制是面向对象程序设计使代码可以复用的重要手段。

●继承在保持原有类特性的基础上可进行扩展,增加功能。这样产生的类,叫做派生类。

●继承呈现了面向对象程序设计的层次结构,继承是类设计层次的复用。

上述图解中,Person是基类,成员变量有姓名和年龄,成员函数有一个Print。基类的成员和方法教师和学生对象都需要,如果不使用继承,学生和教师类就要都写一份。而使用继承的方式,实现了代码的复用,而且,派生类可以增加和扩展新的功能。上述图解也体现了面向对象程序设计的层次结构!

 继承后基类(Person)的(成员函数+成员变量)都变成了子类的一部分。这里体现了复用的性质。

继承定义

语法

class 派生类名 : 继承方式 基类类名

继承关系和访问限定符

●继承关系有三种:公有(public)继承、保护(protected)继承、私有继承(private)。
●访问限定符有三种:public访问、protected访问、private访问。

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

规律总结

 a.基类的private成员继承到了派生类中,但是不可见的,无论你用什么继承方式。不可见指的是基类的私有成员继承到了派生类对象中,但是在语法上限制派生类对象不管在类内还是类外面都不能访问。

b.保护限定符专门为继承而生,假设基类成员不想在类外被直接访问,但需要在派生类中访问,就定义基类的成员属性为保护(protected)。

c.基类的私有成员总是派生类不可见的。其余继承方式和访问限定符的组合取某个成员在基类中的访问限定符和继承方式更小的一个(public>protected>private):
基类成员在派生类中的访问方式 = Min(成员在基类的访问限定符,继承方式)。

d.默认情况下,class的默认继承方式是私有继承,struct的默认继承方式是公有继承。不过最后都显示的写出继承方式。

e.公有继承最为常用,原因在于继承的本质是代码复用的一种手段,保护和私有继承下来的成员只能在派生类使用或者不可见。

以公有继承为例测试

class Base
{
public:
        void Fun1()
        {
                cout << "公有成员函数" << endl;
        }
protected:
        void Fun2()
        {
                cout << "Base保护成员函数" << endl;
        }
private:
        //私有成员变量
        int _id;
};
//公有继承
class Derived : public Base
{
        //公有继承:基类的私有成员在派生类中不可见
        //公有继承: 基类的保护成员在派生类内可以访问,不可以在类外访问
        //公有继承:基类的公有成员在派生类内和类外均可访问。
public:
        void FunDerived()
        {
                //类内访问基类的保护成员
                Fun2();
        }
private:

};
int main()
{
        Derived d1;
        d1.Fun1();

        d1.FunDerived();

        return 0;
}

1.公有继承:基类的私有成员在派生类中不可见 。

2.公有继承: 基类的保护成员在派生类内可以访问,不可以在类外访问。

3.公有继承:基类的公有成员在派生类内和类外均可访问。

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

1.派生类对象可以赋值给 基类的对象、基类的指针或者基类的引用。这里有个形象的说法叫做切片。

2.基类的对象不能赋值给派生类对象。

3. 切割的过程没有发生类型转换

a.在下述示例中,int类型的引用引用double类型的数据。在这个过程中会先生成一个临时变量,在将临时变量,int& 引用的是产生的临时变量,所以必须用const int &。

 b.基类的引用引用派生类

//基类
class Base
{
public:
	int _num = 2023;
};
//派生类
class Derived : public Base
{
public:
	int _num = 2024;
};

int main()
{
	Derived d;

	Base& b = d;
	return 0;
}

赋值的过程没有产生临时变量。

4.赋值转换测试

class Base
{
public:
	int _id = 11;
	int _num = 22;
};

class Derived : public Base
{
public:
	int _name = 333;
};

int main()
{
	Base b;
	Derived d;
	
	d._id = 111;
	d._num = 222;

	//派生类对象赋值给基类的对象
	//b = d;
	//派生类对象赋值给基类的指针
	//Base* dptr = &d;

	//派生类对象赋值给基类的引用
	//Base& pd = d;

	return 0;
}

派生类对象赋值给基类对象 

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

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

继承中的作用域

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

//基类
class Base
{
public:
	int _num = 2023;
};
//派生类
class Derived : public Base
{
public:
	int _num = 2024;
};

int main()
{
	Derived d;

	cout << "Derived _num:" << d._num << endl;
	cout << "Base _num:" << d.Base::_num << endl;
	
	return 0;
}

类比一下局部变量和全局变量,不同作用域的局部变量可以重名,原因是作用不同。这里也是一样的道理,基类和派生类有不同的作用域。

2.因为子类和父类的作用域不同,所以允许出现同名函数或者成员变量。如果子类和父类中有同名成员,子类成员屏蔽掉父类对同名成员的直接访问,这种情况叫做隐藏(重定义)。

3.同名成员变量,子类成员变量屏蔽掉父类同名成员,子类对象默认访问的是子类中的变量。如果向访问父类的同名变量可以通过父类名::父类成员变量的方式显示访问。

4.同名函数的隐藏,只需要函数名相同就可以。不用担心构成重载,因为构成重载的条件之一就是要在同一作用域中。

5.案例1:下述代码同名函数构成什么关系?

A重载  B重写 C隐藏(重定义) D程序报错

//基类
class Base
{
public:
	void Fun()
	{
		cout << "Base->func()" << endl;
	}
};
//派生类
class Derived : public Base
{
public:
	void Fun(int i)
	{
		cout << "Base->func()" << endl;
	}
};

int main()
{
	Derived dd;
	dd.Fun(10);

	return 0; 
}

分析:在继承中函数同名就构成了隐藏。因为基类和派生类各自有独立的作用域,所以不会构成重载。关于上述程序的运行结果,会调用派生类的Fun函数,基类的同名函数被隐藏。

6.案例2:下述代码同名函数构成什么关系?

A重载  B重写 C隐藏(重定义) D程序报错

//基类
class Base
{
public:
	void Fun()
	{
		cout << "Base->func()" << endl;
	}
};
//派生类
class Derived : public Base
{
public:
	void Fun(int i)
	{
		cout << "Base->func()" << endl;
	}
};

int main()
{
	Derived dd;
	dd.Fun();

	return 0; 
}

分析:两题看似一样,不同点是在调用的时候案例1传了参数,而案例2没有传参。分析逻辑是一样的,继承关系中同名函数构成隐藏,所以在派生类对象调用Fun不传递参数的时候,程序报错。因为基类的Fun()被隐藏了。

派生类的默认成员函数 

构造函数

●在普通对象中:构造函数对自定义类型的成员调用其自己的构造函数。对于内置类型不处理。

●在派生类对象中:构造函数对属于基类的以部分调用基类的构造函数,如果基类没有提供默认构造,在派生类初始化列表显示的调用。自定义类型和内置类型的处理方式和普通对象相同。

基类提供默认构造:派生类对象初始化先调用基类构造在调用派生类构造。

//基类
class Base
{
public:
	Base()
	{
		cout << "基类提供默认构造" << endl;
	}
};
//派生类
class Derived : public Base
{
public:
	Derived()
	{
		cout << "派生类构造" << endl;
	}
};

int main()
{
	Derived d;
	return 0;
}

 基类没有提供默认构造:在派生类初始化列表显示的调用。

//基类
class Base
{
public:
	Base(int a)
	{
		cout << "基类没有提供默认构造" << endl;
	}
};
//派生类
class Derived : public Base
{
public:
	Derived()
		:Base(10)
	{
		cout << "派生类构造" << endl;
	}
};

int main()
{
	Derived d;
	return 0;
}

析构函数

●在普通对象中:有资源申请的对象要写析构。完成资源清理的工作。对于自定义类型调用其自己的析构。对于内置类型不处理。

●在派生类对象中:

a、派生类的析构函数调用完后,会自动调用基类的析构函数清理基类成员。 这样做的原因是要保证派生类对象先清理派生类成员再清理基类成员的顺序。

b、对于析构函数,由于某种需要,编译器会将析构函数的名称统一设置为destrutor()。所以子类析构和父类的析构构成隐藏关系。

派生类构造和析构总结

1.生类对象初始化先调用基类的构造函数,再调用派生类的构造函数。

2.派生类对象析构,先调用派生类析构再调用基类的析构。

拷贝构造

●普通对象:对于有资源申请的类,需要显示的定义拷贝构造(否则浅拷贝)。默认生成的拷贝构造,对于自定义类型调用其自己的拷贝构造,对于内置类型逐字节拷贝

●派生类对象:调用基类的拷贝构造完成基类部分的拷贝初始化。

赋值重载

●普通对象:默认生成的赋值重载,对于内置类型逐字节拷贝,对于自定义类型调用其对应的赋值重载。

●派生类对象:派生类对象的operator=必须调用基类的operator=完成基类的复制。

继承与友元

●友元关系不能继承!

class Derived;
class Base
{
	friend void Print1(const Derived& dd);
private:
	string _name = "张三";
	int _age = 18;
};
//派生类
class Derived : public Base
{
private:
	int _id = 150;
};

void Print1(const Derived& dd)
{
	cout << "姓名:" << dd._name << endl;
	cout << "年龄:" << dd._age  << endl;
}

int main()
{
	Derived d;
	Print1(d);

	return 0;
}

基类友元不能访问派生类私有和保护成员。 

class Derived;
class Base
{
	friend void Print1(const Derived& dd);
private:
	string _name = "张三";
	int _age = 18;
};
//派生类
class Derived : public Base
{
private:
	int _id = 150;
};


void Print1(const Derived& dd)
{
	cout << "id: " << dd._id << endl;
}
int main()
{
	Derived d;
	Print1(d);
	return 0;
}

继承与静态成员

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

//基类
class Base
{
public:
	void PrintCount()
	{
		cout << "count = " << count << endl;
	}
public:
	static int count;
};
int Base::count = 0;
//派生类1
class Derived1 : public Base
{};
//派生类2
class Derived2 : public Base
{};


int main()
{
	Base bb;
	Derived1 d1;
	Derived2 d2;

	bb.PrintCount();
	d1.PrintCount();
	d2.PrintCount();

	bb.count++;
	bb.PrintCount();
	d1.PrintCount();
	d2.PrintCount();


	printf("bb:%p\n",&bb.count);
	printf("d1:%p\n",&d1.count);
	printf("d2:%p\n",&d2.count);

	return 0;
}

菱形继承以及菱形虚拟继承

单继承和多继承

单继承:一个子类只有一个直接父类。

 多继承:一个子类有两个或两个以上的直接父类。

菱形继承

菱形继承是多继承的一种情况,继承关系类似菱形。

菱形继承带来了数据冗余和二义性的问题:

//菱形继承
class Base
{
public:
	int _bb;
};

//派生类1
class Derived1 : public Base
{
public:
	int _d1;
};
//派生类2
class Derived2 : public Base
{
public:
	int _d2;
};

class DDerived3 : public Derived1, public Derived2
{};

int main()
{
	DDerived3 dd;

	return 0;
}

数据冗余

 二义性

dd._bb;

分析:在菱形继承中,以上图为例,基类A的成员在D中有两份,造成了数据的冗余。而且在调用的时候存在二义性问题。

虚继承解决数据冗余和二义性

虚拟继承可以解决上述的问题,以上述的继承关系为例,只需要在菱形继承的腰部(Derived1和Derived2)的位置加上virtual关键字即可解决问题。

//菱形虚拟继承
class Base
{
public:
	int _bb;
};

//派生类1
class Derived1 : virtual public Base
{
public:
	int _d1;
};
//派生类2
class Derived2 : virtual public Base
{
public:
	int _d2;
};

class DDerived3 : public Derived1, public Derived2
{};

int main()
{
	DDerived3 dd;

	dd._bb;
	return 0;
}

虚继承的原理(什么是虚基表)

以上述虚继承代码为例:观察内存窗口

1.虚继承后DDeriver3中只有一份虚基类Base的成员。

2. Derived1和Derived2的部分都多了一个地址。

3.通过内存窗口找到这两个地址,可以发现里面有两个非常显眼的数据。这两个地址,叫做虚基表指针。

 4.这两个数据实际上是记录的是找到虚基类的偏移量。这两个表我们称其为虚基表。

5.为什么要通过偏移量找到虚基类成员,当将派生类的对象赋值给基类的对象、基类的指针或者基类的引用时,要发生切片,派生类要能找到属于基类的一部分。

总结:从结构模型的角度分析,虚继承 将虚基类成员放到了对象组成的最下面,虚基类成员同时属于继承于它的派生类,派生类通过指向虚基表的指针可以找到存放偏移量的虚基表。通过偏移量就能找到图中存放在最下面的虚基类成员。

继承和组合

继承

class A
{
private:
	int _a1;
	int _a2;
};
class B : public A
{
private:
	int _b1;
};

组合

//组合
class A
{
private:
	int _a1;
	int _a2;
};
class B
{
private:
	int _b1;
	A _aa;
};

继承和组合的对比

●继承更适合is-a的关系,比如:学生是人,狗是动物等等。 组合更适合has-a的关系,比如:头上有双眼睛,车上有个方向盘等等。

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

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

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

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

相关文章

ESP8266调用NTP服务器进行时间校准

一、背景知识 【1】什么是NTP服务器&#xff1f; NTP是网络时间协议&#xff08;Network Time Protocol&#xff0c;简称NTP&#xff09;&#xff0c;是一种用于同步计算机时间的协议。NTP服务器指的是提供NTP服务的计算机或设备。NTP服务器的主要功能是保证网络上的所有设备…

Linux下信号量使用总结

目录 1.Linux下信号量简介 2.POSIX信号量 2.1 无名信号量 2.2 有名信号量 3.System V信号量 1.Linux下信号量简介 信号量是解决进程之间的同步与互斥的IPC机制&#xff0c;互斥与同步关系存在的症结在于临界资源。 临界资源是在同一个时刻只容许有限个&#xff08;一般只有…

ble系统知识介绍(较为完整,持续更新中)

BLE学习 现在网上关于BLE系统的学习资料实在是太少了&#xff0c;因此本文希望能够聚集一些资料能够系统的入门BLE,本文翻译自Bluetooth_LE_Primer_Paper,还有一些网上的一些资料,如果侵害到了某些作者的权益请及时联系我 参考资料和链接 Bluetooth_LE_Primer_Paper_3MtXws-zP…

GitOps 实践之渐进式发布

本文作者&#xff1a;陈钧桐 腾讯云 CODING DevOps 高级解决方案架构师&#xff0c;从事多年技术布道工作&#xff0c;对于云原生时代下企业数字化转型、IT 与 DevOps 建设、价值流体系搭建等有丰富的经验&#xff0c;曾为多家大型企业提供咨询、解决方案以及内训服务。既关注工…

【Python小技巧】加密又提速,把.py文件编译为.pyd文件(类似dll函数库),你值得拥有!

文章目录 前言一、常见的Python文件格式有哪些&#xff1f;二、准备编译环境1. 安装cython2. 安装Microsoft C 生成工具 三、编译.py文件为.pyd文件1. 编辑原始.py文件2. 准备setup.py文件3. 进行编译 四、测试总结 前言 Python的脚本文件是开源的&#xff0c;若直接发布&#…

i.MX RT1010跨界MCU上手体验(上)

由于项目需要性价比高一些的高性能MCU&#xff0c;了解到NXP的RT1010主频高达500MHZ的MCU&#xff0c;半个月以前已经拿到官方Demo板了&#xff0c;今天抽空上电体验下&#xff0c;在此记录。这颗芯片的优势是主频高&#xff0c;功能全&#xff0c;价格合理&#xff0c;但是需要…

第六章 方法区

文章目录 前言一、&#x1f6fa; 栈、堆、方法区的交互关系二、&#x1f68e; 方法区的理解1、方法区在哪里2、方法区的演变过程 三、&#x1f697; 设置方法区的大小与 OOM1、设置方法区内存的大小2、使用CGLib 让方法区OOM3、如何解决 OOM4、方法区的内部结构5、non-final 的…

利用提示工程优化软件架构:ChatGPT的应用

ChatGPT时代的软件架构全生命周期 简介 在如今日新月异的技术环境中&#xff0c;软件架构师必须不断地寻找和采纳新的工具和方法&#xff0c;以优化开发过程&#xff0c;提高效率&#xff0c;并保证最终产出的质量。其中&#xff0c;人工智能&#xff08;AI&#xff09;已经成…

《淘宝技术这十年》读书笔记

一. 分布式时代 在系统发展的过程中&#xff0c;架构师的眼光至关重要&#xff0c;作为程序员&#xff0c;只要把功能实现即可&#xff0c;但作为架构师&#xff0c;要考虑系统的扩展性、重用性&#xff0c;对于这种敏锐的感觉&#xff0c;有人说是一种“代码洁癖”。淘宝早期…

使用双屏时两个屏幕的色调、亮度不一样如何设置?

当使用双屏时&#xff0c;即使两个屏幕的型号一致也可能存在色差的问题&#xff08;色调不一致&#xff0c;亮度不一致&#xff09;&#xff0c;以下是解决此问题的方法。 Step1.同时按下两个屏幕下方的ok按钮 Step2.此时会进入显示器的OSD界面 Step3.通过按键切换菜单 Step4.…

节省35% MCU开发成本的红外智能洗手器运用方案,N9300-S16音乐芯片

随着全国人民生活水平的逐步提升以及近期疫情影响&#xff0c;公民的健康保护意识也越来越强&#xff0c;洗手液越来越被人们重视以及提倡&#xff0c;即时在受疫情影响是2022年洗手液市场规模也是上升至恐怖34亿元产值&#xff1b;而近年来自动感应洗手液器凭借实用性、便携性…

APP测试面试题快问快答(三)

11. App安装测试的主要内容有哪些&#xff1f; App是客户端程序&#xff0c;客户端程序就需要进行安装才能使用&#xff0c;因此需要测试安装、卸载、升级测试 关注点&#xff1a;正常场景、异常场景。 正常场景&#xff1a; 1. 在不同的操作系统上安装 2. 从不同的安装渠…

windows10安装ElasticSearch

一 安装 Java环境 ElasticSearch使用Java开发的&#xff0c;依赖Java环境&#xff0c;安装 ElasticSearch 之前&#xff0c;需要先安装一个较新版本的 Java&#xff0c;jdk 1.8版本太低了&#xff0c;需要安装jdk 11或更高版本。 Java安装方法请参考 Java 15环境安装 。 二 …

详解字典树原理,代码分析leetcode208. 实现 Trie (前缀树)

0、引言 本文介绍一种能够偶快速查找字符串的树形数据结构-----字典树。介绍其原理&#xff0c;以及通过leetcode208题目这个实例&#xff0c;用数组动手实现一棵字典树&#xff0c;并完成其增、查字符串、查字符串前缀的功能。 1、字典树的应用场景 询问一个单词b&#xff0c…

Mujoco 加载机器人模型(三)

目录 .1 简介 1.1 urdf概述 ​编辑 1.2 导出urdf为可用的xml​编辑 1.3 导出测试​编辑 .2 修改 2.1 添加平面和物体 2.2 关节修改 2.2.1 group 2.2.2 关节修改 2.2.3 关节 解压提供的ur5后 修改compiler的 meshdir路径 <mujoco model"ur5"><compi…

一条耗时100ms的SQL把系统搞崩了

一个项目上线了两个月&#xff0c;除了一些反馈的优化和小Bug之外&#xff0c;项目一切顺利。前期是属于推广阶段&#xff0c;可能使用人员没那么多&#xff0c;当然对于项目部署肯定提前想到并发量了&#xff0c;所以早就把集群安排上&#xff0c;而且还在测试环境搞了一下压测…

2023年的深度学习入门指南(15) - 大模型的幻觉

2023年的深度学习入门指南(15) - 大模型的幻觉 大模型的能力最另人惊讶的&#xff0c;一个是强大的能力&#xff0c;另一个就是时不时一本正经地胡说八道。如果你用的是小一点的模型&#xff0c;可能还见过输出循环内容之类的情况。我们将这种生成不良内容的现象称为幻觉-hall…

3d动画用云渲染靠谱吗?

3d动画是一种利用计算机技术制作的动画形式&#xff0c;它可以模拟真实世界的物体和场景&#xff0c;创造出各种惊人的效果和视觉体验。3d动画广泛应用于影视、游戏、广告、教育等领域&#xff0c;成为当今最流行的艺术表现形式之一。据统计&#xff0c;2019年全球3d动画市场规…

吴恩达老师《机器学习》课后习题1之线性回归

在学习这些内容之前&#xff0c;需要学习python数据分析相关内容&#xff1a; numpy&#xff1a;科学计算库&#xff0c;处理多维数组&#xff0c;进行数据分析 pandas&#xff1a;基于numpy的一种工具&#xff0c;该工具是为了解决数据分析任务而创建的 matplotlib&#xff1a…

Atcoder Beginner Contest 297

A - Double Click AC代码&#xff1a; #include<iostream> #include<algorithm> #include<cstring> using namespace std; const int N110; int t[N]; int main() {int n,d;cin>>n>>d;for(int i1;i<n;i) cin>>t[i];bool flagfalse;i…