Skip to content

面向对象编程基础

本课程入选教育部产学合作协同育人项目 课程主页:http://cpp.njuer.org 课程老师:陈明 http://cv.mchen.org

ppt和代码下载地址
git clone https://gitee.com/cpp-njuer-org/book

第15章

面向对象程序设计

面向对象程序设计基于三个基本概念数据抽象继承和动态绑定

继承和动态绑定对程序的编写有两方面的影响
- 可以更容易地定义与其他类相似但不完全相同的新类
- 在使用这些彼此相似的类编写程序时我们可以在一定程度上忽略掉它们的区别

在很多程序中都存在着一些相互关联但是有细微差别的概念
面向对象的程序设计OOP适用于这类应用

OOP:概述

面向对象程序设计object-oriented programming的核心思想是
- 数据抽象继承和动态绑定

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

继承

通过继承inheritance联系在一起的类构成一种层次关系
通常在层次关系的根部有一个基类base class),
其他类则直接或间接地从基类继承而来这些继承得到的类称为派生类derived class)。
基类负责定义在层次关系中所有类共同拥有的成员而每个派生类定义各自特有的成员

继承

书店中不同书籍的定价策略可能不同
- 有的书籍按原价销售有的则打折销售
- 有时我们给那些购买书籍超过一定数量的顾客打折
- 另一些时候则只对前多少本销售的书籍打折之后就调回原价

对定价策略建模我们首先定义一个名为Quote的类并将它作为层次关系中的基类
Quote的对象表示按原价销售的书籍
- Quote派生出另一个名为Bulk_quote的类它表示可以打折销售的书籍
这些类将包含下面的两个成员函数
- isbn ),返回书籍的ISBN编号
  该操作不涉及派生类的特殊性因此只定义在Quote类中
- net_pricesize_t),返回书籍的实际销售价格前提是用户购买该书的数量达到一定标准
  这个操作显然是类型相关的Quote和Bulk_quote都应该包含该函数

继承

在C++语言中基类将类型相关的函数与派生类不做改变直接继承的函数区分对待
对于某些函数基类希望它的派生类各自定义适合自身的版本
- 此时基类就将这些函数声明成虚函数virtual function

//因此,我们可以将Quote类编写成:
class Quote {
public:
    std::string isbn() const;
    virtual double net_price(std::size_t n) const;
};

继承

派生类必须通过使用类派生列表class derivation list
明确指出它是从哪个哪些基类继承而来的
类派生列表的形式是
- 首先是一个冒号
- 后面紧跟以逗号分隔的基类列表
  其中每个基类前面可以有访问说明符
class Bulk_quote : public Quote { // Bulk_quote inherits from Quote
public:
    double net_price(std::size_t) const override;
};

因为Bulk_quote在它的派生列表中使用了public关键字
因此我们完全可以把Bulk_quote的对象当成Quote的对象来使用

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

动态绑定

通过使用动态绑定dynamic binding),
我们能用同一段代码分别处理Quote和Bulk_quote的对象
- 例如当要购买的书籍和购买的数量都已知时下面的函数负责打印总的费用
// calculate and print the price for the given number of copies, applying any discounts
//返回调用net_price()的结果,并将该结果连同调用isbn()的结果一起打印出来。
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::net_price or Bulk_quote::net_price
    double ret = item.net_price(n);
    os << "ISBN: " << item.isbn() // calls Quote::isbn
       << " # sold: " << n << " total due: " << ret << endl;
     return ret;
}

动态绑定

double print_total(ostream &os,const Quote &item, size_t n){
    double ret = item.net_price(n);
    //...
}
两个有意思的结论
- 因为函数print_total的item形参是基类Quote的一个引用
  我们既能使用基类Quote的对象调用该函数也能使用派生类Bulk_quote的对象调用它
- 又因为print_total是使用引用类型调用net_price函数的
  实际传入print_total的对象类型将决定到底执行net_price的哪个版本

动态绑定

// basic has type Quote; bulk has type Bulk_quote
print_total(cout, basic, 20); //  calls Quote version of net_price
print_total(cout, bulk, 20);  //  calls Bulk_quote version of net_price
//第一条调用句将Quote对象传入print_total,因此当print_total调用net_price时,
//执行的是Quote的版本;在第二条调用语句中,实参的类型是Bulk_quote,因此执行的
//是Bulk_quote的版本(计算打折信息)。
因为在上述过程中函数的运行版本由实参决定即在运行时选择函数的版本
所以动态绑定有时又被称为运行时绑定run-time binding)。

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

定义基类和派生类

定义基类和派生类的方式在很多方面都与我们已知的定义其他类的方式类似
但是也有一些不同之处

定义基类

class Quote {
public:
    Quote() = default;  // = default see § 7.1.4 
    Quote(const std::string &book, double sales_price):
                     bookNo(book), price(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::size_t n) const
               { 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
};
新增的部分是在net_price函数和析构函数之前增加的virtual关键字以及最后的
protected访问说明符作为继承关系中根节点的类通常都会定义一个虚析构函数

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

成员函数与继承

派生类可以继承其基类的成员然而当遇到如net_price这样与类型相关的操作时
派生类必须对其重新定义
- 派生类需要对这些操作提供自己的新定义以覆盖override从基类继承而来的旧定义

在C++语言中基类必须将它的两种成员函数区分开来
- 一种是基类希望其派生类进行覆盖的函数
    -   基类通常将其定义为虚函数virtual)。
        当我们使用指针或引用调用虚函数时该调用将被动态绑定
        根据引用或指针所绑定的对象类型不同
        该调用可能执行基类的版本也可能执行某个派生类的版本
- 另一种是基类希望派生类直接继承而不要改变的函数

成员函数与继承

基类通过在其成员函数的声明语句之前加上关键字virtual使得该函数执行动态绑定
任何构造函数之外的非静态函数都可以是虚函数
关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义
如果基类把一个函数声明成虚函数则该函数在派生类中隐式地也是虚函数

成员函数如果没被声明为虚函数则其解析过程发生在编译时而非运行时
- 对于isbn成员来说这正是我们希望看到的结果
  isbn函数的执行与派生类的细节无关不管作用于Quote对象还是Bulk_quote对象
  isbn函数的行为都一样
  在我们的继承层次关系中只有一个isbn函数
  因此也就不存在调用isbn()时到底执行哪个版本的疑问

访问控制与继承

派生类可以继承定义在基类中的成员
但是派生类的成员函数不一定有权访问从基类继承而来的成员
和其他使用基类的代码一样派生类能访问公有成员而不能访问私有成员

受保护的protected访问运算符
- 在某些时候基类中还有这样一种成员基类希望它的派生类有权访问该成员
  同时禁止其他用户访问

我们的Quote类希望它的派生类定义各自的net_price函数
因此派生类需要访问Quote的price成员此时我们将price定义成受保护的
与之相反派生类访问bookNo成员的方式与其他用户是一样的都是通过调用isbn函数
因此bookNo被定义成私有的即使是Quote派生出来的类也不能直接访问它

练习

什么是虚成员

对于某些函数基类希望它的派生类各自定义适合自身的版本
此时基类就将这些函数声明成虚函数

练习

protected 访问说明符与 private 有何区别

* protected  基类和和其派生类还有友元可以访问
* private  只有基类本身和友元可以访问

练习

定义你自己的 Quote 类和 print_total 函数
#include <string>
class Quote
{
public:
    Quote() = default;
    Quote(const std::string &b, double p) :
        bookNo(b), price(p){}
    std::string isbn() const { return bookNo; }
    virtual double net_price(std::size_t n) const { return n * price; }
    virtual ~Quote() = default;
private:
    std::string bookNo;

protected:
    double  price = 0.0;

};

练习

主函数
#include "ex_15_3.h"
#include <iostream>
#include <string>
#include <map>
#include <functional>
double print_total(std::ostream& os, const Quote& item, size_t n);

int main()
{
    return 0;
}
double print_total(std::ostream &os, const Quote &item, size_t n)
{
    double ret = item.net_price(n);
    os << "ISBN:" << item.isbn()
        << "# sold: " << n << " total due: " << ret << std::endl;
    return ret;
}

定义派生类

派生类必须通过使用类派生列表class derivationlist明确指出
它是从哪个哪些基类继承而来的类派生列表的形式是
首先是一个冒号后面紧跟以逗号分隔的基类列表
其中每个基类前面可以有以下三种访问说明符中的一个
- public
- protected
- private

定义派生类

派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明
//因此,我们的Bulk_quote类必须包含一个net_price成员:
class Bulk_quote : public Quote { // Bulk_quote inherits from Quote
    Bulk_quote() = default;
    Bulk_quote(const std::string&, double, std::size_t,double);
    // overrides the base version in order to implement the bulk purchase discount policy
    double net_price(std::size_t) const override;
private:
    std::size_t min_qty = 0; // minimum purchase for the discount to apply
    double discount = 0.0;   // fractional discount to apply
};
//Bulk_quote类从它的基类Quote那里继承了isbn函数和bookNo、price等数据成员。
//此外,它还定义了net_price的新版本,同时拥有两个新增加的数据成员min_qty
//和discount。这两个成员分别用于说明享受折扣所需购买的最低数量以及一旦该
//数量达到之后具体的折扣信息。

定义派生类

访问说明符的作用是控制派生类从基类继承而来的成员是否对派生类的用户可见
如果一个派生是公有的则基类的公有成员也是派生类接口的组成部分
此外我们能将公有派生类型的对象绑定到基类的引用或指针上

//因为我们在派生列表中使用了public,所以Bulk_quote的接口隐式地包含isbn函数,
//同时在任何需要Quote的引用或指针的地方我们都能使用Bulk_quote的对象。
大多数类都只继承自一个类这种形式的继承被称作单继承

派生类中的虚函数

派生类经常但不总是覆盖它继承的虚函数
如果派生类没有覆盖其基类中的某个虚函数
则该虚函数的行为类似于其他的普通成员
派生类会直接继承其在基类中的版本

派生类可以在它覆盖的函数前使用virtual关键字但不是非得这么做
C++11新标准允许派生类显式地注明它使用某个成员函数覆盖了它继承的虚函数
- 具体做法是在形参列表后面或者在const成员函数的const关键字后面
  或者在引用成员函数的引用限定符后面添加一个关键字override

派生类对象及派生类向基类的类型转换

一个派生类对象包含多个组成部分
- 一个含有派生类自己定义的非静态成员的子对象
- 以及一个与该派生类继承的基类对应的子对象
  如果有多个基类那么这样的子对象也有多个

因此一个Bulk_quote对象将包含四个数据元素
- 它从Quote继承而来的bookNo和price数据成员
  以及Bulk_quote自己定义的min_qty和discount成员

派生类对象及派生类向基类的类型转换

C++标准并没有明确规定派生类的对象在内存中如何分布
但是我们可以认为Bulk_quote的对象包含如图15.1所示的两部分

因为在派生类对象中含有与其基类对应的组成部分
所以我们能把派生类的对象当成基类对象来使用
而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分上

Quote item;        //  object of base type
Bulk_quote bulk;   //  object of derived type
Quote *p = &item;  //  p points to a Quote object
p = &bulk;         //  p points to the Quote part of bulk
Quote &r = bulk;   //  r bound to the Quote part of bulk
这种转换通常称为派生类到基类的derived-to-base类型转换
和其他类型转换一样编译器会隐式地执行派生类到基类的转换

这种隐式特性意味着可以把派生类对象或者派生类对象的引用用在需要基类引用的地方
同样的我们也可以把派生类对象的指针用在需要基类指针的地方

在派生类对象中含有与其基类对应的组成部分这一事实是继承的关键所在

派生类构造函数

派生类也必须使用基类的构造函数来初始化它的基类部分
每个类控制它自己的成员初始化过程

派生类对象的基类部分与派生类对象自己的数据成员都是在构造函数的初始化阶段
执行初始化操作的
派生类构造函数同样是通过构造函数初始化列表来将实参传递给基类构造函数的

//例如,接受四个参数的Bulk_quote构造函数如下所示:
Bulk_quote(const std::string& book, double p,
           std::size_t qty, double disc) :
           Quote(book, p), min_qty(qty), discount(disc) { }
    // as before
};
//该函数将它的前两个参数(分别表示ISBN和价格)传递给Quote的构造函数,
//由Quote的构造函数负责初始化Bulk_quote的基类部分(即bookNo成员和price成员)。
//当(空的)Quote构造函数体结束后,我们构建的对象的基类部分也就完成初始化了。
//接下来初始化由派生类直接定义的min_qty成员和discount成员。
//最后运行Bulk_quote构造函数的(空的)函数体。

派生类构造函数

除非我们特别指出否则派生类对象的基类部分会像数据成员一样执行默认初始化
如果想使用其他的基类构造函数
我们需要以类名加圆括号内的实参列表的形式为构造函数提供初始值
这些实参将帮助编译器决定到底应该选用哪个构造函数来初始化派生类对象的基类部分

首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。

派生类使用基类的成员

派生类可以访问基类的公有成员和受保护成员
// if the specified number of items are purchased, use the discounted price
double Bulk_quote::net_price(size_t cnt) const
{
    if (cnt >= min_qty)
        return cnt * (1 - discount) * price;
    else
        return cnt * price;
}
//该函数产生一个打折后的价格:如果给定的数量超过了min_qty,
//则将discount(一个小于1大于0的数)作用于price。
派生类的作用域嵌套在基类的作用域之内
- 因此对于派生类的一个成员来说它使用派生类成员例如min_qty和discount
 的方式与使用基类成员例如price的方式没什么不同

关键概念:遵循基类的接口

每个类负责定义各自的接口
要想与类的对象交互必须使用该类的接口即使这个对象是派生类的基类部分也是如此

因此派生类对象不能直接初始化基类的成员
- 尽管从语法上来说可以在派生类构造函数体内给它的公有或受保护的基类成员赋值
  但是最好不要这么做和使用基类的其他场合一样派生类应该遵循基类的接口
  并且通过调用基类的构造函数来初始化那些从基类中继承而来的成员

继承与静态成员

如果基类定义了一个静态成员则在整个继承体系中只存在该成员的唯一定义
不论从基类中派生出来多少个派生类对于每个静态成员来说都只存在唯一的实例
class Base {
public:
    static void statmem();
};
class Derived : public Base {
    void f(const Derived&);
};
静态成员遵循通用的访问控制规则
- 如果基类中的成员是private的则派生类无权访问它
- 假设某静态成员是可访问的则我们既能通过基类使用它也能通过派生类使用它
void Derived::f(const Derived &derived_obj)
{
    Base::statmem();    // ok: Base defines statmem
    Derived::statmem(); // ok: Derived inherits statmem
    // ok: derived objects can be used to access static from base
    derived_obj.statmem(); // accessed through a Derived object
    statmem();             // accessed through this object
}

派生类的声明

派生类的声明与其他类差别不大声明中包含类名但是不包含它的派生列表
class Bulk_quote : public Quote; // error: derivation list can't appear here
class Bulk_quote;                // ok: right way to declare a derived class
一条声明语句的目的是令程序知晓某个名字的存在以及该名字表示一个什么样的实体
- 如一个类一个函数或一个变量等
- 派生列表以及与定义有关的其他细节必须与类的主体一起出现

被用作基类的类

如果我们想将某个类用作基类则该类必须已经定义而非仅仅声明
class Quote;   // declared but not defined
// error: Quote must be defined
class Bulk_quote : public Quote { ... };

这一规定的原因显而易见
- 派生类中包含并且可以使用它从基类继承而来的成员为了使用这些成员
  派生类当然要知道它们是什么
- 因此该规定还有一层隐含的意思即一个类不能派生它本身

被用作基类的类

一个类是基类同时它也可以是一个派生类
class Base { /* ... */ } ;
class D1: public Base { /* ... */ };
class D2: public D1 { /* ... */ };
在这个继承关系中Base是D1的直接基类directbase),
同时是D2的间接基类indirectbase)。
直接基类出现在派生列表中而间接基类由派生类通过其直接基类继承而来

每个类都会继承直接基类的所有成员
- 对于一个最终的派生类来说它会继承其直接基类的成员
- 该直接基类的成员又含有其基类的成员
- 依此类推直至继承链的顶端
因此最终的派生类将包含它的直接基类的子对象以及每个间接基类的子对象

防止继承的发生

有时我们会定义这样一种类
- 我们不希望其他类继承它或者不想考虑它是否适合作为一个基类
C++11新标准提供了一种防止继承发生的方法即在类名后跟一个关键字final
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

练习

定义你自己的 Bulk_quote 
#include "ex_15_3.h"
class Bulk_quote : public Quote
{
public:
    Bulk_quote() = default;
    Bulk_quote(const std::string& b, double p, std::size_t q, double disc) :
        Quote(b, p), min_qty(q), discount(disc) {}

    double net_price(std::size_t n) const override;

private:
    std::size_t min_qty = 0;
    double      discount = 0.0;
};

练习

 Quote  Bulk_quote 的对象传给15.2.1节练习中的 print_total 函数
