c++primer第十三章 类继承

本章内容:
单个类就可以提供用于管理对话框的全部资源。通常,类库是以源代码的方式提供的,这意味着可以对其进行修改,以满足需求。但是,C+-+提供了比修改代码更好的方法来扩展和修改类。这种方法叫作类继承(class inheriance)。它能够从已有的类派生出新的类,而派生类继承了原有类(称为基类)的特征,包括方法。

继承能完成的工作:

一个简单的基类

头文件tabetenn0.h

复制代码
#ifndef TABTENNO_H_
#define TABTENNO_H_ // simple base class
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; };
};

#endif

方法文件tabtenn0.cpp

复制代码
// tabtenn0.cppsimple base class methods
#include "tabtenn0.h"
#include <iostream>
#include <cstring>
TableTennisPlayer::TableTennisPlayer(const char *fn,
                                     const char *ln, bool ht)
{
    std::strncpy(firstname, fn, LIM - 1);
    firstname[LIM - 1] = '\0';
    std::strncpy(lastname, ln, LIM - 1);
    lastname[LIM - 1] = '\0';
    hasTable = ht;
}

void TableTennisPlayer::Name() const
{
    std::cout << lastname << "," << firstname;
}

程序文件usett0.cpp

复制代码
#include <iostream>
#include "tabtenn0.h"
int main(void)
{
    using std::cout;
    TableTennisPlayer playerl("Chuck", "Blizzard", true);
    TableTennisPlayer player2("Tara", "Boomdea", false);
    playerl.Name();
    if (playerl.HasTable())
        cout << ":has a table.\n";
    else
        cout << ":hasn't a table.\n";
    player2.Name();
    if (player2.HasTable())
        cout << ":has a table";
    else
        cout << ":hasn't a table.\n";
    return 0;
}

TableTennisPlayer类只是记录会员的名字以及是否有球桌。

派生一个类

--些成员曾经参加过当地的乒乓球锦标赛,需要这样一个类,它能包括成员在比赛中的比分。与其从零开始,不如从 TableTennisClass 类派生出一个类。

冒号指出 RatedPlayer 类的基类是 TableTennisplayer 类。

RatePlayer对象可以使用TableTennisPlayer类的 Name(),HasTable(),ResetTable()方法

继承需要添加

复制代码
class RatePlayer: public TableTennisPlayer
{
    private:
    unsigned int rating;
    public:
    RatePlayer(unsigned int r = 0, const char * fn = "none",
    const char * ln = "none", bool ht = false);
    RaterPlayer(unsigned int r, const TableTennisPlayer & tp);
    unsigned int Rating(){return rating;}
    void ResetRating(unsigned int r){rating = r;}
}

构造函数:访问权限的考虑

派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。例如,RatedPlayer 构造函数不能直接设置继承的成员(firsame、lastame和 hasTable),而必须使用基类的公有方法来访问私有的基类成员。具体地说,派生类构造函数必须使用基类构造函数。

创建派生类对象时,程序首先创建基类对象。

基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表句法来完成这种工作。

复制代码
RatedPlayer::RatedPlayer(unsigned int r, const char * fn, const char * ln,
bool ht):TableTennisPlayer(fn, ln, ht)
{
    rating = r;
}

如果省略成员初始化列表:

基类对象必须首先被创建,如果不调用基类构造函数,程序将使用默认的基类构造函数,因此上述代码等效为

第二个构造函数的代码

因为的类型为 const TableTennisPlayer&,因此将调用基类的复制构造函数。基类没有定义复制构造函数,但第 12章介绍过,如果需要使用复制构造函数,而又没有定义,编译器将自动生成一个。在这种情况下,执行成员复制的隐式复制构造承数是合适的,因为这个类没有使用动态内存分配

构造函数也可以写为

派生类构造函数的要点

使用派生类

头文件

复制代码
#ifndef TABTENNO_H_
#define TABTENNO_H_ // simple base class
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

方法文件

