C++ Primer Plus学习笔记-第十三章:类继承

C++ Primer Plus学习笔记-第十三章:类继承

前言:本章主要探讨类继承相关技术细节,在实际进入探讨之前我们需要观察这样一个继承实例:

#ifndef TABTENN1_H_
#define TABTENN1_H_
#include <string>
using std::string;

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

class RatePlayer : public TableTennisPlayer
{
private:
	unsigned int rating;
public:
	RatePlayer (unsigned int r = 0, const string & fn = "none", const string & ln = "none", bool ht = false);
	RatePlayer(unsigned int r, const TableTennisPlayer & tp);
	unsigned int Rating() const {return rating;};
	void RestRating (unsigned int r) {reting = r;};
};

#endif

上述文件描述了两个类的定义,其中一个继承自另一个;以下是继承可以完成的工作:

  • 可以在已有类的基础上添加新功能
  • 可以给类添加数据
  • 可以修改基类方法的行为
    实现继承的方法是:
class RatedPlayer : public TableTennisPlayer
{
...
}

派生类包含基类对象;使用公有继承,基类的公有对象会成为派生类的公有成员,私有成员会成为继承类的私有成员;
看得出,继承是由冒号,继承修饰符,基类组成的;继承对象具有这些特性:

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

以下是需要向继承类中添加的元素:

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

派生类不能直接访问基类的数据成员,需要通过基类的方法;在使用构造函数创建一个继承类对象之前,首先需要创建基类的类对象:

RatedPlayer::RatedPlayer(unsigned int r, const sting & fn, const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
	rating = r;
}
/*创造继承类前,首先需要创建基类,但程序运行到函数体中时,往往意味着基类继承类已经创建完成;但因为基类还没有被创建,因此这样的语句是不合适的;在函数头后加上一个冒号就可以在执行函数体之前调用基类的构造函数,另外需要注意基类构造函数的参数列表中的变绿全部都必须来自继承类构造函数的参数列表*/

实际上也可以选择不手动执行基类的构造函数,但这样做的后果是:编译器将自动添加构造函数,二这不一定符合设计;因此继承类构造函数的要点是:

  • 首先创建基类对象
  • 派生类构造函数应通过成员初始化列表将基类的信息传递给基类构造函数
  • 派生类构造函数应初始化派生类新增的数据成员
    **另外:派生类对象过期时,程序将首先调用派生类析构函数,再调用基类析构函数;这和创建是的顺序正好相反,也可以看出两者储存早栈中;

派生类可以使用基类的方法,只要它不是基类私有的;另外指向基类的指针或引用其实也可以指向继承类,虽然指针一般要求和被指向的对象匹配,但这是个例外;

类继承可以是多态的,这意味着继承类中可以修改基类的细节:

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

另外,在类继承中可能会定义一个和基类方法同名的类方法,这个方法会覆盖基类中的方法;对于一般的类对象,这么做是没有问题的;但后面这种特殊情况带来一个问题:指向基类的引用或指针也可以指向派生类,调用某个方法时会使用哪个方法呢?一般规律是:

  • 对于一般的类方法,程序会按照指针对应的类型调用类方法
  • 对于类函数定义中有virtual关键字修饰的,程序会调用真实所指向的类的方法

注意:若基类中某public方法被修饰为virtual,那么所继承的方法也是virtual属性的;

虚函数在析构函数领域相当有用;如果一个基类的引用是指向继承类的,那默认的析构函数将不能正确释放空间;将析构函数修饰为virtual,才能对合适的对象调用合适的析构函数;另外,如果要在派生类中重新定制方法的行为,最好也将基类的方法修饰为virtual的;

关于构造函数和其它方法函数:两者使用了不同的技术,导致前者可以访问私有对象而后者不能访问私有对象;

派生类构造函数在初始化基类私有数据时,采用的是成员初始化列表语法;可以将派生类构造函数的信息传给基类构造函数,具体而言是这样操作的:

BrassPlus::BrassPlus(const string & s, long an, double bal, double ml, double r) : Brass(s, an, bal)
{
	maxLoan = ml;
	owesBank = 0.0;
	rate = r;
}

另外需要注意的是:非构造函数不能使用成员初始化列表语法,但派生类方法可以调用公有的基类方法;

有时虽然为派生类定义了修改过的基类方法,但有时会需要使用基类的方法,这是实现的方法:

void BrassPlus::Withdraw(double amt)
{
...
	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";
...
}

另外让我们复习一下cout的格式控制方法;下面这个文件将cout输出数字的方式设置为顶点表示法后将设置信息返回供保存;

format setFormat()
{
	return cout.serf(std::ios_base::fixed,std::ios_base::floatfield);
}
//这段代码创建了一个返回数据类型为format的函数
//它将cout设置为定点表示法后会将最初的状态返回

下面这个函数利用之前收集的格式信息重置cout的状态:

void restore(format f,precis p)
{
	cout.setf(f, std::ios_base::floatfield);
	cout.precision(p);
}
//format是cout.setf()返回的数据类型
//precis是cout.precision()使用的数据类型

虚构函数对一般的析构函数不是必须的,但是对于通过使用new申请内存并使用delete释放内存的虚构函数是十分必要的;对这些使用堆存储数据的对象,使用虚析构函数是十分必要的;

函数名联编指将源代码中的函数调用解释为执行特定的函数代码块;由于函数重载,这项任务相当复杂;

静态联编也称早期联编,是指在编译时决定时使用哪个函数;动态联编也称晚期联编,是指在执行过程中决定执行函数的哪个重载版本;

将派生类引用或指针转换为基类引用或指针被称为向上强制转换;派生类在公有继承中得到基类所有方法和数据成员,所得到的成员也可以在公有继承中传递给下游派生类;反之称为向下强制转换,通常是不允许的,除非有相关类方法完成显式强制转换;

注意:按值传递时不会存在向上强制转换,只有传递引用或是指针时才存在向上强制转换;

编译器对非虚方法采用静态联编,对虚方法采用动态联编;动态联编使得开发者能够重新定义类方法;但动态联编的效率不高,所以默认情况下使用静态联编;

编译器处理虚函数的方法是:给每个对象添加一个隐藏成员,包含了一个指向函数地址数组的指针;这种数组称为虚函数表,储存为类对象进行声明的虚函数地址;例如:基类对象包含一个这样的指针,派生类也包含一个这样的一个指向独立地址表的指针;如果派生类提供了新的虚函数定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该指针将保存函数原始版本的地址;如果派生类定义了虚函数,则该函数的地址也将被添加到派生类的函数指针数组中;

对于虚函数引入的时间空间成本,主要来自:

  • 每个对象都将增大,增大的部分为存储地址的空间
  • 对于每个类,编译器都将创建一个虚函数的地址表(数组)
  • 对于每个函数调用,都需要执行“到表中查找”这一额外操作

以下是有关虚函数的注意事项:

  • 在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的
  • 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法;这成为动态联编或晚期联编
  • 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的

注意:构造函数不能是虚函数,析构函数应当是虚函数!

另外,友元不能是虚函数,因为它本质上**不是类成员,只是拥有特殊的访问权限;**可以通过让友元函数使用虚成员函数来解决;

在派生类中,重新定义基类的方法会将原来的版本覆盖;重新定义不是重载基类方法,只是简单的覆盖罢了;如果重新定义一个版本,那么其它版本也会被覆盖隐藏并无法使用;注意如果不需要修改,则新定义可以调用基类的版本;就像这样:

void Hovel::showperks() const {Dwelling::showperks();}

实际上类中的数据成员有3种状态,之前介绍了private和public,实际第三种是protected;它的含义是:在基类中的表现和private成员一样,但在继承类中仍然可以直接访问,而private在继承类中是只能通过基类的公有接口来访问的;

抽象基类是类层次设计中一个非常重要的设计理念,它包括一个特性,那就是纯虚函数:\

class BaseEllipse
{
private:
	double x;
	double y;
	...
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;//=0是纯虚函数的标志
}

当某个类中包含纯虚函数时,不能创建这个类的实例;纯虚函数理论上讲会在派生类中完善;纯虚函数的意思是,这个函数可以没有任何定义,但向它提供定义 也是没问题的;

除了基类可能使用new和delete之外,派生类中也可能有新加入的使用new和delete的成员;当使用了这种基于堆的内存分配时,往往影响三种函数:析构函数,构造函数,复制构造函数,重载赋值运算符;下面不考虑派生类中不使用new的情况,因为这没有意义;

当派生类使用了new时,它的构造函数首先需要使用初始化成员参数列表在进入函数体之前创建好基类,然后再函数体中完成派生部分的构建;

当派生类使用new申请空间时,析构函数的情况是这样的:

baseDMA::~baseDMA()
{
	delete [] label;
}

hasDMA::~hasDMA()
{
	delete [] style;
}
/*实际上继承类的析构函数不需要显式的新增什么环节,因为在对继承类的类对象使用析构函数时,基类那部分的析构函数是自动调用的*/

那么复制构造函数呢?它需要一些特殊技巧来保证正常工作:

//基类的复制构造函数
baseDMA::baseDMA(const baseDMA & rs)
{
	lable = 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);
}

接下来是赋值运算符:

//基类的赋值运算符重载
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 has & 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;
}

友元函数面临比较尴尬的境地,它本质上不算是成员函数,因此继承派生和它实际上是不相关的;想在派生类中使用基类的友元函数,不能通过作用域解析运算符实现,唯一的办法是强制类型转换:

std::ostream & operator<<(std::ostream, const hasDMA & hs)
{
	os << (const baseDMA &) hs;//强制类型转换
	os << "Style:" << hs.style << endl;
	return os;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值