【C++】《C++ Primer 》第十五章
第十五章 面向对象程序设计
一、OOP:概述
- 面向对象程序设计(OOP)的核心思想是数据抽象、继承和动态绑定。
- 通过使用数据抽象,可以将类的接口和实现分离。
- 使用继承,可以定义相似的类型并对其相似关系建模。
- 使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
- 继承(inheritance):
- 通过继承联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类(base class)。
- 其他类直接或者间接从基类继承而来,这些继承得到的类成为派生类(derived class)。
- 基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
- 对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明为虚函数(virtual function)。
class Quote {
public:
std::string isbn() const;
virtual double net_price(std::size_t n) const;
};
class BulkQuote : public Quote {
public:
double net_price(std::size_t n) const override; // C++11
};
- 动态绑定(dynamic binding),即运行时绑定:
- 使用同一段代码可以分别处理基类和派生类的对象。
- 函数的允许版本由实参决定,即在运行时选择函数的版本。
// 计算并打印销售给定数量的某种书籍所得的费用
double print_total(ostream &os, const Quote &item, size_t n) {
double ret = item.net_price();
os << "ISBN: " << item.isbn() << " # sold: " << n << " total due: " << ret << endl;
return ret;
}
print_total(cout, basic, 20); // 调用Quote的net_price
print_total(cout, bluk, 20); // 调用BulkQuote的net_price
二、定义基类和派生类
1. 定义基类
- 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
- 基类通过在其成员函数的声明语句之前加上关键字
virtual
使得该函数执行动态绑定。任何构造函数之外的非静态函数都可以是虚函数。 - 关键字
virtual
只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明为虚函数,则该函数在派生类隐式地也是隐函数。 - 如果成员函数没有被声明为虚函数,则解析过程发生在编译时而非运行时。
- 访问控制:
protected
: 基类和其派生类还有友元可以访问private
: 只有基类本身和友元可以访问
2. 定义派生类
- 派生类必须通过
派生列表(class derivation list)
明确指出它是从哪个基类继承而来。形式:冒号。后面紧跟以逗号分隔的基类列表,每个基类前面可以是public
、protected
、private
中的一个。 - C++11允许派生类显式地注明它将使用哪个成员函数改写基类地虚函数,即在函数地形参列表之后加上一个override关键字。
- 派生类钩构造函数:派生类必须使用基类的构造函数去初始化它地基类部分。
- 静态成员:如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。
- 派生类的声明:声明中不包含它的派生列表。
- C++11提供了一种防止继承的方法,在类后面跟一个关键字
final
。
3. 类型转换和继承
- 理解基类和派生类之间的类型转换是理解C++语言OOP的关键所在。
- 可以将基类的指针或引用绑定到派生类对象上。基类的指针或引用的静态类型可能与其动态类型不一致。
- 不存在从基类向派生类的隐式类型转换。
- 派生类向基类的自动类型转换只对指针或引用类型有效,对象之间不存在类型转换。
- 当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或者赋值,它的派生类部分将被忽略掉。
三、虚函数
- 使用虚函数可以执行动态绑定,但是动态绑定只有当我们通过指针或引用调用虚函数时才会发生,因为只有这种情况下对象的动态类型才有可能与静态类型不同。
- OOP的核心思想是多态性(polymorphism)。把具有继承关系的多个类型称为多态类型。引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。
- 基类中的虚函数在派生类中隐含地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配。
- C++11允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即在函数的形参列表之后加一个
override
关键字。 - 如果我们想覆盖某个虚函数,但不小心把形参列表弄错了,这个时候就不会覆盖基类中的虚函数。加上
override
可以明确程序员的意图,让编译器帮忙确认参数列表是否有出错。 - 如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
- 某些情况下,如果希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本。使用作用域运算符可以实现这一目的。
- 通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。使用场景:当一个派生类的虚函数调用它覆盖的基类的虚函数版本时。
四、抽象基类
- 纯虚函数(pure virtual):清晰地告诉用户当前地函数是没有实际意义的。纯虚函数无需定义,只用在函数体的位置前书写
=0
就可以将一个虚函数说明为纯虚函数。 - 含有纯虚函数的类就是抽象基类(abstract base class)。它不能创建抽象基类的对象。
五、访问控制与继承
- 受保护的成员:
protected
说明符可以看做是public和private中和后的产物。- 和私有成员类似,受保护的成员对于类的用户来说是不可访问的。
- 和公有成员类似,受保护的成员对于派生类和友元来说是可访问的。
- 派生类的成员和友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
class Base {
protected:
int prot_mem; //
};
class Sneaky: public: Base {
friend void clobber(SneaKy&); // 能访问Sneaky::port_mem
friend void clobber(Base&); // 不能访问Base::port_mem
int j;
};
// 正确:clobber能访问Sneaky对象的private和protected成员
void clobber(Sneaky &s) { s.j = s.prot_mem = 0; }
// 错误:clobber不能访问Base的protected成员
void clobber(Base &b) { b.prot_mem = 0;}
- 某个类对其继承而来的成员的访问权限受到两个因素影响:在基类中该成员的访问说明符;在派生类的派生列表中的访问说明符;
- 派生访问说明符:
- 对于派生类的成员(及其友元)能否访问其直接基类的成员没有说明影响。
- 它的目的是控制派生类用户对于基类成员的访问权限。比如
struct Priv_Drev: private Base{}
意味着在派生类Priv_Drev
中,从Base继承而来的部分都是private
的。
- 友元关系不能继承。每个类负责控制各自成员的访问权限。
- 改变个别成员的可访问性:使用
using
。 - 区别:默认情况下,使用
class
关键字定义的派生类是私有继承的;使用struct
关键字定义的派生类是公有继承的。
六、继承中的类作用域
- 每个类定义自己的作用域,在这个作用域内我们使用定义类的成员。当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。
- 派生类的成员将隐藏同名的基类成员。
- 除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。
七、构造函数和拷贝函数
1. 虚析构函数
- 基类通常应该定义一个虚析构函数,这样就能动态分配继承体系中的对象了。
- 如果基类的析构函数不是虚函数,则
delete
一个指向派生类对象的基类指针将产生未定义的行为。 - 一个例外:通常如果一个类需要析构函数,那么它同样需要拷贝和赋值操作。但是基类的析构函数是一个例外,应该该析构函数为了成为虚函数而令其内容为空,显然无法推断该基类还需要赋值运算符或拷贝构造函数。
- 虚析构函数将阻止合成移动操作。
2. 合成拷贝控制与继承
- 基类或派生类的合成拷贝控制成员的行为和其他合成的构造函数、赋值运算符或析构函数类似;它们对类本身的成员依次进行初始化、赋值或销毁的操作。
3. 派生类的拷贝控制成员
- 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。
- 派生类析构函数:派生类析构函数先执行,然后执行基类的析构函数。
4. 继承的构造函数
- C++11中,派生类可以重用其直接基类定义的构造函数。
- 语法:
class A: public B {
public:
using A::A; // 继承B的构造函数
double get(int) const;
};
- 继承的构造函数的特点:
- 一个构造函数的
using
声明不会改变该构造函数的访问级别。 - 一个
using
声明语句不能指定explicit
或constexpr
。如果基类的构造函数是explicit
或constexpr
,则继承的构造函数也拥有相同的属性。 - 当一个基类构造函数含有默认实参时,这些实参并不会被继承。
- 如果基类含有几个构造函数,则除了两个例外情况,大多数时候派生类会继承所有这些构造函数。
- 第一个例外:派生类可以继承一部分构造函数,而为其他构造函数定义自己的版本。
- 第二个例外:默认、拷贝和移动构造函数不会被继承。
- 一个构造函数的
八、容器和继承
- 当使用容器存放继承体系中的对象时,通常必须采用间接存储的方式。
- 当派生类对象被赋值给基类对象时,其中的派生类部分将被"切掉",因此容器和存在继承关系的类型无法兼容。
- 希望在容器中存放具有继承关系的对象时,实际上存放的通常是基类的指针(更好的选择是智能指针)。
- 对于C++面向对象的编程来说,一个悖论是我们无法直接使用对象进行面向对象编程。相反,必须使用指针和引用。因为指针会增加程序的复杂性,所以经常定义一些辅助的类来处理这些复杂的情况。
九、文本查询程序再探
【C++】《C++ Primer 》第十五章的更多相关文章
- C++Primer 第十五章
//1.面向对象程序设计的核心思想是数据抽象,继承,动态绑定. // 通过使用数据抽象,我们可以将类的接口和实现分离 // 使用继承,可以定义相似的类型并对其相似关系建模 // 使用动态绑定,可以在一 ...
- C++ Primer Plus学习:第十五章
第十五章 友元.异常和其他 友元 友元类 表 0-1 class Tv { public: friend class Remote; } Remote类可以使用Tv的数据成员,Remote类在Tv类后 ...
- 15第十五章UDF用户自定义函数(转载)
15第十五章UDF用户自定义函数 待补上 原文链接 本文由豆约翰博客备份专家远程一键发布
- 《Linux命令行与shell脚本编程大全》 第十五章 学习笔记
第十五章:控制脚本 处理信号 重温Linux信号 信号 名称 描述 1 HUP 挂起 2 INT 中断 3 QUIT 结束运行 9 KILL 无条件终止 11 SEGV 段错误 15 TERM 尽可能 ...
- CSS3秘笈复习:十三章&十四章&十五章&十六章&十七章
第十三章 1.在使用浮动时,源代码的顺序非常重要.浮动元素的HTML必须处在要包围它的元素的HTML之前. 2.清楚浮动: (1).在外围div的底部添加一个清除元素:clear属性可以防止元素包围浮 ...
- Gradle 1.12用户指南翻译——第四十五章. 应用程序插件
本文由CSDN博客貌似掉线翻译,其他章节的翻译请参见: http://blog.csdn.net/column/details/gradle-translation.html 翻译项目请关注Githu ...
- Gradle 1.12 翻译——第十五章. 任务详述
有关其他已翻译的章节请关注Github上的项目:https://github.com/msdx/gradledoc/tree/1.12,或访问:http://gradledoc.qiniudn.com ...
- Gradle 1.12用户指南翻译——第二十五章. Scala 插件
其他章节的翻译请参见: http://blog.csdn.net/column/details/gradle-translation.html 翻译项目请关注Github上的地址: https://g ...
- Gradle 1.12用户指南翻译——第三十五章. Sonar 插件
本文由CSDN博客万一博主翻译,其他章节的翻译请参见: http://blog.csdn.net/column/details/gradle-translation.html 翻译项目请关注Githu ...
随机推荐
- 如何实现OSM地图本地发布并自定义配图
目录 1.缘起 2.准备环境 2.1.安装linux系统 2.2.安装docker 2.3.安装Docker Compose 2.4.安装git 3.发布地图 3.1.拉取代码 3.2.测试网络 3. ...
- Devpress (DxReport)使用ReportDesigner (一) 基本功能
1. Devpress (DxReport)编辑 (1) 新建一个XtraReport. (2) 在报告上点右键添加元素: 元素说明: (1) 其中有报告头,报告尾,页头,页尾,组头,组尾,详细. ...
- Java 8 新特性:日期处理
1. Java 8 日期处理新特性 Java 8基于ISO标准日期系统,提供了java.time包来处理时间日期,且该包下的所有类都是不可变类型而且线程安全. 2. 关键类 Instant:瞬时实 ...
- ubuntu 18 安装xgboost GPU版本
综合上述两个帖子: https://www.cnblogs.com/huadongw/p/6161145.html https://blog.csdn.net/u011587516/article/d ...
- Taro 3.1 beta 发布: 开放式架构新增 4 端支持
作者:凹凸曼-JJ 自 7 月初我们正式发布了 Taro 3,至今半年时间已然略去.期间我们不断地修复着问题,同时也在构想着下一个 minor 版本. 面对小程序平台越来越多的大环境,Taro 是选择 ...
- Flink读写Redis(一)-写入Redis
项目pom文件 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w ...
- 网站开发学习Python实现-Django项目部署-介绍(6.2.1)
@ 目录 1.第一步:找源码 2.第二步:在windows中更改代码 2.第三步:同步到linux中 3.第三步:部署 4.第四步:运行 关于作者 1.第一步:找源码 从github上找一个djang ...
- win10新版wsl2使用指南
本篇文章会介绍win10中wsl2的安装和使用以及遇到的常见问题比如如何固定wsl2地址等问题的总结. 一.wsl2简介 wsl是适用于 Linux 的 Windows 子系统,安装指南:适用于 Li ...
- 用Python实现童年小游戏贪吃蛇
贪吃蛇作为一款经典小游戏,早在 1976 年就面世了,我最早接触它还是在家长的诺基亚手机中.
- exp(cos(t)) - 2*cos(4.*t) + (sin(t./12)).^5;图形
clc; clear all; close all; t = linspace(0,24*pi,1000); r = exp(cos(t)) - 2*cos(4.*t) + (sin(t./12)). ...