C++Prime Plus(4)

69.继承

在一个(单继承)或多个(多继承)已有类的基础上,扩展属性或方法,形成一个更强大的类。

新名词
基类:已有的类
派生类:扩展后的类

优点
代码重用,并且可以重用一个实现不公开的类;

单继承格式

struct/class 派生类名 : 派生方法 基类名
{
	新增的数据成员和成员函数; 
}

首先设计一个简单的基类,设计一个乒乓球俱乐部的会员类:
fig1
fig2
现在要再设计一个类:参加过当地锦标赛的会员类

设计上的考虑:是一个会员,必须包含会员的所有属性和行为;
增加属性:比赛的排名rating;
增加行为:

  • 构造参加过锦标赛的会员
  • 设置排名
  • 返回排名

fig3
由于派生类的方法不能直接访问基类的私有成员,所以我们借助派生类的构造函数初始化列表对基类进行初始化(在初始化列表中调用基类的构造函数或拷贝构造函数)
fig4


对于公有派生的派生类构造函数
1.派生类不能直接访问基类的私有成员,需要使用基类的构造函数;
2.基类对象必须先被构造,如果没有使用基类的构造函数,程序将调用基类的默认构造函数;
3.为了能先构造出基类对象,C++一般在派生类的构造函数中使用成员初始化列表来构建并初始化基类对象。

派生类的析构函数
当派生类被释放时,程序会先调用派生类的析构函数,再调用基类的析构函数。


70.重定义基类的函数

同一个方法在派生类和基类中有不同的行为。例如:
fig5
基本账户信息包括:
fig6
基本账户类成员函数实现:
fig7
重要账户类定义:
fig8
重要账户类成员函数的实现:
fig9
派生类重定义的函数覆盖了基类的同名函数,即派生类对象调用这两个函数时,调用的是派生类重定义的函数。

71.多态与公有继承

多态性(polymorphism)可以简单地概括为“一个接口,多种方法”,它是面向对象编程领域的核心概念。

多态性指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。

1.编译时多态性(静态多态):通过重载函数实现:先期联编 early binding
2.运行时多态性(动态多态):通过虚函数实现 :滞后联编 late binding

C++运行时多态性是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(Override),或者称为重写。

多态的目的:封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了“接口重用”。也即,不论传递过来的究竟是类的哪个对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。

多态最常见的用法就是声明基类类型的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。因为没有多态性,函数调用的地址将是固定的,因此将始终调用到同一个函数,这就无法实现“一个接口,多种方法”的目的了。

注意
1.只有类的成员函数才能声明为虚函数,虚函数仅适用于有继承关系的类对象。普通函数不能声明为虚函数。
2.静态成员函数不能是虚函数,因为静态成员函数不受限于某个对象。
3.内联函数(inline)不能是虚函数,因为内联函数不能在运行中动态确定位置。
4.构造函数不能是虚函数。
5.析构函数可以是虚函数,而且建议声明为虚函数,除非不被当作基类使用。

公有继承
公有继承是最常用的派生方法,派生类是一类特殊的基类。

基于前面重定义的方法,现在我们想对某类对象(包括基类和派生类)执行某个操作,我们可以用指向基类的指针或基类对象的引用遍历所有对象

但是存在问题:基类指针或基类对象的引用只能解释基类部分(以及派生类内的基类部分),不能解释派生类重定义的部分。

为了解决上述问题,我们应该使用虚函数(加virtual)实现多态性。

基本账户类定义
fig10
当基类指针指向派生类对象,并对指针调用虚函数时,编译器会到派生类中寻找函数原型,若不存在,则执行基类函数。

重要账户类定义
fig11
派生类中virtual可以不加,因为编译器默认:与基类虚函数同名的函数也是虚函数。

多态性的例子
fig12

72.protected成员

成员的访问控制分为:public,private,protected。
protected成员是一类特殊的私有成员,可以被派生类的成员函数访问,提高派生类成员函数访问基类成员的方便性。

比如之前内容定义的基本账户类Brass,我们当时声明了成员为私有,所以访问或操作成员只能借助Brass类的成员函数实现。这其实在执行时增加了开销:
fig13
如果我们将基类Brass的成员声明为protected,BrassPlus就能直接操作基类成员。

