一起了解C++虚函数与多态

前言

看到虚函数,是不是脑子里就跑出来很多关键字?virtual?override?巴拉巴拉的。

动态绑定

这个词很唬人,不过确实很好理解。假设我有一个父类Father

class Father{
public:
	virtual void someFunction(){std::cout << "father";}
};

然后我再写一个子类继承自父类

class Son:public Father{
	void someFunction(){std::cout << "son";}
};

好了,现在咱们搞个函数:

void justUse(Father& v)
{
	v.someFunction();
}

事实上,这个函数接受的输入为父类的引用(指针也是一样的效果),但是咱们既可以输入父类对象,又可以输入子类对象,且执行的就是父类或者子类的someFunction。
总而言之,在C++中,当我们使用基类的引用或者是指针调用一个虚函数时,将发生动态绑定。这个过程是发生在运行期间的,而不是编译期!!!

virtual与override

举个例子,比如父类写了一个virtual虚函数,那么子类可以重新实现一遍。而为了让读者直观的看出,子类的这个函数确实是virtual虚函数,于是子类在实现这个虚函数的时候也加上virtual的关键字。
这听起来确实有点麻烦,有没有好的规则或方法?有的,C++规定,父类中声明的virtual虚函数,在其所有的子类中,都将是虚函数,也就是说,子类哪怕不加virtual也是可以的!
那咋让读者一眼就看出来,子类这个函数确实是虚函数呢?下面有请override登场!
override其实就只有两个作用:
1.它用来告诉编译器,“嘿,看看,我修饰的这个函数可是个虚函数,编译器,你赶快去查一下它的父类是不是有这样的一个虚函数,要是没有的话,那你赶紧报错别让它编译通过啊!”
2.它用来告诉读者,“嘿,兄弟,这个函数是个虚函数哦,从父类开始就虚了哦”
那么可不可以virtual和override一起用呢?当然可以啊,没人会拦着你的。

访问限定符和一些继承关系

相信各位对public private和protected一定不陌生,咱们一起回顾一下:在对外这件事情上(指类外部通过类的实例化对象访问类内成员),protected和private是没有什么区别的,都不可见。而在继承关系上就不太一样了,首先,子类不能访问父类的private成员,但是可以访问其protected成员。public继承方式可以不改变父类的任何限定属性,而private继承则将父类完全封盖成private,子类就无法访问了,僵硬。不过protected就好一点,它将父类的public和protected全部变成protected,这样子类就可以在类的内部访问父类这些东西了。(private确实是碰不到摸不着,除非你搞个friend友元)

final关键字

这是c++11新增的东西,它呢有两个作用:
1.用于修饰类,表示这个类无法作为其他类的基类(也就是无法被继承)
2.用于类中函数,表示这个函数就到此为止了,后续的子类不准override它。写个小例子:

  class A{
  virtual void test(int) const;
}

class B : A
{
    void test(int) const final;//不允许后续其他类覆盖test;
}

静态类型与动态类型

这个概念其实还是非常简答的。有人说,多态,其实就是创建一个父类的指针,指向的是子类的实例化对象。也就是说,明面上看,这个指针是父类的指针类型,但是这个家伙指向的实际内存中,却老老实实躺着一个子类的实例化对象。这件事情其实就很能说明问题,父类指针类型是其静态类型,而实际的子类对象才是动态类型,是非得程序运行起来才知道的一件事情。
因此总结一下,基类的指针或者是引用的静态类型,很有可能和动态类型是不一样的。

内存布局

咱们聊了这么长时间的虚函数,那么究竟带有虚函数的类的实例化对象在内存中是如何存储的呢?
比如我们有一个class,里面有funcA和funcB,就俩普通函数,不是啥virtual,另外还有一个int var的变量,于是乎:
代码区:存放funcA与funcB,且这是所有class类的对象所共同拥有的;
堆区:int var;要知道,类的成员函数虽然是代码区被所有类的实例化对象共享,但是类的成员变量可是类的实例化对象独有的。
那么如果出现了虚函数,情况会如何变化呢?
还是那两个函数,只是funcA变成了虚函数,即virtual funcA
代码区:存放funcB,而funcA当然也在代码区,只是暂时不晓得具体的地址
堆区:int val,同时还多了一个四字节的东西,叫虚函数表指针(不得不说,这个指针也是类的实例化对象所独有的)
全局数据区:*funcA。这就很有灵性了,其实这个地方放的是虚函数表,根据查询这个虚函数表,从而找到这个funcA究竟在代码区的哪个地方。
搞得这么复杂干嘛?还虚函数表。。。。。。其实这也是没办法的事情,为了实现动态绑定,再兼顾一下扁平化,也就这个机制性能不错了。

构造函数和析构函数可以是虚函数么?

构造函数不可以是虚函数。刚刚咱们才了解了动态绑定的机制,得创建虚函数表,创建虚函数表指针,这些可都是在类对象创建的时候做的,这个时候你把构造函数搞成虚函数,但是你虚函数的这一套机制又没创建起来。。。。。。铁子,算了吧。
析构函数不仅可以是虚函数,且我推荐最好写成虚函数。为啥呢?你想啊,动态绑定的时候,指针类型可是父类的啊,但是内存里躺着的可能是子类,那么你把这个父类指针交给delete,完了,人家直接调用父类的析构函数,那咱们内存里子类独有的东西咋办啊???所以,懂?

纯虚函数和抽象类

这个东西我第一次接触,不是在学校,而是工作第二年的某一天。我看delphi的代码时,看到了interface,但是想了想,c++好像没有interface的关键字,我把这个问题抛给杨大哥,他让我查查抽象类。
网上一直用这个例子来讲道理:我想描述动物园里的动物,于是我先创建了一个动物父类,然后很多子类都是继承自这个“动物”父类。您想想,有必要创建一个父类“动物”对象么?其实显然是没有的,即使是有,那也没有为这个父类“动物”实现里面的虚函数方法的必要,您说是吧。所以啊,这个“动物”父类,慢慢的,就变成了,一个接口。它呢,只定义一些虚函数的声明,但是它自己不会去实现掉,只需要子类各自去实现即可。
由此引出纯虚函数的概念:纯虚函数是在基类中声明的虚函数,它要求任何派生类都要定义自己的实现方法,以实现多态性。实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数。与虚函数相比,纯虚函数强制规定,父类不必为这个纯虚函数提供缺省实现,同时规定,子类必须实现这个纯虚函数。
纯虚函数的写法也非常简单,就在虚函数的后尾加上=0即可。
含有纯虚函数的基类就变成了抽象类,这没啥好理解的。

重写与重载

其实中文如何翻译我没搞清楚也没想搞清楚。就是override和overload这俩的区别。说的简单一点,override是发生在父子类中的,子类可以去花式实现父类的虚函数。而overload就是,大家的函数名称是一样的,只是形参不一样(可以是形参类型,或者是形参数量),然后编译器就根据输入参数的类型去匹配不同的函数,这就是overload。

猜你喜欢

转载自blog.csdn.net/weixin_44039270/article/details/106626668
今日推荐