从C到C++___类继承(一)公有继承

类继承(一)公有继承

OOP的主要目的之一是提供可重用的代码.而C++类提供了很好的重用性.目前很多厂商都提供了类库,类库由类声明和实现构成.但是,有时候我们需要对厂商提供的类进行修改,而C++提供了比直接修改代码更好的方式–类继承.它允许我们从已有的类派生出新的类,而派生类继承了原有类(或称基类)的特征.通过继承派生出的类通常比设计新类要容易得多.
通过继承可以完成的一些工作:

  1. 可以在基类的基础上添加功能.
  2. 可以给类添加数据.
  3. 可以修改类方法.

类继承还有一个好处是,可以在不公开实现的情况下将自己的类分发给其他人,同时允许他们在类中吧添加新特性.

1. 一个简单的例子

1.1 基类

#include<string>
using std::string;
class TableTennisPlayer//基类
{
    private:
        string firstname;
        string lastname;
        bool hasTable;
    public:
        TableTennisPlayer(const string &fn="none",const string &ln="none",bool ht=false);
        void Name() const;
        bool HasTable() const{return hasTable;};
        void ResetTable(bool v){hasTable=v;};
};
#include<iostream>
TableTennisPlayer::TableTennisPlayer(const string &fn,const string &ln,bool ht)
                    :firstname(fn),lastname(ln),hasTable(ht){}
void TableTennisPlayer::Name() const
{
    std::cout<<lastname<<", "<<firstname;
}

上面这段代码是一个简单的基类的实现,其中构造函数使用了成员初始化列表语法.
也可以这样写:

TableTennisPlayer::TableTennisPlayer(const string &fn,const string &ln,bool ht)
{
    firstname=fn;
    lastname=ln;
    hasTable=ht;
}

而且我们发现,这个类的类成员使用了string类,例如在上面这个构造函数中,首先firstname会调用string类的默认构造函数,然后再调用赋值运算符进行深复制把firstname设置成fn;
但是使用初始化列表语法可以减少一个步骤,它直接使用string的复制构造函数把firstname初始化为fn.

1.2 派生一个类

我们需要在原有TableTennisPlayer类基础上设计一个新的类,它能够包括选手的比赛得分.

class RatedPlayer:public TableTennisPlayer//一个简单的派生类
{
    ...
}

我们使用:指出,RatedPlayerTableTennisPlayer的一个派生类.
而且TableTennisPlayer是一个公有基类,这种称为公有派生.
派生类对象包含基类对象.
使用公有派生,基类的公有成员会成为派生类的公有成员.
基类的私有部分也会成为派生类的一部分,但是只能通过基类的公有和保护方法访问.

上面那段代码表示RatedPlayer对象将具有这些特征:

  • 派生类对象存储了基类的数据成员
  • 派生类对象可以使用基类的方法

所有说,RatedPlayer对象存储着选手姓名,是否由球桌,还可以调用Name()hasTable()ResetTable()方法
我们还需要在继承特性中添加什么?

  • 派生类需要自己的构造函数
  • 派生类可以根据需要添加额外的数据成员和成员函数

基类对象和派生类对象的关系图

  • 关于构造函数的访问权限考虑

派生类不能直接访问基类的私有成员,必须通过基类方法进行访问,例如,RatedPlayer的构造函数不能直接设置继承的成员(firstnamelastnamehasTable),而必须使用TableTennisPlayer的公有方法类访问私有的基类成员.总之,派生类的构造函数必须使用基类的构造函数.
创建派生类对象时,程序首先创造基类对象.

将参数传递给基类构造函数:

一个RatedPlayer的构造函数的例子:

RatedPlayer::RatedPlayer(unsigned int r,const string &fn,const string &ln,bool ht)
                :TableTennisPlayer(fn,ln,ht)
{
    rating=r;
}

