C++类继承

11.11 类继承

面向对象编程的主要目的是提供可重用的代码。使用已有的代码可以节省时间,由于已有的代码已被使用和测试过,因此有助于避免在程序中引入错误。另外,必须考虑的细节越少,便越能专注于程序的整体策略。

函数库也有局限性:除非厂商提供了库函数的源代码(通常是不提供的),否则您将无法根据自己特定的需求,对函数进行扩展和修改,而必须根据库的情况修改自己的程序。

类继承:从已有的类派生出新的类,而派生类继承类基类的特征和方法。

继承可以完成以下工作:

  • 可以在己有类的基础上添加功能。例如,对于数组类,可以添加数学运算。
  • 可以给类添加数据。例如,对于字符串类,可以派生出一个类,并添加指定字符串显示颜色的数
    据成员。
  • 可以修改类方法的行为。例如,对于代表提供给 飞机乘客的服务的Passenger 类,可以派生出提供
    更高级别服务的 FirstClassPassenger 类。
  • 当然,可以通过复制原始类代码,并对其进行修改来完成上述工作,但继承机制只需提供新特性,甚至不需要访问源代码就可以派生出类。因此,如果购买的类库只提供了类方法的头文件和编译后代码,仍可以使用库中的类派生出新的类。

11.11.1 从一个简单的基类开始

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

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

这将首先为 firstname 调用 string 的默认构造函数,再调用 string 的赋值运算符将firstname 设置为fn,但初始化列表语法可减少一个步骤,它直接使用string 的复制构造两数将firstname 初始化为fn。

11.11.1.1 简单举例

TableTennisPlayer.h

#pragma once
// TableTennisPlayer.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; };
};

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

TableTennisPlayer.cpp

// TableTennisPlayer.cpp -- a table-tennis base class
#include "TableTennisPlayer.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)
{
}

main.cpp

// main.cpp -- using a base class
#include <iostream>
#include "TableTennisPlayer.h"
int main(void)
{
	using std::cout;
	using std::endl;
	//这里会先调用const char *的构造函数生成临时变量,然后再调用const string &的复制构造函数生成变量,最后删除临时变量(释放空间)
	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;
	return 0;
}

运行结果:

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

D:\Prj\C++\Inheritance_Example\Debug\Inheritance_Example.exe (进程 11856)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .
11.11.1.2 派生一个类

基本格式:

// RatedPlayer derives from the TableTennisPlayer base class
class RatedPlayer : public TableTennisPlayer
{
    ...
};

冒号指出 RatedPlayer 类的基类是 TableTennisplayer 类。上述特殊的声明头表明 TableTennisPlayer 是一个公有基类,这被称为公有派生。派生类对象包含基类对象。使用公有派生,基类的公有成员将成为派生类的公有成员:基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。

​ 派生类的属性:

  • 派生类对象存储了基类的数据成员(派生类继承了基类的实现);
  • 派生类对象可以使用基类的方法(派生类继承了基类的接口)。

派生类需要添加的特征:

  • 派生类需要自己的构造函数。
  • 派生类可以根据需要添加额外的数据成员和成员函数。

在这里插入图片描述

11.11.1.3 构造函数:访问权限

派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。
具体地说,派生类构造函数必须使用基类构造两数。
创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造两数之前被创建。C++使用成员初始化列表语法来完成这种工作。

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;
}
// alternative version
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp)
: TableTennisPlayer(tp), rating(r)
{
}

在这里插入图片描述

如果省略初始化列表,情况如何呢?

RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
const string & ln, bool ht) // what if no initializer list?
{
    rating = r;
}
//与下面这个有同样的效果:就是使用默认构造函数构造基类
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
const string & ln, bool ht) // : TableTennisPlayer()
{
    rating = r;
}

必须首先创建基类对象,如果不调用基类构造两数,程序将使用默认的基类构造函数
有关派生类构造函数的要点如下:

  • 首先创建基类对象:
  • 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;
  • 派生类构造函数应初始化派生类新增的数据成员。
  • 派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数
11.11.1.4 基类和继承类的特殊关系

特殊关系:

  • 派生类与基类之间有一些特殊关系。其中之一是派生类对象可以使用基类的方法,条件是方法不是私有的。
  • 另外两个重要的关系是:基类指针可以在不进行显式类型转换的情况下指向派生类对象:基类引用可以在不进行显式类型转换的情况下引用派生类对象:

