C++ Primer 学习笔记 第十五章 面向对象程序设计

面向对象程序设计(OOP)基于三个概念:数据抽象(只暴露类的接口,而如何实现的是不透明的,即类的接口和实现分离)、继承(能实现相似的类型)、动态绑定(忽略相似类型的区别,以统一方式使用它)。

继承关系联系在一起的类构成层次关系,在最低层有一个基类,其他类直接或间接地从基类继承而来,称为派生类。基类负责定义所有类的共有成员,而派生类定义各自特有的成员。

例子:
书店有按原价出售的书,也有打折出售的书,我们定义一个基类Quote表示原价出售的书,Bulk_quote表示打折的书,它俩都包含以下成员函数:
1.isbn():返回书籍的isbn号,只用定义在Quote类中。
2.net_price(size_t):返回书的售价,类型相关,应在两个类中都定义。

C++将所有类共有的函数和类型相关的函数区分对待,如基类希望它的派生类各自定义适合自身版本的同名函数,可以将该函数声明成虚函数:

class Quote {
public:
    std::string isbn() const;
    virtual double net_price(std::size_t n) const;
};

派生类要通过派生类列表指出它是从哪个(些)类继承而来:

class Bulk_quote : public Quote {    
public:
    double net_price(std::size_t) const override;
};

派生类列表中类名前有访问说明符,此处为public,因此我们能把Bulk_quote对象当成Quote对象使用。派生类需要在其内部对所有要重新定义的虚函数进行声明,派生类可以在这种函数前加virtual,也可以不加。C++11允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,通过在形参列表后、或const成员函数的const关键字后、或引用成员函数的const引用限定符后面,加override关键字。

动态绑定(运行时绑定):同一段代码处理两种类型,当我们使用基类的引用或指针调用虚函数时将发生动态绑定:

double PrintPrice(const Quote& item) {
    return item.net_price();
}

如上,我们既能使用基类Quote对象作实参,也能使用派生类Bulk_quote对象作实参,而调用的net_price函数版本取决于对象类型。

完善Quote类:

class Quote {
public:
    Quote() = default;
    Quote(const std::string& book, double sales_price) : bookNo(book), price(sales_price) { }
    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;    //虽然派生类也需要使用此成员,但是通过isbn函数获取的,因此不用定义为protected的
protected:
    double price = 0.0;    //不打折时的价格
};

基类通常应定义虚析构函数,即使它不执行任何实际操作。

基类如希望派生类对某些函数进行覆盖,用virtual标识,我们使用引用或指针调用对象的虚函数时,会根据对象的类型进行动态绑定,即需要动态绑定的函数要声明为虚函数。

虚函数可以是基类的非构造函数和非static函数。

virtual关键字只能出现在类内声明而不能出现在类外定义中。

如基类将一个函数声明为虚函数,那么在派生类中该函数也是隐式的虚函数。

非虚函数的成员函数解析过程发生在编译时而非运行时。

派生类继承基类的成员,却不能访问基类的私有成员,而protected访问运算符表示基类希望派生类能访问这些成员而其他用户禁止访问。

定义Bulk_quote类:

class Bulk_quote : public Quote {    
public: 
    Bulk_quote() = default;
    Bulk_quote(const std::string&, double, std::size_t, double);
    double net_price(std::size_t) const override;    //覆盖基类的net_price函数以实现大量购买的折扣政策
private:
    std::size_t min_qty = 0;    //适用折扣政策的最低购买量
    double discount = 0.0;    //折扣额
};

一个派生是公有的说明基类的公有成员也是派生类接口的组成部分,并且能将公有派生类型的对象绑定到基类的引用或指针上。

派生类只继承自一个类被称作单继承。

如果派生类中没有覆盖基类中的虚函数,那么在派生类中该虚函数会直接继承基类中的版本。

派生类对象的组成部分:一个含有派生类自己定义的非静态成员的子对象,一个与该派生类继承的基类对应的子对象。

C++标准没有规定派生类对象在内存中如何分布。

