精华内容
下载资源
问答
  • C++虚函数

    多人点赞 2019-09-03 14:13:44
    C++虚函数纯虚函数虚函数 纯虚函数 纯虚函数 virtual void fun() = 0; 纯虚函数的出现时为了应对一些特殊的情况,即基类无法实现某些抽线行为,交由继承类去实现。 =0告诉编译器此函数为纯虚函数,由此可以引出...

    纯虚函数

    • 纯虚函数
    	virtual void fun() = 0;
    

    纯虚函数的出现时为了应对一些特殊的情况,即基类无法实现某些抽线行为,交由继承类去实现。
    =0告诉编译器此函数为纯虚函数,由此可以引出抽象类。
    抽象类指的是包含一个或者一个以上的纯虚函数的类。
    派生类继承基类的时候必须实现纯虚函数,否则编译不通过。

    虚函数

    • 普通虚函数
    	virtual void fun() {}
    

    首先来说普通虚函数是如何实现多态,虚函数实现多态的主要思想是通过基类指针调用基类和派生类中同名、同参数表的虚函数语句,基类指针指向什么对象,运行时就调用哪个派生类的虚函数。

    比如在继承Person类的Worker、Student类中都有睡觉这个功能,但是他们由于身份的不同,睡眠时间是不同的。

    class Person {
    public:
    	Person() {
    		cout << "Person 构造函数 " << endl;
    	}
    	void eat() {}//吃饭
    	virtual void sleep() {
    		cout << "人类睡8小时最佳..." << endl;
    	}
    	~Person() {
    		cout << "Person 析构函数 " << endl;
    	}
    protected:
    	int age; // 年龄
    	int height; // 身高
    };
    class Worker : public Person {
    public:
    	Worker() {
    		cout << "Worker 构造函数 " << endl;
    	}
    	virtual void sleep() {
    		cout << "工人顶多睡7小时" << endl;
    	}
    	~Worker() {
    		cout << "Worker 析构函数 " << endl;
    	}
    };
    class Student : public Person {
    public:
    	Student() {
    		cout << "Student 构造函数 " << endl;
    	}
    	virtual void sleep() {
    		cout << "学生顶多睡6小时" << endl;
    	}
    	~Student() {
    		cout << "Student 析构函数 " << endl;
    	}
    };
     
    void main(void)
    { 
    	Worker w;   // 1
    	Student s;   // 2
    	Person *p = &w;    // 3
    	p->sleep();  //4
    	p = &s;
    	p->sleep();
    }
    

    虚函数运行结果
    1-4行:由于Worker和Student继承自Person,所以二者在使用类名对象名创建对象时,都会首先调用Person构造函数,再调用自己的构造函数。
    **5-6行:**首先将Worker对象w拷贝给Person基类的指针p,并调用sleep方法。由于sleep方法申明为虚函数,所以基类指针p根据对象w去调用w的sleep方法,输出工人睡觉的时间!Student的对象同理
    7-10行:由于使用类名对象名创建对象,所以在结束时会自动调用彼此的析构函数。析构函数的调用顺序和构造函数相关,最先调用构造函数的最后析构。

    面试问题

    问题一、那么哪些函数不可以申明为虚函数呢?

    1. 构造函数
      首先虚函数的调用需要通过查虚函数表,但是虚函数表需要在构造函数创建实例以后才生成。如果构造函数是虚函数,则无法创建实例,类的成员也不可被访问。
    2. 静态函数
      静态函数属于整个类,不属于任何一个类的对象。本身就是实体,不可被定义为虚函数。
    3. 非成员函数
      非成员函数不属于类,所以无法生成虚函数表。虚函数必须要求是成员函数。
    4. 友元函数
      友元函数定义在类外,虽然有权访问类的private和protected成员,但是友元函数并非成员函数。
    5. 内联成员函数
      内联成员函数在编译时就需要展开,而虚函数在执行时才可动态绑定,所以不适用。

    问题二、为什么使用类名对象名创建对象会自动调用析构函数?

    面试官经常喜欢问的问题有两个

    1. 类名对象创建对象和使用new创建对象有何区别?
    2. 基类析构函数为何必须定义为虚函数?

    问题1:
    类名对象创建对象

    	Person p;
    

    这种方式是在局部栈区创建对象,栈区随着程序的结束栈区会自动释放,所以对象会自动调用析构函数。这种方式创建对象速度快,毕竟是在栈区,比较适合频繁创建对象和对象比较小的应用场景。
    new 创建对象

    	Person* p = new Person();
    	delete p;
    

    这种创建方式在堆上创建对象,程序结束是不会对堆内存进行释放的,对应的程序中应该手动对其进行释放内存。new一般与delete配合使用。

    问题2:
    基类析构函数设置为虚函数是为了防止内存泄漏的情况。如果使用类名对象在栈区创建对象的方式,这是没任何问题的。因为栈区会在程序结束时候自动释放栈区内存,自动调用析构函数。
    但是如果使用new方式创建对象,可能造成在delete释放的时候发生问题。当然也分为两种情况:

    1. delete 释放的指针是派生类的指针
    //	接着上文的代码,修改main函数中对象创建的方式
    class Person {
    public:
    	Person() {
    		cout << "Person 构造函数 " << endl;
    	}
    	virtual void sleep() {
    		cout << "人类睡8小时最佳..." << endl;
    	}
    	~Person() {   //  基类析构函数非虚函数
    		cout << "Person 析构函数 " << endl;
    	}
    protected:
    	int age; // 年龄
    	int height; // 身高
    };
    void main(void)
    {
    	Worker* w = new Worker();
    	Person *p = w;
    	p->sleep();
    	delete w;
    }
    

    从执行结果可以看出,如果delete释放的是派生类指针,基类和派生类都会调用析构函数,不存在内存泄漏的情况。
    在这里插入图片描述
    2. delete 释放的指针是派生类的指针

    void main(void)
    {
    	Worker* w = new Worker();
    	Person *p = w;
    	p->sleep();
    	delete p;
    }
    

    从执行结果可以看出,派生类并没有调用析构,这时产生内粗泄漏。
    在这里插入图片描述
    如果将基类析构函数定义为虚函数,无论delete基类指针还是派生类指针都会自动调用所有析构函数,解决内存泄漏问题

    展开全文
  • c++虚函数

    千次阅读 2017-09-22 20:30:51
    异步赠书:9月重磅新书升级,本本经典 SDCC 2017之区块链技术实战...C++ 虚函数表解析 标签: c++funclass编译器语言iostream 2007-12-18 22:07 319789人阅读 评论(450) 收藏 举报 分类:

    C++ 虚函数表解析

    标签: c++funclass编译器语言iostream
    319789人阅读 评论(450) 收藏 举报
    分类:

    C++ 虚函数表解析

     

    陈皓

    http://blog.csdn.net/haoel

     

     

    前言

     

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

     

     

    关于虚函数的使用方法,我在这里不做过多的阐述。大家可以看看相关的C++的书籍。在这篇文章中,我只想从虚函数的实现机制上面为大家 一个清晰的剖析。

     

    当然,相同的文章在网上也出现过一些了,但我总感觉这些文章不是很容易阅读,大段大段的代码,没有图片,没有详细的说明,没有比较,没有举一反三。不利于学习和阅读,所以这是我想写下这篇文章的原因。也希望大家多给我提意见。

     

    言归正传,让我们一起进入虚函数的世界。

     

     

    虚函数表

     

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

     

              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(),这在上面的程序中得到了验证(把int* 强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用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()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:

     

     

    我们从表中可以看到下面几点,

    1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。

    2)没有被覆盖的函数依旧。

     

    这样,我们就可以看到对于下面这样的程序,

     

                Base *b = new Derive();

     

                b->f();

     

    b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

     

     

     

    多重继承(无虚函数覆盖)

     

    下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。

     

     

     

    对于子类实例中的虚函数表,是下面这个样子:

     

    我们可以看到:

    1)  每个父类都有自己的虚表。

    2)  子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

     

    这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

     

     

     

     

    多重继承(有虚函数覆盖)

     

    下面我们再来看看,如果发生虚函数覆盖的情况。

     

    下图中,我们在子类中覆盖了父类的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()

     

     

    安全性

     

    每次写C++的文章,总免不了要批判一下C++。这篇文章也不例外。通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。水可载舟,亦可覆舟。下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。

     

    一、通过父类型的指针访问子类自己的虚函数

    我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:

     

              Base1 *b1 = new Derive();

                b1->f1();  //编译出错

     

    任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)

     

     

    二、访问non-public的虚函数

    另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。

     

    如:

     

    class Base {

        private:

                virtual void f() { cout << "Base::f" << endl; }

     

    };

     

    class Derive : public Base{

     

    };

     

    typedef void(*Fun)(void);

     

    void main() {

        Derive d;

        Fun  pFun = (Fun)*((int*)*(int*)(&d)+0);

        pFun();

    }

     

     

    结束语

    C++这门语言是一门Magic的语言,对于程序员来说,我们似乎永远摸不清楚这门语言背着我们在干了什么。需要熟悉这门语言,我们就必需要了解C++里面的那些东西,需要去了解C++中那些危险的东西。不然,这是一种搬起石头砸自己脚的编程语言。

     

    在文章束之前还是介绍一下自己吧。我从事软件研发有十个年头了,目前是软件开发技术主管,技术方面,主攻Unix/C/C++,比较喜欢网络上的技术,比如分布式计算,网格计算,P2PAjax等一切和互联网相关的东西。管理方面比较擅长于团队建设,技术趋势分析,项目管理。欢迎大家和我交流,我的MSNEmail是:haoel@hotmail.com 

     

    附录一:VC中查看虚函数表

     

    我们可以在VCIDE环境中的Debug状态下展开类的实例就可以看到虚函数表了(并不是很完整的)

    附录 二:例程

    下面是一个关于多重继承的虚函数表访问的例程:

     

    #include <iostream>

    using namespace std;

     

    class Base1 {

    public:

                virtual void f() { cout << "Base1::f" << endl; }

                virtual void g() { cout << "Base1::g" << endl; }

                virtual void h() { cout << "Base1::h" << endl; }

     

    };

     

    class Base2 {

    public:

                virtual void f() { cout << "Base2::f" << endl; }

                virtual void g() { cout << "Base2::g" << endl; }

                virtual void h() { cout << "Base2::h" << endl; }

    };

     

    class Base3 {

    public:

                virtual void f() { cout << "Base3::f" << endl; }

                virtual void g() { cout << "Base3::g" << endl; }

                virtual void h() { cout << "Base3::h" << endl; }

    };

     

     

    class Derive : public Base1, public Base2, public Base3 {

    public:

                virtual void f() { cout << "Derive::f" << endl; }

                virtual void g1() { cout << "Derive::g1" << endl; }

    };

     

     

    typedef void(*Fun)(void);

     

    int main()

    {

                Fun pFun = NULL;

     

                Derive d;

                int** pVtab = (int**)&d;

     

                //Base1's vtable

                //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+0);

                pFun = (Fun)pVtab[0][0];

                pFun();

     

                //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+1);

                pFun = (Fun)pVtab[0][1];

                pFun();

     

                //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+2);

                pFun = (Fun)pVtab[0][2];

                pFun();

     

                //Derive's vtable

                //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+3);

                pFun = (Fun)pVtab[0][3];

                pFun();

     

                //The tail of the vtable

                pFun = (Fun)pVtab[0][4];

                cout<<pFun<<endl;

     

     

                //Base2's vtable

                //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);

                pFun = (Fun)pVtab[1][0];

                pFun();

     

                //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);

                pFun = (Fun)pVtab[1][1];

                pFun();

     

                pFun = (Fun)pVtab[1][2];

                pFun();

     

                //The tail of the vtable

                pFun = (Fun)pVtab[1][3];

                cout<<pFun<<endl;

     

     

     

                //Base3's vtable

                //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);

                pFun = (Fun)pVtab[2][0];

                pFun();

     

                //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);

                pFun = (Fun)pVtab[2][1];

                pFun();

     

                pFun = (Fun)pVtab[2][2];

                pFun();

     

                //The tail of the vtable

                pFun = (Fun)pVtab[2][3];

                cout<<pFun<<endl;

     

                return 0;

    }

     

    (转载时请注明作者和出处。未经许可,请勿用于商业用途)

     

    更多文章请访问我的Blog: http://blog.csdn.net/haoel

     
    73
    3

    查看评论
    68楼 Zaki_huang 4天前 15:31发表 [回复] [引用] [举报]
    自己做了一遍, 学到很多, 我的博客
    http://www.jianshu.com/writer#/notebooks/15612794/notes/17182281/preview
    67楼 诶阿星 2017-09-13 14:32发表 [回复] [引用] [举报]
    原来你才是原创呀,谢谢分享,受教了!
    66楼 feifeiiong 2017-09-08 17:56发表 [回复] [引用] [举报]
    还有,能看到本条评论的诸位请直接移步 深入探索 C++ 对象模型 看这些所谓专家不知道哪里来的结论真的误人子弟
    65楼 feifeiiong 2017-09-07 21:16发表 [回复] [引用] [举报]
    事实上,楼主写的一开始就是错的,虚函数表是类对象之间共享的,而非每个对象都保存了一份,楼主得到的也只是虚指针的地址,而非虚函数表的地址,事实上对虚函数指针的地址解引用得到的才是虚函数表的地址(因为虚函数指针指向虚函数表),以上经过理论和实际验证。
    Re: CNfreeee 3天前 16:17发表 [回复] [引用] [举报]
    回复feifeiiong:需要先转成一个二级指针,再解引用才是对的
    *(int **)(&amp;b)
    Re: feifeiiong 2017-09-08 11:07发表 [回复] [引用] [举报]
    回复feifeiiong:强烈建议楼主验证自己的代码,楼主身份为博客专家,希望能够对自己的文章负责,谢谢!
    64楼 github_33705958 2017-05-20 20:01发表 [回复] [引用] [举报]
    多重继承的虚表图是错的。好坑
    Re: a_big_pig 2017-07-27 17:28发表 [回复] [引用] [举报]
    回复github_33705958:xcode + mac 验证是对的啊,大兄弟你是不是哪里弄错了
    63楼 shuzhengjiang 2017-05-09 16:26发表 [回复] [引用] [举报]
    不为困穷而改节,这句话应该改成,(识时务者为俊杰!物竞天择,适者生存)
    62楼 shuzhengjiang 2017-05-09 16:25发表 [回复] [引用] [举报]
    不为困穷而改节,这个我觉得要有句话,识时务者为俊杰!
    Re: qq_35687516 2017-08-13 10:37发表 [回复] [引用] [举报]
    回复shuzhengjiang:写的挺好
    61楼 HymanLiuTS 2017-03-22 16:23发表 [回复] [引用] [举报]
    有一个问题,刚刚看<<深度探索C++对象模型>>,它在第9页所说每个类所关联的type-info object会放到虚函数表的第一位,但是楼主的文章中提到表格的第一位放的是虚函数,不过代码验证楼主是对的,难道书中出错了????
    Re: pkxpp 2017-04-21 11:14发表 [回复] [引用] [举报]
    回复lzhui1987:没去试验过,我的理解是:类对象里面的指针指的并不是虚表的起始地址,而是第一个函数地址项的地址。比如说type-info是放在表的第0项,第一个虚函数放在第1项,类对象的vptr指的是第1项所在的地址而已。
    60楼 小朋友87 2016-10-15 17:58发表 [回复] [引用] [举报]
    把重载改成重写
    59楼 stevewongbuaa 2016-09-06 16:42发表 [回复] [引用] [举报]
    写的很好,不过如果要在64位机器运行要把int换成long才行
    58楼 lonely_geek 2016-09-05 20:53发表 [回复] [引用] [举报]
    讲的通俗易懂,但是笔者 对于 重载 和 覆盖 没有区分开,这点不应该了啊。多态是基于 函数的 覆盖,不是重载
    57楼 khing 2016-08-21 22:26发表 [回复] [引用] [举报]
    用VC试了下“一般继承(无虚函数覆盖)”,很惊讶地看到虚函数表里面并没有Derived的函数,大家可以试试
    Re: jinshangyu 2017-02-27 18:20发表 [回复] [引用] [举报]
    回复khing:你肯定是继承写错了 少了virtual关键字
    56楼 Annoymous_ 2016-07-18 21:32发表 [回复] [引用] [举报]
    多重继承部分Not Even Wrong! 多重继承的时候调用函数是要修正地址的

    后面的两段安全性批评更是莫名其妙,要让程序不正常 直接用 (int*)0=0 就可以了,费这么大的功夫到底是在想批判什么
    55楼 huisha25 2016-06-30 15:00发表 [回复] [引用] [举报]
    你好,今天看了你的虚函数讲解,受益匪浅。我从事c++开发也有一段时间了,想找个大牛学习学习,能加个好友吗?
    QQ:872569904
    期待您的回复,谢谢!!!
    54楼 Bingogoc 2016-06-28 16:43发表 [回复] [引用] [举报]
    cout << "虚函数表地址:" << (int*)(&b) << endl;
    cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;
    这个应该改成:
    cout << "虚函数表地址:" << *((long*)(&b) )<< endl;
    cout << "虚函数表 — 第一个函数地址:" << *((long*)*(long*)(&b) )<< endl;
    这样就能解决32位系统与64位系统的问题,两个地址应分别在原有的基础上加*,本人拙见
    Re: 编程界的小学生 2017-08-08 09:34发表 [回复] [引用] [举报]
    回复Bingogoc: 刚开始还以为之前的理解不对呢 看了这篇文章 335楼说的对 我就是这么理解的 该博文还是有错的 希望改之
    Re: jackqk 2016-10-14 11:08发表 [回复] [引用] [举报]
    回复Bingogoc:技术还是得自己学习,别做梦了,大家都有事,陪陪家人也不会教你
    53楼 qq_35053267 2016-05-31 00:07发表 [回复] [引用] [举报]
    是重写不是重载吧?
    52楼 Shepsee 2016-05-08 17:34发表 [回复] [引用] [举报]
    非常清晰的解释 与插图 太感谢了
    51楼 maowei117 2016-04-30 16:35发表 [回复] [引用] [举报]
    我认为:
    1. (int*)&b  
    (int*)&b
    

    得到的不是虚函数表的地址,而是虚函数指针的地址。
    Re: whdugh 2016-08-15 22:19发表 [回复] [引用] [举报]
    回复maowei117:对象取地址也不是虚函数的指针吧
    Re: g360z247j123 2016-07-14 17:39发表 [回复] [引用] [举报]
    回复maowei117:我也认为如此。楼主原文说”C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置“,如果编译器真是这样做,那么对实例对象Base b取地址(&b),得到的是指向虚函数表的指针的地址ptr_addr;对该地址解引用(*ptr_addr)得到指向虚函数表的指针的值(即虚函数表的地址)Vtable_addr
    50楼 ProLianGee 2016-04-03 13:23发表 [回复] [引用] [举报]
    好文章。解决了我很多疑惑。
    49楼 JRSmith7 2016-04-01 20:14发表 [回复] [引用] [举报]
    请教下,例程代码的最后一个模块:
    1. //The tail of the vtable  
    2. pFun = (Fun)pVtab[2][3];  
    3. cout<<pFun<<endl;  
                //The tail of the vtable
                pFun = (Fun)pVtab[2][3];
                cout<<pFun<<endl;
    

    为什么输出是1?已经是最后一个虚函数表了,不应该输出0吗?
    48楼 chenyang1231 2016-03-29 00:08发表 [回复] [引用] [举报]
    个人觉得关于打印虚函数表地址与直接使用虚函数表调用虚函数部分有点错误,附上自己测试过的代码。评论太多了,不知道有没有人发现这个错误,望楼主在博文里改正之,否则容易误人子弟。(如果不是我理解错的话)
    1. cout << "虚函数表的地址" << *((int *)&base) << endl;  
    2.   
    3. ((fun)(*((int *)(*(int *)(&base)) + 0)))();  
    4. ((fun)(*((int *)(*(int *)(&base)) + 1)))();  
    5. ((fun)(*((int *)(*(int *)(&base)) + 2)))();  
    	cout << "虚函数表的地址" << *((int *)&base) << endl;
    
    	((fun)(*((int *)(*(int *)(&base)) + 0)))();
    	((fun)(*((int *)(*(int *)(&base)) + 1)))();
    	((fun)(*((int *)(*(int *)(&base)) + 2)))();
    
    Re: zhangjin739 2016-09-29 23:25发表 [回复] [引用] [举报]
    回复chenyang1231:不用改代码, 楼主应该意思是 虚函数表地址的地址罢了
    Re: xingaide520 2016-06-08 15:21发表 [回复] [引用] [举报]
    回复chenyang1231:能解释下吗,我对这个用法不理解,太多指针了
    47楼 路过少年 2016-01-24 18:57发表 [回复] [引用] [举报]
    mark
    46楼 01_Terry 2015-12-27 16:55发表 [回复] [引用] [举报]
    写的真心好,感谢分享!
    45楼 大毛爱吃鱼 2015-10-29 16:06发表 [回复] [引用] [举报]
    有个问题,关于多重继承(有虚函数覆盖)。继承有几个父类就会有几个虚函数表,然后覆盖的虚函数会取代原来父类的对应的虚函数指针。可是继承的类对象,未覆盖的那些函数并没有在虚函数表中??
    44楼 andy当我遇上你 2015-10-13 18:47发表 [回复] [引用] [举报]
    谢谢楼主,我收藏了!
    43楼 白白皎皎 2015-09-30 10:22发表 [回复] [引用] [举报]
    大神,学习了
    42楼 改变oo 2015-09-08 15:24发表 [回复] [引用] [举报]
    学习了
    41楼 jerome029 2015-09-01 20:15发表 [回复] [引用] [举报]
    顶神牛!
    40楼 woshiyisang 2015-08-20 17:21发表 [回复] [引用] [举报]
    想问一下:虚函数表的内存是什么时候分配的?类定义的时候?
    39楼 rudy_yuan 2015-08-17 07:20发表 [回复] [引用] [举报]
    楼主的代码似乎有点问题?

    这是我写的可以运行的代码:http://www.rudy-yuan.net/archives/128/

    大家可以参考下,便于理解。。。
    38楼 Change_Land 2015-08-06 12:53发表 [回复] [引用] [举报]
    陈老师您好,这里我有一个疑问,就是我们不能在C++的构造函数中调用虚函数,即便是调用了,我们调用到的也是基类本身的那个函数,例如:
    class Transation
    {
    public:
    Transation()
    {
    logTransation();
    }

    virtual void logTransation()
    {
    cout << "call Transation::logTransation" << endl;
    }
    };

    class BuyTransation : public Transation
    {
    public:
    void logTransation() override
    {
    cout << "call BuyTransation::logTransation" << endl;
    }
    };

    class SaleTransation : public Transation
    {
    public:
    void logTransation() override
    {
    cout << "call SaleTransation::logTransation" << endl;
    }
    };

    void main(int argc, char* argv[])
    {
    BuyTransation buy;
    SaleTransation sale;
    }
    输出的结果为:
    call Transation::logTransation
    call Transation::logTransation
    按道理,此时调用logTransation函数的地址已经被派生类的那个logTransation函数的地址替换了,但是并没有出现这样的情况,那么这个虚函数表的赋值逻辑是怎样的呢?
    Re: 从来不作 2015-08-07 14:54发表 [回复] [引用] [举报]
    回复Manistein:对象在构造的时候,首先构造它的基类部分,然后构造自身。你举的例子应该是这样运行的:首先构造基类,设定vptr初值,然后调用基类构造函数,最后构造自身,重新设定vptr的值。
    37楼 Lilyss21 2015-07-26 21:17发表 [回复] [引用] [举报]
    学习了,记录下。
    36楼 稚枭天卓 2015-07-26 15:55发表 [回复] [引用] [举报]
    请教下,虚函数表是一个类的对象共享,还是一个对象就拥有一个虚函数表?
    Re: 从来不作 2015-08-07 14:56发表 [回复] [引用] [举报]
    回复u013630349:每个类一个虚表,每个类的实例拥有一个指向虚表的指针。
    Re: lsfv001 2016-06-11 15:54发表 [回复] [引用] [举报]
    不错,不过有个地方.
    当类有 数据的时候.
    比如int .
    我的编译器
    会这样排.
    第一个虚表的地址.
    int 的数据
    第一个虚表的地址.
    35楼 tppppppppp1 2015-06-23 16:30发表 [回复] [引用] [举报]
    我做实验的时候发现子类继承多个父类的时候子类对象的内存中存放的是“父类1虚函数表,父类1成员变量,父类2虚函数表,父类2成员变量。。。”,为何博主的图中的顺序是“父类1虚函数表,父类2虚函数表,成员变量”?
    34楼 Hellen1101 2015-06-01 23:37发表 [回复] [引用] [举报]
    不错
    33楼 Hellen1101 2015-06-01 23:38发表 [回复] [引用] [举报]
    不错
    32楼 smzx_boy2012 2015-05-23 23:59发表 [回复] [引用] [举报]
    困扰许久的问题终于被解决了,多谢分享,希望我也有一天成长到分享这样的知识的人
    31楼 东东同学 2015-05-06 21:38发表 [回复] [引用] [举报]
    https://github.com/mudongliang/CppLearn
    其中testvtable*.cpp都是关于这个文章中几个例子写的demo,有什么问题,大家给我提一下!
    另外看见有人说第一个代码有问题,不知道是什么意思?在我64bit和32bit机器上都运行没问题啊!
    Re: s985507995 2016-01-14 15:07发表 [回复] [引用] [举报]
    回复mudongliangabcd:本来想把代码也给你放到github上的,提交试了下没权限。贴出来修改代码。
    #include<iostream>
    using namespace std;

    class Base{
    public:
    Base(){}
    // virtual ~Base(){}
    virtual void f(int a, int b){
    cout<<"Base::f()"<<endl;
    int c = a + b;
    cout << "add " << c << endl;
    }
    virtual void g(){
    cout<<"Base::g()"<<endl;
    }
    virtual void h(){
    cout<<"Base::h()"<<endl;
    }
    };

    typedef void(*Fun)(int, int);

    int main(){
    Base b;
    Fun pFun = NULL;
    cout << "虚函数地址:"<<(int *)(&b)<<endl;
    cout << "虚函数-第一个函数地址:"<<(int *)*(int *)(&b) <<endl;

    pFun = (Fun)*((int *)*(int *)(&b));
    pFun(3, 4);
    return 0;
    }
    Re: s985507995 2016-01-14 15:02发表 [回复] [引用] [举报]
    回复mudongliangabcd:1. 上述所有代码都不带构造和析构函数。(如果类中有虚函数,析构函数同样要设计成需函数,否则可能会造成内存泄漏)
    在加入虚析构函数后,按照上述逻辑,在64位机上跑testvtable.cpp,会有一些问题,你可以尝试一下;
    2. 即便如文中所述,找到了虚函数的函数地址。可以尝试在函数内部做运算,我在64位机上,运算结果没有跑对的。这个结果还不知道有没有可以帮忙解释下。
    30楼 chenzhg33 2015-04-02 23:46发表 [回复] [引用] [举报]
    cout << "虚函数表地址:" << (int*)(&b) << endl;
    这句有问题吧?&b是b对象的地址,就算强制转为int*,还是b的地址,虚函数表地址应该是*(int*)&b,对于64位机器应该是*(long*)&b
    Re: 契约无罪 2015-07-25 11:50发表 [回复] [引用] [举报]
    cout << "虚函数表地址:" << (int*)(&b) << endl;
    cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;
    其实改成
    cout<<" pV-Table = "
    cout<<" V-Table" 或者 “ V-Table[0]
    Re: Narsil 2015-04-03 17:29发表 [回复] [引用] [举报]
    回复u010078776:没错,这里很显然错误了,当然,无论 32 位还是 64 位,我们使用 intptr_t 就可以通用了
    29楼 wangzhongjunjun 2015-03-25 16:36发表 [回复] [引用] [举报]
    请教下大神:上面的图片用什么思维导图软件做出来的?
    28楼 bama2488313716 2015-03-23 10:46发表 [回复] [引用] [举报]
    博客写的非常好 但是想问下如果通过普通指针定位到虚函数位置,并且通过普通指针调用该虚函数,如何在虚函数中访问对象的成员变量呢 我知道用类的成员函数指针取代普通指针可以做到。
    27楼 ixshells 2015-03-12 16:06发表 [回复] [引用] [举报]
    好文章,Mark一下
    26楼 969722243 2015-01-18 16:51发表 [回复] [引用] [举报]
    非常好,学习了,多来几篇这样用的文章
    25楼 sunny_ss12 2014-12-20 12:28发表 [回复] [引用] [举报]
    请好, 对于虚函数表中虚函数的地址是不是应该写成
    (Fun)*((int**)*(int**)(&b)+0); // Base::f()
    (Fun)*((int**)*(int**)(&b)+1); // Base::g()
    (Fun)*((int**)*(int**)(&b)+2); // Base::h()
    否则在64位机器下是有错的。
    Re: hahaxiaohuo2015 2015-02-02 14:41发表 [回复] [引用] [举报]
    回复sunny_ss12:同意,但是int**这样很难理解,这位大神能否详细解释下
    (Fun)*((int**)*(int**)(&b))的含义呢?多谢多谢
    Re: sunny_ss12 2015-02-26 17:01发表 [回复] [引用] [举报]
    回复sunyekuan_cafuc:而(int*)(&b)相当于把Base的第一个元素看成了int,在32位机器下int和指针都是4个字节不会出问题,而在64位机器下,int和指针占用的空间大小不同,int是4个字节,而在64位机器下指针是8个字节,这样转换就有问题了
    Re: sunny_ss12 2015-02-26 16:57发表 [回复] [引用] [举报]
    回复sunyekuan_cafuc:我是这样想的:Base对象空间存储的第一个元素是虚函数表地址,即一个指针变量,对Base对象取地址实际上就是对该指针变量取地址,即应该是指向指针的指针,所以是(int**)(&b)而不是(int*)(&b)。不知道说的对不对
    Re: chenzhg33 2015-04-02 23:25发表 [回复] [引用] [举报]
    回复sunny_ss12:关键在于int** ptr是指向指针的指针,ptr+1就是移动8个字节,如果(Fun)*((int*)*(int*)(&b)+2); 也是移动8个字节,就是f函数地址
    24楼 jiangjun12345shi 2014-12-13 14:39发表 [回复] [引用] [举报]
    随便复制别人的 真没意思
    23楼 jiangjun12345shi 2014-12-13 14:38发表 [回复] [引用] [举报]
    随便复制别人的 没他妈的意思
    Re: syj52417 2015-03-05 22:16发表 [回复] [引用] [举报]
    回复jiangjun12345shi:那你能给下原始链接?
    22楼 cheyiliu 2014-12-11 17:17发表 [回复] [引用] [举报]
    学习了
    21楼 hothuangting 2014-12-02 09:00发表 [回复] [引用] [举报]
    好啊
    20楼 忧伤还是快乐 2014-11-20 08:32发表 [回复] [引用] [举报]
    您好:麻烦请问一下即使访问到了子类中非继承的函数和访问到了基类中的private或者protected的函数,这样做有什么实际的意义?谢谢
    19楼 827fast 2014-11-15 02:06发表 [回复] [引用] [举报]
    大体上是对的。有一个地方误导人啊。
    18楼 damotiansheng 2014-11-10 00:16发表 [回复] [引用] [举报]
    崩溃,第一个代码就有两个错误,竟然没人提,搞得我没看懂,一直纠结
    17楼 Full_Speed_Turbo 2014-10-29 10:14发表 [回复] [引用] [举报]
    讲解的很清楚,通俗易懂!博主V5!
    16楼 johnnyforonline 2014-10-23 13:41发表 [回复] [引用] [举报]
    mark
    15楼 Sylvanas_XV 2014-10-14 00:50发表 [回复] [引用] [举报]
    挺好的
    14楼 JakeMiao 2014-10-05 11:01发表 [回复] [引用] [举报]
    很好。但是重载、重写、覆盖三者没有分清楚。
    13楼 xuweiqun 2014-09-30 14:48发表 [回复] [引用] [举报]
    今天在 vs2003 and vs2013 + win7 下测试,发现一个小问题:
    经过多次 debug release测试,虚函数表的结束结点 是个不确定值。
    12楼 liaoruiyan 2014-09-28 18:53发表 [回复] [引用] [举报]
    其实应该解释下(Fun)*((int*)*(int*)(&d)+0);这样的代码,不然看完整篇博文还是迷迷糊糊不懂虚函数表
    11楼 Sylvernass 2014-09-23 23:20发表 [回复] [引用] [举报]
    很不错,这样就大致理解虚函数了
    10楼 kekey1210 2014-09-21 11:32发表 [回复] [引用] [举报]
    9楼 superbin 2014-09-19 15:04发表 [回复] [引用] [举报]
    好牛的技术文章!
    8楼 hustcalm 2014-09-14 11:35发表 [回复] [引用] [举报]
    好文!
    不过还是觉得看《深入理解C++对象模型》是最靠谱的!
    7楼 yonggeno1 2014-09-14 10:06发表 [回复] [引用] [举报]
    是一篇好文章,支持作者!
    6楼 lanshanwanghao 2014-09-12 20:48发表 [回复] [引用] [举报]
    楼主太牛逼了。讲的简单明了
    5楼 lyzsyr 2014-08-27 10:57发表 [回复] [引用] [举报]
    毕业两年了,终于要啃一啃c++了。
    楼主,写的很清楚很透彻。
    学习,膜拜了!
    4楼 寒山-居士 2014-08-12 18:31发表 [回复] [引用] [举报]
    写的很清晰,也很形象,就需要这样的 讲的很活,不像其他文章喜欢咬文嚼字
    3楼 u0116snail 2014-08-12 11:17发表 [回复] [引用] [举报]
    不错,谢谢楼主
    2楼 最怕认真 2014-08-08 19:10发表 [回复] [引用] [举报]
    很详细啊。我感觉一下子清晰了很多。
    1楼 whs2004789 2014-08-01 15:55发表 [回复] [引用] [举报]
    1. typedef void(*Fun)(void);   //void类型的函数指针  
    2.   
    3. class Base   
    4. {  
    5. public:  
    6.     virtual void f() { cout << "Base::f" << endl; }  
    7.     virtual void g() { cout << "Base::g" << endl; }  
    8.     virtual void h() { cout << "Base::h" << endl; }  
    9. private:  
    10.     virtual void j() { cout << "Base::j" << endl;}  
    11. };  
    12.   
    13. class dev: public Base  
    14. {  
    15. public:  
    16.     virtual void k() { cout << "dev::k" << endl; }  
    17. };  
    18.   
    19. int main()  
    20. {  
    21.       
    22.     //Base b1;  
    23.     //b1.j();            //compile error  
    24.     dev d;  
    25.     //d.f();             //compile error  
    26.     //通过函数指针访问到私有的j(), j()对于对象来讲本来是不可见的,指针太强大  
    27.     Fun pFun2 = (Fun)*((int*)*(int*)(&d)+3);   
    28.     pFun2();  
    29.   
    30.     Base *b2 = new dev();  
    31.     //b2->k();           //compile error,父类指针无法call子类特有的虚函数  
    32.     //通过函数指针访问到子类特有的虚函数k(), 指针太强大  
    33.     Fun pFun3 = (Fun)*((int*)*(int*)b2+4);   
    34.     pFun3();  
    35.   
    36.     return 0;  
    37. }  
    typedef void(*Fun)(void);   //void类型的函数指针
    
    class Base 
    {
    public:
    	virtual void f() { cout << "Base::f" << endl; }
    	virtual void g() { cout << "Base::g" << endl; }
    	virtual void h() { cout << "Base::h" << endl; }
    private:
    	virtual void j() { cout << "Base::j" << endl;}
    };
    
    class dev: public Base
    {
    public:
    	virtual void k() { cout << "dev::k" << endl; }
    };
    
    int main()
    {
        
        //Base b1;
    	//b1.j();            //compile error
    	dev d;
    	//d.f();             //compile error
    	//通过函数指针访问到私有的j(), j()对于对象来讲本来是不可见的,指针太强大
        Fun pFun2 = (Fun)*((int*)*(int*)(&d)+3); 
        pFun2();
    
    	Base *b2 = new dev();
    	//b2->k();           //compile error,父类指针无法call子类特有的虚函数
    	//通过函数指针访问到子类特有的虚函数k(), 指针太强大
        Fun pFun3 = (Fun)*((int*)*(int*)b2+4); 
        pFun3();
    
    	return 0;
    }
    
    查看更多评论
    发表评论
    • 用 户 名:
    • u010499172
        
    * 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    展开全文
  • C++ 虚函数

    2013-08-06 17:14:30
    关于C++虚函数的文章网上有很多,这里简明的说明一下; C++虚函数的目的就是为了实现多态; 多态定义(C++大学教程(第二版)):通过继承相关的不同类,它们的对象能够对同一个函数调用做出不同相应。  

    具体详解可以参考这篇文章《C++ 虚函数表解析》;

    关于C++虚函数的文章网上有很多,这里简明的说明一下;


    C++虚函数的目的就是为了实现多态;

    多态定义(C++大学教程(第二版)):通过继承相关的不同类,它们的对象能够对同一个函数调用做出不同相应。

                                                                            多态死通过虚函数实现的。当通过基类指针(或引用)请求适用虚函数时,C++在与对象关联的派生类中正确的选择重定义的函数。



    示例1:

    class CA
    {


    private:
    static UINT prc( PVOID pParam )
    {
    CA * pCA = (CA*)pParam;


    while(1)
    {
    Sleep(1000);
    pCA->show();
    }

    return 0;
    }


    public:
    CA()
    {
    // AfxBeginThread( prc, this );
    }


    public:
    virtual void show()
    {
    AfxMessageBox("CA show");
    }
    };


    class CB : public CA
    {


    public:
    virtual void show()
    {
    AfxMessageBox("CB show");
    }




    };


    class CC : public CA
    {

    public:
    virtual void show()
    {
    AfxMessageBox("CC show");


    };


    void CTDlg::OnButtonClick() 
    {
    // TODO: Add your control notification handler code here

    CA * pCA2CA = (CA*)(new CA);


    pCA2CA->show();


    CA * pCA2CB = (CA*)(new CB);
    pCA2CB->show();


    CA * pCA2CC = (CA*)(new CC );
    pCA2CC->show();
     
    }

    运行结果:显示: CA, CB, CC 相应对话框;


    注:这里的应用最典型的就是, shape的draw函数, 然后继承多种类型,然后具体实现;可以cpicture绘制图片, cline绘制线条等等;





    示例2: 

    class CA
    {
     
    private:
    static UINT prc( PVOID pParam )
    {
    CA * pCA = (CA*)pParam; 

    while(1)
    {
    Sleep(1000);
    pCA->show();
    }

    return 0;
    }


    public:
    CA()

    AfxBeginThread( prc, this );
    }


    public:
    virtual void show()
    {
    AfxMessageBox("CA show");
    }
    };


    class CB : public CA


    public:
    virtual void show()
    {
    AfxMessageBox("CB show");


    };


    void CTDlg::OnButtonClick() 

    {

    CA * pCA2CB = (CA*)(new CB);

     //或者 new CB();

    }

    显示结果:AfxMessageBox("CB show");

    注释:在线程中:CA * pCA = (CA*)pParam;   pCA其实指向了CB;  

               因为内存结构中,CB的开头部分CA后面部分是新添加的内容; 

               构造顺序是,先构造CA接着就是CB先添加的内容;

               通过示例2,可以知道父类可以预留接口然后自动调用;


    展开全文
  • c++虚函数详解(你肯定懂了)

    万次阅读 多人点赞 2018-08-01 11:02:13
    转自:c++虚函数 大牛的文章,就是通俗易懂,言简意赅。 前言 C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数...

    转自:c++虚函数  大牛的文章,就是通俗易懂,言简意赅。

    前言

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

    关于虚函数的使用方法,我在这里不做过多的阐述。大家可以看看相关的C++的书籍。在这篇文章中,我只想从虚函数的实现机制上面为大家 一个清晰的剖析。

    当然,相同的文章在网上也出现过一些了,但我总感觉这些文章不是很容易阅读,大段大段的代码,没有图片,没有详细的说明,没有比较,没有举一反三。不利于学习和阅读,所以这是我想写下这篇文章的原因。也希望大家多给我提意见。

    言归正传,让我们一起进入虚函数的世界。

    虚函数表

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

              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(),这在上面的程序中得到了验证(把int* 强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用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()

    这个时候你应该懂了吧。什么?还是有点晕。也是,这样的代码看着太乱了。没问题,让我画个图解释一下。如下所示:

    <?xml:namespace prefix = v />

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

    下面,我将分别说明“无覆盖”和“有覆盖”时的虚函数表的样子。没有覆盖父类的虚函数是毫无意义的。我之所以要讲述没有覆盖的情况,主要目的是为了给一个对比。在比较之下,我们可以更加清楚地知道其内部的具体实现。

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

    下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:

    请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示

    对于实例:Derive d; 的虚函数表如下:

    我们可以看到下面几点:

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

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

    我相信聪明的你一定可以参考前面的那个程序,来编写一段程序来验证。

     

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

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

     

     

     

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

    我们从表中可以看到下面几点,

    1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。

    2)没有被覆盖的函数依旧。

    这样,我们就可以看到对于下面这样的程序,

                Base *b = new Derive();

                b->f();

    b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

    多重继承(无虚函数覆盖)

    下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数

     

     

    对于子类实例中的虚函数表,是下面这个样子:

    我们可以看到:

    1)  每个父类都有自己的虚表。

    2)  子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

    这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

    多重继承(有虚函数覆盖)

    下面我们再来看看,如果发生虚函数覆盖的情况。

    下图中,我们在子类中覆盖了父类的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()

    安全性

    每次写C++的文章,总免不了要批判一下C++。这篇文章也不例外。通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。水可载舟,亦可覆舟。下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。

    一、通过父类型的指针访问子类自己的虚函数

    我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:

              Base1 *b1 = new Derive();

                b1->f1();  //编译出错

    任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)

    二、访问non-public的虚函数

    另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些no

    n-public的虚函数,这是很容易做到的。

    如:

    class Base {

        private:

                virtual void f() { cout << "Base::f" << endl; 

    }; 

    class Derive : public Base{

    };

    typedef void(*Fun)(void);

    void main() {

        Derive d;

        Fun  pFun = (Fun)*((int*)*(int*)(&d)+0);

        pFun();

    }

    结束语

    C++这门语言是一门Magic的语言,对于程序员来说,我们似乎永远摸不清楚这门语言背着我们在干了什么。需要熟悉这门语言,我们就必需要了解C++里面的那些东西,需要去了解C++中那些危险的东西。不然,这是一种搬起石头砸自己脚的编程语言。

    在文章束之前还是介绍一下自己吧。我从事软件研发有十个年头了,目前是软件开发技术主管,技术方面,主攻Unix/C/C++,比较喜欢网络上的技术,比如分布式计算,网格计算,P2PAjax等一切和互联网相关的东西。管理方面比较擅长于团队建设,技术趋势分析,项目管理。欢迎大家和我交流,我的MSNEmail是:haoel@hotmail.com  

    附录一:VC中查看虚函数表

    我们可以在VCIDE环境中的Debug状态下展开类的实例就可以看到虚函数表了(并不是很完整的)

     

     

    附录 二:例程

    下面是一个关于多重继承的虚函数表访问的例程:

    #include <iostream>

    using namespace std;

    class Base1 {

    public:

                virtual void f() { cout << "Base1::f" << endl; }

                virtual void g() { cout << "Base1::g" << endl; }

                virtual void h() { cout << "Base1::h" << endl; }

    };

    class Base2 {

    public:

                virtual void f() { cout << "Base2::f" << endl; }

                virtual void g() { cout << "Base2::g" << endl; }

                virtual void h() { cout << "Base2::h" << endl; }

    }; 

    class Base3 {

    public:

                virtual void f() { cout << "Base3::f" << endl; }

                virtual void g() { cout << "Base3::g" << endl; }

                virtual void h() { cout << "Base3::h" << endl; }

    };

    class Derive : public Base1, public Base2, public Base3 {

    public:

                virtual void f() { cout << "Derive::f" << endl; }

                virtual void g1() { cout << "Derive::g1" << endl; }

    };

    typedef void(*Fun)(void);

    int main()

    {

                Fun pFun = NULL;

                Derive d;

                int** pVtab = (int**)&d;

                //Base1's vtable

                //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+0);

                pFun = (Fun)pVtab[0][0];

                pFun();

                //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+1);

                pFun = (Fun)pVtab[0][1];

                pFun();

                //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+2);

                pFun = (Fun)pVtab[0][2];

                pFun();

                //Derive's vtable

                //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+3);

                pFun = (Fun)pVtab[0][3];

                pFun();

                //The tail of the vtable

                pFun = (Fun)pVtab[0][4];

                cout<<pFun<<endl;

                //Base2's vtable

                //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);

                pFun = (Fun)pVtab[1][0];

                pFun();

                //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);

                pFun = (Fun)pVtab[1][1];

                pFun();

                pFun = (Fun)pVtab[1][2];

                pFun();

                //The tail of the vtable

                pFun = (Fun)pVtab[1][3];

                cout<<pFun<<endl;

                //Base3's vtable

                //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);

                pFun = (Fun)pVtab[2][0];

                pFun();

                //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);

                pFun = (Fun)pVtab[2][1];

                pFun();

                pFun = (Fun)pVtab[2][2];

                pFun();

                //The tail of the vtable

                pFun = (Fun)pVtab[2][3];

                cout<<pFun<<endl;

                return 0;

    }

    (转载时请注明作者和出处。未经许可,请勿用于商业用途)

     

     

     

    展开全文
  • C++虚函数表剖析

    万次阅读 多人点赞 2016-02-18 20:28:23
    关键词:虚函数,虚表,虚表指针,动态绑定,多态一、概述为了实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心是虚函数表(下文简称虚表)。本文介绍虚函数表是如何实现动态绑定的。二、类的虚表每个...
  • C++虚函数详解

    万次阅读 多人点赞 2019-04-08 19:31:35
    C++虚函数详解 前言 C++的特性使得我们可以使用函数继承的方法快速实现开发,而为了满足多态与泛型编程这一性质,C++允许用户使用虚函数**(virtual function)来完成运行时决议这一操作,这与一般的编译时决定**...
  • C++ 虚函数表解析

    万次阅读 多人点赞 2007-12-18 22:07:00
    C++ 虚函数表解析 陈皓http://blog.csdn.net/haoel 前言 C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。...
  • c++ 虚函数

    千次阅读 2017-09-24 20:43:16
    通过具体的代码,一步步介绍虚函数
  • C++ 虚函数与纯虚函数

    万次阅读 2018-03-06 23:21:34
    一、C++虚函数1、什么是虚函数?(虚函数只能借助于指针或者引用来达到多态的效果)class A{public: virtual void foo()// 虚函数关键字 virtual { cout&lt;&lt;"A::foo() is called"&lt;&...
  • 探讨 C++ 虚函数 virtual

    千次阅读 多人点赞 2021-03-03 21:29:20
    探讨 C++ 虚函数 virtual 有无虚函数的对比 首先写两个简单的类,类 B 继承自类 A。 class A{ public: void print(){ cout << "A" << endl; } }; class B : public A { public: void print(){ cout...
  • C++虚函数简介

    千次阅读 多人点赞 2020-09-14 11:07:51
    本文力求清晰的阐述 C++ 虚函数底层实现机制。有经验的小伙伴儿都知道虚函数是通过虚表实现的,但我相信很多小伙伴儿没有在意过虚函数在虚表中的位置(如果父类和子类虚函数位置不一致?以谁的为准?),我想我们...
  • 继续前一篇《C++ 虚函数之一:对象内存布局》
  • C++虚函数表的应用

    万次阅读 2018-02-11 23:11:26
    本文作为“C++虚函数实现原理”的后续文章,并不打算介绍类的内存布局,本文只介绍如何使用虚函数表的方式来调用该类的私有虚函数。 在阅读本文前需要先了解C++虚函数的实现原理,可以先参考:...
  • 图解C++虚函数

    千次阅读 多人点赞 2016-07-02 17:47:17
    C++虚函数原理非常简单,但很多技术细节已将大家的学习兴趣淹没了。本来以图来注解g++编译器生成的C++虚函数结构。
  • C++虚函数和虚函数表原理

    万次阅读 多人点赞 2018-07-26 19:49:54
    虚函数的地址存放于虚函数表之中。运行期多态就是通过虚函数虚函数表实现的。 类的对象内部会有指向类内部的虚表地址的指针。通过这个指针调用虚函数虚函数的调用会被编译器转换为对虚函数表的访问: ptr-...
  • C++ 虚函数虚函数表指针 虚函数指针

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 25,771
精华内容 10,308
关键字:

c++虚函数

c++ 订阅