73.抽象基类

在基类中,有一类函数叫虚函数,虚函数的作用是用于实现多态性,从基类可以派生出很多派生类,在派生类中可以重定义虚函数,也可以不重定义这个虚函数。当我们用基类指针指向基类对象或者派生类对象,有了虚函数,我们才能实现接口的动态调用。

虚函数中存在一种函数:纯虚函数
在基类中声明的虚函数,在该基类中我们不去定义它,但我们会在派生类中定义。

纯虚函数的声明如下:

virtual 类型 函数名 (参数列表) = 0; 

实例:
fig14
抽象类
如果一个类中至少有一个纯虚函数,则该类被称为抽象类

抽象类的使用:
1.抽象类只能作为其他类的基类,不能建立抽象类对象;
2.可以声明指向抽象类的指针或引用,此指针可指向它的派生类,从而实现多态性;
3.如果派生类中给出了基类所有纯虚函数的实现,则该派生类不再是抽象类,否则仍然是抽象类。

下面程序用于计算各类形状的总面积
fig15
fig16
抽象类的意义:提供一种更系统,更规范化的开发方法(在设计类的时候,可以规范一族类的行为)

74.包含对象成员的类

每个类都包含数据成员,目前为止,我们定义的类的数据成员都是C++的内置类型变量。其实,数据成员也可以是一个类的对象。

例如定义一个学生类,包含属性有:
fig17
所以,学生类包含了两个其他类的对象.

示例如下:
fig18
fig19
fig20

75.私有继承

对于公有继承,基类的公有成员和保护成员在派生类中是公有的和保护的;
对于私有继承,基类的公有成员和保护成员在派生类中都是私有成员,即:基类的方法不再是派生类接口的一部分,但是派生类的成员函数可以使用它们。

这与上一节中将对象作为成员的功能相同,区别在于:
成员对象:有命名的对象;
私有继承:未命名的基类对象;

同样以上一节的学生类为例,我们得到另一种实现方式,相比上一节代码,修改部分如下:
fig21
fig22

76.多重继承

在定义类的时候,可以利用派生。相比之前只从一个类进行继承,我们也可以从多个类上进行继承。对于这种从多个类上进行的扩展,我们称为多重继承。

在多重继承中,我们有多个基类,多重继承格式如下:

class 派生类名 : 派生方法 基类名1, 派生方法 基类名2,{ 新增派生类的数据成员和成员函数 };

多重继承要解决的问题
1.如何区分从不同基类继承的同名函数;
fig23
对于第一个问题:如何区分从不同基类继承的同名函数
解决方案一:指定方法所属的基类

x.A::f()
x.B::f()

缺点:使用C类对象的程序必须知道C类时如何实现的;

解决方案二:
在C类中重新封装两个f函数

class C : public A, public B
{
public:
	void g();
	void h();
void fA() { A::f(); }
void fB() { B::f(); }
}

2.从不同的基类中继承了同一个类的多个实例;
fig24
首先,有没有必要让一个对象包含两个people类实例?没有必要
解决方案:虚基类
使从多个类(这些类有共同的基类)派生出的对象只有一个基类对象,共同派生时共享同一个基类副本。

格式:派生方法前加virtual
fig25
此时,称people是teacher和doctor的虚基类。

虚基类由最终的派生类直接构造
fig26
执行teacher和doctor的构造函数时,将不再调用people的构造函数
换言之,不使用虚基类,最终派生类调用构造函数时,会构造其基类与这些基类共同的基类,导致冲突。有了虚基类,才能实现各自独立的构造。

77.类模板(1)定义及使用

类模板是泛型程序设计的方式,是代码重用的一种方法,类模板可以让不同类型,同样行为的类共享代码。

以ATM模拟为例,最开始我们定义了队列类:
fig27
为了重用代码,我们需要定义Customer类,并改名为Item,才能重用Queue
fig28
为了去除typedef这个操作,我们可以使用C++类模板:template <class Item>或者template <typename Item>,此时包含Item的类就是一个类模板

