0%

深入探究C++

C++的继承与多态

接口继承与实现继承

派生类将基类中除去构造函数和析构函数的其他方法继承了过来。public继承概念由两部分组成,函数接口(function interfaces)继承和函数实现(function implementations)继承。作为类的开发人员,我们主要研究类的三种继承情况:
1、派生类只继承成员函数的接口(也就是声明),需要自己来重新定义该函数的实现;
2、派生类同时继承函数的接口和实现,但又希望能够覆写(override)它们所继承的实现;
3、派生类同时继承函数的接口和实现,并且不允许覆盖任何东西,只能利用父函数的实现;

1
2
3
4
5
6
7
8
9
10
11
class Shape{//形状
public:
virtual void draw() const = 0;
virtual void error(const std::string& msg);
int objectID() const;
...
};


class Rectangle:public Shape{...};//矩形
class Ellipse:public Shape{...};//椭圆

Shape是个抽象类,它的纯虚函数draw使它成为一个抽象类,所以客户不能够创建Shape class的实体,只能创建它的派生类的实体

Shape类声明了三个函数,第一个是draw,在视屏中划出当前对象,第二个是error,准备让那些“需要报导某个错误”的成员函数调用,第三个是objectID,返回当前对象的独一无二的整数识别码,每个函数的声明方式都不相同,draw是个纯虚函数(pure virtual),error是个虚函数( 简朴的(非纯)impure virtual函数),objectID是个非虚函数(non-virtual)函数。

纯虚函数通常有两个特点:它们必须被任何“继承了他们”的具象类重新声明;并且它们在抽象类中通常没有定义。
所以结论是:声明一个纯虚函数的目的是为了让派生类只继承函数的接口。

虚函数(简朴的impure virtual函数)背后的故事和纯虚函数(pure virtual函数)有点不同,一如往常,派生类继承其函数接口,但虚函数(简朴的impure virtual函数)会提供一份实现代码,派生类可能覆写(override)它,所以结论是:

声明虚函数(简朴的impure virtual函数)的目的是让派生类继承该函数的接口和缺省实现,考虑error函数,其接口表示,每个类都必须支持一个“当遇上错误是可调用”的函数,但每个类可自由处理错误,若某个类不想针对错误做出任何特殊行为,它可以退回到Shape类提供的缺省错误处理行为。但是允许虚函数(简朴的impure virtual函数)同时指定函数声明和函数缺省行为,却有可能造成危险,考虑下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//XYZ航空公司的飞机继承体系,该公司只有A型和B型两种飞机,两者都以相同方式飞行,因此XYZ设计的继承体系为:
class Airport{...};


class Airplane{
public :
virtual void fly(const Airport& destination);
...
}


void Airplane::fly(const Airport& destination)
{
//缺省代码,将飞机飞至指定的目的地
}


class ModelA:public Airplane{...};
class ModelB:public Airplane{...};


//现在,新增加一个C型飞机,C型和A型、B型的飞行方式不同,XYZ公司的程序员在继承体系中针对C型飞机加了一个类,但由于急于让飞机上线,竟然忘了定义其fly函数:


class ModelC:public Airplane{
... //为声明fly函数
}

若代码中出现如下操作:
Airport PDX(…);//PDX是机场名字
Airplane* pa = new ModelC;

pa->fly(PDX);//调用Airplane::fly

这将酿成大祸,这个程序试图以ModelA或ModelB的飞行方式来飞ModelC。解决该问题的要点在于切断“虚函数接口”和其“缺省实现”之间的连接,下面是一种做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Airplane{
public:
virtual void fly(const Airplane& destination) = 0;
...
protected:
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination)
{
// 缺省行为,将飞机飞至指定的目的地
}


//现在ModelA和ModelB调用的飞行的缺省实现为:
class ModelA:public Airplane{
public:
virtual void fly(const Airport& destination)
{
defaultFly(destination);
...
}
};


class ModelB:public Airplane{
public:
virtual void fly(const Airport& destination)
{
defaultFly(destination);
...
}
};




//现在ModelC class 不可能意外继承不正确的fly实现代码,因为Airplane中的纯虚函数迫使ModelC必须提供自己的fly版本:




class ModelC:public Airplane{
public:
virtual void fly(const Airport& destination);
{
...
}
};


void ModelC::fly(const Airport& destination)
{
//将C型飞机飞至指定目的地
}

最后考虑Shape的非虚函数objectID()。若成员函数是个非虚函数,意味着它并不打算在派生类中有不同的行为,实际上非虚成员函数所表现的不变性远重要于特异性,因为它表示不论派生类变得多么特异化,就其自身而言,它的行为都不可以改变。

1、接口继承和实现继承不同。在public继承之下,派生类总是继承基类的接口;

2、纯虚函数只是具体指定接口继承;

3、虚函数( 简朴的(非纯)impure virtual函数)具体指定接口继承及缺省实现继承;

