《C++ primer plus》第13章:类继承(4)

访问控制:protected

到目前为止,本书的类示例已经使用了关键字 public 和 private 来控制对类成员的访问。还存在另一个访问类别,这种类别用关键字 protected 表示。关键字 protected 与 private 相似,在类外只能用公有类成员函数来访问 protected 部分中的类成员。private 和 protected 之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。

例如,假如 Brass 类将 balance 成员声明为保护的:

class Brass{
protected:
	double balance;
	...
};

在这种情况下,BrassPlus类可以直接访问balance,而不需要使用 Brass的方法。例如,可以这样编写BrassPlus::Withdraw()的核心:

void BrassPlus::Withdraw(double amt){
	if ( amt < 0 ){
		cout << "Withdrawal amount must be positive; "
				<< "Withdrawal canceled.\n";
	}
	else if ( amt <= balance ){		// access balance directly
		balance -= amt;
	}
	else if ( amt <= balance + maxLoan - owesBank ){
		double advance = amt - balance;
		owesBank += advance * (1.0 + rate);
		cout << "Bank advance: $" << advance << endl;
		cout << "Finance charget: $" << advance * rate << endl;
		Deposit(advance);
		balance -= amt;
	}
	else{
		cout << "Credit limit exceeded. Transaction cancelled.\n";
	}
}

使用保护数据成员可以简化代码的编写工作,但存在设计缺陷。例如,继续以 BrassPlus 为例,如果 balance 是受保护的,则可以按下面的方式编写代码:

void BrassPlus::Reset(double amt){
	balance = amt;
}

Brass类被设计成只能通过 Deposit() 和 Withdraw() 才能修改 balance。但对于 BrassPlus 对象,Reset() 方法将忽略 Withdraw() 中的保护措施,实际上使 balance 成为公有变量。

警告:最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够访问基类数据。
然而,对于成员函数来说,保护访问控制很有用,它让派生类能够访问公众不能使用的内部函数。

抽象基类

至此,介绍了简单继承和较复杂的多态继承。接下来更为复杂的是抽象基类(abstract base class,ABC)。我们来看一些可使用 ABC 的编程情况。
有时候,使用 is-a 规则并不是看上去的那样简单。例如,假设您正在开发一个图形程序,该程序会显示圆和椭圆等。圆是椭圆的一个特殊情况——长轴和短轴等长的椭圆。因此,所有的圆都是椭圆,可以从 Ellipse 类派生出 Circle 类。但涉及到细节时,将发现很多问题。

首先考虑 Ellipse 类包含的内容。数据成员可以包括椭圆中心的坐标、半长轴(长轴的一半)、短半轴(短轴的一半)以及方向角(水平坐标轴与长轴之间的角度)。另外,还可以包括一些移动椭圆、返回椭圆面积、旋转椭圆以及缩放长半轴和短半轴的方法:

class Ellipse{
private:
	double x;			// x-coordinate of the ellipse's center
	double y;			// y-coordinate of the ellipse's center
	double a;			// semimajor axis
	double b;			// semiminor axis
	double angle;	// orientation angle in degrees
	...
public:
	...
	void Move(int nx, int ny) { x = nx; y = ny; }
	virtual double Area() const { return 3.14159 * a * b; }
	virtual void Rotate(double nang) { angle += nang; }
	virtual void Scale(double sa, double sb) { a *= sa; b *= sb; }
};

现在假设从 Ellipse 类派生出一个 Circle 类:

class Circle : public Ellipse{
	...
};

虽然圆是一种椭圆,但是这种派生是笨拙的。例如,圆只需要一个值(半径)就可以描述大小和形状,并不需要有长半轴(a) 和短半轴(b)。Circle 构造函数可以通过将同一个值赋给成员 a 和 b 来照顾这种情况,但将导致信息冗余。angle 参数和 Rotate() 方法对圆来说没有实际意义;而 Scale() 方法(顾名思义)会将两个轴作不同的缩放,将圆变成椭圆。可以使用一些技巧来修正这些问题,例如在 Circle 类中的私有部分包含重新定义的 Rotate() 方法,使Rotate() 不能以公有方式用于圆。但总的来说,不使用继承,直接定义 Circle 类更简单:

class Circle{		// no inheritance
private:
	double x;		// x-coordinate of the circle's center
	double y;		// y-coordinate of the circle's center
	double r;		// radius
	...
public:
	...
	void Move(int nx, int ny) { x = nx; y = ny; }
	double Area() const { return 3.14159 * r * r; }
	void Scale(double sr) { r *= sr; }
	...
};

现在,类只包含所需的成员。但这种解决方法的效率也不高。Circle 和 Ellipse 类有很多共同点,将它们分别定义则忽略了这一事实。

还有一种解决方法,即从 Ellipse 和 Circle 类中抽象出它们的共性,将这些特性放到一个 ABC 中。然后从该 ABC 派生出 Circle 和 Ellipse 类。这样,便可以使用基类指针数组同时管理 Circle 和 Ellipse 对象,即可以使用多态方法)。在这个例子中,这两个类的共同点是中心坐标、Move() 方法(对于这两个类是相同的)和 Area()方法(对于这两个类来说,是不同的)。确实,甚至不能再 ABC 中实现 Area() 方法,因为它没有包含必要的数据成员。 C++ 通过使用纯虚函数(pure virtual function)提供未实现的函数。纯虚函数声明的结尾处为=0,参见 Area() 方法:

class BaseEllipse{		// abstract base class
private:
	double x;			// x-coordinate of center
	double y;			// y-coordinate of center
	..
public:
	BaseEllipse(double x0 = 0, double y0 = 0) :x(x0),y(y0) {}
	virtual ~BaseEllipse() {}
	void Move(int nx, int ny) { x = nx; y = ny; }
	virtual double Area() const = 0;	// a pure virtual function
};

当类声明中包含纯虚函数时,则不能创建该类的对象。这里的理念是,包含纯虚函数的类只用作基类。要成为真正的ABC,必须至少包含一个纯虚函数。原型中的=0 使虚函数成为纯虚函数。这里的方法 Area() 没有定义。但 C++ 甚至允许纯虚函数有定义。例如,也许所有的基类方法都与 Move() 一样,可以在基类中进行定义,但您仍需要将这个类声明为抽象的。在这种情况下,可以将原型声明为虚的:

void Move(int nx, int ny) = 0;

这将使基类成为抽象的,但您仍可以在实现文件中提供方法的定义:

void BaseEllipse::Move(int nx, int ny) { x = nx; y = ny; }

总之,在原型中使用=0 指出类是一个抽象基类,在类中可以不定义该函数。
现在,可以从 BaseEllipse 类派生出 Ellipse 类和 Circle 类,添加所需的成员来完成每个类。需要注意的一点是,Circle 类总是表示圆,而 Ellipse 类总是表示椭圆——也可以是圆。然而,Ellipse 类圆可以被重新缩放为非圆,而 Circle 类圆必须始终为圆。

使用这些类的程序将能够创建 Ellipse 对象和 Circle 对象,但是不能创建 BaseEllipse 对象。由于 Circle 和 Ellipse 对象的基类相同,因此可以用 BaseEllipse 指针数组同时管理这两种对象。像 Circle 和 Ellipse 这样的类有时被称为具体(concrete)类,这表示可以创建这些类的对象。

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

应用 ABC 概念

您可能希望看到一个完整的 ABC 示例,因此这里将这一概念用于 Brass 和 BrassPlus 账户,首先定义一个名为 AcctABC 的 ABC。这个类包含 Brass 和 BrassPlus 类共有的所有方法和数据成员,而那些在 BrassPlus 类和 Brass 类中的行为不同的方法应被声明为虚函数。至少应有一个虚函数是纯虚函数,这样才能使 AcctABC 成为抽象类。

