天天看点

c++学习之类继承

注:本文章仅供个人复习使用。

本章内容包括:

  • is-a关系的继承
  • 如何以公有的方式从一个类派生出另一个类
  • 保护访问
  • 构造函数成员和初始化列表
  • 向上和向下强制转换
  • 虚拟成员函数
  • 静态联编与动态联编
  • 抽象基类
  • 纯虚函数
  • 何时以及如何使用公有继承

首先看看如何以公有的方式从一个类派生出另一个类

下面定义了TableTennisPlayer类,并派生出了一个RatedPlayer类

#ifndef TABTENN0_H_
#define TABTENN0_H_

class TableTennisPlayer
{
private:
    enum {LIM = };
    char firstname [LIM];
    char lastname [LIM];
    bool hasTable;
public:
    TableTennisPlayer(const char * fn = "none",const char * 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 = ,const char * fn = "none",
        const char * ln = "none",bool ht = false);
    RatedPlayer(unsigned int r,const TableTennisPlayer & tp);
    unsigned int Rating(){return rating;}
    void ResetRating(unsigned int r){rating = r;}
};
#endif
           

类RatedPlayer公有继承了TableTennisPlayer类。使用公有继承,基类的公有成员将称为派生类的公有成员,基类的私有部分也将称为派生类的一部分,但只能通过基类的公有和保护方法访问。

上述代码完成了哪些工作呢?RatedPlayer对象将具有一下特性:

  • 派生类对象存储了基类的数据成员(派生类继承了基类的实现);
  • 派生类对象可以使用基类的方法(派生类继承了基类的借口)。

需要在继承特性中添加什么呢?

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

派生类的构造函数必须给新成员和继承的成员提供数据,但是派生类不能直接访问基类的私有成员,而必须通过基类的方法进行访问。也就是说,派生类构造函数必须使用基类的构造函数。

创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入到派生类构造函数之前被创建。C++使用成员初始化列表句法来完成这种工作。例如:

RatedPlayer::RatedPlayer(unsigned int r,const char * fn,const char * ln,
                         bool ht):TableTennisPlayer(fn,ln,ht)   //用成员初始化列表句法来给基类的成员赋值
{
    rating = r;
}
           
RatedPlayer::RatedPlayer(unsigned int r,const TableTennisPlayer & tp)
    :TableTennisPlayer(tp)//调用基类的默认复制构造函数给基类成员赋值
    {
        rating = r;
     }
           

有关派生类构造函数的要点如下:

  • 基类对象首先被创建。
  • 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数。
  • 派生类构造函数应初始化派生类新增的数据成员。
  • 释放对象的顺序与创建对象的顺序相反,即首先执行派生类析构函数,然后自动调用基类析构函数。

派生类和基类之间的特殊关系

  • 派生类对象可以使用基类方法,条件是方法不是私有的;
  • 可以将派生类的对象和地址赋给基类引用和指针,但不能反过来;
  • 不过,基类指针或引用只能用于调用基类方法;
RatedPlayer rplayer1(,"Mallory","Duck",true);
TableTennisPlayer & rt = rplayer;  //可以
TableTennisPlayer * pt = &rplayer;  //可以
rt.Name();  //只能调用基类方法
pt->Name();  //只能调用基类方法
           

多态公有继承

我们可能会希望同一个方法在派生类和基类的行为是不同的。换句话说,方法的行为应取决于调用方法的对象。这种较复杂的行为称为多态,就是指同一个方法的行为将随上下文而异。

有两种重要的机制可用于实现多态公有继承:

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

下面两个类说明了多态性

#ifndef BRASS_H_
#define BRASS_H_
class Brass
{
private:
    enum {MAX = };
    char fullName[MAX];//客户姓名
    long acctNum;//帐号
    double balance;//当前结余
public:
    Brass(const char * s = "Nullbody",long an = -,double bal = );
    void Deposit(double amt);//存款
    virtual void Withdraw(double amt);//取款
    double Balance()const;//当前余额
    virtual void ViewAcct()const;//显示账户信息
    virtual ~Brass(){ }
};

class Brassplus:public Brass
{
private:
    double maxLoan;//透支上限
    double rate;//透支贷款利率
    double owesBank;//当前透支总额
public:
    Brassplus(const char * s = "Nullbody",long an = -,
        double bal = ,double ml = ,
        double r = );
    Brassplus(const Brass & ba,double ml = ,double r = );
    virtual void ViewAcct()const;
    virtual void Withdraw(double amt);
    void ResetMax(double m){maxLoan = m;}
    void ResetRate(double r){rate = r;}
    void ResetOwes(){owesBank = ;}
};

#endif
           

