S15面向对象程序设计

本文深入讲解面向对象程序设计的核心概念,包括数据抽象、继承和动态绑定。探讨了基类和派生类的设计原则,虚函数的使用技巧以及多态性的实现机制。

S15面向对象程序设计


一、OOP:概述

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

1、数据抽象:实现了类的接口与分离,参考S07类

2、继承
通过继承联系在一起的类构成一种层次关系,在层次关系的根部有一个基类,其他类则直接或间接地从基类继承而来,这些继承得到的类称为派生类
(1)基类将类型相关的函数与派生类不做改变直接继承的函数区别对待,当基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数
(2)派生类必须通过类派生列表明确指出它是从哪个/哪些基类继承而来的,类派生列表以冒号开头,紧跟逗号分隔的基类列表,并且必须在内部对所有重新定义的虚函数进行声明,此时可加可不加virtual

class Bulk_quote : public Quote  //定义了public则完全可以将Bulk_quote当作Quote来使用,is-a的关系
{
public:
    double net_price(size_t) const override;  //override显式注明派生类将使用哪个成员函数改写基类虚函数
};

3、动态绑定
通过动态绑定,又称为运行时绑定,即在运行时选择函数的版本,能够利用同一段代码处理不同的对象,具体实现将根据实际传入的类来调用具体的函数

double print_total(ostream &os, const Quote &item, size_t n);   //内部调用了item.net_price(n);

//basic的类型是基类Quote,bulk的类型是派生类Bulk_quote
print_total(cout, basic, 20);     //调用Quote的net_price
print_total(cout, bulk, 20);      //调用Bulk_quote的net_price

注意:当使用基类的引用/指针调用一个虚函数时将发生动态绑定

二、定义基类和派生类

1、定义基类
(1)虚析构函数:基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此
(2)成员函数与继承:

  • 派生类可以继承基类的成员,当遇到与类型相关的操作时,派生类可以对这些操作重新定义(override)以覆盖从基类继承而来的旧定义
  • 基类必须区分出两种成员函数,一种是基类希望派生类进行覆盖的函数,通常定义为虚函数(virtual),并且这个函数在派生类中隐式地也是虚函数,另一种是基类希望派生类直接继承而不改变的函数

注意:任何构造函数之外的非静态函数都可以是虚函数,并且virtual只能出现在类内的声明语句而不能出现在类外的定义语句

(3)访问控制与继承
派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员,派生类能访问public成员而不能访问private成员,对于基类希望它的派生类有权访问同时禁止其他访问的可以定义为protected成员

2、定义派生类
派生类必须通过类派生列表明确指出它是从哪个/哪些基类继承而来,继承自一个基类称为单继承,继承自多个基类称为多继承
(1)派生类中的虚函数
派生类可以覆盖它继承的虚函数,前缀virtual可加可不加,并可以通过override显式说明覆盖(类外定义不需要标注override),派生类也可以不覆盖它继承的虚函数,此时会直接继承这个函数在基类中的版本
(2)派生类对象及派生类向基类的类型转换

  • 一个派生类对象包含多个组成部分:派生类自己定义的非静态成员的子对象,派生类继承的基类对应的子对象(多个基类则有多个子对象)
  • 派生类到基类的类型转换:能把派生类的对象当成基类来使用,也能将基类的指针或引用绑定到派生类对象中的基类部分上,编译器会隐式执行这种转换
Quote item;               //基类对象
Bulk_quote bulk;          //派生类对象
Quote *p = &item;         //p指向Quote对象
p = &bulk;                //p指向bulk的Quote部分
Quote &r = bulk;          //r绑定到bulk的Quote部分

注意:需要基类的时候可以用派生类(含有基类部分,继承的核心),但需要派生类的时候不可以用基类

(3)派生类构造函数
派生类无法直接初始化继承的基类成员,而是要通过基类的构造函数来初始化它的基类部分,首先初始化基类部分,然后按照声明顺序依次初始化派生类的成员

Bulk_quote(const string &book, double p, size_t qty, double disc) 
    : Quote(book, p), min_qty(qty), discount(disc) { }  //调用基类构造函数来初始化派生类对象的基类部分

注意:每个类控制它自己的成员初始化过程