复制代码
// tabtenn0.cppsimple base class methods
#include "tabtenn0.h"
#include <iostream>
#include <cstring>
TableTennisPlayer::TableTennisPlayer(const char *fn,
                                     const char *ln, bool ht)
{
    std::strncpy(firstname, fn, LIM - 1);
    firstname[LIM - 1] = '\0';
    std::strncpy(lastname, ln, LIM - 1);
    lastname[LIM - 1] = '\0';
    hasTable = ht;
}

void TableTennisPlayer::Name() const
{
    std::cout << lastname << "," << firstname;
}

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)
{
}

程序文件

复制代码
#include <iostream>
#include "tabtenn0.h"

int main(void)
{
    using std::cout;
    using std::endl;

    TableTennisPlayer player1("Tara", "Boomdea", false);
    RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
    rplayer1.Name(); // derived obiect uses base method
    if (rplayer1.HasTable())
        cout << ":has a table.\n";
    else
        cout << ":hasn't a table.\n";
    player1.Name(); // base object uses base method
    if (player1.HasTable())
        cout << ":has a table";
    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;
}

派生类和基类之间的关系

派生类与基类之间有一些特殊关系。其中之一是派生类对象可以使用基类的方法,条件是方法不是私有的:

复制代码
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
    rplayer1.Name(); // derived obiect uses base method

另外两个重要的关系是:基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象:

基类指针只能调用基类方法,而不能调用派生类的方法。

但是不可以将基类对象和地址赋给派生类引用和指针:

在下列函数中

rt是基类引用,他可以指向基类对象或派生类对象,show()中使用TableTennis参数或Ratedplayer参数

形参指向基类的指针的函数有相似的关系

继承------is-a关系

c++三种继承关系:

  • 公有继承
  • 保护继承
  • 私有继承

公有继承是最常用的方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。

例如,假设有一个Fruit 类,可以保存水果的重量和热量。因为香蕉是一种特殊的水果,所以可以从Fruit类派生出Banana类。新类将继承原始类的所有数据成员,因此,Banana对象将包含表示香蕉重量和热量的成员。新的Banana 类还添加了专门用于香蕉的成员,这些成员通常不用于水果,例如 Banana InstitutePeelIndex(香蕉机构果皮索引)。因为派生类可以添加特性,所以,将这种关系称为is-a-kind-of/(是一种)关系可能更准确,但是通常使用术语 is-a。

多态公有继承

方法的行为应取决于调用该方法的对象。这种较复杂的行为称为多态--具有多种形态,就是指同一个方法的行为将随上下文而异。有两种重要的机制可用于实现多态公有继承:

例子:

一个类用于表示基本支票账户--Brass Account,另一个类用f表示代表 Brass Plus 支票账户,它添加了透支保护特性。也就是说,如果用户签出一张超出其存款余额的文票--但是超出的数额并不是很大,银行将支付这张支票,对超出的部分收取额外的费用,并追加罚款。可以根据要保存的数据以及允许执行的操作来确定这两种账户的特征。

下面是用于Brass Account支票账户的信息。

Brass Plus支票账户包含BrassAccount的所有信息以及一下信息项:

开发Brass类和BrassPlus类

有关BrassPlus类的信息

  • BrassPlus账户限制用户的透支款额,默认为500,有些客户的限额可能不同

  • 银行可以修改用户的透支限额

  • BrassPlus账户对贷款收取利息,默认为10%,有些用户的利率不同

  • 银行可以修改用户的利率

  • 账户记录用户所欠银行的金额,用户不能通过常规存款或从其他账户转账的方法偿付,必须一现金的方式交给特定的工作人员。

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

  • Brass 类和 BrassPlus 类都声明了 VewAcct()和 Withdraw()方法,但 BrassPlus 对象和 Brass 对象的这些方法的行为是不同的

  • Brass类在:声明 ViewAcct()和 Withdraw()时使用了新关键字virual。这些方法被称为虚方法( virtual method)

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

第二点介绍了声明如何指出方法在派生类的行为的不同。两个VewAcct()原型表明将有2个独立的方法定义。基类版本的限定名为Brass:ViewAcct(),派生类版本的限定名为BrassPlus::ViewAcct()。程序将使用对象类型来确定使用哪个版本:

第三点(使用 virtual)比前两点要复杂。如果方法是通过引用或指针而不是对象调用的,它将确定使川哪一种方法。如果没有使用关键字vinual,程序将根据引用类型或指针类型选择方法:如果使用了virual程序将根术引川或指针指问的对象的类型来选择方法。如果 ViewAcct()不是虚拟的,则程序的行为如下:

使用指针代替,行为类似。

头文件

复制代码
#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

类实现

方法文件

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

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 cancelled.\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
{
    // set. up ###.## format
    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); // restore original format
}

// BrassPlus Methods
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)
// uses implicit copy constructor
{
    maxLoan = ml;
    owesBank = 0.0;
    rate = r;
}

// redefine how ViewAcct()works
void BrassPlus::ViewAcct() const
{
    // set up ###.## format
    ios_base::fmtflags initialState =
        cout.setf(ios_base::fixed, ios_base::floatfield);
    cout.setf(ios_base::showpoint);
    cout.precision(2);
    Brass::ViewAcct(); // display base portion
    cout << "Maximum loan:$" << maxLoan << endl;
    cout << "Owed to bank:$ " << owesBank << endl;
    cout << "Loan Rate:" << 100 * rate << " %\n ";
    cout.setf(initialState);
}

// redefine how Withdraw()works
void BrassPlus::Withdraw(double amt)
{
    // set up ###。##format
    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. Transaction cancelled.\n";
    cout.setf(initialState);
}

派生类并不能直接访问基类的私有数据,而必须使用基类的公有方法才能访问这些数据。访问的方式取决于方法。构造函数使用一种技术,而其他成员函数使用另一种技术。

派生类构造函数在初始化基类私有数据时,采用的是成员初始化列表句法。RatedPlayer 类构造函数和BrassPlus构造函数都使用这种技术。

复制代码
// BrassPlus Methods
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)
// uses implicit copy constructor
{
    maxLoan = ml;
    owesBank = 0.0;
    rate = r;
}

非构造函数不能使用成员初始化列表句法,但派生类方法可以调用公有的基类方法。例如,

BrassPlus版本的 ViewAcct()核心内容如下

复制代码
void BrassPlus::ViewAcct() const
{
    // set up ###.## format
    ios_base::fmtflags initialState =
        cout.setf(ios_base::fixed, ios_base::floatfield);
    cout.setf(ios_base::showpoint);
    cout.precision(2);
    Brass::ViewAcct(); // display base portion
    cout << "Maximum loan:$" << maxLoan << endl;
    cout << "Owed to bank:$ " << owesBank << endl;
    cout << "Loan Rate:" << 100 * rate << " %\n ";
    cout.setf(initialState);
}

调用基类方法Brass::ViewAcct()来显示基类数据成员。

代码必须使用作用域解析操作符,不然

如果代码没有使用作用域解析操作符,编译器将认为VewAcct()是BrassPlus::ViewAcct(),

这将创建个不会终止的递归函数---这种情况可不好。

使用类

复制代码
#include <iostream>
#include "brass.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 Account;n";
    Piggy.Withdraw(4200.00);
    cout << "Pigg account balance;$" << Piggy.Balance() << endl;
    cout << "Withdrawing $4200 from the Hogg Account; \n";
    Hoggy.Withdraw(4200.00);
    Hoggy.ViewAcct();
    return 0;
}

虚方法的行为

在上述代码中,由于方法是通过对象(而不是指针或引用)调用的,所以没有使用虚方法特性。下面米看一个使用了虚方法的例子。假设要同时管理Brass和 BrassPlus账户,如果能使用同一个数组来保存 Brsss 和 BrassPlus 对象,将很有帮助,但这是不可能的。数组中所有元素的类型必须相同,而 Brass 和BrassPlus是不同的类型。不过,可以创建指向 Brass 的指针数组。这样,每个元素的类型都相同,但由于使用的是公有继承模型,因此 Brass 指针既可以指向 Brass对象,也可以指向 BrassPlus 对象。因此,可以使用...个数组来表示多种类型的对象。这就是多态性,程序单13.10是一个简单的例子。

复制代码
#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 opening balance:$";
        cin >> tempbal;
        cout << "Enter lfor Brass Account or " << "2 for Brassplus Account:";
        while (cin >> kind && (kind != '1' && kind != '2'))
            cout << "Enter either 1or 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 decimai 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]; // free memory
    cout << "Done.\n";
    return 0;
}

