c++学习笔记——类继承

从一个类中派生出另一个类,原始类被称为基类,继承类被称为派生类。下面以一个简单的基类说明下两者怎么操作

一个简单的基类

首先基类的头文件:

该类要实现的功能是记录会员的姓名已经是否有球桌。

// tabtenn0.h -- a table-tennis base class
#ifndef TABTENN0_H_
#define 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

实现细节:

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

这里的构造函数使用了成员初始化列表语法,回顾构造函数初始化列表的规则:

(1)构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表;

(2)每个数据成员后面跟着一个放在括号中的初始化式。构造函数TableTennisPlayer的三个形参fn、ln和ht接受传进来的三个参数,然后调用string类的复制构造函数将firstname初始化成fn,将lastname初始化成ln,再将hasTable初始化成ht。

并且使用string类来存储姓名,这比数组更加灵活安全。当然,构造函数也可以像下面这样:

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

这种构造函数的实现过程是:首先为firstname调用string的默认构造函数,再调用string的赋值运算符将firstname设置为fn。列表初始化语法少了赋值的步骤。

主函数文件(程序13.3)

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

在该程序实例化对象时将c-风格字符串作为参数,也就是语句TableTennisPlayer player1("Chuck", "Blizzard", true);但是构造函数中参数类型声明为const string &,这看起来会导致类型不匹配,其实在标准string类中有一个将const char * 作为参数的构造函数,使得风格的字符串可以初始化成string对象时,自动调用这个构造函数。

总而言之,可以将string对象或者c风格字符串作为构造函数TableTennisplayer的参数,将前者作为参数时,将调用接受const string &作为参数string构造函数,而将后者作为参数时,将调用接受const char * 作为参数的string构造函数。

从简单的基类中派生出一个类

派生类功能:包含成员在比赛中的比分,假定派生类的名称是RatedPlayer

基本格式为:

class RatedPlayer : public TableTennisPlayer
{
    ……
}

解读:类头中的冒号指出RatedPlayer类的基类是TableTennisPlayer类,上述特殊的声明头表明TableTennisPlayer是一个公有基类,这被称为公有派生

(1)派生类对象包含基类对象;

(2)使用公有派生,基类的公有成员将成为派生类的公有成员;

(3)基类的私有部分也将成为派生类的一部分,但是只能通过基类的公有和保护方法访问。

派生类RatePlayer的特征:

(1)派生类对象存储了基类的数据成员(派生类继承了基类的实现)

(2)派生类对象可以使用基类的方法(派生类继承了基类的接口)

基类和派生类对象图解

派生类需要在继承特性中添加的东西:(1)派生类自己的构造函数;(2)派生类根据自己的需要添加的额外的数据成员和成员函数。

例如:在RatedPlayer派生类中实现使用另一个数据rating来记录得分以及检索和重置比分地方法,那么派生类的类声明如下:

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

其中有两个构造函数,第一个构造函数中每个成员对应于一个形参(包括新成员rating),第二个构造函数使用一个TableTennisPlayer参数,该参数包含firstname、lastname和hasTable。

构造函数的访问权限

派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。也就是说派生类不能直接设置继承的成员,也就是说此处的RatedPlayer不能直接设置继承的成员(firstname、lastname以及hasTable)而必须使用基类的公有方法来访问私有的基类成员具体而言,就是派生类构造函数必须使用基类构造函数。

创建派生类对象时,必须首先创建基类对象,c++使用成员初始化列表语法来完成这种工作。第一个构造函数的代码如下:

RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
     const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)
//派生构造函数必须包含基类构造函数,因为派生类不能直接访问基类的私有成员,
//必须通过基类的公有方法来访问私有的基类成员
{
    rating = r;
}

其中:TableTennisPlayer(fn, ln, ht)是成员初始化列表,它是可执行代码,可以调用TableTennisPlayer构造函数。例如当执行下面这个语句时

RatedPlayer rplayer1(1140, "Mallory", "Duck", true);

则RatedPlayer构造函数将把实参"Mallory"、'"Duck"、true赋给形参fn、ln和ht。然后把这些参数作为实参传递给TableTennisPlayer构造函数创建一个嵌套TableTennisPlayer对象,并将数据实参"Mallory"、'"Duck"、true存储在该对象中。然后程序进入到RealPlayer构造函数体,完成RealPlayer对象的创建,并将参数r的值(1140)赋给rating成员。

如果上面的构造函数中省略了成员初始化列表,也就是派生类RatedPlayer声明中的第一个构造函数:

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

程序将调用默认的基类构造函数,所以和下面的代码等效:

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

除非意图就是想要使用默认构造函数,可以省略该语句,否则都应显式地调用正确的基类构造函数。

 第二个构造函数

接下来看下派生类RatedPlayer中的第二个构造函数,函数定义下:

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

这个构造函数也将TableTennisPlayer的信息传递给了TableTennisPlayer构造函数(TableTennisPlayer(tp)),由于tp的类型是TableTennisPlayer &,所以将调用基类的复制构造函数。如果基类没有复制构造函数,编译器就将自动生成一个。在该类中没有使用动态内存分配的成员,否则使用默认复制构造函数就会出现多个指针指向同一个内存的情况。也可以对派生类使用成员初始化列表语法。在列表中使用派生类的成员名,就像下面这样:

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

总结一下派生类构造函数的要点如下:

(1)首先创建基类对象

(2)派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;

(3)派生类构造函数应初始化派生类新增的数据成员。

没有提供显式构造函数(显式构造函数是构造函数钱有关键字explicit的函数)时就会使用隐式构造函数。释放对象的顺序和创建对象的顺序相反,即应该执行派生类的析构函数,再自动调用基类的析构函数。

注意:在创建派生类对象时,程序会首先调用基类的构造函数,负责初始化继承的数据成员,然后再调用派生类构造函数,用来初始化新增的数据成员。

使用派生类