我们可以把派生类对象绑定到基类的引用或指针(包括智能指针)上,这隐含着当我们使用基类的引用或指针时,我们并不清楚该引用或指针所绑定对象的真实类型:

Quote item;
Bulk_quote bulk;
Quote* p = &item;    //正确
p = &bulk;    //正确,p指向bulk的Quote部分
Quote& r = bulk;    //正确,r绑定到bulk的Quote部分

如上转换被称为派生类到基类的类型转换,编译器会隐式地进行此转换。

派生类不能直接初始化从基类继承而来的成员,而必须使用基类的构造函数来初始化它的基类部分:

Bulk_quote(const std::string& book, double p, std::size_t qty, double disc) : Quote(book, p), min_qty(qty), discount(disc) { }    //使用基类的构造函数完成基类成员的初始化,虽然也可以在函数体内给基类的公有成员和受保护的成员赋值,但最好不这么做

如在派生类的构造函数的初始化列表中没有使用基类的构造函数,那么基类部分会使用默认构造函数,如基类没有默认构造函数,那么会报错。而初始化列表中的初始化顺序为:首先初始化基类的部分,然后再按派生类成员声明的顺序初始化。

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

如基类中定义了静态成员,则不管有多少个派生类,每个静态成员都只存在唯一的实例。静态成员也遵循访问运算符,如果基类的静态成员是private的,那么派生类无法访问它。

派生类声明时不加派生列表。

被用作基类的类必须被定义,而不能只是声明了。这隐含着一个类不能派生它本身。

一个类可以既是一个基类,又是一个派生类:

class Base { };
class D1 : public Base { };    //Base的直接基类
class D2 : public D1 { };    //Base的间接基类

每个类都会继承直接基类的所有成员,而这个直接基类又包含它的基类的成员,因此最终的派生类包含它所有基类(包括直接基类和间接基类)的子对象。

有时候我们不想一个类被继承,C++11中在类名后加final防止该类被继承:

class NoDerived final { };    //NoDerived不能作为基类
class Base { };    //一个类
class Last final : Base { };    //正确,Last不能作为基类,但可以作为派生类
class Bad : NoDerived { };    //错误,NoDerived是final的
class Bad2 : Last { };    //错误,Last是final的

通常,指针或引用必须绑定在与其类型一致的对象上,例外是当对象的类型有一个可接受的const转换规则或存在继承关系的类。

表达式的静态类型在编译时已知,而动态类型是变量或表达式表示的内存中的对象的类型,直到运行时才可知,如函数形参的基类的指针或引用,直到运行时才知道传入的是基类对象还是派生类对象,因此,它是动态类型。只有使用指针或引用调用虚函数时静态类型和动态类型不一致,如表达式既不是引用或指针,那么它的静态类型和动态类型永远相同。

不存在基类向派生类的隐式转换,因为这可能会访问基类不存在的成员。即使当一个基类引用或指针绑定在一个派生类对象上,也不能隐式从基类转换为派生类,因为编译器不知道基类的引用或指针是否绑定在派生类对象上。但我们可以使用dynamic_cast请求一个类型转换,它的安全检查发生在运行时,也可以使用static_cast,它用于我们已经知道基类向派生类转换是安全的来强制覆盖掉编译器的检查工作。

派生类向基类的转换只存在于引用和指针之间,在对象间不存在转换。

一般,基类的拷贝控制成员的参数都是一个接受const基类类型的引用,因此,我们可以把一个派生类对象用在拷贝控制成员出现的地方。但在拷贝控制成员函数中,只会处理派生类中基类的部分,而其余部分被切掉了。

我们把具有继承关系的多个类型称为多态类型,因为我们能使用这种类型的多种形式而无需在意它们的差异,动态绑定是支持多态性的根本。

一个派生类的函数覆盖基类的虚函数时,它的返回类型和形参类型必须与被它覆盖的基类函数完全一致。上例的返回类型的表述有一个例外,即返回类型为类本身的引用或指针时,此时基类返回基类类型的指针或引用,派生类返回派生类类型的指针或引用,但要求派生类到基类的类型转化是可访问的。

