Bootstrap

【C++ Primer Plus】第13章 类继承

C++提供了比修改代码更好的方法来扩展和修改类。这种方法叫作类继承,它能够从已有的类派生出新的类,而派生类继承了原有类(称为基类)的特征,包括方法。

  1. 可以在已有类的基础上添加功能。
  2. 可以给类添加数据。
  3. 可以修改类方法的行为。(虚函数、纯虚函数)

13.1 简单继承

从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。

示例:一个基类例程

tabtenn0.h

// tabtenn0.h -- a table-tennis base class

#ifndef PRIMERPLUS_TABTENN0_H
#define PRIMERPLUS_TABTENN0_H
#include <string>
using std::string;
// simple base class
class TableTennisPlayer
{
private:
    string firstname;
    string lastname;
    bool hasTable;
public:
    TableTennisPlayer (const string & fn = "none",
                       const string & ln = "none", bool ht = false);
    void Name() const;
    bool HasTable() const { return hasTable; };
    void ResetTable(bool v) { hasTable = v; };
};
#endif //PRIMERPLUS_TABTENN0_H

tabtenn0.cpp

//tabtenn0.cpp -- simple base-class methods
#include "tabtenn0.h"
#include <iostream>

TableTennisPlayer::TableTennisPlayer (const string & fn,
                                      const string & ln,
                                      bool ht) : firstname(fn),lastname(ln), hasTable(ht) {}

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

usett0.cpp

// usett0.cpp -- using a base class
#include <iostream>
#include "tabtenn0.h"

int main ( void )
{
    using std::cout;
    TableTennisPlayer player1("Chuck", "Blizzard", true);
    TableTennisPlayer player2("Tara", "Boomdea", false);
    player1.Name();
    if (player1.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";
    // std::cin.get();
    return 0;
}

out:

Blizzard, Chuck: has a table.
Boomdea, Tara: hasn't a table.

13.1.1 如何派生一个类

class RatedPlayer : public TableTennisPlayer

  1. 冒号指出RatedPlayer类(派生类)的基类是TableTennisplayer类。
  2. public表明TableTennisPlayer是一个公有基类,这被称为公有派生。
  3. 派生类对象包含基类对象。
  4. 使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。
  5. 派生类对象存储了基类的数据成员(派生类继承了基类的实现);
  6. 派生类对象可以使用基类的方法(派生类继承了基类的接口)。
  7. 派生类需要自己的构造函数。构造函数必须给新成员(如果有的话)和继承的成员提供数据。
  8. 派生类可以根据需要添加额外的数据成员和成员函数。
// simple derived class
class RatedPlayer : public TableTennisPlayer
{
private:
    unsigned int rating; // add a data member
public:
    RatedPlayer (unsigned int r = 0, const string & fn = "none",
                 const string & ln = "none", bool ht = false);	// 构造函数1
    RatedPlayer(unsigned int r, const TableTennisPlayer & tp);	// 构造函数2
    unsigned int Rating() const { return rating; } 		// add a method
    void ResetRating (unsigned int r) {rating = r;} 	// add a method
};

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

  1. 派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。
  2. 创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。
  3. 基类构造函数负责初始化继承的数据成员;
  4. 派生类构造函数主要用于初始化新增的数据成员。
  5. 派生类的构造函数总是调用一个基类构造函数。
  6. 可以使用初始化器列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数。
  7. 派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。
  8. 实际上,派生类的默认构造函数总是要进行一些操作:执行自身的代码后调用基类析构函数。
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
						 const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)	// 调用基类默认构造函数
{
    rating = r;
}

RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
						 const string & ln, bool ht) 	// 调用基类复制构造函数
{
	rating = r;
}

示例:一个使用派生类的例程

tabtenn1.h

// tabtenn1.h -- a table-tennis base class

#ifndef PRIMERPLUS_TABTENN1_H
#define PRIMERPLUS_TABTENN1_H

#include <string>
using std::string;
// simple base class
class TableTennisPlayer
{
private:
    string firstname;
    string lastname;
    bool hasTable;
public:
    TableTennisPlayer (const string & fn = "none",
                       const string & ln = "none", bool ht = false);
    void Name() const;
    bool HasTable() const { return hasTable; };
    void ResetTable(bool v) { hasTable = v; };
};

// simple derived class
class RatedPlayer : public TableTennisPlayer
{
private:
    unsigned int rating;
public:
    RatedPlayer (unsigned int r = 0, const string & fn = "none",
                 const string & ln = "none", bool ht = false);
    RatedPlayer(unsigned int r, const TableTennisPlayer & tp);
    unsigned int Rating() const { return rating; }
    void ResetRating (unsigned int r) {rating = r;}
};
#endif //PRIMERPLUS_TABTENN1_H

tabtenn1.cpp

//tabtenn1.cpp -- simple base-class methods
#include "tabtenn1.h"
#include <iostream>

TableTennisPlayer::TableTennisPlayer (const string & fn,
                                      const string & ln, bool ht) : firstname(fn),
                                                                    lastname(ln), hasTable(ht) {}

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

// RatedPlayer methods
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
                         const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
    rating = r;
}

RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp)
        : TableTennisPlayer(tp), rating(r)
{
}

usett1.cpp

// usett1.cpp -- using base class and derived class
#include <iostream>
#include "tabtenn1.h"

int main ( void )
{
    using std::cout;
    using std::endl;
    TableTennisPlayer player1("Tara", "Boomdea", false);
    RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
    rplayer1.Name();          // derived object 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;
// initialize RatedPlayer using TableTennisPlayer object
    RatedPlayer rplayer2(1212, player1);
    cout << "Name: ";
    rplayer2.Name();
    cout << "; Rating: " << rplayer2.Rating() << endl;
    // std::cin.get();
    return 0;
}

out:

Duck, Mallory: has a table.
Boomdea, Tara: hasn't a table.
Name: Duck, Mallory; Rating: 1140
Name: Boomdea, Tara; Rating: 1212

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

  1. 基类指针(或引用)可以在不进行显式类型转换的情况下指向(或引用)派生类对象;
  2. 派生类对象可以使用基类的方法,派生类方法可以调用基类的方法(必要时加作用域解析符::)
  3. 基类指针或引用只能用于调用基类方法。
  4. 通常,C++要求引用和指针类型与赋给的类型匹配,但这一规则对继承来说是例外。
  5. 不可以将基类对象和地址赋给派生类引用和指针。
  6. 对于形参为指向基类的指针的函数,可以使用基类对象的地址或派生类对象的地址作为实参。
  7. 可以将基类对象初始化为派生类对象,也可以将派生对象赋给基类对象
RatedPlayer rplayer(1140, "Mallory", "Duck", true);
TableTennisPlayer & rt = rplayer;
TableTennisPlayer * pt = &rplayer;
rplayer.Name(); // derived object uses base method
rt.Name(); 		// invoke Name() with reference
pt->Name(); 	// invoke Name() with pointer

// 将基类对象初始化为派生类对象
RatedPlayer olaf1(1840, "Olaf", "Loaf", true);
TableTennisPlayer olaf2(olaf1);