检查该函数是否正确
#include "ex_15_3.h"
#include "ex_15_5.h"
#include <iostream>
#include <string>
double print_total(std::ostream& os, const Quote& item, size_t n);
int main(){
    Quote q("textbook", 10.60);
    Bulk_quote bq("textbook", 10.60, 10, 0.3);
    print_total(std::cout, q, 12);
    print_total(std::cout, bq, 12);
    return 0;
}
double print_total(std::ostream &os, const Quote &item, size_t n){
    double ret = item.net_price(n);
    os << "ISBN:" << item.isbn()
        << "# sold: " << n << " total due: " << ret << std::endl;
    return ret;
}

练习

定义一个类使其实现一种数量受限的折扣策略具体策略是当购买书籍的数量不超过
一个给定的限量时享受折扣如果购买量一旦超过了限量则超出的部分将以原价销售
#include "ex_15_5.h"
class Limit_quote : public Quote{
public:
    Limit_quote();
    Limit_quote(const std::string& b, double p, std::size_t max, double disc) :
        Quote(b, p), max_qty(max), discount(disc)    {}
    double net_price(std::size_t n) const override;
private:
    std::size_t max_qty = 0;
    double      discount = 0.0;
};
double Limit_quote::net_price(std::size_t n) const{
    if (n > max_qty)
        return max_qty * price * discount + (n - max_qty) * price;
    else
        return n * discount *price;
}

类型转换与继承

理解基类和派生类之间的类型转换是理解C++语言面向对象编程的关键所在。

通常情况下如果我们想把引用或指针绑定到一个对象上
- 则引用或指针的类型应与对象的类型一致
- 或者对象的类型含有一个可接受的const类型转换规则
存在继承关系的类是一个重要的例外
- 我们可以将基类的指针或引用绑定到派生类对象上
- 例如我们可以用Quote&指向一个Bulk_quote对象
  也可以把一个Bulk_quote对象的地址赋给一个Quote*。

类型转换与继承

可以将基类的指针或引用绑定到派生类对象上有一层极为重要的含义
- 当使用基类的引用或指针实际上我们并不清楚该引用或指针所绑定
  对象的真实类型该对象可能是基类的对象也可能是派生类的对象

和内置指针一样智能指针类也支持派生类向基类的类型转换
- 这意味着我们可以将一个派生类对象的指针存储在一个基类的智能指针内

静态类型与动态类型

当使用存在继承关系的类型时必须
- 将一个变量或其他表达式的静态类型static type
  与该表达式表示对象的动态类型dynamic type区分开来

表达式的静态类型在编译时总是已知的它是变量声明时的类型或表达式生成的类型
动态类型则是变量或表达式表示的内存中的对象的类型动态类型直到运行时才可知
//例如,当print_total调用net_price时
double ret = item.net_price(n);
//我们知道item的静态类型是Quote&,它的动态类型则依赖于item绑定的实参,
//动态类型直到在运行时调用该函数时才会知道。
//如果我们传递一个Bulk_quote对象给print_total,
//则item的静态类型将与它的动态类型不一致。
//如前所述,item的静态类型是Quote&,而在此例中它的动态类型则是Bulk_quote。
如果表达式既不是引用也不是指针则它的动态类型永远与静态类型一致
//例如,Quote类型的变量永远是一个Quote对象,
//我们无论如何都不能改变该变量对应的对象的类型。
基类的指针或引用的静态类型可能与其动态类型不一致一定要理解其中的原因

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

之所以存在派生类向基类的类型转换是因为
- 每个派生类对象都包含一个基类部分
- 而基类的引用或指针可以绑定到该基类部分上

一个基类的对象既可以以独立的形式存在也可以作为派生类对象的一部分存在
如果基类对象不是派生类对象的一部分
- 则它只含有基类定义的成员而不含有派生类定义的成员

因为一个基类的对象可能是派生类对象的一部分也可能不是
所以不存在从基类向派生类的自动类型转换
Quote base;
Bulk_quote* bulkP = &base;  // error: can't convert base to derived
Bulk_quote& bulkRef = base; // error: can't convert base to derived
//如果上述赋值是合法的,
//则我们有可能会使用bulkP或bulkRef访问base中本不存在的成员。

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

即使一个基类指针或引用绑定在一个派生类对象上也不能执行从基类向派生类的转换
Bulk_quote bulk;
Quote *itemP = &bulk;        // ok: dynamic type is Bulk_quote
Bulk_quote *bulkP = itemP;   // error: can't convert base to derived
编译器在编译时无法确定某个特定的转换在运行时是否安全这是因为
- 编译器只能通过检查指针或引用的静态类型来推断该转换是否合法
如果在基类中含有一个或多个虚函数
- 我们可以使用dynamic_cast请求一个类型转换该转换的安全检查将在运行时执行
同样如果我们已知某个基类向派生类的转换是安全的
- 则我们可以使用static_cast来强制覆盖掉编译器的检查工作

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

派生类向基类的自动类型转换只对指针或引用类型有效
在派生类类型和基类类型之间不存在这样的转换
很多时候我们确实希望将派生类对象转换成它的基类类型
但是这种转换的实际发生过程往往与我们期望的有所差别

当我们初始化或赋值一个类类型的对象时实际上是在调用某个函数
- 当执行初始化时我们调用构造函数
- 而当执行赋值操作时我们调用赋值运算符
这些成员通常都包含一个参数该参数的类型是类类型的const版本的引用

因为这些成员接受引用作为参数所以派生类向基类的转换
允许我们给基类的拷贝/移动操作传递一个派生类的对象
- 这些操作不是虚函数
- 当我们给基类的构造函数传递一个派生类对象时
  实际运行的构造函数是基类中定义的那个
  显然该构造函数只能处理基类自己的成员
- 类似的如果我们将一个派生类对象赋值给一个基类对象则实际运行的
  赋值运算符也是基类中定义的那个该运算符同样只能处理基类自己的成员

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

//我们的书店类使用了合成版本的拷贝和赋值操作。
//合成版本会像其他类一样逐成员地执行拷贝或赋值操作:
Bulk_quote bulk;   // object of derived type
Quote item(bulk);  // uses the Quote::Quote(const Quote&) constructor
item = bulk;       // calls Quote::operator=(const Quote&)
//当构造item时,运行Quote的拷贝构造函数。
//该函数只能处理bookNo和price两个成员,它负责拷贝bulk中Quote部分的成员,
//同时忽略掉bulk中Bulk_quote部分的成员。类似的,对于将bulk赋值给item的操作
//来说,只有bulk中Quote部分的成员被赋值给item。
因为在上述过程中会忽略Bulk_quote部分
所以我们可以说bulk的Bulk_quote部分被切掉sliceddown

当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。

练习

给出静态类型和动态类型的定义

表达式的静态类型在编译时总是已知的它是变量声明时的类型或表达式生成的类型
动态类型则是变量或表达式表示的内存中的对象的类型动态类型直到运行时才可知

练习

在什么情况下表达式的静态类型可能与动态类型不同
请给出三个静态类型与动态类型不同的例子

基类的指针或引用的静态类型可能与其动态类型不一致
Bulk_quote bulk;
Quote *pQuote=&bulk;
Quote &rQuote=bulk;
//传派生类对象,则静态类型与动态类型不同
double print_total(ostream& os,const Quoto& item,size_t n);

练习

回忆我们在8.1节进行的讨论
解释将 ifstream 传递给 Sales_data 的read 函数的程序是如何工作的

std::ifstream  std::istream 的派生基类因此 read 函数能够正常工作

关键概念:存在继承关系的类型之间的转换规则

要想理解在具有继承关系的类之间发生的类型转换有三点非常重要
- 从派生类向基类的类型转换只对指针或引用类型有效。· 
- 基类向派生类不存在隐式类型转换。· 
- 和任何其他成员一样派生类向基类的类型转换也可能会由于访问受限而变得不可行

尽管自动类型转换只对指针或引用类型有效
但是继承体系中的大多数类仍然显式或隐式地定义了拷贝控制成员
因此我们通常能够将一个派生类对象拷贝移动或赋值给一个基类对象
不过需要注意的是这种操作只处理派生类对象的基类部分

虚函数

当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定
直到运行时才能知道到底调用了哪个版本的虚函数所以所有虚函数都必须有定义
- 通常情况下如果我们不使用某个函数则无须为该函数提供定义
- 但是我们必须为每一个虚函数都提供定义而不管它是否被用到了
  这是因为连编译器也无法确定到底会使用哪个虚函数

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

当某个虚函数通过指针或引用调用时
编译器产生的代码直到运行时才能确定应该调用哪个版本的函数
被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个
//print_total函数,该函数通过其名为item的参数来进一步调用net_price,
//其中item的类型是Quote&。因为item是引用而且net_price是虚函数,所以到底调用
//net_price的哪个版本完全依赖于运行时绑定到item的实参的实际(动态)类型:
Quote base("0-201-82470-1", 50);
print_total(cout, base, 10);    // calls Quote::net_price
Bulk_quote derived("0-201-82470-1", 50, 5, .19);
print_total(cout, derived, 10); // calls Bulk_quote::net_price
//在第一条调用语句中,item绑定到Quote类型的对象上,因此当print_total
//调用net_price时,运行在Quote中定义的版本。在第二条调用语句中,item绑定到
//Bulk_quote类型的对象上,因此print_total调用Bulk_quote定义的net_price。

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

动态绑定只有当我们通过指针或引用调用虚函数时才会发生
base = derived;         // copies the Quote part of derived into base
base.net_price(20);     // calls Quote::net_price
当我们通过一个具有普通类型非引用非指针的表达式调用虚函数时
在编译时就会将调用的版本确定下来

//例如,如果我们使用base调用net_price,
//则应该运行net_price的哪个版本是显而易见的。
//我们可以改变base表示的对象的值(即内容),
//但是不会改变该对象的类型。
//因此,在编译时该调用就会被解析成Quote的net_price。

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

OOP的核心思想是多态性polymorphism)。
- 多态性这个词源自希腊语其含义是多种形式”。
我们把具有继承关系的多个类型称为多态类型
- 因为我们能使用这些类型的多种形式而无须在意它们的差异
引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在

当我们使用基类的引用或指针调用基类中定义的一个函数时
我们并不知道该函数真正作用的对象是什么类型
- 因为它可能是一个基类的对象也可能是一个派生类的对象
- 如果该函数是虚函数则直到运行时才会决定到底执行哪个版本
  判断的依据是引用或指针所绑定的对象的真实类型

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

另一方面对非虚函数的调用在编译时进行绑定
类似的通过对象进行的函数虚函数或非虚函数调用也在编译时绑定
对象的类型是确定不变的我们无论如何都不可能令对象的动态类型与静态类型不一致
因此通过对象进行的函数调用将在编译时绑定到该对象所属类中的函数版本上

当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。

派生类中的虚函数

当在派生类中覆盖了某个虚函数时可以再一次使用virtual关键字指出该函数的性质
这么做并非必须因为一旦某个函数被声明成虚函数则在所有派生类中它都是虚函数

一个派生类的函数如果覆盖了某个继承而来的虚函数
则它的形参类型必须与被它覆盖的基类函数完全一致

同样派生类中虚函数的返回类型也必须与基类函数匹配
该规则存在一个例外当类的虚函数返回类型是类本身的指针或引用时上述规则无效
//也就是说,如果D由B派生得到,
//则基类的虚函数可以返回B*而派生类的对应函数可以返回D*,
//只不过这样的返回类型要求从D到B的类型转换是可访问的。

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

final和override说明符

派生类如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同
- 这是合法的行为
- 编译器将认为新定义的这个函数与基类中原有的函数是相互独立的
- 这时派生类的函数并没有覆盖掉基类中的版本
就实际的编程习惯而言这种声明往往意味着发生了错误
因为我们可能原本希望派生类能覆盖掉基类中的虚函数但是一不小心把形参列表弄错了

要想调试并发现这样的错误显然非常困难
在C++11新标准中我们可以使用override关键字来说明派生类中的虚函数
- 这么做的好处是在使得意图更加清晰,
- 同时让编译器可以为我们发现一些错误这在编程实践中显得更加重要
- 如果我们使用override标记了某个函数但该函数并没有覆盖已存在的虚函数
  此时编译器将报错

final和override说明符

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;    // error: f3 not virtual
    void f4() override;    // error: B doesn't have a function named f4
};
//在D1中,f1的override说明符是正确的,因为基类和派生类中的f1都是const成员,
//并且它们都接受一个int返回void,所以D1中的f1正确地覆盖了它从B中继承而来的
//虚函数。D1中f2的声明与B中f2的声明不匹配,显然B中定义的f2不接受任何参数
//而D1的f2接受一个int。因为这两个声明不匹配,所以D1的f2不能覆盖B的f2,
//它是一个新函数,仅仅是名字恰好与原来的函数一样而已。因为我们使用override
//所表达的意思是希望能覆盖基类中的虚函数而实际上并未做到,所以编译器会报错。
//因为只有虚函数才能被覆盖,所以编译器会拒绝D1的f3。该函数不是B中的虚函数,
//因此它不能被覆盖。f4的声明也会发生错误,因为B中根本就没有名为f4的函数。

final和override说明符

我们还能把某个函数指定为final如果我们已经把函数定义成final了
则之后任何尝试覆盖该函数的操作都将引发错误
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
};
final和override说明符出现在
- 形参列表包括任何const或引用修饰符以及尾置返回类型之后

虚函数与默认实参

虚函数也可以拥有默认实参
- 如果某次函数调用使用了默认实参则该实参值由本次调用的静态类型决定
如果我们通过基类的引用或指针调用函数则使用基类中定义的默认实参
- 即使实际运行的是派生类中的函数版本也是如此
- 此时传入派生类函数的将是基类函数定义的默认实参
- 如果派生类函数依赖不同的实参则程序结果将与我们的预期不符

如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。

回避虚函数的机制

在某些情况下我们希望对虚函数的调用不要进行动态绑定
- 而是强迫其执行虚函数的某个特定版本
使用作用域运算符可以实现这一目的例如下面的代码
//calls the version from the base class regardless of the dynamic type of baseP
double undiscounted = baseP->Quote::net_price(42);
//该代码强行调用Quote的net_price函数,而不管baseP实际指向的对象类型到底是什么。
//该调用将在编译时完成解析。

通常情况下只有成员函数或友元中的代码才需要
- 使用作用域运算符来回避虚函数的机制

回避虚函数的机制

什么时候我们需要回避虚函数的默认机制呢
- 通常是当一个派生类的虚函数调用它覆盖的基类的虚函数版本时
  在此情况下基类的版本通常完成继承层次中所有类型都要做的共同任务
  而派生类中定义的版本需要执行一些与派生类本身密切相关的操作

如果一个派生类虚函数需要调用它的基类版本但是没有使用作用域运算符
- 则在运行时该调用将被解析为对派生类版本自身的调用从而导致无限递归

练习

为你的 Quote 类体系添加一个名为 debug 的虚函数令其分别显示每个类的数据成员
class Quote{
public:
    virtual void debug() const;
    //其它成员
}
void Quote::debug() const
{
    std::cout << "data members of this class:\n"
              << "bookNo= " <<this->bookNo << " "
              << "price= " <<this->price<< " "<<endl;
}

练习

有必要将一个成员函数同时声明成 override  final 为什么

有必要
override 的含义是重写基类中相同名称的虚函数
final 是阻止它的派生类重写当前虚函数

练习

给定下面的类解释每个 print 函数的机理
class base {
public:
    string name() { return basename;}
    virtual void print(ostream &os) { os << basename; }
private:
    string basename;
};
class derived : public base {
public:
    void print(ostream &os) { print(os); os << " " << i; }
private:
    int i;
};
在上述代码中存在问题吗如果有你该如何修改它

有问题应该改为
void print(ostream &os) override {base::print(os);os <<"derived\n"<<i;}

练习

给定上一题中的类以及下面这些对象说明在运行时调用哪个函数
base bobj;          base *bp1 = &bobj;      base &br1 = bobj;
derived dobj;       base *bp2 = &dobj;      base &br2 = dobj;
(a) bobj.print();   (b)dobj.print();        (c)bp1->name();
(d)bp2->name();     (e)br1.print();         (f)br2.print();


 (a) 编译时base::print()
 (b) 编译时derived::print()
 (c) 编译时base::name()
 (d) 编译时base::name()
 (e) 运行时base::print()
 (f) 运行时derived::print()

抽象基类

假设我们希望扩展书店程序并令其支持多种折扣策略
- 购买量超过一定数量享受折扣
- 购买量不超过某个限额时可以享受折扣但是一旦超过限额就要按原价支付
- 购买量超过一定数量后购买的全部书籍都享受折扣否则全都不打折
上面的每个策略都要求一个购买量的值和一个折扣值
- 定义一个新的名为Disc_quote的类来支持不同的折扣策略
    - 其中Disc_quote负责保存购买量的值和折扣值
    - 其他的表示某种特定策略的类如Bulk_quote将分别继承自Disc_quote
    - 每个派生类通过定义自己的net_price函数来实现各自的折扣策略
