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

参考资料:

  • 《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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

pycharm import torch

目录 1 安装 2 conda环境配置 3 测试 开始学习Pytorch! 1 安装 我的电脑 Windows 11 Python 3.11 Anaconda3-2023.09-0-Windows-x86_64.exe cuda_11.8.0_522.06_windows.exe pytorch &#xff08;管理员命令行安装&#xff09; pycharm-community-2023.3.2.exe 2 c…

集成电路/芯片可循环成本(RE)和非可循环成本(NRE)

数字集成电路&#xff08;Integrated Circuit&#xff0c;IC&#xff09;的成本可以分为可循环成本和非可循环成本两类。 可循环成本是指在芯片制造过程中&#xff0c;随着芯片批量的增加而逐渐降低的成本。可循环成本主要包括&#xff1a; 晶圆制造成本&#xff1a;包括晶圆…

OpenCV-19图像的仿射变换

放射变换是图像旋转&#xff0c;缩放&#xff0c;平移的总称&#xff0c;具体的做法是通过一个矩阵和原图片坐标进行计算&#xff0c;得到新的坐标&#xff0c;完成变换&#xff0c;所以关键就是这个矩阵。 一、仿射变换之图像平移 使用API------warpAffine&#xff08;src &…

C++ --- 基础知识与细节(上)

目录 1. 常量 2.关键字 3.标识符命名规则 4.数据类型 4.1 整型 4.2实型&#xff08;浮点型&#xff09; 4.3 字符型 4.4 字符串型 4.5布尔类型 bool &#xff08;只占一个字节大小&#xff09; 5.sizeof关键字 6.数据的输入 7. goto语句 8. 一维数组 // 数组名是常量&am…

【Filament】材质系统

1 前言 本文主要介绍 Filament 的材质系统&#xff0c;官方介绍详见 → Filament Materials Guide。材质系统中会涉及到一些空间和变换的知识点&#xff0c;可以参考&#xff1a;【Unity3D】空间和变换、【Unity3D】Shader常量、变量、结构体、函数、【OpenGL ES】MVP矩阵变换、…

IDEA中启动项目报堆内存溢出或者没有足够内存的错误

1.报错现象 java.lang.OutOfMemoryError: Java heap space 或者 Could not reserve enough space for object heap 2.解决办法 在运行配置中VM选项后加下面的配置&#xff1a; -server -XX:MaxHeapSize256m -Xms512m -Xmx512m -XX:PermSize128M -XX:MaxPermSize256m 3.JVM虚…

Conmi的正确答案——eclipse C/C++显示“未解析的包含:<xxx.h>”/“Unresolved inclusion: <xxx.h>”

eclipse IDE 版本&#xff1a;2023-12 部分采自&#xff1a;解决方法&#xff1a;关于问题 “C - Unresolved inclusion: <iostream>” 解释事项&#xff1a;方法一可能版本不同&#xff0c;部分界面修改了。这里使用的是方法二的解决方法。&#xff08;或者各位大神的描…

Gartner发布数据安全治理指南:采取四个关键步骤,加快数据安全治理的采用

部署数据安全控制措施是为了保护数据及隐私&#xff0c;但业务领导者通常采用与业务成果直接相关的评判标准对数据进行分类&#xff0c;这与安全领域使用的数据分类观点不同。首席信息安全官&#xff08;CISO&#xff09;应克服沟通障碍&#xff0c;展示如何通过增强数据安全来…

Verilog刷题笔记15

题目&#xff1a; An adder-subtractor can be built from an adder by optionally negating one of the inputs, which is equivalent to inverting the input then adding 1. The net result is a circuit that can do two operations: (a b 0) and (a ~b 1). See Wikipe…

15.云原生之k8s容灾与恢复实战

云原生专栏大纲 文章目录 Velero与etcd介绍Velero与etcd备份应用场景Velero与etcd在k8s备份上的区别 Velero备份恢复流程备份工作流程Velero备份时&#xff0c;若k8s集群发送变化&#xff0c;会发发生情况&#xff1f;Velero 备份pv&#xff0c;pv中数据变化&#xff0c;会发发…