对于上面的代码,有几点需要说明:

  • BrassPlus类在Brass类的基础上添加了3个私有数据成员和3个公有成员函数
  • Brass类和BrassPlus类都说明了ViewAcct( )和Withdraw( )方法,但BrassPlus对象和Brass对象的这些方法的行为是不同的
  • Brass类在声明ViewAcct( )和Withdraw( )时使用了关键字virtual。这些方法被称为虚方法
  • Brass类还声明了一个虚拟析构函数,虽然该析构函数不执行任何操作

第一点没什么好说的。

第二点介绍了声明如何指出方法在派生类的行为的不同。程序将使用对象类型来确定使用哪个版本

Brass dom("Dominic Banker",,);
BrassPlus dot("Dorothy Banker",,);
dom.ViewAcct();//使用Brass::ViewAcct();
dot.ViewAcct();//使用BrassPlus::ViewAcct();
           

第三点(使用virtual)比前面亮点复杂。如果方法是通过引用或者指针调用的,他将确定使用哪一种方法。如果没有关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual程序将根据引用或者指针指向的对象的类型来选择方法。

//如果ViewAcct()不是虚拟的,则程序行为如下
Brass dom("Dominic Banker",,);
BrassPlus dot("Dorothy Banker",,);
Brass & b1 = dom;
Brass & b2 = dot;
b1.ViewAcct();//使用Brass::ViewAcct()
b2.ViewAcct();//使用Brass::ViewAcct()

//如果ViewAcct()是虚拟的,则程序行为如下
Brass dom("Dominic Banker",,);
BrassPlus dot("Dorothy Banker",,);
Brass & b1 = dom;
Brass & b2 = dot;
b1.ViewAcct();//使用Brass::ViewAcct()
b2.ViewAcct();//使用BrassPlus::ViewAcct()
           

第四点,基类声明了应该虚拟析构函数。这样做是为了确保释放派生类对象时,按正确的顺序调用析构函数。

记住:如果要在派生类中重定义基类的方法,通常应将基类方法声明为虚拟的。这样,程序将根据对象类型而不是引用或指针的类型来选择方法版本。为基类声明一个虚拟析构函数也是一种惯例。

类实现:

#include <iostream>
#include <cstring>
#include "brass.h"
using std::cout;
using std::ios_base;
using std::endl;

Brass::Brass(const char * s,long an,double bal)
{
    std::strncpy(fullName,s,MAX-);
    fullName[MAX-] = '\0';
    acctNum = an;
    balance = bal;
}

void Brass::Deposit(double amt)
{
    if(amt < )
        cout<<"Negative deposit not allowed; "
        <<"deposit is cancled.\n";
    else
        balance += amt;
}

void Brass::Withdraw(double amt)
{
    if(amt < )
        cout<<"Withdrawal amount must be positive; "
        <<"Withdrawal canceled.\n";
    else if(amt <= balance)
        balance -= amt;
    else
        cout<<"Withdrawal amount of $"<<amt
        <<" exceeds your balance.\n"
        <<"Withdrawal canceled.\n";
}

double Brass::Balance()const
{
    return balance;
}
void Brass::ViewAcct()const
{
    ios_base::fmtflags initialState = 
        cout.setf(ios_base::fixed,ios_base::floatfield);
    cout.setf(ios_base::showpoint);
    cout.precision();//格式化命令将浮点数的值输出模式设置为定点,即包含两位小数

    cout<<"Client: "<<fullName<<endl;
    cout<<"Account Number: "<<acctNum<<endl;
    cout<<"Balance: $"<<balance<<endl;
    cout.setf(initialState);
}

Brassplus::Brassplus(const char * s,long an,double bal,
                     double ml,double r):Brass(s,an,bal)
{
    maxLoan = ml;
    owesBank = ;
    rate = r;
}

Brassplus::Brassplus(const Brass & ba,double ml,double r):Brass(ba)
{
    maxLoan = ml;
    owesBank = ;
    rate = r;
}

void Brassplus::ViewAcct()const
{
    ios_base::fmtflags initialState =
        cout.setf(ios_base::fixed,ios_base::floatfield);
    cout.setf(ios_base::showpoint);
    cout.precision();//格式化命令将浮点数的值输出模式设置为定点,即包含两位小数

    Brass::ViewAcct();
    cout<<"Maximun loan: $"<<maxLoan<<endl;
    cout<<"Owed to bank: $"<<owesBank<<endl;
    cout<<"Loan Rate: "<< * rate<<"%\n";
    cout.setf(initialState);
}