- 显然我们的Disc_quote类与任何特定的折扣策略都无关
    - 因此Disc_quote类中的net_price函数是没有实际含义的

抽象基类

- 一种无意义的做法
    - 在Disc_quote类中不定义新的net_price此时
      Disc_quote将继承Quote中的net_price函数
    - 用户可能会创建一个Disc_quote对象并为其提供购买量和折扣值
      如果将该对象传给一个像print_total这样的函数
      则程序将调用Quote版本的net_price
    - 显然最终计算出的销售价格并没有考虑我们在创建对象时提供的折扣值
      因此上述操作毫无意义

纯虚函数

我们根本就不希望用户创建一个Disc_quote对象
- Disc_quote类表示的是一本打折书籍的通用概念而非某种具体的折扣策略
将net_price定义成纯虚pure virtual函数从而令程序实现我们的设计意图
- 这样做可以清晰明了地告诉用户当前这个net_price函数是没有实际意义的
和普通的虚函数不一样一个纯虚函数无须定义
- 我们通过在函数体的位置即在声明语句的分号之前书写=0
  就可以将一个虚函数说明为纯虚函数
  其中=0只能出现在类内部的虚函数声明语句处

纯虚函数

class Disc_quote : public Quote {
public:
    Disc_quote() = default;
    Disc_quote(const std::string& book, double price,
              std::size_t qty, double disc):
                 Quote(book, price),
                 quantity(qty), discount(disc) { }
    double net_price(std::size_t) const = 0;
protected:
    std::size_t quantity = 0; //  purchase size for the discount to apply
    double discount = 0.0;    //  fractional discount to apply
};
//Disc_quote也分别定义了一个默认构造函数和一个接受四个参数的构造函数。
//尽管我们不能直接定义这个类的对象,但是Disc_quote的派生类构造函数将会使用
//Disc_quote的构造函数来构建各个派生类对象的Disc_quote部分。
//其中,接受四个参数的构造函数将前两个参数传递给Quote的构造函数,
//然后直接初始化自己的成员discount和quantity。
//默认构造函数则对这些成员进行默认初始化。
我们也可以为纯虚函数提供定义不过函数体必须定义在类的外部
也就是说我们不能在类的内部为一个=0的函数提供函数体

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

含有或者未经覆盖直接继承纯虚函数的类是抽象基类abstract base class)。
- 抽象基类负责定义接口而后续的其他类可以覆盖该接口
- 我们不能直接创建一个抽象基类的对象
//因为Disc_quote将net_price定义成了纯虚函数,所以不能定义Disc_quote的对象。
//我们可以定义Disc_quote的派生类的对象,前提是这些类覆盖了net_price函数:
// 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
//Disc_quote的派生类必须给出自己的net_price定义,否则它们仍将是抽象基类。

不能创建抽象基类的对象。

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

//重新实现Bulk_quote了,这一次我们让它继承Disc_quote而非直接继承Quote:
class Bulk_quote : public Disc_quote {
public:
    Bulk_quote() = default;
    Bulk_quote(const std::string& book, double price,
              std::size_t qty, double disc):
          Disc_quote(book, price, qty, disc) { }
// overrides the base version to implement the bulk purchase discount policy
    double net_price(std::size_t) const override;
};
//这个版本的Bulk_quote的直接基类是Disc_quote,间接基类是Quote。
//每个Bulk_quote对象包含三个子对象:
//一个(空的)Bulk_quote部分、一个Disc_quote子对象和一个Quote子对象。

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

如前所述每个类各自控制其对象的初始化过程
    Bulk_quote(const std::string& book, double price,
              std::size_t qty, double disc):
          Disc_quote(book, price, qty, disc) { }
//因此,即使Bulk_quote没有自己的数据成员,
//它也仍然需要像原来一样提供一个接受四个参数的构造函数。
//该构造函数将它的实参传递给Disc_quote的构造函数,
//随后Disc_quote的构造函数继续调用Quote的构造函数。
//Quote的构造函数首先初始化bulk的bookNo和price成员,当Quote的构造函数结束后,
//开始运行Disc_quote的构造函数并初始化quantity和discount成员,
//最后运行Bulk_quote的构造函数,该函数无须执行实际的初始化或其他工作。

关键概念:重构

在Quote的继承体系中增加Disc_quote类是重构refactoring的一个典型示例
重构负责重新设计类的体系以便将操作和/或数据从一个类移动到另一个类中
对于面向对象的应用程序来说重构是一种很普遍的现象

值得注意的是即使我们改变了整个继承体系
那些使用了Bulk_quote或Quote的代码也无须进行任何改动
不过一旦类被重构或以其他方式被改变),就意味着我们必须重新编译含有这些类的代码了

练习

定义你自己的 Disc_quote  Bulk_quote

//Disc_quote:
class Disc_quote : public Quote
{
public:
    Disc_quote();
    Disc_quote(const std::string& b, double p, std::size_t q, double d) :
        Quote(b, p), quantity(q), discount(d)   { }

    virtual double net_price(std::size_t n) const override = 0;

protected:
    std::size_t quantity;
    double      discount;
};

练习

//Bulk_quote:
class Bulk_quote : public Disc_quote
{
public:
    Bulk_quote() = default;
    Bulk_quote(const std::string& b, double p, std::size_t q, double disc) :
        Disc_quote(b, p, q, disc) {   }

    double net_price(std::size_t n) const override;
    void  debug() const override;
};

练习

改写你在15.2.2节练习中编写的数量受限的折扣策略令其继承 Disc_quote
//Limit_quote:
class Limit_quote : public Disc_quote
{
public:
    Limit_quote() = default;
    Limit_quote(const std::string& b, double p, std::size_t max, double disc):
        Disc_quote(b, p, max, disc)  {   }

    double net_price(std::size_t n) const override
    { return n * price * (n < quantity ? 1 - discount : 1 ); }

    void debug() const override;
};

练习

尝试定义一个 Disc_quote 的对象看看编译器给出的错误信息是什么

testDiscQuote.cpp: In function int main():
testDiscQuote.cpp:5:16: error: 
cannot declare variable q to be of abstract type Disc_quote
    5 |     Disc_quote q;

访问控制与继承

每个类分别控制自己的成员初始化过程
每个类还分别控制着其成员对于派生类来说是否可访问accessible)。

受保护的成员

一个类使用protected关键字来声明那些
它希望与派生类分享但是不想被其他公共访问使用的成员
protected说明符可以看做是public和private中和后的产物
- 和私有成员类似受保护的成员对于类的用户来说是不可访问的。· 
- 和公有成员类似受保护的成员对于派生类的成员和友元来说是可访问的
  此外protected还有另外一条重要的性质。· 
- 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员
  派生类对于一个基类对象中的受保护成员没有任何访问特权

受保护的成员

class Base {
protected:
    int prot_mem;     // protected member
};
class Sneaky : public Base  {
    friend void clobber(Sneaky&);  // can access Sneaky::prot_mem
    friend void clobber(Base&);    // can't access Base::prot_mem
    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; }
//如果派生类(及其友元)能访问基类对象的受保护成员,
//则上面的第二个clobber(接受一个Base&)将是合法的。
//该函数不是Base的友元,但是它仍然能够改变一个Base对象的内容。
//如果按照这样的思路,则我们只要定义一个形如Sneaky 的新类就能非常简单地
//规避掉protected提供的访问保护了。
即派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员
对于普通的基类对象中的成员不具有特殊的访问权限

公有、私有和受保护继承

某个类对其继承而来的成员的访问权限受到两个因素影响
一是在基类中该成员的访问说明符
二是在派生类的派生列表中的访问说明符
class Base {
public:
    void pub_mem();   // public member
protected:
    int prot_mem;     // protected member
private:
    char priv_mem;    // private member
};
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't affect access in the derived class
    int f1() const { return prot_mem; }
};

公有、私有和受保护继承

派生访问说明符对于派生类的成员及友元能否访问其直接基类的成员没什么影响
对基类成员的访问权限只与基类中的访问说明符有关
Pub_Derv和Priv_Derv都能访问受保护的成员prot_mem
同时它们都不能访问私有成员priv_mem

派生访问说明符的目的是
控制派生类用户包括派生类的派生类在内对于基类成员的访问权限
Pub_Derv d1;   //  members inherited from Base are public
Priv_Derv d2;  //  members inherited from Base are private
d1.pub_mem();  //  ok: pub_mem is public in the derived class
d2.pub_mem();  //  error: pub_mem is private in the derived class
//Pub_Derv和Priv_Derv都继承了pub_mem函数。
//如果继承是公有的,则成员将遵循其原有的访问说明符,此时d1可以调用pub_mem。
//在Priv_Derv中,Base的成员是私有的,因此类的用户不能调用pub_mem。

公有、私有和受保护继承

派生访问说明符还可以控制继承自派生类的新类的访问权限
struct Derived_from_Public : public Pub_Derv {
    // ok: Base::prot_mem remains protected in Pub_Derv
    int use_base() { return prot_mem; }
};
struct Derived_from_Private : public Priv_Derv {
    // error: Base::prot_mem is private in Priv_Derv
    int use_base() { return prot_mem; }
};
//Pub_Derv的派生类之所以能访问Base的prot_mem成员是因为
//该成员在Pub_Derv中仍然是受保护的。
//相反,Priv_Derv的派生类无法执行类的访问,
//对于它们来说,Priv_Derv继承自Base的所有成员都是私有的。

假设我们之前还定义了一个名为Prot_Derv的类它采用受保护继承
则Base的所有公有成员在新定义的类中都是受保护的
Prot_Derv的用户不能访问pub_mem
但是Prot_Derv的成员和友元可以访问那些继承而来的成员

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

派生类向基类的转换是否可访问由使用该转换的代码决定
同时派生类的派生访问说明符也会有影响假定D继承自B
- 只有当D公有地继承B时用户代码才能使用派生类向基类的转换
  如果D继承B的方式是受保护的或者私有的则用户代码不能使用该转换。· 
- 不论D以什么方式继承BD的成员函数和友元都能使用派生类向基类的转换
  派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。· 
- 如果D继承B的方式是公有的或者受保护的
  则D的派生类的成员和友元可以使用D向B的类型转换
  反之如果D继承B的方式是私有的则不能使用

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

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

不考虑继承的话我们可以认为一个类有两种不同的用户普通用户和类的实现者
- 普通用户编写的代码使用类的对象这部分代码只能访问类的公有接口成员
- 实现者则负责编写类的成员和友元的代码成员和友元既能访问类的公有部分
  也能访问类的私有实现部分

如果进一步考虑继承的话就会出现第三种用户即派生类
基类把它希望派生类能够使用的部分声明成受保护的
- 普通用户不能访问受保护的成员而派生类及其友元仍旧不能访问私有成员

和其他类一样基类应该将其接口成员声明为公有的
同时将属于其实现的部分分成两组
一组可供派生类访问另一组只能由基类及基类的友元访问
- 对于前者应该声明为受保护的
  这样派生类就能在实现自己的功能时使用基类的这些操作和数据
- 对于后者应该声明为私有的

友元与继承

就像友元关系不能传递一样友元关系同样也不能继承
基类的友元在访问派生类成员时不具有特殊性
类似的派生类的友元也不能随意访问基类的成员
class Base {
    // added friend declaration; other members as before
    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
};
//每个类负责控制自己的成员的访问权限,
//因此尽管看起来有点儿奇怪,但f3确实是正确的。
//Pal是Base的友元,所以Pal能够访问Base对象的成员,
//这种可访问性包括了Base对象内嵌在其派生类对象中的情况。

友元与继承

当一个类将另一个类声明为友元时这种友元关系只对做出声明的类有效
对于原来那个类来说其友元的基类或者派生类不具有特殊的访问能力
// 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't inherit
};

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

改变个别成员的可访问性

有时我们需要改变派生类继承的某个名字的访问级别
通过使用using声明可以达到这一目的
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;
};
//因为Derived使用了私有继承,所以继承而来的成员size和n(在默认情况下)
//是Derived的私有成员。然而,我们使用using声明语句改变了这些成员的可访问性。
//改变之后,Derived的用户将可以使用size成员,而Derived的派生类将能使用n。

改变个别成员的可访问性

通过在类的内部使用using声明语句
我们可以将该类的直接或间接基类中的任何可访问成员例如非私有成员标记出来
using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定
- 也就是说如果一条using声明语句出现在类的private部分
  则该名字只能被类的成员和友元访问
- 如果using声明语句位于public部分则类的所有用户都能访问它
- 如果using声明语句位于protected部分则该名字对于成员友元和派生类是可访问的

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

默认的继承保护级别

使用struct和class关键字定义的类具有不同的默认访问说明符
类似的默认派生运算符也由定义派生类所用的关键字来决定
- 默认情况下使用class关键字定义的派生类是私有继承的
- 而使用struct关键字定义的派生类是公有继承的
class Base { /* ...   */ };
struct D1 : Base { /* ...   */ };   // public inheritance by default
class D2 : Base { /* ...   */ };    // private inheritance by default
常常有一种错觉使用struct关键字和class关键字定义的类之间还有更深层次的差别
事实上唯一的差别就是默认成员访问说明符及默认派生访问说明符
除此之外再无其他不同之处

一个私有派生的类最好显式地将private声明出来,而不要仅仅依赖于默认的设置。显式声明的好处是可以令私有继承关系清晰明了,不至于产生误会。

练习

假设给定了第543页和第544页的类同时已知每个对象的类型如注释所示
判断下面的哪些赋值语句是合法的解释那些不合法的语句为什么不被允许
Base *p = &d1;  //d1 的类型是 Pub_Derv
p = &d2;        //d2 的类型是 Priv_Derv
p = &d3;        //d3 的类型是 Prot_Derv
p = &dd1;        //dd1 的类型是 Derived_from_Public    
p = &dd2;        //dd2 的类型是 Derived_from_Private
p = &dd3;        //dd3 的类型是 Derived_from_Protected

Base *p = &d1; 合法
p = &d2; 不合法
p = &d3; 不合法
p = &dd1; 合法
p = &dd2; 不合法
p = &dd3; 不合法


只有在派生类是使用public的方式继承基类时
用户代码才可以使用派生类到基类derived-to-base的转换

练习

假设543页和544页的每个类都有如下形式的成员函数
void memfcn(Base &b) { b = *this; }
对于每个类分别判断上面的函数是否合法

合法
* Pub_Derv
* Priv_Derv
* Prot_Derv
* Derived_from_Public
* Derived_from_Protected
不合法
* Derived_from_Private

这段代码是在成员函数中使用Base
Priv_Drev中的Base部分虽然是private的但其成员函数依然可以访问
Derived_from_Private继承自Priv_Drev不能访问Priv_Drev中的private成员
因此不合法

练习

编写代码检验你对前面两题的回答是否正确
#include <iostream>
#include <string>

#include "exercise15_5.h"
#include "bulk_quote.h"
#include "limit_quote.h"
#include "disc_quote.h"

class Base
{
public:
    void pub_mem();   // public member
protected:
    int prot_mem;     // protected member
private:
    char priv_mem;    // private member
};

练习

struct Pub_Derv : public    Base
{
    void memfcn(Base &b) { b = *this; }
};
struct Priv_Derv : private   Base
{
    void memfcn(Base &b) { b = *this; }
};
struct Prot_Derv : protected Base
{
    void memfcn(Base &b) { b = *this; }
};

struct Derived_from_Public : public Pub_Derv
{
    void memfcn(Base &b) { b = *this; }
};

练习

struct Derived_from_Private : public Priv_Derv{
    //void memfcn(Base &b) { b = *this; }
};
struct Derived_from_Protected : public Prot_Derv{
    void memfcn(Base &b) { b = *this; }
};
int main(){
    Pub_Derv d1;
    Base *p = &d1;
    Priv_Derv d2;
    //p = &d2;
    Prot_Derv d3;
    //p = &d3;
    Derived_from_Public dd1;
    p = &dd1;
    Derived_from_Private dd2;
    //p =& dd2;
    Derived_from_Protected dd3;
    //p = &dd3;
    return 0;
}

练习

从下面这些一般性抽象概念中任选一个或者选一个你自己的),
将其对应的一组类型组织成一个继承体系

(a) 图形文件格式如giftiffjpegbmp
(b) 图形基元如方格圆锥
(c) C++语言中的类型如类函数成员函数

几何图元类Figure作为公共基类
矩形类Rectangle圆类Circle,球类Sphere,圆锥类Cone,定义为Figure类的派生类

练习

对于你在上一题中选择的类为其添加虚函数及公有成员和受保护的成员
#include<iostream>
using namespace std;
class Figure
{    public:
    virtual void draw() const=0;
    virtual void input_data()=0;
};

练习

class Rectangle: public Figure
{
    protected:
        double left,top,right,bottom;
    public:
        void draw() const
        {    //...... //画矩形
            cout<<"Rectangle:"<<left<<","<<top<<";"<<right<<","<<bottom<<endl;
        }
        void input_data()
        {    cout << "请输入矩形的左上角和右下角坐标 (x1,y1,x2,y2) :";
            cin >> left >> top >> right >> bottom;
        }
        double area() const
        { return (bottom-top)*(right-left); }
};

