C++Primer学习笔记(十一)面向对象编程

面向对象编程基于三个基本概念:数据抽象、继承和动态绑定
在 C++ 中,用类进行数据抽象,用派生类从一个类继承另一个:派生类继承基类的成员。动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数。

继承和动态绑定在两个方面简化了我们的程序:能够容易地定义与其他类相似但又不相同的新类,能够更容易地编写忽略这些相似类型之间区别的程序。

15.1. 面向对象编程:概述

面向对象编程的关键思想是多态性(polymorphism)。意思是“许多形态”。在 C++ 中,多态性仅用于通过继承而相关联的类型的引用或指针。

继承

派生类(derived class)能够继承基类(base class)定义的成员,派生类可以无须改变而使用那些与派生类型具体特性不相关的操作,派生类可以重定义那些与派生类型相关的成员函数。

动态绑定

动态绑定我们能够编写程序使用继承层次中任意类型的对象,无须关心对象的具体类型。使用这些类的程序无须区分函数是在基类还是在派生类中定义的

在 C++ 中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的

15.2. 定义基类和派生类

定义基类

构造函数使用默认实参,允许用 0个、1 个或两个实参进行调用,它用这些实参初始化数据成员。

调用包含默认实参的函数时,可以为该形参提供实参,也可以不提供。如果提供了实参,则它将覆盖默认的实参值;否则,函数将使用默认实参值。

基类成员函数

Item_base 类定义了两个函数,其中一个前面带有保留字virtual。保留字 virtual 的目的是启用动态绑定,指明函数为虚函数。成员默认为非虚函数,对非虚函数的调用在编译时确定。

除了构造函数之外,任意非 static成员函数都可以是虚函数。保留字只在类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上。

基类通常应将派生类需要重定义的任意函数定义为虚函数。

访问控制和继承

在基类中,public 和 private 标号具有普通含义:用户代码可以访问类的public 成员而不能访问 private 成员,private 成员只能由基类的成员和友元访问。派生类对基类的 public 和 private 成员的访问权限与程序中任意其他部分一样:它可以访问 public 成员而不能访问 private 成员。

有时作为基类的类具有一些成员,它希望允许派生类访问但仍禁止其他用户访问这些成员。对于这样的成员应使用受保护的访问标号。protected 成员可以被派生类对象访问但不能被该类型的普通用户访问。

有时作为基类的类具有一些成员,它希望允许派生类访问但仍禁止其他用户
访问这些成员。对于这样的成员应使用受保护的访问标号。protected 成员可以
被派生类对象访问但不能被该类型的普通用户访问。

protected 成员

• 像 private 成员一样,protected 成员不能被类的用户访问。

• 像 public 成员一样,protected 成员可被该类的派生类访问。

• 派生类只能通过派生类对象访问其基类的 protected 成员,派生类对其基类类型对象的 protected 成员没有特殊访问权限。

关键概念:类设计与受保护成员

如果没有继承,类只有两种用户:类本身的成员和该类的用户。将类划分为 private 和 public 访问级别反映了用户种类的这一分隔:用户只能访问 public 接口,类成员和友元既可以访问 public 成员也可以访问 private 成员。

有了继承,就有了类的第三种用户:从类派生定义新类的程序员。派生类的提供者通常(但并不总是)需要访问(一般为 private 的)基类实现,为了允许这种访问而仍然禁止对实现的一般访问,提供了附加的protected 访问标号。类的 protected 部分仍然不能被一般程序访问,但可以被派生类访问。只有类本身和友元可以访问基类的 private 部分,派生类不能访问基类的 private 成员。

接口函数应该为 public 而数据一般不应为 public。
希望禁止派生类访问的成员应该设为 private,提供派生类实现所需操作或数据的成员应设为 protected。
换句话说,提供给派生类型的接口是 protected 成员和 public成员的组合。

派生类

为了定义派生类,使用类派生列表指定基类。类派生列表指定了一个或多个基类,具有如下形式:
    class classname: access-label base-class