要使用派生类,程序就必须能够访问基类声明。这里将两个类的声明至于同一个头文件中,也可以将每个类分别放在独立的头文件中,但是由于这两个类是相关的,所以它们放在同一个头文件中更加合适。

程序13.4(包含基类和派生类的声明头文件)

// tabtenn1.h -- a table-tennis base class
#ifndef TABTENN1_H_
#define 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

程序13.5(实现文件)

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

程序13.6(主函数文件)

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

在主函数文件中,player1是TableTennisPlayer类对象,rplayer1是RatedPlayer类对象,

语句rplayer1.Name();以及player1.HasTable()是派生类对象调用基类方法的格式。

输出结果如下:

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

派生类和基类之间的关系

(1)派生类对象可以使用基类的方法,条件是方法不是私有的

(2)基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象。(c++中规定指针和引用类型与赋给的类型匹配,但是这一个规则对继承是例外,但是这个例外是单向的,也就说派生类引用不能引用基类对象、派生类指针不能指向基类对象)这样做的原因是允许基类对象引用隐式地引用派生类对象,则可以使用基类引用为派生类对象调用基类方法,并且派生类继承了基类的方法,这样做不会出现问题。相反,如果允许将派生类对象引用隐式地引用基类对象,会出现问题,并且是无意义的,因为有的派生类方法基类中并没有。

例如下面:   

RatePlayer rplayer1(1140, "Mallory", "Duck", true);
TableTennisPlayer & rt = rplayer;//引用
TableTennisPlayer * pt = &rplayer;//指针
TableTennisPlayer player("Betsy" , "Bloop", true);
RatedPlayer & rr = player;//不允许,派生类引用不能引用基类对象
RatedPlayer * pr = &player;//不允许,派生类指针不能指向基类对象
rt.Name();
pt->Name();

但是需要注意的是:基类指针或者引用只能用于调用基类方法,也就是说rt和pt不能用来调用派生类的方法。

如果基类引用和指针可以指向派生类对象,那么基类引用定义的函数或指针参数可用于基类对象或者派生类对象。例如:

void Show(const TableTennisPlayer & rt)
{
    using std::cout;
    cout << "Name";
    rt.Name();
    cout << "\nTable:";
    if(rt.HasTable())
        cout << "yes.\n";
    else
        cout << "no.\n";
}
TableTennisPlayer player("Tara", "Boomda", false);
RatedPlayer rplayer(1140, "Mallory", "Duck", true);
Show(player);
Show(rplayer);

其中形参rt是一个基类引用,它可以指向基类对象或者派生类对象,所以可以在Show()中使用TableTennisPlayer参数(player)或者是RatedPlayer参数(rplayer)。指针类似。

1、这样的引用兼容性属性可以将基类对象初始化成派生类对象,例如:

RatedPlayer olaf1(1840, "Olaf", "Loaf", true);
TableTennisPlayer olaf2(olaf1);

其中第二个语句,要想初始化TableTennisPlayer对象olaf2,就需要有一个像TableTennisPlayer(const RatedPlayer &)这样的构造函数,但是在类声明中并没有这样的构造函数,引用兼容性使得基类引用可以引用派生类对象,所以使用隐式复制构造函数TableTennisPlayer(const TableTennisPlayer &),它的形参是基类引用,可以引用派生类。将olaf2初始化成olaf1时,将要使用该构造函数,它复制firstname、lastname和hasTable成员。

2、这样的引用兼容还可以使得派生类对象赋值给基类对象,例如:zh

RatedPlayer olaf1(1840, "olaf", "Loaf", true);
TableTennisPlayer winner;
winner = olaf1;

这时会使用隐式重载赋值运算符(基类引用指向的是派生引用)

TableTennisPlayer & operator = (const TableTennisPlayer &) const;

 继承:is-a关系

c++中有三种派生关系:(1)公有继承;(2)保护继承;(3)私有继承。

其中公有继承是最常用的方式,它建立一个is-a关系(这种关系是is-a-kind-of[是一种],简称为is-a),即派生类对象也是一个基类。新类将继承原始类的所有成员,派生类可以添加特性。

公有继承不建立has-a关系,has-a的关系就想午餐可能包含水果,但是一般午餐并不是水果,所以不能从Fruit类派生出Lunch类,“午餐” has a “水果”是一种has-a关系(午餐有水果),“午餐” is-a “水果”(午餐是水果)就不对了。

公有继承不能建立is-like-a关系, 继承可以在基类基础上添加属性,但是不能删除基类属性。这种情况下,可以设计一个包含共有特征的类,然后以is-a或者has-a关系,在这个类的基础上定义相关的类。

公有继承不建立is-implemented-as-a(作为……来实现)关系。例如使用数组来实现栈,但是从数组中派生出栈类是不合适的,因为栈不是数组。

公有继承不建立uses-a关系。例如,计算机可以使用激光打印机,但是从计算机中派生出打印机类是无意义的。

也就是说在c++中完全可以使用公有继承来建立has-a、is-implemented-as-a关系或者uses-a的关系;但是通常会导致一些编程问题。

多态公有继承

多态作用:可以使同一个方法在派生类和基类中的行为是不同的,也就是方法的行为取决于调用该方法的对象,这种复杂的行为成为多态——具有多种形态。

实现多态的方法

(1)在派生类中重新定义基类的方法;

(2)使用虚方法

下面用一个例子说明下具体的操作步骤:

首先定义一个基本支票账户——Brass Account(类名为Brass),它主要包含客户姓名、账户以及当前结余等数据信息,可以实现创建账户、存款、取款以及显示账户信息等操作。派生类Brass Plus(类名是BrassPlus),它增加了透支保护特性,增加了透支上限、透支贷款利率以及当前的透支总额等数据,对于这个派生类,不增加新的操作,但对于取款操作,必须考虑透支保护,显式操作必须显示Brass Plus账户的其他信息。

首先包含两个类的头文件:

// brass.h  -- bank account classes
#ifndef BRASS_H_
#define BRASS_H_
#include <string>
// Brass Account Class
class Brass
{
private:
    std::string fullName;
    long acctNum;
    double balance;
public:
    Brass(const std::string & 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() {}
};

//Brass Plus Account Class
class BrassPlus : public Brass
{
private:
    double maxLoan;
    double rate;
    double owesBank;
public:
    BrassPlus(const std::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

第一点:

(1)首先BrassPlus类在Brass类的基础上添加了3个私有成员和3个公有成员函数。

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

当一个方法在基类和派生类中行为不同时,基类版本的限定名是Brass::ViewAcct(),派生类版本的限定名是BrassPlus::ViewAcct()。程序会更好根据对象类型来使用那个版本。但是对于行为相同的方法,只能在基类中声明。

(3)Brass类在声明ViewAcct()和Withdraw()都使用了关键字virtual。这些方法被称为虚方法(virtual method)。在基类中将方法声明为虚方法后,它在派生类中自动为虚方法,但是再派生类中使用该关键字来指出那些方法时虚方法更好。

如果没有使用virtual,程序将根据引用类型或者指针类型选择方法。如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。

没有使用关键字virtual,情况如下:

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();
b2_ref.ViewAcct();

上面程序dom被声明为Brass类对象,dot是BrassPlus是BrassPlus对象,两个对象都被赋值给Brass类引用,也就是b1_ref和b2_ref。所以两个引用调用方法ViewAcct()时,都选择了Brass::ViewAcct();也就是1上面说的没有使用关键字Virtual时,会根据引用或者指针的类型来选择方法。

使用关键字virtual,情况如下:

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();
b2_ref.ViewAcct();

虽然两个引用都是Brass类型,但是b2_ref引用是一个BrassPlus对象dot,所以使用的是BrassPlus::ViewAcct();

如果没有使用关键字virtual,程序将根据引用或指针类型选择方法;如果使用了virtual,程序将根据引用或者指针指向的对象的类型(调用方法的对象时派生类还是基类)来选择方法。

类实现文件(程序13.8)

// brass.cpp -- bank account class methods
#include <iostream>
#include "brass.h"
using std::cout;
using std::endl;
using std::string;

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

// Brass methods

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; "
             << "deposit is cancelled.\n";
    else
        balance += amt;
}

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

    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";
    restore(initialState, prec);
}
double Brass::Balance() const
{
    return balance;
}

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

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

// redefine how ViewAcct() works
void BrassPlus::ViewAcct() const
{
    // set up ###.## format
    format initialState = setFormat();
    precis prec = cout.precision(2);

    Brass::ViewAcct();   // display base portion
    cout << "Maximum loan: $" << maxLoan << endl;
    cout << "Owed to bank: $" << owesBank << endl;
    cout.precision(3);  // ###.### format
    cout << "Loan Rate: " << 100 * rate << "%\n";
    restore(initialState, prec); 
}

// redefine how Withdraw() works
void BrassPlus::Withdraw(double amt)
{
    // set up ###.## format
    format initialState = setFormat();
    precis prec = 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";
    restore(initialState, prec); 
}

format setFormat()
{
    // set up ###.## format
    return cout.setf(std::ios_base::fixed, 
                std::ios_base::floatfield);
} 

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

从类实现文件中可以看出派生类并不能直接访问基类的私有数据,而必须使用基类的公有方法才能访问这些数据。访问的方法取决于方法,构造函数使用的是一种技术,而其他成员函数使用的是另一种方法。

在构造函数中:

派生类构造函数在初始化私有数据时,采用的是成员初始化列表语法,就像前面的RatedPlayer一样。原理就是使用成员初始化类表语法,将基类信息传递给基类构造函数,然后使用构造函数体初始化BrassPlus类新增的数据项。

在非构造函数:

例如在BrassPlus版本的ViewAcct方法实现如下:

void BrassPlus::ViewAcct() const
{
    format initialState = setFormat();
    precis prec = cout.precision(2);

    Brass::ViewAcct();   // display base portion
    cout << "Maximum loan: $" << maxLoan << endl;
    cout << "Owed to bank: $" << owesBank << endl;
    cout.precision(3);  // ###.### format
    cout << "Loan Rate: " << 100 * rate << "%\n";
    restore(initialState, prec); 
}

语句  Brass::ViewAcct(); 用来显示基类数据成员,在派生类中使用作用域解析运算符来调用基类方法。这里就是调用了基类方法Brass::ViewAcct()来显示基类成员函数。注意这里必须使用作用域解析运算符,否则编译器将认定ViewAcct()是BrassPlus::ViewAcct(),就相当于创建了一个不会终止的递归函数。

但是对于在派生类中没有重新定义的方法,代码就不必须使用作用域解析运算符。例如BrassPlus::Withdraw()方法实现

void BrassPlus::Withdraw(double amt)
{
    // set up ###.## format
    format initialState = setFormat();
    precis prec = 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";
    restore(initialState, prec); 
}

 方法实现中语句double bal = Balance();使用了基类中方法Balance(),因为在基类中没有重新定义该方法,代码不必对Balance()使用作用域解析运算符。

另外,在实现文件定义了格式式化方法setf()、precision()将浮点数的输出模式设置为定点,即包含两位小数。避免代码重复,将设置格式代码放在辅助函数中,如下。在方法ViewAcct()和Withdraw使用了该格式,输出的模式将不变,因此该方法将格式模式重置为调用前的状态。

函数setFormat()设置定点表示:

方法并返回以前的标记位置:

format setFormat()
{
    // set up ###.## format
    return cout.setf(std::ios_base::fixed, 
                std::ios_base::floatfield);
} 

函数restore()重置格式和精度:

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

 主函数文件(使用Brass类和BrassPlus类)程序13.9

// usebrass1.cpp -- testing bank account classes
// compile with brass.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();
	// std::cin.get();
    return 0; 
}