上面这段代码中:TableTennisPlayer(fn,ln,ht)就是成员初始化列表.它会调用TableTennisPlayer的构造函数创建一个基类对象,然后进入RatedPlayer的构造函数,再把rating的值设置一下.完成派生类对象的创建.总之,派生类对象会嵌套一个基类对象.
如果省略:TableTennisPlayer(fn,ln,ht)这句话,那么编译器就会自动调用TableTennisPlayer的默认构造函数.即相当于:

RatedPlayer::RatedPlayer(unsigned int r,const string &fn,const string &ln,bool ht)
                :TableTennisPlayer()
{
    rating=r;
}

创建派生类对象时,程序首先调用基类构造函数,然后调用派生类的构造函数.基类构造函数初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员.派生类的构造函数总是调用一个基类的构造函数.可以用初始化列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数.

1.3 代码

//类继承1.h
#ifndef DERIVED1
#define DERIVED1
#include<string>
using std::string;
class TableTennisPlayer//基类
{
    private:
        string firstname;
        string lastname;
        bool hasTable;
    public:
        TableTennisPlayer(const string &fn="none",const string &ln="none",bool ht=false);
        void Name() const;
        bool HasTable() const{return hasTable;};
        void ResetTable(bool v){hasTable=v;};
};
class RatedPlayer:public TableTennisPlayer//一个简单的派生类
{
    private:
        unsigned int rating;
    public:
        RatedPlayer(unsigned int r=0,const string &fn="none",const string &ln="none",bool ht=false);
        RatedPlayer(unsigned int r,const TableTennisPlayer & tp);
        unsigned int Rating() const {return rating;};
        void ResetRating(unsigned int r){rating=r;};
};
#endif
//类继承1.cpp
#include"类继承1.h"
#include<iostream>
TableTennisPlayer::TableTennisPlayer(const string &fn,const string &ln,bool ht)
                    :firstname(fn),lastname(ln),hasTable(ht){}
void TableTennisPlayer::Name() const
{
    std::cout<<lastname<<", "<<firstname;
}
RatedPlayer::RatedPlayer(unsigned int r,const string &fn,const string &ln,bool ht)
                :TableTennisPlayer(fn,ln,ht)
{
    rating=r;
}
RatedPlayer::RatedPlayer(unsigned int r,const TableTennisPlayer & tp)
                :TableTennisPlayer(tp),rating(r){}
//类继承1main.cpp
#include<iostream>
#include"类继承1.h"
int main()
{
    using std::cin;
    using std::cout;
    using std::endl;
    TableTennisPlayer player1("Tara","Boomdea",false);
    RatedPlayer rplayer1(1140,"Mallory","Duck",true);
    rplayer1.Name();//派生类对象使用基类方法
    if(rplayer1.HasTable())
        cout<<": has a table.\n";
    else
        cout<<": hasn't a table.\n";
    player1.Name();//基类对象使用基类方法
    if(player1.HasTable())
        cout<<": has a table.\n";
    else
        cout<<": hasn't a table.\n";
    cout<<"Name: ";
    rplayer1.Name();
    cout<<"; Rating: "<<rplayer1.Rating()<<endl;//派生类对象使用派生方法
    RatedPlayer rplayer2 (1212,player1);//使用基类对象初始化派生类对象
    cout<<"Name: ";
    rplayer2.Name();
    cout<<"; Rating: "<<rplayer2.Rating()<<endl;
    return 0;
}
PS D:\study\c++\path_to_c++> g++ -I .\include\ -o 类继承1 .\类继承1.cpp .\类继承1main.cpp
PS D:\study\c++\path_to_c++> .\类继承1.exe
Duck, Mallory: has a table.
Boomdea, Tara: hasn't a table.
Name: Duck, Mallory; Rating: 1140
Name: Boomdea, Tara; Rating: 1212

1.4 派生类和基类的特殊关系

