C++——虚函数(virtual)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zy2317878/article/details/81132157

写在前面

这一篇博客记录一下自己理解的虚函数的相关内容。虚函数在刚开始学习C++的时候并不理解为什么需要这个东西?现在觉得要理解这个概念,需要对面向对象编程这个软件设计模式要有了解。学习C++不光要能理解语法特征,还要明白一些常用的软件设计模式。 这篇博客我也还是会结合一些网络上的资料,加上自己的理解阐述一下对虚函数的理解。

参考资料

面向对象的三大特点

之前也有说过,虚函数与面向对象编程关联很大,所以在了解虚函数之前,可以先加强对“面向对象编程”的理解。面向对象的三大特点:

  • 多态
  • 封装
  • 继承

面向对象中的对象具体是什么概念可以用生活中的东西类比。比如说汽车,我们用语言描述车的时候,脑海里就会浮现车的相关信息:四个轮子、一个外壳、四个车灯、一个方向盘…等。所以程序中我们也可以用对象来描述汽车这种生活中实际的物体。当然,程序要处理的事务千奇百怪,对象可能在生活中有实际对应的(比如汽车、动物)、也有可能是没有实物但存在人们认知中的(比如数据表、一个窗口)、甚至仅仅是逻辑上的概念(比如图形、 车辆这种宽泛的概念)。

所以,对于对象有一个大概的了解,不难理解多态了。汽车有很多种类:轿车、货车、巴士等等;动物也有很多种类:老虎、狮子等等。这些分类的物体也可以看做对象,这些对象与原来的大类对象(比如汽车、动物)体现了一种继承的关系。

而轿车、货车、巴士等多种种类的汽车与大类对象:汽车还是有很多相同的地方,比如:四个轮子,一个方向盘,那么在实际编程过程中,我们希望子类的相同属性能够用尽量少的代码来描述,这样更多的精力就可以放在不同子类的差异上了。那么同样是汽车,四个轮子,可以轿车、货车、巴士具体种类的车辆时,轮子大小不同,但是都是四个轮子啊,所以这里就体现了多态。反正都是汽车,肯定有四个轮子,如果在需要确定汽车种类的时候,我们再决定轮子大小,不需要确定种类的时候,关注汽车的轮子特点就好了。

而封装则通过public、private这些关键字体现。一个对象具有自己的成员变量与成员函数,封装给对象提供了对象之间交互的方法,其他对象或者程序员可以通过这个对象提供的对外接口使用对象,同时对象也能封装自己的关键属性。还是举汽车为例子,咱们开汽车只需要能打开车门、能启动发动机等一些简单功能,但是汽车公司肯定不希望我们能轻易修改汽车的电力系统、发动起机械系统。所以汽车公司就将这些系统封装了,而我们用户只需要使用一些简单的操作,就可以使用汽车了。

以上例子说的有些含糊,专业性不强,如果以后随着学习的深入,会给出一些专业性的解释。如有不准确的地方,还望多多指正。

静态多态与动态多态

上面说明了面向对象的三大特征,由于这篇博客要讨论的是虚函数,所以聚焦于多态这种性质。为了说的清楚,还需要一些铺垫。

对于多态,根据上面的解释,相同对象收到不同消息或不同对象收到相同消息时产生的不同的动作。那么实现多态的方式可以分为静态多态与动态多态。

1.静态多态

静态多态也叫早绑定,这里的早与动态多态相对应,早是指多态的实现是在编译器编译程序的时候就能确定的。结合代码说明:

class Rect       //矩形类
{
public:
    int calcArea(int width);
    int calcArea(int width,int height);
};

//省略calArea()函数具体实现代码,一个参数按照正方形计算面积,两个参数按照长方形计算面积。

int main(){
    Rect.rect;
    rect.calcArea(10);
    rect.calcArea(10,20);
    return 0;
}

这段代码首先声明了一个矩形类,然后声明了函数calArea(),该函数有两种形式,一个参数与两个参数的形式。那么程序一旦编写好,编译器在编译阶段就能根据重载函数的形式实现多态(这里是实现矩形为正方形与一般矩形时面积的不同计算方式)。

2.动态多态