这里 access-label 是 public、protected 或 private,base-class 是已定义的类的名字。

定义派生类

派生类和虚函数

派生类一般会重定义所继承的虚函数。派生类没有重定义某个虚函数,则使用基类中定义的版本。
派生类中虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。
例如,Item_base 类可以定义返回 Item_base* 的虚函数,如果这样,Bulk_item 类中定义的实例可以定义为返回 Item_base* 或Bulk_item*。

派生类对象包含基类对象作为子对象

派生类本身定义的(非 static)成员加上由基类(非 static)成员组成的子对象。

派生类中的函数可以使用基类的成员

因为每个派生类对象都有基类部分,类可以访问共基类的public 和 protected 成员,就好像那些成员是派生类自己的成员一样。

用作基类的类必须是已定义的

这一限制的原因应该很容易明白:每个派生类包含并且可以访问其基类的成员,为了使用这些成员,派生类必须知道它们是什么。这一规则暗示着不可能从类自身派生出一个类

派生类的声明

如果需要声明(但并不实现)一个派生类,则声明包含类名但不包含派生列表。例如,下面的前向声明会导致编译时错误:
  // error: a forward declaration must not include the derivation list
  class Bulk_item : public Item_base;

正确的前向声明为:
  // forward declarations of both derived and nonderived class
  class Bulk_item;
  class Item_base;

virtual 与其他成员函数

虚函数定义

虚函数原理

C++ 中的函数调用默认不使用动态绑定。要触发动态绑定,满足两个条件:

第一,只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;第二,必须通过基类类型的引用或指针进行函数调用。

要理解这一要求,需要理解在使用继承层次中某一类型的对象的引用或指针时会发生什么。

从派生类型到基类的转换

因为每个派生类对象都包含基类部分,所以可将基类类型的引用绑定到派生类对象的基类部分,也可以用指向基类的指针指向派生类对象

基类类型引用和指针的关键点在于静态类型(在编译时可知的引用类型或指针类型)和动态类型(指针或引用所绑定的对象的类型这是仅在运行时可知的)可能不同。

可以在运行时确定 virtual 函数的调用

将基类类型的引用或指针绑定到派生类对象对基类对象没有影响,对象本身不会改变,仍为派生类对象。对象的实际类型可能不同于该对象引用或指针的静态类型,这是 C++ 中动态绑定的关键。

关键概念:C++ 中的多态性

引用和指针的静态类型与动态类型可以不同,这是 C++ 用以支持多态性的基石。

只有通过引用或指针调用,虚函数才在运行时确定。只有在这些情况下,直到运行时才知道对象的动态类型。

在编译时确定非 virtual 调用

非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定。

覆盖虚函数机制

在某些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,这里可以使用作用域操作符:
  Item_base *baseP = &derived;
  double d = baseP->Item_base::net_price(42);

这段代码强制将 net_price 调用确定为 Item_base 中定义的版本,该调用将在编译时确定。

只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。

为什么会希望覆盖虚函数机制?最常见的理由是为了派生类虚函数调用基类中的版本。

派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归。

虚函数与默认实参

公用、私有和受保护的继承

派生类不能访问基类的 private 成员,也不能使自己的用户能够访问那些成员。如果基类成员为 public 或protected,则派生列表中使用的访问标号决定该成员在派生类中的访问级别

• 如果是公用继承,基类成员保持自己的访问级别:基类的 public 成员为派生类的 public 成员,基类的 protected 成员为派生类的 protected成员。
• 如果是受保护继承,基类的 public 和 protected 成员在派生类中为protected 成员。
• 如果是私有继承,基类的的所有成员在派生类中为 private 成员。

接口继承与实现继承

public 派生类继承基类的接口,它具有与基类相同的接口。

使用 private 或 protected 派生的类不继承基类的接口,相反,这些派生通常被称为实现继承。派生类在实现中使用被继承但继承基类的部分并未成为其接口的一部分。

