《C++ Primer》第15章 面向对象程序设计(一)

news2024/11/26 12:14:38

参考资料:

  • 《C++ Primer》第5版
  • 《C++ Primer 习题集》第5版

15.1 OOP:概述(P526)

**面向对象程序设计(object-oriented programming)**的核心思想是数据抽象、继承和动态绑定。

继承

通过继承(inheritance)联系在一起的类构成一种层次关系,在层次关系根部有一个基类(base class),从基类继承而来的类称为派生类(derived class)基类负责定义在层次关系中所有类的共同成员,每个派生类定义各自独有的成员

我们定义一个名为 Quote 的类,表示按原价销售的数据,并将它作为层次关系的基类。Quote 派生出另一个名为 Bulk_quote 的类,表示可以打折销售的书籍:

class Quote {
public:
	string isbn() const;
	virtual double net_price(size_t n) const;
};

class Bulk_quote :public Quote {
public:
	double net_price(size_t n) const override;
};

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

派生类必须通过使用类派生列表(class derivation list)明确指出它从哪些类继承而来。类派生列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,每个基类前面可以有访问说明符。派生类必须对所有重新定义的虚函数进行声明,派生类可以选择在这样的函数之前加上 virtual 关键字,但不是必须的。C++11 新标准允许派生类显式注明使用哪个成员函数改写基类的虚函数,方式是在函数的形参列表增加 override 关键字。

动态绑定

通过动态绑定(dynamic binding),我们能用同一段代码分别处理 QuoteBulk_quote 的对象:

double print_total(ostream &os,
		const Quote &item, size_t n) {
	// 调用Quote::net_price或者Bulk_quote::net_price
	double ret = item.net_price(n);
	os << "ISBN: " << item.isbn()
		<< " # sold: " << n << "total due: " << ret << endl;
	return ret;
}

对于上面的函数,由于其 item 形参是基类 Quote 的一个引用,所以我们既能使用 Quote 对象,也能使用 Bulk_quote 对象调用该函数;因为 print_total 使用引用类型调用 net_price ,所以实际传入 print_total 的对象类型将决定执行 net_price 的哪个版本。

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

15.2 定义基类和派生类(P527)

15.2.1 定义基类(P528)

我们首先完成 Quote 类的定义:

class Quote {
public:
	Quote() = default;
	Quote(string &book, double sales_price):
		bookNo(book), price(sales_price) { }
	string isbn() const { return bookNo; }
	virtual double net_price(size_t n) const 
		{ return n * price; }
	virtual ~Quote() = default;
private:
	string bookNo;     // 书籍的ISBN编号
protected:
	double price = 0.0;    // 书籍的原价
};

基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作。

成员函数与继承

当我们使用引用或指针调用虚函数时,该调用将被动态绑定除构造函数外的任何非静态成员函数都可以是虚函数,关键字 virtual 只能出现在类内部的声明语句之前。如果基类将一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数

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

访问控制和继承

派生类继承定义在基类中的成员,但派生类的成员函数不一定有权访问从基类继承而来的成员。有些时候,我们希望基类中的某些成员可以被派生类访问,而不能被其他用户访问,用 protected 访问说明符可以达到这个效果。

我们希望 Quote 的派生类定义各自的 net_price 函数,因此派生类需要访问 Quoteprice 成员,所以我们将 price 定义成 protected 的。

15.2.2 定义派生类(P529)

class Bulk_quote :public Quote {
public:
	Bulk_quote() = default;
	Bulk_quote(const string &book, double p,
		size_t qty, double disc):
		Quote(book, p), min_qty(qty), discount(disc) { }
	double net_price(size_t n) const override;
private:
	size_t min_qty = 0;    // 适用折扣的最小购买量
	double discount = 0.0;    // 折扣额
};

double Bulk_quote::net_price(size_t n) const {
	if (n >= min_qty) {
		return n * (1 - discount) * price;
	}
	else {
		return n * price;
	}
}

Bulk_quoteQuote 继承了 isbn 函数和 bookNoprice 等数据成员,还定义了自己版本 net_price ,同时增加了两个新的数据成员 min_qtydiscount

我们可以将公有派生类型的对象绑定到基类的引用或指针上。

