c++ 虚函数

前言:以下内容借鉴了网络上其它博客的内容

虚函数的定义

在某基类中用virtual关键字修饰的成员函数,并在一个或多个派生类中可以重新定义的成员函数,就是虚函数。

简介

在c++语言中,基类的成员函数有两种,一种是基类希望其派生类进行覆盖的函数,另一种是基类希望派生类直接继承问不要改变的函数比如说基类是人,有些函数蕴含了人特有的属性,那么派生类即使出现了许多种,但是人特有的属性最好不要变。c++中虚函数的作用主要是实现了多态的机制。
关于多态,简而言之就是用父类型的指针指向子类的实例,然后通过父类的指针调用实际子类的成员函数,这种技术可以让父类的指针有多种形态,用不变的代码但可以实现不同的功能和属性。


虚函数表

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

假设我们有下面这样一个类

class Base {
            public:
            virtual void f() { cout << "Base::f" << endl; }
            virtual void g() { cout << "Base::g" << endl; }
            virtual void h() { cout << "Base::h" << endl; }
            };

我们可以通过Base的实例来得到虚函数表

          typedef void(*Fun)(void); 

          Base b;

           Fun pFun = NULL;

            cout << "虚函数表地址:" << (int*)(&b) << endl;

            cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;

            // Invoke the first virtual function 

            pFun = (Fun)*((int*)*(int*)(&b));

            pFun();

实际运行结果如下:(Windows XP+VS2003, Linux 2.6.22 + GCC 4.1.3)
虚函数表地址:0012FED4
虚函数表 — 第一个函数地址:0044F148
Base::f

在上面这个实例中,我们将&b强行转为int *,获得了虚函数表的地址,然后再次取地址得到第一个虚函数base::f()的地址。因此通过上面的代码,我们知道如果要调用Base::g()和Base::h()的代码分布如下:

            (Fun)*((int*)*(int*)(&b)+0);  // Base::f()

            (Fun)*((int*)*(int*)(&b)+1);  // Base::g()

            (Fun)*((int*)*(int*)(&b)+2);  // Base::h()

上面的代码和说明可以用下图表示。
在这里插入图片描述

注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。


虚函数表在不同继承下的情况

一般继承(无虚函数覆盖)
在这里插入图片描述
在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示。对于实例 Derive d,它的虚函数表如下
在这里插入图片描述

我们可以看到下面几点:

1)虚函数按照其声明顺序放于表中。

2)父类的虚函数在子类的虚函数前面。

一般继承(有虚函数覆盖)

覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。

在这里插入图片描述
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
在这里插入图片描述
从这个表中我们可以看出来:

  • 覆盖的f()函数被放到了虚函数表中原来父类虚函数的位置
    *没有被覆盖过的函数依旧
            Base *b = new Derive();

            b->f();

那么在上面的程序中,实际调用的时候是Derive::f()被调用了

多重继承(无虚函数覆盖)
在这里插入图片描述
那么对于子类是咧的虚函数表,是以下情况
在这里插入图片描述
在上面的表中:

  • 每个父类都有自己的虚表
  • 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
    这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

多重继承(有虚函数的覆盖)
在这里插入图片描述
下面是对应子类实例的虚函数表的图
在这里插入图片描述
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:

            Derive d;

            Base1 *b1 = &d;

            Base2 *b2 = &d;

            Base3 *b3 = &d;

            b1->f(); //Derive::f()

            b2->f(); //Derive::f()

            b3->f(); //Derive::f() 

            b1->g(); //Base1::g()

            b2->g(); //Base2::g()

            b3->g(); //Base3::g()
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值