c++primer第十三章 类继承

news2024/11/17 9:31:25

本章内容:
单个类就可以提供用于管理对话框的全部资源。通常,类库是以源代码的方式提供的,这意味着可以对其进行修改,以满足需求。但是,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关系

什么不能被继承

赋值操作符

私有成员和保护成员

虚方法

析构函数

友元函数

有关基类方法的说明

类函数小结

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2184664.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Android 源码分析】Activity生命周期之onDestroy

忽然有一天&#xff0c;我想要做一件事&#xff1a;去代码中去验证那些曾经被“灌输”的理论。                                                                                  – 服装…

命令行操作的基本指令【Linux】学完使用操作命令行就像喝汤一样快

致谢:Linux常用命令大全(手册) – 真正好用的Linux命令在线查询网站 提供的命令查询 一、认识操作系统 1.1计算机层级结构 操作系统是一款进行软硬件资源管理的软件 计算机从最接近我们的开始是软件->操作系统->设备驱动->硬件,我们学习软件要学习他的使用方法,而…

【光伏混合储能】VSG并网运行,构网型变流器,虚拟同步机仿真

摘要 本文提出了一种基于光伏发电与混合储能系统结合的虚拟同步发电机&#xff08;VSG&#xff09;控制策略&#xff0c;该策略能够在并网运行时稳定电网电压和频率。通过仿真分析&#xff0c;验证了该策略在各种运行工况下的有效性&#xff0c;展示了其在电力系统中的广泛应用…

RDI ADCP命令与ASCII输出结构

RDI ADCP命令与ASCII输出结构 一、RDI垂直式ADCP:1.1固定命令&#xff1a;1.2 向导命令 二、RDI水平式ADCP三、ADCP 公共目录四、常用BBTalk命令五、ADCP的ASCII输出数据文件、流量与数据结构5.1 ASCII类输出&#xff1a;5.2 ASCII 输出数据文件头5.3 ASCII 输出数据集5.4 导航…

PMP--三模--解题--91-100

文章目录 10.沟通管理--沟通管理计划--沟通管理计划是项目管理计划的组成部分&#xff0c;描述将如何规划&#xff0c;结构化、执行与监督项目沟通&#xff0c;以提高沟通的有效性。该计划包括如下信息&#xff1a;干系人的沟通需求。--与干系人沟通的文件&#xff0c;要看沟通…

Prometheus监控MySQL主从数据库

案例分析Prometheus监控MySQL主从数据库 1. 规划节点 节点规划&#xff0c;见表1。 表1 节点规划 IP主机名节点192.168.100.3masterPrometheus Grafana192.168.100.3mastermysql/master mysqld_exporter node_exporter192.168.100.4nodemysql/master mysqld_exporter …

毕业设计选题:基于ssm+vue+uniapp的家庭记账本小程序

开发语言&#xff1a;Java框架&#xff1a;ssmuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;M…

开放式耳机的优缺点?哪个品牌专业?好用的开放式蓝牙耳机分享

我相信很多想入手的开放式耳机的家人都想知道开放式耳机是什么&#xff0c;开放式耳机有什么优缺点&#xff0c;开放式耳机是不是智商税、值不值得购买以及如果要购买的话&#xff0c;有什么专业的开放式耳机品牌推荐。因为我毕竟是测评过三十多款开放式耳机的数码博主&#xf…

67.【C语言】枚举类型