4、非虚函数(non-virtual函数)具体指定接口继承以及强制性实现继承。

总结

接口继承:派生类只继承函数的接口

实现继承:派生类同时继承函数的接口和实现

虚函数是重载的一种表现方式,是一种动态的重载方式。

非虚函数:继承该函数的接口和一份强制性实现,继承类必须含有某个接口,必须使用基类的实现

虚函数:会继承该函数的接口和缺省实现。继承类必须含有某个接口,可以自己实现,也可以不实现,而采用基类定义的缺省实现。

纯虚函数:纯虚函数在基类中没有定义,接口继承。含有纯虚函数的类无法实例化。要求继承类必须含有某个接口,并对接口函数实现。

多态与继承

继承访问修饰符

继承方式有三种——public、protected和private,不同的继承方式对继承到派生类中的基类成员有什么影响? 总的来说,父类成员的访问限定符通过继承派生到子类中之后,访问限定符的权限小于、等于原权限。其中,父类中的private成员只有父类本身及其友元可以访问,通过其他方式都不能进行访问,当然就包括继承。protected多用于继承当中,如果对父类成员的要求是——子类可访问而外部不可访问,则可以选择protected继承方式。

父子类中同名元素

overload重载

函数重载有三个条件,一函数名相同,二形参类型、个数、顺序不同,三相同作用域。根据第三个条件,可知函数重载只可能发生在一个类中

overhide隐藏

在派生类中将基类中的同名成员方法隐藏,要想在派生类对象中访问基类同名成员得加上基类作用域。(注意,如果该同名方法在基类中实现了重载,在派生类对象中同样需要指定作用域,而不能通过简单的传参,调用带参重载方法)

override函数覆盖

基类、派生类中的同名方法 函数头相同(参数、返回值),且基类中该方法为虚函数,则派生类中的同名方法将基类中方法覆盖。函数隐藏和函数覆盖都是发生在基类和派生类之间的,可以这么理解:基类和派生类中的同名函数,除去是覆盖的情况,其他都是隐藏的情况。

引用与指针

. 基类对象和派生类对象

派生类对象可以赋值给基类对象,基类对象不可以赋值给基类对象;对于基类对象和派生类对象,编译器默认支持从下到上的转换,上是基类,下是派生类。

基类指针(引用)和派生类指针(引用)

基类指针(引用)可以指向派生类对象,但只能访问派生类中基类部分的方法,不能访问派生类部分方法。派生类指针(引用)不可以指向基类对象,解引用可能出错,因为派生类的一些方法可能基类没有。

虚函数

分析:当Base类中有虚函数时,不论是Base类还是Derive类,它们的大小都增加了4个字节。并且当Base指向Derive对象时,Base的类型却变为Derive,不再和指针本身的类型相关,这是怎么回事呢?

虚函数指针

实际上,Base和Derive类增加的4个字节就是虚函数指针的大小,每一个类只要有虚函数(包括继承而来的),它就有且只有一个虚函数指针,类的大小就是总的成员变量的大小加上一个虚函数指针的大小。虚函数指针指向的是一张虚表,里面是这个类所有虚函数的地址,一个类对应一张虚函数表,而虚函数指针存在于每一个对象中,并且永远占据对象内存的前四个字节。

虚函数表又称为“虚表”,它在编译期间就已经确定,在程序运行时就会被装载到只读数据段,在整个程序运行期间都会一直存在。一个类实例化的多个对象,它们 的虚函数指针指向的是同一张虚表。

虚函数要求

成员函数能实现为虚函数需要满足两个前提条件: 1.成员方法能取地址 2.成员方法依赖于对象。第一点毋庸置疑,虚函数表中需要存储虚函数的地址。第二点,我们怎么调用虚函数的?通过虚函数指针来找到虚表从而调用其中的方法,而虚函数指针又存在于对象中,所以这就意味着虚函数的调用需要依赖对象。

那么,我们可以确定一些不能实现为虚函数的方法: 1.构造函数——构造函数就是用来创建对象的,如何将其实现为虚函数,使其依赖一个对象调用? 2.inline函数——内联函数直接在调用点展开,不能取地址 3.static方法——静态方法是属于整个类的,不依赖与单个对象。

成员函数能实现为虚函数需要满足两个前提条件: 1.成员方法能取地址 2.成员方法依赖于对象。第一点毋庸置疑,虚函数表中需要存储虚函数的地址。第二点,我们怎么调用虚函数的?通过虚函数指针来找到虚表从而调用其中的方法,而虚函数指针又存在于对象中,所以这就意味着虚函数的调用需要依赖对象。

前面我们探讨了那些不能实现虚函数的情况,析构函数是可以的。那么什么时候应该将析构函数实现为虚函数呢?答案是:当基类指针指向堆上开辟的派生类对象时。

静态绑定发生在编译阶段、动态绑定发生在运行阶段。

-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!