派生类中的虚函数

如果派生类没有覆盖基类中的虚函数,则派生类会直接继承其在基类中的版本。

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

一个派生类对象包含多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及一个与基类对应的子对象,如果有多个基类,则这样的子对象也有多个。C++ 标准没有规定派生类的对象在内存中如何分布,但我们这样认为:

51ccdf8b2b8f8007b11de0c34cf65d9

因为派生类对象中含有基类的部分,所以我们可以把派生类的对当成基类对象来使用:

Quote item;
Bulk_quote bulk;
Quote *p = &item;
p = &bulk;
Quote &r = bulk;

这种转换称为**派生类向基类(derived-to-base)**的转换,编译器会隐式地执行这种转换。

派生类构造函数

尽管派生类对象中含有从基类继承而来的成员,但派生类并不能直接初始化这些成员,而是必须使用基类的构造函数来初始化它的基类部分。

每个类控制自己的成员初始化过程

Bulk_quote 的构造函数执行 Quote 的构造函数,然后再初始化自己定义的 min_qtydiscount 成员。

派生类使用基类的成员

继承与静态成员

如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。假设某静态成员是可访问的,则我们既能通过基类使用它,也能通过派生类使用它:

void Derived::f(const Derived &derived_obj) {
	Base::statmem();    // 正确,Base定义了statmem
	Derived::statmem();    // 正确,Derived继承了statmem
	derived_obj.statmem();    // 通过Derived访问
	statmem();    // 通过this访问
}

派生类的声明

派生类的声明和其他类相同,声明中包含类名但不包含它的派生列表

class Bulk_quote : public Quote;    // 错误,派生类列表不能出现在这里
class Bulk_quote;

被用作基类的类

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

一个类可以同时基类和派生类:

class Base { /* ... */ };
class D1: public Base { /* ... */ };
class D2: public D1 { /* ... */ };

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

防止继承的发生

有时我们会定义这样一种类,不希望其他类继承它。C++11 新标准提供了一种防止继承的方法,即在类名后面跟一个关键字 final

class NoDerived final { /* ... */ };    // NoDerived不能作为基类
class Bad : NoDerived { /* ... */ };    // 错误,NoDerived是final的

15.2.3 类型转换与继承(P534)

理解基类和派生类之间的类型转换是理解 C++ 语言面向对象编程的关键所在。

可以将积累的指针或引用绑定到派生类对象有一层极为重要的意义:当时使用基类的指针或引用时,实际上我们并不清楚这个指针或引用所绑定对象的真实类型。

静态类型和动态类型

当我们使用存在继承关系的类型时,必须将一个变量或表达式的静态类型(static type)和动态类型(dynamic type)区分开来。静态类型在编译时总是已知的,而动态类型直到运行时才可知。

例如,当 print_total 调用 net_price 时:

double ret = item.net_price(n);

item 的静态类型是 Quote& ,它的动态类型依赖于 item 绑定的实参。如果我们传递给 print_total 一个 Bulk_quote 对象,那么 item 的静态类型将与它的动态类型不一致。

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

不存在基类向派生类的自动类型转换:

Quote base;
Bulk_quote *bulkP = &base;    // 错误
Bulk_quote &bulkR = base;    // 错误

即使一个基类指针或引用绑定在一个派生类对象上,我们也不能执行从基类向派生类的转换:

Bulk_quote bulk;
Quote *itemP = &bulk;
Bulk_quote *bulkP = itemP;    // 错误

编译器只能通过检查指针或引用的静态类型来推断转换是否合法。如果基类中至少包含一个虚函数,我们可以使用 dynamic_cast 请求一个类型转换,该转换的安全性检查将在运行时执行

Bulk_quote *bulkP = dynamic_cast<Bulk_quote*>(itemP);    // 正确

如果已知某个基类向派生类的转换是安全的,则我们可以使用 static_cast 来强制覆盖编译器的检查工作。

对象之间不存在类型转换

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

Bulk_quote bulk;
Quote item(bulk);    // 使用Quote::Quote(const Quote&)
item = bulk;    // 使用Quote::operator=(const Quote&)