关键概念:继承与组合

定义一个类作为另一个类的公用派生类时,派生类应反映与基类的“是一种(Is A)”关系。

类型之间另一种常见的关系是称为“有一个(Has A)”的关系。

去除个别成员

如果进行 private 或 protected 继承,则基类成员的访问级别在派生类中比在基类中更受限

派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更严格或更宽松。

class Derived : private Base {
public:
  using Base::size;
protected:
  using Base::n;
  // ...
};

上述的派生类Derived 可以是私有继承Base的情况下,通过一个using 声明来使用基类Base中的成员。

默认继承保护级别

使用 class 保留字定义的派生默认具有 private 继承,而用 struct 保留字定义的类默认具有 public 继承:
  class Base { /* ... */ };
  struct D1 : Base { /* ... */ }; // public inheritance by default
  class D2 : Base { /* ... */ }; // private inheritance by default

有一种常见的误解认为用 struct 保留字定义的类与用 class 定义的类有更大的区别。唯一的不同只是默认的成员保护级别和默认的派生保护级别,没有其他区别

显式指定可清楚指出想要私有继承而不是一时疏忽。

友元关系与继承

友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。

继承与静态成员

如果基类定义 static 成员,则整个继承层次中只有一个这样的成员。
static 成员遵循常规访问控制:如果成员在基类中为private,则派生类不能访问它。

15.3. 转换与继承

理解基类类型和派生类型之间的转换,对于理解面向对象编程在 C++ 中如何工作非常关键。

我们已经看到,每个派生类对象包含一个基类部分,这意味着可以像使用基类对象一样在派生类对象上执行操作。因为派生类对象也是基类对象,所以存在从派生类型引用到基类类型引用的自动转换,即,可以将派生类对象的引用转换为基类子对象的引用,对指针也类似。

派生类到基类的转换

如果有一个派生类型的对象,则可以使用它的地址对基类类型的指针进行赋值或初始化。同样,可以使用派生类型的引用或对象初始化基类类型的引用。

引用转换不同于转换对象

将对象传给希望接受引用的函数时,引用直接绑定到该对象,虽然看起来在传递对象,实际上实参是该对象的引用,对象本身未被复制,并且,转换不会在任何方面改变派生类型对象,该对象仍是派生类型对象。

将派生类对象传给希望接受基类类型对象(而不是引用)的函数时,情况完全不同。在这种情况下,形参的类型是固定的——在编译时和运行时形参都是基类类型对象。如果用派生类型对象调用这样的函数,则该派生类对象的基类部分被复制到形参

用派生类对象对基类对象进行初始化或赋值

对基类对象进行初始化或赋值,实际上是在调用函数:初始化时调用构造函数,赋值时调用赋值操作符。

Item_base item;
Bulk_item bulk; 
Item_base item(bulk); 
item = bulk;

Bulk_item 类型的对象调用 Item_base 类的复制构造函数或赋值操作符时,将发生下列步骤:

将 Bulk_item 对象转换为 Item_base 引用,这仅仅意味着将一个Item_base 引用绑定到 Bulk_item 对象。
• 将该引用作为实参传给复制构造函数或赋值操作符。
• 那些操作符使用 Bulk_item 的 Item_base 部分分别对调用构造函数或赋值的 Item_base 对象的成员进行初始化或赋值。
• 一旦操作符执行完毕,对象即为 Item_base。它包含 Bulk_item 的Item_base 部分的副本,但实参的 Bulk_item 部分被忽略。

派生类到基类转换的可访问性

转换是否访问取决于在派生类的派生列表中指定的访问标号。

要确定到基类的转换是否可访问,可以考虑基类的 public成员是否访问,如果可以,转换是可访问的,否则,转换是不可访问的。

基类到派生类的转换

没有从基类类型到派生类型的(自动)转换,原因在于基类对象只能是基类对象,它不能包含派生类型成员。