练习

const double PI=3.1416;
class Circle: public Figure
{
    protected:
        double x,y,r;
    public:
        void draw() const
        {    //...... //画圆
            cout<<"Circle:"<<x<<","<<y<<":"<<r<<endl;
        }
        void input_data()
        {    cout << "请输入圆的圆心坐标和半径 (x,y,r) :";
            cin >> x >> y >> r;
        }
        double area() const { return r*r*PI; }
};

练习

class Line: public Figure
{
    protected:
        double x1,y1,x2,y2;
    public:
        void draw() const
        {    //...... //画线
            cout<<"Line:"<<x1<<","<<y1<<":"<<x2<<","<<y2<<endl;
        }
        void input_data()
        {    cout << "请输入线段的起点和终点坐标 (x1,y1,x2,y2) :";
            cin >> x1 >> y1 >> x2 >> y2;
        }
};

练习

class Sphere:public Figure{
    protected:
        double x,y,z,r;
    public:
        void draw() const
        {    //
            cout<<"Sphere:"<<x<<","<<y<<","<<z<<":"<<r<<endl;
        }
        void input_data()
        {    cout << "请输入球的球心坐标和半径 (x,y,z,r) :";
            cin >> x >> y >> z>> r;
        }
};

练习

class Cone:public Figure{
    protected:
        double x,y,z,a,b,c,r;
    public:
        void draw() const
        {    //...... //画
            cout<<"Cone:"<<a<<","<<b<<","<<c<<","<<x<<","<<y<<","<<z<<":"<<r<<endl;
        }
        void input_data()
        {    cout << "请输入锥形的顶点坐标,圆心坐标和半径 (a,b,c,x,y,z,r) :";
            cin >>a>>b>>c>> x >> y >> z>> r;
        }
};

练习

int  main(){
    const int MAX_NUM_OF_FIGURES=3;
    Figure *figures[MAX_NUM_OF_FIGURES];
    int count=0;
    //图形数据的输入:
    for (count=0; count<MAX_NUM_OF_FIGURES;    count++)
    {    int shape;
        do
        {    cout << "请输入图形的种类(0:线段,1:矩形,2:圆,3:球形,4:圆锥,-1:结束):";
            cin >> shape;
        } while (shape < -1 || shape > 4);
        if (shape == -1) break;

练习

        switch (shape)
        {    case 0: //线
            figures[count] = new Line;    break;
            case 1: //矩形
            figures[count] = new Rectangle; break;
            case 2: //圆
            figures[count] = new Circle; break;
            case 3://球形
            figures[count] = new Sphere; break;
            case 4://圆锥
            figures[count] = new Cone; break;
        }
        figures[count]->input_data(); //动态绑定到相应类的input_data
    }
    //图形的输出:
    for (int i=0; i<count; i++)
        figures[i]->draw();
    //通过动态绑定调用相应类的draw。
    return 0;
}

继承中的类作用域

每个类定义自己的作用域在这个作用域内我们定义类的成员
当存在继承关系时派生类的作用域嵌套在其基类的作用域之内
- 如果一个名字在派生类的作用域内无法正确解析
  则编译器将继续在外层的基类作用域中寻找该名字的定义

即使派生类和基类的定义是相互分离开来的
因为类作用域有继承嵌套的关系
所以派生类才能像使用自己的成员一样使用基类的成员

Bulk_quote bulk;
cout << bulk.isbn();
//名字isbn的解析将按照下述过程所示:· 
//因为我们是通过Bulk_quote的对象调用isbn的,
    //所以首先在Bulk_quote中查找,这一步没有找到名字isbn。· 
//因为Bulk_quote是Disc_quote的派生类,所以接下来在Disc_quote中查找,仍然找不到。· 
//因为Disc_quote是Quote的派生类,所以接着查找Quote;
//此时找到了名字isbn,所以我们使用的isbn最终被解析为Quote中的isbn。

在编译时进行名字查找

一个对象引用或指针的静态类型决定了该对象的哪些成员是可见的
即使静态类型与动态类型可能不一致当使用基类的引用或指针时会发生这种情况),
但是我们能使用哪些成员仍然是由静态类型决定的
- 举个例子我们可以给Disc_quote添加一个新成员
  该成员返回一个存有最小或最大数量及折扣价格的pair
class Disc_quote : public Quote {
public:
    std::pair<size_t, double> discount_policy() const
        { return {quantity, discount}; }
    // other members as before
};
//我们只能通过Disc_quote及其派生类的对象、引用或指针使用discount_policy:
Bulk_quote bulk;
Bulk_quote *bulkP = &bulk; //  static and dynamic types are the same
Quote *itemP = &bulk;      //  static and dynamic types differ
bulkP->discount_policy();  //  ok: bulkP has type Bulk_quote*
itemP->discount_policy();  //  error: itemP has type Quote*
//尽管在bulk中确实含有一个名为discount_policy的成员,
//但是该成员对于itemP却是不可见的。itemP的类型是Quote的指针,意味着对
//discount_policy的搜索将从Quote开始。显然Quote不包含名为discount_policy的成员,
//所以我们无法通过Quote的对象、引用或指针调用discount_policy。

名字冲突与继承

和其他作用域一样派生类也能重用定义在其直接基类或间接基类中的名字
此时定义在内层作用域即派生类的名字将隐藏定义在外层作用域即基类的名字
struct Base {
    Base(): mem(0) { }
protected:
    int mem;
};
struct Derived : Base {
    Derived(int i): mem(i) { } // initializes Derived::mem to i
                               // Base::mem is default initialized
    int get_mem() { return mem; }  // returns Derived::mem
protected:
    int mem;   // hides mem in the base
};
//get_mem中mem引用的解析结果是定义在Derived中的名字,下面代码输出结果将是42。
Derived d(42);
cout << d.get_mem() << endl;       // prints 42

派生类的成员将隐藏同名的基类成员。

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

我们可以通过作用域运算符来使用一个被隐藏的基类成员
struct Derived : Base {
    int get_base_mem() { return Base::mem; }
    // ...
};
作用域运算符将覆盖掉原有的查找规则并指示编译器从Base类的作用域开始查找mem
如果使用最新的Derived版本运行上面的代码则d.get_mem()的输出结果将是0

除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。

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

理解函数调用的解析过程对于理解C++的继承至关重要
假定我们调用p->mem()(或者obj.mem()),则依次执行以下4个步骤
- 首先确定p或obj的静态类型
  因为我们调用的是一个成员所以该类型必然是类类型。· 
- 在p或obj的静态类型对应的类中查找mem
  如果找不到则依次在直接基类中不断查找直至到达继承链的顶端
  如果找遍了该类及其基类仍然找不到则编译器将报错。· 
- 一旦找到了mem就进行常规的类型检查以确认找到的mem本次调用是否合法。·
- 假设调用合法则编译器将根据调用的是否是虚函数而产生不同的代码
     如果mem是虚函数且我们是通过引用或指针进行的调用
      则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本
      依据是对象的动态类型
     反之如果mem不是虚函数或者我们是通过对象而非引用或指针进行的调用
      则编译器将产生一个常规函数调用

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

声明在内层作用域的函数并不会重载声明在外层作用域的函数
- 因此定义派生类中的函数也不会重载其基类中的成员
如果派生类即内层作用域的成员与基类即外层作用域的某个成员同名
- 则派生类将在其作用域内隐藏该基类成员
  即使派生类成员和基类成员的形参列表不一致基类成员也仍然会被隐藏掉
struct Base {
    int memfcn();
};
struct Derived : Base {
    int memfcn(int);   // hides memfcn in the base
};
Derived d; Base b;
b.memfcn();       //  calls Base::memfcn
d.memfcn(10);     //  calls Derived::memfcn
d.memfcn();       //  error: memfcn with no arguments is hidden
d.Base::memfcn(); //  ok: calls Base::memfcn

虚函数与作用域

我们现在可以理解为什么基类与派生类中的虚函数必须有相同的形参列表了
- 假如基类与派生类的虚函数接受的实参不同
  则我们就无法通过基类的引用或指针调用派生类的虚函数了例如

虚函数与作用域

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);      // parameter list differs from fcn in Base
    virtual void f2(); // new virtual function that does not exist in Base
};
class D2 : public D1 {
public:
    int fcn(int); // nonvirtual function hides D1::fcn(int)
    int fcn();    // overrides virtual fcn from Base
    void f2();    // overrides virtual f2 from D1
};
//D1的fcn函数并没有覆盖Base的虚函数fcn,原因是它们的形参列表不同。
//实际上,D1的fcn将隐藏Base的fcn。此时拥有了两个名为fcn的函数:
//一个是D1从Base继承而来的虚函数fcn;
//另一个是D1自己定义的接受一个int参数的非虚函数fcn。

通过基类调用隐藏的虚函数

给定上面定义的这些类后我们来看几种使用其函数的方法
Base bobj;  D1 d1obj; D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn(); // virtual call, will call Base::fcn at run time
bp2->fcn(); // virtual call, will call Base::fcn at run time
bp3->fcn(); // virtual call, will call D2::fcn at run time
D1 *d1p = &d1obj; D2 *d2p = &d2obj;
bp2->f2(); // error: Base has no member named f2
d1p->f2(); // virtual call, will call D1::f2() at run time
d2p->f2(); // virtual call, will call D2::f2() at run time
//前三条调用语句是通过基类的指针进行的,
//因为fcn是虚函数,所以编译器产生的代码将在运行时确定使用虚函数的哪个版本。
//判断的依据是该指针所绑定对象的真实类型。在bp2的例子中,实际绑定的对象是D1类型
//而D1并没有覆盖那个不接受实参的fcn,所以bp2进行的调用将在运行时解析为Base版本。

//接下来的三条调用语句是通过不同类型的指针进行的,
//每个指针分别指向继承体系中的一个类型。
//因为Base类中没有f2(),所以第一条语句是非法的,
//即使当前的指针碰巧指向了一个派生类对象也无济于事。

通过基类调用隐藏的虚函数

再观察一些对于非虚函数fcnint的调用语句
Base *p1 = &d2obj; D1 *p2 = &d2obj; D2 *p3 =  &d2obj;
p1->fcn(42);  // error: Base has no version of fcn that takes an int
p2->fcn(42);  // statically bound, calls D1::fcn(int)
p3->fcn(42);  // statically bound, calls D2::fcn(int)
//在上面的每条调用语句中,指针都指向了D2类型的对象,
//但是由于我们调用的是非虚函数,所以不会发生动态绑定。
//实际调用的函数版本由指针的静态类型决定。

覆盖重载的函数

和其他函数一样成员函数无论是否是虚函数都能被重载
派生类可以覆盖重载函数的0个或多个实例
如果派生类希望所有的重载版本对于它来说都是可见的
- 那么它就需要覆盖所有的版本或者一个也不覆盖

有时一个类仅需覆盖重载集合中的一些而非全部函数
此时如果我们不得不覆盖基类中的每一个版本的话显然操作将极其烦琐

一种好的解决方案是为重载的成员提供一条using声明语句
这样我们就无须覆盖基类中的每一个重载版本了
using声明语句指定一个名字而不指定形参列表
- 所以一条基类成员函数的using声明语句就可以把该函数的
  所有重载实例添加到派生类作用域中
  此时派生类只需要定义其特有的函数就可以了
  而无须为继承而来的其他函数重新定义

类内using声明的一般规则同样适用于重载函数的名字
基类函数的每个实例在派生类中都必须是可访问的
对派生类没有重新定义的重载版本的访问实际上是对using声明点的访问

练习

假设第550页的 D1 类需要覆盖它继承而来的 fcn 函数你应该如何对其进行修改
如果你修改之后 fcn 匹配了 Base 中的定义则该节的那些调用语句将如何解析

将D1的fcn函数更改 int fcn()
p2->fcn(42)将报错

构造函数与拷贝控制

位于继承体系中的类需要控制当其对象执行一系列操作时发生什么样的行为
这些操作包括创建拷贝移动赋值和销毁
如果一个类基类或派生类没有定义拷贝控制操作则编译器将为它合成一个版本
- 当然这个合成的版本也可以定义成被删除的函数

虚析构函数

类通常应该定义一个虚析构函数这样我们就能动态分配继承体系中的对象了
当我们delete一个动态分配的对象的指针时将执行析构函数
如果该指针指向继承体系中的某个类型
则有可能出现指针的静态类型与被删除对象的动态类型不符的情况
- 例如如果我们delete一个Quote类型的指针
  则该指针有可能实际指向了一个Bulk_quote类型的对象
  如果这样的话编译器就必须清楚它应该执行的是Bulk_quote的析构函数
我们通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本
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
};

虚析构函数

和其他虚函数一样析构函数的虚属性也会被继承
- 无论Quote的派生类使用合成的析构函数还是定义自己的析构函数都将是虚析构函数
只要基类的析构函数是虚函数就能确保delete基类指针时将运行正确的析构函数版本
Quote *itemP = new Quote;   //  same static and dynamic type
delete itemP;               //  destructor for Quote called
itemP = new Bulk_quote;     //  static and dynamic types differ
delete itemP;               //  destructor for Bulk_quote called

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

虚析构函数

之前我们曾介绍过一条经验准则
- 即如果一个类需要析构函数那么它也同样需要拷贝和赋值操作
基类的析构函数并不遵循上述准则它是一个重要的例外
- 一个基类总是需要析构函数而且它能将析构函数设定为虚函数
  此时该析构函数为了成为虚函数而令内容为空
  我们显然无法由此推断该基类还需要赋值运算符或拷贝构造函数

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

基类需要一个虚析构函数这一事实还会对基类和派生类的定义产生另外一个间接的影响
- 如果一个类定义了析构函数即使它通过=default的形式使用了合成的版本
  编译器也不会为这个类合成移动操作

练习

哪种类需要虚析构函数虚析构函数必须执行什么样的操作

基类通常应该定义一个虚析构函数
执行操作是清除本类中定义的数据成员
若本类没有定义指针成员使用合成版本即可
若本类定义了指针成员一般需要自定义析构函数对指针成员适当清除

合成拷贝控制与继承

基类或派生类的合成拷贝控制成员的行为
与其他合成的构造函数赋值运算符或析构函数类似
它们对类本身的成员依次进行初始化赋值或销毁的操作此外这些合成的成员还负责
使用直接基类中对应的操作对一个对象的直接基类部分进行初始化赋值或销毁的操作
例如
- 合成的Bulk_quote默认构造函数运行Disc_quote的默认构造函数
  后者又运行Quote的默认构造函数。· 
- Quote的默认构造函数将bookNo成员默认初始化为空字符串
  同时使用类内初始值将price初始化为0。· 
- Quote的构造函数完成后继续执行Disc_quote的构造函数
  它使用类内初始值初始化qty和discount。· 
- Disc_quote的构造函数完成后继续执行Bulk_quote的构造函数
  但是它什么具体工作也不做
类似的合成的Bulk_quote拷贝构造函数使用合成的Disc_quote拷贝构造函数
后者又使用合成的Quote拷贝构造函数
其中Quote拷贝构造函数拷贝bookNo和price成员
Disc_quote拷贝构造函数拷贝qty和discount成员

无论基类成员是合成的版本还是自定义的版本都没有太大影响
唯一的要求是相应的成员应该可访问并且不是一个被删除的函数

在我们的Quote继承体系中所有类都使用合成的析构函数
其中派生类隐式地使用而基类通过将其虚析构函数定义成=default而显式地使用
一如既往合成的析构函数体是空的其隐式的析构部分负责销毁类的成员
派生类的析构函数除了销毁派生类自己的成员还负责销毁派生类的直接基类
该直接基类又销毁它自己的直接基类以此类推直至继承链的顶端


如前所述Quote因为定义了析构函数而不能拥有合成的移动操作
因此当我们移动Quote对象时实际使用的是合成的拷贝操作
Quote没有移动操作意味着它的派生类也没有

派生类中删除的拷贝控制与基类的关系

就像其他任何类的情况一样基类或派生类也能出于同样的原因
将其合成的默认构造函数或者任何一个拷贝控制成员定义成被删除的函数
此外某些定义基类的方式也可能导致有的派生类成员成为被删除的函数
- 如果基类中的默认构造函数拷贝构造函数拷贝赋值运算符或析构函数是
  被删除的函数或者不可访问则派生类中对应的成员将是被删除的
  原因是编译器不能使用基类成员来执行派生类对象基类部分的构造赋值或销毁操作
- 如果在基类中有一个不可访问或删除掉的析构函数则派生类中合成的默认
  和拷贝构造函数将是被删除的因为编译器无法销毁派生类对象的基类部分。· 
- 和过去一样编译器将不会合成一个删除掉的移动操作
  当我们使用=default请求一个移动操作时如果基类中的对应操作是删除的或不可
  访问的那么派生类中该函数将是被删除的原因是派生类对象的基类部分不可移动
  同样如果基类的析构函数是删除的或不可访问的
  则派生类的移动构造函数也将是被删除的

