C++多态公有继承
一、说明
在派生类对象使用基类的方法时,可能不会做任何修改。但也会遇到这样一种情况,希望同一个方法在派生类和基类中的行为是不同的。换句话说,方法的行为应取决于调用该方法的对象。这种较复杂的行为称为多态—具有多种形态,即同一个方法的行为随上下文而异。有两种重要的机制可以用于实现多态公有继承:
(1)在派生类中重新定义基类的方法
(2)使用虚方法
具体的实现方式,容我慢慢道来
二、设计基类与派生类
注意: is-a关系是不可逆的,即水果不是香蕉。同时本节设计的Brass基类对象不具备BrassPlus派生类对象的所有功能。
// Brass Account Base 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);
virtual ~Brass();
void Deposit(double amt);
double Balance() const;
virtual void Withdraw(double amt);
virtual void ViewAcct() const;
};
// Brass Plus Account Derived 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);
~BrassPlus();
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; }
};
三、基类与派生类说明
3.1 第二节基类与派生类说明
(1)BrassPlus类在Brass类的基础上添加了3个私有数据和3个公有函数
(2)Brass类和BrassPlus类都声明了ViewAcct()和Withdraw()方法,但BrassPlus对象和Brass对象的这些方法的行为是不同的。
详细说明: 此说明介绍了声明如何指出方法在派生类的行为的不同。两个ViewAcct()原型表明将2个独立的方法定义。基类版本的限定名为Brass::ViewAcct(),派生类版本的限定名为BrassPlus::ViewAcct(),程序将使用对象类型来确定使用哪个版本,Withdraw()方法与ViewAcct()使用类同。对于在两个类中行为相同的方法则只在基类中声明。
举例说明如下:
Brass dom("Test", 1234, 1234.56);
BrassPlus dot("Test1", 5678, 5678.91);
// 使用Brass::ViewAcct()
dom.ViewAcct();
// 使用BrassPlus::ViewAcct()
dot.ViewAcct();
(3)Brass类在声明ViewAcct()和Withdraw()时使用了关键字virtual。这些方法被称为虚方法。
详细说明: 使用virtual则要比前两点复杂。如果方法是通过指针或引用而不是对象调用的,它将确定使用哪一种方法。
- 如果没有关键字virtual,程序见根据引用或指针的类型选择调用的方法
- 如果使用virtual关键字,程序将根据引用或指针指向的对象的类型选择调用的方法
举例说明如下:
// 此示例是使用的引用做说明,指针使用与引用类似
Brass dom("Test", 1234, 1234.56);
BrassPlus dot("Test1", 5678, 5678.91);
Brass &b1_ref = dom;
Brass &b2_ref = dot;
// 如果ViewAcct()函数使用virtual关键字声明
// 使用Brass::ViewAcct()
b1_ref.ViewAcct();
// 使用BrassPlus::ViewAcct()
b2_ref.ViewAcct();
// 如果ViewAcct()函数没有使用virtual关键字声明
// 使用Brass::ViewAcct()
b1_ref.ViewAcct();
// 使用Brass::ViewAcct()
b2_ref.ViewAcct();
注意: 在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚方法后,它在派生类中将自动称为虚方法。然而,在派生类声明中使用关键字virtual来指出哪些函数是虚函数也不失为一个好办法。 关键字virtual只用于类声明的方法原型中,而不使用于方法定义。
(4)Brass类还声明了一个虚析构函数。
详细说明: 基类声明了一个虚析构函数,这样做是为了确保释放派生类对象时,按正确的顺序调用析构函数。
3.2 基类与派生类实现及使用说明
// 全局函数声明
std::ios_base::fmtflags setformat();
void restore(std::ios_base::fmtflags fmt, std::streamsize pc);
// 基类实现
Brass::Brass(const std::string &s, long an, double bal)
{
fullname = s;
acctnum = an;
balance = bal;
}
Brass::~Brass()
{
}
void Brass::Deposit(double amt)
{
if (amt < 0)
std::cout << "Negative deposit not allowed; " << "deposit is cancelled.\n";
else
balance += amt;
}
double Brass::Balance() const
{
return balance;
}
void Brass::Withdraw(double amt)
{
// 格式设置
std::ios_base::fmtflags initialstate = setformat();
std::streamsize precis = std::cout.precision(2);
if (amt < 0)
std::cout << "Withdrawal amount must be positive; " << "Withdrawal canceled.\n";
else if (amt < balance)
balance -= amt;
else
std::cout << "Withdrawal amount of $" << amt << " exceeds your balance.\n" << "Withdrawal canceled.\n";
restore(initialstate, precis);
}
void Brass::ViewAcct() const
{
// 设置格式
std::ios_base::fmtflags initialstate = setformat();
std::streamsize precis = std::cout.precision(2);
std::cout << "Client: " << fullname << std::endl;
std::cout << "Account Number: " << acctnum << std::endl;
std::cout << "Balance: $" << balance << std::endl;
restore(initialstate, precis);
}
// 派生类实现
BrassPlus::BrassPlus(const std::string &s, long an, double bal, double ml, double r)
: Brass(s, an, bal)
, maxloan(ml)
, rate(r)
, owesbank(0.0)
{
}
BrassPlus::BrassPlus(const Brass &ba, double ml, double r)
: Brass(ba)
, maxloan(ml)
, rate(r)
, owesbank(0.0)
{
}
BrassPlus::~BrassPlus()
{
}
// 派生类重新定义了ViewAcct()工作方式
void BrassPlus::ViewAcct() const
{
// 格式设置
std::ios_base::fmtflags initialstate = setformat();
std::streamsize precis = std::cout.precision(2);
Brass::ViewAcct();
std::cout << "Maximum loan: $" << maxloan << std::endl;
std::cout << "Owed to bank: $" << owesbank << std::endl;
std::cout.precision(3);
std::cout << "Loan Rate: " << 100 * rate << "%\n";
restore(initialstate, precis);
}
// 派生类重新定义了Withdraw()工作方式
void BrassPlus::Withdraw(double amt)
{
// 格式设置
std::ios_base::fmtflags initialstate = setformat();
std::streamsize precis = std::cout.precision(2);
double bal = Balance();
if (bal < 0)
Brass::Withdraw(amt);
else if (amt <= bal + maxloan - owesbank) {
double advance = amt - bal;
owesbank += advance * (1.0 + rate);
// 银行预付款
std::cout << "Band advance: $" << advance << std::endl;
// 财务费用
std::cout << "Financd charge: $" << advance * rate << std::endl;
Deposit(advance);
Brass::Withdraw(amt);
}
else
std::cout << "Credit limit exceeded. Transaction cancelled.\n";
restore(initialstate, precis);
}
// 全局函数实现
std::ios_base::fmtflags setformat()
{
return std::cout.setf(std::ios_base::fixed, std::ios_base::floatfield);
}
void restore(std::ios_base::fmtflags fmt, std::streamsize pc)
{
std::cout.setf(fmt, std::ios_base::floatfield);
std::cout.precision(pc);
}
详细说明:
(1)派生类不能直接访问基类的私有数据,而必须使用基类的公有方法才能访问这些数据。访问的方式取决于方法。
- 派生类构造函数在初始化基类私有数据时,采用成员初始化列表语法,将基类信息传递给基类构造函数。
- 派生类非构造函数不能使用成员初始化列表语法,但派生类方法可以调用公有的基类方法。
(2)在派生类方法中,标准技术是使用作用域解析运算符来调用基类方法。代码举例如下:
// 代码如果没有使用作用域解析运算符,编译器将认为ViewAcct()是BrassPlus::ViewAcct(),这将创建一个不会终止的递归
// 函数
void BrassPlus::ViewAcct() const
{
Brass::ViewAcct();
}
(3)方法ViewAcct()与Withdraw()使用格式化方法setf()和precision()将浮点值的输出模式设置为定点,即包含两位小数。
3.3 虚方法
设计的Brass基类指针既可以指向Brass对象,也可以指向BrassPlus对象,因此可以使用一个数组来表示多种类型的对象。这就是多态性。多态性示例代码如下:
const int CLIENTS = 2;
Brass *clients[CLIENTS];
clients[0] = new Brass("Test0", 1234, 1234.56);
clients[1] = new BrassPlus("Test1", 5678, 5678.91);
for (int i = 0;i < CLIENTS;++i)
{
clients[i]->ViewAcct();
}
多态性说明:
(1)假如ViewAcct()是使用关键字virtual声明
- 如果数组成员指向的是Brass对象,则调用的是Brass::ViewAcct()。
- 如果数组成员指向的是BrassPlus对象,则调用的是BrassPlus::ViewAcct()。
(2)假如ViewAcct()不是虚方法
- 则在任何情况下都将调用Brass::ViewAcct()。
3.4 为何使用虚析构函数
- 如果析构函数不是虚方法,则将只调用对应于指针类型的析构函数。对于Brass * 指针将只调用Brass基类的析构函数,即使Brass * 指针指向的是BrassPlus对象。
- 如果析构函数是虚方法,将调用相应对象类型的析构函数。因此,如果指针指向的是BrassPlus对象,将调用BrassPlus的析构函数,然后自动调用基类的析构函数。因此,使用虚析构函数可以确保正确的析构函数序列被调用。
- 如果BrassPlus包含一个执行某些操作的析构函数,则Brass必须有一个虚析构函数,即使Brass的析构函数不执行任何操作。