k8s---对外服务 ingress

目录 目录 目录 ingress与service ingress的组成 ingress-controller&#xff1a; ingress暴露服务的方式 2.方式二&#xff1a;DaemonSethostnetworknodeSelector DaemonSethostnetworknodeSelector如何实现 3.deploymentNodePort&#xff1a; 虚拟主机的方式实现http代…

CHAPTER 9: 《DESIGN A WEB CRAWLER》第9章 《设计一个web爬虫》

CHAPTER 9: 《DESIGN A WEB CRAWLER》第九章 设计一个web爬虫 在本章中&#xff0c;我们将重点介绍网络爬虫设计&#xff1a;一种有趣而经典的系统设计 面试问题。 网络爬虫被称为机器人或蜘蛛。它被搜索引擎广泛用于发现网络上的新内容或更新内容。内容可以是网页、图像、视频…

TCP的三次握手,四次挥手

三次握手 第一次握手&#xff1a;客户端发送SYN报文&#xff0c;井发送seq为x序列号给服务端&#xff0c;等待服务端的确认第二次握手&#xff1a;服务端发送SYNACK报文&#xff0c;并发送seq为Y的序列号&#xff0c;在确认序列号为x1第三次握手&#xff1a;客户端发送ACK报文&…

基于Python flask京东服装数据分析可视化系统,可视化多种多样

技术介绍 京东服装品牌数据分析系统是基于Python Flask框架开发的一款用于分析和展示京东服装品牌数据的Web应用程序。该系统利用Flask提供了一个简单而强大的后端框架&#xff0c;结合Request库进行网络爬虫获取京东服装品牌数据&#xff0c;并使用Pyecharts进行可视化展示&a…

【Internet Protocol】ip介绍,如何组局域网实现远程桌面和文件共享

文章目录 1.何为“上网”1.1 定义1.2 为什么连了WiFi就能上网了&#xff1f; 2.ip2.1 什么是ip2.2 为什么区分广域网和局域网&#xff0c;ip的唯一性2.3 如何查看设备的ip2.4 什么叫"ping"2.5 区分是否两个ip是否在同一局域网2.5.1 最稳妥的方式&#xff1a;ip&m…

VirtualBox安装kail虚拟机并配置内外网双网卡网络

VirtualBox安装kail虚拟机并配置内外网双网卡网络 Kali Linux是一份基于Debian的发行&#xff0c;它带有一套安全和计算机取证工具。本文档适用于在VirtualBox平台上安装kali-2023.4虚拟机。 1. 安装准备 1.1 安装平台 Windows 11 1.2. 软件信息 软件名称软件版本安装路径…

95%数据格式都支持?3D模型格式转换引擎HOOPS Exchange真绝了!

一、HOOPS Exchange概述 HOOPS Exchange是一款用于读写专有和开放CAD数据格式的通用工具包&#xff0c;专为开发3D工程应用程序而设计&#xff0c;通过单一界面&#xff0c;即可读取和写入30多种CAD文件格式&#xff0c;无需依赖任何CAD系统。HOOPS Exchange可以无缝集成到CAD…

【计算机图形学】习题课:Viewing

【计算机图形学】Viewing 部分问题与解答 CS100433 Computer Graphics Assignment 21 Proof the composed transformations defined in global coordinate frame is equivalent to the composed transformations defined in local coordinate frame but in different composing…

rust跟我学五:是否安装双系统

图为RUST吉祥物 大家好,我是get_local_info作者带剑书生,这里用一篇文章讲解get_local_info是怎么得到检测双系统的。 首先,先要了解get_local_info是什么? get_local_info是一个获取linux系统信息的rust三方库,并提供一些常用功能,目前版本0.2.4。详细介绍地址:[我的Ru…

ubuntu opengl安装使用

文章目录 1.opengl的安装2.安装GLFW3.安装glad4.调用示例参考 1.opengl的安装 OpenGL到底是什么。一般它被认为是一个API(Application Programming Interface, 应用程序编程接口)&#xff0c;包含了一系列可以操作图形、图像的函数。然而&#xff0c;OpenGL本身并不是一个API&…