派生类中删除的拷贝控制与基类的关系

class B {
public:
    B();
    B(const B&) = delete;
    // other members, not including a move constructor
};
class D : public B {
    // no constructors
};
D d;     // ok: D's synthesized default constructor uses B's default constructor
D d2(d); // error: D's synthesized copy constructor is deleted
D d3(std::move(d)); // error: implicitly uses D's deleted copy constructor
//基类B含有一个可访问的默认构造函数和一个显式删除的拷贝构造函数。
//因为我们定义了拷贝构造函数,所以编译器将不会为B合成一个移动构造函数
//因此,我们既不能移动也不能拷贝B的对象。如果B的派生类希望它自己的对象能被
//移动和拷贝,则派生类需要自定义相应版本的构造函数。当然,在这一过程中派生
//类还必须考虑如何移动或拷贝其基类部分的成员。在实际编程过程中,如果在基类
//中没有默认、拷贝或移动构造函数,则一般情况下派生类也不会定义相应的操作。

移动操作与继承

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

因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作
所以当我们确实需要执行移动操作时应该首先在基类中进行定义
//我们的Quote可以使用合成的版本,不过前提是Quote必须显式地定义这些成员。
//一旦Quote定义了自己的移动操作,那么它必须同时显式地定义拷贝操作
class Quote {
public:
    Quote() = default;             // memberwise default initialize
    Quote(const Quote&) = default; // memberwise copy
    Quote(Quote&&) = default;      // memberwise copy
    Quote& operator=(const Quote&) = default; // copy assign
    Quote& operator=(Quote&&) = default;      // move assign
    virtual ~Quote() = default;
    // other members as before
};
//通过上面的定义,我们就能对Quote的对象逐成员地
//分别进行拷贝、移动、赋值和销毁操作了。
//而且除非Quote的派生类中含有排斥移动的成员,否则它将自动获得合成的移动操作。

练习

我们为什么为 Disc_quote 定义一个默认构造函数
如果去掉该构造函数的话会对 Bulk_quote 的行为产生什么影响

因为Disc_quote的默认构造函数会运行Quote的默认构造函数
而Quote默认构造函数会完成成员的初始化工作
如果去除掉该构造函数的话
Bulk_quote的默认构造函数而无法完成Disc_quote的初始化工作

派生类的拷贝控制成员

派生类构造函数在其初始化阶段中不但要初始化派生类自己的成员
- 还负责初始化派生类对象的基类部分
派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时
- 也要拷贝和移动基类部分的成员
派生类赋值运算符也必须为其基类部分的成员赋值

和构造函数及赋值运算符不同的是析构函数只负责销毁派生类自己分配的资源
如前所述对象的成员是被隐式销毁的类似的派生类对象的基类部分也是自动销毁的

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

定义派生类的拷贝或移动构造函数

通常使用对应的基类构造函数初始化对象的基类部分
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, we must explicitly call that
    // constructor in the constructor initializer list
    D(const D& d): Base(d)      // copy the base members
                 /* initializers for members of D */ { /* ...  */ }
    D(D&& d): Base(std::move(d)) // move the base members
                 /* initializers for members of D */ { /* ...  */ }
};
//初始值Base(d)将一个D对象传递给基类构造函数。尽管从道理上来说,Base可以
//包含一个参数类型为D的构造函数,但是在实际编程过程中通常不会这么做。
//相反,Base(d)一般会匹配Base的拷贝构造函数。
//D类型的对象d将被绑定到该构造函数的Base&形参上。
//Base的拷贝构造函数负责将d的基类部分拷贝给要创建的对象。

定义派生类的拷贝或移动构造函数

//假如我们没有提供基类的初始值的话:
// probably incorrect definition of the D copy constructor
// base-class part is default initialized, not copied
D(const D& d) /* member initializers, but no base-class initializer    */
    { /* ...   */ }
//在上面的例子中,Base的默认构造函数将被用来初始化D对象的基类部分。
//假定D的构造函数从d中拷贝了派生类成员,则这个新构建的对象的配置将非常奇怪:
//它的Base成员被赋予了默认值,而D成员的值则是从其他对象拷贝得来的。

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

派生类赋值运算符

与拷贝和移动构造函数一样派生类的赋值运算符也必须显式地为其基类部分赋值
// Base::operator=(const Base&) is not invoked automatically
D &D::operator=(const D &rhs)
{
    Base::operator=(rhs); // assigns the base part
    // assign the members in the derived class, as usual,
    // handling self-assignment and freeing existing resources as appropriate
    return *this;
}
//上面的运算符首先显式地调用基类赋值运算符,令其为派生类对象的基类部分赋值。
//基类的运算符(应该可以)正确地处理自赋值的情况,如果赋值命令是正确的,
//则基类运算符将释放掉其左侧运算对象的基类部分的旧值,然后利用rhs为其赋一个新值。
//随后,我们继续进行其他为派生类成员赋值的工作。

无论基类的构造函数或赋值运算符是自定义的版本还是合成的版本
派生类的对应操作都能使用它们
//例如,对于Base::operator=的调用语句将执行Base的拷贝赋值运算符,
//至于该运算符是由Base显式定义的还是由编译器合成的无关紧要。

派生类析构函数

如前所述在析构函数体执行完成后对象的成员会被隐式销毁
类似的对象的基类部分也是隐式销毁的
和构造函数及赋值运算符不同派生类析构函数只负责销毁由派生类自己分配的资源
class D: public Base {
public:
    // Base::~Base invoked automatically
    ~D() { /* do what it takes to clean up derived members   */ }
};
对象销毁的顺序正好与其创建的顺序相反
派生类析构函数首先执行然后是基类的析构函数
以此类推沿着继承体系的反方向直至最后

在构造函数和析构函数中调用虚函数

派生类对象的基类部分将首先被构建
当执行基类的构造函数时该对象的派生类部分是未被初始化的状态
类似的销毁派生类对象的次序正好相反
因此当执行基类的析构函数时派生类部分已经被销毁掉了
由此可知当我们执行上述基类成员的时候该对象处于未完成的状态

为了能够正确地处理这种未完成状态
编译器认为对象的类型在构造或析构的过程中仿佛发生了改变一样
- 也就是说当我们构建一个对象时需要把对象的类和构造函数的类看作是同一个
对虚函数的调用绑定正好符合这种把对象的类和构造函数的类看成同一个的要求
对于析构函数也是同样的道理
上述的绑定不但对直接调用虚函数有效对间接调用也是有效的
这里的间接调用是指通过构造函数或析构函数调用另一个函数

在构造函数和析构函数中调用虚函数

不妨考虑当基类构造函数调用虚函数的派生类版本时会发生什么情况
这个虚函数可能会访问派生类的成员毕竟如果它不需要访问派生类成员的话
则派生类直接使用基类的虚函数版本就可以了
然而当执行基类构造函数时它要用到的派生类成员尚未初始化
如果我们允许这样的访问则程序很可能会崩溃

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

练习

定义 Quote  Bulk_quote 的拷贝控制成员令其与合成的版本行为一致
为这些成员以及其他构造函数添加打印状态的语句使得我们能够知道正在运行哪个程序
使用这些类编写程序预测程序将创建和销毁哪些对象
重复实验不断比较你的预测和实际输出结果是否相同直到预测完全准确再结束

#include<iostream>
#include<string>

class Quote
{
public:
    Quote() = default;
    Quote(const std::string &book, double sales_price) :
        bookNo(book), price(sales_price) {
            std::cout<<"Quote(const std::string &book, double sales_price)"
                <<std::endl;
        }
    std::string isbn() const { return bookNo; }

练习

    virtual double net_price(std::size_t n) const;
    virtual ~Quote() {std::cout<<"~Quote()"<<std::endl; }
    virtual std::ostream & debug(std::ostream & os) const;

    Quote(const Quote &q) : bookNo(q.bookNo), price(q.price) {
        std::cout << "Quote(const Quote &q)" << std::endl;  }
    Quote(Quote && q) : bookNo(std::move(q.bookNo)),  price(std::move(q.price)) {
                std::cout << "Quote(Quote && q)" << std::endl; }
    Quote & operator=(const Quote &);
    Quote & operator=(Quote &&);

private:
    std::string bookNo;

protected:
    double price = 0.0;
};

练习

class Disc_quote : public Quote
{
public:
    Disc_quote() = default;
    Disc_quote(const std::string & book, double price,
        std::size_t qty, double disc) :
        Quote(book, price), quantity(qty), discount(disc) {
            std::cout<<"Disc_quote(const std::string & book, double price,"
               <<"std::size_t qty, double disc) "<<std::endl;
        }

    std::ostream & debug(std::ostream & os) const override;
    double net_price(std::size_t) const override= 0;

    Disc_quote(const Disc_quote &q) :
        Quote(q), quantity(q.quantity), discount(q.discount) {
            std::cout << "Disc_quote(const Disc_quote &q)" << std::endl; }

练习

    Disc_quote(Disc_quote && q) :
        Quote(std::move(q)), quantity(std::move(q.quantity)),
        discount(std::move(q.discount)) {
            std::cout << "Disc_quote(Disc_quote && q)" << std::endl; }
    Disc_quote & operator=(const Disc_quote &);
    Disc_quote & operator=(Disc_quote &&);
    ~Disc_quote()
    {
        std::cout << "~Disc_quote()"<<std::endl;
    }


protected:
    std::size_t quantity = 0;
    double discount = 0.0;
};

练习

class Bulk_quote : public Disc_quote
{
public:
    Bulk_quote() = default;
    Bulk_quote(const std::string & book, double price,
            std::size_t qty, double disc) :
        Disc_quote(book, price, qty, disc) {
            std::cout<<"Bulk_quote(const std::string & book,"<<
            "double price, std::size_t qty, double disc) "<<std::endl; }

    double net_price(std::size_t) const override;

    Bulk_quote(const Bulk_quote &q) : Disc_quote(q) {
        std::cout << "Bulk_quote(const Bulk_quote &q)" << std::endl; }
    Bulk_quote(Bulk_quote && q) : Disc_quote(std::move(q)) {
        std::cout << "Bulk_quote(Bulk_quote && q)" << std::endl; }
    Bulk_quote & operator=(const Bulk_quote &);
    Bulk_quote & operator=(Bulk_quote &&);

练习

    ~Bulk_quote() override
    {
        std::cout << "~Bulk_quote() "<<std::endl;
    }
};

double print_total(std::ostream & os, const Quote & item, std::size_t cnt);

double Quote::net_price(std::size_t n) const
{
    return n * price;
}

double print_total(std::ostream & os, const Quote & item, std::size_t n)
{
    double ret = item.net_price(n);
    os << "ISBN: " << item.isbn() << " # sold: " << n << " total due: " << ret << std::endl;
    return ret;
}

练习

double Bulk_quote::net_price(std::size_t cnt) const
{
    if(cnt >= quantity)
        return cnt * (1 - discount) * price;
    else
        return cnt * price;
}

std::ostream & Quote::debug(std::ostream & os) const
{
    os << bookNo << " " << price;
    return os;
}

std::ostream & Disc_quote::debug(std::ostream & os) const
{
    Quote::debug(os) << " " << quantity << " " << discount;
    return os;
}

练习

Quote & Quote::operator=(const Quote & rhs)
{
    std::cout << "Quote & Quote::operator=(const Quote & rhs)" << std::endl;
    bookNo = rhs.bookNo;
    price = rhs.price;
    return *this;
}

Quote & Quote::operator=(Quote && rhs)
{
    std::cout << "Quote & Quote::operator=(Quote && rhs)" << std::endl;
    bookNo = std::move(rhs.bookNo);
    price = std::move(rhs.price);
    return *this;
}

练习

Disc_quote & Disc_quote::operator=(const Disc_quote & rhs)
{
    Quote::operator=(rhs);
    std::cout << "Disc_quote & Disc_quote::operator=(const Disc_quote & rhs)"
        << std::endl;
    quantity = rhs.quantity;
    discount = rhs.discount;
    return *this;
}

Disc_quote & Disc_quote::operator=(Disc_quote && rhs)
{
    Quote::operator=(std::move(rhs));
    std::cout << "Disc_quote & Disc_quote::operator=(Disc_quote && rhs)" <<
        std::endl;
    quantity = rhs.quantity;
    discount = rhs.discount;
    return *this;
}

练习

Bulk_quote & Bulk_quote::operator=(const Bulk_quote & rhs)
{
    Disc_quote::operator=(rhs);
    std::cout << "Bulk_quote & Bulk_quote::operator=(const Bulk_quote & rhs)"
        << std::endl;
    return *this;
}

Bulk_quote & Bulk_quote::operator=(Bulk_quote && rhs)
{
    Disc_quote::operator=(std::move(rhs));
    std::cout << "Bulk_quote & Bulk_quote::operator=(Bulk_quote && rhs)"
        << std::endl;
    return *this;
}

练习

int main()
{
    Bulk_quote b("abc",1,1,1);
    Bulk_quote b2(b);//Bulk_quote b2=b;
    b=b2;
    b=std::move(b2);
    return 0;
}

练习

输出
Quote(const std::string &book, double sales_price)
Disc_quote(const std::string & book, double price,std::size_t qty, double disc) 
Bulk_quote(const std::string & book,double price, std::size_t qty, double disc) 
Quote(const Quote &q)
Disc_quote(const Disc_quote &q)
Bulk_quote(const Bulk_quote &q)
Quote & Quote::operator=(const Quote & rhs)
Disc_quote & Disc_quote::operator=(const Disc_quote & rhs)
Bulk_quote & Bulk_quote::operator=(const Bulk_quote & rhs)
Quote & Quote::operator=(Quote && rhs)
Disc_quote & Disc_quote::operator=(Disc_quote && rhs)
Bulk_quote & Bulk_quote::operator=(Bulk_quote && rhs)
~Bulk_quote() 
~Disc_quote()
~Quote()
~Bulk_quote() 
~Disc_quote()
~Quote()

继承的构造函数

在C++11新标准中派生类能够重用其直接基类定义的构造函数
尽管如我们所知这些构造函数并非以常规的方式继承而来
但是为了方便我们不妨姑且称其为继承
一个类只初始化它的直接基类出于同样的原因一个类也只继承其直接基类的构造函数
类不能继承默认拷贝和移动构造函数
如果派生类没有直接定义这些构造函数则编译器将为派生类合成它们
派生类继承基类构造函数的方式是提供一条注明了直接基类名的using声明语句
举个例子我们可以重新定义Bulk_quote类令其继承Disc_quote类的构造函数
class Bulk_quote : public Disc_quote {
public:
    using Disc_quote::Disc_quote; // inherit Disc_quote's constructors
    double net_price(std::size_t) const;
};

继承的构造函数

通常情况下using声明语句只是令某个名字在当前作用域内可见
而当作用于构造函数时using声明语句将令编译器产生代码
对于基类的每个构造函数编译器都生成一个与之对应的派生类构造函数
换句话说对于基类的每个构造函数
编译器都在派生类中生成一个形参列表完全相同的构造函数
这些编译器生成的构造函数形如
derived(parms) : base(args) { }
//其中,derived是派生类的名字,base是基类的名字,parms是构造函数的形参列表,
//args将派生类构造函数的形参传递给基类的构造函数。
//在我们的Bulk_quote类中,继承的构造函数等价于:
Bulk_quote(const std::string& book, double price,
          std::size_t qty, double disc):
      Disc_quote(book, price, qty, disc) { }
如果派生类含有自己的数据成员则这些成员将被默认初始化

继承的构造函数的特点

和普通成员的using声明不同一个构造函数的using声明不会改变该构造函数的访问级别
- 例如不管using声明出现在哪儿
  基类的私有构造函数在派生类中还是一个私有构造函数
  受保护的构造函数和公有构造函数也是同样的规则
而且一个using声明语句不能指定explicit或constexpr
- 如果基类的构造函数是explicit或者constexpr则继承的构造函数也拥有相同的属性
当一个基类构造函数含有默认实参时这些实参并不会被继承
- 相反派生类将获得多个继承的构造函数
  其中每个构造函数分别省略掉一个含有默认实参的形参
- 例如如果基类有一个接受两个形参的构造函数其中第二个形参含有默认实参
  则派生类将获得两个构造函数一个构造函数接受两个形参没有默认实参),
  另一个构造函数只接受一个形参它对应于基类中最左侧的没有默认值的那个形参

继承的构造函数的特点

如果基类含有几个构造函数
则除了两个例外情况大多数时候派生类会继承所有这些构造函数
- 第一个例外是派生类可以继承一部分构造函数而为其他构造函数定义自己的版本
  如果派生类定义的构造函数与基类的构造函数具有相同的参数列表
  则该构造函数将不会被继承定义在派生类中的构造函数将替换继承而来的构造函数