下面的程序的头文件声明了 AcctABC 类(ABC)、Brass类 和 BrassPlus类(两者都是具体类)。为帮助派生类访问基类数据,AcctABC 提供了一些保护方法;派生类方法可以调用这些方法,但它们并不是派生类对象的公有接口的组成部分。AcctABC 还提供了一个保护成员函数,用于处理格式化(以前是使用非成员函数处理的)。另外,AcctABC 类还有两个纯虚函数,所以它确实是抽象类。

// acctabc.h -- bank account classes
#ifndef ACCTABC_H_
#define ACCTABC_H_

#include<iostream>
#include<string>

// Abstract Base Class

class AcctABC{
private:
    std::string fullName;
    long acctNum;
    double balance;
protected:
    struct Formatting{
        std::ios_base::fmtflags flag;
        std::streamsize pr;
    };
    const std::string & FullName() const { return fullName; }
    long AcctNum() const { return acctNum; }
    Formatting SetFormat() const;
    void Restore(Formatting & f) const;
public:
    AcctABC(const std::string & s = "Nullbody", long an = -1, double bal = 1.0);
    void Deposit(double amt);
    virtual void Withdraw(double amt) = 0;  // pure virtual function
    double Balance() const { return balance; };
    virtual void ViewAcct() const = 0;      // pure virtual function
    virtual ~AcctABC() {}
};


// Brass Account Class
class Brass : public AcctABC{
public:
    Brass(const std::string & s = "Nullbody", 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 std::string & 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; }
    virtual ~BrassPlus() {}
};


#endif
// acctab.cpp -- bank account class methods
#include <ios>
#include<iostream>
#include"13.11_acctabc.h"

using std::cout;
using std::ios_base;
using std::endl;
using std::string;

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

void AcctABC::Withdraw(double amt){
    balance -= amt;
}

// protected methods for formatting
AcctABC::Formatting AcctABC::SetFormat() const{
    // set up ###.### format
    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 methods
void Brass::Withdraw(double amt){
    if (amt < 0){
        cout << "Withdrawal amount must be positive; "
             << "withdrawal canceled.\n";
    }
    else if (amt <= Balance()){
        AcctABC::Withdraw(amt);
    }
    else{
        cout << "Withdrawal amount of $" << amt
             << " exceeds your balance.\n"
             << "Withdrawal cancelde.\n";
    }
}


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


// BrassPlus Methods
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 Brass & ba, double ml, double r) : AcctABC(ba){
    // uses implicit copy constructor
    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.precision(3);
    cout << "Loan Rate: " << 100 * rate << "%\n";

    Restore(f);
}


void BrassPlus::Withdraw(double amt){
    Formatting f = 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";
    }
    Restore(f);
}
// usebrass3.cpp -- polymorphic example using an abstract base class
// compile with acctacb.cpp

#include <iostream>
#include <string>
#include "13.11_acctabc.h"

const int CLIENTS = 4;

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

    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: ";
        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++){
        cout << i << " delete" << endl;
        delete p_clients[i];
    }

    cout << "Done.\n";

    return 0;
}

ABC 理念

在处理继承的问题上,RatedPlayer 示例使用的方法比较随意,而 ABC 方法比它更具系统性、更规范。设计 ABC 之前,首先应开发一个模型——指出编程问题所需的类以及它们之间相互关系。一种学院派思想认为,如果要设计类继承层次,则只能将那些不会用作基类的类设计为具体的类。这种方法的设计更清晰,复杂程度更低。
可以将 ABC 看作是一种必须实施的接口。ABC要求具体派生类覆盖其纯虚函数——迫使派生类遵循 ABC 设置的接口规则。这种模型在基于组件的编程模式中很常见,在这种情况下,使用 ABC 使得组件设计人员能够制定 “接口约定”, 这样确保了从 ABC 派生的所有组件都至少支持 ABC 指定的功能。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值