第十五章 面向对象程序设计

一、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)明确指出它是从哪个基类继承而来。形式:冒号。后面紧跟以逗号分隔的基类列表,每个基类前面可以是publicprotectedprivate中的一个。
  • 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声明语句不能指定explicitconstexpr。如果基类的构造函数是explicitconstexpr,则继承的构造函数也拥有相同的属性。
    • 当一个基类构造函数含有默认实参时,这些实参并不会被继承。
    • 如果基类含有几个构造函数,则除了两个例外情况,大多数时候派生类会继承所有这些构造函数。
      • 第一个例外:派生类可以继承一部分构造函数,而为其他构造函数定义自己的版本。
      • 第二个例外:默认、拷贝和移动构造函数不会被继承。

八、容器和继承

  • 当使用容器存放继承体系中的对象时,通常必须采用间接存储的方式。
  • 当派生类对象被赋值给基类对象时,其中的派生类部分将被"切掉",因此容器和存在继承关系的类型无法兼容。
  • 希望在容器中存放具有继承关系的对象时,实际上存放的通常是基类的指针(更好的选择是智能指针)。
  • 对于C++面向对象的编程来说,一个悖论是我们无法直接使用对象进行面向对象编程。相反,必须使用指针和引用。因为指针会增加程序的复杂性,所以经常定义一些辅助的类来处理这些复杂的情况。

九、文本查询程序再探

【C++】《C++ Primer 》第十五章的更多相关文章

  1. C++Primer 第十五章

    //1.面向对象程序设计的核心思想是数据抽象,继承,动态绑定. // 通过使用数据抽象,我们可以将类的接口和实现分离 // 使用继承,可以定义相似的类型并对其相似关系建模 // 使用动态绑定,可以在一 ...

  2. C++ Primer Plus学习:第十五章

    第十五章 友元.异常和其他 友元 友元类 表 0-1 class Tv { public: friend class Remote; } Remote类可以使用Tv的数据成员,Remote类在Tv类后 ...

  3. 15第十五章UDF用户自定义函数(转载)

    15第十五章UDF用户自定义函数 待补上 原文链接 本文由豆约翰博客备份专家远程一键发布

  4. 《Linux命令行与shell脚本编程大全》 第十五章 学习笔记

    第十五章:控制脚本 处理信号 重温Linux信号 信号 名称 描述 1 HUP 挂起 2 INT 中断 3 QUIT 结束运行 9 KILL 无条件终止 11 SEGV 段错误 15 TERM 尽可能 ...

  5. CSS3秘笈复习:十三章&十四章&十五章&十六章&十七章

    第十三章 1.在使用浮动时,源代码的顺序非常重要.浮动元素的HTML必须处在要包围它的元素的HTML之前. 2.清楚浮动: (1).在外围div的底部添加一个清除元素:clear属性可以防止元素包围浮 ...

  6. Gradle 1.12用户指南翻译——第四十五章. 应用程序插件

    本文由CSDN博客貌似掉线翻译,其他章节的翻译请参见: http://blog.csdn.net/column/details/gradle-translation.html 翻译项目请关注Githu ...

  7. Gradle 1.12 翻译——第十五章. 任务详述

    有关其他已翻译的章节请关注Github上的项目:https://github.com/msdx/gradledoc/tree/1.12,或访问:http://gradledoc.qiniudn.com ...

  8. Gradle 1.12用户指南翻译——第二十五章. Scala 插件

    其他章节的翻译请参见: http://blog.csdn.net/column/details/gradle-translation.html 翻译项目请关注Github上的地址: https://g ...

  9. Gradle 1.12用户指南翻译——第三十五章. Sonar 插件

    本文由CSDN博客万一博主翻译,其他章节的翻译请参见: http://blog.csdn.net/column/details/gradle-translation.html 翻译项目请关注Githu ...

随机推荐

  1. Echarts入门教程精简实用系列

    引语:echarts.js是百度团队推出的一款用于图表可视化的插件,用于以图表的形式展现数据,功能强大,上手简单 1.从官方网站中下载所需的echarts.js文件,该文件因功能广泛,包体较大,可自行 ...

  2. Object not found! The requested URL was not found on this server.... 报错解决方案

    服务器(centos6.5) lnmp 报错如下 Object not found! The requested URL was not found on this server. The link ...

  3. MyBatis详细源码解析(上篇)

    前言 我会一步一步带你剖析MyBatis这个经典的半ORM框架的源码! 我是使用Spring Boot + MyBatis的方式进行测试,但并未进行整合,还是使用最原始的方式. 项目结构 导入依赖: ...

  4. vue seo 优化

    预渲染prerender-spa-plugin 如果你只是用来改善少数营销页面(例如 /, /about, /contact 等)的 SEO,那么你可能需要预渲染.无需使用 web 服务器实时动态编译 ...

  5. Exception in thread "main" java.lang.RuntimeException: java.lang.RuntimeException: Unable to instantiate org.apache.hadoop.hive.metastore.HiveMetaStoreClient

    hive安装时遇到的问题 解压后指定了hive-env.sh文件的Hadoop_home  & hive_conf 两个参数后,先直接bin/hive 用Derby数据库启动一下,然后再配置其 ...

  6. Flink读写Redis(三)-读取redis数据

    自定义flink的RedisSource,实现从redis中读取数据,这里借鉴了flink-connector-redis_2.11的实现逻辑,实现对redis读取的逻辑封装,flink-connec ...

  7. 精尽Spring MVC源码分析 - HandlerAdapter 组件(二)之 ServletInvocableHandlerMethod

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  8. 网站开发学习Python实现-Django项目部署-同步之前写的博客(6.2.2)

    @ 目录 1.说明 2.思路 3.代码 关于作者 1.说明 之前写的博客都在csdn和博客园中 要将博客同步到自己的博客网站中 因为都是使用markdown格式书写的,所以直接爬取上传就完事 2.思路 ...

  9. js--数组的filter()过滤方法的使用

    前言 你还在通过for循环遍历数组吗?你还在遍历之后一项一项的通过if判断过滤你需要的数据吗?你还在写着一大堆代码实现一个简单的过滤数据功能吗?那么,今天他来了.他就是这里要介绍的es6中数组filt ...

  10. 会Python了不起吗?是的,简直开挂

    前段时间听说了一件事,彻底刷新了我对"黑科技"的认知. 有一个小学弟,大学4年混得风生水起,恋爱.赚钱.写论文.找工作,样样都很顺利,简直是妥妥的人生赢家. 问他凭什么?张口就是: ...