C++ 的面向对象三个特性 --继承、封装、多态的实现 ===》编译时多态 && 虚函数是什么,虚函数怎么实现,什么时候用虚函数

C++支持两种多态性:编译时多态性,运行时多态性。

  • 编译时多态性:通过重载函数实现。
  • 运行时多态性:通过虚函数实现。

1. 虚函数和多态的关系:

虚函数 往往 用于 实现 C++的多态性 

2.  什么是运行时多态:

(1)Wiki定义 : 指计算机程序运行时,相同的消息 发送给 多个不同的类别对象,系统可依据对象所属类别,引发 该对应类别的方法,而有不同的行为。

简单来说,多态 就是指   相同的消息  给予  不同的对象 会引发 不同的动作

==》 常见的使用场景 :父类指针 指向 子类实例,调用函数时,实际调用的是 指针指向的实际类型,即子类 的成员函数 。

多态性 使得程序只有 在 运行时 才能动态确定 到底是执行哪个函数,

            而 不是在编译时静态确定的

3. 什么是 虚函数:

虚函数则是加了 virtual 修饰词的类的成员函数


4. 一个 虚函数 和普通函数使用的比较:

 可以总结为:“当使用类的指针调用成员函数时,

                       普通函数指针类型 决定,

                       虚函数 由  指针指向的实际类型 决定”。

C++ 的面向对象三个特性 --继承、封装、多态的实现 ===》编译时多态 && 虚函数是什么,虚函数怎么实现,什么时候用虚函数_第1张图片

[上述代码解释]:在上述例子中,我们首先定义了一个基类base,基类有一个名为vir_func的虚函数,和一个名为func的普通成员函数。而类A,B都是由类base派生的子类,并且都对成员函数进行了重载。然后我们定义三个base*类型的指针Base、a、b分别指向类base、A、B。可以看到,当使用这三个指针调用func函数时,调用的都是基类base的函数。而使用这三个指针调用虚函数vir_func时,调用的是指针指向的实际类型的函数。最后,我们将指针b做强制类型转换,转换为A*类型,然后分别调用func和vir_func函数,发现普通函数调用的是类A的函数,而虚函数调用的是类B的函数。

以上,我们可以得出结论“当使用类的指针调用成员函数时,普通函数由指针类型决定,而虚函数由指针指向的实际类型决定”。   


5. 虚函数是怎么实现的:

对于一个 只包含非静态成员变量和普通成员函数的类,

如“class C {void fun_a();void fun_b();int var};”,   其内存布局如下图:

C++ 的面向对象三个特性 --继承、封装、多态的实现 ===》编译时多态 && 虚函数是什么,虚函数怎么实现,什么时候用虚函数_第2张图片

 其中 成员函数放在代码区,为该类的所有对象公有,即不管新建多少个该类的对象,所对应的都是同一个函数存储区的函数

而成员变量则为各个对象所私有,即每新建一个对象都会新建一块内存区用来存储var值。

在调用成员函数时,程序会根据类的类型,找到对应代码区所对应的函数并进行调用。

在文章开头的例子中,Base、a、b都是base类型的指针。调用普通函数时,程序根据指针的类型到类base所对应的代码区找到所对应的函数,所以都调用了类base的func函数,即指针的类型决定了普通函数的调用。

==》 对于虚函数,又是怎么实现的呢?

对于“class D{void func_a(); virtul void func_b(); int var}” 这个类,其内存布局为:

C++ 的面向对象三个特性 --继承、封装、多态的实现 ===》编译时多态 && 虚函数是什么,虚函数怎么实现,什么时候用虚函数_第3张图片

这时如果sizeof一个类D的对象,会发现比类C的对象大4个字节。

多出来的这4个字节就是实现虚函数的关键----虚函数表指针vptr

虚函数表指针vptr 指向一张名为 “虚函数表”(vtbl)的表,而表中的数据则为函数指针,存储了虚函数fun_b()具体实现所对应的位置。

注意,普通函数、虚函数、虚函数表都是同一个类的所有对象公有的

           只有 成员变量 和 虚函数表指针vptr 是每个对象私有的,sizeof的值也只包括vptr和var所占内存的大小(也是个常出现的问题),并且vptr通常会在对象内存的最起始位置。

        另外,当类有多个虚函数时,仍然只有一个虚函数表指针vptr,而此时的虚函数表vtbl中会有多个函数指针,分别指向对应的虚函数实现区域。 

类的各个实例 共享 类 的 虚函数表,但,每个实例 都有一个属于自己的成员,vptr 指向 该类的虚函数表 【且,虚表指针是对象的第一个数据成员】

》一个非常清晰的文章:明明白白——虚函数,虚指针,虚表,虚继承 - 简书 (jianshu.com)


一些常见 虚函数 相关 问题:

1. 构造函数和析构函数可以是虚函数吗

-- 答案是构造函数不能是虚函数,析构函数可以是虚函数且推荐最好设置为虚函数

下面我们来看看为什么。首先,我们已经知道虚函数的实现则是通过对象内存中的vptr来实现的。而构造函数是用来实例化一个对象的,通俗来讲就是为对象内存中的值做初始化操作。那么在构造函数完成之前,vptr是没有值的,也就无法通过vptr找到作为虚函数的构造函数所在的代码区,无法执行 属于这个类自己的构造虚函数  , 所以构造函数只能作为普通函数存放在类所指定的代码区中。那么为什么析构函数推荐最好设置为虚函数呢?如文章开头的例子中,当我们delete(a)的时候,如果析构函数不是虚函数,那么调用的将会是基类base的析构函数。而当继承的时候,通常派生类会在基类的基础上定义自己的成员,此时我们当时是希望可以调用派生类的析构函数对新定义的成员也进行析构啦。

对于构造函数和析构函数与虚函数的关系我们也可以从另一方面来理解。通常是如例子中base *a=new(A)这样创建一个对象时调用构造函数,此时我们是在new函数中直接指定类名字的,当然会直接调用对应类的构造函数,不会出现基类类型实际指向子类的情况出现。而调用析构函数通常是针对对象的操作,如delete(a),此时我们当然希望可以调用到a实际指向的类型(类A)的析构函数,故需要设置为虚函数。

你可能感兴趣的:(C++,虚函数)