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

一、概述

1、面向对象程序设计的核心思想:数据抽象、继承和动态绑定

  • 数据抽象:将类的接口与实现分离
  • 继承:可以定义相似的类型并对相似关系建模
  • 动态绑定:可以一定程度上忽略类似类型的区别,而以同一的方式使用它们的对象

2、当使用基类的引用(或指针)调用一个虚函数时将发生动态绑定

二、虚函数

1、基类希望它的派生类各自定义适合自己的版本,对基类函数进行覆盖,此时基类应该将这些函数声明成虚函数

  • 在函数前加virtual

2、派生类必须在其内部对所有重定义的虚函数进行声明

  • 显式说明将重写基类的虚函数,在最后加override
  • 如果不希望基类(函数)被继承,在最后加final

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

4、任何构造函数外的非静态函数都可以是虚函数;

  • 如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也为虚函数
  • 成员函数如果没有声明成虚函数,则解析过程发生在编译时而非运行时,也就无法发生动态绑定
  • 如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本

5、所有虚函数必须有定义、不管是否被用到:被虚表实现

  • 如果基类不想要被定义,可以使用 = 0;来实现纯虚函数
  • 含有(或未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类负责定义接口,而后续的其他类可以覆盖该接口。不能直接创建一个抽象基类的对象

6、基类中的虚函数在派生类中隐含地也是一个虚函数。当派生类覆盖了某个虚函数,该函数在基类中的形参必须与派生类中的形参严格匹配

  • 一旦某个函数被声明成虚函数,则在所有派生类中都是虚函数
  • 一个派生类如果覆盖了某个函继承来的虚函数,则它的形参必须与被它覆盖的基类函数完全一致
  • 如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致
  • 通常只有成员函数(友元)中的代码才需要使用作用域运算符来回避虚函数的机制
    • 当一个派生类的虚函数调用它覆盖的基类的虚函数版本时需要回避虚函数

三、定义基类和派生类

1、派生类必须通过类派生列表明确指出它从哪个(哪些)基类继承而来

2、每个基类前可以有三种访问说明符:public、protected、private

  • protected:派生类可以访问,其他用户禁止访问
  • 访问说明符的作用是控制派生类从基类继承来的成员是否对派生类的用户可见
  • 受保护的成员对于派生类的成员和友元来说是可访问的
  • 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权
  • 派生访问说明符对于派生类的成员(友元)能否访问其直接基类的成员没有什么影响。对于基类成员的访问权限置于基类中的访问说明符
    • 派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限
    • 对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的;反之则不行
    • 不能继承友元关系;每个类负责控制各自成员的访问权限

3、派生类必须使用基类中的构造函数来初始化它的基类部分

  • 每个类控制它自己的成员初始化过程
  • 除非我们特别指出,否则派生类对象的基类部分会像数据成员一样执行默认初始化
  • 首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员

4、如果基类定义了一个静态成员,则整个继承体系中只存在成员的唯一定义,不论基类中派生出多少个派生类

5、派生类的声明包含类名但不包含它的派生列表

  • 派生列表以及定义有关的细节必须与类的主体一起出现

6、如果想要将某个类用作基类,这个类必须已经定义而非仅仅声明

  • 一个类不能派生它本身
  • 最终的派生类将包含它的直接基类的子对象以及每个间接基类的子对象

7、静态类型和动态类型

  • 表达式的静态类型在编译时总是已知的,它是变量声明时或表达式生成的类型
  • 动态类型则是变量或表达式表示的内存中的对象的类型,直到运行时才知道
  • 如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致
  • 不存在基类向派生类的隐式类型转换
  • 当我们用一个派生类对象为一个基类对象初始化或赋值时,只有派生类对象中的基类部分会被拷贝、移动或赋值、其他的派生类部分被忽略掉
  • 一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致(动态绑定),但是我们能使用哪些成员仍然是有静态类型决定的

四、继承中的类作用域

1、派生类的作用域嵌套在基类的作用域中

  • 先通过找派生类中的对象,如果没有找到则再在基类中找

2、名字查找先于类型检查

  • 定义派生类中的函数不会重载其基类中的成员
  • 只要派生类的成员和基类的某个成员同名,即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉,而不是覆盖
    • 所以基类和派生类中的虚函数必须有相同的形参列表,如果不一样则无法覆盖原来的函数,就无法动态绑定

3、成员函数无论是否是虚函数都能被重载。如果派生类希望所有的重载版本对于它来说都是可见的,那么它就需要覆盖所有版本或者一个都不覆盖

  • 一条基类成员函数的using声明可以把该函数的所有重载实例添加到派生类的作用域中,此时只需要定义其特有的函数就可以了
    • using声明语句改变了成员的可访问性
    • using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定

五、构造函数与拷贝控制

1、基类总是需要析构函数,而且如果派生类要动态绑定来调用派生类的析构函数,则基类需要定义虚析构函数

  • 虚析构函数将阻止合成移动操作

2、实际编程中,如果基类没有默认、拷贝或移动构造函数,则一般派生类也不会定义相应的操作

3、因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当我们确定需要执行移动操作时,应该首先在基类中进行定义

4、派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内整个对象

  • 如果我们想拷贝或移动基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝或构造函数
  • 派生类析构函数只负责销毁有派生类自己分配的资源,然后会执行基类的析构函数

5、using声明语句只是令某个名字在当前作用域可见

  • 当作用域构造函数时,using语句将令编译器产生代码,对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数
  • 和普通成员的using声明不同,一个构造函数的using声明不会改变该构造函数的访问级别:基类是什么级别,派生类还是什么级别
  • 一个using声明语句不能是explicit或constexpr的
  • 一个基类构造函数含有默认实参时,这些实参并不会被继承;相反,派生类会获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参

6、如果基类含有几个构造函数,除了两个例外,大多数情况派生类会继承所有这些构造函数

  • 第一个例外:派生类可以继承一部分构造函数,而为其他构造函数定义自己的版本
  • 第二个例外:默认、拷贝和移动构造函数不会被继承

六、容器与继承

1、当派生类对象被赋值给基类对象时,其中的派生类部分被“切掉”,因此容器和存在继承关系的类型无法兼容

  • 当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类的指针/智能指针
  • 可以把一个派生类的(智能)指针转换为基类的(智能)指针,
posted @ 2015-10-22 21:01  dylqt  阅读(158)  评论(0编辑  收藏  举报