C++ Primer 第五版 第15章 面向对象程序设计

面向对象程序设计基于三个基本概念:数据抽象、继承和动态绑定。

继承和动态绑定对编写程序有两方面的影响:一是我们可以更容易地定义与其他类相似但不完全相同的新类;二是在使用这些彼此相似的类编写程序时,我们可以在一定程度上忽略掉它们的区别。

一、OOP:概述

面向对象程序设计(object-oriented programming)的核心思想是数据抽象、继承和动态绑定。通过使用数据抽象,我们可以将类的接口与实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。

继承

基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。

在C++语言中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待。对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数(virtual function)。

举例:定义一个名为Quote的类,并将它作为层次关系的基类。Quote的对象表示按原价销售的书籍。Quote派生出另一个名为bulk_quote的类,它表示可以打折销售的书籍。

派生类必须通过使用类派生列表(class derivation list)明确指出它是从哪个(哪些)基类继承而来的。类派生列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有访问说明符:

派生类内部(成员函数或友元函数)使用基类成员时:不受继承方式的影响,只看该成员在基类中的访问属性。

派生类外部(派生类用户)使用基类成员时:不同的继承方式决定了基类成员在派生类中的访问属性,从而对派生类用户的访问权限产生影响。

public继承:所有基类成员在派生类中保持原有的访问级别。

之后我们将继续学习protected继承和private继承。

派生类必须在其内部对所有重新定义的虚函数进行声明。派生类可以在这样的函数之前加上virtual关键字,但也并不是非得这么做。C++11允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表之后增加一个override关键字。

动态绑定

因为在上述过程中函数的运行版本由实参决定,即在运行时选择函数的版本,所以动态绑定有时又被称为运行时绑定。

二、定义基类和派生类
1. 定义基类

成员函数与继承

任何构造函数之外的非静态函数都可以是虚函数。关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。

成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时。

访问控制与继承

派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。和其他使用基类的代码一样,派生类能访问公有成员,而不能访问私有成员。不过有些时候基类中还有这样一种成员,基类希望它的派生类有权访问该成员,同时禁止其他用户访问。我们用受保护的(protected)访问运算符说明这样的成员。

2. 定义派生类

派生类必须通过类派生列表明确指出它是从哪个(哪些)基类继承而来的。

类派生列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有以下三个访问说明符中的一个:public、protected或者private。

派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明。

访问说明符的作用是控制派生类从基类继承而来的成员是否对派生类的用户可见。

派生类中的虚函数

如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类回直接继承其在基类中的版本。

派生类对象及派生类向基类的类型转换

C++标准并没有明确规定派生类的对象在内存中如何分布。

因为在派生类对象中含有与其基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用,而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分上。

这种转换通常称为派生类到基类的类型转换。和其他类型转换一样,编译器会隐式地执行派生类到基类的转换。这种隐式特性意味着我们可以把派生类对象或者派生类对象的引用用在需要基类引用的地方;同样的,我们也可以把派生类对象的指针用在需要基类指针的地方。

派生类构造函数

尽管在派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。和其他创建了基类对象的代码一样,派生类也必须使用基类的构造函数来初始化它的基类部分

派生类对象通过构造函数初始化列表来将实参传递给基类构造函数。

除非我们特别指出,否则派生类对象的基类部分会像数据成员一样执行默认初始化。

派生类使用基类的成员

派生类可以访问基类的公有成员和受保护成员。

继承与静态成员

如果基类定义了一个静态成员,则在整个继承体系只存在该成员的唯一定义。

派生类的声明

派生类的声明中包含类名但是不包含它的派生列表:

一条声明语句的目的是令程序知晓某个名字的存在以及改名字表示一个什么样的实体,如一个类、一个函数或一个变量等。

被用作基类的类

如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明:

一个类是基类,同时它也可以是一个派生类·:

在这个继承关系中,Base是D1的直接基类(direct base),同时是D2的间接基类(indirect base)。

每个类都会继承直接基类的所有成员。

防止继承的发生

C++提供了一种防止继承发生的方法,即在类名后跟一个关键字final。

3. 类型转换与继承

通常情况下,如果我们想把引用或指针绑定到一个对象上,则引用或指针的类型应与对象的类型一致,或者对象的类型含有一个可接受的const类型转换规则。存在继承关系的类是一个重要的例外:我们可以将基类的指针或引用绑定到派生类对象上。例如,我们可以用Quote&指向一个Bulk_quote对象,也可以把一个Bulk_quote对象的地址赋给一个Quote*。

(可以将派生类当作基类来使用)