- 第二个例外是默认拷贝和移动构造函数不会被继承这些构造函数按照正常规则被合成
  继承的构造函数不会被作为用户定义的构造函数来使用
  因此如果一个类只含有继承的构造函数则它也将拥有一个合成的默认构造函数

练习

重新定义你的Bulk_quote类令其继承构造函数
class Bulk_quote:public Disc_quote{
public:
    using Disc_quote::Disc_quote;
//其它成员
}

容器与继承

当我们使用容器存放继承体系中的对象时通常必须采取间接存储的方式
因为不允许在容器中保存不同类型的元素
所以我们不能把具有继承关系的多种类型的对象直接存放在容器当中

假定我们想定义一个vector令其保存用户准备购买的几种书籍
- 显然我们不应该用vector保存Bulk_quote对象
  因为我们不能将Quote对象转换成Bulk_quote
- 我们也不应该使用vector保存Quote对象此时虽然我们可以
  把Bulk_quote对象放置在容器中但是这些对象再也不是Bulk_quote对象了
vector<Quote> basket;
basket.push_back(Quote("0-201-82470-1", 50));
// ok, but copies only the Quote part of the object into basket
basket.push_back(Bulk_quote("0-201-54848-8", 50, 10, .25));
// calls version defined by Quote, prints 750, i.e., 15 * $50
cout << basket.back().net_price(15) << endl;
//basket的元素是Quote对象,因此当我们向该vector中添加一个Bulk_quote对象时,
//它的派生类部分将被忽略掉
当派生类对象被赋值给基类对象时其中的派生类部分将被切掉”,
因此容器和存在继承关系的类型无法兼容

在容器中放置(智能)指针而非对象

当我们希望在容器中存放具有继承关系的对象时
我们实际上存放的通常是基类的指针更好的选择是智能指针)。
和往常一样这些指针所指对象的动态类型可能是基类类型也可能是派生类类型
vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("0-201-82470-1", 50));
basket.push_back(
    make_shared<Bulk_quote>("0-201-54848-8", 50, 10, .25));
// calls the version defined by Quote; prints 562.5, i.e., 15 * $50 less the discount
cout << basket.back()->net_price(15) << endl;
//因为basket存放着shared_ptr,所以我们必须解引用basket.back()的返回值
//以获得运行net_price的对象。我们通过在net_price的调用中使用->以达到这个目的。
//如我们所知,实际调用的net_price版本依赖于指针所指对象的动态类型。

//正如我们可以将一个派生类的普通指针转换成基类指针一样
//我们也能把一个派生类的智能指针转换成基类的智能指针。
//在此例中,make_shared<Bulk_quote>返回一个shared_ptr<Bulk_quote>对象,
//当我们调用push_back时该对象被转换成shared_ptr<Quote>。

练习

定义一个存放 Quote 对象的 vector Bulk_quote 对象传入其中
计算 vector 中所有元素总的 net_price
#include<iostream>
#include<string>
#include<vector>
#include<memory>
#include "Quote.h"
int main(){
    std::vector<Quote> v;
    for (unsigned i = 1; i != 10; ++i)
        v.push_back(Bulk_quote("abc", 10 , 10, 0.1));
    double total = 0;
    for (const auto& b : v){
        total += b.net_price(20);
    }
    std::cout << total << std::endl;
    std::cout << "======================\n\n";

练习

    std::vector<std::shared_ptr<Quote>> pv;
    for (unsigned i = 1; i != 10; ++i)
        pv.push_back(std::make_shared<Bulk_quote>(
                        Bulk_quote("abc",   10, 10, 0.1)));
    double total_p = 0;
    for (auto p : pv)    {
        total_p += p->net_price(20);
    }
    std::cout << total_p << std::endl;
    return 0;
}

    // 1800
    //1620

练习

再运行一次你的程序这次传入 Quote 对象的 shared_ptr 
如果这次计算出的总额与之前的不一致解释为什么;如果一直也请说明原因

因为智能指针导致了多态性的产生所以这次计算的总额不一致

编写Basket类

对于C++面向对象的编程来说一个悖论是我们无法直接使用对象进行面向对象编程
相反我们必须使用指针和引用
因为指针会增加程序的复杂性所以我们经常定义一些辅助的类来处理这种复杂情况
首先我们定义一个表示购物篮的类
class Basket {
public:
    // Basket uses synthesized default constructor and copy-control members
    void add_item(const std::shared_ptr<Quote> &sale)
        { items.insert(sale); }
    // prints the total price for each book and the overall total for all items in the basket
    double total_receipt(std::ostream&) const;
private:
    // function to compare shared_ptrs needed by the multiset member
    static bool compare(const std::shared_ptr<Quote> &lhs,
                        const std::shared_ptr<Quote> &rhs)
    { return lhs->isbn() < rhs->isbn(); }
    // multiset to hold multiple quotes, ordered by the compare member
    std::multiset<std::shared_ptr<Quote>, decltype(compare)*>
                  items{compare};
};

编写Basket类

//我们的类使用一个multiset来存放交易信息,这样我们就能保存同一本书的多条交易记录
//而且对于一本给定的书籍,它的所有交易信息都保存在一起
//multiset的元素是shared_ptr。因为shared_ptr没有定义小于运算符,
//所以为了对元素排序我们必须提供自己的比较运算符。在此例中,我们定义了一个
//名为compare的私有静态成员,该成员负责比较shared_ptr所指的对象的isbn。
//我们初始化multiset,通过类内初始值调用比较函数
// multiset to hold multiple quotes, ordered by the compare member
std::multiset<std::shared_ptr<Quote>, decltype(compare)*>
              items{compare};
//定义了一个指向Quote对象的shared_ptr的multiset。
//这个multiset将使用一个与compare成员类型相同的函数来对其中的元素进行排序。
//multiset成员的名字是items,我们初始化items并令其使用我们的compare函数。

定义Basket的成员

Basket类只定义两个操作
//第一个成员是我们在类的内部定义的add_item成员,
//该成员接受一个指向动态分配的Quote的shared_ptr,
//然后将这个shared_ptr放置在multiset中。
//第二个成员的名字是total_receipt,它负责将购物篮的内容逐项打印成清单,
//然后返回购物篮中所有物品的总价格:

定义Basket的成员

double Basket::total_receipt(ostream &os) const
{
    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's at least one element with this key in the Basket
        // print the line item for this book
        sum += print_total(os, **iter, items.count(*iter));
    }
    os << "Total Sale: " << sum << endl; // print the final overall total
    return sum;
}
//与通常的循环语句依次读取每个元素不同,我们直接令iter指向下一个关键字,
//调用upper_bound函数可以令我们跳过与当前关键字相同的所有元素。
//对于upper_bound函数来说,它返回的是一个迭代器,该迭代器指向所有与iter关键字
//相等的元素中最后一个元素的下一位置。因此,我们得到的迭代器或者指向集合的
//末尾,或者指向下一本书籍。

在for循环内部我们通过调用print_total来打印购物篮中每本书籍的细节
sum += print_total(os, **iter, items.count(*iter));
//**iter是一个Quote对象(或者Quote的派生类的对象)。
//我们使用multiset的count成员来统计在multiset中有多少元素的键值相同(即ISBN相同)。

//print_total调用了虚函数net_price,因此最终的计算结果依赖于**iter的动态类型。
//print_total函数打印并返回给定书籍的总价格,我们把这个结果添加到sum当中,
//最后当循环结束后打印sum。

隐藏指针

Basket的用户仍然必须处理动态内存原因是add_item需要接受一个shared_ptr参数
因此用户不得不按照如下形式编写代码
Basket bsk;
bsk.add_item(make_shared<Quote>("123", 45));
bsk.add_item(make_shared<Bulk_quote>("345", 45, 3, .15));
下一步是重新定义add_item使得它接受一个Quote对象而非shared_ptr
新版本的add_item将负责处理内存分配这样它的用户就不必再受困于此了
我们将定义两个版本一个拷贝它给定的对象另一个则采取移动操作
void add_item(const Quote& sale);  // copy the given object
void add_item(Quote&& sale);       // move the given object

唯一的问题是add_item不知道要分配的类型
当add_item进行内存分配时它将拷贝或移动它的sale参数
在某处可能会有一条如下形式的new表达式
new Quote(sale)
不幸的是这条表达式所做的工作可能是不正确的new为我们请求的类型分配内存
因此这条表达式将分配一个Quote类型的对象并且拷贝sale的Quote部分
然而sale实际指向的可能是Bulk_quote对象此时该对象将被迫切掉一部分

模拟虚拷贝

为了解决上述问题我们给Quote类添加一个虚函数该函数将申请一份当前对象的拷贝
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::move(*this));}
    // other members as before
};
class Bulk_quote : public Quote {
    Bulk_quote* clone() const & {return new Bulk_quote(*this);}
    Bulk_quote* clone() && {return new Bulk_quote(std::move(*this));}
    // other members as before
};
因为我们拥有add_item的拷贝和移动版本所以我们分别定义clone的左值和右值版本
每个clone函数分配当前类型的一个新对象其中
const左值引用成员将它自己拷贝给新分配的对象右值引用成员则将自己移动到新数据中

可以使用clone很容易地写出新版本的add_item:

class Basket {
public:
    void add_item(const Quote& sale) // copy the given object
      { items.insert(std::shared_ptr<Quote>(sale.clone())); }
    void add_item(Quote&& sale)      // move the given object
      { items.insert(std::shared_ptr<Quote>(std::move(sale).clone())); }
    // other members as before
};
//和add_item本身一样,clone函数也根据作用于左值还是右值而分为不同的重载版本。
//- 在此例中,第一个add_item函数调用clone的const左值版本,
//- 第二个函数调用clone的右值引用版本。
//在右值版本中,尽管sale的类型是右值引用类型,但实际上sale本身(和任何其他
//变量一样)是个左值。因此,我们调用move把一个右值引用绑定到sale上。

//我们的clone函数也是一个虚函数。sale的动态类型(通常)决定了到底运行Quote的
//函数还是Bulk_quote的函数。无论我们是拷贝还是移动数据,clone都返回一个新分配
//对象的指针,该对象与clone所属的类型一致。我们把一个shared_ptr绑定到这个对象上,
//然后调用insert将这个新分配的对象添加到items中。注意,因为shared_ptr支持派生
//类向基类的类型转换,所以我们能把shared_ptr<Quote>绑定到Bulk_quote*上。

练习

编写你自己的Basket类用它计算上一个练习中交易记录的总价格
//Basket.h:
#ifndef Basket_h
#define Basket_h
#include "Quote.h"
#include <set>
#include <memory>
class Basket
{
public:
    // Basket使用合成的默认构造函数和拷贝控制成员
    // copy verison
    void add_item(const Quote& sale)
    {
        items.insert(std::shared_ptr<Quote>(sale.clone()));
    }

练习

    // move version
    void add_item(Quote&& sale)
    {
        items.insert(std::shared_ptr<Quote>(std::move(sale).clone()));
    }
    // 打印每本书的总价和购物篮中所有书的总价
    double total_receipt(std::ostream& os) const;
private:
    // function to compare needed by the multiset member
    static bool compare(const std::shared_ptr<Quote>& lhs,
        const std::shared_ptr<Quote>& rhs)
    {
        return lhs->isbn() < rhs->isbn();
    }
    // hold multiple quotes, ordered by the compare member
    std::multiset<std::shared_ptr<Quote>, decltype(compare)*>
        items{ compare };
};
#endif

练习

//Basket cpp:
#include "Basket.h"
double Basket::total_receipt(std::ostream &os) const
{
    double sum = 0.0;  // 保存实时计算出的总价格
    // iter指向ISBN相同的一批元素中的第一个
    // upper_bound返回一个迭代器,该迭代器指向这批元素的尾后位置
    for (auto iter = items.cbegin(); iter != items.cend();
        iter = items.upper_bound(*iter))
        //  ^^^^^^^^^^^^^^^^^^^^^^^^^^^
        // @note   this increment moves iter to the first element with key
        //         greater than  *iter.
    {
        sum += print_total(os, **iter, items.count(*iter));
    }                                   // ^^^^^^^^^^^^^ using count to fetch
    // the number of the same book.
    os << "Total Sale: " << sum << std::endl;
    return sum;
}

练习

//main:
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <fstream>
#include "Basket.h"
int main()
{
    Basket basket;

    for (unsigned i = 0; i != 10; ++i)
        basket.add_item(Bulk_quote("abc", 10, 10, 0.3));

    for (unsigned i = 0; i != 10; ++i)
        basket.add_item(Bulk_quote("def", 20, 20, 0.4));

    for (unsigned i = 0; i != 10; ++i)
        basket.add_item(Quote("ghi", 40));

练习

    std::ofstream log("log.txt", std::ios_base::app | std::ios_base::out);

    basket.total_receipt(log);
    return 0;
}
//g++ test15_30.cpp Basket.cpp Quote.cpp 
$ cat log.txt 
ISBN: abc # sold: 10 total due: 70
ISBN: def # sold: 10 total due: 200
ISBN: ghi # sold: 10 total due: 400
Total Sale: 670

文本查询程序再探

我们扩展12.3节的文本查询程序用它作为说明继承的最后一个例子
在上一版的程序中我们可以查询在文件中某个指定单词的出现情况
我们将在本节扩展该程序使其支持更多更复杂的查询操作
在后面的例子中我们将针对下面这个小故事展开查询
Alice Emma has long flowing red hair.
Her Daddy says when the wind blows
through her hair,it looks almost alive,
like a fiery bird in flight.
A beautiful fiery bird,he tells her,
magical but untamed.
"Daddy,shush,there is no such thing,"
she tells him,at the same time wanting
him to tell her more.
Shyly,she asks,"I mean,Daddy,is there?"

文本查询程序再探

我们的系统将支持如下查询形式。· 

- 单词查询用于得到匹配某个给定string的所有行
Executing Query for: Daddy
Daddy occurs 3 times
(line 2) Her Daddy says when the wind blows
(line 7) "Daddy,shush,there is no such thing,"
(line 10) Shyly,she asks,"I mean,Daddy,is there?"

- 逻辑非查询使用~运算符得到不匹配查询条件的所有行
Executing Query for: (Alice)
(Alice) occurs 9 times
(line 2) Her Daddy says when the wind blows
(line 3) through her hair,it looks almost alive,
(line 4) like a fiery bird in flight....

文本查询程序再探

- 逻辑或查询使用 | 运算符返回匹配两个条件中任意一个的行
Executing Query for: (hair | Alice)
(hair | Alice) occurs 2 times
(line 1) Alice Emma has long flowing red hair.
(line 3) through her hair,it looks almost alive,

- 逻辑与查询使用&运算符返回匹配全部两个条件的行
Executing query for: (hair &Alice)
(hair &Alice) occurs 1 time
(line 1) Alice Emma has long flowing red hair.

文本查询程序再探

此外我们还希望能够混合使用这些运算符比如
fiery &bird | wind
在类似这样的例子中我们将使用C++通用的优先级规则对复杂表达式求值
因此这条查询语句所得行应该是如下二者之一在该行中或者fiery和bird同时出现
或者出现了wind
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,
在输出内容中首先是那条查询语句我们使用圆括号来表示查询被解释和执行的次序
接下来系统将按照查询结果中行号的升序显示结果并且每一行只显示一次

面向对象的解决方案

我们可能会认为使用TextQuery类来表示单词查询然后从该类中派生出其他查询是
一种可行的方案然而这样的设计实际上存在缺陷
为了理解其中的原因我们不妨考虑逻辑非查询
单词查询查找一个指定的单词为了让逻辑非查询按照单词查询的方式执行
我们将不得不定义逻辑非查询所要查找的单词但是在一般情况下我们无法得到这样的单词
相反一个逻辑非查询中含有一个结果值需要取反的查询语句单词查询或任何其他查询
类似的一个逻辑与查询和一个逻辑或查询各包含两个结果值需要合并的查询语句

由上述观察结果可知我们应该将几种不同的查询建模成相互独立的类
这些类共享一个公共基类
WordQuery //Daddy
NotQuery  //~Alice
OrQuery   //hair | Alice
AndQuery  //hair & Alice

面向对象的解决方案

这些类将只包含两个操作
- eval接受一个TextQuery对象并返回一个QueryResult
  eval函数使用给定的TextQuery对象查找与之匹配的行。· 
- rep返回基础查询的string表示形式
  eval函数使用rep创建一个表示匹配结果的QueryResult
  输出运算符使用rep打印查询表达式

关键概念:继承与组合

继承体系的设计本身是一个非常复杂的问题已经超出了范围
然而有一条设计准则非常重要也非常基础

当我们令一个类公有地继承另一个类时派生类应当反映与基类的是一种Is A)”关系
在设计良好的类体系当中公有派生类的对象应该可以用在任何需要基类对象的地方

类型之间的另一种常见关系是有一个Has A)”关系具有这种关系的类暗含成员的意思

//在我们的书店示例中,基类表示的是按规定价格销售的书籍的报价。
//Bulk_quote“是一种”报价结果,只不过它使用的价格策略不同。
//我们的书店类都“有一个”价格成员和ISBN成员。

抽象基类

