C++12:多态

目录

多态的概念:

多态的实现

 隐藏和多态的区别

 协变

 为什么推荐父类析构函数加上virtual关键字?

​编辑

 override 和 final关键字

 如何创建一个不能被继承的类?

抽象类

虚函数表

 多态的原理

 静态绑定

动态绑定

​编辑


多态的概念:

不同的对象执行相同的动作的时候产生不一样的效果,这一点有一点像函数重载,但是不一样的是我们控制的是传入的参数,而不是不同的对象。

我们其实可以将这个行为形象的理解为"双标"。

以“买东西”为例,不同的人购买商品的时候可能会因为身份的不同导致付出的金钱也不相同。

那么如何实现多态呢?

多态的实现

  • 实现多态的前提是继承,不仅需要继承,还需要满足“三同”,并且实现多态的函数必须是虚函数。
  • 在函数名称前面加个virtual形成虚函数,虚函数需要满足三同,父类子类函数,名字相同,参数相同,返回值相同
  • 这种效果称之为重写或者覆盖,一定要满足以上三同,满足三同条件还不算完,必须以父类的指针或者引用调用函数产生多态效果
  • 当函数没有添加virtual关键字的时候,就不构成重写而是隐藏
  • 子类实现多态时,其实现多态的函数可以不加virtual,但是这是一种不太好的行为,我们能加上就加上。

 隐藏和多态的区别

多态也称之为重写或者覆盖,对比隐藏,重写/覆盖的需求更多

隐藏只需要函数名相同,但是重写需要三同还有虚函数,他们的关系像一个同心圆


演示:

创建三个类来模拟不同的人购买商品的行为产生不同效果

class Person 
{
public:
	virtual void  OderFood()
	{
		cout << "普通顾客购买商品--正常价" << " ";
		cout << endl;
	}
private:
	string s = ("顾客");
};

class VIP : public Person
{
	virtual void OderFood() 
	{
		cout << "VIP顾客购买商品--8.5折" << " ";
		cout << endl;

	}
};

class Friend : public Person
{
	virtual void OderFood()
	{
		cout << "朋友购买商品--3折" << " ";
		cout << endl;
	}
};

 协变

 协变的应用范围很小,不做细述

  • 派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
  • 三同中,返回值可以不同但是返回值必须是父子类关系的指针或者引用,
  • 不符合规则的返回值会报错

 遵从规则后,构成多态

 其他类型的父子类型指针也是可以的

 为什么推荐父类析构函数加上virtual关键字?

 我们早就在继承学习的时候了解过,为了保证析构顺序的正确性,我们在子类编写析构函数时,不需要去显示调用父类的析构函数,编译器会自己帮助我们进行析构操作,顺序是先析构子类再析构父类。

但是很多文章都建议对父类的析构函数增加一个virtual关键字,这是为什么?

  • 加上了virtual关键字后,父类的析构会与子类的析构构成多态,而多态的析构函数可以解决一些特殊情况

用父类类型去存放子类对象的时候,是一个特殊情况

 坏了,子类对象的内存泄漏了,原因是为何?

  • 我们知道delete的底层是调用operator delete(),此时传递的参数类型是父类对象的指针也就是Father*,而然后delete再去调用自定义类型的析构函数,但是Father类内部的析构函数只能析构它自己的成员变量,造成了内存泄漏
  • 那么,多态可以解决这个问题,传入什么对象就调用对应的析构函数,那么我们就在父类的析构函数前面加上一个vitutal。

但是问题来了,构成多态的条件是三同,虽然说返回值以及参数析构函数都是一样的,但是名字怎么办?

  • 编译器在调用继承的析构函数的过程中,并不会像正常函数一样直接调用,而是转换成一个同名的析构函数destructor。这样,就成功构成了多态,delete使用的也是一个父类的指针去调用了虚函数,此时成功构成了多态的效果并且成功析构了子类的内存空间。

 override 和 final关键字

overide关键字可以检查派生类虚函数是否按照语法规则重写了基类中的某个虚函数,如果没有重写则报错

 final,使用final关键字修饰虚函数,表示当前的虚函数不能被重写

 如何创建一个不能被继承的类?

  • 1.构造私有
  • 父类的构造函数一旦私有,继承时,子类无法调取父类的构造函数,进而无法生成对应父类的成员。
  • 2.类定义时加上final
  • final修饰的类对象意为最终类,最终类不可被继承,不过虚函数的本意就是被重写,fianl使用的场景非常的少

总而言之:final置于父类使其变成最终类无法被继承,override则检查子类是否完成重写构成多态。

抽象类

 在一个虚函数后面加上=0,这个函数就变成纯虚函数,包含纯虚函数的对象成为抽象类,抽象类不能实例化出对象,其派生类也不行,只有重写了纯虚函数,子类才能构建出对象。

 重写抽象类内部的纯虚函数后,子类可以成功生成对象

那么纯虚函数的意义体现在哪?

纯虚函数使得派生类的虚函数必须重写,体现了接口继承。

一些概念:

  1. 子类不实现父类所有的纯虚函数,则子类还属于抽象类,仍然不能实例化对象
  2. 抽象类可以定义指针,而且经常这样做,其目的就是用父类指针指向子类从而实现多态

以下是一个算得上阴间的题目

我们直接理解的话,会认为是一个多态调用,故而认定答案选C,但是这题的陷阱程度非常之高,这题选B。

看似非常不搭边的答案是如下过程得来的。

首先,程序以B类生成了一个对象并且将其对应堆上的空间用p指针保存了起来,接着,调用当前对象内部的text函数。根据继承,我们知道此时B对象内部的模型应该是这样的

一个继承自父类的text函数,一个重写完毕的func函数

