第15章_面向对象程序设计

OOP: 概念

  • 面向对象程序设计的核心思想是数据抽象、继承和动态绑定
  • 在 C++ 语言中,使用基类的引用(或指针)调用一个虚函数时将发生动态绑定

定义基类和派生类

定义基类

  • 基类通常都应该定义各虚析构函数,即使该函数不执行任何实际操作也是如此
  • 成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时

定义派生类

  • 派生类经常(但不总是)覆盖它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本
  • 在一个对象中,继承自基类的部分和派生类自定义的部分不一定是连续存储的
  • 每个类控制它自己的成员初始化过程,派生类也必须使用基类的构造函数来初始化它的基类部分
  • 除非特别指出,否则派生类对象的基类部分就会像数据成员一样执行默认初始化。如果想使用其他的基类构造函数,需要以类名加圆括号内的实参列表的形式为构造函数提供初始值
  • 首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员
  • 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义
  • 静态成员遵循通用的访问控制规则,如果基类中的成员是 private 的,则派生类无权访问它。假设静态成员是可访问的,则既能通过基类使用它也能通过派生类使用它

类型转换与继承

  • 如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致
  • 如果已知某个基类向派生类的转换是安全的,则可以使用 static_const 来强制覆盖掉编译器的检查工作。否则,使用 dynamic_cast

虚函数

  • 当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数
  • 当通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来
  • 如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致

抽象基类

  • 含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类
  • 不能创建抽象基类的对象
  • 派生类构造函数只初始化它的直接基类

访问控制与继承

  • protected 关键字来声明那些它希望与派生类分享但是不想被其他公共访问使用的成员
  • 就像友元关系不能传递一样,友元关系同样也不能继承。基类的友元在访问派生类成员时不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员
  • 有时需要改变派生类的某个名字的访问级别,通过使用 using 声明

继承中的类作用域

  • 派生类的作用域位于基类作用域之内
  • 和其他作用域一样吗,派生类也能重用定义在其直接基类或间接基类中的名字,此时定义在内层作用域(即派生类)的名字将隐藏定义在外层作用域(即基类)的名字
  • 可以使用作用域运算符来使用一个被隐藏的基类成员
  • 名字查找优先与类型检查。
  • 声明在内层作用域的函数并不会重载声明在外层作用域的函数,因此,定义派生类中的函数也不会重载其基类中的成员。如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉
  • 如果派生类希望所有的重载版本对于它来说都是可见的,那么它就需要覆盖所有的版本,或者一个也不覆盖。有时一个类仅需覆盖重载集合中的一些而非全部函数,此时,如果我们不得不覆盖基类中的每一个版本的话,显然操作将极其烦琐。一种好的解决方案是为重载的成员提供一条 using 声明语句,这样就无需覆盖基类中的每一个重载版本类。

构造函数与拷贝控制

虚析构函数

  • 如果基类的析构函数不是虚函数,则 delete 一个指向派生类对象的基类指针将产生未定义的行为。

合成拷贝控制与继承

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

派生类的拷贝控制成员

如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本

继承的构造函数

容器与继承

  • 当派生类对象被赋值给基类对象时,其中的派生类部分将被切掉,因此容器和存在基础关系的类型无法兼容
  • 当希望在容器中存放具有基础关系的对象时,实际上存放的通常是基类的指针。