然后再定义队列类:
fig29
在实现Queue类的方法时,其中的方法都变成了函数模板,在实现时需要在类名后面加模板参数:
fig30
注意:类模板的方法实现必须在类模板声明的头文件下,否则会出现链接错误

模板类的使用

类模板的实例化:用真正的类代替类模板的模板参数,形成一个真正的类。
对象定义:

类模板名<模板参数实际类型> 对象名(实际参数列表);

比如:Queue<Customer> line(20);

对象的使用:和普通对象的使用完全一致。


类模板和函数模板:使用template<typename T>template<class T>(模板参数)作为类型T(包括内置类型和类类型)的占位符,调用时用具体类型代入形成真正的类或者函数


78.类模板(2)非类型参数

在定义类的时候,如果类中某些属性的类型没有确定下来,我们可以将类定义为类模板,没有确定下来的属性类型就设计为模板参数,模板参数除了可以是类类型,也可以是C++内置类型;

通常我们将固定为内置类型的模板参数称为非类型参数,比如:template<int MAX>

类模板通常被用作容器:容器即将相同的存储方案用于不同类型场景,比如Queue就是一个容器,Queue中的元素Item被泛型化,可以用不同的具体类型代入。

当容器大小确定时,大小也可以作为模板参数。

栈类设计
比如我们设计一个栈类,这是一个后进先出的容器,栈的存储可以基于数组也可以基于链表,如果基于数组,数组大小在编译时就是一个确定值。

数据成员:
1.数组:大小作为模板参数
2.栈顶指针
成员函数:
进栈,出栈,判断栈为空,判断栈为满

fig31
类模板定义:
fig32
再次强调:与普通类的实现不同,类模板的方法实现必须在类模板声明的头文件下,否则会出现链接错误

类模板的使用:
fig33
非类型参数的注意事项
实际参数一定是编译时的常量;
非类型参数的实际参数只能是整型,枚举,指针类型;
类方法中不能修改非类型参数值,也不能引用它的地址;

类模板结合非类型参数可以帮助我们设计大小确定的容器,比如我们自定义的数组

79.类模板(3)类模板实例

下面内容为类模板实现的实例,在C++数组中,不检查下标的有效范围,比如我们定义数组a[10],下标有效范围是0-9,但是我们操作a[11]时,不会中断操作,这导致程序出现意外的错误。所以,我们要设计一个检查下标范围的安全数组。

注意下标运算符[ ]的重载,[ ]是一个二元运算符,在C++中,a[k]其实是a[]k。
fig34
fig35

80.类模板(4)模板的多功能性

类模板是某些成员函数或数据成员的类型还没有确定下来的类,当类型确定后,类模板就变成了普通类。类模板具有多功能性。

功能一:具有普通类的功能–作为基类
类模板也可以像普通类一样,作为其他类的基类,比如在上一节中,我们定义了检查下标范围的安全数组类,现在我们可以派生一个下标范围可以指定的安全数组(不局限在0到n-1)。
fig36
fig37
功能二:普通类的功能—作为类的成员
fig38
功能三:普通类的功能—作为类模板参数
fig39
功能四:递归使用模板
fig40

81.类模板(5)模板的具体化

类模板必须经过实例化才能成为真正的类,类模板可以隐式实例化,也可以显式实例化;

隐式实例化:定义对象时给出模板的实际参数
比如:

ArrayTP<int, 10> arr;
ArrayTP<int, 10> *arrp;
arrp=new ArrayTP<int, 10>;

隐式实例化只是分配了空间,不会生成成员函数的实例,只有当成员函数被调用时才会被实例化。

显式实例化:生成完整的类模板实例,包括所有成员函数
声明方法:
在程序的类声明的前面加上显式实例化声明:

template class 类名<模板的实际参数列表>;

比如现在我们定义了一个ArrayTP,新增了一个成员函数foo
fig41
对于以下程序,程序正常编译,正常执行,因为没有用到函数foo。
fig42
但是如果加上显式实例化声明,编译会出错,因为会自动实例化foo成员函数,但double类型不能执行取模运算(取模运算只能用于整型)。
fig43
类模板的具体化 或 类模板的特化
与实例化相对应,类模板还有具体化(或称为特化),类模板的特化与函数模板的特化都是为了缩小泛型编程的范围。