RatedPlayer olaf1(1840, "Olaf", "Loaf", true);
TableTennisPlayer winner;
winner = olaf1; // 可以将派生对象赋给基类对象

13.2 继承:is-a关系

  1. C++有3种继承方式:公有继承、保护继承和私有继承。
  2. 公有继承是最常用的方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。
  3. 公有继承不建立 has-a关系。
  4. 公有继承不能建立is-like-a关系,也就是说,它不采用明喻。
  5. 公有继承不建立is-implemented-as-a(作为……来实现)关系。
  6. 公有继承不建立uses-a关系。

13.3 多态公有继承

同一个方法在派生类和基类中的行为是不同的。

  1. 在派生类中重新定义基类的方法,需要使用虚方法。然后,程序将根据对象类型而不是引用(或指针)的类型来选择方法版本。
  2. 方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法。
  3. 关键字virtual只用于类声明的方法原型中,而没有用于方法定义中。
  4. 如果析构函数不是虚的,则将只调用对应于指针类型的析构函数。
  5. 如果析构函数是虚的,将调用相应对象类型的析构函数。
  6. 如果派生类包含一个执行某些操作的析构函数,则基类必须有一个虚析构函数,即使该析构函数不执行任何操作。
  7. 可以创建指向 Brass的指针数组。这样,每个元素的类型都相同,但由于使用的是公有继承模型,因此Brass指针既可以指向Brass对象,也可以指向 BrassPlus对象。因此,可以使用一个数组来表示多种类型的对象。这就是多态性。
  8. 如果要在派生类中重新定义基类的方法,则将它设置为虚方法;否则,设置为非虚方法。
// 如果ViewAcct( )是虚的
virtual void ViewAcct() const;

Brass dom("Dominic Banker", 11224, 4183.45);
BrassPlus dot("Dorothy Banker", 12118, 2592.00);
Brass & b1_ref = dom;
Brass & b2_ref = dot;
b1_ref.ViewAcct(); 	// use Brass::ViewAcct()
b2_ref.ViewAcct(); 	// use BrassPlus::ViewAcct()

// 如果ViewAcct( )不是虚的,都调用基类方法

示例:一个多态继承例程

brass.h

// virtual 如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。
#ifndef PRIMERPLUS_BRASS_H
#define PRIMERPLUS_BRASS_H
#include <iostream>
#include <cstring>
using namespace std;

class Brass
{
private:
    string fullName;    // 客户姓名
    long acctNum;       // 账号
    double balance;     // 当前余额
public:
    Brass(const string & s = "Nullboby", 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 string & s = "Nullbody", long an = -1,
              double bal = 0.0, double ml = 500,
              double r = 0.11125);
    BrassPlus(const Brass & ba, double ml = 500, double r = 0.11125);
    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 //PRIMERPLUS_BRASS_H

brass.cpp

#include "brass.h"

typedef ios_base::fmtflags format;
typedef streamsize precis;
format setFormat();
void restore(format f, precis p);

Brass::Brass(const string & s, long an, double bal)
{
    fullName = s;
    acctNum = an;
    balance = bal;
}

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

void Brass::Withdraw(double amt)
{
    format initialState = setFormat();
    precis prec = cout.precision(2);

    if (amt < 0)
        cout << "Negative withdraw not allowed.\n";
    else if (amt <= balance)
        balance -= amt;
    else
        cout << "money insufficient.\n";
    restore(initialState, prec);
}

double Brass::Balance() const
{
    return balance;
}

void Brass::ViewAcct() const
{
    format initialState = setFormat();
    precis prec = cout.precision(2);
    cout << "Client: " << fullName << endl;
    cout << "Account Number: " << acctNum << endl;
    cout << "Balance: $" << balance << endl;
    restore(initialState, prec);
}

// ------------------------------------------------------------------

BrassPlus::BrassPlus(const string & 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
{
    format initialState = setFormat();
    precis prec = cout.precision(2);

    Brass::ViewAcct();
    cout << "Maximum loan: $" << maxLoan << endl;
    cout << "Owed to bank: $" << owesBank << endl;
    cout.precision(3);
    cout << "Loan Rate: " << 100*rate << "%\n";

    restore(initialState, prec);
}

void BrassPlus::Withdraw(double amt)
{
    format initialState = setFormat();
    precis prec = cout.precision(2);

    double bal = Balance();     // 调用基类的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 << "Fiance charge: $" << advance * rate << endl;
        Deposit(advance);
        Brass::Withdraw(amt);
    }
    else
        cout << "Credit limit exceeded.\n";
    restore(initialState, prec);
}

format setFormat()
{
    return cout.setf(ios_base::fixed, ios_base::floatfield);
}

void restore(format f, precis p)
{
    cout.setf(f, ios_base::floatfield);
    cout.precision(p);
}

usebrass.cpp

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

13.4 静态联编和动态联编

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

13.4.1 指针和引用类型的兼容性

  1. 将派生类引用或指针转换为基类引用或指针——称为向上强制转换(upcasting),这使公有继承不需要进行显式类型转换。
  2. 将基类指针或引用转换为派生类指针或引用——称为向下强制转换(downcasting)。如果不使用显式类型转换,则向下强制转换是不允许的。
  3. 隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。C++使用虚成员函数来满足这种需求。
// 对于使用基类引用或指针作为参数的函数调用,将进行向上转换。
void fr(Brass & rb);    // uses rb.ViewAcct()
void fp(Brass * pb);    // uses pb->ViewAcct()
void fv(Brass b);       // uses b.ViewAcct()
int main()
{
    Brass b("Billy Bee", 123432, 10000.0);
    BrassPlus bp("Betty Beep", 232313, 12345.0);
    fr(b);      // uses Brass::ViewAcct()
    fr(bp);     // uses BrassPlus::ViewAcct()
    fp(b);      // uses Brass::ViewAcct()
    fp(bp);     // uses BrassPlus::ViewAcct()
    fv(b);      // uses Brass::ViewAcct()
    fv(bp);     // uses Brass::ViewAcct()
}

13.4.2 虚成员函数和动态联编

1、为什么有两种类型的联编?为什么默认为静态联编?

  1. 虽然非虚函数的效率比虚函数稍高,但不具备动态联编功能。
  2. 效率:由于静态联编的效率更高,因此被设置为C++的默认选择。Strousstrup说,C++的指导原则之一是,不要为不使用的特性付出代价(内存或者处理时间)
  3. 模型:仅将那些预期将被重新定义的方法声明为虚的。 如果要在派生类中重新定义基类的方法,则将它设置为虚方法;否则,设置为非虚方法。

2、虚函数的工作原理?

编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table,vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。

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

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

13.4.3 有关虚函数注意事项

