Chinaunix首页 | 论坛 | 博客
  • 博客访问: 4241894
  • 博文数量: 176
  • 博客积分: 10059
  • 博客等级: 上将
  • 技术积分: 4681
  • 用 户 组: 普通用户
  • 注册时间: 2006-03-24 12:27
文章分类

全部博文(176)

文章存档

2012年(1)

2011年(4)

2010年(14)

2009年(71)

2008年(103)

分类: C/C++

2008-05-05 22:58:01

By zieckey (http://blog.chinaunix.net/u/16292/)

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。

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


这里我们着重看一下这张虚函数表。在C++的标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

假设我们有这样的一个类:

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的实例来得到虚函数表。 下面是实际例程:



#include <stdio.h>

class Base {
public:
    virtual void a() { printf("Base::a()\n"); }
    virtual void b() { printf("Base::b()\n"); }
    virtual void c() { printf("Base::c()\n"); }
    virtual ~Base(){}
};

/*
定义一个无型参的返回类型为void的函数指针变量类型:Fun,
使用方法:
Fun pFun;
pFun=function;//function为已经定义的无型参的返回类型为void的函数
pFun();
*/

typedef void(*Fun)();

int main(int argc, char* argv[])
{
    Base *b = new Base();
    printf("Base对象b的地址:%p\n", b );//b为Base类的一个实例对象的首地址,此地址开始的四字节的内容存放的是虚函数表的地址

    
    printf("虚函数表地址:%p\n", (int*)( *(int*)b ) );
    
    printf("虚函数表第一个地址(该地址内的 内容为第一个函数的地址):%p\n", (int*)(*(int*)b) );//此处实际上就是虚函数表的首地址

    printf("虚函数表第二个地址(该地址内的 内容为第二个函数的地址):%p\n", (int*)(*(int*)b) +1 );
    printf("虚函数表第三个地址(该地址内的 内容为第三个函数的地址):%p\n", (int*)(*(int*)b) +2 );
    
    printf("虚函数表 ——第一个函数地址:%p\n", (int*)*((int*)(*(int*)b)) );
    printf("虚函数表 ——第二个函数地址:%p\n", (int*)*((int*)(*(int*)b) +1) );
    printf("虚函数表 ——第三个函数地址:%p\n", (int*)*((int*)(*(int*)b) +2) );

    Fun pFun = (Fun)*( (int*)(*(int*)b)+1 );
    pFun();
    ((Fun)*( (int*)*(int*)b+2 )) ();
    return 0;
}


运行结果:


Base对象b的地址:0x9281008
虚函数表地址:0x80489c8
虚函数表第一个地址(该地址内的 内容为第一个函数的地址):0x80489c8
虚函数表第二个地址(该地址内的 内容为第二个函数的地址):0x80489cc
虚函数表第三个地址(该地址内的 内容为第三个函数的地址):0x80489d0
虚函数表 ——第一个函数地址:0x80486e8
虚函数表 ——第二个函数地址:0x80486d4
虚函数表 ——第三个函数地址:0x80486c0
Base::g()
Base::h()



下面来解释一下程序中比较费解的表达式。

    a、printf("虚函数表地址:%p\n", (int*)( *(int*)b ) );
这一句,b是一个Base类型的指针,(int*)b把这个指针自身所在的内存地址取出来了,*(int*)b把这个地址的内容的一个4字节数据取出来了,这个4B数据本身又是一个地址,所以做了(int*)的强制转换,就是(int*)( *(int*)b )了。
这里注意“*(int*)b” 与“*b”的不同,b是一个Base类型的指针,同时也是一个地址,那么 *b 就代表了一个Base类型的变量了,而“*(int*)b”却只是把b这个地址的一个4字节数据取出来了。


    b、printf("虚函数表第二个地址(该地址内的 内容为第二个函数的地址):%p\n", (int*)(*(int*)b) +1   );
“(int*)(*(int*)b) +1”这个有上面的解释可知是在“(int*)(*(int*)b) ”地址基础上,增加4B偏移量,那么很自然的该地址的内容就是第二个虚函数的首地址。


    c、Fun pFun = (Fun)*(  (int*)(*(int*)b)+1  );
前面typedef已经处已经给出了说明,    (Fun)*(  (int*)(*(int*)b)+1  )实际上是把地址 “*(  (int*)(*(int*)b)+1  )”强制性转换为一个函数的入口地址,该函数无型参返回void。

同过这几点的解释,这个程序看懂应该没有问题了。
   
也许你不太相信程序运行的结果,没关系,一开始我也不敢确定是否正确,这里我们可通过GDB调试看看内存就知道了:



[root@localhost src]# g++ virtualTable.cpp -g
[root@localhost src]# gdb a.out
GNU gdb Red Hat Linux (6.6-8.fc7rh)
Copyright (C) 2006 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu"...
Using host libthread_db library "/lib/libthread_db.so.1".
(gdb) b main
Breakpoint 1 at 0x8048566: file virtualTable.cpp, line 22.
(gdb) r
Starting program: /mnt/study/unix/document/C_CPP_Programming/src/a.out

Breakpoint 1, main () at virtualTable.cpp:22
22 Base *b = new Base();
(gdb) n
23 printf("Base对象b的地址:%p\n", b );//b为Base类的一个实例对象的首地址,此地址开始的四字节的内容存放的是虚函数表的地址
(gdb)
Base对象b的地址:0x8f6c008
25 printf("虚函数表地址:%p\n", (int*)( *(int*)b) );
(gdb)
虚函数表地址:0x80489c8
27 printf("虚函数表第一个地址(该地址内的 内容为第一个函数的地址):%p\n", (int*)(*(int*)b) );//此处实际上就是虚函数表的首地址
(gdb) x/1aw 0x8f6c008    ======>这一命令打印出对象b的首地址‘0x8f6c008’的内容,是虚函数表的地址,与上面由程序打印的虚函数表地址一致
0x8f6c008: 0x80489c8 <_ZTV4Base+8>
(gdb) n
虚函数表第一个地址(该地址内的 内容为第一个函数的地址):0x80489c8
28 printf("虚函数表第二个地址(该地址内的 内容为第二个函数的地址):%p\n", (int*)(*(int*)b) +1 );
(gdb) n
虚函数表第二个地址(该地址内的 内容为第二个函数的地址):0x80489cc
29 printf("虚函数表第三个地址(该地址内的 内容为第三个函数的地址):%p\n", (int*)(*(int*)b) +2 );
(gdb) n
虚函数表第三个地址(该地址内的 内容为第三个函数的地址):0x80489d0
31 printf("虚函数表 ——第一个函数地址:%p\n", (int*)*((int*)(*(int*)b)) );
(gdb) x/3aw 0x80489c8    ======>这一命令打印出虚函数首地址‘0x80489c8’开始的以4字节为单位的三个单位的内存里的内容,正好与下面三个虚函数的地址一致
0x80489c8 <_ZTV4Base+8>: 0x80486e8 <_ZN4Base1aEv> 0x80486d4 <_ZN4Base1bEv> 0x80486c0 <_ZN4Base1cEv>
(gdb) n
虚函数表 ——第一个函数地址:0x80486e8
32 printf("虚函数表 ——第二个函数地址:%p\n", (int*)*((int*)(*(int*)b) +1) );
(gdb)
虚函数表 ——第二个函数地址:0x80486d4
33 printf("虚函数表 ——第三个函数地址:%p\n", (int*)*((int*)(*(int*)b) +2) );
(gdb)
虚函数表 ——第三个函数地址:0x80486c0
35 Fun pFun = (Fun)*( (int*)(*(int*)b)+1 );
(gdb) c
Continuing.
Base::b()    ======>这里的通过虚函数的地址,用函数指针的方式访问虚函数,得到的结果正常,表明上述虚函数地址没有错误。
Base::c()

Program exited normally.
(gdb)



参考文档:
http://blog.csdn.net/haoel/archive/2007/12/18/1948051.aspx
http://blog.chinaunix.net/u/16292/showart_673980.html


补充说明:gdb命令
x/3aw 0x80489c8
表示从内存地址 0x80489c8 读取内容,
w表示以四字节为一个单位,
3表示连续读取三个单位,
a表示按十进制显示
具体可以参考:   
阅读(3227) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~