多态特性体现为

复制代码
for (i = 0; i < CLIENTS; i++)
    {
        p_clients[i]->ViewAcct();
        cout << endl;
    }

为何使用虚拟析构函数

静态联编和动态联编

程序调用函数时,将使用哪个可执行代码块呢?编译器负责回答这个问题。将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding)。

在C++中,由于函数重载的缘故,这项任务更复杂。编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而,C/C++编译器可以在编译过程完成这种联编。在编译过程中进行联编被称为静态联编(static binding),又称为早期联编(eanly binding)。不过,虚函数使这项工作变得更困难。

将选择哪种类型的对象。所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(dyamic binding),又称为晚期联编(late binding)

指针和引用类型的兼容性

将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasting),这使公有继承不需要进行显式类型转换。该规则是is-a关系的一部分。

将指向对象的指针作为函数参数时,也是如此。向上强制转换是可传递的,也就是说,如果从 BrassPlus派生出BrassPlusPlus类,则Brass指针或引用可以引用Brass 对象、BrassPlus对象或 BrassPlusPlus 对象。

将基类指针或引用转换为派生类指针或引用--称为向下强制转换(downcasting)。相反的过程一如果不使用显式类型转换,则向下强制转换是不允许的。原因是is-a关系通常是不可逆的。派生类可以新增数据成员,因此使用这些数据成员的类成员函数不能应用于基类

隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。C++使用虚成员函数来满足这种需求。

虚成员函数和动态联编

对于代码

如果没有定义虚函数,将 ViewAcct()关联到 Brass::ViewAcct()。简而言之,编泽器对非虚方法使用静态联编。

但是,如果在基类中将 ViewAcct()声明为虚拟的,则bp->ViewAcct()根据对象类型(BrassPlus)调用BrassPlus:ViewAcct(),编译器对虚方法使用动态联编。

为什么有两种类型的联编以及为什么默认为静态联编

静态效率更高

虚函数的工作原理

、编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table,vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址:如果派生类没有重新定义虚函数,该 vb将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的资质也被添加到vtbl中。

有关虚函数的注意事项:

  • 在基类方法的声明中使用关键字 vintual可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚拟的。
  • 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。这种行为非常重要,因为这样基类指针或引用可以指向派生类对象。
  • 加果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚拟的。

对于一些特殊函数

构造函数

构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以将类构造函数声明为虚拟的没有什么意义。

析构函数

析构函数应当是虚函数,除非类不用做基类。例如,假设 Employee 是基类,Singer 是派生类,并添加一个 char *成员,该成员指向由 new 分配的内存。当 Singer 对象过期时,必须调用~Singer()析构函数来释放内存

如果使用默认的静态联编,delete 语句将调用~Employee()析构函数。这将释放由 Singer 对象中的Employee 部分指向的内存,但不会释放新的类成员指向的内存。但如果析构函数是虚拟的,则上述代码将先调用~Singer 析构函数释放由 Singer 组件指向的内存,然后,调用~Employee()析构函数来释放由Employee 组件指问的内存,

友元函数
没有重新定义的函数
重新定义隐藏方法

编译器警告:

代码的含义

新定义的函数覆盖了原来的的函数,是的showperks()不接受参数。简而言之,重新定义继承的方法并不是重载。如果在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何。

这引出了两条经验规则:第一,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返问类型协变(covariance ofretumntype),因为允许返回类型随类类型的变化而变化:

第二,如果基类声明被重载了,则应在派生类总重新定义所有的基类版本

如果只定义一个版本,则另外两个版本会被隐藏。

访问控制:protected

关键字 protected与private 相似,在类外只能用公有类成员来访问 protected 部分中的类成员。private 和 protected之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来说保护成员的行为一私有成员相似:但对于派生类来说,保护成员的行为与公有成员相似。

如果定义balance为保护类型

则在派生类中的方法中可以直接访问balance

抽象基类

抽象基类(abstract base class, ABC)

例如椭圆类

圆类

可以抽象为