派生类对象可以使用基类的方法,当然了方法不是私有的
rplayer1.Name();//派生类对象使用基类方法
基类指针可以再不进行显式类型转换的情况下指向派生类对象;基类引用可以再不进行显式类型转换的情况下引用派生类对象.

RatedPlayer rplayer1(1140,"Mallory","Duck",true);
TableTennisPlayer &rt=rplayer1;//基类引用,引用派生类对象
TableTennisPlayer *pt=&rplater1;//基类指针,指向派生类对象
rt.Name();
pt->Name();

但是,基类引用或者指针只能用来调用基类的方法,所以rt.Rating()这样的调用派生类方法是不允许的.
C++要求引用和指针的类型与指向的类型匹配,但是这个规则对继承来说是个例外.
然而这种规则是单向的,不可以将基类对象和地址赋给派生类引用和指针.

TableTennisPlayer player1("Tara","Boomdea",false);
RatedPlayer & rr=player1;//不允许把基类对象赋给派生类引用
RatedPlayer * pr=&player1;//不允许把基类对象地址赋给派生类指针

实际上这种规则很有道理.因为派生类对象相当于是基类对象的扩充版,基类对象有的它都有,所以可以把派生类对象看成是一种基类对象来操作;但是基类对象相当于派生类对象的阉割版,它缺少了一些作为派生类对象的数据,所以我们不能把它看出派生类对象来操作.

例如,有一个函数:

void show(const TableTennisPlayer & rt)
{
    rt.Name();
    if(rt.HasTable())
        cout<<": has a table.\n";
    else
        cout<<": hasn't a table.\n";
}

这个函数的形参rt是一个基类引用,那么它可以指向基类或派生类对象,所以如下代码是合理的:

TableTennisPlayer player1("Tara","Boomdea",false);
RatedPlayer rplayer1(1140,"Mallory","Duck",true);
show(player1);
show(rplayer1);

同样的,形参是指向基类的指针的函数,也存在相似的关系.它可以使用基类对象的地址或者派生类对象的地址作为实参.
这种性质体现了,派生类和基类的兼容性.
甚至,我可以用派生类对象来初始化基类对象.

RatedPlayer olaf1(1840,"Olaf","Loaf",true);
TableTennisPlayer olaf2(olaf1);

这是因为,olaf2会调用隐式复制构造函数TableTennisPlayer(const TableTennisPlayer &)来创建olaf2.
同样的,我可以把派生类对象赋值给基类对象,因为调用了隐式重载赋值运算符.

2. is-a 关系

C++有3种继承方式:公有继承,保护继承和私有继承.但是公有继承是最常用的方式.
一般来说,类和类的关系有5种:is-a 关系, has-a 关系, is-like 关系, is-implemented-as-a 关系, uses-a 关系.
但是对于公有继承来说,我们最好用它创建 is-a 关系;因为你如果使用公有继承来创建其他关系,通常会出现问题.
可以这样说,公有继承就是用来建立 is-a 关系的.

  • is-a 关系

派生类对象也是一个基类对象,只要基类对象可以做到的,派生类对象也可以做到,而派生类对象能做到的,基类对象不一定能做到.这个很像是,哲学上讲的普遍性和特殊性的关系.
例如,我们有一个Fruit类,我们可以以此派生一个Banana类,我们知道香蕉是一种水果,水果有的共性香蕉都有,而香蕉有的特性,却不一定是是水果的共性.实际上,TableTennisPlayerRatedPlayer就是 is-a 关系.
is-a 关系就是 is-a-kind-of(是一种) 关系.即派生类对象是一种基类对象.

  • has-a 关系

公有继承不建立 has-a 关系.
例如,我们不能通过Fruit类派生出Lunch类.因为,午餐可以包含水果也可以不包含水果,午餐也不是 is-a-kind-of 水果.但是我们可以明确的一件事是:午餐可以有水果. 那么正确做法是,把Fruit作为Lunch的数据成员.

  • is-like 关系