  1. 在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的。
  2. 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。这种行为非常重要,因为这样基类指针或引用可以指向派生类对象。
  3. 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
  4. **构造函数不能是虚函数。**创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以将类构造函数声明为虚的没什么意义。
  5. **析构函数应当是虚函数,除非类不用做基类。**即使基类不需要显式析构函数提供服务,也不应依赖于默认构造函数,而应提供虚析构函数,即使它不执行任何操作:virtual ~Brass() {}
  6. 友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。如果由于这个原因引起了设计问题,可以通过让友元函数使用虚成员函数来解决。
  7. 如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的。
  8. 重新定义继承的方法并不是重载。如果在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何。
    1. 如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返回类型协变(covariance of return type),因为允许返回类型随类类型的变化而变化。
    2. 如果基类虚函数声明被重载了,则应在派生类中重新定义所有的基类版本。如果只重新定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。

13.5 访问控制:protected

  1. 关键字protected与private相似:在类外只能用公有类成员来访问protected部分中的类成员。
  2. private和protected之间的区别:派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。

13.6 抽象基类(abstract base class,ABC)

有些基类的方法和数据在派生类中用不到,浪费资源。
例如:从Ellipse和Circle类中抽象出它们的共性,将这些特性放到一个ABC中。然后从该ABC派生出Circle和Ellipse类(具体类)。这样,便可以使用基类指针数组同时管理Circle和Ellipse对象,即可以使用多态方法。

  1. 要成为真正的ABC,必须至少包含一个纯虚函数。
  2. 当类声明中包含纯虚函数时,则不能创建该类的对象(不能实例化)。
  3. 包含纯虚函数的类只用作基类。
  4. C++通过使用纯虚函数(pure virtual function)提供未实现的函数。
  5. 纯虚函数声明的结尾处为=0。virtual double Area() = 0;在ABC中可以不定义该函数。

示例:一个ABC例程

例子:首先定义一个名为AcctABC的ABC。这个类包含Brass和BrassPlus类共有的所有方法和数据成员,而那些在BrassPlus类和Brass类中的行为不同的方法应被声明为虚函数。

acctacb.h

// acctabc.h

#ifndef PRIMERPLUS_ACCTABC_H
#define PRIMERPLUS_ACCTABC_H
#include <iostream>
#include <cstring>
using namespace std;

// Abstract Base fullName
class AcctABC
{
private:
    string fullName;    // 姓名
    long acctNum;       // 账户
    double balance;     // 余额
protected:  // 派生类的成员可以直接访问基类的保护成员
    string hobby = "eat";
    struct Formatting
    {
        std::ios_base::fmtflags flag;
        std::streamsize pr;
    };
    const string & FullName() const {return fullName;}
    long AcctNum() const {return acctNum;}
    Formatting SetFormat() const;
    void Restore(Formatting & f) const;
public:
    AcctABC(const string & 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() {}       // 基类的析构函数必须是虚的
};

// Brass Account Class
class Brass : public AcctABC    // Brass是AcctABC的派生类
{
public:
    Brass(const string & s, long an = -1, double bal = 0.0) : AcctABC(s, an, bal) {}
    virtual void Withdraw(double amt);
    virtual void ViewAcct() const;
    virtual ~Brass() {}
};

// Brass Plus Account Class
class BrassPlus : public AcctABC
{
private:
    double maxLoan;     // 透支上限
    double rate;        // 透支贷款利率
    double owesBank;    // 当前的透支总额
public:
    BrassPlus(const string & s = "Nullbody", long an = -1,
              double bal = 0.0, double ml = 500,
              double r = 0.11125);
    BrassPlus(const AcctABC & ba, double ml = 500, double r = 0.11125);
    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; }
    virtual ~BrassPlus() {}
};

#endif //PRIMERPLUS_ACCTABC_H

acctabc.cpp

#include "acctabc.h"

// ABC 基类方法定义

AcctABC::AcctABC(const string & s, long an, double bal)
{
    fullName = s;
    acctNum = an;
    balance = bal;
}

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

void AcctABC::Withdraw(double amt)  // 纯虚函数定义
{
    balance -= amt;
}

// 基类的protected 方法定义
AcctABC::Formatting AcctABC::SetFormat() const
{
    Formatting f;
    f.flag = cout.setf(ios_base::fixed, ios_base::floatfield);
    f.pr = cout.precision(2);
    return f;
}

void AcctABC::Restore(Formatting & f) const
{
    cout.setf(f.flag, ios_base::floatfield);
    cout.precision(f.pr);
}

// 派生类Brass方法定义

void Brass::Withdraw(double amt)
{
    if (amt < 0)
        cout << "Negative withdraw not allowed.\n";
    else if (amt <= Balance())
        AcctABC::Withdraw(amt);
    else
        cout << "money insufficient.\n";
}

void Brass::ViewAcct() const
{
    Formatting f = SetFormat();
    cout << "Brass Client: " << FullName() << endl;
    cout << "Account Number: " << AcctNum() << endl;
    cout << "Balance: $" << Balance() << endl;
    cout << "Hobby: " << hobby << endl;
    Restore(f);
}

// 派生类BrassPlus 的方法定义

BrassPlus::BrassPlus(const string & s, long an, double bal,
                     double ml, double r) : AcctABC(s, an, bal)
{
    maxLoan = ml;
    owesBank = 0.0;
    rate = r;
}

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

void BrassPlus::ViewAcct() const
{
    Formatting f = SetFormat();
    cout << "BrassPlus Client: " << FullName() << endl;
    cout << "Account Number: " << AcctNum() << endl;
    cout << "Balance: $" << Balance() << endl;
    cout << "Maximum loan: $" << maxLoan << endl;
    cout << "Owed to bank: $" << owesBank << endl;
    cout << "Hobby: " << hobby << endl;
    cout.precision(3);
    cout << "Loan Rate: " << 100*rate << "%\n";
    Restore(f);
}

void BrassPlus::Withdraw(double amt)
{
    Formatting f = SetFormat();
    double bal = Balance();     // 调用基类的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 << "Fiance charge: $" << advance * rate << endl;
        Deposit(advance);
        AcctABC::Withdraw(amt);
    }
    else
        cout << "Credit limit exceeded.\n";
    Restore(f);
}

useacctabc.cpp
可以创建指向AcctABC的指针数组。这样,每个元素的类型都相同,但由于使用的是公有继承模型,因此AcctABC指针既可以指向Brass对象,也可以指向BrassPlus对象。 因此,可以使用一个数组来表示多种类型的对象。这就是多态性。

