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

转载自https://github.com/applenob/Cpp_Primer_Practice,看C++primer的时用的笔记。自己做了一些补充,感谢前人的总结

成都创新互联公司于2013年开始,是专业互联网技术服务公司,拥有项目成都网站制作、成都网站设计、外贸营销网站建设网站策划,项目实施与项目整合能力。我们以让每一个梦想脱颖而出为使命,1280元宣州做网站,已为上家服务,为宣州各地企业和个人服务,联系电话:13518219792

OOP:概述

  • 面向对象程序设计(object-oriented programming)的核心思想是数据抽象、继承和动态绑定。
  • 继承(inheritance):
    • 通过继承联系在一起的类构成一种层次关系。
    • 通常在层次关系的根部有一个基类(base class)。
    • 其他类直接或者简介从基类继承而来,这些继承得到的类成为派生类(derived class)。
    • 基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
    • 对于某些函数,基类希望它的派生类个自定义适合自己的版本,此时基类就将这些函数声明成虚函数(virtual function)。
    • 派生类必须通过使用类派生列表(class derivation list)明确指出它是从哪个基类继承而来。形式:一个冒号,后面紧跟以逗号分隔的基类列表,每个基类前都可以有访问说明符。class Bulk_quote : public Quote{};
    • 派生类必须在其内部对所有重新定义的虚函数进行声明。可以在函数之前加上virtual关键字,也可以不加。C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即在函数的形参列表之后加一个override关键字。
  • 动态绑定(dynamic binding,又称运行时绑定):
    • 使用同一段代码可以分别处理基类和派生类的对象。
    • 函数的运行版本由实参决定,即在运行时选择函数的版本。

定义基类和派生类

定义基类

  • 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
  • 基类通过在其成员函数的声明语句前加上关键字virtual使得该函数执行动态绑定
  • 如果成员函数没有被声明为虚函数,则解析过程发生在编译时而非运行时。
  • 访问控制:
    • protected : 基类和和其派生类还有友元可以访问。
    • private : 只有基类本身和友元可以访问。

定义派生类

  • 派生类必须通过类派生列表(class derivation list)明确指出它是从哪个基类继承而来。形式:冒号,后面紧跟以逗号分隔的基类列表,每个基类前面可以有一下三种访问说明符的一个:publicprotectedprivate
  • C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即在函数的形参列表之后加一个override关键字。
  • 派生类构造函数:派生类必须使用基类的构造函数去初始化它的基类部分。
  • 静态成员:如果基类定义了一个基类成员,则在整个继承体系中只存在该成员的唯一定义。
  • 派生类的声明:声明中不包含它的派生列表。
  • C++11新标准提供了一种防止继承的方法,在类名后面跟一个关键字final

类型转换与继承

  • 理解基类和派生类之间的类型抓换是理解C++语言面向对象编程的关键所在。
  • 可以将基类的指针或引用绑定到派生类对象上。

静态类型与动态类型:如果基类在实参中是引用或者指针的形式,那么就可以动态类型转换,即基类可以被派生类表示。但如果是实参既不是引用也不是指针,则它的静态类型和动态类型是一致的

  • 不存在从基类向派生类的隐式类型转换。
  • 派生类向基类的自动类型转换只对指针或引用类型有效,对象之间不存在类型转换
  • 如果将一个派生类对象赋值或拷贝给基类对象,该操作只会运行基类成员的拷贝构造函数或者赋值函数,而派生类中的数据会被切掉(sliced down)/当用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或者赋值,它的派生类部分将会被忽略掉

存在继承关系的类型之间的转换规则

  • 从派生类向基类的类型转换只对指针或引用类型有效
  • 基类向派生类不存在隐式类型转换
  • 和任何其他成员一样,派生类想基类的类型转换也可能会由于访问受限而变得不可行

虚函数

  • 使用虚函数可以执行动态绑定。

  • OOP的核心思想是多态性(polymorphism)。

    • 引用或指针的静态类型和动态类型不同这一事实正是c++支持多态的根本所在
  • 当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。

  • 派生类必须在其内部对所有重新定义的虚函数进行声明。可以在函数之前加上virtual关键字,也可以不加。

  • C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即在函数的形参列表之后加一个override关键字。

  • 如果我们想覆盖某个虚函数,但不小心把形参列表弄错了,这个时候就不会覆盖基类中的虚函数。加上override可以明确程序员的意图,让编译器帮忙确认参数列表是否出错。

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

  • 通常,只有成员函数(或友元)中的代码才需要使用作用域运算符::)来回避虚函数的机制。

抽象基类

  • 纯虚函数(pure virtual):清晰地告诉用户当前的函数是没有实际意义的。纯虚函数无需定义,只用在函数体的位置前书写=0就可以将一个虚函数说明为纯虚函数。
  • 含有纯虚函数的类是抽象基类(abstract base class)。不能创建抽象基类的对象。
  • 派生类构造函数只初始化他的直接基类,也就是说派生类如果没有自己的数据成员也要提供一个接受基类中参数的构造函数。