如果知道从基类到派生类的转换是安全的,就可以使用static_cast强制编译器进行转换。或者,可以用 dynamic_cast申请在运行时进行检查

15.4. 构造函数和复制控制

每个派生类对象由派生类中定义的(非 static)成员加上一个或多个基类子对象构成,这一事实影响着派生类型对象时,也会构造、复制、赋值和撤销这些基类子对象。

构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。

基类构造函数和复制控制

像任意其他成员一样,构造函数可以为 protected 或 private,某些类需要只希望派生类使用的特殊构造函数,这样的构造函数应定义为protected。

派生类构造函数

派生类的构造函数受继承关系的影响,每个派生类构造函数除了初始化自己的数据成员之外,还要初始化基类

合成的派生类默认构造函数

派生类的合成默认构造函数除了初始化派生类的数据成员之外,它还初始化派生类对象的基类部分。基类部分由基类的默认构造函数初始化

定义默认构造函数

因为 Bulk_item 具有内置类型成员,所以应定义自己的默认构造函数(全为类类型时,可以隐式调用他们各自的默认构造函数):
class Bulk_item : public Item_base {
public:
  Bulk_item(): min_qty(0), discount(0.0) { }
  // as before
};

首先使用 Item_base 的默认构造函数初始化Item_base 部分,那个构造函数将 isbn 置为空串并将 price 置为 0。Item_base 的构造函数执行完毕后,再初始化 Bulk_item 部分的成员并执行构造函数的函数体(函数体为空)。

向基类构造函数传递实参

派生类构造函数的初始化列表只能初始化派生类的成员,不能直接初始化继承成员。相反派生类构造函数通过将基类包含在构造函数初始化列表中来间接初始化继承成员。

构造函数初始化列表为类的基类和成员提供初始值,它并不指定初始化的执行次序。首先初始化基类,然后根据声明次序初始化派生类的成员。

在派生类构造函数中使用默认实参

只能初始化直接基类

一个类只能初始化自己的直接基类。直接就是在派生列表中指定的类。

关键概念:重构

将 Disc_item 加到 Item_base 层次是重构(refactoring)的一个例子。
重构包括重新定义类层次,将操作和/或数据从一个类移到另一个类。
为了适应应用程序的需要而重新设计类以便增加新函数或处理其他改变时,最有可能需要进行重构。
重构常见在面向对象应用程序中非常常见。值得注意的是,虽然改变了继承层次,使用 Bulk_item 类或 Item_base 类的代码不需要改变。

关键概念:尊重基类接口

构造函数只能初始化其直接基类的原因是每个类都定义了自己的接口。

复制控制和继承

只包含类类型或内置类型数据成员、不含指针的类一般可以使用合成操作,复制、赋值或撤销这样的成员不需要特殊控制。具有指针成员的类一般需要定义自己的复制控制来管理这些成员。

定义派生类复制构造函数

如果派生类显式定义自己的复制构造函数或赋值操作符,则该定义将完全覆盖默认定义。被继承类的复制构造函数和赋值操作符负责对基类成分以及类自己的成员进行复制或赋值。

如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分:
class Base { /* ... */ };
  class Derived: public Base {
  public:
    Derived(const Derived& d):
      Base(d) /* other member initialization */ { /*... */ }
};
初始化函数 Base(d) 将派生类对象 d 转换为它的基类部分的引用,并调用基类复制构造函数。

若不显示指定基类构造函数,派生类中的基类部分会使用基类的默认构造函数,而不会使用实参中的基类部分.如下:

Derived(const Derived& d) /* derived member initizations */
              {/* ... */ }

派生类赋值操作符

赋值操作符必须防止自身赋值。

派生类析构函数

析构函数的工作与复制构造函数和赋值操作符不同:派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员:
class Derived: public Base {
  public:
    ~Derived() { /* do what it takes to clean up derived members*/ }
};
对象的撤销顺序与构造顺序相反:首先运行派生析构函数,然后按继承层次依次向上调用各基类析构函数。