在这四种查询之间并不存在彼此的继承关系从概念上来说它们互为兄弟
因为所有这些类都共享同一个接口所以我们需要定义一个抽象基类来表示该接口
//我们将所需的抽象基类命名为Query_base,以此来表示它的角色是整个查询继承体系的根节点。

Query_base类将把eval和rep定义成纯虚函数其他代表某种特定查询类型的类必须
覆盖这两个函数我们将从Query_base直接派生出WordQuery和NotQuery
AndQuery和OrQuery都具有系统中其他类所不具备的一个特殊属性
- 它们各自包含两个运算对象
- 为了对这种属性建模我们定义另外一个名为BinaryQuery的抽象基类
  该抽象基类用于表示含有两个运算对象的查询
  AndQuery和OrQuery继承自BinaryQuery而BinaryQuery继承自Query_base

由这些分析我们将得到如图所示的类设计结果:

Query_base继承体系

将层次关系隐藏于接口类中

为了使程序能正常运行我们必须首先创建查询命令最简单的办法是编写C++表达式
例如可以编写下面的代码来生成之前描述的复合查询
Query q = Query("fiery") & Query("bird") | Query("wind");
我们将定义一个名为Query的接口类由它负责隐藏整个继承体系
Query类将保存一个Query_base指针该指针绑定到Query_base的派生类对象上
Query类与Query_base类提供的操作是相同的
- eval用于求查询的结果rep用于生成查询的string版本
- 同时Query也会定义一个重载的输出运算符用于显示查询

用户将通过Query对象的操作间接地创建并处理Query_base对象
我们定义Query对象的三个重载运算符以及一个接受string参数的Query构造函数
这些函数动态分配一个新的Query_base派生类的对象
    &运算符生成一个绑定到新的AndQuery对象上的Query对象;· 
    |运算符生成一个绑定到新的OrQuery对象上的Query对象;· 
    ~运算符生成一个绑定到新的NotQuery对象上的Query对象;· 
    接受string参数的Query构造函数生成一个新的WordQuery对象

使用Query表达式创建的对象

理解这些类的工作机理

在这个应用程序中很大一部分工作是构建代表用户查询的对象
- 对于读者来说认识到这一点非常重要
- 例如像上面这样的表达式将生成一系列相关对象的集合
一旦对象树构建完成后对某一条查询语句的求值或生成表示形式的过程基本上
就转换为沿着箭头方向依次对每个对象求值或显示的过程由编译器为我们组织管理
- 例如如果我们对q即树的根节点调用eval函数则该调用语句将令q所指的
  OrQuery对象eval它自己对该OrQuery求值实际上是对它的两个运算对象执行eval操作
  一个运算对象是AndQuery另一个是查找单词wind的WordQuery
  接下来对AndQuery求值转化为对它的两个WordQuery求值
  分别生成单词fiery和bird的查询结果

对于面向对象编程的新手来说要想理解一个程序最困难的部分往往是理解程序的设计思路
一旦你掌握了程序的设计思路接下来的实现也就水到渠成了

Query程序设计用到的类

Query程序接口类和操作   
 TextQuery      该类读入给定的文件并构建一个查找图包含一个query操作
                它接受一个string实参返回一个QueryResult对象
                该QueryResult对象表示string出现的行 
 QueryResult    该类保存一个query操作的结果 
 Query          是一个接口类指向Query_base派生类的对象 
 Query q(s)     将Query对象q绑定到一个存放着string s的新WordQuery对象上 
 q1 & q2        返回一个Query对象该Query绑定到一个存放q1和q2的新AndQuery对象上 
 q1  q2         返回一个Query对象该Query绑定到一个存放q1和q2的新OrQuery对象上 
 ~q             返回一个Query对象该Query绑定到一个存放q的新NotQuery对象上 

Query程序实现类   
 Query_base     查询类的抽象基类 
 WordQuery      Query_base的派生类用于查找一个给定的单词 
 NotQuery       Query_base的派生类用于查找一个给定的单词 
 BinaryQuery    Query_base的派生类查询结果是Query运算对象没有出现的行的集合 
 OrQuery        Query_base的派生类返回它的两个运算对象分别出现的行的并集 
 AndQuery       Query_base的派生类返回它的两个运算对象分别出现的行的交集 

练习

已知s1s2s3和s4都是string判断下面的表达式分别创建了什么样的对象
(a) Query(s1) | Query(s2) & ~Query(s3);
(b) Query(s1) | (Query(s2) & ~Query(s3));
(c) (Query(s1) & (Query(s2)) | (Query(s3) & Query(s4)));

(a) OrQuery, AndQuery, NotQuery, WordQuery
(b) OrQuery, AndQuery, NotQuery, WordQuery
(c) OrQuery, AndQuery, WordQuery

Query_base类和Query类

开始程序的实现过程首先定义Query_base类
// 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::line_no; // used in the eval functions
    virtual ~Query_base() = default;
private:
    // eval returns the QueryResult that matches this Query
    virtual QueryResult eval(const TextQuery&) const = 0;
    // rep is a string representation of the query
    virtual std::string rep() const = 0;
};
eval和rep都是纯虚函数因此Query_base是一个抽象基类
因为我们不希望用户或者派生类直接使用Query_base所以它没有public成员
所有对Query_base的使用都需要通过Query对象
因为Query需要调用Query_base的虚函数所以我们将Query声明成Query_base的友元

Query类

Query类对外提供接口同时隐藏了Query_base的继承体系
每个Query对象都含有一个指向Query_base对象的shared_ptr
因为Query是Query_base的唯一接口所以Query必须定义自己的eval和rep版本

接受一个string参数的Query构造函数将创建一个新的WordQuery对象
然后将它的shared_prt成员绑定到这个新创建的对象上
&|~运算符分别创建AndQueryOrQuery和NotQuery对象
这些运算符将返回一个绑定到新创建的对象上的Query对象
为了支持这些运算符Query还需要另外一个构造函数
- 它接受指向Query_base的shared_ptr并且存储给定的指针
我们将这个构造函数声明为私有的
- 原因是我们不希望一般的用户代码能随便定义Query_base对象
因为这个构造函数是私有的所以我们需要将三个运算符声明为友元

形成了上述设计思路后,Query类本身就比较简单了

// 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&(const Query&, const Query&);
public:
    Query(const std::string&);  // builds a new WordQuery
    // interface functions: call the corresponding Query_base operations
    QueryResult eval(const TextQuery &t) const
                            { return q->eval(t); }
    std::string rep() const { return q->rep(); }
private:
    Query(std::shared_ptr<Query_base> query): q(query) { }
    std::shared_ptr<Query_base> q;
};

Query类

我们首先将创建Query对象的运算符声明为友元
之所以这么做是因为这些运算符需要访问那个私有构造函数

在Query的公有接口部分我们声明了接受string的构造函数不过没有对其进行定义
因为这个构造函数将要创建一个WordQuery对象所以我们应该首先定义WordQuery类
随后才能定义接受string的Query构造函数

另外两个公有成员是Query_base的接口
其中Query操作使用它的Query_base指针来调用各自的Query_base虚函数
实际调用哪个函数版本将由q所指的对象类型决定并且直到运行时才能最终确定下来

Query的输出运算符

输出运算符可以很好地解释我们的整个查询系统是如何工作的
std::ostream & operator<<(std::ostream &os, const Query &query)
{
    // Query::rep makes a virtual call through its Query_base pointer to rep()
    return os << query.rep();
}
当我们打印一个Query时输出运算符调用Query类的公有rep成员
运算符函数通过指针成员虚调用当前Query所指对象的rep成员
也就是说当我们编写如下代码时
Query andq = Query(sought1) & Query(sought2);
cout << andq << endl;

输出运算符将调用andq的Query::rep
而Query::rep通过它的Query_base指针虚调用Query_base版本的rep函数
因为andq指向的是一个AndQuery对象所以本次的函数调用将运行AndQuery::rep

练习

当一个 Query 类型的对象被拷贝移动赋值或销毁时将分别发生什么

拷贝移动赋值销毁一个shared_ptr的指针

练习

当一个 Query_base 类型的对象被拷贝移动赋值或销毁时将分别发生什么

Query_base无数据成员仅拷贝移动赋值销毁类本身
执行默认语义

派生类

对于Query_base的派生类来说最有趣的部分是这些派生类如何表示一个真实的查询
- 其中WordQuery类最直接它的任务就是保存要查找的单词

其他类分别操作一个或两个运算对象
- NotQuery有一个运算对象AndQuery和OrQuery有两个
在这些类当中运算对象可以是Query_base的任意一个派生类的对象
- 一个NotQuery对象可以被用在WordQueryAndQueryOrQuery或另一个NotQuery中
为了支持这种灵活性运算对象必须以Query_base指针的形式存储
这样我们就能把该指针绑定到任何我们需要的具体类上

然而实际上我们的类并不存储Query_base指针而是直接使用一个Query对象
就像用户代码可以通过接口类得到简化一样我们也可以使用接口类来简化我们自己的类
至此我们已经清楚了所有类的设计思路接下来依次实现它们

WordQuery类

一个WordQuery查找一个给定的string
它是在给定的TextQuery对象上实际执行查询的唯一一个操作
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;    // word for which to search
};
和Query_base一样WordQuery没有公有成员
同时Query必须作为WordQuery的友元这样Query才能访问WordQuery的构造函数
每个表示具体查询的类都必须定义继承而来的纯虚函数eval和rep
我们在WordQuery类的内部定义这两个操作eval调用其TextQuery参数的query成员
由query成员在文件中实际进行查找rep返回这个WordQuery表示的string即query_word)。

定义了WordQuery类之后我们就能定义接受string的Query构造函数了
inline Query::Query(const std::string &s): q(new WordQuery(s)) { }
这个构造函数分配一个WordQuery然后令其指针成员指向新分配的对象

NotQuery类及~运算符

~运算符生成一个NotQuery其中保存着一个需要对其取反的Query
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 "~(" + query.rep() +")";}
    QueryResult eval(const TextQuery&) const;
    Query query;
};
inline Query operator~(const Query &operand)
{
    return std::shared_ptr<Query_base>(new NotQuery(operand));
}
因为NotQuery的所有成员都是私有的所以我们一开始就要把~运算符设定为友元
为了rep一个NotQuery我们需要将~符号与基础的Query连接在一起
我们在输出的结果中加上适当的括号这样读者就可以清楚地知道查询的优先级了

NotQuery类及~运算符

值得注意的是在NotQuery自己的rep成员中对rep的调用最终执行的是一个虚调用
query.rep()是对Query类rep成员的非虚调用接着Query::rep将调用q->rep(),
这是一个通过Query_base指针进行的虚调用

~运算符动态分配一个新的NotQuery对象其return语句隐式地使用接受一个
shared_ptr<Query_base>的Query构造函数也就是说return语句等价于
// allocate a new NotQuery object
// bind the resulting NotQuery pointer to a shared_ptr<Query_base
shared_ptr<Query_base> tmp(new NotQuery(expr));
return Query(tmp); // use the Query constructor that takes a shared_ptr
eval成员比较复杂因此我们将在类的外部实现它

BinaryQuery类

BinaryQuery类也是一个抽象基类它保存操作两个运算对象的查询类型所需的数据
class BinaryQuery: public Query_base {
protected:
    BinaryQuery(const Query &l, const Query &r, std::strings):
          lhs(l), rhs(r), opSym(s) { }
    // abstract class: BinaryQuery doesn't define eval
    std::string rep() const { return "(" + lhs.rep() + " "
                                         + opSym + " "+ rhs.rep() + ")";}
    Query lhs, rhs;    // right- and left-hand operands
    std::string opSym; // name of the operator
};
BinaryQuery中的数据是两个运算对象及相应的运算符符号
构造函数负责接受两个运算对象和一个运算符符号然后将它们存储在对应的数据成员中
要想rep一个BinaryQuery我们需要生成一个带括号的表达式
表达式的内容依次包括左侧运算对象运算符以及右侧运算对象
就像我们显示NotQuery的方法一样对rep的调用最终是
对lhs和rhs所指Query_base对象的rep函数进行虚调用

BinaryQuery不定义eval而是继承了该纯虚函数
因此BinaryQuery也是一个抽象基类我们不能创建BinaryQuery类型的对象

AndQuery类、OrQuery类及相应的运算符

AndQuery类和OrQuery类以及它们的运算符都非常相似
class AndQuery: public BinaryQuery {
    friend Query operator& (const Query&, const Query&);
    AndQuery(const Query &left, const Query &right):
                        BinaryQuery(left, right, "&") { }
    // concrete class: AndQuery inherits rep and defines the remaining pure virtual
    QueryResult eval(const TextQuery&) const;
};
inline Query operator&(const Query &lhs, const Query &rhs){
    return std::shared_ptr<Query_base>(new AndQuery(lhs,rhs));
}
class OrQuery: public BinaryQuery {
    friend Query operator|(const Query&, const Query&);
    OrQuery(const Query &left, const Query &right):
                BinaryQuery(left, right, "|") { }
    QueryResult eval(const TextQuery&) const;
};
inline Query operator|(const Query &lhs, const Query &rhs){
    return std::shared_ptr<Query_base>(new OrQuery(lhs,rhs));
}

AndQuery类、OrQuery类及相应的运算符

这两个类将各自的运算符定义成友元
并且各自定义了一个构造函数通过运算符创建BinaryQuery基类部分
它们继承BinaryQuery的rep函数但是覆盖了eval函数

~运算符一样&|运算符也返回一个绑定到新分配对象上的shared_ptr
在这些运算符中return语句负责将shared_ptr转换成Query

练习

针对图15.3构建的表达式

(a) 例举出在处理表达式的过程中执行的所有构造函数
a Query q = Query("fiery") & Query("bird") | Query("wind");
//在ch15/text_query文件夹下 g++ *.cpp && ./a.out
WordQuery::WordQuery(wind)
Query::Query(const std::string& s) where s=wind
WordQuery::WordQuery(bird)
Query::Query(const std::string& s) where s=bird
WordQuery::WordQuery(fiery)
Query::Query(const std::string& s) where s=fiery
BinaryQuery::BinaryQuery()  where s=&
AndQuery::AndQuery()
Query::Query(std::shared_ptr<Query_base> query)
BinaryQuery::BinaryQuery()  where s=|
OrQuery::OrQuery
Query::Query(std::shared_ptr<Query_base> query)

练习

(b) 例举出 cout << q 所调用的 rep
b
Query::rep()
BinaryQuery::rep()
Query::rep()
WodQuery::rep()
Query::rep()
BinaryQuery::rep()
Query::rep()
WodQuery::rep()
Query::rep()
WodQuery::rep()

(c) 例举出 q.eval() 所调用的 eval
c
Query::eval
WordQuery::eval
AndQuery::eval
OrQuery::eval

练习

实现 Query 类和 Query_base 其中需要定义rep 而无须定义 eval
//Query:
#ifndef QUERY_H
#define QUERY_H

#include <iostream>
#include <string>
#include <memory>
#include "query_base.h"
#include "queryresult.h"
#include "textquery.h"
#include "wordquery.h"

练习

/**
* @brief interface class to manage the Query_base inheritance hierachy
*/
class Query
{
    friend Query operator~(const Query&);
    friend Query operator|(const Query&, const Query&);
    friend Query operator&(const Query&, const Query&);
public:
    // build a new WordQuery
    Query(const std::string& s) : q(new WordQuery(s))
    {
        std::cout << "Query::Query(const std::string& s) where s=" + s + "\n";
    }

练习

    // interface functions: call the corresponding Query_base operatopns
    QueryResult eval(const TextQuery& t) const
    {
        return q->eval(t);
    }
    std::string rep() const
    {
        std::cout << "Query::rep() \n";
        return q->rep();
    }
private:
    // constructor only for friends
    Query(std::shared_ptr<Query_base> query) :
        q(query)
    {
        std::cout << "Query::Query(std::shared_ptr<Query_base> query)\n";
    }
    std::shared_ptr<Query_base> q;
};

练习

inline std::ostream&
operator << (std::ostream& os, const Query& query)
{
    // make a virtual call through its Query_base pointer to rep();
    return os << query.rep();
}

#endif // QUERY_H

练习

//Query_base:
#ifndef QUERY_BASE_H
#define QUERY_BASE_H
#include "textquery.h"
#include "queryresult.h"

练习

/**
* @brief abstract class acts as a base class for all concrete query types
*        all members are private.
*/
class Query_base
{
    friend class Query;
protected:
    using line_no = TextQuery::line_no; //  used in the eval function
    virtual ~Query_base() = default;
private:
    // returns QueryResult that matches this query
    virtual QueryResult eval(const TextQuery&) const = 0;

    // a string representation of this query
    virtual std::string rep() const = 0;
};

#endif // QUERY_BASE_H

练习

在构造函数和 rep 成员中添加打印语句
运行你的代码以检验你对本节第一个练习中(a)(b)两小题的回答是否正确

Query q = Query("fiery") & Query("bird") | Query("wind");