1.定义 对于有限的情况,一一列举 如一周有7天,从周一到周日;光学三原色(Red Green Blue) 2.格式 enum 枚举类型名 {//枚举常量 }; 备注:enum为enumeration缩写 3.枚举成员变量的值 #include <stdio.h> enum color {Red,Green,Blue };int main() {printf("%d…

健康信息管理:SpringBoot的创新应用

第2章相关技术 2.1 B/S架构 B/S结构的特点也非常多&#xff0c;例如在很多浏览器中都可以做出信号请求。并且可以适当的减轻用户的工作量&#xff0c;通过对客户端安装或者是配置少量的运行软件就能够逐步减少用户的工作量&#xff0c;这些功能的操作主要是由服务器来进行控制的…

C++平台跳跃游戏

目录 开头程序Game.cpp源文件Player.h头文件Player.cpp源文件 程序的流程图程序游玩的效果下一篇博客要说的东西 开头 大家好&#xff0c;我叫这是我58。 程序 Game.cpp源文件 #include <iostream> #include "Player.h" using namespace std; void printma…

【环保背景高管1009】2022顶刊论文数据,环保背景高管对投资的影响探究

今天给大家分享的是国内顶刊数量经济技术经济研究2022年发布的名为《高管环保背景与绿色投资者进入》论文使用到的数据——高管环保背景数据以及管理自主权的高管环保背景数据&#xff0c;该文章利用中国上市公司沪深A股2007-2017年的面板数据&#xff0c;采用双向固定效应模型…

YOLOv11改进 | 独家创新- 注意力篇 | YOLOv11结合全新多尺度线性注意力机制MLAttention(全网独家创新)

1. MLAttention介绍 (1). 多尺度卷积操作&#xff1a;MLAttention通过多尺度卷积操作来增强不同尺度的特征表达能力。采用了多种卷积核尺寸&#xff08;例如5x5、1x7、7x1、1x11、11x1、1x21、21x1&#xff09;的深度可分离卷积来捕捉不同感受野的特征。较小的卷积核擅长捕捉细…

四,MyBatis-Plus 当中的主键策略和分页插件的(详细实操使用)

四&#xff0c;MyBatis-Plus 当中的主键策略和分页插件的(详细实操使用) 文章目录 四&#xff0c;MyBatis-Plus 当中的主键策略和分页插件的(详细实操使用)1. 主键策略1.1 主键生成策略介绍 2. 准备工作&#xff1a;2.1 AUTO 策略2.2 INPUT 策略2.3 ASSIGN_ID 策略2.3.1 雪花算…

基础算法之双指针--Java实现(下)--LeetCode题解:有效三角形的个数-查找总价格为目标值的两个商品-三数之和-四数之和

这里是Themberfue 今天继续讲解经典算法 > 双指针算法 没看过上篇文章的小伙伴记得去看看哦&#x1f618; 有效三角形的个数 题目链接&#xff1a;有效三角形的个数 题目解析 题目要求在该数组中任意选取三个数&#xff0c;看这三个数是否可以构成可以一个有效三角形。最后…

2024上半年网络安全漏洞态势报告

2024年9月26日&#xff0c;“2024中国数字经济创新发展大会”在汕头成功召开&#xff0c;大会汇聚业界精英&#xff0c;旨在全面探讨在新形势新挑战下&#xff0c;如何“健全数据安全体系 构建可信流通环境”。在《数据安全与合规发展专题》分论坛上&#xff0c;工业和信息化部…

Windows远程Kylin系统-xrdp

Windows远程Kylin系统-xrdp 一. 查看开放端口 查看是否有3389端口二. 安装xrdp Kylin对应的是centos8 下载链接&#xff1a;https://rhel.pkgs.org/8/epel-x86_64/xrdp-0.10.1-1.el8.x86_64.rpm.html rpm -Uvh 包名 systemctl start xrdp 启动服务 systemctl enable xrdp …

只出现一次的数字|||(考察点为位操作符)

目录 一题目&#xff1a; 二思路汇总&#xff1a; 三代码解答&#xff1a; 一题目&#xff1a; leetcode原题链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 二思路汇总&#xff1a; 思路&#xff1a;如果直接对数组按位异或&#xff0c;那么最后得到的是a^b&a…

pnpm install的时候失败提示python问题

忘记是哪个依赖了&#xff0c;npm正常&#xff0c;pnpm的时候就异常&#xff0c;但是报错里python异常 解决方法&#xff1a;安装python就行 ennn免安装的python好麻烦 网上找教程安装python好麻烦&#xff0c;发现微软可以直接安装&#xff0c;就用微软的安装了 查看结果 p…

数字化转型新时代:TOGAF框架助力企业颠覆式创新

1. 从传统到未来&#xff1a;TOGAF如何驱动企业的数字化转型 在传统业务模式面临挑战的时代&#xff0c;TOGAF&#xff08;The Open Group Architecture Framework&#xff09;提供了帮助企业完成从传统到数字化业务模式转型的强大工具。通过系统化的架构开发方法&#xff08;…