C++通过使用纯虚函数(pure virtualfunction)提供未实现的函数。纯虚函数声明的结尾处为=0,参见Area()方法:

当类声明中包含纯虚函数时,则不能创建该类的对象。这里的理念是,包含纯虚函数的类只用作基类。要成为真正的 ABC,必须至少包含一个纯虚函数。原型中的=0 使虚函数成为纯虚函数。这里的方法 Area()没有定义,但 C++甚至允许纯虚函数有定义。

应用ABC概念

复制代码
#ifndef ACCTABC_H_
#define ACCTABC_H_

#include <iostream>
class AcctABC
{
private:
    enum
    {
        MAX = 35
    };
    char fullName[MAX];
    long acctNum;
    double balance;

protected:
    const char *FulName() 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

方法文件

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

// Abstract Base Class

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;
}

// protected method
std::ios_base::fmtflags AcctABC::SetFormat() const
{
    // set. up ###.## format
    ios_base::fmtflags initialState =
        cout.setf(ios_base::fixed, ios_base::floatfield);
    cout.setf(ios_base::showpoint);
    cout.precision(2);
    return initialState;
}
// Brass methods

void Brass::ViewAcct() const
{
    // set. up ###.## format
    ios_base::fmtflags initialState = SetFormat();
    cout << " Brass Client:" << FulName() << endl;
    cout << "Account Number:" << AcctNum() << endl;
    cout << "Balance:$" << Balance() << endl;
    cout.setf(initialState); // restore original format
}

void AcctABC::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";
}

// BrassPlus Methods
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)
// uses implicit copy constructor
{
    maxLoan = ml;
    owesBank = 0.0;
    rate = r;
}

// redefine how ViewAcct()works
void BrassPlus::ViewAcct() const
{
    // set up ###.## format
    ios_base::fmtflags initialState = SetFormat();
    cout << " Brass Client:" << FulName() << endl;
    cout << "Account Number:" << AcctNum() << endl;
    cout << "Balance:$" << Balance() << endl;
    cout << "Maximum loan:$" << maxLoan << endl;
    cout << "Owed to bank:$ " << owesBank << endl;
    cout << "Loan Rate:" << 100 * rate << " %\n ";
    cout.setf(initialState);
}

