本 Wiki 开启了 HTTPS。但由于同 IP 的 Blog 也开启了 HTTPS,因此本站必须要支持 SNI 的浏览器才能浏览。为了兼容一部分浏览器,本站保留了 HTTP 作为兼容。如果您的浏览器支持 SNI,请尽量通过 HTTPS 访问本站,谢谢!
这里会显示出您选择的修订版和当前版本之间的差别。
两侧同时换到之前的修订记录前一修订版后一修订版 | 前一修订版 | ||
cs:programming:cpp:cpp_primer:15_oop [2024/01/14 13:46] – 移除 - 外部编辑 (未知日期) 127.0.0.1 | cs:programming:cpp:cpp_primer:15_oop [2024/11/19 14:42] (当前版本) – [“豁免”单个成员] codinghare | ||
---|---|---|---|
行 1: | 行 1: | ||
+ | | ||
+ | C++ Primer 笔记 第十五章\\ | ||
+ | ---- | ||
+ | 面向对象编程的核心点有三个: | ||
+ | * **数据抽象**(// | ||
+ | * **继承**(// | ||
+ | * **动态绑定**(// | ||
+ | 本章主要会介绍继承与动态绑定。这两个特性会让定义功能类似的类变得更简单,也会让用户在使用这些类时不用考虑一些细小的区别。 | ||
+ | ====面向对象总览==== | ||
+ | ===继承=== | ||
+ | 当类中存在着继承关系时,这些类实际上处于**层级关系**。这种层级关系通常: | ||
+ | * **根部**有一个**基类**(// | ||
+ | * **派生类**(// | ||
+ | ==虚函数概述== | ||
+ | 在基类中,函数被分为两类: | ||
+ | * 依赖类型的函数,也就是需要在派生类中重定义的函数 | ||
+ | * 基类中已经定义,希望被派生类继承,但不希望被派生类修改的函数 | ||
+ | 对于第一类函数,C++ 允许我们在**不改变该函数名的同时**,在派生类中对其进行重新定义。这一类的函数被称为**虚函数**(// | ||
+ | ==Quote 类实例== | ||
+ | 假设我们希望建立一个类 // | ||
+ | * '' | ||
+ | * '' | ||
+ | 同时,我们希望指定不同的售卖书籍的策略。这里使用 //Quote// 的派生类 // | ||
+ | * '' | ||
+ | * '' | ||
+ | 因此,基类 //Quote// 的声明如下: | ||
+ | <code cpp> | ||
+ | class Quote { | ||
+ | public: | ||
+ | std:: | ||
+ | virtual double net_price(std:: | ||
+ | }; | ||
+ | </ | ||
+ | ==派生类的定义== | ||
+ | 派生类的定义中必须指明该类继承自哪些基类。通常我们通过 '':'' | ||
+ | <code cpp> | ||
+ | class Bulk_quote : public Quote { // Bulk_quote inherits from Quote | ||
+ | public: | ||
+ | double net_price(std:: | ||
+ | }; | ||
+ | </ | ||
+ | 需要注意的几个点: | ||
+ | * 列表中的 '' | ||
+ | * 派生类中**必须声明**所有需要重新定义的虚函数。这些函数可以不加 '' | ||
+ | * C++11 中允许显式的使用 '' | ||
+ | ===动态绑定=== | ||
+ | 动态绑定允许用户使用同样的代码处理不同的对象。比如我们可以使用 '' | ||
+ | <code cpp> | ||
+ | // calculate and print the price for the given number of copies, applying any discounts | ||
+ | double print_total(ostream &os, | ||
+ | const Quote &item, size_t n) | ||
+ | { | ||
+ | // depending on the type of the object bound to the item parameter | ||
+ | // calls either Quote:: | ||
+ | double ret = item.net_price(n); | ||
+ | os << "ISBN: " << item.isbn() // calls Quote::isbn | ||
+ | << | ||
+ | | ||
+ | } | ||
+ | </ | ||
+ | 几个需要注意的地方: | ||
+ | * '' | ||
+ | * '' | ||
+ | 调用的结果如下: | ||
+ | <code cpp> | ||
+ | // basic has type Quote; bulk has type Bulk_quote | ||
+ | print_total(cout, | ||
+ | print_total(cout, | ||
+ | </ | ||
+ | 上述的调用过程中,选择 '' | ||
+ | <WRAP center round info 100%> | ||
+ | C++ 中,当使用**指向基类的引用或者指针**调用虚函数时,会发生动态绑定。 | ||
+ | </ | ||
+ | ====定义基类和派生类==== | ||
+ | ===定义基类=== | ||
+ | 接着 //Quote// 的例子。// | ||
+ | <code cpp> | ||
+ | class Quote { | ||
+ | public: | ||
+ | Quote() = default; | ||
+ | Quote(const std::string &book, double sales_price): | ||
+ | | ||
+ | std::string isbn() const { return bookNo; } | ||
+ | // returns the total sales price for the specified number of items | ||
+ | // derived classes will override and apply different discount algorithms | ||
+ | virtual double net_price(std:: | ||
+ | { return n * price; } | ||
+ | virtual ~Quote() = default; // dynamic binding for the destructor | ||
+ | private: | ||
+ | std::string bookNo; // ISBN number of this item | ||
+ | protected: | ||
+ | double price = 0.0; // normal, undiscounted price | ||
+ | }; | ||
+ | </ | ||
+ | 该实现中出现了三个新的知识点: | ||
+ | * '' | ||
+ | * 析构函数也被定义为了 '' | ||
+ | * '' | ||
+ | <WRAP center round info 100%> | ||
+ | 基类需要定义**虚析构函数**,即便该析构函数不会投入使用。 | ||
+ | </ | ||
+ | ==继承中需要被重新定义的成员== | ||
+ | 派生类往往需要对某些成员进行重新定义。比如 //Quote// 实例中的 '' | ||
+ | * 在基类中被定义为**虚函数** | ||
+ | * 通过**间接访问的方式**(指针,引用)调用该函数可以**实现动态绑定** | ||
+ | * 虚函数可以是除了构造函数以外的任何**非静态**成员 | ||
+ | 定义该类函数时,只需要在基类的声明前面加上 '' | ||
+ | \\ < | ||
+ | ==继承中被直接继承的成员== | ||
+ | 成员被直接继承意味着派生类对其的要求与基类相同。这种成员的特点是: | ||
+ | * **不会被声明为虚函数** | ||
+ | * 解析会发生在**编译期** | ||
+ | * 无论是基类还是派生类,调用该类成员都会产生相同的逻辑行为,比如例子中的 '' | ||
+ | ==继承中的访问控制== | ||
+ | 根据基类成员中的访问说明符: | ||
+ | * **public**:这类基类成员可以被派生类访问,也可以被类外的其他用户访问 | ||
+ | * **private**:只能被基类的其他成员访问 | ||
+ | * **protected**:可以被基类与派生类的成员访问,但无法被类外的其他用户访问 | ||
+ | 可以看出在 //Quote// 的实现中,**存在重写**的函数(虚函数)往往会用到 '' | ||
+ | ===定义派生类=== | ||
+ | 派生类的定义有几个特点: | ||
+ | * 派生类需要指明继承自哪些类;该过程通过派生列表(// | ||
+ | * 需要**重写**的成员(虚函数)必须在**派生类中**进行**声明** | ||
+ | * C++11 中,被重写的函数可以通过 '' | ||
+ | 来看一下 // | ||
+ | <code cpp> | ||
+ | class Bulk_quote : public Quote { // Bulk_quote inherits from Quote | ||
+ | Bulk_quote() = default; | ||
+ | Bulk_quote(const std:: | ||
+ | // overrides the base version in order to implement the bulk purchase discount policy | ||
+ | double net_price(std:: | ||
+ | private: | ||
+ | std::size_t min_qty = 0; // minimum purchase for the discount to apply | ||
+ | double discount = 0.0; // fractional discount to apply | ||
+ | }; | ||
+ | </ | ||
+ | <WRAP center round info 100%> | ||
+ | 如果虚函数**没有**在派生类中进行**重写**,那么派生类会**直接继承基类中该函数的定义**。 | ||
+ | </ | ||
+ | ==派生列表中的访问说明符== | ||
+ | 派生列表中的说明符也分为三种:'' | ||
+ | * 基类中的**公有成员**会成为派生类接口的**一部分** | ||
+ | * 该派生类类型的指针和引用可以绑定到基类对象上 | ||
+ | 以上面的实现举例,由于 // | ||
+ | <WRAP center round help 100%> | ||
+ | 像 // | ||
+ | </ | ||
+ | ==派生类对象与派生类到基类的转换== | ||
+ | 实际上,一个派生类的对象可以大致被分为两个部分: | ||
+ | * **自定义部分**:该部分是一个子对象,包含了所有的,需要在派生类中**重写**的**非静态**成员 | ||
+ | * **继承部分**:该部分也是一个子对象,包含了所有继承自基类的成员 | ||
+ | 比如 // | ||
+ | \\ {{ : | ||
+ | 因为派生类对象包含了对应的基类成员,因此我们可以像使用基类对象一样使用派生类对象。具体的来说,我们可以将**基类的引用或指针**绑定到**派生类对象中的基类部分**上: | ||
+ | <code cpp> | ||
+ | Quote item; // object of base type | ||
+ | Bulk_quote bulk; // | ||
+ | |||
+ | Quote *p = & | ||
+ | p = & | ||
+ | Quote &r = bulk; // | ||
+ | </ | ||
+ | 这种转换通常被称为**派生类到基类的转换**(// | ||
+ | <WRAP center round important 100%> | ||
+ | 派生类的**子对象在内存中并不一定是连续存储的**。 | ||
+ | </ | ||
+ | ==派生类的构造函数== | ||
+ | 尽管派生类对象包含了继承自基类的成员,但这些成员并不能被派生类直接初始化,而是需要**使用基类的构造函数**进行初始化。派生类中的成员初始化顺序如下: | ||
+ | - 派生类将需要的**参数传递给基类**构造函数 | ||
+ | - 基类构造函数初始化派生类对象的**基类部分**成员 | ||
+ | - 派生类构造函数按**声明的顺序**初始化**派生类对象自身**的成员 | ||
+ | 以 // | ||
+ | <code cpp> | ||
+ | Bulk_quote(const std:: | ||
+ | | ||
+ | | ||
+ | // as before | ||
+ | }; | ||
+ | </ | ||
+ | 当 //Quote// 的构造函数执行完毕时(初始化列表以及函数体执行完毕以后),// | ||
+ | 派生类中调用基类的构造函数与正常的调用一致:如果想**调用指定的构造函数版本,则需要提供对应的参数**;如果不提供参数,则基类构造函数会进行**默认初始化**。 | ||
+ | <WRAP center round tip 100%> | ||
+ | 成员的初始化由其**对应的类**控制。 | ||
+ | </ | ||
+ | |||
+ | ==在派生类中使用基类的成员== | ||
+ | 基类成员可以以 '' | ||
+ | <code cpp> | ||
+ | // if the specified number of items are purchased, use the discounted price | ||
+ | double Bulk_quote:: | ||
+ | { | ||
+ | if (cnt >= min_qty) | ||
+ | return cnt * (1 - discount) * price; | ||
+ | else | ||
+ | return cnt * price; | ||
+ | } | ||
+ | </ | ||
+ | 该实现需要访问基类中的 '' | ||
+ | 值得一提的是,派生类的作用域是**嵌套**在基类作用域中的。因此,派生类使用成员的方式与基类是一致的。 | ||
+ | <WRAP center round box 100%> | ||
+ | **关键概念:遵循基类的接口**\\ \\ | ||
+ | 需要明确的是,每个类都定义了自己的接口;因此即便是基类与派生类,**类与类之间的交互应该通过接口来实现**。这也是为什么我们需要使用基类的构造函数来初始化派生类中的基类部分的原因。 | ||
+ | </ | ||
+ | ==继承与静态成员== | ||
+ | 如果基类中包含了**静态成员**,那么该成员在整个继承体系(层级)中**有且只有唯一的一个实例**: | ||
+ | <code cpp> | ||
+ | class Base { | ||
+ | public: | ||
+ | static void statmem(); | ||
+ | }; | ||
+ | class Derived : public Base { | ||
+ | void f(const Derived& | ||
+ | }; | ||
+ | </ | ||
+ | 静态成员遵循一般的访问控制: | ||
+ | <code cpp> | ||
+ | void Derived:: | ||
+ | { | ||
+ | Base:: | ||
+ | Derived:: | ||
+ | // ok: derived objects can be used to access static from base | ||
+ | derived_obj.statmem(); | ||
+ | statmem(); | ||
+ | } | ||
+ | </ | ||
+ | ==派生类的声明== | ||
+ | 与定义不同,派生类的声明**不包括派生列表**。**派生列表必须与派生类的定义同时出现**: | ||
+ | <code cpp> | ||
+ | class Bulk_quote : public Quote; // error: derivation list can't appear here | ||
+ | class Bulk_quote; | ||
+ | </ | ||
+ | ==使用基类== | ||
+ | 基类在被派生类使用之前,**必须拥有完整定义**: | ||
+ | <code cpp> | ||
+ | class Quote; | ||
+ | // error: Quote must be defined | ||
+ | class Bulk_quote : public Quote { ... }; | ||
+ | </ | ||
+ | 这是因为派生类中包含了基类部分。如果希望使用这个部分中的成员,必须要有成员的定义。这也暗示了**基类的派生类不能是自己**。 | ||
+ | ==直接基类与间接基类== | ||
+ | * **直接基类**(// | ||
+ | * **间接基类**(// | ||
+ | <code cpp> | ||
+ | class Base { /* ... */ } ; | ||
+ | class D1: public Base { /* ... */ }; | ||
+ | class D2: public D1 { /* ... */ }; | ||
+ | </ | ||
+ | 如果一个派生类继承自间接基类,那么该类的基类部分将继承层级中之前所有**间接基类**的**基类部分**。 | ||
+ | ==阻止继承== | ||
+ | C++ 11 中通过在类声明后面添加 '' | ||
+ | <code cpp> | ||
+ | class NoDerived final { /* */ }; // NoDerived can't be a base class | ||
+ | class Base { /* */ }; | ||
+ | // Last is final; we cannot inherit from Last | ||
+ | class Last final : Base { /* */ }; // Last can't be a base class | ||
+ | class Bad : NoDerived { /* */ }; // error: NoDerived is final | ||
+ | class Bad2 : Last { /* */ }; // error: Last is final | ||
+ | </ | ||
+ | ===类型转换与继承=== | ||
+ | 之前提到过,基类的引用 / 指针可以绑定 / 指向其派生类对象: | ||
+ | <code cpp> | ||
+ | double print_total(ostream &os, const Quote &item, size_t n) {....}; | ||
+ | Bulk_quote bulk; // | ||
+ | Quote &r = bulk; // | ||
+ | Quote *p = &bulk; // p points to the Quote part of bulk | ||
+ | </ | ||
+ | 当使用基类的引用或者指针类型的对象时,我们**不能确定**该对象**绑定的实际类型**;该类型可能是基类对象,也可能是派生类对象。因此,使用此类型对象时,需要考虑两种类型:**静态类型**(// | ||
+ | <WRAP center round info 100%> | ||
+ | 智能指针也支持派生类到基类的转换。 | ||
+ | </ | ||
+ | |||
+ | |||
+ | ==静态类型与动态类型== | ||
+ | 静态类型与动态类型存在如下的区别: | ||
+ | * 静态类型指**变量声明时定义**的类型,或是**表达式得出的结果**的类型。这类类型在**编译期就能确认** | ||
+ | * 动态类型指**变量或者表达式在内存中表示的实际类型**,这类类型往往需要在**运行期**才能确认 | ||
+ | 以 '' | ||
+ | <WRAP center round info 100%> | ||
+ | 需要注意的是,如果表达式的类型(接受参数的类型)**不是指针或者引用类型**,那么其静态类型与动态类型是**一**致的。 | ||
+ | </ | ||
+ | ==不存在从基类到派生类的隐式转换== | ||
+ | 之所以存在派生类到基类的转换,是因为派生类中的存在**基类部分**,且该部分可以被基类的指针或者引用绑定。而基类对象不一定是派生类的一部分(绝大部分情况下,派生类都会定义基类中没有的成员)。因此,从基类到派生类的转换是不允许的,因为该转换很可能**导致访问基类中不存在的成员**: | ||
+ | <code cpp> | ||
+ | Quote base; | ||
+ | Bulk_quote* bulkP = & | ||
+ | Bulk_quote& | ||
+ | </ | ||
+ | 由于存在这种限制,即便是基类的指针 / 引用与派生类对象已经绑定,该类转换也不被允许发生: | ||
+ | <code cpp> | ||
+ | Bulk_quote bulk; | ||
+ | Quote *itemP = & | ||
+ | Bulk_quote *bulkP = itemP; | ||
+ | </ | ||
+ | 这是因为编译器没有办法在编译期确定对应的转换是否在运行期是安全的。具体的来说,编译期只能**通过检查静态类型**来判断该转换是否合法。\\ \\ | ||
+ | 如果需要这样类型转换,有两种解决方案: | ||
+ | * 当基类中存在虚函数时,使用 '' | ||
+ | * 如果已知基类到派生类的类型转换是安全的时候,可以使用 '' | ||
+ | ==不存在基类对象与派生类对象之间的隐式转换== | ||
+ | 派生类到基类的隐式转换只能应用于引用 / 指针类型上: | ||
+ | * 不存在基类到派生类的对象转换 | ||
+ | * 派生类的对象可能转换为基类对象,但结果可能不满足预期 | ||
+ | 由于派生类到基类的转换应用于引用类型,因此拷贝 / 移动 / 赋值操作都可以使用派生类的对象作为参数。但需要注意的是,当传递派生类对象到这些操作中时,会调用基类的拷贝构造/ | ||
+ | <code cpp> | ||
+ | Bulk_quote bulk; // object of derived type | ||
+ | Quote item(bulk); | ||
+ | item = bulk; // calls Quote:: | ||
+ | </ | ||
+ | 上面的例子中,'' | ||
+ | <WRAP center round box 100%> | ||
+ | 几个关键的概念: | ||
+ | * **派生类到基类**的隐式转换只能应用于**指针 / 引用类型** | ||
+ | * **不存在**基类到派生类的隐式转换 | ||
+ | * 派生类到基类的隐式转换可能会受**访问控制**的影响 | ||
+ | * 可以将派生类对象作为基类的对象的拷贝控制成员的参数,但这类操作只会对**派生类对象中的基类部分**进行处理。 | ||
+ | </ | ||
+ | |||
+ | ====虚函数==== | ||
+ | 当某个继承层级中定义了一个虚函数,该层级中的所有同名函数都被隐式的定义为虚函数。由于虚函数的调用直到运行期才可以确定具体的版本,因此层级中**所有的虚函数都必须被定义**,无论该函数是否会被调用。 | ||
+ | ===虚函数的解析时机=== | ||
+ | 虚函数的解析时机取决于调用虚函数的方式。如果**通过引用或者指针的方式**调用某个虚函数,那么该虚函数将直到运行期才能确定被调用的具体版本。也就是说,**被调用的版本取决于调用者的动态类型**。比如之前的 '' | ||
+ | <code cpp> | ||
+ | Quote base(" | ||
+ | print_total(cout, | ||
+ | Bulk_quote derived(" | ||
+ | print_total(cout, | ||
+ | </ | ||
+ | 如果调用的方式是通过**非引用或者指针**的方式,那么**调用的虚函数版本有调用者的静态类型决定**: | ||
+ | <code cpp> | ||
+ | base = derived; | ||
+ | base.net_price(20); | ||
+ | </ | ||
+ | 上例中,对 '' | ||
+ | ==关键概念:C++ 中的多态== | ||
+ | 面向对象编程的关键概念是**多态**(// | ||
+ | 当使用引用或者指针调用基类中的函数时,调用对象的类型是不确定的。如果被调用的函数时虚函数,那么该函数的版本需要等到运行期决定,因为只有到了运行期才能决定引用或指针绑定对象的真正类型。\\ \\ | ||
+ | 另外,以下两种调用都会在**编译期**绑定: | ||
+ | * 对非虚函数的调用 | ||
+ | * 通过对象本身调用任何函数(无论是不是虚函数) | ||
+ | 这种情况下,对象的静态类型与动态类型不做区分,也无法区分。 | ||
+ | <WRAP center round tip 100%> | ||
+ | 只有在使用**引用或者指针**调用虚函数的时候,才会在运行期解析该调用。也只有在这种情况下,调用者的静态类型与动态类型才会不同。 | ||
+ | </ | ||
+ | ===派生类中的虚函数=== | ||
+ | 派生类中的虚函数有两个重要的特点: | ||
+ | * 当基类中某个函数被声明为虚函数时,其所有派生类中的该函数**都是虚函数**。因此,派生类中虚函数不需要显式的使用 '' | ||
+ | * 定义派生类中的虚函数时,其**参数列表**和**返回值**需要完全与基类中的被覆盖版本**完全一致**。 | ||
+ | <WRAP center round info 100%> | ||
+ | “返回值与基类中版本一致”这个论断有一个例外。当虚函数的返回类型是调用者**自身的**引用或是指针时,该规则无效。比如,如果 //D// 继承自 // | ||
+ | 该例外受继承访问控制的影响(需要可访问)。 | ||
+ | </ | ||
+ | ===final 和 override 说明符=== | ||
+ | C++ 中允许对派生类中的虚函数赋予**与基类同名函数不同的参数列表**。但在这种情况下,编译器会将其**视作不同的函数**,而非派生类对基类的重写。实践中,这样的写法通常是错误的;这可能是因为作者原本希望重写,但弄错了参数列表。\\ \\ | ||
+ | C++ 11 中提供了关键字 '' | ||
+ | <code cpp> | ||
+ | struct B { | ||
+ | virtual void f1(int) const; | ||
+ | virtual void f2(); | ||
+ | void f3(); | ||
+ | }; | ||
+ | struct D1 : B { | ||
+ | void f1(int) const override; // ok: f1 matches f1 in the base | ||
+ | void f2(int) override; // error: B has no f2(int) function | ||
+ | void f3() override; | ||
+ | void f4() override; | ||
+ | }; | ||
+ | </ | ||
+ | C++ 11 中还提供了另外一个关键字 '' | ||
+ | <code cpp> | ||
+ | struct D2 : B { | ||
+ | // inherits f2() and f3() from B and overrides f1(int) | ||
+ | void f1(int) const final; // subsequent classes can't override f1 (int) | ||
+ | }; | ||
+ | struct D3 : D2 { | ||
+ | void f2(); // ok: overrides f2 inherited from the indirect base, B | ||
+ | void f1(int) const; // error: D2 declared f2 as final | ||
+ | }; | ||
+ | </ | ||
+ | ===虚函数与默认参数=== | ||
+ | 虚函数也可以拥有默认参数。当虚函数被调用时,如果需要使用默认参数,则默认参数的值以**调用者的静态类型**中的默认值为准。举例来说,如果基类中与派生类中的虚函数拥有不同值的默认参数,当使用基类的引用或指针调用该虚函数时,即便我们传递的是派生类的对象,最后该虚函数也会使用基类中的默认参数值。 | ||
+ | <WRAP center round help 100%> | ||
+ | 如果虚函数使用默认参数,应该基类和派生类中默认参数一致。 | ||
+ | </ | ||
+ | ===回避虚函数的机制=== | ||
+ | 某些情况下,我们不希望使用虚函数的机制来调用虚函数,而是希望调用某个类中指定的版本,此时需要通过**指定该版本的作用域**来调用指定的虚函数版本。比如强制调用 '' | ||
+ | <code cpp> | ||
+ | // calls the version from the base class regardless of the dynamic type of baseP | ||
+ | double undiscounted = baseP-> | ||
+ | </ | ||
+ | 这样调用的话,无论 '' | ||
+ | <WRAP center round alert 100%> | ||
+ | 当使用派生类虚函数调用其基类版本时,省略作用域运算符将导致该虚函数调用其自身,从而导致无限递归。 | ||
+ | </ | ||
+ | ====抽象基类==== | ||
+ | 之前的 '' | ||
+ | 但有一个问题是, '' | ||
+ | 一个办法是不在 | ||
+ | ===纯虚函数=== | ||
+ | 实际上,像 '' | ||
+ | * 某些没有任何意义的虚函数不应该在这种类型的类中实现 | ||
+ | * 这种类型的类不应该实体化,也就是不应该拥有对象 | ||
+ | C++ 中通过将对应的虚函数定义为**纯虚函数**(// | ||
+ | * 与虚函数不同,纯虚函数**不需要定义**(如果要定义只能在类外) | ||
+ | * '' | ||
+ | 下面是 | ||
+ | <code cpp> | ||
+ | // class to hold the discount rate and quantity | ||
+ | // derived classes will implement pricing strategies using these data | ||
+ | class Disc_quote : public Quote { | ||
+ | public: | ||
+ | Disc_quote() = default; | ||
+ | Disc_quote(const std:: | ||
+ | std::size_t qty, double disc): | ||
+ | | ||
+ | | ||
+ | double net_price(std:: | ||
+ | protected: | ||
+ | std::size_t quantity = 0; // purchase size for the discount to apply | ||
+ | double discount = 0.0; // fractional discount to apply | ||
+ | }; | ||
+ | </ | ||
+ | 需要注意的是, '' | ||
+ | ===抽象基类=== | ||
+ | 像 '' | ||
+ | <code cpp> | ||
+ | // Disc_quote declares pure virtual functions, which Bulk_quote will override | ||
+ | Disc_quote discounted; // error: can't define a Disc_quote object | ||
+ | Bulk_quote bulk; // ok: Bulk_quote has no pure virtual functions | ||
+ | </ | ||
+ | ==关键概念:重构== | ||
+ | 向层级中添加 '' | ||
+ | ==派生类的构造函数只会初始化直接基类== | ||
+ | 接着之前的例子。定义了 '' | ||
+ | <code cpp> | ||
+ | // the discount kicks in when a specified number of copies of the same book are sold | ||
+ | // the discount is expressed as a fraction to use to reduce the normal price | ||
+ | class Bulk_quote : public Disc_quote { | ||
+ | public: | ||
+ | Bulk_quote() = default; | ||
+ | Bulk_quote(const std:: | ||
+ | std::size_t qty, double disc): | ||
+ | Disc_quote(book, | ||
+ | // overrides the base version to implement the bulk purchase discount policy | ||
+ | double net_price(std:: | ||
+ | }; | ||
+ | </ | ||
+ | 需要注意的是,派生类中的构造函数只会对直接基类的基类部分初始化,比如上面的 | ||
+ | <code cpp> | ||
+ | Bulk_quote(const std:: | ||
+ | Disc_quote(book, | ||
+ | </ | ||
+ | 实际上进行了三个部分的初始化: | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | |||
+ | 可以看出来的是,每个类控制着自身部分的初始化。而派生类传递的参数只能传递给直接基类;如果是间接基类,需要由派生类的直接基类再进行一次传递。 | ||
+ | ====访问控制和继承==== | ||
+ | 在类继承的同时,被继承的类还可以指定派生类对自身成员的访问方式。 | ||
+ | ===protected 成员=== | ||
+ | '' | ||
+ | * 当前类的**使用者**无法访问 '' | ||
+ | * 当前类的**实现者**(类成员,友元函数)可以访问 '' | ||
+ | * 派生类的**实现者**(类成员,友元函数)**只能通过派生类对象** 访问基类中的 '' | ||
+ | 下面是详细的例子: | ||
+ | <code cpp> | ||
+ | class Base { | ||
+ | protected: | ||
+ | int prot_mem; | ||
+ | }; | ||
+ | class Sneaky : public Base { | ||
+ | friend void clobber(Sneaky& | ||
+ | friend void clobber(Base& | ||
+ | int j; // j is private by default | ||
+ | }; | ||
+ | // ok: clobber can access the private and protected members in Sneaky objects | ||
+ | void clobber(Sneaky &s) { s.j = s.prot_mem = 0; } | ||
+ | // error: clobber can't access the protected members in Base | ||
+ | void clobber(Base &b) { b.prot_mem = 0; } | ||
+ | </ | ||
+ | 假设上述的程序中可以通过基类的对象来访问基类中的受保护成员,那么: | ||
+ | <code cpp> | ||
+ | void clobber(Base &b) { b.prot_mem = 0; } | ||
+ | </ | ||
+ | 这个版本的 '' | ||
+ | ===三种继承方式=== | ||
+ | 继承的成员是否能访问取决于以下两个因素的组合: | ||
+ | * 成员本身是否可以访问 | ||
+ | * 派生列表是否允许改成员被访问 | ||
+ | 派生列表用于指定**派生类的使用者** 对基类成员的访问方式。继承的访问控制(方式)有三种: | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | 派生类的访问控制符不影响派生类成员(友元)对基类成员的访问。基类成员是否能被派生类成员访问只取决于该基类成员在基类中的权限。比如下面的例子: | ||
+ | * 无论以公有或私有的方式继承 '' | ||
+ | * '' | ||
+ | <code cpp> | ||
+ | class Base { | ||
+ | public: | ||
+ | void pub_mem(); | ||
+ | protected: | ||
+ | int prot_mem; | ||
+ | private: | ||
+ | char priv_mem; | ||
+ | }; | ||
+ | struct Pub_Derv : public Base { | ||
+ | // ok: derived classes can access protected members | ||
+ | int f() { return prot_mem; } | ||
+ | // error: private members are inaccessible to derived classes | ||
+ | char g() { return priv_mem; } | ||
+ | }; | ||
+ | struct Priv_Derv : private Base { | ||
+ | // private derivation doesn' | ||
+ | int f1() const { return prot_mem; } | ||
+ | }; | ||
+ | Pub_Derv d1; // | ||
+ | Priv_Derv d2; // members inherited from Base are private | ||
+ | d1.pub_mem(); | ||
+ | d2.pub_mem(); | ||
+ | </ | ||
+ | 需要注意的是,**继承列表的访问控制也会影响派生类的派生类**,比如以 '' | ||
+ | <code cpp> | ||
+ | struct Derived_from_Public : public Pub_Derv { | ||
+ | // ok: Base:: | ||
+ | int use_base() { return prot_mem; } | ||
+ | }; | ||
+ | struct Derived_from_Private : public Priv_Derv { | ||
+ | // error: Base:: | ||
+ | int use_base() { return prot_mem; } | ||
+ | }; | ||
+ | </ | ||
+ | ===派生类到基类的转换与访问性=== | ||
+ | 派生类到基类的转换是否可以执行,取决于两点: | ||
+ | * 使用该转换的代码 | ||
+ | * 派生列表的继承方式 | ||
+ | 假设 '' | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | <WRAP center round tip 100%> | ||
+ | 如果我们可以访问基类中的公有成员,那么我们就可以进行派生类到基类的转换。 | ||
+ | </ | ||
+ | ==受保护的成员与类的设计== | ||
+ | 当不考虑的继承的时候,类的用户被分为两种: | ||
+ | * 使用者:这种用户通过**接口**(**类的公有成员**)访问类 | ||
+ | * 实现者(类成员 / 友元):这种用户需要既可以访问类的公有成员,也可以访问类的私有成员 | ||
+ | 当考虑有继承情况的时候,类的用户需要被分为三种: | ||
+ | * 使用者:该类用户同样通过接口(公有成员)访问类 | ||
+ | * **基类的实现者**:该类用户要求可以访问基类中的所有成员 | ||
+ | * **派生类实现者**:该类用户要求可以访问派生类自身的所有成员,以及基类中提供给派生类**专用**的成员('' | ||
+ | 鉴于这样的分类,基类的设计应该作如下分割: | ||
+ | * 接口部分:定义为公有成员,供使用者使用 | ||
+ | * 实现部分: | ||
+ | * 供**派生类**使用的部分:定义为**受保护成员** | ||
+ | * 只**供基类**使用的部分:定义为**私有成员** | ||
+ | ===友元与继承=== | ||
+ | **友元关系是无法继承的**。友元关系与所在的类绑定,因此通过派生类的友元访问基类成员,或是通过基类的友元访问派生类的成员都是不可行的。来看下面一个例子: | ||
+ | <code cpp> | ||
+ | class Base { | ||
+ | // added friend declaration; | ||
+ | friend class Pal; // Pal has no access to classes derived from Base | ||
+ | }; | ||
+ | class Pal { | ||
+ | public: | ||
+ | int f(Base b) { return b.prot_mem; } // ok: Pal is a friend of Base | ||
+ | int f2(Sneaky s) { return s.j; } // error: Pal not friend of Sneaky | ||
+ | // access to a base class is controlled by the base class, even inside a derived object | ||
+ | int f3(Sneaky s) { return s.prot_mem; } // ok: Pal is a friend | ||
+ | }; | ||
+ | </ | ||
+ | 需要注意的是,我们可以通过**基类的友元函数**访问派生类中**基类部分**的成员。注意这里的 '' | ||
+ | 同时,友元关系只对被**声明的友元类**有效,对其派生类无效: | ||
+ | <code cpp> | ||
+ | // D2 has no access to protected or private members in Base | ||
+ | class D2 : public Pal { | ||
+ | public: | ||
+ | int mem(Base b) | ||
+ | { return b.prot_mem; } // error: friendship doesn' | ||
+ | }; | ||
+ | </ | ||
+ | ===使用 using “豁免”单个成员=== | ||
+ | 如果希望对某个指定的成员进行访问级别的变更,我们可以使用 '' | ||
+ | <code cpp> | ||
+ | class Base { | ||
+ | public: | ||
+ | std::size_t size() const { return n; } | ||
+ | protected: | ||
+ | std::size_t n; | ||
+ | }; | ||
+ | class Derived : private Base { // note: private inheritance | ||
+ | public: | ||
+ | // maintain access levels for members related to the size of the object | ||
+ | using Base::size; | ||
+ | protected: | ||
+ | using Base::n; | ||
+ | }; | ||
+ | </ | ||
+ | 上例中,由于 '' | ||
+ | 可见的是,被 '' | ||
+ | <WRAP center round info 100%> | ||
+ | //using// 声明应该只提供给那些被允许访问的成员。 | ||
+ | </ | ||
+ | ===默认继承的保护级别=== | ||
+ | 由于类可以定义为 '' | ||
+ | * class 中,默认的继承方式是**私有**的 | ||
+ | * struct 中,默认的继承方式是**公有**的 | ||
+ | <code cpp> | ||
+ | class Base { /* ... */ }; | ||
+ | struct D1 : Base { /* ... */ }; // public inheritance by default | ||
+ | class D2 : Base { /* ... */ }; // private inheritance by default | ||
+ | </ | ||
+ | <WRAP center round tip 100%> | ||
+ | 需要进行私有继承的成员应该**显式的提供私有关键字**,而不是使用默认的方式来继承。这样做可以令私有关系明确。 | ||
+ | </ | ||
+ | ===要点总结=== | ||
+ | ==成员的访问控制== | ||
+ | * 成员的访问控制由当前类决定。 | ||
+ | * 成员的访问控制可以被继承方式改变 | ||
+ | ==派生类到基类的转换== | ||
+ | * 是否可以转换取决于基类中的公有成员是否可访问 | ||
+ | * **使用者**需要**公有继承**这些成员保证访问性 | ||
+ | * **派生类实现者**没有限制,因为基类部分对于派生类实现者是没有访问限制的 | ||
+ | * **派生类的子派生类实现者**需要通过**公有 / 保护**继承的方式来保证访问性 | ||
+ | ==protected 相关== | ||
+ | * protected 的成员是给派生类的**对象**使用的 | ||
+ | * 派生类成员只能通过**对象**的方式访问 Protected 成员 | ||
+ | ==友元的不可传递性== | ||
+ | * 基类的友元只能访问对应的基类部分数据(基类成员,以及派生类中的基类部分成员) | ||
+ | * 友元的派生类无法继承友元的访问权限 | ||
+ | ====继承中的类作用域==== | ||
+ | 在继承中,**派生类的作用域嵌套在基类的作用域中**。如果名字在派生类中不能解析,编译器会到其直接基类中查找改名字。这种特性使得我们可以用派生类对象调用基类中的成员,比如之前 '' | ||
+ | <code cpp> | ||
+ | Bulk_quote bulk; | ||
+ | cout << bulk.isbn(); | ||
+ | </ | ||
+ | '' | ||
+ | ===名字查找发生在编译期=== | ||
+ | 对象(或者引用,指针)的静态类型决定了该对象的哪些成员是可见的。也就是说,无论对象的动态类型最终是什么,也不会影响某个成员的可见性。一个常见的例子是,使用基类访问派生类中的成员。此时有两种方式访问: | ||
+ | * 通过基类对象访问,也就是基类的静态类型与动态类型相同 | ||
+ | * 通过基类的指针或者引用访问,也就是基类的静态类型与动态类型可能不相同 | ||
+ | 但无论哪种方式,都无法访问派生类中的成员。比如下面的例子: | ||
+ | <code cpp> | ||
+ | class Disc_quote : public Quote { | ||
+ | public: | ||
+ | std:: | ||
+ | { return {quantity, discount}; } | ||
+ | // other members as before | ||
+ | }; | ||
+ | |||
+ | Bulk_quote bulk; | ||
+ | Bulk_quote *bulkP = &bulk; // static and dynamic types are the same | ||
+ | Quote *itemP = & | ||
+ | bulkP-> | ||
+ | itemP-> | ||
+ | </ | ||
+ | 上面的例子中, '' | ||
+ | * 名字的查找发生在编译期,因此只能由静态类型决定 | ||
+ | * 名字的查找是逐级往上的。由于派生类的作用域嵌套在基类中,因此派生类的元素对基类并不可见。**当使用基类对象调用成员时,名字搜寻的范围只有基类(以及其上级)**,而不包括派生类。 | ||
+ | ===命名冲突与继承=== | ||
+ | 由于派生类的作用域属于嵌套作用域,因此派生类中定义的,任意与基类中相同的名字,都会**隐藏**基类中的定义: | ||
+ | <code cpp> | ||
+ | struct Base { | ||
+ | Base(): mem(0) { } | ||
+ | protected: | ||
+ | int mem; | ||
+ | }; | ||
+ | struct Derived : Base { | ||
+ | Derived(int i): mem(i) { } // initializes Derived:: | ||
+ | // Base::mem is default initialized | ||
+ | int get_mem() { return mem; } // returns Derived:: | ||
+ | protected: | ||
+ | int mem; // hides mem in the base | ||
+ | }; | ||
+ | |||
+ | Derived d(42); | ||
+ | cout << d.get_mem() << endl; // prints 42 | ||
+ | </ | ||
+ | 与一般情况类似,如果想使用基类中的同名成员,可以通过指定作用域来实现: | ||
+ | <code cpp> | ||
+ | struct Derived : Base { | ||
+ | int get_base_mem() { return Base::mem; } | ||
+ | // ... | ||
+ | }; | ||
+ | </ | ||
+ | <WRAP center round info 100%> | ||
+ | 派生类中的重名成员会隐藏基类中的成员。因此,不推荐在派生类中使用基类中已存在的名字定义成员。 | ||
+ | </ | ||
+ | ==关键概念:名字查找与继承== | ||
+ | 名字查找在存在继承关系的类中会有如下的查找顺序: | ||
+ | 假设有如下调用: | ||
+ | <code cpp> | ||
+ | p-> | ||
+ | </ | ||
+ | - 首先,编译器会确定 '' | ||
+ | - 其次,查看 '' | ||
+ | - 如果 '' | ||
+ | - 如果 '' | ||
+ | - 当找到 '' | ||
+ | - 如果 '' | ||
+ | - 否则,按一般的非虚函数调用处理 | ||
+ | ==名字查找在类型检查之前== | ||
+ | 与一般名字查找规则相同: | ||
+ | * 派生类中定义的函数**不会重载**基类中的同名函数 | ||
+ | * 基类中的函数会被派生类中的同名函数隐藏,**无论两个函数是否具有相同的参数列表** | ||
+ | 这是因为编译器会首先查找名字。假设派生类中存在同名函数,当编译器找到该函数时,名字查找也就结束了。之后,编译器才会进行类型的匹配。比如下面的例子中,被调用的 '' | ||
+ | <code cpp> | ||
+ | struct Base { | ||
+ | int memfcn(); | ||
+ | }; | ||
+ | struct Derived : Base { | ||
+ | int memfcn(int); | ||
+ | }; | ||
+ | Derived d; Base b; | ||
+ | b.memfcn(); | ||
+ | d.memfcn(10); | ||
+ | d.memfcn(); | ||
+ | d.Base:: | ||
+ | </ | ||
+ | ===虚函数与作用域=== | ||
+ | 可见,虚函数为什么需要保证一致的参数列表。如果参数列表不一致,则重写不会发生;取而代之的是,派生类会隐藏基类中的同名函数。在没有重写的情况下,我们无法通过基类指针或应用的方式调用派生类中的重写版本: | ||
+ | <code cpp> | ||
+ | class Base { | ||
+ | public: | ||
+ | virtual int fcn(); | ||
+ | }; | ||
+ | class D1 : public Base { | ||
+ | public: | ||
+ | // hides fcn in the base; this fcn is not virtual | ||
+ | // D1 inherits the definition of Base::fcn() | ||
+ | int fcn(int); | ||
+ | virtual void f2(); // new virtual function that does not exist in Base | ||
+ | }; | ||
+ | class D2 : public D1 { | ||
+ | public: | ||
+ | int fcn(int); // nonvirtual function hides D1:: | ||
+ | int fcn(); | ||
+ | void f2(); // overrides virtual f2 from D1 | ||
+ | }; | ||
+ | </ | ||
+ | 上例中,'' | ||
+ | * '' | ||
+ | * '' | ||
+ | 如果对上述的类进行如下的调用: | ||
+ | <code cpp> | ||
+ | Base bobj; D1 d1obj; D2 d2obj; | ||
+ | Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj; | ||
+ | bp1-> | ||
+ | bp2-> | ||
+ | bp3-> | ||
+ | </ | ||
+ | 当使用基类的指针对 '' | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | 当调用 '' | ||
+ | <code cpp> | ||
+ | D1 *d1p = &d1obj; D2 *d2p = &d2obj; | ||
+ | bp2-> | ||
+ | d1p-> | ||
+ | d2p-> | ||
+ | </ | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | 如果调用 '' | ||
+ | <code cpp> | ||
+ | Base *p1 = &d2obj; D1 *p2 = &d2obj; D2 *p3 = &d2obj; | ||
+ | p1-> | ||
+ | p2-> | ||
+ | p3-> | ||
+ | </ | ||
+ | 因为 '' | ||
+ | * '' | ||
+ | * '' | ||
+ | ===重写被重载的函数=== | ||
+ | 在 C++ 中,虚函数也可以被重载。而在派生类中,我们可以对虚函数的重载版本进行重写。重写可以针对不同的重载版本,但需要注意的是,如果希望**所有重载版本的定义对派生类可见**,我们有两个选择: | ||
+ | * 要么重写**所有的**重载版本 | ||
+ | * 要么一个不重写 | ||
+ | 这样会带来一个问题,当只需要重写一部分重载版本时,这个过程会变得很麻烦。\\ \\ | ||
+ | 一种解决方案是使用 '' | ||
+ | 需要注意的是,使用 '' | ||
+ | ====构造函数与拷贝控制==== | ||
+ | 处于继承层级中的类同样存在着拷贝控制。 | ||
+ | ===虚析构函数=== | ||
+ | 继承对拷贝控制最大的一个影响是,**基类中的析构函数通常应该被定义为虚函数**。这样做是因为需要对拥有动态资源的派生类进行准确的析构。也就是说,虚析构函数允许我们使用动态绑定的方式来释放资源。比如之前的例子 '' | ||
+ | <code cpp> | ||
+ | class Quote { | ||
+ | public: | ||
+ | // virtual destructor needed if a base pointer pointing to a derived object is deleted | ||
+ | virtual ~Quote() = default; // dynamic binding for the destructor | ||
+ | }; | ||
+ | </ | ||
+ | 如果此时 '' | ||
+ | <code cpp> | ||
+ | Quote *itemP = new Quote; | ||
+ | delete itemP; | ||
+ | itemP = new Bulk_quote; | ||
+ | delete itemP; | ||
+ | </ | ||
+ | <WRAP center round alert 100%> | ||
+ | 如果基类中,析构函数不被声明为虚函数,那么通过**基类指针** 对派生类资源进行 //delete// 操作是**未定义行为**。 | ||
+ | </ | ||
+ | 需要注意的是,基类的析构函数是三五原则一个重要的例外。基类需要一个内容为空的虚析构函数,而并不意味着需要添加其他拷贝控制的部分。\\ \\ | ||
+ | 这也带来一个影响:编译器不会为定义了析构函数的类合成移动操作。 | ||
+ | ===合成的拷贝控制与继承=== | ||
+ | C++ 中,派生类中的**合成拷贝控制成员**,在处理基类部分的资源时,会调用**基类中对应的拷贝控制成员**来处理。比如之前的 '' | ||
+ | * '' | ||
+ | * '' | ||
+ | * 整个过程类似于一个递归的过程,最内层的基类 '' | ||
+ | |||
+ | \\ {{ : | ||
+ | |||
+ | 该规则同样适用于拷贝成员。比如拷贝构造函数,析构函数等。需要注意的是,该规则被应用的前提是**基类中对应的拷贝控制成员是可访问的(没有被删除的)**,而与成员是否是合成的并没有关系。 | ||
+ | ==基类,派生类和被删除的拷贝控制成员== | ||
+ | C++ 11 中允许拷贝成员被删除。鉴于派生类拷贝控制成员会使用基类的拷贝控制成员,基类中的拷贝控制成员会影响派生类中对应成员的可删除性。总的来说: | ||
+ | * **基类中的**默认构造函数,拷贝构造函数,拷贝赋值运算符以及析构函数被删除无法访问,会导致派生类中对应成员被删除。因为派生类的拷贝成员无法使用基类中的对应成员完成拷贝控制。 | ||
+ | * **基类中的析构函数**无法访问或者被删除,会导致派生类中的**合成构造函数**被删除,因为派生类无法释放其基类部分中申请的资源。 | ||
+ | * 默认情况下,编译器不会合成移动成员。如果使用 '' | ||
+ | 下面的例子中,'' | ||
+ | <code cpp> | ||
+ | class B { | ||
+ | public: | ||
+ | B(); | ||
+ | B(const B&) = delete; | ||
+ | // other members, not including a move constructor | ||
+ | }; | ||
+ | class D : public B { | ||
+ | // no constructors | ||
+ | }; | ||
+ | D d; | ||
+ | D d2(d); // error: D's synthesized copy constructor is deleted | ||
+ | D d3(std:: | ||
+ | </ | ||
+ | 这种情况下,由于 '' | ||
+ | <WRAP center round info 100%> | ||
+ | 通常情况下,如果基类中没有可用的拷贝控制成员,则派生类中也不会有。 | ||
+ | </ | ||
+ | ==移动操作与继承== | ||
+ | 由于多数基类都会定义虚析构函数, | ||
+ | <code cpp> | ||
+ | class Quote { | ||
+ | public: | ||
+ | Quote() = default; | ||
+ | Quote(const Quote&) = default; // memberwise copy | ||
+ | Quote(Quote&& | ||
+ | Quote& operator=(const Quote&) = default; // copy assign | ||
+ | Quote& operator=(Quote&& | ||
+ | virtual ~Quote() = default; | ||
+ | // other members as before | ||
+ | }; | ||
+ | </ | ||
+ | 由于基类中定义了拷贝/ | ||
+ | ===派生类中的拷贝控制成员=== | ||
+ | 派生类中的拷贝控制成员分为两类: | ||
+ | * 需要同时负责处理基类部分与派生类部分的成员:拷贝 / 移动构造函数,拷贝 / 移动赋值运算符 | ||
+ | * 只需要处理所在类的成员:析构函数() | ||
+ | |||
+ | ==定义派生类的拷贝 / 移动构造函数== | ||
+ | 定义派生类的拷贝 / 移动构造函数时,需要显式的委托**基类中对应的拷贝 / 移动构造函数**,以此来初始化派生类对象中的基类部分: | ||
+ | <code cpp> | ||
+ | class Base { /* ... */ } ; | ||
+ | class D: public Base { | ||
+ | public: | ||
+ | // by default, the base class default constructor initializes the base part of an object | ||
+ | // to use the copy or move constructor, | ||
+ | // constructor in the constructor initializer list | ||
+ | D(const D& d): Base(d) | ||
+ | /* initializers for members of D */ { /* ... */ } | ||
+ | D(D&& | ||
+ | /* initializers for members of D */ { /* ... */ } | ||
+ | }; | ||
+ | </ | ||
+ | 被用于初始化的派生类对象 '' | ||
+ | 需要注意的是,这部分不可省略。如果以下面的形式对派生类对象进行拷贝 / 移动构造: | ||
+ | <code cpp> | ||
+ | // probably incorrect definition of the D copy constructor | ||
+ | // base-class part is default initialized, | ||
+ | D(const D& d) /* member initializers, | ||
+ | { /* ... */ } | ||
+ | </ | ||
+ | 这种情况下,'' | ||
+ | * 基类部分由基类的**默认构造函数**初始化 | ||
+ | * 派生类部分由派生类的拷贝 / 移动构造函数初始化 | ||
+ | 这样得到的结果是一个只拷贝了一半的对象,显然是不合理的。 | ||
+ | <WRAP center round important 100%> | ||
+ | 派生类拷贝 / 移动构造函数必须显式的委托基类中对应的构造函数进行基类部分的构造。 | ||
+ | </ | ||
+ | ==派生类的赋值运算符== | ||
+ | 派生类的赋值过程分为两个份: | ||
+ | * 调用基类的赋值运算符 | ||
+ | * 按照赋值运算符的常规设计方法设计派生类中的赋值过程 | ||
+ | <code cpp> | ||
+ | // Base:: | ||
+ | D & | ||
+ | { | ||
+ | Base:: | ||
+ | // assign the members in the derived class, as usual, | ||
+ | // handling self-assignment and freeing existing resources as appropriate | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | 无论基类中的赋值运算符是合成的还是自定义的,都可以通过以 '' | ||
+ | ==派生类的析构函数== | ||
+ | 当析构函数的函数体执行完毕后,类成员会被隐式的**自动**销毁。因此,派生类的析构函数不需要对基类部分进行额外的销毁工作;也就是说派生类的析构函数只负责释放被派生类申请的资源(成员): | ||
+ | <code cpp> | ||
+ | class D: public Base { | ||
+ | public: | ||
+ | // Base::~Base invoked automatically | ||
+ | ~D() { /* do what it takes to clean up derived members | ||
+ | }; | ||
+ | </ | ||
+ | 与一般析构函数的销毁顺序类似,派生类对象的销毁过程也是反向的,**会先运行派生类的析构函数,再运行基类的析构函数**。 | ||
+ | ==在构造函数和析构函数中调用虚函数== | ||
+ | 在对派生类对象进行构建与销毁中,基类部分与派生类部分是分别进行的;而在此期间,被构建的对象属于一种不完全的状态。以构造函数为例,其基类初始化完毕,但派生类还没有初始化。 | ||
+ | \\ \\ | ||
+ | 由于这种不完全的状态并不满足动态绑定的要求,因此编译器将该处于不完全状态的对象的类型,视作与其构造函数一个类型。也就是说,派生类对象在构造过程中,如果没有完成派生类成员的初始化,则此时该对象会被视作基类类型的对象。 | ||
+ | \\ \\ | ||
+ | 而当我们在派生类成员被构造之前尝试调用虚函数;具体的来说,在基类的构造函数中调用虚函数,会导致该调用的绑定不再是动态绑定,而直接会绑定到与构造函数类型相同的对象上(基类类型上)。此时对虚函数的调用,实际上是在**调用基类中的虚函数版本**。来看下面的例子: | ||
+ | <code cpp> | ||
+ | class Dog | ||
+ | { | ||
+ | public: | ||
+ | Dog() { | ||
+ | cout<< | ||
+ | bark() ; | ||
+ | } | ||
+ | ~Dog() { bark(); } | ||
+ | virtual void bark() { cout<< | ||
+ | void seeCat() { bark(); } | ||
+ | }; | ||
+ | |||
+ | class Yellowdog : public Dog | ||
+ | { | ||
+ | public: | ||
+ | Yellowdog() { cout<< | ||
+ | void bark() { cout<< | ||
+ | }; | ||
+ | |||
+ | int main() | ||
+ | { | ||
+ | Yellowdog d; | ||
+ | d.seeCat(); | ||
+ | } | ||
+ | </ | ||
+ | 基类的构造函数 '' | ||
+ | < | ||
+ | Constructor called //Dog::cstr | ||
+ | Virtual method called // | ||
+ | Derived class Constructor called // Yellowdog:: | ||
+ | Derived class Virtual method called // | ||
+ | Virtual method called // | ||
+ | </ | ||
+ | 这种行为也很好解释:如果允许未完成对象进行动态绑定,则实际上是允许对未初始化成员的访问;这种访问很可能会导致程序的崩溃。\\ \\ | ||
+ | [[https:// | ||
+ | ==习题中的问题== | ||
+ | < | ||
+ | 比如下面的: | ||
+ | <code cpp> | ||
+ | Disc_quote(Disc_quote&& | ||
+ | </ | ||
+ | 第一眼看上去很奇怪,为什么参数命名是右值,却仍然要使用 '' | ||
+ | 之所以有这样的机制,是为了**防止对象被移动两次**。\\ \\ | ||
+ | [[https:// | ||
+ | \\ \\ <color # | ||
+ | 本节习题中,抽象类中的析构函数应该被定义为纯虚函数。如果只在类中写: | ||
+ | <code cpp> | ||
+ | virtual ~Disc_quote() = 0; | ||
+ | </ | ||
+ | 而不进行类外定义的话,会出现如下错误: | ||
+ | <code bash> | ||
+ | undefined reference to `Disc_quote:: | ||
+ | </ | ||
+ | 这是因为,即便抽象基类不需要实体化,也需要提供一个析构函数供派生类销毁属于该抽象基类中的资源。\\ \\ | ||
+ | [[https:// | ||
+ | ===构造函数的继承=== | ||
+ | C++11 中提供了让派生类继承**直接基类**的构造函数的方法,格式如下: | ||
+ | <code cpp> | ||
+ | using derived(parms) : base(args) { } | ||
+ | </ | ||
+ | 该方法使用 '' | ||
+ | <code cpp> | ||
+ | class Bulk_quote : public Disc_quote { | ||
+ | public: | ||
+ | using Disc_quote:: | ||
+ | double net_price(std:: | ||
+ | }; | ||
+ | </ | ||
+ | 如果派生类拥有属于自己的成员,那么这些成员会进行**默认初始化**。 | ||
+ | <WRAP center round box 100%> | ||
+ | 通过作业中的实际测试,如果只是单纯的使用继承的构造函数,是无法对这些派生类的成员进行初始值设定的。 | ||
+ | </ | ||
+ | |||
+ | 该方法实际等同于: | ||
+ | <code cpp> | ||
+ | Bulk_quote(const std:: | ||
+ | std::size_t qty, double disc): | ||
+ | Disc_quote(book, | ||
+ | </ | ||
+ | ==继承构造函数的特性== | ||
+ | 在继承的过程中: | ||
+ | * 构造函数的访问控制级别不会改变;比如私有的构造函数继承后依然是私有的 | ||
+ | * 构造函数的继承不影响 '' | ||
+ | 默认情况下,构造函数的继承会将基类中**所有的**构造函数继承到派生类中。但有几个例外: | ||
+ | - 当派生类中定义的构造函数与基类构造函数具有相同的参数列表,则此时该基类构造函数不会被继承,而是会被派生类中的对应构造函数取代。 | ||
+ | - 拷贝 / 移动构造函数,默认构造函数均不会被继承。 | ||
+ | ==继承构造函数中有默认参数的情况== | ||
+ | **基类构造函数中的默认参数不会被继承。**这种情况下,该基类构造函数可能会被分为多个构造函数分别继承。总的来说可以分为两部分: | ||
+ | * 参数与基类个数相同的构造函数(可以覆盖所有的默认值的构造函数) | ||
+ | * 参数中不带有某些带默认值参数的构造函数 | ||
+ | 下面是一个测试的例子: | ||
+ | <code cpp> | ||
+ | struct Base | ||
+ | { | ||
+ | Base(int a, int b = 1, int c = 2):x(a), y(b), z(c) {} | ||
+ | int x {0}; | ||
+ | int y {0}; | ||
+ | int z {0}; | ||
+ | }; | ||
+ | |||
+ | struct D :public Base | ||
+ | { | ||
+ | using Base::Base; | ||
+ | void print() {std::cout << x << " " << y << " " << z << std::endl; } | ||
+ | }; | ||
+ | |||
+ | int main(int argc, char const *argv[]) | ||
+ | { | ||
+ | D d1(1, 2, 3); | ||
+ | d1.print(); | ||
+ | D d2(4,5); | ||
+ | d2.print(); | ||
+ | D d3(6); | ||
+ | d3.print(); | ||
+ | </ | ||
+ | 输出为: | ||
+ | <code cpp> | ||
+ | 1 2 3 | ||
+ | 4 5 2 | ||
+ | 6 1 2 | ||
+ | </ | ||
+ | 当继承的构造函数中参数数量小于基类构造函数时,就会有带默认值的参数被省略。而该默认值会作为被构造对象的初始值。被省略的参数都是带有默认值的;被省略的方向是从右到左。 | ||
+ | ====容器与继承==== | ||
+ | 基类的类型与派生类的类型不同,而容器只允许指定单个类型。因此,如果希望将存在继承关系的对象**同时**存入某个容器时,必须采用**间接**访问的办法。如果直接进行存储,那么将会导致派生类对象被转换为基类对象,且派生类部分的成员会丢失。比如下面的例子: | ||
+ | <code cpp> | ||
+ | vector< | ||
+ | basket.push_back(Quote(" | ||
+ | // ok, but copies only the Quote part of the object into basket | ||
+ | basket.push_back(Bulk_quote(" | ||
+ | // calls version defined by Quote, prints 750, i.e., 15 * $50 | ||
+ | cout << basket.back().net_price(15) << endl; | ||
+ | </ | ||
+ | 当 '' | ||
+ | ===在容器中使用智能指针代替对象=== | ||
+ | 我们可以通过**间接的方式**(指针),也就是定义一个存储智能指针的容器(通常)来解决与继承相关的对象的存储问题。由于多态的存在,这些指针指向对象的动态类型可以是基类,也可以是派生类: | ||
+ | <code cpp> | ||
+ | vector< | ||
+ | basket.push_back(make_shared< | ||
+ | basket.push_back( | ||
+ | make_shared< | ||
+ | // calls the version defined by Quote; prints 562.5, i.e., 15 * $50 less the discount | ||
+ | cout << basket.back()-> | ||
+ | </ | ||
+ | 当 '' | ||
+ | ===实例:Basket 类=== | ||
+ | 由上一节的内容得知,使用指针作为管理容器的中对象的工具是非常方便的事情。本节中,我们可以通过利用 // | ||
+ | \\ \\ | ||
+ | ==容器的选择== | ||
+ | 在这个实例中,我们选择 // | ||
+ | * 每一本书都可以对应多个 '' | ||
+ | * // | ||
+ | 该容器的实现如下: | ||
+ | <code cpp> | ||
+ | // function to compare shared_ptrs needed by the multiset member | ||
+ | static bool compare(const std:: | ||
+ | const std:: | ||
+ | { return lhs-> | ||
+ | | ||
+ | // multiset to hold multiple quotes, ordered by the compare member | ||
+ | std:: | ||
+ | items{compare}; | ||
+ | </ | ||
+ | // | ||
+ | <WRAP center round important 100%> | ||
+ | 由于静态成员函数的可见范围是 file scope, 因此**其定义必须处于其声明所在文件中**。\\ | ||
+ | [[https:// | ||
+ | </ | ||
+ | |||
+ | 再回过来看 '' | ||
+ | <code cpp> | ||
+ | explicit multiset( const Compare& | ||
+ | const Allocator& | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | 很显然,'' | ||
+ | * [[https:// | ||
+ | * [[https:// | ||
+ | </ | ||
+ | |||
+ | 基本容器选择好之后,我们需要为 '' | ||
+ | * 添加 '' | ||
+ | * 计算并打印每本书的销售额的功能 '' | ||
+ | ==add_items() 成员的实现== | ||
+ | 由于 '' | ||
+ | <code cpp> | ||
+ | void add_item(const std:: | ||
+ | { items.insert(sale); | ||
+ | </ | ||
+ | 在使用的时候,只需要使用对象调用该函数即可。该函数的初始值可以通过 '' | ||
+ | <code cpp> | ||
+ | Basket bsk; | ||
+ | bsk.add_item(make_shared< | ||
+ | bsk.add_item(make_shared< | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | 另外一个小关注点,作者在传递 shared_ptr 的时候使用了**引用**的方式。可以看看相关的讨论:\\ [[https:// | ||
+ | </ | ||
+ | |||
+ | ==total_receipt() 成员的实现== | ||
+ | '' | ||
+ | * 遍历整个 '' | ||
+ | * 对同一本书籍的销售额进行累加并打印 | ||
+ | 实现如下: | ||
+ | <code cpp> | ||
+ | double Basket:: | ||
+ | { | ||
+ | double sum = 0.0; // holds the running total | ||
+ | // iter refers to the first element in a batch of elements with the same ISBN | ||
+ | // upper_bound returns an iterator to the element just past the end of that batch | ||
+ | for (auto iter = items.cbegin(); | ||
+ | iter != items.cend(); | ||
+ | iter = items.upper_bound(*iter)) { | ||
+ | // we know there' | ||
+ | // print the line item for this book | ||
+ | sum += print_total(os, | ||
+ | } | ||
+ | os << "Total Sale: " << sum << endl; // print the final overall total | ||
+ | return sum; | ||
+ | } | ||
+ | </ | ||
+ | 这里使用了成员 '' | ||
+ | <code cpp> | ||
+ | iter = items.upper_bound(*iter) | ||
+ | </ | ||
+ | 当当前循环结束之后,'' | ||
+ | \\ \\ | ||
+ | 接下来我们使用 '' | ||
+ | <code cpp> | ||
+ | sum += print_total(os, | ||
+ | </ | ||
+ | 有两点要注意: | ||
+ | * '' | ||
+ | * '' | ||
+ | ==改进需求:隐藏智能指针== | ||
+ | 之前的实现中,'' | ||
+ | <code cpp> | ||
+ | sk.add_item(make_shared< | ||
+ | bsk.add_item(make_shared< | ||
+ | </ | ||
+ | 我们希望将其改造为接受 '' | ||
+ | <code cpp> | ||
+ | void add_item(const Quote& sale); | ||
+ | void add_item(Quote&& | ||
+ | </ | ||
+ | 但这样做会带来一个问题。由于 '' | ||
+ | ==改进策略:模拟虚拷贝== | ||
+ | 由于 '' | ||
+ | <code cpp> | ||
+ | class Quote { | ||
+ | public: | ||
+ | // virtual function to return a dynamically allocated copy of itself | ||
+ | // these members use reference qualifiers; see §13.6.3 (p. 546) | ||
+ | virtual Quote* clone() const & {return new Quote(*this); | ||
+ | virtual Quote* clone() && | ||
+ | {return new Quote(std:: | ||
+ | // other members as before | ||
+ | }; | ||
+ | class Bulk_quote : public Quote { | ||
+ | Bulk_quote* clone() const & {return new Bulk_quote(*this); | ||
+ | Bulk_quote* clone() && | ||
+ | | ||
+ | // other members as before | ||
+ | }; | ||
+ | </ | ||
+ | 而通过重写 '' | ||
+ | <code cpp> | ||
+ | class Basket { | ||
+ | public: | ||
+ | void add_item(const Quote& sale) // copy the given object | ||
+ | { items.insert(std:: | ||
+ | void add_item(Quote&& | ||
+ | { items.insert( | ||
+ | //named rvalue is treated as lvaule, so std::move here. | ||
+ | std:: | ||
+ | // other members as before | ||
+ | }; | ||
+ | </ | ||
+ | '' | ||
+ | 除此之外,还注意的是,为了与 '' | ||
+ | ====再探实例:TextQueries==== | ||
+ | 相较于之前使用单个关键词进行查询的 // | ||
+ | <code bash> | ||
+ | # ~Negation operator, yield lines that don’t match the query | ||
+ | ~(Alice) | ||
+ | # | Or operator, return lines matching either of two queries | ||
+ | hair | Alice | ||
+ | # & and operator, return lines matching both queries | ||
+ | hair & Alice | ||
+ | </ | ||
+ | 输出与之前的实例相同,输出结果为关键词出现的次数,以及所在行的打印: | ||
+ | <code bash> | ||
+ | Executing Query for: ((fiery & bird) | wind) | ||
+ | ((fiery & bird) | wind) occurs 3 times | ||
+ | (line 2) Her Daddy says when the wind blows | ||
+ | (line 4) like a fiery bird in flight. | ||
+ | (line 5) A beautiful fiery bird, he tells her, | ||
+ | </ | ||
+ | ===解决方案:面向对象=== | ||
+ | 通过仔细观察,我们发现这些运算实际上都可以以对象的形式来呈现(感觉有点像 functor)。具体如下: | ||
+ | <code cpp> | ||
+ | WordQuery // Daddy | ||
+ | NotQuery | ||
+ | OrQuery | ||
+ | AndQuery | ||
+ | </ | ||
+ | 以对象的方式来表示运算的过程意味着: | ||
+ | * 查询的**关键次表达式**将以对象的形式进行构造 | ||
+ | * 根据查询关键词的结果,以表达式的意义来对最终结果进行组合 | ||
+ | 从实现上来讲,有两个函数需要动态绑定: | ||
+ | * '' | ||
+ | * '' | ||
+ | \\ \\ {{ : | ||
+ | <WRAP center round box 100%> | ||
+ | recover: // | ||
+ | </ | ||
+ | 而最后的打印结果,将通过调用 // | ||
+ | ==~Query() 的特殊解决思路== | ||
+ | '' | ||
+ | ==抽象类的设计== | ||
+ | 通过上面的信息可以发现,这些用于表示关键词组合的表达式对象可以完全表现为,以**单个关键词为基础的查询输出结果**(行号 set)的**组合**关系。具体的来说: | ||
+ | * 对对应组合对象中所有的单个关键词均进行一次查询 | ||
+ | * 将得到的结果使用重写的 '' | ||
+ | 根据这样的思路,我们定义一个抽象类 '' | ||
+ | 需要注意的是,逻辑运算的算子数量要求存在不同。对于要求双算子的两种运算 ''&'' | ||
+ | \\ \\ < | ||
+ | <WRAP center round box 100%> | ||
+ | **关键概念:组合与继承**\\ \\ | ||
+ | 在类设计中有两个关键的关系:**是(// | ||
+ | </ | ||
+ | ==使用接口类隐藏类的层级== | ||
+ | <WRAP center round box 100%> | ||
+ | 接口类是一个很重要的概念(也就是我们经常提到的句柄类(// | ||
+ | </ | ||
+ | 在应用中,查询是以表达式的形式出现的。如果在表达式中直接使用已有类类型的对象作为算子,显然是很不方便的。为解决这个问题,我们希望用一个接口类(// | ||
+ | * 指向 '' | ||
+ | * '' | ||
+ | * '' | ||
+ | 由于我们希望使用者只基于 '' | ||
+ | * ''&'' | ||
+ | * '' | ||
+ | * '' | ||
+ | |||
+ | 除此之外,单个关键词的对象类型 '' | ||
+ | ==表达式是如何以对象的形式表现的== | ||
+ | 根据之前的内容,我们的表达式的最终结果,实质上是**不同单个关键词查询结果的逻辑运算**。因此,一个查询表达式的执行,实际上是在以 '' | ||
+ | \\ \\ {{ : | ||
+ | 而具体执行的过程,也是按照这个树的顺序进行调用,直到达到最基本的单位时得出查询结果后,再依次返回上级,进行最后的结果的运算(类似递归的概念)。 | ||
+ | ===Query_Base & Query 的实现=== | ||
+ | 几个要点: | ||
+ | * '' | ||
+ | * 上述两个函数的参数和返回类型都必须匹配 '' | ||
+ | * 析构函数需要是虚函数。由于该虚函数实际上是被派生类使用,因此放在 '' | ||
+ | * 需要添加 '' | ||
+ | <code cpp> | ||
+ | // abstract class acts as a base class for concrete query types; all members are private | ||
+ | class Query_base { | ||
+ | friend class Query; | ||
+ | protected: | ||
+ | using line_no = TextQuery:: | ||
+ | virtual ~Query_base() = default; | ||
+ | private: | ||
+ | // eval returns the QueryResult that matches this Query | ||
+ | virtual QueryResult eval(const TextQuery& | ||
+ | // rep is a string representation of the query | ||
+ | virtual std::string rep() const = 0; | ||
+ | }; | ||
+ | </ | ||
+ | ==Query 类== | ||
+ | 首先,由于 '' | ||
+ | <code cpp> | ||
+ | private: | ||
+ | Query(std:: | ||
+ | std:: | ||
+ | </ | ||
+ | 其次,由于逻辑运算符需要借助 '' | ||
+ | 除此之外,'' | ||
+ | <code cpp> | ||
+ | QueryResult eval(const TextQuery &t) const { return q-> | ||
+ | std::string rep() const { return q-> | ||
+ | </ | ||
+ | 大致的实现如下: | ||
+ | <code cpp> | ||
+ | // interface class to manage the Query_base inheritance hierarchy | ||
+ | class Query { | ||
+ | // these operators need access to the shared_ptr constructor | ||
+ | friend Query operator~(const Query &); | ||
+ | friend Query operator|(const Query&, const Query& | ||
+ | friend Query operator& | ||
+ | public: | ||
+ | Query(const std:: | ||
+ | // interface functions: call the corresponding Query_base operations | ||
+ | QueryResult eval(const TextQuery &t) const | ||
+ | { return q-> | ||
+ | std::string rep() const { return q-> | ||
+ | private: | ||
+ | Query(std:: | ||
+ | std:: | ||
+ | }; | ||
+ | </ | ||
+ | 需要注意的是,任何需要用到之后类类型具体定义的函数,都只能在次做出**声明**,比如构造 '' | ||
+ | ==Query 的输出== | ||
+ | '' | ||
+ | <code cpp> | ||
+ | std:: | ||
+ | operator<< | ||
+ | { | ||
+ | // Query::rep makes a virtual call through its Query_base pointer to rep() | ||
+ | return os << query.rep(); | ||
+ | } | ||
+ | </ | ||
+ | ===其他派生类的实现=== | ||
+ | 之前提到,派生类对象是通过 '' | ||
+ | 除此之外,派生类也需要自身的 '' | ||
+ | ==WordQuery 的实现== | ||
+ | '' | ||
+ | * 存储关键词,这里使用私有 string 成员 | ||
+ | * 根据关键词初始化该类,这里使用 string 作为初始值进行构造 | ||
+ | * 打印相关信息;由于单个的关键次查询可以直接调用 '' | ||
+ | <code cpp> | ||
+ | class WordQuery: public Query_base { | ||
+ | friend class Query; // Query uses the WordQuery constructor | ||
+ | WordQuery(const std::string &s): query_word(s) { } | ||
+ | // concrete class: WordQuery defines all inherited pure virtual functions | ||
+ | QueryResult eval(const TextQuery &t) const | ||
+ | { return t.query(query_word); | ||
+ | std::string rep() const { return query_word; } | ||
+ | std::string query_word; | ||
+ | }; | ||
+ | </ | ||
+ | 当 '' | ||
+ | <code cpp> | ||
+ | inline Query:: | ||
+ | </ | ||
+ | ==NotQuery 和 ~ 运算== | ||
+ | '' | ||
+ | * “指针的存放”:本来应该有一个指向 '' | ||
+ | * '' | ||
+ | * '' | ||
+ | - 该实现使用了 '' | ||
+ | - 而 '' | ||
+ | - 之后在添加额外信息辅助理解即可 | ||
+ | <code cpp> | ||
+ | class NotQuery: public Query_base { | ||
+ | friend Query operator~(const Query &); | ||
+ | NotQuery(const Query &q): query(q) { } | ||
+ | // concrete class: NotQuery defines all inherited pure virtual functions | ||
+ | std::string rep() const {return " | ||
+ | QueryResult eval(const TextQuery& | ||
+ | Query query; | ||
+ | }; | ||
+ | </ | ||
+ | 其对应的运算符重载的实现如下: | ||
+ | <code cpp> | ||
+ | inline Query operator~(const Query & | ||
+ | { | ||
+ | return std:: | ||
+ | } | ||
+ | </ | ||
+ | ==抽象类 BinaryQuery 的实现== | ||
+ | * 除开 '' | ||
+ | * 由于 '' | ||
+ | * '' | ||
+ | * 由于运算的意义不同,'' | ||
+ | <code cpp> | ||
+ | class BinaryQuery: | ||
+ | protected: | ||
+ | BinaryQuery(const Query &l, const Query &r, std::string s): | ||
+ | lhs(l), rhs(r), opSym(s) { } | ||
+ | // abstract class: BinaryQuery doesn' | ||
+ | std::string rep() const { return " | ||
+ | + opSym + " " | ||
+ | + rhs.rep() + " | ||
+ | Query lhs, rhs; // right- and left-hand operands | ||
+ | std::string opSym; // name of the operator | ||
+ | }; | ||
+ | </ | ||
+ | ==AndQuery 与 OrQuery== | ||
+ | - 这两个类均继承自 '' | ||
+ | - 这两个类是具体类,需要分别实现自身的 '' | ||
+ | - '' | ||
+ | <code cpp> | ||
+ | class AndQuery: public BinaryQuery { | ||
+ | friend Query operator& | ||
+ | AndQuery(const Query &left, const Query & | ||
+ | BinaryQuery(left, | ||
+ | // concrete class: AndQuery inherits rep and defines the remaining pure virtual | ||
+ | QueryResult eval(const TextQuery& | ||
+ | }; | ||
+ | inline Query operator& | ||
+ | { | ||
+ | return std:: | ||
+ | } | ||
+ | |||
+ | class OrQuery: public BinaryQuery { | ||
+ | friend Query operator|(const Query&, const Query& | ||
+ | OrQuery(const Query &left, const Query & | ||
+ | BinaryQuery(left, | ||
+ | QueryResult eval(const TextQuery& | ||
+ | }; | ||
+ | inline Query operator|(const Query &lhs, const Query &rhs) | ||
+ | { | ||
+ | return std:: | ||
+ | } | ||
+ | </ | ||
+ | ===eval() 函数的实现=== | ||
+ | '' | ||
+ | ==OrQuery:: | ||
+ | '' | ||
+ | <code cpp> | ||
+ | // returns the union of its operands' | ||
+ | QueryResult | ||
+ | OrQuery:: | ||
+ | { | ||
+ | // virtual calls through the Query members, lhs and rhs | ||
+ | // the calls to eval return the QueryResult for each operand | ||
+ | auto right = rhs.eval(text), | ||
+ | // copy the line numbers from the left-hand operand into the result set | ||
+ | auto ret_lines = | ||
+ | | ||
+ | // insert lines from the right-hand operand | ||
+ | ret_lines-> | ||
+ | // return the new QueryResult representing the union of lhs and rhs | ||
+ | return QueryResult(rep(), | ||
+ | </ | ||
+ | ==AndQuery:: | ||
+ | '' | ||
+ | <code cpp> | ||
+ | // returns the intersection of its operands' | ||
+ | QueryResult | ||
+ | AndQuery:: | ||
+ | { | ||
+ | // virtual calls through the Query operands to get result sets for the operands | ||
+ | auto left = lhs.eval(text), | ||
+ | // set to hold the intersection of left and right | ||
+ | auto ret_lines = make_shared< | ||
+ | // writes the intersection of two ranges to a destination iterator | ||
+ | // destination iterator in this call adds elements to ret | ||
+ | set_intersection(left.begin(), | ||
+ | | ||
+ | | ||
+ | return QueryResult(rep(), | ||
+ | } | ||
+ | </ | ||
+ | ==NotQuery:: | ||
+ | '' | ||
+ | - 按照关键词查询,得到结果行 set | ||
+ | - 读取整个字典的总行数 | ||
+ | - 以字典的总行数作为循环次数,在每一个循环中检查结果行 set 中的行号是否在字典中 | ||
+ | - 如果不在,则证明当前行没有查询的关键词,将该行行号插入到最后的结果 set('' | ||
+ | <WRAP center round todo 100%> | ||
+ | 书上的算法应该是某种字符串比较的算法,待研究。 | ||
+ | </ | ||
+ | <code cpp> | ||
+ | // returns the lines not in its operand' | ||
+ | QueryResult | ||
+ | NotQuery:: | ||
+ | { | ||
+ | // virtual call to eval through the Query operand | ||
+ | auto result = query.eval(text); | ||
+ | // start out with an empty result set | ||
+ | auto ret_lines = make_shared< | ||
+ | // we have to iterate through the lines on which our operand appears | ||
+ | auto beg = result.begin(), | ||
+ | // for each line in the input file, if that line is not in result, | ||
+ | // add that line number to ret_lines | ||
+ | auto sz = result.get_file()-> | ||
+ | for (size_t n = 0; n != sz; ++n) { | ||
+ | // if we haven' | ||
+ | // check whether this line is present | ||
+ | if (beg == end || *beg != n) | ||
+ | ret_lines-> | ||
+ | else if (beg != end) | ||
+ | ++beg; // otherwise get the next line number in result if there is one | ||
+ | } | ||
+ | return QueryResult(rep(), | ||
+ | } | ||
+ | </ |