输出为:

Client: Porcelot Pigg
Account Number: 381299
Balance: $4000.00

Client: Horatio Hogg
Account Number: 382288
Balance: $3000.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan Rate: 11.125%

Depositing $1000 into the Hogg Account:
New balance: $4000
Withdrawing $4200 from the Pigg Account:
Withdrawal amount of $4200.00 exceeds your balance.
Withdrawal canceled.
Pigg account balance: $4000
Withdrawing $4200 from the Hogg Account:
Bank advance: $200.00
Finance charge: $22.25
Client: Horatio Hogg
Account Number: 382288
Balance: $0.00
Maximum loan: $500.00
Owed to bank: $222.25
Loan Rate: 11.125%

程序13.9中没有使用虚方法特性,程序13.10的主函数文件中演示了虚方法的作用:想要一个数组同时包含基类Brass对象和派生类对象BrassPlus对象,在c++中是不能实现的,因为数组的所有元素类型都必须相同。然而,可以先创建指向Brass对象的指针数组,这样数组中国每个元素都相同,由于公有继承模型允许Brass对象指针指向Brass对象也可以指向BrassPlus对象,所以可以使用一个数组来表示多种类型的对象,这个就是多态性。

主函数文件(程序13.10)

// usebrass2.cpp -- polymorphic example
// compile with brass.cpp
#include <iostream>
#include <string>
#include "brass.h"
const int CLIENTS = 4;

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

   Brass * 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: ";
       while (cin >> kind && (kind != '1' && kind != '2'))
           cout <<"Enter either 1 or 2: ";
       if (kind == '1')
           p_clients[i] = new Brass(temp, tempnum, tempbal);
       else
       {
           double tmax, trate;
           cout << "Enter the overdraft limit: $";
           cin >> tmax;
           cout << "Enter the interest rate "
                << "as a decimal fraction: ";
           cin >> trate;
           p_clients[i] = new BrassPlus(temp, tempnum, tempbal,
                                        tmax, trate);
        }
        while (cin.get() != '\n')
            continue;
   }
   cout << endl;
   for (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";         
 /* code to keep window open 
   if (!cin)
      cin.clear();
   while (cin.get() != '\n')
      continue;
*/
   return 0; 
}

如果数组成员指向的是Brass对象,则调用Brass::ViewAcct();如果指向的是BrassPlus对象,则调用BrassPlus::ViewAcct()。

输出结果如下:

Enter client's name: yang
Enter client's account number: 112233
Enter opening balance: $1500
Enter 1 for Brass Account or 2 for BrassPlus Account: 1
Enter client's name: guo
Enter client's account number: 121213
Enter opening balance: $1800
Enter 1 for Brass Account or 2 for BrassPlus Account: 2
Enter the overdraft limit: $350
Enter the interest rate as a decimal fraction: 0.12
Enter client's name: zhou
Enter client's account number: 212118
Enter opening balance: $5200
Enter 1 for Brass Account or 2 for BrassPlus Account: 0.10
Enter either 1 or 2: Enter either 1 or 2: Enter client's name: tim
Enter client's account number:233255
Enter opening balance: $688
Enter 1 for Brass Account or 2 for BrassPlus Account: 1

Client: yang
Account Number: 112233
Balance: $1500.00

Client: guo
Account Number: 121213
Balance: $1800.00
Maximum loan: $350.00
Owed to bank: $0.00
Loan Rate: 12.000%

Client: zhou
Account Number: 212118
Balance: $5200.00

Client: tim
Account Number: 233255
Balance: $688.00

Done.

(4)Brass基类还声明一个虚析构函数,虽然该析构函数不执行任何操作。这样做的原因是确保释放派生类对象时,按正确的顺序调用析构函数,一般都需要在基类中这样声明一个这样的函数,这是一个惯例。

如果折钩函数不是虚的,则将只调用对应于指针类型的析构函数,这就意味着程序13.10中只有Brass的析构函数被调用,即使指针指向的是BrassPlus对象。如果析构函数是虚的,将调用相应对象类型的析构函数。也就是说析构函数是虚的话,如果指向的是Brass'Plus对象,将调用BrassPlus的析构函数,然后自动调用基类的析构函数。

静态联编和动态联编

在编译过程中进行联编称为静态联编,又称为早期联编。但是虚函数使得这项工作变得负责,因为编译器不知道用户选择的是哪一种类型的函数,使用哪一个函数是不确定的。所以编译器必须生成能够在程序运行时选择正确的虚方法的代码,这就是动态联编,又称为晚期联编。

指针和引用类型的兼容性

 向上强制转换:将派生类指针或者引用转换成基类引用或者指针,不需要显式类型转换(基类指针或者引用=派生类指针或者引用)这是is-a规则的一部分,它是隐式转换的,向上强制转换是可传递的,也就是说如果从BrassPlus类中派生出BrassPlusPlus类,则Brass指针或者引用可以引用Brass对象、BrassPlus对象或者BrassPlusPlus对象。隐式向上强制转换使用基类指针或者应用可以指向基类对象或者派生类对象,所以需要动态联编。c++使用虚函数来满足这种需求。

向下强制转换:将基类指针或者引用转换成派生类指针或者引用(派生类指针或者引用=基类指针或者引用),向下强制转换是需要显式类型转换,原因就是is-a关系是不可逆的,派生类可以新增数据成员,但是这些数据成员不能应用于基类。

虚成员函数和动态联编

首先回顾下静态联编的知识,如下,如果方法ViewAcct()没有被声明成虚的,则bp->ViewAcct()将根据bp指针的类型(Brass*)调用的是Brass::ViewAcct(),编译器对非虚方法使用静态联编。

 然后动态联编和虚方法:如果方法ViewAcct()方法被声明为虚的,则bp->ViewAcct()将根据bp指针指向的对象ophelia(BrassPlus类型)调用的是BrassPlus::ViewAcct()。在这个简单的例子中知道对象的类型是BrassPlus类型,但是再程序13.10这种稍微复杂的例子在编译阶段并不能知道用户输入的是哪一个类型的对象,只有在运行程序时才能确定,总之,编译器对于虚方法使用动态联编。

