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

2023-11-09

面向对象程序设计(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) { }    // 使用基类的构造函数完成基类成员的初始化,虽然也可以在函数体内给基类的公有成员和受保护的成员赋值,但最好不这么做

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

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

如基类中定义了静态成员,则不管有多少个派生类,每个静态成员都只存在唯一的实例:

class A {
public:
    static int s;
};

int A::s = 5;

class B : public A { };

int main() {
    B b;
    A a;
    a.s = 10;
    cout << b.s << endl;    // 输出10
}

静态成员也遵循访问说明符,如果基类的静态成员是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转换规则(指针或引用是const的而对象本身不是)或存在继承关系的类。

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

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

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

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

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

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

派生类如果定义了一个与基类中虚函数的函数名相同的函数而它们两个的形参列表不同,那么派生类中的同名函数并没有覆盖基类中的虚函数,它们俩是相互独立的,但一般不这么使用,因为可能是形参列表搞错而导致没有覆盖基类的虚函数,一般这样的错误很难检查,因此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的友元函数能访问Sneaky对象的private和protected成员
}

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

如上,如果能通过友元函数来访问基类的protected成员,那么我们就可以创建一个基类的派生类,然后使用此派生类的友元函数来修改基类对象的值了。虽然我们也可以通过第一个友元函数这种用派生类对象来修改基类的成员,但必须先创建这样一个派生类对象,而不能直接修改或获取已有基类对象的protected成员。

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

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

派生类向基类转换,当D继承自B时:
1.只有D public继承自B时,用户代码才能进行派生类向基类的转换。private和protected继承时不能进行上述转换。
2.不管D以什么方式继承自B,D的成员函数和友元都能完成派生类向基类的转换,因为它俩都可以访问D的private成员,即使是私有继承,也能访问到B中public成员。
3.如D以public或protected方式继承自B,那么D的派生类的成员和友元可以完成派生类D向基类B的转换,因为D的派生类就算是私有继承自D,也可以访问B中的public成员和protected成员。

以上能实现转换的原则为,如果基类的公有成员是可访问的,那么就能实现转换。因为派生类向基类转换后,通过基类的引用或指针只能使用基类的成员,因此需要基类的public成员可访问,protected和private成员本来就无法使用类的用户的代码访问,因此只对public成员有要求。

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

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的非public成员,友元类不能继承。

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

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),除非D1中的fcn是覆盖的基类的虚函数
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属性,但默认实参值不会被继承:

class A {
private:
    A(int i = 5) {    // 必须要有参数,否则就是默认构造函数,默认构造函数不会被继承
        cout << "in A's private constructor" << endl;
    }
};

class B : public A {
public:
    using A::A;
};

int main() {
    B b(2);    // 调用失败,如果将A的构造函数改为public的,即可打印in A's private constructor
}

如基类中有含有默认实参的构造函数,则继承时将该函数继承为多个构造函数,其中每一个构造函数省略掉一个有默认实参的形参,如基类有一个接受两个形参的构造函数,其中第二个形参含默认实参,则会被继承为两个构造函数:一个构造函数含两个形参(这两个形参都没有默认实参),另一个构造函数只接受一个形参,这个形参对应于基类中最左边那个没有默认实参的形参:

class A {
public:
    A(int i, int j = 3) : mem(j) {
        cout << i << " " << mem << endl;
    }
private:
    int mem;
};

class B : public A {
public:
    using A::A;
};

int main() {
    B b(2);    // 输出2 3
}

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

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