公有继承不能建立 is-like 关系,也就是说,它不采用明喻.
例如,我们经常说律师像鲨鱼一样敏锐.但是我们不能从Shark类种派生出Lawyer类,因为鲨鱼可以在水下呼吸,律师却不能.继承可以在Shark类基础上添加属性,但是不能删除属性,就像你不能把水下呼吸属性删除一样.
我们可以这样实现 is-like 关系,先设计一个包含共有属性的类,以它为基础然后用 is-a 关系 或者 has-a 关系,建立一个新类.例如,设计出Sharp类表示敏锐的事物,然后利用 is-a 关系,派生出一个Lawyer类.

  • is-implemented-as-a 关系

公有继承不能建立 is-implemented-as-a 关系.也就是说它不建立 作为…来实现关系.
例如,可以用数组来实现栈,但是Array类无法派生出Stack类.因为数组索引不是栈的属性,而且栈不一定要用数组来实现,它还可以用链表实现.
我们可以这样实现这种关系,让栈包含一个私有的Array对象成员来隐藏数组的实现.

  • uses-a 关系

公有继承不能建立 uses-a 关系.也就是说它不建立 可以使用 关系
例如,电脑可以使用打印机.但是Computer类无法派生出Printer类.因为打印机根本就不是电脑.
我们可以这样建立 uses-a 关系,用友元函数或新建一个包含两者的类,来实现打印机和电脑之间的通讯.

3.多态公有继承

我们提供的第一个例子是很简单的,派生类对象使用基类的方法,而未作任何修改。然而,我们有时候希望用一个方法在派生类和基类中的行为是不同的。即方法的行为取决于调用该类方法的对象。这种行为称为多态。
有两种方法实现多态公有继承:

  • 在派生类中重新定义基类的方法
  • 使用虚方法

3.1 一个银行账户的例子

假如你现在是一个银行的程序员。你收到一个项目要求你开发两个类。一个类表示基本支票账户Brass,另一个类表示BrassPlus类,它增加了透支保护特性。也就是说,如果用户签出一张超出存款余额的支票,银行将支付这张支票,但是需要对超出余额的部分收取费用。

关于Brass账户的信息:

  • 客户姓名
  • 账号
  • 余额

可以执行的操作:

  • 创建账户
  • 存款
  • 取款
  • 显示账户信息

关于BrassPlus账户的信息包含一切Brass账户的信息,并且新增如下信息:

  • 透支上限
  • 透支贷款利率
  • 当前透支总额

不需要新增操作,但是有两种操作的实现不同:

  • 对于取款操作,必须考虑透支保护
  • 显示操作必须显示BrassPlus的其他信息

关于透支系统的细节:

  • BrassPlus账户限制了客户的透支上限,默认上限是500元
  • 银行可以修改客户透支上限
  • BrassPlus账户对贷款收取利息.默认贷款利率是11.125%
  • 银行可以修改客户的贷款利率
  • 客户欠银行的钱(透支金额加上利息),必须通过现金的方式交给特定的银行员工

一个关键的问题是:能使用公有继承完成类的设计吗?
BrassPlus类和Brass满足 is-a 关系。因为只要是Brass有的,BrassPlus都有。

3.2 代码实现

//类继承2.h
#ifndef BRASS_H
#define BRASS_H
#include<string>
using std::string;
class Brass
{
    private:
        string fullName;//客户姓名
        long acctNum;//账号
        double balance;//余额
    public:
        Brass(const string &s="Nullbody",long an=-1, double bal=0.0);//构造函数,创建账户
        void Deposit(double amt);//存款
        virtual void Withdraw(double amt);//取款
        double Balance() const{return balance;};
        virtual void ViewAcct() const;//显示账户信息
        virtual ~Brass(){};
};
class BrassPlus : public Brass
{
    private:
        double maxLoan;//透支上限
        double rate;//贷款利率
        double owesBank;//透支总额
    public:
        BrassPlus(const string &s="Nullbody",long an=-1, double bal=0.0,
                    double ml=500,double r=0.11125);
        BrassPlus(const Brass & ba,double ml=500,double r=0.11125);
        virtual void Withdraw(double amt);//取款
        virtual void ViewAcct() const;//显示账户信息
        void ResetMax(double m){maxLoan=m;}//设置透支上限
        void ResetRate(double r){rate=r;}//设置贷款利率
        void ResetOwes(){owesBank=0;}//设置透支总额
};

