P29-c++类继承-06抽象基类详细介绍

1. 抽象基类

至此,介绍了简单继承和较复杂的多态继承。接下来更为复杂的是抽象基类( 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 ellipses 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 circlets center
	double y; //y-coordinate of the circlets center
	double r; // radius
public:
	void Move(int nx, int ny)  {x =nx; y=ny;}
	double Area()const( return 3.14159*r+ ri
	void Scale(double sr) { r *= sr; }
}

现在,类只包含所需的成员。但这种解决方法的效率也不高。 Circle i和 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 yo=0) : x(x0), y(y0) {}
	virtual ~BaseEllipse ()
	void Move(int nx, int ny) {x = xn, 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, ny) {x=nx; y=ny;}

总之,在原型中使用=0指出类是一个抽象基类,在类中可以不定义该函数。

现在,可以从 BaseEllipse 类派生出 Ellips类和 Circle类,添加所需的成员来完成每个类。需要注意的点是, Circle类总是表示圆,而 Ellipse类总是表示椭圆一也可以是圆。然而, Ellipse类圆可被重新缩放为非圆,而 Ciecle类圆必须始终为圆。
使用这些类的程序将能够创建 Ellipse对象和 Circle对象,但是不能创建 BaseEllipse 对象。由于 Circle和 Ellipse对象的基类相同,因此可以用 BaseEllipse 指针数组同时管理这两种对象。像 Circle和 Ellipse这样的类有时被称为具体( concrete)类,这表示可以创建这些类型的对象。

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

2.应用ABC概念

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

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

/*
	author:梦悦foundation
	公众号:梦悦foundation
	可以在公众号获得源码和详细的图文笔记
*/

// 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 = 0.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; }
};

#endif

接下来需要实现那些不是内联函数的方法,如程序清单13.12所示。
程序清单13.12 acctabc.cpp

/*
	author:梦悦foundation
	公众号:梦悦foundation
	可以在公众号获得源码和详细的图文笔记
*/

// acctabc.cpp -- bank account class methods
#include <iostream>
#include "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 canceled.\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); 
}

保护方法 FullName()和 AcctNum()提供了对数据成员 fullName和 acctNum的只读访问,使得可以进一步定制每个派生类的 ViewAcct()。
这个版本在设置输出格式方面做了两项改进。前一个版本使用两个函数调用来设置输出格式,并使用一个函数调用来恢复格式:

format initialState =setFormat();
precis prec = cout.precision(2);
restore(initialState, prec); // restore original format

这个版本定义了一个结构,用于存储两项格式设置;并使用该结构来设置和恢复格式,因此只需两个函数调用:

    struct Formatting 
    {
         std::ios_base::fmtflags flag;
         std::streamsize pr;
    };
Formatting  f = SetFormat();
Restore(f);

因此代码更整洁。
旧版本存在的问题是, setFormat()和 Restore()都是独立的函数,这些函数与客户定义的同名函数发生冲突。

解决这种问题的方式有多种,一种方式是将这些函数声明为静态的,这样它们将归文件 brass. cpp及其继任 acctabc.cpp私有。另一种方式是,将这些函数以及结构 Formatting放在一个独立的名称空间中。
但这个示例探讨的主题之一是保护访问权限,因此将这些结构和函数放在了类定义的保护部分。
这使得它们对基类和派生类可用,同时向外隐藏了它们对于 Brass 和 BrassPlus账户的这种新实现,使用方式与旧实现相同,因为类方法的名称和接口都与以前一样。例如,为使程序清单13.10能够使用新的实现,需要采取下面的步骤将 usebrass2.cpp转换为usebrass3.cpp

  • 使用 acctabc.cpp而不是 brass.cpp来链接 usebrass2.cpp
  • 包含文件 acctabc.h,而不是brass.h
  • 将下面的代码:
Brass *p_clients[CLIENTS];

替换为:

AcctABC * p_clients[CLIENTS];

程序清单13.13是修改后的文件,并将其重命名为 usebrass3.cpp。
程序清单13.13 usebrass3.cpp

/*
	author:梦悦foundation
	公众号:梦悦foundation
	可以在公众号获得源码和详细的图文笔记
*/

// usebrass3.cpp -- polymorphic example
// compile with acctacb.cpp
#include <iostream>
#include <string>
#include "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++)
   {
       delete p_clients[i];  // free memory
   }
   cout << "Done.\n";    
   // cin.get();
   return 0; 
}

该程序本身的行为与非抽象基类版本相同,因此如果输入与给程序清单13.10提供的输入相同,输出也将相同。

3. 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、付费专栏及课程。

余额充值