派生类如果定义了一个与基类中虚函数的函数名相同的函数而它们两个的形参列表不同,那么派生类中的同名函数并没有覆盖基类中的虚函数,它们俩是相互独立的,但一般不这么使用,因为可能是形参列表搞错而导致没有覆盖基类的虚函数,一般这样的错误很难检查,因此C++11新增了override关键字使程序员意图更加清晰并且让编译器帮我们发现形参列表中的错误,如果使用了override关键字而该函数没有覆盖基类的虚函数,编译器会报错。

我们还能把虚函数指定为final的,则该函数不能被它所在类的派生类所覆盖。一般基类我们不会在使用virtual关键字的同时使用final,这没有意义,一般我们用于派生类覆盖了基类虚函数的函数中,这样这个函数就不会被它所在类的派生类覆盖了。函数的final关键字出现在形参列表、const或引用修饰符、尾置返回类型之后。

虚函数也能有默认实参,如果某次调用使用了默认实参,则默认实参的值由本次调用的静态类型决定,即使用基类类型的指针或引用调用使用了默认实参的函数时,默认实参的值为基类的虚函数的默认实参,即使本次调用的动态类型为派生类类型,因此,如虚函数要使用默认实参,则基类和派生类的默认实参值最好一致。

不要动态绑定:

double undiscounted = baseP->Quote::net_price(42);    //强行调用基类的net_price函数,而不管baseP的动态类型是什么

以上调用在编译时完成解析。一般用在派生类的虚函数调用它基类的虚函数版本时。

如书店中有多种折扣策略,我们为每种折扣策略都定义一个类,这些类都继承自Disc_quote类,该基类负责定义每种折扣策略类的公共成员,如购买量和折扣值,以及net_price函数来计算折扣。但Disc_price类的net_price函数不应该被定义,它不代表任何折扣策略,同样的,我们也不希望用户定义Disc_quote类,它不代表任何折扣策略的对象。我们可以将Disc_quote类的net_price函数声明为纯虚函数来达到以上目的,纯虚函数表示这个函数目前还没有意义。定义纯虚函数:

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) { }
    virtual double net_price(std::size_t) const = 0;    //纯虚函数,虚函数的形参列表要和派生类的覆盖版本一致,因此纯虚也需要形参列表,通过=0来将其声明为纯虚函数
protected:
    std::size_t quantity = 0;    //折扣适用的购买量
    double discount = 0.0;    //表示折扣
};

如上,虽然我们不能定义Disc_quote类型对象,但也要定义它的构造函数,以供它的派生类使用。=0只能出现在类内部的声明处。我们也可以为纯虚函数提供定义,不过必须定义在类外。

含有或未经覆盖直接继承纯虚函数的类是抽象基类。我们不能定义抽象基类的对象。

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

如果我们此时重新定义基于Disc_quote的Bulk_quote类,即重新设计类的体系,将一个类中的成员或操作移动到了另一个类,这个过程叫重构。使用Bulk_quote的应用代码不会因为重构而需要修改,但需要重新编译含有这些类的代码。

每个类各自控制着自己的成员对于派生类来说是否可访问。

protected访问符说明派生类的成员或友元只能通过派生类对象来访问基类的受保护成员:

class Base {
protected:
    int prot_mem;
};

class Sneaky : public base {
    friend void clobber(Sneaky&);
    friend void clobber(Base&);
    int j;    //private成员
};

void clobber(Sneaky& s) {
    s.j = s.prot_mem = 0;    //正确,Sneaky的友元函数clobber能访问Sneaky对象的private和protected成员
}

void clobber(Base& b) {
    b.prot_mem = 0;    //错误,Sneaky的友元函数clobber不能访问Base的protected成员
}

如上,如果能通过友元函数来访问基类的protected成员,那么我们就可以创建一个基类的派生类,然后使用此派生类的友元函数来修改基类对象的值了。