(4)派生类使用基类的成员
派生类可以访问基类的publicprotected成员,遵循基类的接口,尽可能调用接口来访问基类,例如用基类的构造函数来初始化派生类中基类的部分
(5)继承与静态成员
整个继承体系中只存在静态成员的唯一定义,不论有多少基类和派生类,只要在这个继承体系内都只有该静态成员唯一的实例,并且遵循通用的访问控制规则
(6)派生类的声明
派生类在其他地方的声明不需要标明派生列表,派生列表及与定义有关的细节与类的主体一同出现
(7)被用作基类的类
一个类被作为基类必须已经是完整定义的,一个类是基类同时也可以是派生类,直接基类和间接基类,最终的派生类将包含所有直接基类的子对象和所有间接基类的子对象

class Base {...};               //Base将作为D1的直接基类,D2的间接基类,必须是完整定义的
class D1: public Base {...};    //D1是Base的派生类,同时也是D2的直接基类
class D2: public D1 {...};      //D2包含了Base子对象和D1子对象

(8)防止继承的发生
当不希望一个类被其他类继承时,即不能作为基类,就在类名后跟随final

class Last final : Base {...};  //Last是Base的派生类,并且Last不会再被继承

3、类型转换与继承
(1)静态类型与动态类型
当使用存在继承关系的类型时,必须区分静态类型和动态类型,静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型,而动态类型是内存中的对象的类型,直到运行时才可知

注意:如果表达式非指针/引用,则静态类型与动态类型一致,对于基类的指针/引用其静态类型和动态类型可能不一致(因为基类指针/引用可以绑定到派生类上)

(2)不存在从基类向派生类的隐式类型转换
由于派生类对象包含基类的子对象,故派生类中一定能找到基类对象的部分,而反过来基类对象可能独立存在也可能是某个派生类对象的子对象,因此只能从派生类向基类,而不能从基类向派生类转换

Quote base;
Bulk_quote *bulkp = &base;   //错误
Bulk_quote &bulkr = base;    //错误
Bulk_quote bulk;
Quote *itemP = &bulk;        //正确,派生类转换为基类
Bulk_quote *bulkP = itemP;   //错误!

注意:即使一个基类指针/引用绑定在派生类上,同样也不能从这个基类指针/引用隐式转换到派生类

(3)在对象之间不存在类型转换
派生类向基类的转换只对指针/引用有效,在派生类类型和基类类型之间不存在这样的转换;派生类向基类的转换使得允许给基类的拷贝/移动函数传递一个派生类的对象,实际运行的函数是基类中的,因此只能处理派生类中基类部分的成员,而对于派生类其他部分就被简单切掉(忽略)了

Bulk_quote bulk;    //派生类对象
Quote item(bulk);   //正确,调用Quote::Quote(const Quote&)构造函数
item = bulk;        //正确,调用Quote::operator=(const Quote&)
                    //处理时只处理Quote有的bookNo/price成员,同时忽略掉bulk中的Bulk_quote部分的成员

(4)存在继承关系的类型之间的转换规则

  • 从派生类向基类的类型转换只对指针或引用类型有效
  • 基类向派生类不存在隐式类型转换
  • 和任何其他成员一样,派生类向基类的转换也可能由于访问受限(例如私有继承)而变得不可行
  • 显式/隐式定义了拷贝控制成员时,可以将派生类对象拷贝、移动、赋值给基类对象,但只处理派生类对象的基类部分
三、虚函数

1、对虚函数的调用可能在运行时才被解析
动态绑定只有当通过指针和引用调用虚函数时才会发生,而其他调用(如普通类型的表达式)虚函数时在编译期就会确定调用的函数版本,所有虚函数(纯虚函数除外)都必须定义(普通函数若未使用可以不定义)

注意:C++作为面向对象的语言,核心思想在于多态性,具有继承关系的多个类型称为多态类型,引用/指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在

2、派生类中的虚函数
虽然在派生类中覆盖一个虚函数时也可以前缀virtual,但不是必要的,一旦一个函数被声明为虚函数,则在所有派生类中它都是虚函数,并且形参类型与被覆盖基类函数完全一致,返回类型只允许一个不同的例外:基类中虚函数可以返回基类对象的指针/引用,派生类中虚函数可以返回派生类对象的指针/引用,这也要求派生类到基类的类型转换是可访问的

