面向对象程序设计

文章目录
  1. 1. OOP 概述
    1. 1.1. 继承
    2. 1.2. 动态绑定
  2. 2. 定义基类和派生类
    1. 2.1. 定义基类
      1. 2.1.1. 成员函数与继承
      2. 2.1.2. 访问控制
    2. 2.2. 定义派生类
      1. 2.2.1. 派生类构造函数
      2. 2.2.2. 派生类使用基类的成员
      3. 2.2.3. 继承和静态成员
      4. 2.2.4. 派生类的声明
      5. 2.2.5. 被用作基类的类
      6. 2.2.6. 防止继承
    3. 2.3. 类型转换和继承
      1. 2.3.1. 静态类型 动态类型
      2. 2.3.2. 不存在从基类向派生类的隐式类型转换
      3. 2.3.3. 在对象之间不存在类型转换
  3. 3. 虚函数
    1. 3.1. 派生类中的虚函数
    2. 3.2. final和override
    3. 3.3. 虚函数和默认实参
    4. 3.4. 回避虚函数机制
  4. 4. 抽象基类
    1. 4.1. 含有纯虚函数的类是抽象基类
    2. 4.2. 派生类构造函数只能初始化它的直接基类
  5. 5. 访问控制与继承
    1. 5.1. 共有、私有、受保护继承
    2. 5.2. 派生类向基类转化的可行性
    3. 5.3. 改变个别成员的可访问性
    4. 5.4. 默认的继承保护级别
  6. 6. 继承中类的作用域
    1. 6.1. 在编译时进行名字查找
    2. 6.2. 名字冲突和继承
    3. 6.3. 通过作用域运算符来使用隐藏的成员
    4. 6.4. 一如既往,名字查找优先于类型检查
    5. 6.5. 虚函数和作用域
  7. 7. 构造函数与拷贝控制
    1. 7.1. 虚析构函数
    2. 7.2. 合成拷贝控制与继承
    3. 7.3. 派生类的拷贝控制成员
    4. 7.4. 派生类的析构函数
    5. 7.5. 在构造函数和析构函数中
    6. 7.6. 继承的构造函数
  8. 8. 容器与继承
  1. OOP概述
  2. 定义基类和派生类
  3. 虚函数
  4. 抽象基类
  5. 访问控制和继承
  6. 继承中的类作用域
  7. 构造函数和拷贝控制
  8. 容器与继承
  9. 文本查询程序

面向对象程序设计的基本概念:

  1. 数据抽象
  2. 继承
  3. 动态绑定

OOP 概述

面向对象程序设计(object oriened programming)的核心思想是数据抽象、继承、动态绑定。通过使用数据抽象,我们可以将接口和实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略类型的区别,而以统一的方式使用它们的对象。

继承

基类负责定义层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。

基类将类型相关的函数声明为虚函数。

派生类通过使用派生列表明确指出它是从哪个基类继承而来的。每个基类前面可以有访问说明符。

动态绑定

函数的运行版本由实参决定,即在运行时选择函数的版本,所以动态绑定有事又称为运行时绑定(runtime binding)。

在C++语言中,当我们使用基类的指针或者引用调用一个虚函数时将发生动态绑定。

定义基类和派生类

定义基类

基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也该如此。

成员函数与继承

派生类可以继承其基类的成员,然而遇到与类型相关的操作时,派生类必须对其重新定义。换句话说:派生类需要对这些操作提供自己的新定义以覆盖(override)从基类继承而来的旧定义。

任何构造函数之外的非静态函数都可以是虚函数。关键字virtual只能出现字类内部声明语句之前,而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式的也是虚函数。

访问控制

基类希望它的派生类有权限访问该成员,同时禁止其他用户访问,我们用受保护的访问说明符来说明这样的成员。

定义派生类

派生类将其基类继承而来的成员函数中需要覆盖的那些重新声明。

在一个对象中,继承自基类的部分和派生类自定义的部分不一定是连续存储的。

因为在派生类对象中,含有基类部分,所以我们能把派生类的对象当成基类对象使用。而且我们也能将基类的指针和引用绑定到派生类对象中的基类部分上。这种转化通常称为派生类到基类的类型转换。

派生类构造函数

  1. 尽管在派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员,和其他创建了基类的代码一样,派生类也必须使用基类的构造函数来初始化它的基类部分。每个类控制它自己的成员初始化。
  2. 首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。

派生类使用基类的成员

  1. 派生类可以访问基类的公有成员和受保护成员
  2. 派生类的作用域嵌套在基类的作用域中。

继承和静态成员

  1. 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义,不论从基类中派生出多少个派生类,对于每个静态成员来说,都只存在唯一的实例。
  2. 静态成员遵循通用的访问控制,如果基类中的成员是private的,则派生类无权访问它,假设某静态成员是可访问的,则我们既能通过基类使用它,也能通过派生类使用它。