动态联编和静态联编的优缺点:

首先明确程序默认的静态联编,原因是若将动态联编设置成默认,为了实现动态联编的功能就必须采用一些措施来跟踪基类指针或者引用指向的对象类型,这会增加额外的开销,如果没有在派生类中重新定义基类中的方法,就没有必要增加这部分开销。所以默认情况下是静态联编更加合理,效率更高。c++的原则就是不要为不使用的特性付出时间或者内存等等,所以仅仅当程序需要虚函数时,才使用它们。如果要在派生类中重新定义基类的方法,则将它设置成虚方法,否则设置为非虚。

有关虚函数的注意事项:

(1)在基类方法声明中使用关键字virtual可使该方法在基类以及所有的派生类中是虚的

(2)如果使用指向对象的引用或者指针来调用虚函数,程序会根据使用为该引用或者指针引用或者指向的对象的类型定义的方法,这称为动态联编或者晚期联编。

(3)在基类中应该把需要在派生类中重新定义的方法都声明成虚的。

1.构造函数:构造函数不能是虚的,因为在创建派生类对象时应调用派生类自己的构造函数,然后再调用基类的一个构造函数。没有重新定义基类的构造函数,将构造函数声明为虚的无意义。

2.析构函数:当一个类被用作基类时,它的析构函数应该是虚的,解释如下:

例如,Employee是基类,Singer是派生类,在派生类中添加一个char*成员,该成员指向由new'分配的内存。当singer对象过期时,必须调用~Singer来释放内存。

Employee * pe = new Singer;//声明了一个基类Employee的指针,指向派生类的内存
……
delete pe;

如果虚构函数不是虚的,也就使用默认的静态联编,那语句delete pe;将调用~·Employee()析构函数来释放内存(因为指针pe是Employee类型的指针),如果基类Employee的析构函数是虚的,那将先调用~Singer()析构函数释放由Singer组件指向的内存,然后调用~Employee()来释放由Employee组件指向的内存。

即使这个类不作为基类,将其析构函数声明为虚的也不是错误,一般情况下给基类一个虚析构函数,即使它并不需要。

3.友元

友元不能是虚函数,因为友元函数不是成员函数,只有成员函数才能是虚函数。所以原先学习的友元函数在派生类中可能有有一些问题,但是可以让友元函数使用虚函数来解决。

4.没有重新定义

对于没有在派生类中重新定义的成员函数,将使是该函数的积累版本,如果派生类位于派生链中,将使用最新的虚函数版本,例外的情况是基类版本是隐藏的。

5.重新定义将隐藏方法

隐藏基类版本的情况如下:

 像上面的代码中在基类中将函数showperks()重新定义了一个不接受任何参数的函数,这种定义方式不会生成两个重载版本,而是隐藏了一个接受int参数的基类版本。所以重新定义继承的方法并不是重载,在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,无论参数特征标如何。如下:

基类中的接受int参数的函数版本被隐藏,所以第三条语句不合法。 

有两条经验规则需要注意:

(1)如果在派生类中重新定义继承的方法,应该确保与原来的原型完全相同,但返回类型是基类的引用或者指针,可以将其修改为指向派生类的引用或者指针。这种特性被称为返回类型协变,因为允许返回类型随类类型的变化而变化,这种特性只适合返回值,不适用于参数,如下:

如果在基类声明中函数被重载(这里在基类中将函数showperks重载了三个版本,注意上面接受的在基类中“重载”(隐藏)情况),则应该在派生类中重新定义所有的基类版本。 如下:

如果只在派生类中重新定义一个版本,则另外两个版本就被隐藏,派生类对象将无法使用它。 如果不需要修改,则新定义可只调用基类版本。

访问控制:protected

 前面介绍了关键字public和private来控制对类成员的访问。现在介绍关键字protected(类的私有成员):在类外只能用公有类成员来访问protected部分中的类成员,这一点和关键字private相似。

关键字private和关键字protected的区别主要是:派生类的成员可以直接访问基类的保护成员,但是不能直接访问基类的私有成员。

综上,对于外部世界来说,保护成员的行为与私有成员相似,但是对于派生类来说,保护成员和公有成员类似。

使用保护成员可以简化代码的编写工作,但存在设计缺陷,最好还是对类数据成员采用私有访问控制,不要使用保护访问控制,同时通过基类方法使得派生类能够访问基类数据。但是对于成员函数来说,保护访问控制很有用,它让派生类能够访问公众不能使用的内部函数。

抽象基类(ABC)

适用的情况举例:基类是图形类,派生类为椭圆类,椭圆又派生出圆类。在椭圆的数据成员有中心的坐标,长轴、短轴以及方向角等,另外,也许还有一些可以返回椭圆面积的、旋转图形及缩放图形的方法。圆是椭圆的一种特殊情况,但是从椭圆中派生出圆是笨拙的,因为椭圆类中的一些数据成员比如说短轴和长轴对圆来说是没有意义的,旋转图形的方法对圆也是没有意义。继承椭圆会造成信息冗余。不如直接定义一个原理,不走继承。

除了重新定义一个圆类,还有一种方法是从椭圆类和圆类中抽象出椭圆和圆的共同特性,将这些特性放在一个抽象基类(ABC)中,然后再从该抽象基类(这里基类命名为BaseEllipse)中分别派生出椭圆类(Ellipse)和圆类(Circle)。这样可以使用基类指针数组同时管理圆类和椭圆类。

现在具体数理一下椭圆和圆这两个类:

共同的成员:中心坐标,Move(移动)方法和Area(面积)方法

