c++學習之類繼承

注:本文章僅供個人複習使用。
本章內容包括:

  • is-a關係的繼承
  • 如何以公有的方式從一個類派生出另一個類
  • 保護訪問
  • 構造函數成員和初始化列表
  • 向上和向下強制轉換
  • 虛擬成員函數
  • 靜態聯編與動態聯編
  • 抽象基類
  • 純虛函數
  • 何時以及如何使用公有繼承

首先看看如何以公有的方式從一個類派生出另一個類
下面定義了TableTennisPlayer類,並派生出了一個RatedPlayer類

#ifndef TABTENN0_H_
#define TABTENN0_H_

class TableTennisPlayer
{
private:
    enum {LIM = 20};
    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 = 0,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(1140,"Mallory","Duck",true);
TableTennisPlayer & rt = rplayer;  //可以
TableTennisPlayer * pt = &rplayer;  //可以
rt.Name();  //只能調用基類方法
pt->Name();  //只能調用基類方法

多態公有繼承
我們可能會希望同一個方法在派生類和基類的行爲是不同的。換句話說,方法的行爲應取決於調用方法的對象。這種較複雜的行爲稱爲多態,就是指同一個方法的行爲將隨上下文而異。
有兩種重要的機制可用於實現多態公有繼承:

  • 在派生類中重新定義基類的方法
  • 使用虛方法

下面兩個類說明了多態性

#ifndef BRASS_H_
#define BRASS_H_
class Brass
{
private:
    enum {MAX = 35};
    char fullName[MAX];//客戶姓名
    long acctNum;//帳號
    double balance;//當前結餘
public:
    Brass(const char * s = "Nullbody",long an = -1,double bal = 0.0);
    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 = -1,
        double bal = 0.0,double ml = 500,
        double r = 0.10);
    Brassplus(const Brass & ba,double ml = 500,double r = 0.1);
    virtual void ViewAcct()const;
    virtual void Withdraw(double amt);
    void ResetMax(double m){maxLoan = m;}
    void ResetRate(double r){rate = r;}
    void ResetOwes(){owesBank = 0;}
};

#endif

對於上面的代碼,有幾點需要說明:

  • BrassPlus類在Brass類的基礎上添加了3個私有數據成員和3個公有成員函數
  • Brass類和BrassPlus類都說明了ViewAcct( )和Withdraw( )方法,但BrassPlus對象和Brass對象的這些方法的行爲是不同的
  • Brass類在聲明ViewAcct( )和Withdraw( )時使用了關鍵字virtual。這些方法被稱爲虛方法
  • Brass類還聲明瞭一個虛擬析構函數,雖然該析構函數不執行任何操作

第一點沒什麼好說的。
第二點介紹了聲明如何指出方法在派生類的行爲的不同。程序將使用對象類型來確定使用哪個版本

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

第三點(使用virtual)比前面亮點複雜。如果方法是通過引用或者指針調用的,他將確定使用哪一種方法。如果沒有關鍵字virtual,程序將根據引用類型或指針類型選擇方法;如果使用了virtual程序將根據引用或者指針指向的對象的類型來選擇方法。

//如果ViewAcct()不是虛擬的,則程序行爲如下
Brass dom("Dominic Banker",1124,2344.32);
BrassPlus dot("Dorothy Banker",12118,234.34);
Brass & b1 = dom;
Brass & b2 = dot;
b1.ViewAcct();//使用Brass::ViewAcct()
b2.ViewAcct();//使用Brass::ViewAcct()

//如果ViewAcct()是虛擬的,則程序行爲如下
Brass dom("Dominic Banker",1124,2344.32);
BrassPlus dot("Dorothy Banker",12118,234.34);
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-1);
    fullName[MAX-1] = '\0';
    acctNum = an;
    balance = bal;
}

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

void Brass::Withdraw(double amt)
{
    if(amt < 0)
        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(2);//格式化命令將浮點數的值輸出模式設置爲定點,即包含兩位小數

    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 = 0.0;
    rate = r;
}

Brassplus::Brassplus(const Brass & ba,double ml,double r):Brass(ba)
{
    maxLoan = ml;
    owesBank = 0.0;
    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(2);//格式化命令將浮點數的值輸出模式設置爲定點,即包含兩位小數

    Brass::ViewAcct();
    cout<<"Maximun loan: $"<<maxLoan<<endl;
    cout<<"Owed to bank: $"<<owesBank<<endl;
    cout<<"Loan Rate: "<<100 * 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(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. Transation cancelled.\n";
    cout.setf(initialState);
}

測試代碼:

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

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

    int i;
    for(i = 0;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 = 0;i < CLIENTS;i++)
    {
        p_clients[i]->ViewAcct();
        cout<<endl;
    }
    for(i = 0;i < CLIENTS;i++)
        delete p_clients[i];
    cout<<"Done.\n";
    return 0;
}

爲何需要虛擬析構函數?
在上面的測試代碼中,使用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 = 35};
    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 = -1,double bal = 0.0);
    void Deposit(double amt);
    virtual void Withdraw(double amt) = 0;//純虛函數
    double Balance() const{return balance;}
    virtual void ViewAcct()const  = 0;//純虛函數
    virtual ~AcctABC(){ }
};

class Brass : public AcctABC
{
public:
    Brass(const char * s = "Nullbody",long an = -1,
        double bal = 0.0):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 = -1,
        double bal = 0.0,double ml = 500,
        double r = 0.10);
    Brassplus(const Brass & ba,double ml = 500,double r = 0.1);
    virtual void ViewAcct()const;
    virtual void Withdraw(double amt);
    void ResetMax(double m){maxLoan = m;}
    void ResetRate(double r){rate = r;}
    void ResetOwes(){owesBank = 0;}
};
#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 -1);
    fullName[MAX-1] = '\0';
    acctNum = an;
    balance = bal;
}

void AcctABC::Deposit(double amt)//存錢
{
    if(amt < 0)
        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(2);
    return initialState;
}

void Brass::Withdraw (double amt)
{
    if(amt < 0)
        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 = 0.0;
    rate = r;
}

Brassplus::Brassplus(const Brass & ba,double ml,double r)
    :AcctABC(ba)
{
    maxLoan = ml;
    owesBank = 0.0;
    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: "<<100*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 * (1.0 + 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.返回對象和返回引用

一些類方法返回對象,而另一些則返回引用。如果可以不返回對象,則應返回引用。
首先,返回對象涉及到生成返回對象的臨時拷貝,耗時,佔資源。返回引用則可以節省時間和內存。
不過,函數不能返回在函數中創建的臨時對象的引用,因爲當函數結束時,臨時對象將消失,因此這種 引用是非法的。在這種情況下應返回在函數中臨時創建的對象,以生成一個調用程序可以使用的拷貝。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章