派生访问说明符不影响派生类的成员或友元访问其直接基类的成员,它的作用是说明继承自基类的成员是public的还是private的,即影响的是派生类的用户(包括派生类的派生类)。派生访问说明符有public、private、protected。

如是public继承,则被继承的成员将遵循该成员基类的访问说明符。如是protected继承,那么基类的public成员被继承为protected的。如是private继承,那么基类的所有成员被继承为private的。

派生类向基类转换,当D继承自B时:
1.只有D public继承自B时,用户代码才能进行派生类向基类的转换。private和protected继承时不能进行上述转换。
2.不管D以什么方式继承自B,D的成员函数和友元都能完成派生类向基类的转换。
3.如D以public或protected方式继承自B,那么D的派生类的成员和友元可以完成派生类向基类的转换。

以上能实现转换的原则为,如果基类的公有成员是可访问的,那么就能实现转换。

友元不能继承。基类的友元不能访问派生类的私有和受保护成员,派生类的友元也不能访问基类的私有和受保护成员:

class Base {
    friend class Pal;    
    //其余不变
};

class Pal {
    int f(Base b) {
        return b.prot_mem;    //能访问Base类的protected成员
    }
    
    int f2(Sneaky s) {
        return s.j;    //不能访问Base派生类的private成员
    }

    int f3(Sneaky s) {
        return s.prot_mem;    //能访问Base派生类的Base部分的成员
    }
};

继承自Pal的派生类不能访问Base,友元类不能继承。

可以改变派生类中对于某个基类成员的访问级别:

class Base {
public:
    std::size_t size() const {
        return n;
    }
protected:
    std::size_t n;
};

class Derived : private Base {    //private继承
public:
    using Base::size;    //将基类的size成员访问级别改为public
protected:
    using Base::n;
};

但派生类只能为它所能访问到的成员改变访问级别,如不能改变基类中的private成员访问级别,因为派生类中访问不到基类的private成员。

默认继承情况下,class是private继承,struct是public继承,但最好都写出来,比较清晰。class和struct除了成员的默认访问权限和继承时的差别外,一模一样。

派生类的作用域嵌套在基类的作用域之内。因此在名字解析时,先在派生类的作用域类找,找不到再到基类的作用域中找。也因此,派生类中的对象将隐藏基类中的同名对象,但我们可以使用作用域运算符访问基类中的同名对象。实践中,派生类最好不要定义除虚函数之外的其他与基类同名的成员。

一个对象、引用、指针的静态类型决定了该对象的哪些成员可见。不能用一个基类指针访问派生类的特有成员,即使该指针指向的是派生类。

派生类中的与基类同名的函数成员会覆盖而不是重载基类中的函数,即使它们两个的形参列表不同,这是因为名字查找发生在类型检查之前:

struct Base {
    int memfcn();
};

struct Derived : Base {
    int memfcn(int);    //覆盖基类的同名成员函数
};

Derived d;
Base b;
d.memfcn();    //错误,形参列表为空的同名基类成员函数被隐藏了
d.Base::memfcn();    //正确,显式调用基类中的成员

上例中从Base继承而来的memfcn函数实际上还存在,只不过在名字查找时,会先在派生类中查找,如找到了,就不会再到外层作用域即基类的作用域中找了,这和作用域嵌套的原理相同。

虚函数如只被继承而没有在派生类中被覆盖,那么派生类中的该函数还是虚函数。

class Base {
public:
    virtual int fcn();
};

class D1 : public Base {
public:
    int fcn(int);    //隐藏了基类中的fcn函数
    virtual void f2();   
};

class D2 : public D1 {
public:
    int fcn(int);    //隐藏了D1中的fcn函数
    int fcn();    //覆盖了Base的虚函数fcn
    void f2();    //覆盖了D1中的虚函数f2
};

Base bobj;
D1 d1obj;
D2 d2obj;

Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;

bp1->fcn();    //通过指针调用虚函数,将在运行时调用Base::fcn()
bp2->fcn();    //通过指针调用虚函数,将在运行时调用Base::fcn(),由于bp2是基类类型指针,因此不能访问D1::fcn(int)
bp3->fcn();    //通过指针调用虚函数,将在运行时调用D2::fcn()