可以将基类的指针或引用绑定到派生类对象上有一层极为重要的含义:当使用基类的引用(或指针)时,实际上我们并不清楚该引用(或指针)所绑定对象的真实类型。该对象可能是基类的对象,也可能是派生类的对象。

静态类型与动态类型

表达式的静态类型(static type)在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;动态类型(dynamic type)则是变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知。

基类的指针或引用的静态类型可能与其动态类型不一致。

不存在从基类到派生类的隐式类型转换

在对象之间不存在类型转换

派生类向基类的自动类型转换只对指针或引用类型有效,在派生类类型和基类类型之间不存在这样的转换。

当我们初始化或赋值一个类类型的对象时,实际上是在调用某个函数。当执行初始化时,我们调用构造函数;而当执行赋值操作时,我们调用赋值运算符。这些成员都包含一个参数,该参数的类型是类类型的const版本的引用。

三、虚函数

当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定。因为我们直到运行时才能知道到底调用了哪个版本的虚函数,所以所有虚函数都必须有定义。通常情况下,如果我们不使用某个函数,则无须为该函数提供定义。但是我们必须为每一个虚函数都提供定义,而不管它是否被用到了,这是因为连编译器也无法确定到底会使用哪个虚函数。

对虚函数的调用可能在运行时才被解析

当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数。被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。

派生类中的虚函数

派生类中虚函数的返回类型也必须与基类函数匹配。该规则有一个例外,当类的虚函数返回类型是类本身的指针或引用时,上述规则无效。也就是说,如果D由B派生得到,则基类的虚函数可以返回B*而派生类的对应函数可以返回D*,只不过这样的返回类型要求从D到B的类型转换是可访问的。

final和override说明符

虚函数与默认实参

回避虚函数的机制

在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本。通过作用域运算符可以实现这一目的。

四、抽象基类
纯虚函数

纯虚函数无须定义。我们通过在函数体的位置(即在声明语句的分号之前)书写 =0 就可以将一个虚函数说明为纯虚函数。其中,=0只能出现在类内部的虚函数声明语句处。

我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。我们不能在类的内部为一个=0的函数提供函数体。

含有纯虚函数的类是抽象基类

含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。我们不能(直接)创建一个抽象基类的对象。

派生类构造函数只初始化它的直接基类

五、访问控制与继承

每个类分别控制自己的成员初始化过程。与之类似,每个类还分别控制着其成员对于派生类来说是否可访问(accessible)。

受保护的成员

一个类使用protected关键字来声明那些它希望与派生类分享但不想被其他公共访问使用的成员。

· 和私有成员类似,受保护的成员对于类的用户来说是不可访问的。

· 和公有成员相似,受保护的成员对于派生类的成员和友元来说是可访问的。

· 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。

公有、私有和受保护继承

某个类对其继承而来的成员的访问权限受到两个因素影响:一是在基类中该成员的访问说明符,二是在派生类的派生列表中的访问说明符。

对基类成员的访问权限只与基类中的访问说明符有关。

派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限:

派生访问说明符还可以控制继承自派生类的新类的访问权限:

派生类向基类转换的可访问性

友元与继承

就像友元关系不能传递一样,友元关系同样也不能继承。

改变个别成员的可访问性

有时我们需要改变派生类继承的某个名字的访问级别,通过使用using声明可以达到这一目的。

默认的继承保护级别

默认情况下,使用class关键字定义的派生类是私有继承的;而使用struct关键字定义的派生类是公有继承的,

在使用struct关键字和class关键字定义的类之间唯一的差别就是默认成员访问说明符及默认派生访问说明符;除此之外,再无其他不同之处。

六、继承中的类作用域

当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。

在编译时进行名字查找

一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致(当使用基类的引用或指针时会发生这种情况),但是我们能使用哪些成员仍然是由静态类型决定的。

举个例子,我们给Disc_quote添加一个新成员,该成员返回一个存有最小(或最大)数量及折扣价格的pair:

名字冲突与继承

和其他作用域一样,派生类也能重用定义在其直接基类或间接基类中的名字,此时定义在内层作用域(即派生类)的名字将隐藏定义在外层作用域(即基类)的名字。

通过作用域运算符来使用隐藏的成员

名字查找先于类型查找

如果派生类(即内层作用域)的成员与基类(即外层作用域)的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉

虚函数与作用域

覆盖重载的函数

七、构造函数与拷贝控制

如果一个类(基类或派生类)没有定义拷贝控制操作,则编译器将为它合成一个版本。这个合成的版本可以定义成删除的函数。

1. 虚析构函数

继承关系对基类拷贝控制最直接的影响是基类通常应该定义一个虚析构函数,这样我们就嫩滑动态分配继承体系中的对象了。