如果基类引用和指针可以指向派生类对象,将出现一些很有趣的结果:

  • 基类引用定义的函数参数可用于基类对象和派生类对象
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 player1("Tara", "Boomdea", false);
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
Show(player1); // works with TableTennisPlayer argument
Show(rplayer1); // works with RatedPlayer argument

  • 对于形参为指向基类的指针的两数,也存在相似的关系。它可以使用基类对象的地址或派生类对象的地址作为实参:
void Wohs(const TableTennisPlayer * pt); // function with pointer parameter

TableTennisPlayer player1("Tara", "Boomdea", false);
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
Wohs(&player1); // works with TableTennisPlayer * argument
Wohs(&rplayer1); // works with RatedPlayer * argument

  • 引用兼容性属性也让您能够将基类对象初始化为派生类对象,尽管不那么直接。假设有这样的代码:
RatedPlayer olaf1(1840, "Olaf", "Loaf", true);
TableTennisPlayer olaf2(olaf1);

11.11.2 继承: Is-a关系

实际上,C++有3种继承方式:公有继承、保护继承和私有继承。公有继承是最常用的方式,它建立一种 is-a 关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。
继承是—个is-a关系;is-a不是对称。
派生类可以添加特性,所以,将这种关系称为 is-a-kind-of(是一种)关系可能更准确,但是通常使用术语 is-a。

has-a relationship:午饭有水果。继承不是has-a的关系。
然而可以在基类的基础上添加属性,但不能删除基类的属性。在有些情况下,可以设计一个包含共有特征的类,然后以 is-a 或 has-a关系,在这个类的基础上定义相关的类。

is-like- a relationship: lawyers are like sharks 继承不是is-like- a的关系
公有继承不建立 is-implemented-as-a(作为…来实现)关系。例如,可以使用数组来实现栈,但从Array 类派生出 Stack 类是不合适的,因为栈不是数组。

公有继承不建立 uses-a 关系。例如,计算机可以使用激光打印机,但从 Computer 类派生出 Printer 头(或反过来)是没有意义的。然而,可以使用友元两数或类来处理 Printer 对象和 Computer 对象之间的通信。

11.11.3 多态共有继承

有两种重要机制实现多态共有继承:

  • 在派生类中重新定义基类的方法。
  • 使用虚方法。

如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。如果没有使用关键字 virtual,程序将根据引用类型或指针类型选择方法:如果使用了 virtual,程序将根据引用或指针指向的对象的类型来选择方法。

virtual关键字是在方法原型的类声明中使用的,而不是在方法的定义中实现的。
因此,经常在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法。然后,在派生类声明中使用关键字 virtual 来指出哪些函数是虛两数也不失为一个好办法。

基类声明了一个虚析构函数。这样做是为了确保释放派生对象时,按正确的顺序调用析构函数。

如果不是虚析构函数的话,在使用delete删除指针指向的存储空间时,只会调用基类的析构函数而不会调用继承类的构造函数,这样的话继承类定义的参数就无法释放内存,可能会导致内存泄漏。

Brass * p_clients[CLIENTS];
p_clients[i] = new Brass(temp, tempnum, tempbal);
p_clients[i] = new BrassPlus(temp, tempnum, tempbal, tmax, trate);
delete p_clients[i]; // free memory

11.11.4 静态和动态联编

将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。

在编译过程中进行的联编称为静态联编。
编译器必须生成能够在程序运行时选择正确的虚方法的代码,被称为动态联编。

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

C++不允许将一种类型的地址赋给另一种类型的指针,也不允许一种类型的引用指向另一种类型

double x = 2.5;
int * pi = &x; // invalid assignment, mismatched pointer types
long & rl = x; // invalid assignment, mismatched reference type

将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasting),这使公有继承不需要进行显式类型转换。
相反的过程:将基类指针或引用转换为派生类指针或引用一—称为向下强制转换 (downcasting)。
如果不使用显式类型转换,则向下强制转换是不允许的。
对于使用基类引用或指针 作为参数的两数调用,将进行向上转换。
在这里插入图片描述

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()
    ...
}