D1 *d1p = &d1obj, *d2p = &d2obj;
bp2->f2();    //错误,通过基类对象指针只能访问基类有的成员
d1p->f2();    //运行时调用D1::f2()
d2p->f2();    //运行时调用D2::f2()

如基类中有一组重载函数,我们在派生类中想要有所有版本(即所有形式的形参列表)的重载函数,那么我们就要在派生类中定义所有版本的重载函数或一个都不覆盖,因为,如果只定义一个重载函数,那么基类中的这组重载函数都不能被访问了,因为名字查找发生在类型检查之前。但有时我们只需要重写基类这组重载函数中的一部分,另一部分保持不变,那么我们需要在派生类中定义所有版本的重载函数,这很繁琐,我们可以用using改进。using声明后面跟一个不含形参列表的名字,因此一条using语句就可以把这组重载函数添加到派生类作用域中,此时我们只需重定义那些需要的函数了。

基类通常应定义一个虚析构函数,这样就可以动态分配此继承体系中类型的对象了。因为delete不知道传给它的指针实际指向基类还是派生类类型的对象,有了虚析构函数,就可以运行时解析到正确的析构函数了。而如果不定义虚析构函数直接delete一个指向派生类对象的基类指针,该行为未定义。

定义了虚析构函数的基类,它的派生类的析构函数也是虚函数,不管它是被定义的还是合成的。

一般定义了析构函数的类也需要定义拷贝控制成员,但基类是个意外。

定义了析构函数的类编译器不会为其合成移动操作。

合成的派生类的默认构造函数通过基类的默认构造函数完成基类成员的初始化,只要基类的可访问且不是被删除的函数即可。如基类没有默认构造函数,则派生类的默认构造函数因没有可用的基类默认构造函数而编译出错。

如基类中默认构造函数、拷贝构造函数、拷贝赋值运算符、析构函数是被删除的或不可访问的,则派生类中对应成员是被删除的,因为派生类需要使用基类的对应成员完成工作。

如基类的析构函数是被删除的或不可访问的,那么派生类的默认构造函数、移动构造函数和拷贝构造函数是被删除的,因为无法销毁派生类中的基类部分。

当我们用=default请求一个移动操作时,如基类中的对应移动操作是删除的或不可访问的,那么派生类中的该成员是被删除的,因为无法移动派生类中基类的部分。

基类一般都有虚析构函数,因而没有移动成员,如我们需要移动成员,则应显式地定义它,此时它的派生类也将获得合成的移动操作(只要派生类中没有不能移动的成员)。要知道显式定义移动成员会使合成的拷贝构造函数和合成的拷贝赋值运算符被定义为删除的。

派生类的拷贝和移动构造函数、赋值运算符也要拷贝和移动基类部分的成员。但派生类的析构函数只需要释放派生类分配的资源。

为派生类定义拷贝或移动构造函数时,常使用基类的对应成员完成操作,做法是委托构造函数:

class Base { ... };

class D : public Base {
public:
    D(const D &d) : Base(d) { ... }    //拷贝构造函数形参时基类的引用,可以绑定到派生类对象上
    D(D &&d) : Base(std::move(d)) { ... }
};

如我们没有使用基类的构造函数:

D(const D &d) { }

则基类会使用默认构造函数来初始化基类成员,此时基类部分成员是默认构造的,而派生类部分是从其他对象拷贝而来。

派生类赋值运算符:

//Base::operator=(const Base&)
D &D::operator=(const D &rhs) {
    Base::operator=(rhs);    //为基类部分赋值
    //为派生类部分赋值过程
    return *this;
}

对象的销毁过程与其创建过程顺序相反,先执行派生类的析构函数,再执行基类的析构函数。

当类的构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。因为如果基类构造函数使用了派生类的虚函数,此时派生类还未构造好,会访问未初始化的派生类部分。

C++11中,我们可以继承直接基类的构造函数:

class Bulk_quote : public Disc_quote {
public:
    using Disc_quote::Disc_quote;    //继承DIsc_quote的所有构造函数
    //其他成员
};

对于每个继承而来的构造函数,编译器都生成一个派生类的构造函数,它们的形参相同,并且派生类使用这些形参传递给基类的对应版本的构造函数来初始化基类的成员,而派生类的成员会被默认初始化。

一个构造函数的using声明不会改变该构造函数的访问级别,并且继承来的构造函数保留explicit和constexpr属性,但默认实参值不会被继承。

如基类中有含有默认实参的默认构造函数,则继承时将该默认函数继承为多个构造函数,其中每一个构造函数分别省略掉一个含有默认实参的形参,如含有两个形参而其中一个有默认实参的构造函数,会被继承为含有两个不含默认实参的构造函数和含有一个不含默认实参的构造函数(有默认实参的那个参数被省略了)。

继承基类的构造函数时,可以通过定义一个与某个基类的构造函数版本相同的派生类的构造函数,它们有一样的形参列表,会覆盖基类的版本。

默认、移动、拷贝构造函数不会被继承,它们会合成正常版本的。

我们不能在容器中存放继承体系中的对象,它们类型不同。如买书,如vector的类型为Bulk_quote,我们就不能存Quote类型对象,如类型为Quote,则保存进来的派生类对象的派生类部分被切掉了。因此我们应该将基类指针(最好是共享指针,因为派生类的智能指针类型也能转化为基类的共享指针类型)存放进容器中。

OOP编程中,我们必须使用引用或指针才能实现动态绑定,而不能直接使用对象,这增加了程序的复杂性,因此一般定义一些辅助类处理它,以下是一个表示购物篮的类:

class Basket {
public:
    //向购物篮中添加物品
    void add_item(const std::shared_ptr<Quote> &sale) {
        items.insert(sale);
    }  
    //打印清单
    double total_receipt(std::ostream&) const;
private:
    //该函数是用于multiset的比较函数,因为multiset默认使用<运算符规定的顺序元素,而Quote类没有<运算符,因此需定义比较元素大小的函数
    static bool compare(const std::shared_ptr<Quote> &lhs, const std::shared_ptr<Quote> &rhs) {
        return lhs->isbn() < rhs->isbn();
    }
    std::multiset<std::shared_ptr<Quote>, decltype(compare) *> items{compare};    //存放要购买的物品,存放的实际是Quote类指针,意味着该指针可以指向Quote及其所有派生类的对象,此处compare必须是花括号括起来的,因为类内数据成员初始化时只能使用花括号或拷贝初始化,而添加比较函数时拷贝初始化又是不行的
};

double Basket::total_receipt(ostream &os) const {
    double sum = 0.0;    
    for (auto iter = item.cbegin(); iter != items.cend(); iter = items.upper_bound(*iter)) {
        sum += print_total(os, **iter, item.count(*iter));    //print_total函数调用了net_price虚函数,因此会动态绑定,结果依赖于**iter的类型
    }
    os << "Total Sale: " << sum << endl;
    return sum;
}

但Basket的add_item函数依然有动态内存操作,用户使用时需传入一个共享指针,可以改进接口为:

void add_item(const Quote &sale);    //拷贝给定对象
void add_item(Quote &&);    //移动给定对象

在实现部分会有在堆内存开辟空间以保存给定内容的语句,但我们不知道实际传入的动态类型,如只写为new Quote(sale),会将派生类切掉只属于派生类部分。我们需要虚拷贝功能:

class Quote {
public:
    virtual Quote* clone() const & {
        return new Quote(*this);
    }
    virtual Quote* clone() && {
        return new Quote(std::move(*this));
    }
    //...
};

class Bulk_quote : public Quote {
public:
    Bulk_quote* clone() const & {
        return new Bulk_quote(*this);
    }
    Bulk_quote* clone() const && {
        return new Bulk_quote(std::move(*this));
    }
    //...
};