这里注意!之所以引起误解的点就在这个func函数,这里有个非常关键的问题,这个在B类内部的text函数,它里面调用的func究竟是谁的?

以我们初见的视角来看,它调用了B类当前的func函数也就是B->func

但是!这是一个继承自父类的函数,我们知道,如果直接在类内部调用它自己的成员函数,那么this指针将会代指自身的类,那么在这里其实这个func应该是A* ->func()

我们回过头过去看A内部的func,其缺省值val给予的是1

那么为什么打印的是B的内容?

这里就与多态的真实“重写”的概念有关,多态构成的重写,针对的是实现,也就是花括号内部函数的实现方法,并不会更改缺省值,此时构成了重写,但是指针调用的是A*,故而构成了及其难以理解的答案B->1;

这道题的出题人可谓是非常的过分了,其考多态的本意方向不错可是陷阱却如此难以察觉!不过也在这段分析之中加深了理解,也算是不错的经历。

虚函数表

 直观的了解虚函数表的存在就是使用sizeof。

这样的一个对象,用结构体的对齐知识进行计算,我们很容易得到这个Text的大小应该是8

但是打印其大小则是12

这里就是虚函数表存在的证明了,当某一个类内部有虚函数时,编译器会为个类生成一个虚函数表,这个表是一个函数指针数组,也就意味着并不是每加一个虚函数整体大小就会+4或者8

virtual funciton ptr 监视窗口可以看得见

这个指针数组的指针存放的就是虚函数的地址了。

 

这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的?

答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多人都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。

虚函数存放在代码段内部对象内部的虚表存放的是函数地址。

虚函数表在编译阶段就早已生成,而虚表指针则是在构造函数的初始化列表阶段才初始化

总结一下派生类的虚表生成:

a.先将基类中的虚表内容拷贝一份到派生类虚表中

b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

d.多继承的时候,就会可能有多张虚表

e.父类对象的虚表与子类对象的虚表没有任何关系,这是两个不同的对象

f.一个类的不同对象共享该类的虚表

 多态的原理

那么神奇的多态是怎么实现出来的?

以以下两个类举例

 它们在监视窗口中的虚函数指针数组中的地址,实现多态的虚函数地址是不同的。

  • 那么我们可以认为,虚函数的重写是先将父类的虚函数地址拷贝到子类中,然后再对实现了多态的虚函数进行覆盖,我们之前也提过,重写也可以称之为覆盖。那么这里的行为就非常好理解了。
  • 重写指代了语法层面的多态行为,继承了父类的接口,对其进行重写。
  • 而覆盖则是指代了底层层面的多态行为,先将父类的虚函数表拷贝到子类,然后查找哪一个虚函数实现了重写,针对其虚函数进行覆盖。

 静态绑定

class Text
{
public:
    virtual void Func1()
    {
        cout << "父类::Func1" << endl;

    }
    virtual void Func2()
    {
        cout << "父类::Func2" << endl;

    }
    void Func3()
    {
        cout << "父类::Func3" << endl;

    }
};
class t : public Text
{
public:
    virtual void Func1()
    {
        cout << "子类::Func1" << endl;

    }

    void Func3()
    {
        cout << "子类::Func3" << endl;

    }
};

 当我们对以上的类进行一次普通的分类调用,父类对象和子类对象各自调用它们自己的Func3

得到的答案没什么问题

而当置换一种方法进行调用的时候得到的结果则会不同。

那么这种行为就是普通调用,没有实现多态的Func3被视为普通调用之后,调用的函数就会以类型为准,也就是父类为准,调用的都是父类的Func3

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,
比如:函数重载
动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体
行为,调用具体的函数,也称为动态多态

动态绑定

 而当我们使用多态调用的时候,则是能成功调用到子类的Func1()

那么它们的区别在哪?或者问,编译器是怎么实现分辨出这个函数是不是多态的?

我们到反汇编里面看一下它们在栈帧中的行为

这个是普通调用

 这个是多态调用

很显然,多态所需要执行的指令更多

  • 那么在这个过程中,多态调用没法直接确定类型,所以它需要其他的方式寻找到被重写的虚函数。
  • 具体的行为则是:编译器对父类的类型进行调用,只能看到当前对象中父类的那一部分,这个行为对子类也是相同,在子类的对象中也只能看见父类的那一部分。但是这并没有封死编译器检查当前虚函数表内部函数地址的不同来调用不同的函数。

 

 那么总结一下:多态的实际实现可以简单理解为调用当前对象内部的虚函数表内部不同的函数地址以实现多态。

那么有一个问题,为什么普通调用没法触发多态?非要用父类的指针或者引用才能实现多态?直接辨别不同的类型实现多态不是很方便吗?或者换言之,由于多态的实现是依靠虚表内部函数地址的不同而实现的,那么普通对象之间的调用虚表不应该也是不同的吗?为什么不能实现多态?

但是问题就发生在使用父类对象的时候究竟会不会拷贝虚表?或者说,能不能允许拷贝的情况发生?

  • 我们分析一下就知道假如真的允许拷贝,那么会产生错误,一个父类的对象,接收了来自另一个父类对象的赋值拷贝,假如其中的成员变量以及虚表都拷贝了过去,这没什么,毕竟接收的对象类型是一样的,但是一旦是一个子类,并且将虚表拷贝到父类对象里面,那不就"夺舍"了吗?通俗来讲,假若说允许以父类对象实现多态,那么一个父类对象很有可能会产生二义性,因为一旦被一个子类赋值,它外面看上去是一个父类,但是内部是子类了,所以不能。那么只能换一种方法实现了,那就是使用父类的指针或者引用。

  1.  inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
  2. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
  3. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
  4. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。
  5. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚数表中去查找。
  6. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)

 到这,多态的大致记述就结束了,由于多继承的模型过于复杂,没有选择记述。

        

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值