我们不能在容器中存放继承体系中的对象,它们类型不同。如买书,如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_base() = 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::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类对象,该WordQuery类对象被保存在Query对象的q成员里,之后使用~运算符,该运算符用NotQuery的接受一个Query的构造函数生成一个NotQuery类对象,该NotQuery类对象中保存的query成员就是调用逻辑非运算符的Query类对象,因此在该NotQuery的rep函数中调用Query::rep()时,Query::rep()又会使用q->rep()虚调用WordQuery::rep(),最终打印出"~(aaa)"。不难发现,每次实际工作的(即执行查找操作的)都是WordQuery类对象,它被包含在Query类对象中,每当我们使用一次逻辑运算符,如~,就生成一个对应的NotQuery类对象,这个新生成的NotQuery类对象中会含有调用这个逻辑非运算符的Query类对象的指针,同时,逻辑非运算符又将它包含在一个新的Query类对象中,无限套娃,我们在最外层的Query对象上调用如rep(),它会使用Query中的q指针动态访问Query_base继承体系中某个类的对象的rep(),此处的q指向的对象取决于我们最后一步用的运算符,如最后一步用的逻辑非运算符,则q指向的就是NotQuery类对象,于是调用NotQuery类对象的rep(),该函数中又动态调用了倒数第二步逻辑运算生成的Query对象的q指针指向的对象的rep函数,重复进行直到调用了WordQuery对象的rep(),这是真正做事的rep()。

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());
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • 人工智能中非平衡数据处理方法、欠采样、过采样讲解(简单易懂)

    非平衡数据产生现象及原因 非平衡数据是人工智能安全中经常遇到的问题 一方面 在采集和准备数据时 由于安全事件发生的可能性不同等因素的影响 使得训练数据存在非平衡 另一方面 机器学习模型的攻击者也可能利用非平衡数据学习所产生的分类效果在多数类
  • springboot集成积木报表JimuReport,打成war包放到tomcat下运行报错,启动过滤器异常

    springboot集成积木报表JimuReport 打成war包放到tomcat下运行报错 启动过滤器异常 这里用的tomcat8 5 这个问题真是找了半天都没解决 真是栓Q啊 把war包放到tomcat9 0下运行 就正确了 一定注意
  • 初学者必看Markdown 使用指南

    什么是 Markdown Markdown 文档遵循一种特定的语法 容易阅读和写作 它们是纯文本 所以能够通过电脑上的任何文本编辑器来创建 然后这些文档能够转换成网页 而网页是用一个叫做 HTML 的语言标记创建的 Markdown 只是一
  • Microsoft Office 2003的安装

    哈喽 大家好 今天一起学习的是office2003的安装 这个老版本的office可是XP操作系统的老搭档了 有兴趣的小伙伴也可以来一起试试手 一 测试演示参数 演示操作系统 Windows XP 不建议win7及以上操作系统使用 系统类型
  • 文件使用磁盘的实现--OS

    文件使用磁盘的实现 通过文件使用磁盘 代码如下 在fs read write c中 int sys write int fd const char buf int count fd为文件索引 buf为缓冲区 count表示要处理的字符串长度
  • 图的遍历(BFS和DFS)

    一 遍历 lt 1 gt 遍历 把所有元素都看一遍 每看到一个元素 针对条件进行处理 lt 2 gt 线性逻辑 顺序存储 void fun1 type data int num for int i 0 i lt num i 逐个处理 typ
  • QT 通信之 QByteArray

    QT上位机的串口通信发送和接收数据都需要用到QByteArray QByteArray存储的是char型字符 但QByteArray提供的数组操作 比char更方便 这篇文章主要讲一下QByteArray在实际应用中的一些函数 以及QBby
  • 机器学习5:评估器estimator

    要定义与tf estimator一起使用的自定义模型 需要使用tf estimator Estimator tf estimator LinearRegressor 线性回归实际上是一个tf estimator Estimator的子类 我
  • ASCII码对照表(十进制和十六进制)

    表 A 1 DEC 多国字符集 十六进制代码 MCS 字符或缩写 DEC 多国字符名 ASCII 控制字符 1 00 NUL 空字符 01 SOH 标题起始 Ctrl A 02 STX 文本起始 Ctrl B
  • CentOS下载ISO镜像的方法

    目录 一 CentOS 介绍 二 进入CentOS 官方网站 三 步骤 一 CentOS 介绍 CentOS 中文意思是社区企业操作系统是Linux发行版之一 是免费的 开源的 可以重新分发的开源操作系统 CentOS Linux发行版是一
  • JDK1.8新特性——lambda表达式和函数式接口

    一 lambda表达式 1 概念 Lambda表达式时一种特殊的匿名内部类 语法更加简洁 Lambda表达式允许把函数作为一个方法的参数 函数作为方法参数传递 将代码像数据一样传递 这里的匿名内部类的理解 我们可以在下述情况中来帮助大家了解
  • 服务器运行多个安卓系统,一台服务器可以做几个云手机

    一台服务器可以做几个云手机 内容精选 换一换 本文介绍使用云手机服务时需要了解的基本概念 云手机是一台包含原生安卓操作系统 具有虚拟手机功能的云服务器 简单来说 云手机 云服务器 Android OS 您可以远程实时控制云手机 实现安卓AP