访问控制与继承

  • 受保护的成员:
    • protected说明符可以看做是publicprivate中的产物。
    • 类似于私有成员,受保护的成员对类的用户来说是不可访问的。
    • 类似于公有成员,受保护的成员对于派生类的成员和友元来说是可访问的。
    • 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
  • 派生访问说明符:
    • 对于派生类的成员(及友元)能否访问其直接基类的成员没什么影响。
    • 派生访问说明符的目的是:控制派生类用户对于基类成员的访问权限(即类的使用者的权限,如果是private,则不能通过派生类直接调用基类中的数据成员)。比如struct Priv_Drev: private Base{}意味着在派生类Priv_Drev中,从Base继承而来的部分都是private的。
  • 友元关系不能继承。
  • 改变个别成员的可访问性:使用using
  • 默认情况下,使用class关键字定义的派生类是私有继承的;使用struct关键字定义的派生类是公有继承的。
    • 15.18:派生类公有的继承基类的时候,用户代码才能使用派生类向基类转换

继承中的类作用域

  • 每个类定义自己的作用域,在这个作用域内我们定义类的成员。当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。
  • 派生类的成员将隐藏同名的基类成员。
  • 除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。

名字查找与继承

假设调用p->mem()或者obj.mem(),需要依次调用以下四个步骤:

  • 首先确定p的静态类型。因为我们调用的是一个成员,所以该类型必然是类类型。
  • 在p的静态类型对应的类中查找mem。如果找不到,则依次在直接基类中不断查找直至到达继承链的顶端。如果找遍了该类及其基类仍然找不到,则编译器将报错。
  • 一旦找到了mem,就进行常规的类型检查已确认对于当前找到的mem,本次调用是否合法。
  • 假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码:
    • 如果mem是虚函数且我们是通过引用或指针进行的调用,则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本,依据是对象的动态类型
    • 反之,如果mem不是虚函数或者我们是通过对象(而非指针或引用)进行的调用,则编译器将产生一个常规函数调用。

构造函数与拷贝控制

虚析构函数

  • 基类通常应该定义一个虚析构函数,这样我们就能动态分配继承体系中的对象了。
  • 如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。
  • 虚析构函数将阻止合成移动操作。

合成拷贝控制与继承

  • 基类或派生类的合成拷贝控制成员的行为和其他合成的构造函数、赋值运算符或析构函数类似:他们对类本身的成员依次进行初始化、赋值或销毁的操作。
  • 如果基类中的默认构造函数,拷贝构造函数、拷贝赋值函数或者析构函数是被删除的函数或者不可访问,则派生类中对应的成员将是被删除的
  • 如果在基类中有一个不可访问或者删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类对象的基类部分
  • 如果派生类确实需要移动操作,需要在基类中定义移动操作。只要有移动操作就必须有显式的拷贝操作

派生类的拷贝控制成员

  • 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。
  • 派生类析构函数:派生类析构函数先执行,然后执行基类的析构函数。

继承的构造函数

  • C++11新标准中,派生类可以重用其直接基类定义的构造函数。
  • using Disc_quote::Disc_quote;,注明了要继承Disc_quote的构造函数。

容器与继承

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

文本查询程序再探

  • 使系统支持:单词查询、逻辑非查询、逻辑或查询、逻辑与查询。

面向对象的解决方案

  • 将几种不同的查询建模成相互独立的类,这些类共享一个公共基类:
    • WordQuery
    • NotQuery
    • OrQuery
    • AndQuery
  • 这些类包含两个操作:
    • eval:接受一个TextQuery对象并返回一个QueryResult
    • rep:返回基础查询的string表示形式。
  • 继承和组合:
    • 当我们令一个类公有地继承另一个类时,派生类应当反映与基类的“是一种(Is A)”的关系。
    • 类型之间另一种常见的关系是“有一个(Has A)”的关系。
  • 对于面向对象编程的新手来说,想要理解一个程序,最困难的部分往往是理解程序的设计思路。一旦掌握了设计思路,接下来的实现也就水到渠成了。

Query程序设计:

操作 解释
Query程序接口类和操作
TextQuery 该类读入给定的文件并构建一个查找图。包含一个query操作,它接受一个string实参,返回一个QueryResult对象;该QueryResult对象表示string出现的行。
QueryResult 该类保存一个query操作的结果。
Query 是一个接口类,指向Query_base派生类的对象。
Query q(s) Query对象q绑定到一个存放着string s的新WordQuery对象上。
q1 & q2 返回一个Query对象,该Query绑定到一个存放q1q2的新AndQuery对象上。
q1 | q2 返回一个Query对象,该Query绑定到一个存放q1q2的新OrQuery对象上。
~q 返回一个Query对象,该Query绑定到一个存放q的新NotQuery对象上。
Query程序实现类
Query_base 查询类的抽象基类
WordQuery Query_base的派生类,用于查找一个给定的单词
NotQuery Query_base的派生类,用于查找一个给定的单词
BinaryQuery Query_base的派生类,查询结果是Query运算对象没有出现的行的集合
OrQuery Query_base的派生类,返回它的两个运算对象分别出现的行的并集
AndQuery Query_base的派生类,返回它的两个运算对象分别出现的行的交集

网站名称:第十五章 面向对象程序设计
浏览地址:http://pwwzsj.com/article/dsoipig.html