#endif

对于上面这段声明,有几点需要说明一下

  • BrassPlus类在Brass类的基础上添加了3个数据成员和3个公有成员函数

    这一点没什么新鲜的

  • Brass类和BrassPlus类都声明了Withdraw()ViewAcct()方法,但是不同类的方法行为不同

    我们声明了两个方法,基类的限定名是Brass::ViewAcct(),派生类版本的限定名是BrassPlus::ViewAcct()。程序会根据调用对象的类型来确定使用哪个版本。
    同样的Withdraw()也有两个版本,而对于在两个类中行为相同的方法(如Deposit()Balance()),则只需在基类中声明。

  • Brass类使用了关键词virtual.使用了virtual的方法叫做虚方法.

    使用关键词virtual比较复杂。如果方法是通过引用或者指针而不是对象调用的,它将确定使用哪一种方法。如果没有virtual,程序将根据引用类型或者指针类型选择方法;如果使用了virtual,程序将根据引用或者指针指向的对象的类型来选择方法。
    如果ViewAcct()不是虚的,则

    Brass dom;
    BrassPlus dot;
    Brass & b1_ref=dom;
    Brass & b2_ref=dot;
    b1_ref.ViewAcct();//使用Brass::ViewAcct()
    b2_ref.ViewAcct();//使用Brass::ViewAcct()
    

    由于引用变量的类型是Brass,所有选择了Brass::ViewAcct(),如果使用指针代替引用,行为会与之类似。
    如果ViewAcct()是虚的,则

    Brass dom;
    BrassPlus dot;
    Brass & b1_ref=dom;
    Brass & b2_ref=dot;
    b1_ref.ViewAcct();//使用Brass::ViewAcct()
    b2_ref.ViewAcct();//使用BrassPlus::ViewAcct()
    

    这两个引用的类型都是Brass,但是b2_ref引用是一个BrassPlus对象,所以使用的是BrassPlus::ViewAcct()。如果使用指针代替引用,行为会与之类似。
    虚函数这种行为非常方便。因此,经常在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明成虚的后,它在派生类中将自动成为虚方法。但是为了强调,我们在派生类的声明中也可使用关键词virtual指出哪些函数是虚方法。

  • Brass类声明了一个虚析构函数,虽然该析构函数不执行任何操作。

    基类声明了一个虚析构函数。这样做主要是为了确保释放对象时,按照正确的顺序调用析构函数。
    如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的。
    为基类声明一个虚析构函数也是一个惯例。

下面是关于类的实现:

//类继承2.cpp
#include<iostream>
#include"类继承2.h"
using std::cout;
using std::endl;

typedef std::ios_base::fmtflags format;
typedef std::streamsize precis;
format setFormat();
void restore(format f,precis p);

Brass::Brass(const string &s,long an, double bal)//构造函数,创建账户
            :fullName(s),acctNum(an),balance(bal){}
void Brass::Deposit(double amt)//存款
{
    if(amt<0)
        cout<<"negative deposit not allowed;"<<"deposit is cancelled.\n";
    else
        balance+=amt;
}
void Brass::Withdraw(double amt)//取款
{
    format initialState=setFormat();
    precis prec=cout.precision(2);

    if(amt<0)
        cout<<"withdrawal amount must be positive;"<<"withdrawal is cancelled.\n";
    else if(amt<=balance)
        balance-=amt;
    else
        cout<<"withdrawal amount of $"<<amt<<" exceeds your balance;"
            <<"withdrawal is cancelled.\n";
    restore(initialState,prec);
}
void Brass::ViewAcct() const//显示账户信息
{
    format initialState=setFormat();
    precis prec=cout.precision(2);

    cout<<"Client: "<<fullName<<endl;
    cout<<"Account Number: "<<acctNum<<endl;
    cout<<"Balance: $"<<balance<<endl;

    restore(initialState,prec);
}

