天天看點

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.傳回對象和傳回引用

一些類方法傳回對象,而另一些則傳回引用。如果可以不傳回對象,則應傳回引用。

首先,傳回對象涉及到生成傳回對象的臨時拷貝,耗時,占資源。傳回引用則可以節省時間和記憶體。

不過,函數不能傳回在函數中建立的臨時對象的引用,因為當函數結束時,臨時對象将消失,是以這種 引用是非法的。在這種情況下應傳回在函數中臨時建立的對象,以生成一個調用程式可以使用的拷貝。

通用的規則:如果函數傳回在函數中建立的臨時拷貝對象,則不要使用引用。如果傳回的是通過引用或指針傳遞給它的對象,則應按引用傳回對象。

繼續閱讀