void Brassplus::Withdraw(double amt)
{
    ios_base::fmtflags initialState =
        cout.setf(ios_base::fixed,ios_base::floatfield);
    cout.setf(ios_base::showpoint);
    cout.precision();

    double bal = Balance();
    if(amt <= bal)
        Brass::Withdraw(amt);
    else if(amt <= bal + maxLoan - owesBank)
    {
        double advance = amt - bal;
        owesBank += advance *( + rate);
        cout<<"Bank advance: $"<<advance<<endl;
        cout<<"Finance charge: $"<<advance * rate<<endl;
        Deposit(advance);
        Brass::Withdraw(amt);
    }
    else
        cout<<"Credit limit exceeded. Transation cancelled.\n";
    cout.setf(initialState);
}
           

测试代码:

#include <iostream>
#include "brass.h"

const int CLIENTS = ;
const int LEN = ;
int main()
{
    using std::cin;
    using std::cout;
    using std::endl;
    Brass * p_clients[CLIENTS];

    int i;
    for(i = ;i < CLIENTS;i++)
    {
        char temp[LEN];
        long tempnum;
        double tempbal;
        char kind;
        cout<<"Enter client's name: ";
        cin.getline(temp,LEN);
        cout<<"Enter client's account number: ";
        cin>>tempnum;
        cout<<"Enter openig 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);//将派生类的地址对象赋给基类指针
        }
        while(cin.get() != '\n')
            continue;
    }
    cout<<endl;
    for(i = ;i < CLIENTS;i++)
    {
        p_clients[i]->ViewAcct();
        cout<<endl;
    }
    for(i = ;i < CLIENTS;i++)
        delete p_clients[i];
    cout<<"Done.\n";
    return ;
}
           

为何需要虚拟析构函数?

在上面的测试代码中,使用delete释放由new分配的对象的代码说明了为何基类应包含一个虚拟析构函数,虽然有时好像并不需要析构函数。如果析构函数不是虚拟的,则将只调用对于应用指针类型的析构函数。对于上面的测试代码,这意味着只有Brass的析构函数被调用,即使指针是指向一个BrassPlus对象。如果析构函数是虚拟的,将调用相应对象类型的析构函数。因此,如果指针指向的是BrassPlus对象,将调用BrassPlus的析构函数,然后自动调用基类的析构函数。因此,使用虚拟析构函数可以确保正确的析构函数序列被调用。对于上面的代码这种正确的行为并不很重要,因为析构函数没有执行任何操作。但是,如果BrassPlus包含一个执行某些操作的析构函数,则Brass必须有一个虚拟析构函数,即使该析构函数不执行任何操作。

静态联编和动态联编

将源代码中的函数调用解释为执行特定的函数带模块被成为函数的联编(binding)。在编译过程中进行联编被称为静态联编。编译器必须生成能够在程序运行时选择正确的虚方法代码,这被称为动态联编。

编译器对非虚方法使用静态联编,对虚方法使用动态联编。

在大多数情况下,动态联编很好,因为它能够让程序选择特定类型设计的方法。但是静态联编的效率更高,而且它不需要重新定义该函数。

提示:如果要在派生类中重新定义基类的方法,则将它设置为虚方法,否则,设置为非虚方法。

使用虚函数时,在内存和执行速度方面有一定的成本

  • 每个对象都将增大,增大量为存储地址的空间
  • 对每个类,编译器都创建一个虚函数地址表(数组)
  • 每个函数调用都需要执行一步额外的操作,即到表中查找地址

抽象基类(abstract base class,ABC)

抽象基类就是从多个相似的类中抽象出它们的共性,将这些共性放到抽象基类中去。

抽象基类必须至少包含一个纯虚函数,纯虚函数声明的结尾处为 = 0 ,在该基类中不能定义该函数。

看下面的实例代码

acctabc.h

#ifndef ACCTABC_H_
#define ACCTABC_H_
#include <iostream>
class AcctABC   //抽象基类
{
private:
    enum {MAX = };
    char fullName[MAX];
    long acctNum;
    double balance;
protected:
    const char * FullName()const {return fullName;}
    long AcctNum()const {return acctNum;}
    std::ios_base::fmtflags SetFormat()const;
public:
    AcctABC(const char * s = "Nullbody",long an = -,double bal = );
    void Deposit(double amt);
    virtual void Withdraw(double amt) = ;//纯虚函数
    double Balance() const{return balance;}
    virtual void ViewAcct()const  = ;//纯虚函数
    virtual ~AcctABC(){ }
};

class Brass : public AcctABC
{
public:
    Brass(const char * s = "Nullbody",long an = -,
        double bal = ):AcctABC(s,an,bal){ }
    virtual void Withdraw(double amt);
    virtual void ViewAcct()const;
    virtual ~Brass(){ }
};

class Brassplus : public AcctABC
{
private:
    double maxLoan;
    double rate;
    double owesBank;
public:
    Brassplus(const char * s = "Nullbody",long an = -,
        double bal = ,double ml = ,
        double r = );
    Brassplus(const Brass & ba,double ml = ,double r = );
    virtual void ViewAcct()const;
    virtual void Withdraw(double amt);
    void ResetMax(double m){maxLoan = m;}
    void ResetRate(double r){rate = r;}
    void ResetOwes(){owesBank = ;}
};
#endif
           