3、finaloverride说明符
(1)final:定义为final的虚函数将禁止任何后续尝试覆盖该函数的操作
(2)override:对于虚函数来说,若在派生类中定义的“虚函数”形参列表与基类不一样,这也是合法的,编译器会认为这是两个同名的不同函数(类似重载),并且不会发生覆盖,若通过override显式说明覆盖而形参列表不一样,则编译器才会报错;同样的若是一个基类的普通函数而在派生类中使用override试图覆盖也会报错,但不使用override则会认为是重载

struct B
{
    virtual void f1(int) const;
    virtual void f2();
    void f3();
};
struct D1 : B
{
    void f1(int) const override;    //正确,f1与基类B中的f1匹配
    void f2(int) override;          //错误,类型不匹配
    void f3() override;             //错误,f3不是虚函数
    void f4() override;             //错误,基类没有f4
};
struct D2 : B
{
    void f1(int) const final;       //正确,不允许后续其他继承D2的类覆盖f1
};
struct D3 : D2
{
    void f2();                      //正确,覆盖间接基类B继承来的f2
    void f1(int) const;             //错误,D2开始f1已经是final
};

注意:final/override说明符出现在形参列表(包括const或任何引用修饰符)及尾置类型返回之后

4、虚函数与默认实参
如果某次调用使用了默认实参,默认实参的值由本次调用的静态类型决定,尽可能不用默认实参

5、回避虚函数的机制
若希望不要动态绑定,则使用作用域运算符来显式指定调用的函数版本,往往利用基类中的虚函数完成共同的操作,而利用派生类中的虚函数完成与派生类自身相关的操作再调用基类版本的函数

注意:若在派生类中虚函数里调用基类版本而没有用::时会解析成调用自身造成无限递归

四、抽象基类

1、纯虚函数
纯虚函数只需要声明而不需要定义,通过在类内声明虚函数时末尾书写=0替代函数体{}就可以将一个虚函数说明为纯虚函数

2、含有纯虚函数的类是抽象基类
含有纯虚函数的类是抽象基类,抽象基类负责定义接口,而后续的其他类可以覆盖该接口,不能(直接)创建一个抽象基类的对象,但是派生类可以使用抽象基类的构造函数来构建派生类中抽象基类子对象那一部分

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

注意:重构(refactoring)负责重新设计类的体系以便将操作/数据从一个类移动到另一个类,对于OOP的程序来说,这是很普遍的现象

五、访问控制与继承

每个类分别控制自己的成员初始化的过程,同时也分别控制成员对于派生类来说的可访问性

1、受保护的成员,protected
(1)和private类似,protected成员对于类的用户来说是不可访问的
(2)和public类似,受保护的成员对于派生类的成员和友元来说是可访问的
(3)派生类的成员或友元只能通过派生类来访问派生类对象中基类部分的受保护成员,而对基类对象中的成员不具有特殊的访问权限

class Base
{
    friend class Pal;
protected:
    int prot_mem;
};
class Sneaky : public Base
{
    friend void clobber(Sneaky&);       //能访问Sneaky::prot_mem
    friend void clobber(Base&);         //不能访问Base::prot_mem
    int j;
};
class Pal
{
public:
    int f(Base b){ return b.prot_mem; }     //正确,Pal是Base的friend
    int f2(Sneaky s){ return s.j; }         //错误,Pal不是Sneaky的friend且s.j是private成员
    int f3(Sneaky s){ return s.prot_mem; }  //正确,Pal是Base的friend且s.prot_mem是继承来的成员
                                            //对基类的访问权限由基类本身控制,对于派生类的基类部分也是如此
}

2、公有、私有和受保护继承
某个类对继承而来的成员的访问权限受两点影响:一是基类中该成员的访问说明符,二是派生列表中访问说明符

生访问说明符父类中的成员访问说明符说明
publicpublic继承为子类的public,允许类以外代码访问
publicprivate仍旧是父类的private,子类无法访问
publicprotected继承为子类的protected,只有子类可以访问
privatepublic继承为子类的private,只有子类可以访问
privateprivate仍旧是父类的private,子类无法访问
privateprotected继承为子类的private,只有子类可以访问
protectedpublic继承为子类的protected,只有子类可以访问
protectedprivate仍旧是父类的private,子类无法访问
protectedprotected继承为子类的protected,只有子类可以访问