按值传递导致只将 BrassPlus 对象的 Brass 部分传递给两数fv()。但随引用和指针发生的隐式向上转换导致函数fr()和fp()分别为 Brass 对象和 BrassPlus 对象使用 Brass:: ViewAcct( )和 BrassPlus: ViewAcct()。

11.11.4.2 虚成员函数和动态联编

编译器对非虚成员函数使用静态联编。

编译器对虚成员函数使用动态联编。

11.11.4.2.1 为什么有两种联编以及为什么默认为静态联编

如果要在派生类中重新定义基类的方法,则将它设置为虚方法;否则,设置为非虚方法。
如果动态联编让您能够重新定义类方法,而静态联编在这方面很差,为何不摒弃静态联编呢?原因有两个一效率和概念模型。
为使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销
由于静态联编的效率更高,因此被设置为 C++的默认选择。Strousstrup 说,C++的指导原则之一是,不要为不使用的特性付出代价(内存或者处理时间)。仅当程序设计确实需要虚函数时,才使用它们。
不将该两数设置为虚两数,有两方面的好处:首先效率更高:其次,指出不要重新定义该函数。这表明,仅将那些预期将被重新定义的方法声明为虚的。
当然,设计类时,方法属于哪种情况有时并不那么明显。与现实世界中的很多方面一样,类设计并不是一个线性过程。

11.11.4.2.2 虚函数是如何工作的

通常,编泽器处理程两数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虛函数表(virtual function table,vtbl)。虚两数表中存储了为类对象进行声明的虚函数的地址。
在这里插入图片描述

总之,使用虚两数时,在内存和执行速度方面有一定的成本,包括:

  • 每个对象都将增大,增大量为存储地址的空间;
  • 对于每个类,编译器都创建一个虚两数地址表(数组);
  • 对于每个两数调用,都需要执行一项额外的樂作,即到表中查找地址。
11.11.4.2.3 虚函数的注意事项
  • 在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虛的。
  • 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。这种行为非常重要,因为这样基类指针或引用可以指向派生类对象。
  • 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
11.11.4.2.4 构造函数

构造函数不能是虛两数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以将类构造函数声明为虛的没什么意义。

11.11.4.2.5 Destructors

析构函数应当是虚函数,除非类不用作基类。

Employee * pe = new Singer; // legal because Employee is base for Singer
...
delete pe; // ~Employee() or ~Singer()?

如果使用默认的静态联编,delete 语句将调用~Employee()析构函数。这将释放由 Singer 对象中的Employee 部分指向的内存,但不会释放新的类成员指向的内存。但如果析构函数是虚的,则上述代码将先调用~Singer 析构函数释放由 Singer 组件指向的内存,然后,调用~Employee()析构两数来释放由 Employee组件指向的内存。

这意味着,即使基类不需要显式析构函数提供服务,也不应依赖于默认构造两数,而应提供虛析构两数,即使它不执行任何操作:
virtual ~Baseclass()

提示:通常应给基类提供一个虚析构西数,即使它并不需要析构西数

11.11.4.2.6 Friends

友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。如果由于这个原因引起了设计问题,可以通过让友元函数使用虛成员函数来解决。

11.11.4.2.7 没有重新定义

如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本。

11.11.4.2.8 重新定义导致隐藏方法
class Dwelling
{
public:
    virtual void showperks(int a) const;
    ...
};
class Hovel : public Dwelling
{
public:
    virtual void showperks() const;
    ...
};

Hovel trump;
trump.showperks(); // valid
trump.showperks(5); // invalid

新定义将showperks()定义为一个不接受任何参数的函数。重新定义不会生成函数的两个重载版本,而是隐藏了接受一个 int 参数的基类版木。总之,重新定义继承的方法并不是重载。如果在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何。

参数不同将会导致隐藏所有同名的基类方法。

这引出了两条经验规则:
第一,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返回类型协变 (covariance of return type ),因为允许返回类型随类类型的变化而变化。

class Dwelling
{
public:
    // a base method
    virtual Dwelling & build(int n);
    ...
};
class Hovel : public Dwelling
{
public:
    // a derived method with a covariant return type
    virtual Hovel & build(int n); // same function signature
    ...
};

第二,如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。
如果只重新定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。注意,如果不需
要修改,则新定义可只调用基类版本。

