c++编程(21)——类与对象(7)——多态

欢迎来到博主的专栏:c++编程
博主ID:代码小豪


多态

多态可以简单理解为一个接口的多种形态,多态的特性可以分为动态多态和静态多态,c++中多态包含了这两种特性,在c++面对对象编程特性中,体现更多的是动态多态,因此本篇首先来讲讲动态多态

多态可以为不同的数据类型提供统一的接口,举一个现实中的例子,那就是乘客买车票时,如果你是成人,那么就是全价票,如果是学生,则是半价票,如果是儿童或老人,那么可能就是半价或者免费的车票。

在上述的例子当中,买票这一接口是固定的,但是根据不同的数据类型(买票的人),会有不同的执行效果,在编程中的体现就是多态了。

多态的定义和形成条件

在c++中想要实现多态,首先要达到的条件是类之间构成继承关系(即多态的对象是基类和派生类)。

博主以动物的叫声为例

设计一个基类animal,在设计一个成员函数sound,当调用这个sound时,会在输出这个动物的叫声。

然后设计派生类dog和派生类cat,dog发出的叫声和cat发出的叫声当然不一样,于是代码如下:

class animal
{
	void sound() { cout << "animal sound"; }
};

class dog :public animal
{
	void sound() { cout << "wolf wolf"; }
};

class cat :public animal
{
	void sound() { cout << "meow meow"; }
};

现在,我们调用dog对象的sound函数和调用cat对象的sound函数就会有不同的效果。

dog dog1;
cat cat1;
dog1.sound();//wolf wolf
cat1.sound();//meow meow

那么这是否说明cat和dog构成多态了呢?答案为否。因为多态需要我们调用同一个接口sound,但是dog的sound函数和cat的sound函数虽然名字一致,但是接口并非统一。这里引出构成多态的第二个要素,虚函数。(第一个要素是类之间要构成继承体系。)

虚函数

虚函数的关键字是virtual,将其成员函数的声明之前加上virtual以形成虚函数。除了构造函数以外的非静态成员函数都能声明成虚函数。如果在基类中将某个函数声明为虚函数,那么在其派生类当中也会将其视为虚函数。

在基类animal的sound函数声明之前加上virtual构成虚函数。

class animal
{
public:
	virtual void sound() { cout << "animal sound"; }
};

如果派生类存在与基类同名的函数,如果同名的是普通成员函数,构成隐藏。如果是虚函数,则构成重写(也可称覆盖)。比如当animal的sound函数成为虚函数时,派生类dog和cat的sound函数则自动构成对虚函数sound的重写。对于在派生类中重写的虚函数,可以加上virtual,也可以不加virtual。都算是完成了派生类的虚函数对基类的重写。

class dog :public animal
{
public:
	void sound() { cout << "wolf wolf"; }//对于sound的重写
};

class cat :public animal
{
public:
	virtual void sound() { cout << "meow meow"; }//对于sound的重写
};

在c++11中新增了关键字override,用于说明该虚函数是对基类虚函数的重写。使得重写的虚函数变得更加直观。同时也能让编译器检查override修饰的虚函数是否完成了重写

class dog :public animal
{
public:
	void sound() override { cout << "wolf wolf"; }//对于sound的重写
};

如果不想某个虚函数被重写,可以用关键字final修饰虚函数。此时派生类无法重写该虚函数。

class animal
{
public:
	virtual void sound()final { cout << "animal sound"; }
};

通常来说,重写的虚函数加上virtual和override以表达该虚函数是完成某个虚函数的重写,可以提高代码的可读性。

多态怎么写?

前面讲了很多关于构成多态的条件
1、必须是构成继承体系的基类与派生类
2、基类的某些的函数是虚函数,并且在派生类中完成了重写。
而现在,dog,cat和animal也是成功的构成了多态,那么多态到底是怎么用呢?

多态是可以使用统一的接口来处理不同类型的数据,因此先来设计一个统一的接口。

void animalsound(animal& animal)
{
	animal.sound();
}

animalsound函数接收一个animal基类类型的引用参数,然后调用该引用参数的sound函数。我们来思考一个问题,如果该函数参数基类animal的引用指向其派生类dog和cat,会发生什么结果。

dog dog1;
cat cat1;
animalsound(dog1);//传入的参数类型是dog
animalsound(cat1);//传入的参数类型是cat

如果cat、dog与animal之间没有联系的话,会导致传入的参数与形式参数的类型错误而导致编译报错。但如果cat、dog与animal形成了继承关系,就能使基类的指针或引用指向派生类对象(前一篇文章提到)。

如果派生类与基类构成继承关系,且派生类中存在对基类虚函数的重写,就能通过基类的指针或引用来调用派生类中重写的函数

