分享

虚函数的实现原理

 guitarhua 2012-06-19

虚函数的实现原理

对多态是什么并不陌生,但是之前只知道其作用和一般的使用方法,对其具体实现原理并不清楚。今天在解决遇到的问题时,参考了一些相关的文章,在这里总结一下。

多态(Polymorphisn)

多态性是OOP的核心概念。直观的说就是基类在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数。
C++的多态性是通过虚函数来实现的,也就是以virtual关键字修饰的函数。先看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using namespace std;
 
class Human
{
    public:
        virtual void eat()
        {
            cout << "Human eat food." << endl;
        }
        void speak()
        {
            cout << "Human can speak." << endl;
        }
};
 
class Baby : public Human
{
    public:
        void eat()
        {
            cout << "Baby drink milk." << endl;
        }
        void speak()
        {
            cout << "Baby can't speak yet." << endl;
        }
};
 
int main()
{
    Human *ph = new Baby;
    ph->eat();
    ph->speak();
    return 0;
}

这段程序的运行结果是:
Baby drink milk.
Human can speak.

只需在把基类的成员函数设为virtual,其派生类的相应的函数也会自动变为虚函数。因此Baby类中的eat()函数也是虚函数,其前面的virtual可省略。

对一般的函数来说,不论基类指针指向的是哪个具体的派生类对象,最终被调用的还是基类成员函数。所以,用基类指针ph调用speak()时,基类的speak()被调用。

但对于虚函数来说,会根据具体的派生类对象,调用相应派生类的函数。所以,ph调用eat()时,最终是它所指向的Baby类对象的成员函数被调用。

同时也注意到,要实现多态,还有一个关键之处是就是一切用指向基类的指针或引用来操作对象。

实现原理

当一个类中有虚函数存在时,编译器就会为它插入一段数据,并为之创建一个表。那段数据叫做虚表指针(vptr),指向那个表,那个表叫做虚表 (vtbl)。每个类都有自己的vtbl,vtbl的作用就是保存自己类中虚函数的地址,可以把vtbl形象地看成一个数组,这个数组的每个元素存放的就 是虚函数的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
using namespace std;
 
class A
{
    public:
        virtual void fun1()
        {
            cout << "A::fun1()" << endl;
        }
        virtual void fun2()
        {
            cout << "A::fun2()" << endl;
        }
};
 
class B : public A
{
    public:
        void fun1()
        {
            cout << "B::fun1()" << endl;
        }
        void fun2()
        {
            cout << "B::fun2()" << endl;
        }
};
 
int main()
{
    A *pa = new A;
    pa->fun1();
    delete pa;
 
    return 0;
}

毫无疑问,调用了A::fun1(),但是A::fun1()不是像普通函数那样直接找到函数地址而执行的。真正的执行方式是:首先取出vptr的 值,这个值就是vtbl的地址,由于调用的函数A::fun1()是第一个虚函数,所以取出vtbl第一个表项里的值,这个值就是A::fun1()的地 址了,最后调用这个函数。因此只要vptr不同,指向的vtbl就不同,而不同的vtbl里装着对应类的虚函数地址,所以这样虚函数就可以完成它的任务, 多态就是这样实现的。

而对于class A和class B来说,他们的vptr指针存放在何处?其实这个指针就放在他们各自的实例对象里。由于class A和class B都没有数据成员,所以他们的实例对象里就只有一个vptr指针。通过上面的分析,可以用下面一段代码来描述这个带有虚函数的类的简单模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
using namespace std;
 
class A
{
    public:
        virtual void fun1()
        {
            cout << "A::fun1()" << endl;
        }
        virtual void fun2()
        {
            cout << "A::fun2()" << endl;
        }
};
 
class B : public A
{
    public:
        void fun1()
        {
            cout << "B::fun1()" << endl;
        }
        void fun2()
        {
            cout << "B::fun2()" << endl;
        }
};
 
int main()
{
    void (*fun1)(A *);
    A *p = new B;
    long lVptrAddr;
    memcpy(&lVptrAddr, p, 4);
    memcpy(&fun1, reinterpret_cast<long *>(lVptrAddr), 4);
 
    fun1(p);
    delete p;
 
    return 0;
}

一步一步开始分析:
1. void (*fun)(A *); 这段定义了一个函数指针名字叫做fun,而且有一个A*类型的参数,这个函数指针待会用来保存从vtbl里取出的函数地址。

2. A *p = new B; new B是向内存申请一个内存单元的地址然后隐式地保存在一个指针中。然后把这个地址附值给A类型的指针p.

3. long lVptrAddr; 这个long类型的变量用来保存vptr的值。

4. memcpy(&lVptrAddr, p, 4); 前面讲了,他们的实例对象里只有vptr指针,所以我们就放心大胆地把p所指的4bytes内存里的东西复制到lVptrAddr中,所以复制出来的 4bytes内容就是vptr的值,即vtbl的地址。

5. memcpy(&fun, reinterpret_cast(lVptrAddr), 4); 取出vtbl第一个表项里的内容,并存放在函数指针fun里。需要注意的是lVptrAddr里面是vtbl的地址,但lVptrAddr不是指针,所以要把它先转变成指针类型。

6. fun(p); 这里就调用了刚才取出的函数地址里的函数,也就是调用了B::fun()这个函数。至于为什么会有参数p,其实类成员函数调用时,会有个this 指针,这个p就是那个this指针,只是在一般的调用中编译器自动处理了而已,而在这里则需要自己处理。

7. delete p; 释放由p指向的自由空间;

如果调用B::fun2()怎么办?那就取出vtbl的第二个表项里的值就行了。

1
2
3
memcpy(&fun, reinterpret_cast<long*>(lVptrAddr + 4), 4);
 //OR
memcpy(&fun, reinterpret_cast<long*>(lVptrAddr) + 1, 4);

第一种方法加4是因为一个指针的长度是4bytes,所以加4。第二种方法更符合数组的用法,因为lVptrAddr被转成了long*型,所以加1就是往后移sizeof(long)的长度。

参考资料:http://baike.baidu.com/view/161302.htm

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多