理解C++关键字(7)---理解虚函数virtual

本文将介绍C++中虚函数的作用、特点及存储方式。

1、虚函数的作用

在介绍虚函数之前,需要了解一下早绑定(静态多态)和晚绑定(动态多态)的概念。

1.1 静态多态

早绑定,一般可通过模板实现。例如函数重载,对于同名的两个或多个函数,在编译阶段,编译器根据函数的形参个数及类型来决定调用哪个函数。

观察下面的例子:

#include<iostream>
using namespace std;
class Base{
    public:
        void Output(){cout<<"Base 类"<<endl;}
        void Output(int value){cout<<"传入数据值为:   "<<value<<endl;}
};
int main()
{
    Base b;
    b.Output();   //输出“Base 类”
    b.Output(3);  //输出“出入数据值为:  3”
    return 0;
}

上面的例子通过函数重载的方式,简单的实现了静态多态。
另外,静态多态的实现不一定非得依赖于面向对象编程。他提供了一个实现某个功能的模板,由编译器来决定采用模板中的哪一个。

1.2 动态多态

动态多态(晚绑定),一般通过虚函数实现,依赖于面向对象编程。简言之,虚函数可以实现晚绑定即动态多态。
观察下面的例子:

class Base{                   //基类
    public:
        virtual void Output(){cout<<"Base 类"<<endl;}
        ~Base(){cout<<"父类的析构函数!"<<endl;};           //基类的析构函数
};
class Child:public Base{     //公有继承自Base类
    public:
        virtual void Output(){cout<<"Child 类"<<endl;}
        ~Child(){cout<<"子类的析构函数!"<<endl;};          //子类的析构函数
};
int main()
{
    Base *b=new Child;
    b->Output();             //输出“Child 类”
    delete b;                //删除父类的指针,此时只会输出“父类的析构函数!”
    return 0;
}

通过将Output()函数声明为虚函数,从而可以用父类的指针访问的基类的成员函数,实现了“动态多态”。动态多态是在程序运行阶段实现的。

分析以上过程,可以发现,在此过程中,只申请了父类的指针,当指针的动作完成后,会自动的调用父类的析构函数,而子类的析构函数却始终没有运行。若在子类中,在堆上申请了一块内存,此时有可能会造成内存泄露。

为了避免内存泄露,需要将父类的析构函数声明为虚析构函数。此时,在删除父类指针时,首先调用子类的析构函数,然后调用父类的析构函数。

2、虚函数的存储方式

一般,函数在内存中存储时,都有一个入口地址。那么,对于虚函数来说,他们的入口地址是怎么存储的呢?是虚函数表。

当子类继承自一个父类时,对于父类和子类中的虚函数,有两种情况,覆盖和不覆盖。
所谓覆盖,就是子类中的虚函数名称与父类的虚函数名称相同,此时子类中函数的入口地址会覆盖父类中对应的虚函数的入口地址。从而,可以利用父类的指针访问子类的函数成员。
对于不覆盖的情形,各个虚函数在虚函数表中的位置遵循“父类的在前,子类的在后,而且按声明顺序排列”。

观察下面的代码:

#include<iostream>
using namespace std;

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

};
class Child:public Base{
    public:
        virtual void func1(){cout<<"Child func1"<<endl;}   //覆盖
        virtual void func4(){cout<<"Child func4"<<endl;}   //不覆盖
        virtual void func5(){cout<<"Child func5"<<endl;}   //不覆盖
};
int main()
{
    typedef void (*Fun)(void);
    Child c;                             //**子类对象**
    Fun pFun=NULL;

    pFun=(Fun)*((int*)*(int*)(&c)+0);    //取虚函数表中第一个虚函数的入口地址
    pFun();                              //运行,
    pFun=(Fun)*((int*)*(int*)(&c)+1);    //取虚函数表中第二个虚函数的入口地址
    pFun();
    pFun = (Fun)*((int*)*(int*)(&c) + 2);    //取虚函数表中第三个虚函数的入口地址
    pFun();
    pFun = (Fun)*((int*)*(int*)(&c) + 3);    //取虚函数表中第四个虚函数的入口地址
    pFun();
    pFun = (Fun)*((int*)*(int*)(&c) + 4);    //取虚函数表中第五个虚函数的入口地址
    pFun();

    return 0;
}

上面的程序依次输出:

Child func1
Base func2
Base func3
Child func4
Child func5

可以发现,“Child func1”把“Base func1”覆盖,而没被覆盖的函数遵循“父类在前,子类在后,并且按声明顺序排列”。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值