派生类的声明

  1. 派生类的声明不能包含派生列表

被用作基类的类

如果我们想将某个类用作基类,则该类必须已经定义,而非仅仅声明

防止继承

final

类型转换和继承

  1. 通常情况下,如果我们想把引用或指针绑定到一个对象上,则引用或指针的类型与对象的类型一样
  2. 我们可以将基类的指针或引用绑定到派生类的对象上。

静态类型 动态类型

  1. 静态类型:它是变量声明时的类型或表达式生产的类型,编译时是已知的
  2. 动态类型:变量或表达式内存中的对象类型,运行时才可知
  3. 如果表达式既不是指针,也不是引用,则它的动态类型和静态类型一样

不存在从基类向派生类的隐式类型转换

  1. 之所以存在从派生类向基类的类型转换,是因为每个派生类对象中包含一个基类部分
  2. 基类的对象可能是派生类也可能不是,所以不存在从基类向派生类的自动类型转换

在对象之间不存在类型转换

  1. 派生类向基类的自动类型转换只对指针和引用有效,对象之间不存在这样的转化。
  2. 因为构造函数不能是虚函数,当我们给基类的构造函数传递一个派生类对象时,实际运行的构造函数是基类定义的那个,显然,构造函数只能处理基类自己的成员,类似的,如果我们将一个派生类对象赋值给一个基类对象,则它实际运行的赋值运算符也是基类中定义的那个。该运算符同样只能处理基类自己的成员。

虚函数

当我们使用基类的引用或者指针调用一个虚函数时将发生动态绑定。我们必须为每一个虚函数提供定义,而不管它是否会用到,这是因为编译器也无法确定到底会使用哪个虚函数。

###对虚函数的调用可能在运行时才被解析

当某个虚函数通过指针或者引用调用时,编译器产生的代码知道运行时才确定应该调用那个版本的函数。被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那个。

  1. 成员函数如果没用被声明为虚函数,解析过程发生在编译时候(当且仅当使用指针调用或引用调用的时候)
  2. 成员函数声明为虚函数,根据调用指针在运行的时候确定调用的版本。

派生类中的虚函数

  1. 一旦某个函数声明成虚函数,则在所有派生类中都是虚函数
  2. 一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与它被覆盖的基类函数完全一致。
  3. 虚函数的返回类型是类本身的指针或引用,可以是本身类。

final和override

使用override来说明派生类的虚函数,这么做的好处:

  1. 使得程序员的意图更加明确
  2. 同时让编译器帮我们发现一些错误

虚函数和默认实参

如果某次函数调用使用了默认实参,则该实参由本次调用的静态类型决定。

回避虚函数机制

使用作用域运算符可以不对虚函数动态绑定。

抽象基类

  1. 我们在函数体(即在声明语句的分号之前)加上=0就可以将一个虚函数声明为纯虚函数,其中=0只能出现在类内部的虚函数声明处。
  2. 纯虚函数的目的是不希望用户创建类型的对象。
  3. 可以为纯虚函数提供定义,必须在类的外部

含有纯虚函数的类是抽象基类

抽象基类负责接口的定义,后续的其他类可以覆盖该接口,不能直接创建一个抽象基类的对象。

派生类构造函数只能初始化它的直接基类

1
2
3
4
Bulk_quote(const std::string& book,double price,std::size_t qty,double disc):
Disk_quote(book,price,qty,disc)
{
}

派生类显示的著名他使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表后面增加一个override关键字。

回避虚函数使用:在函数名前加作用域运算符。

访问控制与继承

  1. protected用来声明希望与派生类共享、但不被共有访问的成员。
  2. 派生类成员和友元只能通过派生类对象访问基类的受保护的成员。
  3. protected成员对于派生类和友元来说可以访问。
  4. protected对于类的用户来说不可访问。

共有、私有、受保护继承

某个类对其继承而来的成员的访问权限收到两个因素的影响:

  1. 基类中该成员的访问说明符
  2. 派生列表中派生访问说明符

  3. 派生访问说明符对于派生类的成员(及友元)能否访问直接基类的成员没什么影响,对基类成员的访问权限只与基类的访问说明符有关

  4. 派生访问说明符的目的是控制派生类用户(包括派生类的派生类)对于基类成员的访问权限

派生类向基类转化的可行性

假设D继承自B

  1. 只有D共有继承B时,用户代码才能使用派生类向基类转换,如果D继承B的方式是受保护的或私有的,则用户代码不能转化
  2. 不论以什么方式继承B,D的成员函数和友元都能使用派生类向基类转化,派生类向其直接基类的类型转换对于派生类的成员和友元来说,永远可以访问
  3. 如果D继承B的方式是共有、受保护继承,则D的派生类的成员或友元可以使用D向B的转化,反之,如果D继承B的方式是私有的,则不能。