class Dwelling
{
public:
    // three overloaded showperks()
    virtual void showperks(int a) const;
    virtual void showperks(double x) const;
    virtual void showperks() const;
    ...
};
class Hovel : public Dwelling
{
public:
    // three redefined showperks()
    virtual void showperks(int a) const;
    virtual void showperks(double x) const;
    virtual void showperks() const;
    ...
};

11.11.4.2.9 override标识符

虚说明符override指出程序员要覆盖一个虚函数:将其放在参数列表后面。如果声明与基类方法不匹配,编译器将视为错误。

class Action
{
	int a;
public:
	Action(int i = 0) : a(i) {}
	int val() const { return a; };
	virtual void f(char ch) const { std::cout << val() << ch << "\n"; }
};
class Bingo : public Action
{
public:
	Bingo(int i = 0) : Action(i) {}
	virtual void f(char ch) const override { std::cout << val() << ch << "!\n"; }
};

virtual void f(char* ch) const override { std::cout << val() << ch << "!\n"; }

上面这句将导致编译错误。

说明符override并非关键字,编译器根据上下文确定他们是否有特殊含义;在其他上下文中,可将他们用作常规标识符,如变量名或枚举。

11.11.4.2.10 final标识符

禁止派生类重写特定的虚方法,可在参数列表后面加上final。

virtual void f(char ch) const final { std::cout << val() << ch << "\n";}

说明符final并非关键字,编译器根据上下文确定他们是否有特殊含义;在其他上下文中,可将他们用作常规标识符,如变量名或枚举。

11.11.5 访问控制: protected

关键字 protected 与 private 相似,在类外只能用公有类成员来访问 protected 部分中的类成员。private 和 protected 之间的区别只有在基类派生的类中才会体现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。

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

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

Brass 类被设计成只能通过 Deposit()和 Withdraw()才能修改 balance。但对于 BrassPlus 对象,Reset()方法将忽略WithdrawO中的保护措施,实际上使balance 成为公有变量。
警告:最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够访问基类数据。

然而,对于成员函数来说,保护访问控制很有用,它让派生类能够访问公众不能使用的内部函数。

11.11.6 抽象基类

11.11.6.1 ABC的定义

对于圆圈和椭圆的类定义,圆圈也是椭圆,因此考虑圆圈继承椭圆。但是这样会带来一个问题,圆圈只需要一个半径而椭圆需要长半长和短半长,而且椭圆涉及到旋转角度,但是旋转圆圈就没什么意义;对于椭圆的方法收缩椭圆,涉及到两个半长的收缩,如果圆圈继承收缩方法,不重写的话会将圆圈收缩成椭圆,这是不允许的。由此看来,还不如直接将圆圈定义为一个新的类而不是继承类。基于此,提出了ABC的方法。

还有一种解决方法,即从 Ellipse 和 Circle 类中抽象出它们的共性,将这些特性放到一个 ABC 中。然后从该 ABC 派生出 Circle 和 Ellipse 类。

C++通过使用纯虛函数 (pure virtual function)提供未实现的函数。纯虚函数声明的结尾处为=0。

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, ny) { x = nx; y = ny; }
    virtual double Area() const = 0; // a pure virtual function
    ...
}

当类声明中包含纯虚函数时,则不能创建该类的对象。这里的理念是,包含纯虛函数的类只用作基类。要成为真正的 ABC,必须至少包含一个纯虛函数。原型中的=0 使虚函数成为纯虛函数。

总之,原型中的=0指出这个类为抽象基类,这个类不用定义函数。但是C++甚至允许纯虚函数有定义。

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

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

ABC要求派生类覆盖其纯虚函数—迫使派生类遵循ABC设置的接口规则。

11.11.6.2 ABC的举例

acctabc.h

#pragma once
// 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

acctabc.cpp

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

main.cpp

// usebrass3.cpp -- polymorphic example using an abstract base class
// 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";
	return 0;
}

运行结果:

Enter client's name: Jasmine
Enter client's account number: 1
Enter opening balance: $99999999
Enter 1 for Brass Account or 2 for BrassPlus Account: 1
Enter client's name: Booo
Enter client's account number: 2
Enter opening balance: $9999999
Enter 1 for Brass Account or 2 for BrassPlus Account: 2
Enter the overdraft limit: $999
Enter the interest rate as a decimal fraction: 0.0123
Enter client's name: baba
Enter client's account number: 3
Enter opening balance: $99999999
Enter 1 for Brass Account or 2 for BrassPlus Account: 1
Enter client's name: mama
Enter client's account number: 6
Enter opening balance: $99999999
Enter 1 for Brass Account or 2 for BrassPlus Account: 2
Enter the overdraft limit: $9999
Enter the interest rate as a decimal fraction: 0.0123

Brass Client: Jasmine
Account Number: 1
Balance: $99999999.00

BrassPlus Client: Booo
Account Number: 2
Balance: $9999999.00
Maximum loan: $999.00
Owed to bank: $0.00
Loan Rate: 1.230%

Brass Client: baba
Account Number: 3
Balance: $99999999.00

BrassPlus Client: mama
Account Number: 6
Balance: $99999999.00
Maximum loan: $9999.00
Owed to bank: $0.00
Loan Rate: 1.230%

Done.

D:\Prj\C++\Abstract_Base_Classes\Debug\Abstract_Base_Classes.exe (进程 18408)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

11.11.7 继承和动态内存分配

11.11.7.1 Case 1: 继承类未使用new

假设基类未使用动态内存分配:

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

// derived class without DMA
class lacksDMA :public baseDMA
{
private:
    char color[40];
public:
    ...
};

现在不需要为lackDMA类自定义显式析构函数、复制构造函数、赋值运算符。

11.11.7.2 Case 2: 继承类使用new
// derived class with DMA
class hasDMA :public baseDMA
{
private:
    char * style; // use new in constructors
public:
    ...
};

现在必须为lackDMA类自定义显式析构函数、复制构造函数、赋值运算符。
咱就是说各管各的。

11.11.7.2.1 析构函数

派生类析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行工作的进行清理。
因此,hasDMA 析构函数必须释放指针 style 管理的内存,并依赖于baseDMA 的析构函数来释放指针 label管理的内存。

baseDMA::~baseDMA() // takes care of baseDMA stuff
{
    delete [] label;
}
hasDMA::~hasDMA() // takes care of hasDMA stuff
{
    delete [] style;
}

11.11.7.2.2 复制构造函数
baseDMA::baseDMA(const baseDMA & rs)
{
    label = new char[std::strlen(rs.label) + 1];
    std::strcpy(label, rs.label);
    rating = rs.rating;
}

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

11.11.7.2.3 赋值操作符重载
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;
}

11.11.7.3 case2 举例

dma.h

#pragma once
// 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

#include "dma.h"
#include <cstring>
// baseDMA methods
baseDMA::baseDMA(const char* l, int r)
{
	label = new char[std::strlen(l) + 1];
	strcpy_s(label, std::strlen(l)+1, l);
	rating = r;
}
baseDMA::baseDMA(const baseDMA& rs)  //这是一个复制构造函数
{
	label = new char[std::strlen(rs.label) + 1];
	strcpy_s(label, std::strlen(rs.label) + 1, 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];
	strcpy_s(label, std::strlen(rs.label) + 1, 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)
{
	strcpy_s(color, 39, c);
	color[39] = '\0';
}
lacksDMA::lacksDMA(const char* c, const baseDMA& rs)
	: baseDMA(rs) //使用member initialization list初始化基类部分
{
	strcpy_s(color, COL_LEN - 1, c);
	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];
	strcpy_s(style, std::strlen(s) + 1, s);
}

hasDMA::hasDMA(const char* s, const baseDMA& rs)
	: baseDMA(rs)
{
	style = new char[std::strlen(s) + 1];
	strcpy_s(style, std::strlen(s) + 1, s);
}
hasDMA::hasDMA(const hasDMA& hs)
	: baseDMA(hs) // invoke base class copy constructor
{
	style = new char[std::strlen(hs.style) + 1];
	strcpy_s(style, std::strlen(hs.style) + 1, 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];
	strcpy_s(style, std::strlen(hs.style) + 1, 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;
}

main.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;
	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: Mercator

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

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


D:\Prj\C++\Inheritance_Dynamic_Memory_Allocation\Debug\Inheritance_Dynamic_Memory_Allocation.exe (进程 6276)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

11.11.8 Class Design Review

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jasmine-Lily

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值