共同的数据成员中心坐标,方法Move中的实现细节可能会有所以不同(但可以肯定的是需要使用ABC中的数据成员坐标),但面积的计算方法圆和椭圆完全不同,圆需要的半径以及椭圆需要的半轴等数据在基类中并没有对应的数据成员。所以在基类中不能实现Area方法,此时可以将Area方法定义成纯虚方法。c++通过使用纯虚方法提供未实现的函数,纯虚函数声明的结尾处为=0,详见下面:

注意 :当类声明中包含纯虚方法时,不能创建该类的对象。换句换说,就是包含纯虚函数的类只能用作基类。要成为ABC,就必须至少包含一个纯虚函数。

这里Area()方法原型最末的=0表明该方法是一个纯虚函数,这个函数在实现文件中是没有办法定义的(缺少必要的数据),它只有一个函数原型,允许在实现文件中不定义它。在c++中也可以使得有定义的函数成为纯虚函数。例如,使得Move方法变成纯虚函数:

函数原型是 void Move(int nx, ny) = 0;

然后在实现文件中提供它的定义方法:void BaseEllipse::Move(int nx, ny) {x = nx, y = ny;}

最后,使用这些类的程序可以创建除Ellipse对象和Circle对象,但是不能创建BaseEllipse基类对象。由于Circle对象和Ellipse对象的基类相同,可以使用BaseEllipse指针数组同时管理这两种对象。Circle类和Ellipse类被称为具体类。

综上,ABC描述的是至少使用一个纯虚函数的接口,从ABC派生出的类将根据派生类的具体特征,使用常规虚函数来实现这种接口。

继承和动态内存分配

当基类使用动态内存分配,并重新定义复制和赋值构造函数,会影响到派生类的实现,影响方式取决于派生类的属性。

第一种:派生类不使用new

析构函数:如果派生类没有使用动态内存分配, 也没有包含一些不常用、需要特殊处理的设计特性,则不需要为派生类定义显式析构函数。

复制构造函数:首先回顾默认复制构造函数执行成员复制,这对于动态内存分配来说是不合适的,(前面介绍过会出现一些问题,出现多个指针指向同一个内存的情况等),但是对于没有使用new的派生类是合适的。派生类的默认复制构造函数使用基类显式的复制构造函数来复制派生类对象的基类部分。同时派生类中有没有使用new,所以基类不需要显式地定义复制构造函数,基类使用默认的复制构造函数进行成员复制即可。

是否需要重载赋值运算符:与复制1构造函数一样,类的默认赋值运算符将自动使用基类的赋值运算符来对基类组件进行赋值。而在派生类中并没有使用new,对于在派生类中新的成员进行常规赋值是可以的,所有结论就是不需要在派生类中显式的重载赋值运算符。

第二种情况:派生类使用了new

这种情况下就必须为派生类定义析构函数、复制构造函数以及复制运算符。下面用一个例子依次考虑这些情况:

基类:

class baseDMA
{
private:
    char * label;
    int rating;
public:
    baseDMA{const char* l = "null", int r = 0};
    baseDWA{const baseDMA & rs};
    virtul ~baseDAM();
    baseDAM & operator={const baseMDA & rs};
……
};

派生类:

class hasDAM:public baseDMA
{
private:
    char * style;//使用了new
public:
    ……
};

(1)析构函数

派生类析构函数自动调用基类的析构函数,并且对派生类构造函数执行的工作进行清理,所以,hasDMA析构函数必须释放指针style管理的内存,并且依赖于baseDAM的析构函数来释放指针label管理的内存。所以在基类和派生类中析构函数belike:

baseMAD::~baseDAM()        //基类
{
    delete [] label;
}
hasDMA:: ~hasDAM()        //派生类
{
    delete [] style;
}

(2)复制构造函数

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

上面是基类的复制构造函数,使用了strlen()函数来确定存储c风格字符串所需的空间内存(字符数加上存储空字符所需的1字节)并使用函数strcpy()来将原始字符串复制到目的地;

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

上面是派生类的复制构造函数,hasDMA的复制构造函数只能访问hasDMA的数据,所以它必须调用baseDMA的复制构造函数来处理共享的baseDMA数据(基类的私有成员必须通过基类的公有方法访问)。

这里还有一点需要注意,成员列表初始化将一个hasDMA引用传递给baseDMA构造函数【baseDMA(hs)】,这里不需要参数为hasDMA引用的构造函数,可以直接使用参数是baseDMA引用的构造函数,因为基类引用可以指向派生类对象。结果就是baseDMA复制构造函将使用hasDMA参数的baseDMA部分来构造新对象的baseDMA部分。

(3)赋值运算符

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

上面是基类的赋值运算符

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

上面是派生类对象的赋值运算符,由于hasDMA也使用了new,所以必须显式的定义一个重载的赋值运算符。它是hasDMA类的方法,所以可以直接访问hasDMA的数据。对于从基类baseDMA中继承的成员,可以通过在该函数中显式地调用基类赋值运算符来完成这个工作。

其中语句 baseDMA :: operator = (hs);这个语句使用的是函数表示法,可以使用作用域解析运算符表明使用的是基类中的operator=()函数。不能使用运算符表示法(*this = hs),因为this指针不能使用作用域解析运算符,出现在该函数中this指针就是调用该函数的hasDMA对象,而不是baseDMA对象,如果使用了该语句编译器将使用hasD::operator=(),进而形成递归调用。

总结就是:当基类对象和派生类都使用了动态内存分配,派生类的析构函数、复制构造函数、赋值运算符都必须使用相应的基类方法来处理基类元素。对于析构函数,这是自动完成的,无需在派生类构造函数中显式地调用相关析构函数。对于复制构造函数,通过在初始化列表中显式地调用基类的复制构造函数来完成;如果不这样做就将自动调用基类的默认构造函数,对于使用了new的成员,这样做会引发一些问题;对于赋值运算符,通过使用作用域解析运算符显式地调用基类的赋值运算符来完成。

动态内存分配和友元的继承实例