acctabc.cpp

#include <iostream>
#include <cstring>
#include "acctabc.h"
using std::cout;
using std::ios_base;
using std::endl;

AcctABC::AcctABC(const char * s,long an,double bal)
{
    std::strncpy(fullName,s,MAX -);
    fullName[MAX-] = '\0';
    acctNum = an;
    balance = bal;
}

void AcctABC::Deposit(double amt)//存钱
{
    if(amt < )
        cout<<"Negative deposit not allowed;deposit is cancelled.\n";
    else
        balance += amt;
}

void AcctABC::Withdraw(double amt)//取钱
{
    balance += amt;
}

//保护成员函数
ios_base::fmtflags AcctABC::SetFormat()const //设置格式
{
    ios_base::fmtflags initialState = 
        cout.setf(ios_base::fixed,ios_base::floatfield);
    cout.setf(ios_base::showpoint);
    cout.precision();
    return initialState;
}

void Brass::Withdraw (double amt)
{
    if(amt < )
        cout<<"Withdraw amount must be positive;withdraw canceled.\n";
    else if(amt <= Balance())
        AcctABC::Withdraw(amt);
    else
        cout<<"Withdraw amount of $"<<amt
        <<" wxceeds your balance.\n"
        <<"Withdraw canceled.\n";
}
void Brass::ViewAcct()const
{
    ios_base::fmtflags initialState = SetFormat();
    cout<<"Brass Client: "<<FullName()<<endl;
    cout<<"Account Number: "<<AcctNum()<<endl;
    cout<<"Balance: $"<<Balance()<<endl;
}

Brassplus::Brassplus(const char * s,long an,double bal,
                     double ml,double r):AcctABC(s,an,bal)
{
    maxLoan = ml;
    owesBank = ;
    rate = r;
}

Brassplus::Brassplus(const Brass & ba,double ml,double r)
    :AcctABC(ba)
{
    maxLoan = ml;
    owesBank = ;
    rate = r;
}

void Brassplus::ViewAcct()const
{
    ios_base::fmtflags intialState = SetFormat();

    cout<<"Brass Client: "<<FullName()<<endl;
    cout<<"Account Number: "<<AcctNum()<<endl;
    cout<<"Balance: $"<<Balance()<<endl;
    cout<<"Maximun loan: $"<<maxLoan<<endl;
    cout<<"Owed to bank : $"<<owesBank<<endl;
    cout<<"Loan Rate: "<<*rate<<"%\n";
}

void Brassplus::Withdraw(double amt)
{
    ios_base::fmtflags initialState = SetFormat();

    double bal = Balance();
    if(amt <= bal)
        AcctABC::Withdraw(amt);
    else if(amt <= bal + maxLoan - owesBank)
    {
        double advance = amt - bal;
        owesBank += advance * ( + rate);
        cout<<"Bank advance: $"<<advance<<endl;
        cout<<"Finance charge: $"<<advance * rate<<endl;
        Deposit(advance);
        AcctABC::Withdraw(amt);
    }
    else
        cout<<"Credit limit exceeded.Transaction cancelled.\n";
    cout.setf(initialState);
}
           

继承和动态内存分配

第一种情况:派生类不使用new

在这种情况下,不需要为类定义显示析构函数,复制构造函数和赋值操作符。

第二种情况:派生类使用new

在这种情况下,必须为派生类定义显示析构函数,复制构造函数和赋值操作符。

类设计回顾

1.按值传递对象与传递引用

通常,编写使用对象作为参数的函数时,应按引用而不是按值来传递对象。这样做的原因之一是为了提高效率。按值传递对象涉及到生成临时拷贝,即调用复制构造函数,然后调用析构函数。调用这些函数需要时间,复制大型对象比传递引用话费的时间要多得多。如果不修改对象,应将参数声明为const引用。

按引用传递对象的另外一个原因是,在继承使用虚函数时,被定义为接收基类引用参数的函数可以接受派生类。

2.返回对象和返回引用

一些类方法返回对象,而另一些则返回引用。如果可以不返回对象,则应返回引用。

首先,返回对象涉及到生成返回对象的临时拷贝,耗时,占资源。返回引用则可以节省时间和内存。

不过,函数不能返回在函数中创建的临时对象的引用,因为当函数结束时,临时对象将消失,因此这种 引用是非法的。在这种情况下应返回在函数中临时创建的对象,以生成一个调用程序可以使用的拷贝。

通用的规则:如果函数返回在函数中创建的临时拷贝对象,则不要使用引用。如果返回的是通过引用或指针传递给它的对象,则应按引用返回对象。

继续阅读