注意:派生访问说明符说明了父类中的成员在继承时访问权限应该如何变化,例如继承是public则成员遵循原访问说明符等,基类的private在任意继承下派生类中都不可见

3、派生类向基类转换的可访问性
假定D继承自B:
(1)只有当D公有地继承B时,用户代码才能使用派生类向基类的转换,私有/受保护继承时用户代码不能转换
(2)不论D如何继承,D的成员函数和友元都能使用派生类向基类的转换,即派生类向直接基类的类型转换对于派生类和友元来说永远可访问
(3)如果D公有/受保护地继承B时,则D的派生类的成员和友元可以使用D向B的类型转换,私有继承则不能转换

注意:要确定到基类的转换是否可访问,可以考虑基类的public成员是否可访问,如果可以,转换是可以访问的,否则,转换是不可访问的,即可以将转换看作是public的成员函数

4、友元和继承
友元关系不能传递,同样不能继承,基类的友元在访问派生类时不具有特殊性,派生类的友元在访问基类时也不具有特殊性,每个类负责控制自己的成员的访问权限,对派生类中基类的部分访问权限也由基类控制而不是派生类,并且权限也仅限做出友元声明的类,对于友元的基类/派生类同样不具有特殊性(友元关系不可传递/继承)

5、改变个别成员的可访问性
使用using声明可以改变派生类继承的某个名字的访问级别

class Base
{
public:
    size_t size() const { return n; }
protected:
    size_t n;
}
class Derived : private Base  //private继承,则publicsize()和protected的n都变为Derived的private
{
public:
    using Base::size;  //子类可以访问size因此可以using,且前方是public,则Derived的用户可以直接使用size
protected:
    using Base::n;     //子类可以访问n因此可以using,且前方是protected,则Derived的派生类可以使用n
}

注意:只有派生类自身能访问的名字,才能用using,且using声明的名字访问权限由using前的访问说明符来决定

6、默认的继承保护级别
struct默认成员均是publicclass默认成员均是private一样,不加派生访问说明符时,struct默认public继承,class默认private继承

六、继承中的类作用域

派生类的作用域嵌套在其基类的作用域中,如果一个名字在派生类的作用域内无法解析,编译器则继续在外层的基类作用域中寻找该名字的定义

1、在编译时进行名字查找
(1)一个对象/引用/指针的静态类型决定了对象的哪些成员是可见的,即使静态类型与动态类型可能不一致,但仍由静态类型决定成员是否可访问

//设discount_policy()是class Bulk_quote的public non-virtual成员
Bulk_quote bulk;
Bulk_quote *bulkP = &bulk;    //静态类型与动态类型一致
Quote *itemP = &bulk;         //静态类型是Quote,动态绑定到Bulk_quote上,不一致
bulkP->discount_policy();     //正确,静态类型是Bulk_quote,可以访问discount_policy()
itemP->discount_policy();     //错误,虽然动态类型是Bulk_quote但是静态是Quote,从Quote开始无法找到dis.

(2)名字查找的过程
假定调用p->mem()p.mem()

  • 首先确定p的静态类型(引用、指针、对象)
  • p的静态类型对应的类中查找mem,如果找不到则依次到直接基类中不断查找直至顶端,若一直找不到则编译器报错
  • 若找到了mem则进行函数匹配的常规类型检查,以确认本次调用是否合法
  • 若调用合法,则编译器根据mem是否是虚函数产生不同的代码:如果mem是虚函数且是通过指针调用的(p->mem()),则将在运行时动态绑定;如果mem不是虚函数或是通过对象调用的(p.mem()p不是引用,若引用则会动态绑定),则进行常规函数调用

2、名字冲突与继承
与作用域的覆盖一致,派生类能重用定义在其直接/间接基类中的名字,此时定义在内层作用域(派生类)的名字隐藏外层作用域(基类)的名字

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

注意:除了覆盖虚函数之外,派生类最好不要重用定义在基类中的名字

4、名字查找优先于类型检查
声明在内层作用域的函数不会重载外层作用域的函数,只要同名就直接隐藏

5、虚函数与作用域
基类与派生类中的虚函数必须有相同的形参列表(覆盖),否则就无法通过基类的引用/指针来调用派生类的虚函数