BrassPlus::BrassPlus(const string &s,long an, double bal,
                    double ml,double r)
            :Brass(s,an,bal),maxLoan(ml),rate(r),owesBank(0.0){}

BrassPlus::BrassPlus(const Brass & ba,double ml,double r)
            :Brass(ba),maxLoan(ml),rate(r),owesBank(0.0){}

void BrassPlus::ViewAcct() const//显示账户信息
{
    format initialState=setFormat();
    precis prec=cout.precision(2);

    Brass::ViewAcct();
    cout<<"Maximum loan: $"<<maxLoan<<endl;
    cout<<"Owed to bank: $"<<owesBank<<endl;
    cout.precision(3);//###.###
    cout<<"Loan Rate: "<<100*rate<<"%\n";
    
    restore(initialState,prec);
}

void BrassPlus::Withdraw(double amt)//取款
{
    format initialState=setFormat();
    precis prec=cout.precision(2);

    double bal=Balance();
    if(amt<=bal)
        Brass::Withdraw(amt);
    else if(amt<=bal+maxLoan-owesBank)
    {//处理透支
        double advance=amt-bal;//银行预支金额
        owesBank+=advance*(1.0+rate);//透支总额
        cout<<"Bank advance: $"<<advance<<endl;
        cout<<"Finance charge: $"<<advance*rate<<endl;//手续费(利息)
        Deposit(advance);
        Brass::Withdraw(amt);
    }
    else
        cout<<"Credit limit exceeded.Transaction cancelled.\n";
    restore(initialState,prec);
}


format setFormat()
{
    //设置输出格式###.##
    return cout.setf(std::ios_base::fixed,
                    std::ios_base::floatfield);
}
void restore(format f,precis p)
{
    cout.setf(f,std::ios_base::floatfield);
    cout.precision(p);
}
  • 关于对浮点数的输出格式问题

    一般我们不使用cout直接输出浮点数类型的数字,因为它格式不统一,包括是否显示小数点,保留几位小数等都不是固定的。

    typedef std::ios_base::fmtflags format;
    typedef std::streamsize precis;
    format setFormat()
    {
        //设置输出格式###.##
        return cout.setf(std::ios_base::fixed,
                        std::ios_base::floatfield);
    }
    void restore(format f,precis p)
    {
        cout.setf(f,std::ios_base::floatfield);
        cout.precision(p);
    }
    
    

    关于setFormat()做的事情是,他把浮点数输出格式变成定点,然后返回值是ios_base::fmtflags类型的,其实就是这个函数返回一个状态即cout的输出格式。
    然后cout.precision(2);就是来设置精度的,如果采用定点格式输出浮点数,那么该句话的意思就是保留2位小数。
    restore()做的事情就是设置格式和精度。

    format initialState=setFormat();
    precis prec=cout.precision(2);
            ...
    restore(initialState,prec);
    

    所以上面这段代码做的事情就是:把cout对于浮点数的输出格式改成定点,用initialState存储这个格式,然后设置输出精度为保留两位小数,用prec存储这个精度。最后,把cout的格式和精度都设置成最初的格式。

  • 构造函数

    这里的构造函数都是使用成员初始化列表语法,将基类信息传递给基类构造函数,然后使用构造函数体初始化BrassPlus的新增的数据项。

  • BrassPlus::ViewAcct()BrassPlus::Withdraw()的实现

    void BrassPlus::ViewAcct() const//显示账户信息
    {
        format initialState=setFormat();
        precis prec=cout.precision(2);
    
        Brass::ViewAcct();
        cout<<"Maximum loan: $"<<maxLoan<<endl;
        cout<<"Owed to bank: $"<<owesBank<<endl;
        cout.precision(3);//###.###
        cout<<"Loan Rate: "<<100*rate<<"%\n";
        
        restore(initialState,prec);
    }
    

    上面这段代码是实现BrassPlus::ViewAcct()的。由于派生类无法访问基类的私有数据,那么只会通过基类的公有方法来访问了,这里通过限定名方法调用了基类的显示账户信息的方法Brass::ViewAcct();,如果不使用作用域解析符,那么编译器会把它当成BrassPlus::ViewAcct()

    void BrassPlus::Withdraw(double amt)//取款
    {
        format initialState=setFormat();
        precis prec=cout.precision(2);
    
        double bal=Balance();
        if(amt<=bal)
            Brass::Withdraw(amt);
        else if(amt<=bal+maxLoan-owesBank)
        {//处理透支
            double advance=amt-bal;//银行预支金额
            owesBank+=advance*(1.0+rate);//透支总额
            cout<<"Bank advance: $"<<advance<<endl;
            cout<<"Finance charge: $"<<advance*rate<<endl;//手续费(利息)
            Deposit(advance);
            Brass::Withdraw(amt);
        }
        else
            cout<<"Credit limit exceeded.Transaction cancelled.\n";
        restore(initialState,prec);
    }
    

    上面这段代码是BrassPlus::Withdraw()的实现。由于派生类无法访问基类的私有数据,但是取款操作肯定需要知道余额,所以我们在基类中添加了一个Balance()公有方法,这一接口的增加是非常重要的。而修改余额的方法也必须使用公有类方法Deposit()Brass::Withdraw()
    总之,在设计派生类的时候如果发现缺少一些必要的数据项,可以在基类中增加接口