// compile with acctacb.cpp
#include <iostream>
#include <cstring>
#include "acctabc.h"
const int CLIENTS = 4;
int main()
{
    // 可以创建指向AcctABC的指针数组。
    // 这样,每个元素的类型都相同,但由于使用的是公有继承模型,因此AcctABC指针既可以指向Brass对象,也可以指向BrassPlus对象。
    // 因此,可以使用一个数组来表示多种类型的对象。这就是多态性。
    AcctABC * p_clients[CLIENTS];
    std::string temp;
    long tempnum;
    double tempbal;
    char kind;
    for (int i = 0; i < CLIENTS; i++)
    {
        cout << "Enter client's name:";
        getline(cin,temp);
        cout << "Enter client's account number:";
        cin >> tempnum;
        cout << "Enter opening balance: $";
        cin >> tempbal;
        cout << "Enter 1 for Brass Account or 2 for BrassPlus Account (q to quit):";
        while (cin >> kind && (kind != '1' && kind != '2'))
            cout <<"Enter either 1 or 2 (q to quit):";
        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 (int i = 0; i < CLIENTS; i++)
    {
        p_clients[i]->ViewAcct();
        cout << endl;
    }
    for (int i = 0; i < CLIENTS; i++)
    {
        delete p_clients[i]; // free memory
    }
    cout << "Done.\n";
    return 0;
}

13.7 继承和动态内存分配

第一种情况:假设基类使用了动态内存分配,派生类不需执行任何特殊操作,不需要为派生类定义显式析构函数、复制构造函数和赋值运算符。

  1. 复制类成员或继承的类组件时,则是使用该类的复制构造函数完成的。(lacksDMA类的默认复制构造函数使用显式baseDMA复制构造函数来复制lacksDMA对象的baseDMA部分。)
  2. 类的默认赋值运算符将自动使用基类的赋值运算符来对基类组件进行赋值。
  3. 派生类对象的这些属性也适用于本身是对象的类成员。

第二种情况:假设基类和派生类都使用了new,在这种情况下,必须为派生类定义显式析构函数、复制构造函数和赋值运算符。

  1. 派生类析构函数自动调用基类的析构函数,而自身析构函数是对派生类新增指针成员指向内存的释放。
  2. 派生类复制构造函数在初始化成员列表中调用基类的复制构造函数,如果不这样做,将自动调用基类的默认构造函数。基类复制构造函数有一个基类引用参数,而基类引用可以指向派生类型。(baseDMA复制构造函数将使用hasDMA参数的baseDMA部分来构造新对象的baseDMA部分)。
  3. 派生类的显式赋值运算符可以通过作用域解析运算符显式调用基类赋值运算符,完成所有继承的基类对象的赋值。
// 基类使用new
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);
};

// 派生类使用new
class hasDMA :public baseDMA
{
private:
    char * style;   // use new in constructors
public:
    ~hasDMA();
};

baseDMA::~baseDMA() // 基类虚构函数
{
    delete [] label;
}

// 派生类析构函数自动调用基类的析构函数,而自身析构函数是对派生类新增指针成员指向内存的释放。
hasDMA::~hasDMA()   // 派生类虚构函数
{
    delete [] style;
}

// BaseDMA的复制构造函数遵循用于char数组的常规模式,
// 即使用strlen( )来获悉存储C-风格字符串所需的空间、分配足够的内存(字符数加上存储空字符所需的1字节)
// 并使用函数strcpy( )将原始字符串复制到目的地
baseDMA::baseDMA(const baseDMA & rs) // 基类的复制构造函数,基类引用可以指向派生类型
{
    label = new char[std::strlen(rs.label) + 1];
    std::strcpy(label, rs.label);
    rating = rs.rating;
}

// hasDMA复制构造函数只能访问hasDMA的数据,
// 因此它必须调用 baseDMA复制构造函数来处理共享的baseDMA数据
hasDMA::hasDMA(const hasDMA & hs) : baseDMA(hs)     // 派生类的复制构造函数
{
    style = new char[std::strlen(hs.style) + 1];
    std::strcpy(style, hs.style);
}

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

// 派生类的显式赋值运算符可以通过显式调用基类赋值运算符,完成所有继承的基类对象的赋值。
hasDMA & hasDMA::operator=(const hasDMA & hs)       // 派生类的赋值构造函数
{
    if (this == &hs)
        return *this;
    baseDMA::operator=(hs); // copy base portion
    delete [] style;        // prepare for new style
    style = new char[std::strlen(hs.style) + 1];
    std::strcpy(style, hs.style);
    return *this;
}

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

因为友元不是成员函数,所以不能使用作用域解析运算符来指出要使用哪个函数,可以使用强制类型转换(派生类转为基类),以便匹配原型时能够选择正确的函数。
dma.h

#ifndef PRIMERPLUS_DMA_H
#define PRIMERPLUS_DMA_H
#include <iostream>

//  Base Class Using DMA
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);
};

// derived class without DMA
// no destructor needed
// uses implicit copy constructor
// uses implicit assignment operator
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);
};

// derived class with DMA
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 //PRIMERPLUS_DMA_H

dma.cpp

// dma.cpp --dma class methods

#include "dma.h"
#include <cstring>

// baseDMA methods
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
    delete [] style;         // prepare for new style
    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;
}

usedma.cpp

// usedma.cpp -- inheritance, friends, and DMA
// compile with dma.cpp
#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 << "Displaying baseDMA object:\n";
    cout << shirt << endl;
    cout << "Displaying lacksDMA object:\n";
    cout << balloon << endl;
    cout << "Displaying hasDMA object:\n";
    cout << map << endl;
    lacksDMA balloon2(balloon);
    cout << "Result of lacksDMA copy:\n";
    cout << balloon2 << endl;
    hasDMA map2;
    map2 = map;
    cout << "Result of hasDMA assignment:\n";
    cout << map2 << endl;
    // std::cin.get();
    return 0;
}

out:

Displaying baseDMA object:
Label: Portabelly
Rating: 8

Displaying lacksDMA object:
Label: Blimpo
Rating: 4
Color: red

Displaying hasDMA object:
Label: Buffalo Keys
Rating: 5
Style: Mercator

Result of lacksDMA copy:
Label: Blimpo
Rating: 4
Color: red

Result of hasDMA assignment:
Label: Buffalo Keys
Rating: 5
Style: Mercator

13.8 类设计回顾

13.8.1 编译器生成的成员函数

1、默认构造函数: 要么没有参数,要么所有的参数都有默认值。

  • 作用一:创建对象
  • 作用二:调用基类的默认构造函数以及调用本身是对象的成员所属类的默认构造函数。
  • 注意一:如果派生类构造函数的成员初始化列表中没有显式调用基类构造函数,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分。在这种情况下,如果基类没有构造函数,将导致编译阶段错误。
  • 注意二:如果类包含指针成员,则必须使用显示构造函数初始化这些成员。

2、复制构造函数: 接受其所属类的对象作为参数。

何时使用复制构造函数:

  1. 将新对象初始化为一个同类对象;
  2. 按值将对象传递给函数;
  3. 函数按值返回对象;
  4. 编译器生成临时对象。

如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数。
在某些情况下,成员初始化是不合适的。例如,使用new初始化的成员指针通常要求执行深复制,或者类可能包含需要修改的静态变量。

3、默认的赋值运算符 :用于处理同类对象之间的赋值。

  1. 如果语句创建新的对象,则使用初始化;如果语句修改已有对象的值,则是赋值
  2. 如果成员为类对象,则默认成员赋值将使用相应类的赋值运算符,函数返回一个类对象引用。
  3. 如果需要显式定义复制构造函数,则基于相同的原因,也需要显式定义赋值运算符。

13.8.2 其他的类方法