当我们用一个派生类对象为一个基类对象初始化或赋值时,派生类对象中只有基类部分会被拷贝、移动、赋值,它的派生类部分会被忽略掉。

15.3 虚函数(P536)

我们知道,使用基类的引用或指针调用一个虚函数成员时,会执行动态绑定。由于我们直到运行时在确定到底调用哪个版本的虚函数,所以在调用前所有虚函数都必须有定义

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

需要强调的是,动态绑定只有当我们通过指针或引用调用虚函数才会发生。如果我们用一个普通对象调用虚函数,在编译时就会将调用的版本确定下来。

派生类中的虚函数

一旦某个函数被声明称虚函数,则在所有派生类中它都是虚函数。

一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。同样,派生类中虚函数的返回类型也必须与基类函数相同。该规则存在一个例外,当类的虚函数返回类型是类本身的指针或引用时,上述规则无效。

finaloverride说明符

如果派生类中定义了一个函数,该函数与基类中虚函数的名字相同但形参列表不同,这仍然是合法行为,编译器认为这个新函数与基类中的函数是相互独立的。在 C++11 新标准中,我们可以使用 override 关键字说明派生类中的虚函数,如果我们用 override 标记了某个函数,但该函数没有覆盖已存在的虚函数,此时编译器将报错:

class B {
	virtual void f1(int) const;
	virtual void f2();
	void f3();
};
class D1 : public B {
	void f1(int) const override;
	void f2(int) override;    // B没有形如f2(int)的虚函数
	void f3() override;    // f3不是虚函数
	void f4() override;    // B中没有名为f3的函数
};

我们还能把某个函数指定为 final ,一旦某个函数被标记为 final ,则任何覆盖该函数的行为都将引发错误:

class B {
	virtual void f1(int) const final;
};
class D1 : public B {
	void f1(int) const override;    // 无法覆盖final函数
};

overridefinal 都出现在形参列表、const 、引用修饰符、尾置返回类型之后。

虚函数与默认实参

虚函数也可以有默认实参,但在函数调用中,默认实参的值由静态类型决定

如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。

回避虚函数的机制

在某些情况下,我们不希望对虚函数的调用进行动态绑定,而是强制执行某一特定版本。使用作用域运算符可以达成这一目的:

double undiscounted = baseP->Quote::net_price(42);

上面的调用将在编译时完成解析。

15.4 抽象基类(P540)

假设我们希望扩展前面书店程序的定义,令其支持多种打折策略。每种打折策略都需要一个购买量和一个折扣值,我们可以定义的一个新类 Disc_quote 来支持不同的折扣策略,表示特定打折策略的类将继承自 Disc_quote 并定义自己的 net_price 函数。

Disc_quote 类的 net_price 是没有任何意义的,所以直接继承 Quote 中的 net_price 即可。

由于 Disc_quote 不代表任何一种具体的打折策略,所以我们不希望用户创建 Disc_quote 类型的对象。

纯虚函数

我们可以将 Disc_quotenet_price 函数定义成纯虚(pure virtual)函数,明确告诉用户这个函数没有实际意义。和普通虚函数不同,纯虚函数无需定义,在函数体的位置书写 =0 就能将一个虚函数声明成纯虚函数,其中,=0 只能出现在类内部的虚函数声明处

class Disc_quote :public Quote {
public:
	Disc_quote() = default;
	Disc_quote(const string &book, double price,
		size_t qty, double disc) :
		Quote(book, price), quantity(qty),
		discount(disc) { }
	double net_price(size_t) const = 0;
protected:
	size_t quantity = 0;    // 折扣适用的购买量
	double discount = 0.0;    // 表示折扣的小数值
};

我们也可以为纯虚函数提供定义,但函数体必须定义在类的外部

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

含有(或未经覆盖直接继承)纯虚函数的类是抽象基类(abstract base class)。抽象基类负责定义接口,后续的派生类可以覆盖该接口。

我们不能直接创建一个抽象基类的对象:

Disc_quote discounted;    // 错误

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

我们重新实现 Bulk_quote

class Bulk_quote :public Disc_quote {
public:
	Bulk_quote() = default;
	Bulk_quote(const string &book, double p,
		size_t qty, double disc):
			Disc_quote(book, p, qty, disc) { }
	double net_price(size_t n) const override;
};