6、覆盖重载的函数
成员函数无论是否是虚函数都能被重载,派生类可以覆盖重载函数的0个或多个实例,如果派生类希望所有的重载版本对它都是可见的,那么就需要覆盖所有的版本或者一个都不覆盖,如果只需要覆盖了重载函数一部分则要使用using声明来确保其他未覆盖的版本也可见(否则根据名字查找规则,只覆盖了部分重载函数,就会因为同名隐藏掉其他没有被覆盖的)

class Base
{
public:
    virtual void fun(int a) { cout << "Base::fun(int)" << endl; };
    virtual void fun(double a) { cout << "Base::fun(double)" << endl; };
    virtual void fun(char a) { cout << "Base::fun(char)" << endl; };
};
class D1 :public Base
{
public:
    using Base::fun;  //使用using使得其他不需做覆盖但是也要求可见的重载函数变得可见
    void fun(int a) override { cout << "D1::fun(int)" << endl; };
};
int main()
{

    D1 d1;
    D1 *b = &d1;
    b->fun(0.0);     //有using,则Base::fun(double);没有using,则D1::fun(int),其他未覆盖重载函数被隐藏
    b->fun(0);       //有using,则D1::fun(int);没有using,则D1::fun(int)
    return 0;
}
七、构造函数与拷贝控制

1、虚析构函数
继承关系对基类拷贝控制最直接的影响是基类通常需要定义一个虚析构函数,这样就能确保在delete基类指针时将运行正确的析构函数版本

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

注意:虚析构函数(即使使用=default的合成版本)将阻止合成移动操作

2、合成拷贝控制与继承
(1)派生类中删除的拷贝控制与基类的关系
某些定义基类的方式将导致派生类成员称为被删除的函数

  • 基类中默认与拷贝构造函数、拷贝赋值运算符、析构函数如果是被删除的或者不可访问的,则派生类中对应的成员将是被删除的,显然这是因为派生类对象需要基类成员来构造、赋值、销毁派生类对象中的基类部分
  • 基类中如果有不可访问或删除的析构函数,则派生类中合成的默认/拷贝构造函数将是删除的,显然这是因为派生类对象需要基类析构函数来销毁派生类对象的基类部分
  • 基类中如果有不可访问或删除的析构函数或移动操作,则派生类中的移动操作是删除的,显然这是因为派生类中的基类部分需要基类的移动操作,并且移动操作要保证移后源对象可析构

注意:如果在基类中没有默认、拷贝、移动构造函数,则一般情况下在派生类中也不会定义相应的操作(否则派生类要考虑如何操作基类部分的成员)

(2)移动操作与继承
大多数基类会定义虚析构函数,因此如果要有移动操作必须自己显式定义,一旦基类定义了移动操作则必须同时显式定义拷贝操作,并且除非派生类中含有排斥移动的成员,否则会自动获得合成的相应操作

3、派生类的拷贝控制成员
当派生类定义了拷贝/移动操作时,该操作负责拷贝/移动包括基类成员在内的整个对象
(1)定义派生类的拷贝或移动构造函数
当定义派生类的拷贝或移动构造函数时,使用对应的基类构造函数来初始化派生类对象的基类部分,默认情况下基类默认构造函数初始化派生类对象中的基类部分,若想拷贝/移动基类部分则需要在派生类构造函数初始值列表中显式使用基类的拷贝/移动构造函数

class D : public Base
{
public:
    D(const D &d) : Base(d) ...
    D(D &&d) : Base(std::move(d)) ...
    D(const D &d) : ...    //不显式调用Base(d)会导致D类对象的基类部分默认初始化而其他部分是拷贝而来这种奇异情况
}

(2)派生类赋值运算符
同理,也不会被自动调用,需要显式调用基类赋值运算符

注意:无论基类的拷贝、移动、赋值是自定义还是合成版本,派生类的对应操作都能直接使用

(3)派生类析构函数
析构函数只负责销毁自己所在类分配的资源,销毁整个对象时,派生类析构函数首先执行,然后自动执行直接基类的析构函数,以此类推沿继承体系反方向依次销毁,直至析构整个对象
(4)在构造函数和析构函数中调用虚函数
如果构造/析构函数调用了某个虚函数,则应该执行与构造/析构函数所属类相应的虚函数版本

4、继承的构造函数
C++11中允许派生类继承直接基类的构造函数,但不能继承默认、拷贝、移动构造函数,如果派生类没有显式定义则编译器将合成默认、拷贝、移动构造函数
派生类通过using来继承直接基类的构造函数,并且如果派生类有自己的数据成员,将默认初始化,继承构造函数涉及默认实参参考《C++primer》p.558