虚析构函数将阻止合成移动操作

如果一个类定义了析构函数,即使它通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动操作。

2. 合成拷贝控制与继承
派生类中删除的拷贝控制与基类的关系

移动操作与继承

大多数基类都会定义一个虚析构函数。因此在默认情况下,基类通常不含有合成的移动操作,而且在它的派生类中也没有合成的移动操作。

因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当我们确实需要执行移动操作时应该首先在基类中进行定义。

3. 派生类的拷贝控制成员

定义派生类的拷贝或移动构造函数

当为派生类定义拷贝或移动构造函数时,我们通常使用对应的基类构造函数初始化对象的基类部分:

派生类赋值运算符

与拷贝和移动构造函数一样,派生类的赋值运算符也必须显式地为其基类部分赋值。

无论基类的构造函数或赋值运算符是自定义的版本还是合成的版本,派生类的对应操作都能使用它们。

派生类析构函数

在析构函数体执行完成后,对象的成员会被隐式销毁。类似的,对象的基类部分也是隐式销毁的。和构造函数及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源:

在构造函数和析构函数中调用虚函数

4. 继承的构造函数

类不能继承默认、拷贝和移动构造函数。如果派生类没有直接定义这些构造函数,则编译器将为派生类合成它们。

派生类继承基类构造函数的方式是提供一条注明了(直接)基类名的using声明语句。

继承的构造函数的特点

和普通成员的using声明不一样,一个构造函数的using声明不会改变该构造函数的访问级别。例如,不管using声明出现在哪儿,基类的私有构造函数在派生类中还是一个私有构造函数;受保护的构造函数和公有构造函数也是同样的规则。

而且,一个using声明不能指定explicit或constexpr。如果基类的构造函数是explicit或者constexpr,则继承的构造函数也拥有相同的属性。

八、容器与继承

当我们使用容器存放继承体系中的对象时,通常必须采取间接存储的方式。

在容器中放置(智能)指针而非对象

当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类的指针(更好的选择是智能指针)。这些指针所指的动态类型可能是基类类型,也可能是派生类类型。

模拟虚拷贝

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

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

相关文章

HTML静态网页成品作业(HTML+CSS)—— 金宝贝儿童教育机构介绍网页(2个页面)

🎉不定期分享源码,关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 🏷️本套采用HTMLCSS,未使用Javacsript代码,共有2个页面。 二、作品演示 三、代…

Stable diffusion prompts 使用语法、参数讲解、插件安装教程

Stable diffusion prompts 使用语法、参数讲解、插件安装教程 本文基于 Stable diffusion WebUI 进行讲解(安装在 AutoDL 上,安装在本地电脑上的也同样适用本教程)。 初始界面: 文件目录结构: 上图红框中的 4 个文件…

requests模块编写漏洞检测工具

#尝试使用python登录pikachu爆破模块 #发送post数据包,包含用户名密码,对接受到的响应进行判断,如何为登录成功 #爆破密码 with open(passwor.txt,r) as f: passwordf.readlines() for i in password: data {username: admin, password: i, …

数据结构——算法和算法效率的度量

目录 一、引言 二、算法 1 算法的基本概念 2 算法的复杂度 2.1 时间复杂度 2.1.1 概念 2.1.2 大O的渐进表示 3 算法的空间复杂度 3.1 概念 3.2 实例 4 实例分析 5 结论 一、引言 大家在写代码的时候有没有发现写同样功能的代码有多种不同的写法,而不同的代…

51种企业应用架构模式详解

01 什么是企业应用 我的职业生涯专注于企业应用,因此,这里所谈及的模式也都是关于企业应用的。(企业应用还有一些其他的说法,如“信息系统”或更早期的“数据处理”。)那么,这里的“企业应用”具体指的是什…

[原型资源分享]经典产品饿了么UI模版部件库

​部件库预览链接:https://f13gm0.axshare.com 支持版本: Axrure RP 8 文件大小: 3MB 文档内容介绍 基本部件:表单样式:12款、数据样式:10款、服务样式:6款、导航:5款、业务组件:7款、 模板…

MySQL之查询性能优化(三)

查询性能优化 重构查询的方式 在优化有问题的查询时,目标应该是找到一个更优的方法获得实际需要的记过——而不是一定总是需要从MySQL获取一模一样的结果集。有时候,可以将查询转换一种写法让其返回一样的结果,但是性能更好。但也可以通过修…

Python魔法之旅-魔法方法(14)