3.3 使用Brass类和BrassPlus

//类继承2main.cpp
#include<iostream>
#include"类继承2.h"

int main()
{
    using std::cout;
    using std::endl;
    Brass Piggy("Porcelot Pigg",381299,4000.00);
    BrassPlus Hoggy("Horatio Hogg",382288,3000.00);
    Piggy.ViewAcct();
    cout<<endl;
    Hoggy.ViewAcct();
    cout<<endl;
    cout<<"Depositing $1000 into the Hogg Account:\n";
    Hoggy.Deposit(1000.00);
    cout<<"New balance: $"<<Hoggy.Balance()<<endl;
    cout<<"Withdrawing $4200 from the Pigg Acount:\n";
    Piggy.Withdraw(4200.00);
    cout<<"Piggy Acount balance: $"<<Piggy.Balance()<<endl;
    cout<<"Withdrawing $4200 from the Hogg Acount:\n";
    Hoggy.Withdraw(4200.00);
    Hoggy.ViewAcct();
}
PS D:\study\c++\path_to_c++> g++ -I .\include\ -o 类继承2 .\类继承2.cpp .\类继承2main.cpp
PS D:\study\c++\path_to_c++> .\类继承2.exe
Client: Porcelot Pigg
Account Number: 381299
Balance: $4000.00

Client: Horatio Hogg
Account Number: 382288
Balance: $3000.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan Rate: 11.125%

Depositing $1000 into the Hogg Account:
New balance: $4000
Withdrawing $4200 from the Pigg Acount:
withdrawal amount of $4200.00 exceeds your balance;withdrawal is cancelled.
Piggy Acount balance: $4000
Withdrawing $4200 from the Hogg Acount:
Bank advance: $200.00
Finance charge: $22.25
Client: Horatio Hogg
Account Number: 382288
Balance: $0.00
Maximum loan: $500.00
Owed to bank: $222.25
Loan Rate: 11.125%

上面这段代码只是稍微演示一下,但是没用使用虚方法的特性。看看下面这段代码:

//类继承3.cpp
#include<iostream>
#include<string>
#include"类继承2.h"
const int CLIENTS=4;

int main()
{
    using std::cin;
    using std::cout;
    using std::endl;
    using std::string;
    Brass *p_clients[CLIENTS];
    string temp;
    long tempnum;
    double tempbal;
    char kind;

    for(int i=0;i<CLIENTS;i++)
    {
        cout<<"Enter client's name: ";
        std::getline(cin,temp);
        cout<<"Enter client's account number: ";
        cin>>tempnum;
        cout<<"Enter opening balance: $";
        cin>>tempbal;
        cout<<"Enter 1 for Brass Account or 2 for BrassPlus Account: ";
        while (cin>>kind &&(kind !='1' && kind !='2'))
        {
            cout<<"Enter either 1 or 2 :";
        }
        if(kind=='1')
            p_clients[i]=new Brass(temp,tempnum,tempbal);
        else
        {
            double tmax,trate;
            cout<<"Enter the overdraft limit: $";
            cin>>tmax;
            cout<<"Enter the interest rate as a decimal fraction: ";
            cin>>trate;
            p_clients[i]=new BrassPlus(temp,tempnum,tempbal,tmax,trate);
        }
        cin.ignore();
    }
    cout<<endl;
    for(int i=0;i<CLIENTS;i++)
    {
        p_clients[i]->ViewAcct();
        cout<<endl;
    }
    for(int i=0;i<CLIENTS;i++)
    {
        delete p_clients[i];
    }
    cout<<"done!\n";
    
}

PS D:\study\c++\path_to_c++> g++ -I .\include\ -o 类继承3 .\类继承3.cpp .\类继承2.cpp
PS D:\study\c++\path_to_c++> .\类继承3.exe
Enter client's name: Harry Fishsong
Enter client's account number: 112233
Enter opening balance: $1500
Enter 1 for Brass Account or 2 for BrassPlus Account: 1
Enter client's name: Dinah Otternoe
Enter client's account number: 121213
Enter opening balance: $1800
Enter 1 for Brass Account or 2 for BrassPlus Account: 2
Enter the overdraft limit: $350
Enter the interest rate as a decimal fraction: 0.12
Enter client's name: Brenda Birdherd
Enter client's account number: 212118
Enter opening balance: $5200
Enter 1 for Brass Account or 2 for BrassPlus Account: 2
Enter the overdraft limit: $800
Enter the interest rate as a decimal fraction: 0.10
Enter client's name: Tim Turtletop
Enter client's account number: 233255
Enter opening balance: $688
Enter 1 for Brass Account or 2 for BrassPlus Account: 1

Client: Harry Fishsong
Account Number: 112233
Balance: $1500.00

Client: Dinah Otternoe
Account Number: 121213
Balance: $1800.00
Maximum loan: $350.00
Owed to bank: $0.00
Loan Rate: 12.000%

Client: Brenda Birdherd
Account Number: 212118
Balance: $5200.00
Maximum loan: $800.00
Owed to bank: $0.00
Loan Rate: 10.000%

Client: Tim Turtletop
Account Number: 233255
Balance: $688.00

done!

上面这段代码中Brass *p_clients[CLIENTS];指出数组的每个元素都是一个指向Brass对象的指针。如果不使用虚方法,p_clients[i]->ViewAcct();将都调用用Brass::ViewAcct()

  • 关于为什么需要虚析构函数

    如果不使用虚析构函数,那么将只会调用对应于指针类型的析构函数。对于上面这段代码,那么只有Brass的析构函数被调用,即使指针指向的是BrassPlus对象。虽然对于上面代码代码说,由于析构函数什么都不做,所以说虚析构方法可以有可无。但是,我们下次再设计派生类的时候,如果BrassPlus的析构函数需要做一下操作时,那么我们必须在基类中使用虚析构函数,来确保派生类对象正确被析构。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值