虚析构函数

处理继承层次中的对象时,指针的静态类型可能与被删除对象的动态类型不同,可能会删除实际指向派生类对象的基类类型指针
如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为。

要保证运行适当的析构函数,基类中的析构函数必须为虚函数:
class Item_base {
  public:
    virtual ~Item_base() { }
};

如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同
Item_base *itemP = new Item_base; // same static and dynamic type
delete itemP;            // ok: destructor for Item_base called
itemP = new Bulk_item;        // ok: static and dynamic types differ
delete itemP;            // ok: destructor for Bulk_item called

像其他虚函数一样,析构函数的虚函数性质都将继承。因此,如果层次中根类的析构函数为虚函数,则派生类析构函数也将是虚函数,无论派生类显式定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数

构造函数和赋值操作符不是虚函数

将类的赋值操作符设为虚函数很可能会令人混淆,而且不会有什么用处。

构造函数和析构函数中的虚函数

构造派生类对象时首先运行基类构造函数初始化对象的基类部分。在执行基类构造函数时,对象的派生类部分是未初始化的。实际上,此时对象还不是一个派生类对象
为了适应这种不完整,初始化派生类对象时,在基类构造函数或析构函数中,将派生类对象当作基类类型对象对待。
如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。

在执行 父类的构造时, 对象不完整,因为子类的构造函数还没有执行
这个时候 虚函数表还没有初始化正确
在父类的构造函数里 调用虚函数 调用不到子类的重载的虚函数,只能调用到父类里的虚函数

15.5. 继承情况下的类作用域

每个类都保持着自己的作用域,在该作用域中定义了成员的名字。在继承情况下,派生类的作用域嵌套在基类作用域中。如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义

名字查找在编译时发生

对象、引用或指针的静态类型决定了对象能够完成的行为。甚至当静态类型和动态类型可能不同的时候(就像使用基类类型的引用或指针指向派生类对象时,它仍然只能访问派生类对象的基类部分),静态类型仍然决定着可以使用什么成员。

名字冲突与继承

与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。

可以使用作用域操作符访问被屏蔽的基类成员

作用域与成员函数

在基类和派生类中使用同一名字的成员函数(即使形参表不同),其行为与数据成员一样:在派生类作用域中派生类成员将屏蔽基类成员。即使函数原型不同,基类成员也会被屏蔽

d.memfcn()这个调用编译器需要查找名字 memfcn,并在 Derived 类中找到。一旦找到了名字,编译器就不再继续查找了

局部作用域中声明的函数不会重载全局作用域中定义的函数,同样,派生类中定义的函数也不重载基类中定义的成员

重载函数

如果派生类重定义了重载成员,则通过派生类型只能访问派生类中重定义的那些成员。

如果派生类想通过自身类型使用的重载版本,则派生类必须要么重定义所有重载版本,要么一个也不重定义。

虚函数与作用域

要获得动态绑定,必须通过基类的引用或指针调用虚成员(根据指向的对象区分)。当我们这样做时,编译器器将在基类中查找函数。假定找到了名字,编译器就检查实参是否与形参匹配。

虚函数必须在基类和派生类中拥有同一原型(函数名和形参表相同)。如果基类成员与派生类成员接受的实参不同,就没有办法通过基类类型的引用或指针调用派生类函数。

通过基类调用被屏蔽的虚函数

Base bobj; D1 d1obj; D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn();   // ok: virtual call, will call Base::fcnat run time
bp2->fcn();   // ok: virtual call, will call Base::fcnat run time
bp3->fcn();   // ok: virtual call, will call D2::fcnat run time

在 bp2 的情况,基本对象是 D1 类的,D1 类没有重定义不接受实参的虚函数版本,因此通过 bp2 的函数调用(在运行时)调用 Base 中定义的版本

关键概念:名字查找与继承