1、构造函数

  • 构造函数不同于其他类方法,因为它创建新的对象,而其他类方法只是被现有的对象调用。这是构造函数不被继承的原因之一
  • 继承意味着派生类对象可以使用基类的方法,然而,构造函数在完成其工作之前,对象并不存在。

2、析构函数

  • 一定要定义显式析构函数来释放类构造函数使用new分配的所有内存,并完成类对象所需的任何特殊的清理工作。
  • 对于基类,即使它不需要析构函数,也应提供一个虚析构函数。

3、转换:只有一个参数

将其他类型转换为类对象:

  1. 使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换。
  2. 在带一个参数的构造函数原型中使用explicit将禁止进行隐式转换,但仍允许显式转换。

将类对象转换为其他类型,应定义转换函数:

  1. 转换函数可以是没有参数的类成员函数,也可以是返回类型被声明为目标类型的类成员函数。
  2. 即使没有声明返回类型,函数也应返回所需的转换值。
  3. 对于某些类,包含转换函数将增加代码的二义性。
  4. C++11支持将关键字explicit用于转换函数。explicit允许使用强制类型转换进行显式转换,但不允许隐式转换。

4、按值传递对象与传递引用

通常,编写使用对象作为参数的函数时,应按引用而不是按值来传递对象。这样做的原因之一是为了提高效率。按值传递对象涉及到生成临时拷贝,即调用复制构造函数,然后调用析构函数。调用这些函数需要时间,复制大型对象比传递引用花费的时间要多得多。如果函数不修改对象,应将参数声明为const引用。
按引用传递对象的另外一个原因是,在继承使用虚函数时,被定义为接受基类引用参数的函数可以接受派生类。

5、返回对象和返回引用

有些类方法返回对象。有时方法必须返回对象,但如果可以不返回对象,则应返回引用。
应返回引用而不是返回对象的的原因在于:返回对象涉及生成返回对象的临时副本,这是调用函数的程序可以使用的副本。因此,返回对象的时间成本包括调用复制构造函数来生成副本所需的时间和调用析构函数删除副本所需的时间。返回引用可节省时间和内存。直接返回对象与按值传递对象相似:它们都生成临时副本。同样,返回引用与按引用传递对象相似:调用和被调用的函数对同一个对象进行操作。
然而,并不总是可以返回引用的原因:函数不能返回在函数中创建的临时对象的引用,因为当函数结束时,临时对象将消失,因此这种引用将是非法的。在这种情况下,应返回对象,以生成一个调用程序可以使用的副本。

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

6、使用const

  • 可以用const来确保方法不修改参数。Star::Star(const char * s) {...}
  • 可以使用const来确保方法不修改调用它的对象。void Star::show() const {...}这里const表示const Star * this,而this指向调用的对象。
  • 可以使用const来确保引用或指针返回的值不能用于修改对象中的数据const Stock & Stock::topval(const Stock & s) const因为this和s都被声明为const,所以函数不能对它们进行修改,这意味着返回的引用也必须被声明为const。
  • 注意:如果函数将参数声明为指向const的引用或指针,则不能将该参数传递给另一个函数,除非后者也确保了参数不会被修改。

13.8.3 公有继承的考虑因素

1、is-a关系

最好的方法可能是创建包含纯虚函数的抽象数据类,并从它派生出其他的类。
无需进行显式类型转换,基类指针就可以指向派生类对象,基类引用可以引用派生类对象。另外,反过来是行不通的,即不能在不进行显式类型转换的情况下,将派生类指针或引用指向基类对象。

2、什么不能被继承

构造函数是不能继承的:

  • 派生类构造函数通常使用成员初始化列表语法来调用基类构造函数,以创建派生对象的基类部分。
  • 如果派生类构造函数没有使用成员初始化列表语法显式调用基类构造函数,将使用基类的默认构造函数。
  • 在继承链中,每个类都可以使用成员初始化列表将信息传递给相邻的基类。
  • C++11新增了一种让您能够继承构造函数的机制,但默认仍不继承构造函数。

析构函数也是不能继承的:

  • 在释放对象时,程序将首先调用派生类的析构函数,然后调用基类的析构函数。
  • 如果基类有默认析构函数,编译器将为派生类生成默认析构函数。
  • 通常,对于基类,其析构函数应设置为虚的。
  • 抽象基类必须至少有一个虚析构函数。

赋值运算符是不能继承的:

  • 派生类继承的方法的特征标与基类完全相同,但赋值运算符的特征标随类而异,这是因为它包含一个类型为其所属类的形参。

3、赋值运算符

  • 如果编译器发现程序将一个对象赋给同一个类的另一个对象,它将自动为这个类提供一个赋值运算符。
  • 这个运算符的默认或隐式版本将采用成员赋值,即将原对象的相应成员赋给目标对象的每个成员。
  • 如果对象属于派生类,编译器将使用基类赋值运算符来处理派生对象中基类部分的赋值。如果显式地为基类提供了赋值运算符,将使用该运算符。
  • 如果成员是另一个类的对象,则对于该成员,将使用其所属类的赋值运算符。
  • 如果类构造函数使用new来初始化指针,则需要提供一个显式赋值运算符。因为对于派生对象的基类部分,C++将使用基类的赋值运算符,所以不需要为派生类重新定义赋值运算符,除非它添加了需要特别留意的数据成员(如指针成员)。
  • 如果派生类使用了new,则必须提供显式赋值运算符。必须给类的每个成员提供赋值运算符,而不仅仅是新成员。其中一条语句调用基类的赋值运算符。
  • 可以将派生对象赋给基类对象,但这只涉及基类的成员。

基类引用能自动引用派生类对象。

派生类引用如何自动引用基类对象:
方法一:转换构造函数:可以接受一个类型为基类的参数和其他参数,条件是其他参数有默认值。即将基类对象转换为派生类对象进行了定义
BrassPlus(const Brass & ba, double ml = 500, double r = 0.1);
方法二:赋值运算符:定义一个用于将基类赋给派生类的赋值运算符。
BrassPlus & BrassPlus ::operator=(const Brass &) {...}

4、私有成员与保护成员

  • 对派生类而言,保护成员类似于公有成员;但对于外部而言,保护成员与私有成员类似。
  • 派生类可以直接访问基类的保护成员,但只能通过基类的成员函数来访问私有成员。
  • 因此,将基类成员设置为私有的可以提高安全性,而将它们设置为保护成员则可简化代码的编写工作,并提高访问速度。
  • 使用私用数据成员比使用保护数据成员更好,但保护方法很有用。

5、虚方法

如果希望派生类能够重新定义方法,则应在基类中将方法定义为虚的,这样可以启用晚期联编(动态联编)

6、析构函数

基类的析构函数应当是虚的。这样,当通过指向对象的基类指针或引用来删除派生对象时,程序将首先调用派生类的析构函数,然后调用基类的析构函数,而不仅仅是调用基类的析构函数。

7、友元函数

由于友元函数并非类成员,因此不能继承。