类模板的特化可以理解为:对特定类型参数去定义其专属功能的同名类模板;

例如,一个排序各种元素的数组类:

template <class T>
class sortarray {};

通常,排序时用的是>和<进行比较,但对于C风格的字符串,必须用strcmp函数进行比较,因此,我们需要为C风格字符串定制一个特化版本:

template <> class sortarray<const char*> {};

在使用时,形如 sortarray<int> a1; 就用于普通的版本,而 sortarray<const char*> a2; 就用于定制的版本(为C风格字符串定制的);

另外,类模板支持部分特化,注意与78.类模板(2)非类型参数中的内容进行区分;

比如对于:
fig44
我们可以将T3具体化为int:
fig45
和函数模板的特化类似,编译器先检查是否是普通类;再检查是否是类模板特化(通过类模板的参数列表检查,如果出现对应的参数类型,就认为是类模板特化);再检查是否是普通的类模板。

82.类模板(6)友元

对于普通的类,我们可以定义友元(友元是可以访问类私有成员的函数或类)

友元分类:
友元函数:全局函数
友元类:另一个类
友元成员函数:另一个类的成员函数

对于类模板,我们也可以定义友元,类模板的友元分为三类:非模板友元,约束模板友元,非约束模板友元;

非模板友元
声明格式:
fig46
作用:类B和全局函数f是类模板A所有实例的友元,B的所有成员函数和全局函数f可以访问类模板A的所有实例的私有成员。

约束模板友元:一个类模板或者一个函数模板是另外一个类模板的友元(注意,还需满足模板参数一致时才是友元)
模板参数一致时是友元,例如,为ArrayTP重载输出(新头文件下改名FArrayTP):
fig47
fig48
非约束模板友元:不管模板参数是否相同,都可以是类模板的友元
比如,输出两个ArrayTP类对象的第一个元素,两个对象可以有不同的模板实际参数(新头文件下改名FArrayTP)
fig49
fig50

83.友元详解

友元:允许其他的函数或者其他类的成员函数访问我们类的私有成员。友元分为三大类:友元函数,友元类,友元成员函数。

友元函数:允许访问类私有成员的全局函数,通常是作为运算符重载函数。


友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字friend


友元类:允许友元类的所有成员函数访问私有成员,通常是专门设计的服务类。

友元成员函数:允许访问私有成员的某个类的某个成员函数。


实际上,友元在我们的类内无所谓公有私有,只要我们在类内进行声明就行


友元类:为某个类定制的工具
比如:电视机是一个类,为该电视机量身定做的遥控器也是一个类,遥控器为了控制电视机需要直接访问某个内部功能,遥控器应该改设为电视机的友元。

友元类的声明:friend class 类名;
可以出现在类定义中的任何地方,建议放在最前面或者最后面。

友元类可以用于物联网编程,比如我们先定义电视机类,遥控器类作为其友元类:
fig51
电视机类的成员函数实现如下:
fig52
物联网控制模拟为:
fig53

84.嵌套类

嵌套类:在另一个类中定义的类;

嵌套类通常用于帮助另一个类,例如:在队列Queue中定义Node
fig54
嵌套类的访问权限
与普通成员一样:
1.定义在private部分:只有所在的类可以使用;
2.定义在protected部分:所在类和派生类可用;
3.定义在public部分:所有函数,类都能用;

私有或保护内的嵌套类的使用:嵌套类在访问控制方面是独立的,嵌套类所在类的成员函数只能访问嵌套类的公有部分;如需访问嵌套类的私有部分,必须在嵌套类中将所在的类声明为友元。(类的友元可以访问类的私有部分)

公有嵌套类的使用

所在类名::嵌套类名

fig55
什么场合需要公有嵌套类
不同的类需要类似的工具,如:不同的电视机需要不同的遥控器,而不同的遥控器需要不同的类名,如果遥控器都是相同的功能(只是操作对象是不同的电视机),实现起来就会很重复:
fig56
但是我们使用嵌套类,公有嵌套类可以让不同的遥控器使用同样的类名:
fig57

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值