理解 C++ 中继承层次的关键在于理解如何确定函数调用。确定函数调用遵循以下四个步骤:
1. 首先确定进行函数调用的对象、引用或指针的静态类型。
2. 在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。如果不能在类或其相关基类中找到该名字,则调用是错误的。
3. 一旦找到了该名字,就进行常规类型检查,查看如果给定找到的定义,该函数调用是否合法。
4. 假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。

15.6. 纯虚函数

在函数形参表后面写上 = 0 以指定纯虚函数

将函数定义为纯虚能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的版本决不会调用

含有(或继承)一个或多个纯虚函数的类是抽象基类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象

15.7. 容器与继承

加入派生类型的对象时,只将对象的基类部分保存在容器中。
若容器保存派生类类型对象,则不能插入基类对象。

因为派生类对象在赋值给基类对象时会被“切掉”,所以容器与通过继承相关的类型不能很好地融合。

15.8. 句柄类与继承

不能使用对象支持面向对象编程,相反,必须使用指针或引用。

使用指针或引用会加重类用户的负担。

C++ 中一个通用的技术是定义包装(cover)类或句柄类。句柄类存储和管理基类指针。指针所指对象的类型可以变化,它既可以指向基类类型对象又可以指向派生类型对象。因此用户通过句柄类访问继承层次的操作
因为句柄类使用指针执行操作,虚成员的行为将在运行时根据句柄实际绑定的对象的类型而变化。因此,句柄的用户可以获得动态行为但无须操心指针的管理

句柄有两个重要的设计考虑因素:
• 像对任何保存指针的类一样,必须确定对复制控制做些什么。包装了继承层次的句柄通常表现得像一个智能指针或者像一个值。
• 句柄类决定句柄接口屏蔽还是不屏蔽继承层次,如果不屏蔽继承层次,用户必须了解和使用基本层次中的对象。

复制未知类型

句柄类经常需要在不知道对象的确切类型时分配书籍对象的新副本。

解决这个问题的通用方法是定义虚操作进行复制,我们称将该操作命名为 clone。

为了句柄类,需要从基类开始,在继承层次的每个类型中增加 clone,基类必须将该函数定义为虚函数:
class Item_base {
  public:
    virtual Item_base* clone() const{ return new Item_base(*this); }
};

每个类必须重定义该虚函数。因为函数的存在是为了生成类对象的新副本,所以定义返回类型为类本身:
class Bulk_item : public Item_base {
  public:
    Bulk_item* clone() const{ return new Bulk_item(*this); }
};

虚函数在继承体系中的声明应该是相同的,但是有一种例外情况:基类中的虚函数返回的是指向某一基类(并不一定是这个基类)的指针或者引用,那么派生类中的虚函数可以返回基类虚函数返回的那个基类的派生类(或者是它的指针或者引用)

15.9.1. 面向对象的解决方案

可以考虑使用第 10.6.2 节的 TextQuery 表示单词查询,然后从TextQuery 类派生其他类。

概念上,“非”查询不是一种单词查询,相反,非查询“有一个”查询(单词或其他任意种类的查询),非查询对该查询的值求反。

注意到这一点,我们将不同种类的查询建模为独立的类,它们共享一个公共基类:
  WordQuery // Shakespeare
  NotQuery // ~Shakespeare
  OrQuery // Shakespeare | Marlowe
  AndQuery // William & Shakespeare

我们不继承 TextQuery,而是使用 TextQuery 类保存文件并建立相关的word_map,使用查询类建立表达式,这些表达式最终对 TextQuery 对象中的文件运行查询。

抽象接口类

已经识别出四各查询类,这些类在概念上是兄弟类。它们共享相同的抽象接口,这暗示我们定义一个抽象基类(第 15.6 节)以表示由查询执行的操作。将该抽象基类命名为 Query_base,以指出它的作用是作为查询继承层次的根。

posted @ 2020-08-13 18:42  thsj  阅读(194)  评论(0)    收藏  举报