动态多态也叫做晚绑定,这里的晚与静态多态相对应,之前的静态多态一般通过函数重载实现,函数接收不同的参数来实现一定程度上对不同消息的不同响应。但是静态多态仅局限一种对象,要想对不同对象实现类似的功能,就需要通过继承来实现,也就是这里所说的动态多态。

下面以代码为例:

class Shape{                        //形状类
public:
     double calcArea(){
         cout<<"calcArea"<<endl;
         return 0;
     }
};
class Circle : public Shape {     //公有继承自形状类的圆形类
public:
    Circle(double r);
    double calcArea();
private:
    double m_dR;
};

double Circle::calcArea(){
    return 3.14*m_dR*m_dR;
}

class Rect : public Shape{       //公有继承自形状类的矩形类
public:
    Rect(double width,double height);
    double calArea();
private:
    double m_dWidth;
    double m_dHeight;
};

double Rect::calcArea(){
    return m_dWidth*m_dHeight;
}

int main(){
    Shape *shape1=new Circle(4.0);
    Shape *shape2=new Rect(3.0,5.0);
    shape1->calcArea();
    shape2->calcArea();
    .......
    return 0;
}

从以上代码可以看出,这个程序与静态多态不同的是,通过定义指向基类指针,但赋值不同子类的对象,从而实现了多态。而且由于两个子类各自重载了基类的成员函数,直接通过指向基类的指针调用基类的成员函数,也能根据实际指针指向的子类对象来调用合适的成员函数。这种多态不是根据重载函数接收的参数的个数来判断的,而是通过判断程序运行时基类指针实际指向的子类对象来判断应该调用什么成员函数。所以就是动态多态。

动态多态的问题——内存泄漏

动态多态很好,大大提高了程序的灵活性。但是也有问题需要解决:内存泄漏。所以绕了一大圈,我们终于可以说明虚函数的功能了:基类的虚析构函数。之所以先说这个,因为这个我觉得最难懂。换一段代码来解释:

#include <iostream>

using namespace std;

class Base{
private:
    int i;
public:
    Base(){
        cout << "Base count" << endl;
    }

    // virtual ~Base(){
    //  cout << "  Base descount" << endl;
    // }

    //对应输出:
    //Base count
    //    Inherit count
    //      Inherit descoutn
    //  Base descount
    //Base count
    //  Base descount

    ~Base(){
        cout << "  Base Descount" << endl;
    }
    //对应输出:
    //Base count
    //    Inherit count
    //  Base Descount
    //Base count
    //  Base Descount

};

class Inherit : public Base{
private:
    int num;
public:
    Inherit(){
        cout << "    Inherit count" << endl;
    }
    ~Inherit(){
        cout << "      Inherit descoutn" << endl;
    }
};

int main(){
    Base *p = new Inherit;
    delete p;
    Base *q = new Base;
    delete q;
    return 0;
}

注意这段代码中,基类的析构函数一个有virtual修饰,另一个没有,注意他们的输出。主函数都是创建指向基类的指针,但是删除的时候,加了virtual修饰的类能正确析构基类与继承类。但是没有加的就不能调用继承类的析构函数。

虚析构函数可以防止内存泄露,定义一个基类的指针p,在delete p时,如果基类的析构函数是虚函数,这时只会看p所赋值的对象,如果p赋值的对象是派生类的对象,就会调用派生类的析构函数(毫无疑问,在这之前也会先调用基类的构造函数,在调用派生类的构造函数,然后调用派生类的析构函数,基类的析构函数,所谓先构造的后释放);如果p赋值的对象是基类的对象,就会调用基类的析构函数,这样就不会造成内存泄露。

如果基类的析构函数不是虚函数,在delete p时,调用析构函数时,只会看指针的数据类型,而不会去看赋值的对象,这样就会造成内存泄露。

虚函数表

虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

虚函数表的指针4个字节大小(vptr),存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。

虚函数表何时建立

在《深度探索C++对象模型》的4.2节中,可以给出解答:

表格中的virtual functions地址是如何被建构起来的?在C++中,virtual functions(可经由其class object被调用)可以在编译时期获知。此外,这一组地址是固定不变的,执行期不可能新增或替换之。由于程序执行时,表格的大小和内容都不会改变,所以其建构和存取皆可以由编译器完全掌控,不需要执行期的任何介入。

猜你喜欢

转载自blog.csdn.net/zy2317878/article/details/81132157
今日推荐