随机推荐

  • linux 批量解压gz文件夹,linux 批量解压gz bz2文件

    一 批量解压bz2文件 find maxdepth 1 name bz2 xargs i tar xvjf 这条命令可解压当前目录下的所有bz2文件 批量解压是比较郁闷的事 以前尝试各种方法 甚至用脚本循环语句解压都不行 现在发现这条命令可
  • JSON对象转换成Byte(字节)数组

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 如果你不了解JSON对象 请看这里 JSON对象转换成 byte 数组 Byte byteArray Byte jsonData bytes NSLog s byteArr
  • 如何追踪泄漏者信息?软件保护工具VMProtect独有水印快速锁定目标!

    VMProtect是一种很可靠的工具 可以保护应用程序代码免受分析和破解 但只有在应用程序内保护机制正确构建且没有可能破坏整个保护的严重错误的情况下 才能实现最好的效果 VMProtect提供了一种独特的功能 可以将有关受保护文件所有者的隐
  • 基于Python的微博大数据舆情分析,舆论情感分析可视化系统

    运行效果图 基于Python的微博大数据舆情分析 舆论情感分析可视化系统 系统介绍 微博舆情分析系统 项目后端分爬虫模块 数据分析模块 数据存储模块 业务逻辑模块组成 先后进行了数据获取和筛选存储 对存储后的数据库数据进行提取分析处理等操作
  • LeetCode 541. 反转字符串 II

    题目链接 https leetcode cn problems reverse string ii C 代码如下 class Solution public string reverseStr string s int k int n s
  • vant ui Swipe pc端滑动失效

    这里我使用了vant的Swipe组件 由于vant是移动端的组件库 对pc端会有兼容性问题 例如Swipe 移动端是 touch 该组件做了相应的监听 而PC端是 mouse 没有做对应的监听 因此在pc端无法用鼠标拖动图片 1 安装插件
  • redis BITFIELD详解

    支持子命令和整型 本命令会把Redis字符串当作位数组 并能对变长位宽和任意未字节对齐的指定整型位域进行寻址 下面是已支持的命令列表 GET
  • MYSQL深入学习(一)

    1 mysql 体系结构 连接池组件 管理服务和工具组件 sql接口组件 查询分析器组件 优化器组件 查询缓存组件 插件式存储引擎 mysql的特点 可以根据需求 动态的配置存储引擎 物理文件
  • idea控制台输出中文乱码解决

    解决Intellij IDEA控制台logger info system out println等中文乱码问题 一 编写环境乱码 二 控制台打印乱码 又包含3种 当我们使用Intellij IDEA开发时 首当其冲就是中文乱码问题 造成中文
  • unity物体范围内随机生成

    这个脚本需要挂载到需要随机生成的物体上 但不能是空物体 using System Collections using System Collections Generic using UnityEngine public class Ran
  • CDN回源原理和CDN多级缓存

    一 CDN概念 CDN的全称是Content Delivery Network 即内容分发网络 其基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节 使内容传输的更快 更稳定 CDN是通过在网络各处放置节点服务器所构成的
  • STL案例——评委打分案例

    有5名选手 选手ABCDE 10个评委分别对每一名选手打分 去除最高分 去除评委中最低分 取消平均分 1 创建五名选手 放到vector中 2 遍历vector容器 取出每一个选手 执行for循环 可以把10个评分打分存到deque容器中
  • Rxjs在Angular中的简单应用

    Angular中集成了Rxjs库 Rxjs是javascript的一个响应式编程库 它提供了很多api 可以很方便的处理和操作应用中的数据 我们在自己的angular项目中新建一个组件 ng generate component rx bu
  • Java多线程两种实现

    在java中实现多线程的方式有两种 一种是继承Thread类 另一个是实现Runnable接口 对于两种实现 各有优缺点 接下来进行对比总结一下 这两种方法 都可以实现多线程 以下为两种实现的写法 继承Thread类的方式 package
  • 五、语言特性之<=default,=delete、using、noexcept、override、final、以及和const对比>

    目录 一 default delete 1 首先我们要回顾一下编译器提供的默认函数 2 何时需要自定义big three 构造函数 拷贝构造 拷贝赋值 big five 新增移动构造函数 移动赋值函数 3 default delete关键字
  • Yearning做SQL审核

    系统环境 Centos7一 Inception安装 1 安装相关依赖包 yum install bison ncurses libs libncurses5 devel ncurses devel wget git cmake openss
  • C++模板特例化

    模板是用来写一些独立化特定类型的代码 但是对于有些类型 在处理时 细节上却有所差别 常见的如char 如 现在你打算写一个栈 可以用于任何数据类型 那你肯定首先想到的就是模板啦 template
  • LeetCode-797. All Paths From Source to Target

    Given a directed acyclic graph of N nodes Find all possible paths from node 0 to node N 1 and return them in any order T
  • 【满分】【华为OD机试真题2023 JAVA&JS】知识图谱新词挖掘1

    华为OD机试真题 2023年度机试题库全覆盖 刷题指南点这里 知识图谱新词挖掘1 知识点滑窗 时间限制 1s 空间限制 256MB 限定语言 不限 题目描述 小华负责公司知识图谱产品 现在要通过新词挖掘完善知识图谱 新词挖掘 给出一个待挖掘
  • C++ Primer 学习笔记 第十五章 面向对象程序设计

    面向对象程序设计 OOP 基于三个概念 数据抽象 只暴露类的接口 而如何实现的是不透明的 即类的接口和实现分离 继承 能实现相似的类型 动态绑定 忽略相似类型的区别 以统一方式使用它 继承关系联系在一起的类构成层次关系 在最低层有一个基类