目录 一、概述 1、定义 2、作用 二、应用场景 1、构造和析构 2、操作符重载 3、字符串和表示 4、容器管理 5、可调用对象 6、上下文管理 7、属性访问和描述符 8、迭代器和生成器 9、数值类型 10、复制和序列化 11、自定义元类行为 12、自定义类行为 13、类型检…

【Python】pyinstaller打包时添加详细信息

在要被打包的py文件同级目录新建version.txt,写入以下内容 # UTF-8 # # For more details about fixed file info ffi see: # http://msdn.microsoft.com/en-us/library/aa381058.aspx # VSVersionInfo(ffiFixedFileInfo(filevers(1, 4, 0, 5),prodvers(1, 4, 0, 5…

AIGC 011-SAM第一个图像分割大模型-分割一切!

AIGC 011-SAM第一个图像分割大模型-分割一切! 文章目录 0 论文工作1论文方法2 效果 0 论文工作 这篇论文介绍了 Segment Anything (SA) 项目,这是一个全新的图像分割任务、模型和数据集。SA 项目是一个具有里程碑意义的工作,它为图像分割领域…

迎七一党史知识竞赛答题怎么做

迎七一党史知识竞赛答题,不仅是对于党史知识的检验,更是对于参赛者学习态度和综合能力的考量。在参与这类竞赛时,我们需要做好充分的准备,掌握一定的答题技巧,才能取得好的成绩。 首先,我们要深入了解竞赛…

FFmpeg播放器的相关概念【1】

播放器框架 相关术语 •容器/文件(Conainer/File):即特定格式的多媒体文件,比如mp4、flv、mkv等。 • 媒体流(Stream):表示时间轴上的一段连续数据,如一段声音数据、一段…

UFS Explorer Professional Recovery: 如何从启用了 mSATA 缓存的 Drobo 设备中恢复数据

天津鸿萌科贸发展有限公司是 UFS Explorer Professional Recovery 数据恢复软件的授权代理商。 UFS Explorer Professional Recovery 数据恢复软件提供综合性的解决方案,用于解决复杂的数据恢复案例,包括那些采用特殊存储技术的案例,或介质受…

上海亚商投顾:创业板指震荡收涨 超70家ST股跌停

上海亚商投顾前言:无惧大盘涨跌,解密龙虎榜资金,跟踪一线游资和机构资金动向,识别短期热点和强势个股。 一.市场情绪 沪指昨日震荡震荡,创业板指走势稍强,盘中一度涨超1%,黄白二线分化严重。算…

vue ts 导入 @/assets/ 红色显示的问题解决

vue ts 导入 /assets/ 红色显示的问题解决 一、问题描述 在使用的时候这样导入会出现如上的错误。 在使用的时候,导入的类型也没有对应的代码提示,说明导入有问题。 二、解决 在 tsconfig.json 中添加如下内容: {"compilerOptions&…

AI大模型探索之路-实战篇15: Agent智能数据分析平台之整合封装Tools和Memory功能代码

系列篇章💥 AI大模型探索之路-实战篇4:深入DB-GPT数据应用开发框架调研 AI大模型探索之路-实战篇5:探索Open Interpreter开放代码解释器调研 AI大模型探索之路-实战篇6:掌握Function Calling的详细流程 AI大模型探索之路-实战篇7…

46.ThreadPoolExcutor接口

线程池状态 ThreadPoolExcutor使用int高3位来表示线程池状态,低29位表示线程数量 状态高三位接收新任务处理阻塞队列任务说明RUNNING111YYSHUTDOWN000NY不会接收新任务,但会处理阻塞队列剩余任务,比较温和,已经提交的任务都会执…

C++ STL-迭代器函数对象适配器

目录 一.迭代器 二. 函数对象 三. 适配器 一.迭代器 是一种通用的指针类型,可以用来遍历 STL 容器中的元素。 具有以下作用和意义: 提供一种通用的方式来访问容器中的元素。允许对不同类型的容器进行统一的操作。增强了代码的灵活性和可扩展性。 一…

【C++题解】1085 - 寻找雷劈数

问题:1085 - 寻找雷劈数 类型:for循环 题目描述: 把整数 3025 从中剪开分为 30 和 25 两个数,此时再将这两数之和平方,计算结果又等于原数。 (3025)(3025)55553025 ,这样的数叫“雷劈数”。 求所有符合这…

Photoshop版本选择及系统要求

1、ps2018cc/2020cc版本 适合新手,增加了很多智能化操作,非常方便好上手。 2020: 2、ps2015版本 cc2015版本不论是功能还是硬件上,都是不二选择,适合于配置较低的电脑,该有的基本功能它都有。 3、2021/2…