派生类的友元函数如何使用基类的友元函数:

  • 可以通过强制类型转换将,派生类引用或指针转换为基类引用或指针,然后使用转换后的指针或引用来调用基类的友元函数。
    ostream & operator<<(ostream & os, const hasDMA & hs) { os << (const baseDMA &) hs; return os;}
  • 也可以使用运算符dynamic_cast<>来进行强制类型转换。os << dynamic_cast<const baseDMA &> (hs);

8、有关使用基类方法的说明

  • 以公有方式派生的类的对象可以通过多种方式来使用基类的方法。
  • 派生类对象自动使用继承而来的基类方法,如果派生类没有重新定义该方法。
  • 派生类的构造函数自动调用基类的构造函数。
  • 派生类的构造函数自动调用基类的默认构造函数,如果没有在成员初始化列表中指定其他构造函数。
  • 派生类构造函数显式地调用成员初始化列表中指定的基类构造函数。
  • 派生类方法可以使用作用域解析运算符来调用公有的和受保护的基类方法。
  • 派生类的有元函数可以通过强制类型转换,将派生类引用或指针转换为基类引用或指针,然后使用该引用或指针来调用基类的友元函数。

13.8.4 类函数小结

C++类函数有很多不同的变体,其中有些可以继承,有些不可以。 有些运算符函数既可以是成员函数,也可以是友元,而有些运算符函数只能是成员函数。
其中op=表示诸如+=、*=等格式的赋值运算符。

13.9 总结

  1. 继承通过使用已有的类(基类)定义新的类(派生类),使得能够根据需要修改编程代码。公有继承建立is-a关系,这意味着派生类对象也应该是某种基类对象。作为is-a模型的一部分,派生类继承基类的数据成员和大部分方法,但不继承基类的构造函数、析构函数和赋值运算符。派生类可以直接访问基类的公有成员和保护成员,并能够通过基类的公有方法和保护方法访问基类的私有成员。可以在派生类中新增数据成员和方法,还可以将派生类用作基类,来做进一步的开发。每个派生类都必须有自己的构造函数。程序创建派生类对象时,将首先调用基类的构造函数,然后调用派生类的构造函数;程序删除对象时,将首先调用派生类的析构函数,然后调用基类的析构函数。
  2. 如果要将类用作基类,则可以将成员声明为保护的,而不是私有的,这样,派生类将可以直接访问这些成员。然而,使用私有成员通常可以减少出现编程问题的可能性。如果希望派生类可以重新定义基类的方法,则可以使用关键字virtual将它声明为虚的。这样对于通过指针或引用访问的对象,能够根据对象类型来处理,而不是根据引用或指针的类型来处理。具体地说,基类的析构函数通常应当是虚的。
  3. 可以考虑定义一个ABC:只定义接口,而不涉及实现。例如,可以定义抽象类Shape,然后使用它派生出具体的形状类,如Circle和Square。ABC必须至少包含一个纯虚方法,可以在声明中的分号前面加上=0来声明纯虚方法。
  4. 不一定非得定义纯虚方法。对于包含纯虚成员的类,不能使用它来创建对象。纯虚方法用于定义派生类的通用接口。

13.10 复习题

1.派生类从基类那里继承了什么?
派生类继承了基类大部分的成员函数。基类的保护成员成为派生类的保护成员,可以直接访问。基类的私有成员被继承,但不能直接访问。

2.派生类不能从基类那里继承什么?
不能继承构造函数、析构函数、赋值运算符和友元。

  • 初始化子类时,会先调用父类的构造函数,然后调用子类的构造函数。析构函数则相反。
  • 关于赋值运算符为什么不能被继承:如果派生类中声明的成员与基类相同,那么,基类的成员会被覆盖,哪怕基类的成员与派生类成员的数据类型和参数个数都不相同。
  • 友元函数不是类成员函数。

3.假设baseDMA ::operator=( )函数的返回类型为void,而不是baseDMA &,这将有什么后果?如果返回类型为baseDMA,而不是baseDMA &,又将有什么后果?
如果返回的类型为void,仍可以使用单个赋值,但不能使用连锁赋值;P394
如果方法返回一个对象,而不是引用,则该方法的执行速度将有所减慢,这是因为返回语句需要复制对象。

4.创建和删除派生类对象时,构造函数和析构函数调用的顺序是怎样的?
派生类对象创建时最先调用基类的构造函数,创建基类的对象,然后创建派生类的对象。调用析构函数的顺序正好相反。

5.如果派生类没有添加任何数据成员,它是否需要构造函数?
需要,每个类都必须有自己的构造函数。如果派生类没有添加新成员,则构造函数可以为空,但必须存在。

6.如果基类和派生类定义了同名的方法,当派生类对象调用该方法时,被调用的将是哪个方法?
只调用派生类方法。它取代基类定义。仅当派生类没有重新定义方法或使用作用域解析运算符时,才会调用基类方法。然而,应把将所有要重新定义的函数声明为虚函数。

7.在什么情况下,派生类应定义赋值运算符?
如果派生类构造函数使用new或new[ ]运算符来初始化类的指针成员(动态内存分配),则应定义一个赋值运算符。更普遍地说,如果对于派生类成员来说,默认赋值不正确,则应定义赋值运算符。

8.可以将派生类对象的地址赋给基类指针吗?可以将基类对象的地址赋给派生类指针吗?
可以将派生类对象的地址赋给基类指针;但只有通过显式类型转换,才可以将基类对象的地址赋给派生类指针(向下转换),而使用这样的指针不一定安全。

9.可以将派生类对象赋给基类对象吗?可以将基类对象赋给派生类对象吗?
可以将派生类对象赋给基类对象,对于派生类中新增的数据成员都不会传递给基类对象,然而,程序将使用基类的赋值运算符。
仅当派生类定义了转换运算符(即包含将基类引用作为唯一参数的构造函数)或使用基类为参数的赋值运算符时,相反方向的赋值才是可能的。

10.假设定义了一个函数,它将基类对象的引用作为参数。为什么该函数也可以将派生类对象作为参数?
它可以这样做,因为C++允许基类引用指向从该基类派生而来的任何类型。

11.假设定义了一个函数,它将基类对象作为参数(即函数按值传递基类对象)。为什么该函数也可以将派生类对象作为参数?
按值传递对象将调用复制构造函数。由于形参是基类对象,因此将调用基类的复制构造函数。复制构造函数以基类引用为参数,该引用可以指向作为参数传递的派生对象。最终结果是,将生成一个新的基类对象,其成员对应于派生对象的基类部分。

12.为什么通常按引用传递对象比按值传递对象的效率更高?
按引用(而不是按值)传递对象,这样可以确保函数从虚函数受益。在派生类中重新定义基类的方法,需要使用虚方法。然后,程序将根据对象类型而不是引用(或指针)的类型来选择方法版本。
另外,按引用(而不是按值)传递对象可以节省内存和时间,尤其对于大型对象。按值传递对象的主要优点在于可以保护原始数据,但可以通过将引用作为const类型传递,来达到同样的目的。