这么说有点绕口,但是我们仔细观察上述代码。
(1)cat和dog是基类animal的派生
(2)cat和dog的成员函数sound完成了对animal中虚函数sound的重写
(3)在统一接口animalsound中,用基类animal的引用来指向派生类dog和cat

符合构成多态的条件,因此在函数中的参数虽然是animal类型的引用,但是实际调用的sound函数是cat和dog中对于基类中的虚函数sound的重写。

animalsound(dog1);//wolf wolf
animalsound(cat1);//meow meow

那么这么做的好处是什么?因为如果我想调用这两个函数,我直接创建cat和dog这两个对象就行了,何必这么麻烦用到多态的性质呢?

好处1:提高代码的复用性,如果不设计多态,那么就需要设计出调用cat类函数的接口,也要设计出调用dog类的接口,利用多态的特性只需要设计统一的接口即可

好处2:便于维护,如果我需要在代码中新增一个派生类,比如设计出cow这个类,利用多态,可以减少对原有代码的修改。

抽象基类

基类animal代表的是动物的整体,因此基类animal的sound函数本质上是没有意义的函数,因为动物的叫声是千奇百怪的,没有一种声音可以完全代表整个动物界的叫声。因此为虚函数sound写出实现是一个多于的事情

通过上面的分析可知,基类animal中的虚函数sound根本不需要定义,本质上我们根本就不需要去创建animal对象。代入现实的情况就是,没有一个对象具备动物的所有的特征,这个animal只是一个抽象的概念,即动物的整体概念,将其实例化完全没有意义。

纯虚函数

既然基类animal的虚函数sound不需要定义,那么我们可以将其声明为纯虚函数,纯虚函数不用定义,只需在函数的声明加上=0,就可以将一个虚函数声明为纯虚函数。如下:

class animal
{
public:
	virtual void sound() = 0;//纯虚函数
};

拥有纯虚函数的基类称为抽象基类。抽象基类负责定义接口,提供给派生类重写这些接口的具体实现。抽象基类不能被实例化出对象。

多态的实现原理

我们尝试写一个基类base,如下:

class Base
{
public:
	virtual void func1() { cout<<"func1()" << endl; }
	virtual void func2() { cout << "func2()" << endl; }
	void func3() { cout << "func3" << endl; }
private:
	int _n;
};

接着我们运行下列代码。

cout << sizeof(Base);

如果在你对对台缺乏了解的情况下,你可能会认为Base的大小应该是4字节,但是运行结果可知,在32位系统下,Base的大小为8字节,在64位的系统下,Base的大小为16字节。这又是为什么呢?

我们打开调试窗口。
在这里插入图片描述
可以发现,基类Base多了一个指针_vfptr。这个指针并非是我们自己定义的,那么这个指针代表着什么呢?

虚函数表

每个构成多态的基类和派生类的对象都会有一个隐藏成员指针_vfptr。这个指针指向一个函数指针数组,这个函数指针数组的元素指向虚函数,我们将这个数组称为虚函数表

我们可以通过调试代码来验证这个观点,先创建一个Base对象,然后调用虚函数func1.

Base b1;
b1.func1();

在这里插入图片描述再打开反汇编
在这里插入图片描述
可以看到,b1调用func1的地址是061311h,而虚函数表的第一个元素也是061311h,这说明func1的地址储存在虚函数表的第一个元素。

这代表着,虚函数表会记录当前类中的所有虚函数的地址,当对象调用虚函数时,会在虚函数表当中查找。

我们定义一个基类Base的派生类Derive。只重写func1,不重写func2,然后对比和基类的虚表有什么区别。

class Derive :public Base
{
public:
	virtual void func1() override{ cout << "Derive::func1" << endl; }
};

在这里插入图片描述
对比Base和Derive的虚表可知

(1)由于Derive重写了func1,所以在Base虚表内的关于func1的地址和Derive虚表内关于func1的地址不同
(2)Derve并未重写func2,因此在Base和Derive的虚表当中,func2的地址一致

由此得出规律。定义了虚函数的类都拥有虚表,如果派生类重写了基类的虚函数,就会改变虚函数表中相关虚函数的地址,反之,则与基类共用一个虚函数。

实现多态原理依靠于虚函数表,之所以基类的引用或指针可以调用派生类重写的虚函数,原因就在于,调用虚函数时是直接访问虚函数表中存储的空间地址。而不同派生类重写的不同虚函数的地址并不同,但是在相对于虚表中的位置相同。通过访问虚表的方式就能实现访问不同函数的功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

代码小豪

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

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

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

打赏作者

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

抵扣说明:

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

余额充值