class Bulk_quote : public Disc_quote
{
public:
    using Disc_quote::Disc_quote; //编译器将生成形如derived(parms) : base(args) { }的构造函数
    ...
};

//上述继承构造函数等价于
Bulk_quote(const string &book, double price, size_t qty, double disc) : Disc_quote(book, price, qty, disc) { }

注意:这里用到using和其他using不同,这里的using不会改变该构造函数的访问级别,即例如不管using声明出现在何处,基类的私有构造函数用using继承之后在派生类中依然是私有的,并且基类的构造函数若有explicit/constexpr则都会继承,无法选择

注意:如果基类含有多个构造函数,则在以下两个情况外,这些构造函数都可以通过using继承

  • 部分派生类定义的构造函数与基类的构造函数具有相同的参数列表,则派生类的定义会替换继承的构造函数,其他没有相同参数列表的构造函数继续继承
  • 默认、拷贝、移动构造函数不会被继承,将自动合成,并且继承的构造函数不会作为用户定义的构造函数来使用,因此若只有继承构造函数,则依然会合成一个默认构造函数
八、容器与继承

当派生类对象被赋值给基类对象时,派生类部分将被直接丢弃,因此容器与存在继承关系的类型无法兼容,往往采用容器中存放基类的指针的方法,派生类的智能指针也能自动转换成基类的智能指针

九、一些参考代码
class Quote
{
public:
    Quote() = default;
    Quote(const string &book, double sales_price) :bookNo(book), price(sales_price) {}
    string isbn() const { return bookNo; }
    virtual double net_price(size_t n) const { return n*price; }

    //对虚构函数进行动态绑定,否则的话在派生类虚构时会出现问题
    virtual ~Quote() = default;

    virtual void debug() const { cout << "bookNo = " << bookNo << endl; cout << "price = " << price << endl; }
private:
    string bookNo;
protected:
    double price = 0.0;
};

class Disc_quote :public Quote
{
public:
    Disc_quote() = default;

    //基类的数据成员虽然继承下来了但是是private的,派生类无法访问
    //只能调用基类的构造函数Quote(book, p)来初始化基类部分的成员
    Disc_quote(const string &book, double p, size_t qty, double disc) :Quote(book, p), quantity(qty), discount(disc) {}

    //=0表明是纯虚函数
    double net_price(size_t n) const = 0;

    //派生类无法访问基类bookNo,只能调用基类的debug来打印,注意不可省略Quote::
    void debug() const override { Quote::debug(); cout << "quantity = " << quantity << endl; cout << "discount = " << discount << endl; }

//protected则派生类可以访问而其他类无法访问
protected:
    size_t quantity = 0;
    double discount = 0.0;
};

class Bulk_quote :public Disc_quote
{
public:
    Bulk_quote() = default;
    Bulk_quote(const string &book, double p, size_t qty, double disc) :Disc_quote(book, p, qty, disc) {}

    //使用override来显式说明覆盖,让编译器帮忙检查类型不匹配等错误
    double net_price(size_t n) const override;
};

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

class Limited_quote :public Disc_quote
{
public:
    Limited_quote() = default;
    Limited_quote(const string &book, double p, size_t qty, double disc) :Disc_quote(book, p, qty, disc) {}
    double net_price(size_t n) const override;
};

inline double Limited_quote::net_price(size_t n) const
{
    if (n <= quantity)
    {
        return n*(1 - discount)*price;
    }
    else
    {
        return n*price;
    }
}

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