13.假设Corporation是基类,PublicCorporation是派生类。再假设这两个类都定义了head( )函数,ph是指向Corporation类型的指针,且被赋给了一个PublicCorporation对象的地址。如果基类将head( )定义为:a.常规非虚方法;b.虚方法; 则ph->head( )将被如何解释?
在派生类中重新定义基类的方法,需要使用虚方法。然后,程序将根据对象类型而不是引用(或指针)的类型来选择方法版本。
如果head( )是一个常规方法,则ph->head( )将调用Corporation::head( );
如果head( )是一个虚函数,则ph->head( )将调用PublicCorporation::head( )。

13.11 编程练习

第一题和第二题

两种C-风格字符串的内存分配(基类使用静态内存分配,派生类使用动态内存分配)

13p1.h


#ifndef PRIMERPLUS_13P1_H
#define PRIMERPLUS_13P1_H
#include <iostream>
using namespace std;

// base class
class Cd    // represents a CD disk
{
private:
    char performers[50];
    char label[20];
    int selections;     // number of selections
    double playtime;    // playing time in minutes
public:
    Cd(const char * s1, const char * s2, int n, double x);
    Cd(const Cd & d);
    Cd();
    virtual ~Cd() {}
    virtual void Report() const; // reports all CD data
    Cd & operator=(const Cd & d);
};

// 派生类
class Classic : public Cd
{
private:
    char *name;
public:
    Classic();  // 默认构造函数
    Classic(const Classic & c);   // 复制构造函数
    Classic(const char *s1, const char *s2, const char *s3, int n, double x);
    ~Classic(){ delete [] name; }
    virtual void Report() const;
    Classic & operator=(const Classic & c);
};

#endif //PRIMERPLUS_13P1_H

13p1.cpp

#include "13p1.h"
#include <cstring>

Cd::Cd(const char * s1, const char * s2, int n, double x)
{
    strncpy(performers, s1, 50);
    if (strlen(s1) >= 50)
        performers[49] = '\0';
    else
        performers[strlen(s1)] = '\0';
    strncpy(label, s2, 20);
    if (strlen(s2) >= 20)
        label[19] = '\0';
    else
        label[strlen(s2)] = '\0';
    selections = n;
    playtime = x;
}

Cd::Cd(const Cd & d)
{
    strcpy(performers, d.performers);
    strcpy(label, d.label);
    selections = d.selections;
    playtime = d.playtime;
}

Cd::Cd()
{
    performers[0] = '\0';
    label[0] = '\0';
    selections = 0;
    playtime = 0.0;
}

void Cd::Report() const
{
    cout << "performers:" << performers << endl;
    cout << "label:" << label << endl;
    cout << "selections:" << selections << endl;
    cout << "playtime:" << playtime << endl;
}

Cd & Cd::operator=(const Cd & d)
{
    if (this == &d)
        return *this;
    strcpy(performers, d.performers);
    strcpy(label, d.label);
    selections = d.selections;
    playtime = d.playtime;
    return *this;
}

// 派生类方法定义
Classic::Classic() : Cd()
{
    name = nullptr;		// 初始化为一个空指针
}
// 复制构造函数
Classic::Classic(const Classic & c) : Cd(c)
{
    name = new char[strlen(c.name) + 1];
    strcpy(name, c.name);
}

Classic::Classic(const char *s1, const char *s2, const char *s3,
                 int n, double x) : Cd(s2, s3, n, x)
{
    name = new char[strlen(s1) + 1];
    strcpy(name, s1);
}

void Classic::Report() const
{
    Cd::Report();
    cout << "name:" << name << endl;	// 这里要注意
}

Classic & Classic::operator=(const Classic & c)
{
    if (this == &c)
        return *this;
    Cd::operator=(c);
    delete [] name;		// 这里要注意
    name = new char[strlen(c.name) + 1];
    strcpy(name, c.name);
    return *this;
}

use13p1.cpp

#include<iostream>
#include"13p1.h"
using namespace std;

void Bravo(const Cd &disk);

int main(void)
{
    Cd c1("Beatles", "Capitol", 14, 35.5);
    Classic c2 = Classic("Piano Sonata in B flat,Fantasia in C", "Alfred Brendel", "Philips", 2, 57.17);

    Cd *pcd = &c1;  // 指针指向基类
    cout << "Using object directly:\n";
    c1.Report();    // 打印基类
    c2.Report();    // 打印派生类
    cout << "Using type cd * pointer to objects:\n";
    pcd->Report();  // 打印基类
    pcd = &c2;      // 指针指向派生类
    pcd->Report();  // 打印派生类

    cout << "Calling a function with a Cd reference argument:\n";
    Bravo(c1);      // 引用和指针一样
    Bravo(c2);

    cout << "Testing assignment:";
    Classic copy;
    copy = c2;      // 使用赋值运算符函数
    copy.Report();  // 打印派生类

    return 0;
}

void Bravo(const Cd & disk)
{
    disk.Report();
}

第三题

13p3.h


#ifndef PRIMERPLUS_13P3_H
#define PRIMERPLUS_13P3_H
#include <iostream>
using namespace std;

class DmaABC
{
private:
    char * label;
    int rating;
public:
    DmaABC(const char * l = "NULL", const int r =0);
    DmaABC(const DmaABC & rs);
    DmaABC &operator=(const DmaABC &rs);
    virtual ~DmaABC();
    virtual void View() const;
    friend ostream &operator<<(ostream &os, const DmaABC &rs);
};

//
class baseDMA : public DmaABC
{
public:
    baseDMA(const char * l = "NULL", int r = 0) : DmaABC(l, r) {}
    baseDMA(const baseDMA & rs) : DmaABC(rs) {}
    virtual void View() const;
    friend ostream &operator<<(ostream &os, const baseDMA &rs);
};

//
class lacksDMA :public DmaABC
{
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 DmaABC & d);
    friend ostream & operator<<(ostream & os, const lacksDMA & ls);
    virtual void View() const;
};

//
class hasDMA :public DmaABC
{
private:
    char * style;
public:
    hasDMA(const char * s = "none", const char * l = "null", int r = 0);
    hasDMA(const char * s, const DmaABC & d);
    hasDMA(const hasDMA & hs);
    ~hasDMA();
    hasDMA & operator=(const hasDMA & hs);
    friend ostream & operator<<(ostream & os, const hasDMA & hs);
    virtual void View() const;
};
#endif

13p3.cpp


#include "13p3.h"
#include <cstring>

// 抽象基类定义
DmaABC::DmaABC(const char * l, const int r)
{
//    label = nullptr;  // 为什么不能定义为空指针,因为有参数了
    label = new char[strlen(l) + 1];
    std::strcpy(label, l);
    rating = 0;
}

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

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

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

void DmaABC::View() const
{
    cout << "label :" << label << endl;
    cout << "rating :" << rating << endl;
}

ostream &operator<<(ostream &os, const DmaABC &d)
{
    os << "label :" << d.label << endl;
    os << "rating :" << d.rating << endl;
//    return os;
}

// baseDMA mothods
void baseDMA::View() const
{
    DmaABC::View();
}