// redefine how Withdraw()works
void BrassPlus::Withdraw(double amt)
{
    // set up ###。##format
    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

该声明包含:析构函数,复制构造函数,重载赋值操作符;

从baseDMA 派生出lackDMA

不需要为派生类定义显式析构函数,复制构造函数,重载赋值操作符;

派生类使用new

这种情况需要定义定义显式析构函数,复制构造函数,重载赋值操作符。

使用动态内存分配和友元的继承范例

头文件

复制代码
#ifndef DMA_H_
#define DMA_H_

#include <iostream>

class baseDMA
{
private:
    char *label;
    int rating;

public:
    baseDMA(const char *l = "null", int r = 0);
    baseDMA(const baseDMA &rs);
    virtual ~baseDMA();
    baseDMA &operator=(const baseDMA &rs);
    friend std::ostream &operator<<(std::ostream &os, const baseDMA &rs);
};

class lacksDMA : public baseDMA
{
private:
    enum
    {
        COL_LEN = 40
    };
    char color[COL_LEN];

public:
    lacksDMA(const char *c = "blank", const char *l = "null", int r = 0);
    lacksDMA(const char *c, const baseDMA &rs);
    friend std::ostream &operator<<(std::ostream &os, const lacksDMA &rs);
};

class hasDMA : public baseDMA
{
private:
    char *style;

public:
    hasDMA(const char *s = "none", const char *l = "null",
           int r = 0);
    hasDMA(const char *s, const baseDMA &rs);
    hasDMA(const hasDMA &hs);
    ~hasDMA();
    hasDMA &operator=(const hasDMA &rs);
    friend std::ostream &operator<<(std::ostream &os, const hasDMA &rs);
};
#endif

方法文件

复制代码
#include "dma.h"
#include <cstring>

baseDMA::baseDMA(const char *l, int r)
{
    label = new char[std::strlen(l) + 1];
    std::strcpy(label, l);
    rating = r;
}

baseDMA::baseDMA(const baseDMA &rs)
{
    label = new char[std::strlen(rs.label) + 1];
    std::strcpy(label, rs.label);
    rating = rs.rating;
}

baseDMA::~baseDMA()
{
    delete[] label;
}

baseDMA &baseDMA::operator=(const baseDMA &rs)
{
    if (this == &rs)
        return *this;
    delete[] label;
    label = new char[std::strlen(rs.label) + 1];
    std::strcpy(label, rs.label);
    rating = rs.rating;
    return *this;
}

std::ostream &operator<<(std::ostream &os, const baseDMA &rs)
{
    os << "Label: " << rs.label << std::endl;
    os << "Rating: " << rs.rating << std::endl;
    return os;
}

// lacksDMA methods
lacksDMA::lacksDMA(const char *c, const char *l, int r) : baseDMA(l, r)
{
    std::strncpy(color, c, 39);
    color[39] = '\0';
}

lacksDMA::lacksDMA(const char *c, const baseDMA &rs) : baseDMA(rs)
{
    std::strncpy(color, c, COL_LEN - 1);
    color[COL_LEN - 1] = '\0';
}

std::ostream &operator<<(std::ostream &os, const lacksDMA &ls)
{
    os << (const baseDMA &)ls;
    os << "Color:" << ls.color << std::endl;

    return os;
}

// hasDMA::methods
hasDMA::hasDMA(const char *s, const char *l, int r) : baseDMA(l, r)
{
    style = new char[std::strlen(s) + 1];
    std::strcpy(style, s);
}

hasDMA::hasDMA(const char *s, const baseDMA &rs) : baseDMA(rs)
{
    style = new char[std::strlen(s) + 1];
    std::strcpy(style, s);
}

hasDMA::hasDMA(const hasDMA &hs) : baseDMA(hs)
// invoke base class copy constructor
{
    style = new char[std::strlen(hs.style) + 1];
    std::strcpy(style, hs.style);
}

hasDMA::~hasDMA()
{
    delete[] style;
}

hasDMA &hasDMA::operator=(const hasDMA &hs)
{
    if (this == &hs)
        return *this;
    baseDMA::operator=(hs); // copy base portion
    style = new char[std::strlen(hs.style) + 1];
    std::strcpy(style, hs.style);
    return *this;
}
std::ostream &operator<<(std::ostream &os, const hasDMA &hs)
{
    os << (const baseDMA &)hs;
    os << "Style: " << hs.style << std::endl;
    return os;
}

程序文件

复制代码
#include <iostream>
#include "dma.h"
int main()
{
    using std::cout;
    using std::endl;
    baseDMA shirt("Portabelly", 8);
    lacksDMA balloon("red", "Blimpo", 4);
    hasDMA map("Mercator", "Buffalo Keys", 5);
    cout << shirt << endl;
    cout << balloon << endl;
    cout << map << endl;
    lacksDMA balloon2(balloon);
    hasDMA map2;
    map2 = map;
    cout << balloon2 << endl;
    cout << map2 << endl;
    return 0;
}

类设计回顾(待整理)

编译器生成的成员函数

默认构造函数

复制构造函数

赋值操作符

其他的类方法

构造函数

析构函数

转换

按值传递对象和传递引用

返回对象和返回引用

使用const

公有继承的考虑因素

is-a关系

什么不能被继承

赋值操作符

私有成员和保护成员

虚方法

析构函数

友元函数

有关基类方法的说明

类函数小结

相关推荐
Scott9999HH1 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
2401_841495642 小时前
【操作系统】进程同步与互斥实验报告
c++·算法·操作系统·进程·并发·同步·互斥
fqbqrr2 小时前
2607C++,soui与安卓
c++·soui
码智社2 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海2 小时前
python 列表、元组、集合和字典
开发语言·python
克里斯蒂亚诺更新3 小时前
ruoyi角色权限的对应关系
笔记
萧瑟余晖3 小时前
JDK 26 新特性详解
java·开发语言
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
a1117765 小时前
2FA 验证码生成器(github登录验证 app)
笔记·学习
我的xiaodoujiao6 小时前
API 接口自动化测试详细图文教程学习系列32--Allure测试报告2
python·学习·测试工具·pytest