首先,头文件(程序13.14)

这个实例中包含有一个友元函数,说明派生类如何访问基类的友元:

// dma.h  -- inheritance and dynamic memory allocation
#ifndef DMA_H_
#define 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

实现文件:

// dma.cpp --dma class methods

#include "dma.h"
#include <cstring>
#pragma warning(disable:4996)

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

 需要注的是基类hasDMA类的友元:

friend std::ostream & operator<<(std::ostream & os, 
                                     const hasDMA & rs);

其中作为hasDMA类的友元,它是可以访问成员style的。在该函数中基类baseDMA类的友元opeerator<<(),这样它就是可以访问成员label和rating。又因为友元不是成员函数,所以不能使用作用域解析运算符来指出要使用哪一个函数,此时使用强制类型转换,代码将参数const hasDMA &转化成const baseDMA &参数,以便于匹配原型时可以使用正确的函数。

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

其中语句os<< (const baseDMA &) hs;对形参hs进行了强制类型转换。

主函数文件(程序13.16)

// 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("AMercator", "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; 
}

 输出为:

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: AMercator

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

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

类设计回顾

编译器生成的成员函数

首先回顾下编译器自动生成一些公有成员函数——特殊公有函数 

(1)默认构造函数

首先明确默认构造函数要么没有参数,要么所有参数都有默认值。如果没有定义任何构造函数,那编译器将定义默认构造函数。这个默认构造函数可以使用者可以成功创建对象,默认构造函数还有一个功能是调用基类的默认构造函数以及调用本身是对象的成员所属类的默认构造函数。另外,如果派生类构造函数的成员初始化列表没有显式地调用基类构造函数,则编译器就使用基类的默认构造函数来构造派生类对象的基类部分。

如果定义了某种构造函数,编译器就不会定义默认构造函数。在这种情况下,如果需要默认构造函数,则必须自己提供。

提供默认构造函数的主要动机是确保对象总能被正确地初始化。另外,如果类包含指针成员,则必须初始化这些成员,所以最好的做法就是显式地提供一个默认构造函数,将所有类数据成员都初始化成合理的值。

(2)复制构造函数

复制构造函数接受其所属类的对象作为参数,在下属情况下将会调用复制构造函数:

(1)将新对象初始化为同一个同类对象;

(2)按值将对象传递给函数;

(3)函数按值返回对象;

(4)编译器生成临时对象。

注意浅复制和深度复制的区别,例如,使用new初始化的成员指针通常要求执行深度复制,或者类中可能包含需要修改的静态变量,这时是需要定义自己的复制构造函数。

(3)赋值运算符

默认的复制运算符用于处理同类对象之间的赋值。注意初始化和赋值的区别:如果语句创建新的对象,则使用初始化,如果语句修改已有的对象的值,就是赋值。

默认的赋值是成员赋值。若成员类对象,则默认成员赋值将使用相应类的赋值运算符。如果需要显式定义复制构造函数,则基于相同的原因,也需要显式地定义赋值运算符。

编译器一般不会生成将一种类型赋给另一类型的赋值运算赋,这时有两种方法可以解决的:(1)定义一个返回值是需要类型的函数,第二种就是强制类型转化。第一种方法运行速度快,但是需要的代码多,第二种有风险,可能会引起编译器混乱。

其他类方法

(1)构造函数

构造函数不同于其他函数的是它是用于创建类对象,其他类方法则是被现有对象调用,这是构造函数不能被继承的原因之一。

(2)析构函数

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

(3)转换

只使用一个参数的构造函数可定义为从参数类型到类类型的转换,如下:

Star(const char *);

上面这个构造函数原型将const char* 转换成Star类类型。

还可以将可转换的类型传递给以类为参数的函数时,将调用转换构造函数,如下:

Star north;
north = "polaris";

 第一行代码使用了默认构造函数创建了一个名为north的类对象,第二行首先使用Star::star(const char*)函数生成一个Star对象(赋值运算符右边),再调用Star::operator = (const Star*)函数接收这个这个Star对象为参数,将这个Star对象赋值给north。

相反,要将类对象转换成其他类型,应该定义转换函数。转换函数可以是没有参数的类成员函数,也可以是返回类型被声明没为目标类型的类成员函数,即使没有声明返回类型,函数也应该返回所需的转换值。关于转换函数详细见之前的笔记。

(4)按值传递对象与引用

一般,编写使用对象作为参数的函数时,应该引用而不是按值传递对象。这样做的好处有:

(a)提高效率,因为按值传递对象涉及到生成临时拷贝,即调用复制构造函数,然后调用析构函数。调用这些函数需要时间,复制大型对象比传递引用花费的时间更长,如果函数不修改对象,应将参数声明为conts 引用。

(b)在继承使用虚函数时,被定义为接收基类引用参数的函数可以接受派生类。

(5)返回对象和返回引用

首先在编码方面。返回对象和引用的区别只在函数头

Star noval (const Star &);//返回对象
Star & noval(const Star &);//返回引用

优先返回引用的原因:返回对象涉及生成返回对象的临时副本,这个副本使调用函数的程序可以使用该对象,因此返回对象的时间成本包含调用复制构造函数来生成副本所需的时间和调用析构函数删除副本的时间。返回引用可以节省时间和内存。

直接返回对象和按值传递对象相似:它们都生成临时对象。

返回引用和按引用传递对象相似:调用和被调用的函数对同一个对象进行操作。

不能返回引用的场景:

函数不能返回在函数中创建的临时对象的引用,这是因为函数结束时,临时对象将消失,这种引用也会变成非法。这种情况下应该返回对象,以生成一个调用程序可以使用的副本。

综上的规则就是:如果函数返回在函数中创建的临时对象,则不要使用引用。如果函数返回的是通过引用或者指针传递给它的对象,这应该按引用返回对象。

(6)使用const

使用const关键字,可以确保方法不修改参数,如下函数头:

const Stock & Stock::topval(const Stock & s) const
{
    if (s.total_val > total_val)
        return s;
    else
        return *this;
}

通常可以将返回引用的函数放在赋值语句的左侧,也就是意味着可以将值赋给引用的对象。第一个const可以确保返回的值(这个值就是s(参数s引用的Stock对象)或者this指针(调用函数的对象))不能用于修改对象(调用函数的对象或者参数s引用的Stock类对象)中的值。并且该方法中因为this和s都被声明为const类型,所以函数不对它们进行修改,这就意味着返回的引用也必须声明成const。

第二个const可以确保不修改参数,如果函数将参数声明成指向const的引用和指针,则不能将该参数传递给另一个函数,除非另一个函数也确保参数不会被修改。

第三个const可以确保方法不修改调用它的对象。

公有继承的考虑因素

(1)is-a关系

派生类必须遵循is-a关系,如果派生类不是一种特殊的基类,就不要使用公有派生。在某些情况下,可以创建包含纯虚函数的抽象数据类,并从它派生出其他的类。

表示is-a关系的最好方法之一就是,无需进行显式类型转换,基类指针就可以指向派生类对象,基类引用可以引用派生类对象,但是反过来则不可以。

(2)什么不能被继承

首先,构造函数不能被继承,也就说创建派生类对象时,必须调用派生类的构造函数。但是,派生类构造函数通常使用成员初始化列表语法来调用基类构造函数,已创建派生类对象的基类部分。如果派生类构造函数没有使用列表初始化列表显式调用基类构造函数,将使用基类的默认构造函数。

其次,析构函数也不能被继承。在释放对象时,程序首先调用派生类的析构函数,再调用基类的析构函数。如果基类有默认析构函数,编译器将为派生类生成默认析构函数。一般情况下,将基类的析构函数设置为虚的。

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

(3)赋值运算符

默认或者隐式版本的赋值运算符采用的成员赋值,即将原对象的相应成员赋给目标对象的每个成员。如果对象属于派生类,编译器将使用基类赋值运算符来处理基类部分的赋值。前面也多次提及到如果类构造函数使用了new来初始化指针,则需要提供一个显式赋值运算符。基类中使用了new,但是派生类没有使用,则只需为基类显式定义一个赋值运算符,派生类不需要重新定义赋值运算符。如果派生类也使用了new,则必须为派生类显式地定义一个赋值运算符。

将派生类对象赋值给基类对象,将调用基类的赋值运算符来处理基类部分,同时忽略派生类独有的成员。

但是将基类对象赋值给派生类对象,不一定会成功,如下:

Brass gp("Griff Hexbait", 21234, 12000);//创建基类对象
BrassPlus temp;//创建派生类对象
temp = gp;//将基类对象赋值给派生类对象

第三行代码因为左边是派生类对象,所以将调用派生类的赋值运算符函数,该函数的函数头是BrassPlus::operator = (const BrassPlus &),但是派生类的引用不能自动引用基类对象。为了解决的这个问题,现在有以下两种情况可以解决

(a)转换构造函数

转换构造函数可以接受一个类型为基类的参数和有默认值的其它参数。如下:

BrassPlus(const Brass & ba, double ml = 500, double r = 0.1);

 (b)定义一个将基类赋给派生类的赋值运算符,如下:

BrassPLus & BrassPlus :: operator = (const Brass &) {  }

 (4)私有成员和保护成员

对于派生类而言,保护成员类似于公有成员,但是对于外部来说,保护成员与私有成员相似。派生类可以直接访问基类的保护成员,但是只能通过基类的成员函数才能访问私有成员。所以将基类成员设置为私有可以提高安全性,将基类成员设置为保护成员可以简化代码的编写工作,并提高访问速度。

(5)虚方法

如果希望派生类能够重新定义方法,则应该在基类中将方法定义为虚的,这样可以启用动态联编;如果不打算在派生类中重新定义方法,则不需要将其声明为虚的。

一些不适当的代码也会被禁止动态联编

void show(const Brass & rba)
{
    rba.ViewAcct();
    cout << endl;
}
void inadequate(Brass ba)
{
    ba.ViewAcct();
    cout << endl;
}

例如,上面的代码中第一个函数按引用传递对象,第二个按值传递函数。现在假设将一个派生类对象传递给上述两个函数。

BrassPlus bazz("Buzz Parsec", 00001111, 4300);
show(buzz);
inadequate(buzz);

 show函数调用使rba参数成为BrassPlus对象buzz引用,所以show函数体中的ViewAcct被解释为BrassPlus版本。但是在inadequate函数(它是按值传递对象的)中,ba是Brass构造函数创建的一个Brass对象(向上的强制转换可以使得构造函数可以引用一个BrassPlus对象)。所以,在inadequate中,ba.ViewAcct()是Brass版本,所以只能显示buzz中基类的那部分。

(6)析构函数

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

(7)友元函数

首先明确一点,友元函数并不是类成员函数,所以它不能被继承。要想使派生类的友元函数能够使用基类的友元函数,可以通过强制类型转化将派生类引用或指针强制转换成基类引用或指针,然后使用转换后的指针或者引用来调用基类的友元函数。

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

以公有方式派生的类的对象可以通过多种方式来使用基类的方法:

(1)派生类对象自动使用继承而来的基类方法,如果派生类没有重新定义该方法

(2)派生类的构造函数自动调用基类的默认构造函数,如果没有在成员初始化列表中指定其他构造函数。

(3)派生类的构造函数自动调用基类的构造函数

(4)派生类构造函数显式地调用成员初始化列表中指定的基类构造函数

(5)派生类方法可以使用作用域解析运算符来调用公有和受保护的基类方法

(6)派生类的友元函数可以通过强制类型转换,将派生类引用或指针转换成基类指针引用或者指针,然后使用该引用或者指针来调用基类的友元函数。

类函数小结

其中op=表示诸如+=、*=等格式的赋值运算符。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值