ostream & operator<<(ostream &os, const baseDMA &rs)
{
    os << (const DmaABC &) rs;
//    return os;
}

// lacksDMA methods
lacksDMA::lacksDMA(const char * c, const char * l, int r) : DmaABC(l, r)
{
    strncpy(color, c, COL_LEN-1);
    if (strlen(c) >= (COL_LEN-1))
        color[COL_LEN-1] = '\0';
    else
        color[strlen(c)] = '\0';
}

lacksDMA::lacksDMA(const char * c, const DmaABC & d) : DmaABC(d)
{
    strncpy(color, c, COL_LEN - 1);
    if (strlen(c) >= (COL_LEN-1))
        color[COL_LEN-1] = '\0';
    else
        color[strlen(c)] = '\0';
}

ostream & operator<<(ostream & os, const lacksDMA & ls)
{
    os << (const DmaABC &) ls;
    os << "color :" << ls.color << endl;
//    return os;
}

void lacksDMA::View() const
{
    DmaABC::View();
    cout << "color :" << color << endl;
}


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

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

hasDMA::hasDMA(const hasDMA & hs) : DmaABC(hs)
{
    style = new char[std::strlen(hs.style) + 1];
    strcpy(style, hs.style);
}

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

hasDMA & hasDMA::operator=(const hasDMA & hs)
{
    if (this == &hs)
        return *this;
    DmaABC::operator=(hs);  // copy base portion
    delete [] style;         // prepare for new style
    style = new char[std::strlen(hs.style) + 1];
    std::strcpy(style, hs.style);
    return *this;
}

ostream & operator<<(ostream & os, const hasDMA & hs)
{
    os << (const DmaABC &) hs;
    os << "Style :" << hs.style << endl;
    return os;
}

void hasDMA::View() const
{
    DmaABC::View();
    cout << "Style :" << style << endl;
}

use13p3.cpp

#include <iostream>
#include "13p3.h"
int main()
{
    baseDMA shirt("Portabelly", 8);
    lacksDMA balloon("red", "Blimpo", 4);
    hasDMA map("Mercator", "Buffalo Keys", 5);
    cout << "Displaying baseDMA object:\n";
    cout << shirt << endl;
    cout << "Displaying lacksDMA object:\n";
    cout << balloon << endl;
    cout << "Displaying hasDMA object:\n";
    cout << map << endl;
    lacksDMA balloon2(balloon);
    cout << "Result of lacksDMA copy:\n";
    cout << balloon2 << endl;
    hasDMA map2;
    map2 = map;
    cout << "Result of hasDMA assignment:\n";
    cout << map2 << endl;
    map2.View();
    // std::cin.get();

    return 0;
}

第四题

13p4.h


#ifndef PRIMERPLUS_13P4_H
#define PRIMERPLUS_13P4_H
#include <iostream>
using namespace std;

class Port
{
private:
    char * brand;
    char style[20]; // i.e., tawny, ruby, vintage
    int bottles;
public:
    Port(const char * br = "none", const char * st = "none", int b = 0);
    Port(const Port & p); // copy constructor
    virtual ~Port() { delete [] brand; }
    Port & operator=(const Port & p);
    Port & operator+=(int b); // adds b to bottles
    Port & operator-=(int b); // subtracts b from bottles, if available
    int BottleCount() const { return bottles; }
    virtual void Show() const;
    friend ostream & operator<<(ostream & os, const Port & p);
};

class VintagePort : public Port // style necessarily = "vintage"
{
private:
    char * nickname; // i.e., "The Noble" or "Old Velvet", etc.
    int year; // vintage year
public:
    VintagePort();
    VintagePort(const char * br, const char * st, int b, const char * nn, int y);
    VintagePort(const VintagePort & vp);
    ~VintagePort() { delete [] nickname; }
    VintagePort & operator=(const VintagePort & vp);
    void Show() const;
    friend ostream & operator<<(ostream & os, const VintagePort & vp);
};

#endif //PRIMERPLUS_13P4_H

13p4.cpp

#include "13p4.h"
#include <cstring>

Port::Port(const char * br, const char * st, int b)
{
    brand = new char[strlen(br) + 1];
    strcpy(brand, br);
    strncpy(style, st, 20);
    if (strlen(st) >= 20)
        style[19] = '\0';
    else
        style[strlen(st)] = '\0';
    bottles = b;
}

Port::Port(const Port & p)
{
    brand = new char[strlen(p.brand) + 1];   // 注意复制构造函数中也需要为指针变量开辟内存
    strcpy(brand, p.brand);
    strcpy(style, p.style);
    bottles = p.bottles;
}

Port & Port::operator=(const Port & p)
{
    if (this == &p)
        return *this;
    delete [] brand;
    brand = new char[strlen(p.brand) + 1];
    strcpy(brand, p.brand);
    strcpy(style, p.style);
    bottles = p.bottles;
    return *this;
}

Port & Port::operator+=(int b)
{
    bottles += b;
    return *this;
}

Port & Port::operator-=(int b)  // 这里注意
{
    bottles -= b;
    return *this;
}

void Port::Show() const
{
    cout << "brand :" << brand << endl;
    cout << "style :" << style << endl;
    cout << "bottles :" << bottles << endl;
}

ostream & operator<<(ostream & os, const Port & p)
{
    os << p.brand << ", " << p.style << ", " << p.bottles;    // 这里注意最后的分号
    return os;
}


// 派生类
VintagePort::VintagePort() : Port()
{
    nickname = nullptr;
    year = 0;
}

VintagePort::VintagePort(const char * br, const char * st, int b, const char * nn, int y) : Port(br, st, b)
{
    nickname = new char[strlen(nn) + 1];
    strcpy(nickname, nn);
    year = y;
}

VintagePort::VintagePort(const VintagePort & vp) : Port(vp)
{
    nickname = new char[strlen(vp.nickname) + 1];
    strcpy(nickname, vp.nickname);
    year = vp.year;
}

VintagePort & VintagePort::operator=(const VintagePort & vp)
{
    if (this == &vp)
        return *this;
    delete [] nickname;
    Port::operator=(vp);
    nickname = new char[strlen(vp.nickname) + 1];
    strcpy(nickname, vp.nickname);
    year = vp.year;
    return *this;
}

void VintagePort::Show() const
{
    Port::Show();
    cout << "nickname :" << nickname << endl;
    cout << "year :" << year << endl;
}

ostream & operator<<(ostream & os, const VintagePort & vp)
{
    os << (const Port &)vp;
    os << ", " << vp.nickname << ", " << vp.year;
    return os;  // 这里注意
}

use13p4.cpp

#include "13p4.h"

int main(void)
{
    Port port1("port1", "port111", 100);
    cout << port1 << endl;

    VintagePort vp1("vp1", "vp111", 100, "VintagePort1", 10);
    cout << vp1 << endl;

    VintagePort vp2 = vp1;
    cout << vp2 << endl << endl;

    Port *p_port;

    p_port = &port1;
    p_port->Show();
    cout << endl;
    p_port = &vp1;
    p_port->Show();
    return 0;
}

;