这个版本的 Bulk_quote 的直接基类是 Disc_quote ,间接基类是 Quote

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

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

相关文章

逆变器之变压器基础知识

1 基础知识 我们的磁芯要工作的话&#xff0c;必须要有磁场&#xff0c;有磁场就就会有磁力线。 磁场我们是看不着摸不见的&#xff0c;为了好的描述磁场&#xff0c;我们就用磁力线来表示&#xff0c; 磁力线是有方向的&#xff0c;在一个磁铁的内部磁力线是从 S指向N的&…

Elasticsearch:和 LIamaIndex 的集成

LlamaIndex 是一个数据框架&#xff0c;供 LLM 应用程序摄取、构建和访问私有或特定领域的数据。 LlamaIndex 是开源的&#xff0c;可用于构建各种应用程序。 在 GitHub 上查看该项目。 安装 在 Docker 上设置 Elasticsearch 使用以下 docker 命令启动单节点 Elasticsearch 实…

【创作活动】ChatGPT 和文心一言哪个更好用?

文章目录 文心一言优点缺点 ChatGPT优点缺点 Java编码能力比较对人工智能的看法 ChatGPT是由OpenAI开发的交互式AI大模型&#xff0c; 文心一言是由百度研发的知识增强大语言模型&#xff0c;本文从Java开发的角度对比一下哪个更好用&#xff08;本文仅用于投稿CSDN创造活动&am…

【playwright】新一代自动化测试神器playwright+python系列课程15_playwright网页相关操作_网页截图

Playwright 网页截图 在做web自动化测试时&#xff0c;脚本执行时会出现执行失败的情况&#xff0c;这个时候就需要分析失败的原因&#xff0c;由于脚本执行时是不需要人工盯着执行的&#xff0c;这个时候就需要在脚本执行失败时保留某些信息方便脚本执行完成后来分析失败的原…

SpringMVC(全局异常处理.动态接收Ajax请求)

1.全局异常处理 1 异常处理器 基于AOP 用户发起请求, SpringMVC接受请求, SpringMVC加载静态资源问题说明 请求过去了,但没有处理 规则说明:静态资源进入SpringMVC框架之后,没有找到要怎样处理静态资源的方法,所以他们就不解决,也就不显示 解决方法:SpringMVC基于Servlet处理…

添加边界值分析测试用例

1.1创建项目成功后会自动生成封装好的函数&#xff0c;在这些封装好的函数上点击右键&#xff0c;添加边界值分析测试用例&#xff0c;如下图所示。 1.2生成的用例模版是不可以直接运行的&#xff0c;需要我们分别点击它们&#xff0c;让它们自动生成相应测试用例。如下图所示&…

华为路由器配置访问控制列表ACL用例

配置要求 如组网图所示&#xff0c;R3为服务器&#xff0c;R1为客户端&#xff0c;客户端与服务器 之间路由可达。其中R1和R2间互联物理接口地址分别为 10.1.2.1/24和10.1.2.2/24&#xff0c;R2和R3间互联物理接口地址分别 为10.1.3.2/24和10.1.3.1/24。另外&#xff0c;R1上创…

企业计算机服务器中了mkp勒索病毒如何处理,mkp勒索病毒解密

网络技术的不断发展&#xff0c;为企业的生产运营提供了非常有利条件&#xff0c;但也为企业的数据安全埋下隐患&#xff0c;近期&#xff0c;众多企业的服务器遭到了mkp勒索病毒攻击&#xff0c;导致企业计算机服务器瘫痪无法正常工作&#xff0c;严重影响了企业正常生活运营。…

统计学-R语言-5.3

文章目录 前言分位数统计量的标准误总结 前言 本篇文章即为概率与分布的最后一篇文章。 分位数 分位数函数是累积分布函数的反函数。 p-分位数是具有这样性质的一个值&#xff1a;小于或等于它的概率为p。 根据定义&#xff0c;中位数即50%分位数。 分位数通常用于置信区间的…

系统架构的演变:从单体到微服务的旅程

