《C++ Primer Plus》读书笔记 第13章 类继承

第13章 类继承

1.派生一个类

下面是一个表示乒乓球会员的类:

class TableTennisPlayer
{
private:
	string firstname;
	string lastname;
	bool hasTable;
public:
	TableTennisPlayer(const string & fn = “none”,
  		const string  & ln = “none”, bool ht = flase);
	void Name() const;
	bool HasTable() const { return hasTable; }
	void ResetTable(bool v) { hasTable = v; }
};

将TableTennisPlayer类作为一个基类,派生出一个RatedPlayer类:

class RatedPlater : 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; }
};

冒号指出RatedPlayer类的基类是TableTennisPlayer类。public表明TableTennisPlayer是一个公有基类,这被称为公有派生使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问

RatedPlayer对象将具有以下特征:

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

派生类需要添加下面这些特性:

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

派生类的构造函数必须使用基类的构造函数。创建派生类对象时,程序首先创建基类对象。这意味着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表语法来完成这种工作:

RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
	const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
	rating = r;
}

必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数

派生类的构造函数的要点如下:

  • 首先创建基类对象;
  • 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;
  • 派生类构造函数应初始化派生类新增的成员。

释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数。

2.派生类和基类之间的关系

派生类对象可以使用基类的公有方法

RatedPlayer rplayer1(1140, “Mallory”, “Duck”, true);
rplayer1.Name();

基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象

RatedPlayer rplayer1(1140, “Mallory”, “Duck”, true);
TableTennisPlayer & rt = rplayer;
TableTennisPlayer * pt = &rplayer;
rt.Name();
pt->Name();

基类指针或引用只能用于调用基类方法,因此,不能使用rt或pt来调用派生类的方法。

通常,C++要求引用和指针类型与赋给的类型匹配,但这一规则对继承来说是例外。然而,这种例外只是单向的,不可以将基类对象和地址赋给派生类引用和指针

由于基类引用和指针可以指向派生类对象。那么下列情况都是合法的:

  • 基类引用定义的函数或指针参数可用于基类对象或派生类对象。
  • 形参为指向基类的指针的函数可以使用基类对象的地址或派生类对象的地址作为实参;
  • 可以将基类对象初始化为派生类对象。这会将派生类对象中嵌套的基类对象初始化为该基类对象。
  • 可以将派生对象赋给基类对象。

3.多态公有继承

在派生类时,可能会遇到希望同一个方法在派生类和基类中的行为是不同的。即方法的行为应取决于调用该方法的对象。这种行为称为多态——具有多种形态,即同一个方法的行为随上下文而异。有两种机制可用于实现多态公有继承:

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

如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的,即在方法声明前加上关键字virtual。被声明为virtual的方法称为虚方法

方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法。然而,在派生类声明中也可以用关键字vritual来指出哪些函数是虚函数

如果方法是通过引用或指针而不是对象调用,则该方法是否为虚方法将会有所不同:如果不是虚方法,程序将根据引用类型或指针类型选择方法;如果是虚方法,程序将根据引用或指针指向的对象的类型来选择方法

关键字virtual只用于类声明的方法原型中,而方法在类外定义时不能使用virtual。

在派生类中需要用到基类方法时,标准技术是使用作用域解析运算符来调用基类方法。如果该方法是虚方法,则必须使用作用域解析运算符来调用,否则编译器无法确定是基类方法还是派生类方法;如果该方法不是虚方法,可以不使用作用域解析运算符。

在基类中使用虚析构函数可以确保正确的析构函数序列被调用。如果派生类包含一个执行某些操作的析构函数,则基类必须有一个虚析构函数,即使该析构函数不执行任何操作。

4.静态联编和动态联编

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

在编译过程中进行联编被称为静态联编,又称为早期联编

在程序运行时进行联编被称为动态联编,又称为晚期联编

将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这使公有继承不需要进行显式类型转换。向上强制转换是可传递的。

将基类指针或引用转换为派生类指针或引用被称为向下强制转换。如果不使用显式类型转换,则向下强制转换是不允许的。

隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。C++使用虚成员函数来满足这种要求。

总之,编译器对非虚方法使用静态联编,对虚方法使用动态联编。

动态联编为使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销。所以静态联编的效率更高。C++的指导原则之一是,不要为不适用的特性付出代价。仅当程序设计确实需要虚函数时,才使用它们。

通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表。虚函数表中存储了为类对象进行声明的虚函数的地址。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该虚函数表将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址也将被添加到虚函数表中。无论类中包含了几个虚函数,都只在对象中添加一个地址成员,只是表的大小不同而已。调用函数时,程序将查看存储在对象中的虚函数表的地址,然后转向相应的函数地址表。

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

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

5.虚函数注意事项

  • 构造函数不能是虚函数。派生类不继承基类的构造函数,所以将类构造函数声明为虚的没什么意义。
  • 析构函数应当是虚函数,除非类不用做基类。即使基类不需要显式析构函数提供服务,也不应依赖于默认构造函数,而应提供虚析构函数,即使它不执行任何操作。
  • 友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。
  • 如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本
  • 重新定义不会生成函数的两个重载版本,而是会将老版本的函数隐藏。如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。这种特性被称为返回类型协变如果基类声明被重载了,则应在派生类中重新定义所有的基类版本

6.protected访问控制

类成员的访问权限有三类:public, private, protected

关键字protected和private相似,在类外只能用公有类成员来访问protected部分中的类成员。

protected和private不同之处在于,派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。

因此,对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。

基类的保护成员会成为派生类的保护成员

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

7.抽象基类

C++通过使用纯虚函数提供未实现的函数。纯虚函数声明的结尾处为0。当类声明中包含纯虚函数时,该类被称为抽象基类(abstract base class, ABC)。不能创建抽象基类的对象。纯虚函数可以没有定义

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

8.继承和动态内存分配

下面是一个baseDMA基类:

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

如果基类使用了动态内存分配,派生类不使用动态内存分配

class lacksDMA : public baseDMA
{
private:
	char color[40];
public:
	...
};

不需要为派生类定义显式析构函数、复制构造函数和赋值运算符

如果基类使用了动态内存分配,派生类也使用了动态内存分配:

class hasDMA : public baseDMA
{
private:
	char * style;
public:
	...
};

必须为派生类定义显式析构函数、复制构造函数和赋值运算符。定义方法如下:

派生类析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行的工作进行清理:

hasDMA::~hasDMA()
{
	delete [] style;
}

派生类的复制构造函数不能访问基类的私有数据,因此它必须调用基类的复制构造函数:

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

这里的成员初始化列表将一个hasDMA引用传递给baseDMA构造函数。因为基类引用可以指向派生类型,所以baseDMA复制构造函数将使用hasDMA参数的baseDMA部分来构造新对象的basDMA部分。

派生类的显式复制运算符必须负责所有继承的基类对象的赋值,这可以通过显式调用基类复制运算符来完成这项工作:

hasDMA & hasDMA::operator=(const hasDMA & 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;
}

语句baseDMA::operator=(hs);实际上的含义为:

*this = hs;

编译器将使用hasDMA::operator=(),从而形成无限递归调用。使用函数表示法使得赋值运算符被正确调用。

9.基类中的友元函数

友元函数不能被继承。那么派生类如何使用基类的友元?例如,基类baseDMA中有一个友元:

friend std::ostream & operator<<(std::ostream & os, const baseDMA & rs);

派生类hasDMA中有一个友元:

friend std::ostream & operator<<(std::ostream & os, const hasDMA & rs);

hasDMA类的友元能够访问style成员,但不是baseDMA的友元,不能直接访问成员labla和rating。这需要使用baseDMA类的友元函数operator<<(),并使用强制类型转换:

std::ostream & operator<<(std::ostream & os, const hasDMA & hs)
{
	os << (const baseDMA &) hs;
	os << “Style:<< hs.style << endl;
	return os;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值