int main()
{
    Quote Q("222-5-999", 29.99);
    Bulk_quote B("854-2-111", 15.8, 5, 0.5);
    Limited_quote L("213-0-543", 40, 30, 0.88);
    Disc_quote A; //错误,无法直接生成抽象基类
                  //VS提示"object of abstract class type "Disc_quote" is not allowed: 
                  //function "Disc_quote::net_price is a pure virtual function"
                  //编译Error"'Disc_quote':cannot instantiate abstract class"
    print_total(cout, Q, 2);
    print_total(cout, B, 2);
    print_total(cout, L, 2);
    cout << "Q" << endl;
    Q.debug();
    cout << "B" << endl;
    B.debug();
    cout << "L" << endl;
    L.debug();

    return 0;
}
练习15.20:
class Base
{
public:
    void pub_mem();
protected:
    int prot_mem;
private:
    char priv_mem;
};
struct Pub_Derv :public Base
{
    //正确//公有继承,转换(均指*this代表当前的类型向直接/间接基类Base的转换)看作是父类的public,此时可访问
    void memfcn(Base &b) { b = *this; }  
};
struct Priv_Derv :private Base
{
    //正确//私有继承,转换看作是父类的public,此时可访问
    void memfcn(Base &b) { b = *this; }  
};
struct Prot_Derv :protected Base
{
    //正确//保护继承,转换看作是父类的public,此时可访问
    void memfcn(Base &b) { b = *this; }  
};
struct Derived_from_Public :public Pub_Derv  
{
    //正确//从公有继承继承,转换在Base中是public而继承到Pub_Derv中也是public,则此时可访问
    void memfcn(Base &b) { b = *this; }  
};
struct Derived_from_Private :public Priv_Derv  
{
    //错误//Base is inaccessible//从私有继承继承,转换在Base中是public
    //而继承到Priv_Derv中变成private,子类此时不可访问父类的private
    void memfcn(Base &b) { b = *this; }  
};
struct Derived_from_Protected :public Prot_Derv  
{
    //正确//从保护继承继承,转换在Base中是public而继承到Prot_Derv中变成protected,只有子类可以访问
    void memfcn(Base &b) { b = *this; }  
};

int main()
{
    Pub_Derv d1;
    Priv_Derv d2;
    Prot_Derv d3;
    Derived_from_Public dd1;
    Derived_from_Private dd2;
    Derived_from_Protected dd3;
    Base *p;
    p = &d1;  //正确
    p = &d2;  //错误//conversion to inaccessible base class is not allowed
    p = &d3;  //错误//same ...
    p = &dd1; //正确
    p = &dd2; //错误//same ...
    p = &dd3; //错误//same ...
    return 0;
}
虚函数与作用域练习:
class Base
{
public:
    virtual int fcn();
};
class D1 : public Base
{
public:
    int fcn(int);        //没有覆盖Base中的虚函数fcn而隐藏了它,并且D1继承了Base::fcn()只是被隐藏了
    virtual void f2();   //新的虚函数,Base中不存在
};
class D2 : public D1
{
public:
    int fcn(int);        //非虚函数,隐藏了D1的fcn(int)
    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(),运行时绑定D1但是未覆盖,调用Base中的fcn
bp3->fcn();  //虚调用,同上找到Base::fcn(),发现是虚函数且是指针调用,运行时绑定D2,调用D2中的fcn

D1 *d1p = &d1obj; D2 *d2p = &d2obj;
bp2->f2();   //错误,查找名字根据静态类型在Base中找不到f2,错误
d1p->f2();   //虚调用,运行时调用D1::f2()
d2p->f2();   //虚调用,查找名字根据静态类型找到D2::f2(),调用D2::f2()

//指针均指向D2类型的对象,但由于没有涉及虚函数,下面未发生动态绑定
Base *p1 = &d2obj; D1 *p2 = &d2obj; D2 *p3 = &d2obj; 
p1->fcn(42); //错误,查找名字根据静态类型找到Base::fcn,不能接受42,调用不合法
p2->fcn(42); //查找名字根据静态类型找到D2::fcn(int)且非虚函数,静态绑定,常规函数调用
p3->fcn(42); //查找名字根据静态类型找到D3::fcn(int)且非虚函数,静态绑定,常规函数调用
容器与继承练习15.29:
Quote Q("222-5-999", 29.99);
Bulk_quote B("854-2-111", 15.8, 5, 0.5);
Limited_quote L("213-0-543", 40, 30, 0.88);
vector<shared_ptr<Quote>> basket;               //存放基类的智能指针
basket.push_back(make_shared<Quote>(Q));        //使用make_shared生成智能指针
basket.push_back(make_shared<Bulk_quote>(B));
basket.push_back(make_shared<Limited_quote>(L));
double sum(0.0);
for (auto st = basket.begin(); st != basket.end(); ++st)
{
    sum += (*st)->net_price(5);                 //根据具体*st所指的类型进行动态绑定调用net_price()
}
cout << sum << endl;
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值