对于代码的某个节点,如果基类的共有成员是可访问的,则派生类向基类的类型转换也是可访问的,反之不行。

###友元和继承

爸爸受保护的属性,儿子的小三(友元)想通过儿子窃取,不行(友元不能传递)

爸爸受保护的属性,爸爸的小三想通过儿子获取,可以(谁的小三就能获取到谁的保护属性,小三具有破坏性)(因为儿子有爸爸的基因)

不能继承友元关系,每个类负责控制各个成员的访问权限。

改变个别成员的可访问性

有时我们需要改变派生类继承的某个名字的访问权限,通过使用using声明。

可以将类的直接基类或间接基类找那个的任何可访问成员(例如非私有)标记出来,using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定。

派生类只能为那些它可以访问的名字提供using声明

默认的继承保护级别

  1. class的访问说明符、派生访问说明符默认都是private
  2. struct的访问说明符、派生访问说明符默认都是public的

两个角度理解访问控制:

  1. 普通用户
  2. 实现者

继承中类的作用域

派生类的作用域位于基类的作用域内,所以在派生类中,可以像使用自己成员一样,使用基类成员。

在编译时进行名字查找

一个对象、引用、指针的静态类型决定了该对象的那些成员是可见的。搜索名字从对象静态类型的类开始。

名字冲突和继承

和其他作用域一样,派生类也能重新定义在其直接基类或间接基类中的名字,此时,定义在内层作用域的名字将隐藏定义在外层作用(即基类)的名字

通过作用域运算符来使用隐藏的成员

假设饿哦们调用p->mem():

  1. 首先确定p的静态类型
  2. 从静态类型对应的类开始查找,知道继承的顶端,如果没有找到名字,则编译器报错
  3. 如果找到,就进行常规的类型检查
  4. 如果类型检查合法。
    1. 如果mem是虚函数且通过指针或引用调用,则编译器产生代码将在运行时确定到底运行那个版本,依据对象的动态类型
    2. 反之,编译器产生常规函数调用

一如既往,名字查找优先于类型检查

声明在内层作用域的函数并不会重载在外层作用域的函数,定义在派生类中的函数不会重载基类中的成员。和其他作用域一样,定义在派生类的成员和基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员,即使派生类成员和基类成员的形参列表不一致。基类成员任然会被隐藏。

虚函数和作用域

构造函数与拷贝控制

虚析构函数

析构函数定义成虚函数,确保删除指针能执行正确的版本。主要防止删除指向派生类的基类指针,删除错误。

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

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

派生类必须在其内部对所有重新定义的虚函数进行声明。派生类可以在这样的函数之前加上virtual关键字,但是并不是非得这么做。C++11新标准允许派生类显示的注明它将使用哪个成员函数改写基类的虚函数,具体的措施是在该函数的形参列表之后增加一个关键字override关键字。

合成拷贝控制与继承

大多数基类都会定义一个虚析构函数,因此在默认情况下,基类通常不含有合成的移动操作,而且在它的派生类中也没有合成的移动操作。

派生类的拷贝控制成员

在默认情况下,基类默认构造函数初始化派生类对象的基类部分,如果我们想拷贝或移动基类部分,则必须在派生类的构造函数初始值列表中显示的使用基类的拷贝或移动构造函数。

与拷贝和移动构造函数一样,派生类的赋值运算符也必须显示的为其基类部分赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
///拷贝构造函数
D(const D & d):Base(d),/*D成员的初始值*/
{
}
// 赋值运算符
D& D::operator=(const D &rhs)
{
Base::operator=(rhs);

///派生类
return *this;
}

///析构函数 Base::~Base被自动调用
~D();

派生类的析构函数

派生类的析构函数首先执行,然后是基类的析构函数。

在构造函数和析构函数中

继承的构造函数

派生类能够重用直接基类定义的构造函数,类不能继承默认、拷贝和移动构造函数,如果派生类没有直接定义这些构造函数,则编译器将为派生类合成它们。派生类继承基类构造函数的方式是提供一条注明了基类名的using声明语句

1
using Disc_quote::Disc_quote;

通常情况下,using声明语句令某个名字在当前作用域内可见,而当作用域构造函数时,using声明语句令编译器产生代码。对于基类的每个构造函数,编译器生成一个与之对应的派生类构造函数。一个构造函数的using声明不会改变构造函数的访问级别。

当一个基类构造函数含有默认时才能时,这些实参不会继承,相反,派生类将获得多个继承的构造函数。

容器与继承

当我们使用容器存放继承体系中的对象时,通常必须采用间接存储方式,因为不允许在容器中保存不同类型的元素。