文章目录 前言一、单体架构简图 二、垂直架构简图 三、水平架构简图 四、面向服务架构&#xff08;SOA&#xff09;简图 五、微服务架构简图 总结 前言 随着信息技术的快速发展&#xff0c;系统架构也在不断演变。从早期的单体架构到现代的微服务架构&#xff0c;每一次的变革都…

Leetcode454四数相加Ⅱ(java实现)

今天&#xff0c;我们分享的题目是Leetcode454四数相加Ⅱ&#xff0c;我们先来看题目&#xff1a; 首先可以从题意中大体得知&#xff0c;也是从某个集合中找有没有符合条件的元素。遇见这种类型的题目我们可以考虑用哈希表&#xff0c;本题我们选取的是map集合&#xff0c;因…

Damicms漏洞挖掘

今天又是挖漏洞的一天&#xff0c;&#xff08;寒假了也不能停下挖洞的步伐&#xff09; “《学而不思则罔&#xff0c;不学不思则爽》” ​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​ 1.存储型XSS XSS…

Spring MVC学习之——自定义日期转化器

日期转换器 在数据库中的日期数据是date类型&#xff0c;而如何我们想在页面自己添加数据&#xff0c;一般是使用年-月-日的形式&#xff0c;这种形式不仅date类型接收不到&#xff0c;而且传来的是String类型&#xff0c;此时&#xff0c;我们就可以自定义日期转换器来接收数…

React 基于Ant Degisn 实现table表格列表拖拽排序

效果图&#xff1a; 代码&#xff1a; myRow.js import { MenuOutlined } from ant-design/icons; import { DndContext } from dnd-kit/core; import { restrictToVerticalAxis } from dnd-kit/modifiers; import {arrayMove,SortableContext,useSortable,verticalListSorti…

C——语言内存函数

目录 一、memcpy的使用和模拟实现 1.memcpy函数原型 2.memcpy函数的使用 3.memcpy函数的模拟实现 二、memmove的使用和模拟实现 1.memmove函数原型 2.memmove函数的使用 3.memmove函数的模拟实现 三、memset的使用 1.memset函数原型 2.memset函数的使用 四、memcmp…

【话题】ChatGPT 和文心一言哪个更好用

星火说 ChatGPT 智能回复&#xff1a;ChatGPT能够根据上下文理解用户的问题&#xff0c;并给出相应的回答。它使用深度学习算法来理解和生成文本&#xff0c;因此可以处理各种复杂的问题和话题。语言准确性&#xff1a;ChatGPT的语言模型经过了大量的训练数据&#xff0c;因此…

vue项目本地开发完成后部署到服务器后报404是什么原因

文章目录 一、如何部署二、404问题为什么history模式下有问题为什么hash模式下没有问题解决方案 参考文献 一、如何部署 前后端分离开发模式下&#xff0c;前后端是独立布署的&#xff0c;前端只需要将最后的构建物上传至目标服务器的web容器指定的静态目录下即可 我们知道vu…

macOS系统下载安装IDEA 操作流程

目录 第一步 进入官网&#xff0c;选择箭头指向的版本 第二步 下载完成后打开&#xff0c;拖动安装包安装​编辑 第三步 点击" project"&#xff0c;在JDK下拉框选择"Download JDK" 第四步 下载完成以后&#xff0c;点击右下角的Create按钮。 第一步 进…

【Docker】在Windows操作系统安装Docker前配置环境

欢迎来到《小5讲堂》&#xff0c;大家好&#xff0c;我是全栈小5。 这是《Docker容器》序列文章&#xff0c;每篇文章将以博主理解的角度展开讲解&#xff0c; 特别是针对知识点的概念进行叙说&#xff0c;大部分文章将会对这些概念进行实际例子验证&#xff0c;以此达到加深对…

PMP考试中常见的组织/人员/职位/角色的含义,以及典型真题分析3

华研荟继续为您分享在PMP考试中经常出现的一些组织、人员、职位和角色的含义&#xff0c;并且通过近年真题分析来给大家体会考试中如何理解&#xff0c;如何解答。 这篇文章来看客户和供应商/分包商&#xff0c;也是PMP考试中经常出现的两类人&#xff08;角色&#xff09;。 …