//新版add_item
class Basket {
public:
    void add_item(const Quote& sale) {
        items.insert(std::shared_ptr<Quote>(sale.clone()));
    }
    void add_item(Quote&& sale) {
        items.insert(std::shared_ptr<Quote>(std::move(sale).clone()));
    }
};

改进文本查询程序:功能说明:
1.单词查询:匹配给定单词的所有行。(Daddy)
2.逻辑非:匹配没有给定单词的所有行。(~(Daddy))
3.逻辑或:匹配两个条件中的符合任意一个条件的所有行。(daddy | Alice)
4.逻辑与:匹配符合两个条件的所有行。(daddy & Alice)

此外,还能支持逻辑运算符的混合使用,优先级与内置的运算符优先级一致。

类的设计:
在这里插入图片描述
以上类中,Query_base和BinaryQuery是抽象基类。

这些类只包含两个操作:
1.eval:接受一个TextQuery对象查询结果并返回一个QueryResult。
2.rep:打印要进行的操作的string版本,如~Query(“aaa”)转换成string版本为~(aaa)。

但以上继承体系用户层代码不应看到,我们定义一个Query类隐藏整个继承体系,它保存一个Query_base的指针绑定到Query_base的派生类上,Query的操作也是eval和rep,并且它会重载运算符以完成操作。

Query的重载运算符和构造函数作用:
1.&运算符生成一个绑定到AndQuery上的Query对象。
2.|运算符生成一个绑定到OrQuery上的Query对象。
3.~运算符生成一个绑定到NotQuery上的Query对象。
4.接受string参数的Query构造函数生成一个新的WordQuery对象。

使用Query:

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

根据上述代码创建对象过程:
在这里插入图片描述
综上,Query的设计:
在这里插入图片描述
Query_base类:

class Query_base{
    friend class Query;
protected:
    using line_no = TextQuery::line_no;
    virtual ~Query() = default;
private:
    virtual QueryResult eval(const TextQuery&) const = 0;
    virtual std::string rep() const = 0;
};

Query类:

class Query {
    friend Query operator~(const Query &);
    friend Query operator|(const Query&, const Query&);
    friend Query operator&(const Query&, const Query&);
public:
    Query(const std::string&);
    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) { }    //私有构造函数,仅供友元使用,接受一个Query_Base的指针赋值给q,这是重载的运算符函数创建相应Query对象时需要的
    std::shared_ptr<Query_base> q;
};

输出运算符:

std::ostream &operator<<(std::ostream &os, const Query &query) {
    return os << query.rep();
}

Query andq = Query(sought1) & Query(sought2);
cout << andq << endl;    //调用andq的Query::rep函数,而此函数会调用andq的q成员的rep函数,而此q成员指向的是AndQuery类型对象,因此实际调用的是AndQuery::rep函数

接下来是Query_base的派生类设计,它的派生类应能进行任意两种派生类类型的运算,如AndQuery可能两端分别是AndQuery类对象和WordQuery类对象,为实现此灵活性,必须以Query_base的指针形式存储运算对象。我们实际上不需要以Query_base指针存储运算对象,而是直接使用一个Query对象存储,这样使用其他类的接口可以简化类。

WordQuery类:

class WordQuery : public Query_base {
    friend class Query;    //Query要使用WordQuery的构造函数
    WordQuery(const std::string &s) : query_word(s) { }
    QueryResult eval(const TextQuery &t) const {
        return t.query(query_word);
    }
    std::string rep() const {
        return query_word;
    }
    std::string query_word;    //要查找的单词
};

WordQuery类只会被Query调用,因此它的所有成员包括构造函数成员都是私有的,因此需要把Query声明为友元。

现在就能定义Query类的接受一个string的构造函数了:

//将q指向一个WordQuery类对象,此对象创建在堆内存中
inline Query(const std::string &s) : q(new WordQuery(s)) { }

NotQuery类:

//NotQuery也会被当做一个Query类处理,NotQuery中也含有~运算符作用的Query对象
class NotQuery : public Query_base {
    friend Query operator~(const Query &);
    NotQuery(const Query &q) : query(q) { }
    std::string rep() const {
        return "~(" + query.rep() + ")";    //此处调用看似是实调用,但保存~运算符的运算对象指针的Query对象会调用q->rep(),这是虚调用
    }
    QueryResult eval(const TextQuery &) const;    //计算过程
    Query query;    //此Query仅保存~运算符的运算对象的指针,即数据成员q
};

inline Query operator~(const Query &operand) {
    return std::shared_ptr<Query_base>(new NotQuery(operand));    //~操作符创建一个NotQuery对象的指针,而返回类型为Query,这隐含着使用Query的接受一个指针的构造函数来进行类型转换
}

模拟一下~Query("aaa")的真实计算过程,首先这会调用Query的接受一个string的构造函数生成一个WordQuery类对象,之后使用~运算符,该运算符用NotQuery的接受一个Query的构造函数生成一个NotQuery类对象,该NotQuery类对象中保存的query成员的q指针指向的是上述刚生成的WordQuery类对象,因此在该NotQuery的rep函数中调用Query::rep(),之后Query::rep()又使用q->rep()虚调用WordQuery::rep(),最终打印出"~(aaa)"。

BinaryQuery类:

class BinaryQuery : public Query_base {
protected:
    BinaryQuery(const Query &l, const Query &r, std::string s) : lhs(l), rhs(r), opSym(s) { }
    std::string rep() const {
        return "(" + lhs.rep() + " " + opSym + " " + rhs.rep() + ")";
    }
    Query lhs, rhs;    //保存左右运算对象
    std::string opSym;    //保存运算符
};

BinaryQuery不定义eval,而是直接继承该纯虚函数,因此它也是抽象基类。而rep()可以直接被派生类继承使用。

AndQuery类:

class AndQuery : public BinaryQuery {
    friend Query operator&(const Query &, const Query &);
    AndQuery(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 AndQuery(lhs, rhs));
}

OrQuery类:

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));
}

OrQuery::eval():

//返回两个运算对象查询结果set的并集
//TextQuery中含begin和end成员以允许我们对保存行号的set迭代,还含get_file成员以返回指向待查询文件的shared_ptr
QueryResult OrQuery::eval(const TextQuery &text) const {
    auto right = rhs.eval(text), left = lhs.eval(text);    //左右运算对象的QueryResult
    auto ret_lines = make_shared<set<line_no>>(left.begin(), left.end());    //将左侧运算对象的行号拷贝到结果的set中
    ret_lines->insert(right.begin(), right,end());    //将右侧运算对象的行号拷贝到结果set中,此时完成了行号的或操作
    return QueryResult(rep(), ret_lines, left.get_file());    //该构造函数三个形参含义:第一个表示查询的string,第二个表示指向匹配行号set的shared_ptr,第三个表示指向文件vector的shared_ptr
}

AndQuery::eval():

QueryResult AndQuery::eval(const TextQuery &text) const {
    auto left = lhs.eval(text), right = rhs.eval(text);
    auto ret_lines = make_shared<set<line_no>>();
    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,它接收两个输入序列和一个表示位置的迭代器,上例中用插入器表示,插入器绑定在*ret_lines即一个set上,插入位置为该set的begin()位置。

NotQuery::eval():

QueryResult NotQuery::eval(const TextQuery &text) const {
    auto result = query.eval(text);
    auto ret_lines = make_shared<set<line_no>>();
    auto beg = result.begin(), end = result.end();    //表示保存行号的set的整个范围
    auto sz = result.get_file()->size();    //文件行数
    for (size_t n = 0; n != sz; ++n) {
        if (beg == end || *beg != n) {    //若当前循环到的行数不等于set中beg指向的行数或查询出来的所有行数已经都判断过时,将当前行插入ret_lines
             ret_lines->insert(n);   
        }
        else if (beg != end) {    //如结果行数还没判断完,并且当前循环到的行存在与结果行中时,递增beg
            ++beg;
        }
    }
    return QueryResult(rep(), ret_lines, result.get_file());
}
发布了211 篇原创文章 · 获赞 11 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/tus00000/article/details/105368994