WordQuery::WordQuery(wind)
Query::Query(const std::string& s) where s=wind
WordQuery::WordQuery(bird)
Query::Query(const std::string& s) where s=bird
WordQuery::WordQuery(fiery)
Query::Query(const std::string& s) where s=fiery
BinaryQuery::BinaryQuery()  where s=&
AndQuery::AndQuery()
Query::Query(std::shared_ptr<Query_base> query)
BinaryQuery::BinaryQuery()  where s=|
OrQuery::OrQuery
Query::Query(std::shared_ptr<Query_base> query)

练习

std::cout << q <<std::endl;
Query::rep() 
BinaryQuery::rep()
Query::rep() 
WodQuery::rep()
Query::rep() 
BinaryQuery::rep()
Query::rep() 
WodQuery::rep()
Query::rep() 
WodQuery::rep()
((fiery & bird) | wind)   

练习

如果在派生类中含有 shared_ptr<Query_base> 类型的成员而非 Query 类型的成员
则你的类需要做出怎样的改变

参考15.35

练习

下面的声明合法吗如果不合法请解释原因;如果合法请指出该声明的含义
BinaryQuery a = Query("fiery") & Query("bird");
AndQuery b = Query("fiery") & Query("bird");
OrQuery c = Query("fiery") & Query("bird");

1. 不合法因为 BinaryQuery 是抽象类
2. 不合法& 操作返回的是一个 Query 对象
3. 不合法& 操作返回的是一个 Query 对象

eval函数

eval函数是我们这个查询系统的核心
每个eval函数作用于各自的运算对象同时遵循的内在逻辑也有所区别
OrQuery的eval操作返回两个运算对象查询结果的并集而AndQuery返回交集
与它们相比NotQuery的eval函数更加复杂一些它需要返回运算对象没有出现的文本行

为了支持上述eval函数的处理我们需要使用QueryResult在它当中定义添加的成员
假设QueryResult包含begin和end成员允许在QueryResult保存的行号set中进行迭代
假设QueryResult还包含名为get_file的成员它返回一个指向待查询文件的shared_ptr

我们的Query类使用了12.3.2节练习第435页为QueryResult定义的成员

OrQuery::eval

一个OrQuery表示的是它的两个运算对象结果的并集对于每个运算对象来说我们
通过调用eval得到它的查询结果因为这些运算对象的类型是Query所以调用eval
也就是调用Query::eval而后者实际上是对潜在的Query_base对象的eval进行虚调用
每次调用完成后得到的结果是一个QueryResult它表示运算对象出现的行号
我们把这些行号组织在一个新set中
QueryResult OrQuery::eval(const TextQuery& text) const{
    // virtual calls through the Query members, lhs and rhs
    // the calls to eval return the QueryResult for each operand
    auto right = rhs.eval(text), left = lhs.eval(text);
    // copy the line numbers from the left-hand operand into the result set
    auto ret_lines = make_shared<set<line_no>>(left.begin(), left.end());
    // insert lines from the right-hand operand
    ret_lines->insert(right.begin(), right.end());
    // return the new QueryResult representing the union of lhs and rhs
    return QueryResult(rep(), ret_lines, left.get_file());
}

OrQuery::eval

    auto ret_lines = make_shared<set<line_no>>(left.begin(), left.end());
    ret_lines->insert(right.begin(), right.end());
//我们使用接受一对迭代器的set构造函数初始化ret_lines。一个QueryResult的begin
//和end成员返回行号set的迭代器,因此,创建ret_lines的过程实际上是拷贝了left
//集合的元素。接下来对ret_lines调用insert,并将right的元素插入进来。
//调用结束后,ret_lines将包含在left或right中出现过的所有行号。
    return QueryResult(rep(), ret_lines, left.get_file());
//eval函数在最后构建并返回一个表示混合查询匹配的QueryResult。
//QueryResult的构造函数接受三个实参:一个表示查询的string、一个指向匹配
//行号set的shared_ptr和一个指向输入文件vector的shared_ptr。
//我们调用rep生成所需的string,调用get_file获取指向文件的shared_ptr。
//因为left和right指向的是同一个文件,所以使用哪个执行get_file函数并不重要。

AndQuery::eval

AndQuery的eval和OrQuery很类似
唯一的区别是它调用了一个标准库算法来求得两个查询结果中共有的行
// returns the intersection of its operands' result sets
QueryResult
AndQuery::eval(const TextQuery& text) const
{
    // virtual calls through the Query operands to get result sets for the operands
    auto left = lhs.eval(text), right = rhs.eval(text);
    // set to hold the intersection of left and right
    auto ret_lines = make_shared<set<line_no>>();
    // writes the intersection of two ranges to a destination iterator
    // destination iterator in this call adds elements to ret
    set_intersection(left.begin(), left.end(),
                   right.begin(), right.end(),
                   inserter(*ret_lines, ret_lines->begin()));
    return QueryResult(rep(), ret_lines, left.get_file());
}
其中我们使用标准库算法set_intersection来合并两个set
关于set_intersection在附录A.2.8中有详细的描述

AndQuery::eval

    set_intersection(left.begin(), left.end(),
                   right.begin(), right.end(),
                   inserter(*ret_lines, ret_lines->begin()));
//set_intersection算法接受五个迭代器。
//它使用前四个迭代器表示两个输入序列,最后一个实参表示目的位置。
//该算法将两个输入序列中共同出现的元素写入到目的位置中。
//在上述调用中我们传入一个插入迭代器作为目的位置。
//当set_intersection向这个迭代器写入内容时,实际上是向ret_lines插入一个新元素。
//和OrQuery的eval函数一样,
//AndQuery的eval函数也在最后构建并返回一个表示混合查询匹配的QueryResult。

NotQuery::eval

NotQuery查找运算对象没有出现的文本行
QueryResult NotQuery::eval(const TextQuery& text) const{
    // virtual call to eval through the Query operand
    auto result = query.eval(text);
    auto ret_lines = make_shared<set<line_no>>();
    // we have to iterate through the lines on which our operand appears
    auto beg = result.begin(), end = result.end();
    // 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()->size();
    for (size_t n = 0; n != sz; ++n) {
        // if we haven't processed all the lines in result
        // check whether this line is present
        if (beg == end || *beg != n)
            ret_lines->insert(n);  // if not in result, add this line
        else if (beg != end)
            ++beg; // otherwise get the next line number in result if there is one
    }
    return QueryResult(rep(), ret_lines, result.get_file());
}

和其他eval函数一样我们首先对当前的运算对象调用eval所得的结果QueryResult
中包含的是运算对象出现的行号但我们想要的是运算对象未出现的行号
也就是说我们需要的是存在于文件中但是不在result中的行

要想得到最终的结果我们需要遍历不超过输出文件大小的所有整数
并将所有不在result中的行号放入到ret_lines中
我们使用beg和end分别表示result的第一个元素和最后一个元素的下一位置
因为遍历的对象是一个set所以当遍历结束后获得的行号将按照升序排列

循环体负责检查当前的编号是否在result当中如果不在将这个数字添加到ret_lines中
如果该数字属于result则我们递增result的迭代器beg
一旦处理完所有行号就返回包含ret_lines的一个QueryResult对象
和之前版本的eval类似该QueryResult对象还包含rep和get_file的运行结果

练习

 实现 Query 类和 Query_base 求图15.3中表达式的值并打印相关信息
 验证你的程序是否正确


见book/ch15/test15_39 

$ g++ *.cpp && ./a.out
((fiery & bird) | wind) occurs 2 times
        (line 2) Her Daddy says when the wind blows
        (line 4) like a fiery bird in flight.

((fiery & bird) | wind)

练习

 OrQuery  eval 函数中如果 rhs 成员返回的是空集将发生什么

不会发生什么代码如下

std::shared_ptr<std::set<line_no>> ret_lines =
       std::make_shared<std::set<line_no>>(left.begin(), left.end());

如果 rhs 成员返回的是空集 set 当中不会添加什么

练习

重新实现你的类这次使用指向 Query_base 的内置指针而非 shared_ptr
请注意做出上述改动后你的类将不能再使用合成的拷贝控制成员

成员q的类型改为Query_base *
修改拷贝构造函数 拷贝赋值运算符和析构函数 方法见13章练习
用到q的地方也修改

见book/ch15/test15_41 

$ g++ *.cpp && ./a.out
((fiery & bird) | wind) occurs 2 times
        (line 2) Her Daddy says when the wind blows
        (line 4) like a fiery bird in flight.

((fiery & bird) | wind)

练习

从下面的几种改进中选择一种设计并实现它:


(a) 按句子查询并打印单词而不再是按行打印
(b) 引入一个历史系统用户可以按编号查阅之前的某个查询
    并可以在其中添加内容或者将其余其他查询组合
(c) 允许用户对结果做出限制比如从给定范围的行中跳出匹配的进行显示

(a)TextQuery类中 使用'.'而不是换行符来识别一行
   print函数输出sentence而不是line

小结

继承使得我们可以编写一些新的类
这些新类既能共享其基类的行为又能根据需要覆盖或添加行为
动态绑定使得我们可以忽略类型之间的差异
其机理是在运行时根据对象的动态类型来选择运行函数的哪个版本
继承和动态绑定的结合使得我们能够编写具有特定类型行为但又独立于类型的程序

在C++语言中动态绑定只作用于虚函数并且需要通过指针或引用调用

在派生类对象中包含有与它的每个基类对应的子对象
因为所有派生类对象都含有基类部分
所以我们能将派生类的引用或指针转换为一个可访问的基类引用或指针

本课程

- 一周目 
    - 将课程建到云上 
- 二周目 
    - 业界主流教材 CPP PRIMER 内容丰富 
- 未来 三周目
    - 增强专业特色 

致谢

本系列ppt参考资料
cpp primer 第五版 
cpp primer 习题集 
程序设计教程用C++语言编程 第3版陈家骏 机械工业出版社 
C++ Primer Plus第6版中文版
qt 在线帮助https://www.qt.io/developers
github cpp primer相关仓库
- applenob/Cpp_Primer_Practice
- jzplp/Cpp-Primer-Answer
- Mooophy/Cpp-Primer
- huangmingchuan/Cpp_Primer_Answers
等等

当执行派生类的构造拷贝移动和赋值操作时
首先构造拷贝移动和赋值其中的基类部分然后才轮到派生类部分
析构函数的执行顺序则正好相反首先销毁派生类接下来执行基类子对象的析构函数
基类通常都应该定义一个虚析构函数即使基类根本不需要析构函数也最好这么做
将基类的析构函数定义成虚函数的原因是为了确保当我们删除一个基类指针
而该指针实际指向一个派生类对象时程序也能正确运行

派生类为它的每个基类提供一个保护级别
public基类的成员也是派生类接口的一部分
private基类的成员是不可访问的
protected基类的成员对于派生类的派生类是可访问的但是对于派生类的用户不可访问

实践课

  • 从课程主页 cpp.njuer.org 打开 《面向对象编程基础》实验课 八 继承派生类 https://developer.aliyun.com/adc/scenario/72ddf18d191e460585cd42bee1720c2b
  • 使用g++编译代码
  • 编辑一个 readme.md 文档,键入本次实验心得.
  • 使用git进行版本控制 可使用之前的gitee代码仓库
      - 云服务器elastic compute service,简称ecs
      - aliyun linux 2是阿里云推出的 linux 发行版
      - vim是从vi发展出来的一个文本编辑器.
      - g++ 是c++编译器
    
习题1
定义 Quote  Bulk_quote 的拷贝控制成员令其与合成的版本行为一致
为这些成员以及其他构造函数添加打印状态的语句使得我们能够知道正在运行哪个程序
使用这些类编写程序预测程序将创建和销毁哪些对象
重复实验不断比较你的预测和实际输出结果是否相同直到预测完全准确再结束

习题2
将图形基元如线段方格圆锥组成继承体系
并将基类的打印函数和输入数据函数定义为虚函数并在main函数中调用

习题3
定义一个int型元素的线性表类LinearList可以返回元素个数
查找元素返回位置在指定位置插入删除元素
再利用LinearList通过组合或继承定义一个队列类提供入队和出队功能
编辑c++代码和markdown文档,使用git进行版本控制
yum install -y git gcc-c++
使用git工具进行版本控制
git clone你之前的网络git仓库test(或其它名字)
cd test 进入文件夹test
(clone的仓库,可移动旧文件到目录weekN:  mkdir -p weekN ; mv 文件名 weekN;)

vim test1.cpp
g++ ./test1.cpp 编译
./a.out 执行程序
git add . 加入当前文件夹下所有文件到暂存区
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
vim readme.md 键入新内容(实验感想),按ESC 再按:wq退出
git add .
git commit –m "weekN" 表示提交到本地,备注weekN

git push 到你的git仓库

git log --oneline --graph 可看git记录
键入命令并截图或复制文字,并提交到群作业.
cat test* readme.md

提交

  • 截图或复制文字,提交到群作业.
  • 填写阿里云平台(本实验)的网页实验报告栏,发布保存.本次报告不需要分享提交
  • 填写问卷调查 https://rnk6jc.aliwork.com/o/cppinfo

关于使用tmux

sudo yum install -y tmux
cd ~ && wget https://cpp.njuer.org/tmux && mv tmux .tmux.conf
tmux 进入会话 .
前缀按键prefix= ctrl+a, 
prefix+c创建新面板,
prefix+"分屏,
prefix+k选上面,prefix+j选下面,
prefix+1选择第一,prefix+n选择第n,
prefix+d脱离会话
tmux attach-session -t 0 回到会话0

vim 共分为三种模式

图片1

- 命令模式
  - 刚启动 vim,便进入了命令模式.其它模式下按ESC,可切换回命令模式
    - i 切换到输入模式,以输入字符.
    - x 删除当前光标所在处的字符.
    - : 切换到底线命令模式,可输入命令.
- 输入模式
  - 命令模式下按下i就进入了输入模式.
    - ESC,退出输入模式,切换到命令模式
- 底线命令模式
  - 命令模式下按下:英文冒号就进入了底线命令模式.
    - wq 保存退出

vim 常用按键说明

除了 i, Esc, :wq 之外,其实 vim 还有非常多的按键可以使用.命令模式下
- 光标移动
  - j下 k上 h左 l右
  - w前进一个词 b后退一个词
  - Ctrl+d 向下半屏  ctrl+u 向上半屏
  - G 移动到最后一行 gg 第一行 ngg 第n行
- 复制粘贴
  - dd 删一行 ndd 删n行
  - yy 复制一行 nyy复制n行
  - p将复制的数据粘贴在下一行 P粘贴到上一行
  - u恢复到前一个动作 ctrl+r重做上一个动作
- 搜索替换
  - /word 向下找word     word 向上找
  - n重复搜索 N反向搜索
  - :1,$s/word1/word2/g从第一行到最后一行寻找 word1 字符串,并将该字符串
    取代为 word2

vim 常用按键说明

底线命令模式下
- :set nu   显示行号
- :set nonu 取消行号
- :set paste    粘贴代码不乱序
把caps lock按键映射为ctrl,能提高编辑效率.

Markdown 文档语法

# 一级标题
## 二级标题
*斜体* **粗体**
- 列表项
  - 子列表项
> 引用
[超链接](http://asdf.com)
![图片名](http://asdf.com/a.jpg)

|表格标题1|表格标题2|
 |-|-|
|内容1|内容2|

复习脉络

继承派生动态内存一定要搞透
什么是面向对象封装继承多态各自举例子
智能指针的特性shared, weak, unique
构造析构拷贝构造拷贝赋值移动构造移动赋值和右值引用
会用 容器和迭代器

复习脉络

# 面向对象
面向对象的三大特性封装继承多态
类的访问权限privateprotectedpublic
类的构造函数 析构函数 拷贝构造函数 拷贝赋值运算符 移动构造函数 移动赋值运算符
析构函数的作用
移动构造函数与拷贝构造函数对比
深拷贝与浅拷贝
什对虚函数和多态的理解:什么是虚函数 ,多态是如何实现的编程实现一下
重载(覆盖)override ?隐藏
如何避免拷贝
C++ 类对象的初始化顺序 析构顺序 
静态绑定和动态绑定
实现一个单例模式
C++ 用数组\链表\容器实现String类,队列类栈类常用操作

复习脉络

# 编译内存相关
从源代码到可执行程序中间的过程是什么样的
栈和堆的区别
智能指针的原理
四种智能指针auto_ptrunique_ptrshared_ptrweak_ptr
参数传递时值传递引用传递指针传递的区别
如何将左值转换成右值 std::move函数
右值引用
指针常量与常量指针

复习脉络

# 关键字库函数
lambda 表达式匿名函数的具体应用和使用场景
explicit 的作用避免编译器进行隐式类型转换
static 的作用修饰局部变量全局变量类中成员变量类中成员函数
const关键字修饰变量指针类对象类中成员函数
class  struct 区别
迭代器的作用
vector容量满了会发生什么
map和unordered_map有什么区别
cast转换
map和set有什么区别

复习脉络

# 复习做过的作业

谢谢