精华内容
下载资源
问答
  • D的虚函数存放的既有继承自B的虚函数B::foo,又有重写(override)了基类虚函数B::bar的D::bar,还有新增的虚函数D::quz。 提示:为了描述方便,本文在探讨对象内存布局时,将忽略内存对齐对布局的影响。 2. ...
  • //析构函数做成员函数 }; Base::~Base()//成员函数实现 { cout; } class Derived:public Base { public: Derived(); ~Derived(); private: int *p; }; Derived::Derived() { p=new int(0);//从堆上分配一个int型...
  • 继承虚函数与非虚函数

    千次阅读 2018-01-09 18:06:20
    在看公司代码时, 发现了一处关于虚函数的我比较难以理解的地方,大致描述如下: 子类继承父类, 包括继承了虚函数和非虚函数 子类调用父类的非虚函数 Base::PrintWord(), 在这个非虚函数里它又调用了虚函数...

    在看公司代码时, 发现了一处关于虚函数的我比较难以理解的地方,大致描述如下:

    1. 子类继承父类, 包括继承了虚函数和非虚函数
    2. 子类调用父类中的非虚函数 Base::PrintWord(), 在这个非虚函数里它又调用了虚函数doPrintWord()
    3. 实验表明调用的虚函数执行的是重写的子类虚函数。

    大致的代码可以简写为以下一个例子:

    #include <iostream>
    
    using namespace std;
    
    class Base{
    
    public:
        void PrintWord(const char* c)
        {
            printf("called in Base::PrintWord, and  pointer of this is : 0x%x\n",this);
            doPrintWord(c);
        }
        virtual void doPrintWord(const char* c)
        {
            printf("%s",c);
            printf("Base here\n");
    
            printf("Base : 虚函数的this指针地址:0x%x \n\n",this);
    
        }
    };
    
    
    class Derived: public Base
    {
    public:
        void testprint(const char* c)
        {
            Base::PrintWord(c);
        }
    private:
        void doPrintWord(const char* c)
        {
            printf("%s",c);
            printf("Derived here\n");
    
            printf("子类虚函数doPrintWord的this指针地址:0x%x \n\n",this);
        }
    };
    
    int main(int argc, char *argv[])
    {
        Derived ss;
    
        ss.testprint("");
    
    
        printf("Derived : 0x%x\n",&ss);
    
        return 0;
    }

    为了解决这一疑问,有必要理清虚函数和非虚函数的区别:

    虚函数: 虚函数其实是一种动态绑定机制,因为在编译时,编译器是不知道是该调用父类中的虚函数还是子类中的虚函数的,而是在程序执行过程中,动态确定的。虚函数的本质是,C++编译器透过某个表格,在执行时期「间接」调用实际上欲绑定的函数(注意「间接」这个字眼)。这样的表格称为虚拟函数表(常被称为vtable)。每一个「内含虚拟函数的类」,编译器都会为它做出一个虚拟函数表,表中的每一笔元素都指向一个虚拟函数的地址。此外,编译器当然也会为类别加上一项成员变量,是一个指向该虚拟函数表的指针(常被称为vptr)。
    每一个由此类衍生出来的对象,都有这么一个vptr。当我们透过这个对象调用虚拟函数,事实上是透过vptr 找到虚拟函数表,再找出虚拟函数的真正地址。

    如何理解上面的间接调用呢?下文会详细说以下如何间接调用虚函数的。

    非虚函数: 非虚函数在编译器就已经静态确定,地址也是静态确定的, 一个子类 如果继承他的父类非虚函数,那么他们其实共享一个非虚函数地址。
    假如存在一个非虚函数NonVirtualFunc(), 那么Base::NonVirtualFunc()和Derived::NonVirtualFunc() 具有一个相同的函数地址, 这里的函数地址可以利用调试器在调试阶段可以看出来, 是相同的。

    子类在调用这些非虚函数和虚函数,都经历了怎样的过程?

    一个子类的成员函数在被调用时,都会默认传入一个参数this. 这个this 关键字 区别与函数地址, 他代表的是实例地址,一个类中的非静态变量、虚函数表都是通过this关键字进行寻址的。

    非虚函数的地址对编译期来说“静态”的,也就是函数地址在编译期就已经确定了,实例地址对于非虚函数只是那个 this 指针参数。

    虚函数的地址,是先到实例的地址this前面去查找它的虚函数表所在的地址。然后从虚函数表里取出该函数所对应的元素(虚函数表是一个函数指针数组)来call的。(当然一个已知的类的虚函数表的内容 也是编译期静态的,但不同类的虚函数表内容不同,即运行时多态的基础)

    this 指针

    Q1:什么情况下可以使用this指针?

    this只能在成员函数中使用。全局函数,静态函数都不能使用this。实际上,成员函数默认第一个参数为T* const register this。

    为什么this指针不能在静态函数中使用?

    大家可以这样理解,静态函数如同静态变量一样,他不属于具体的哪一个对象,静态函数表示了整个类范围意义上的信息,而this指针却实实在在的对应一个对象,所以this指针当然不能被静态函数使用了。
    静态函数只能调用静态函数和使用静态变量,但是this指针所指向的变量是非静态的,所以静态函数中使用this指针不合理也不合法。

    Q2:

    问题解释

    void testprint(const char* c)
    {
        Base::PrintWord(c);
    }
    
    

    对于上述的函数调用来实现多态,我们就尝试用以上的知识来解释一下:

    1. 对于非虚函数PrintWord(), 子类在继承时保持了该函数的函数地址不变, 子类在调用Base::PrintWord()时, 相当于调用了Derived::PrintWord()

    2. 由于一个类的非静态成员函数在调用非静态成员函数时,会将this指针作为参数默认的传入到函数中, 那么该调用过程就类似于Derived::PrintWord(this, c);

    3. 所以在Base::PrintWord(this, c) 中调用虚函数doPrintWord(),其在虚函数表中寻址是基于当前this指针进行寻址的,而this指针当前指向的是子类对象,那么最终执行的虚函数是子类重载的虚函数doPrintWord(),最终实现了多态。

    写到这,我觉得公司原有的代码虽然没有问题,实现了多态,但是这样的写法确实让人感到迷惑,不如直接调用子类继承的非虚函数来的直接。

    展开全文
  • C++虚函数的实现

    千次阅读 2019-05-09 16:41:17
    http://blog.kongfy.com/2015/08/探索c虚函数在g的实现/?utm_source=tuicool&utm_medium=referral https://blog.csdn.net/haoel/article/details/1948051 目录 一、虚函数表解析 前言 虚函数表 一般继承...

    http://blog.kongfy.com/2015/08/探索c虚函数在g中的实现/?utm_source=tuicool&utm_medium=referral

    https://blog.csdn.net/haoel/article/details/1948051

    目录

    一、虚函数表解析

    前言

    虚函数表

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

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

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

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

    安全性

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

    2、访问non-public的虚函数

    结束语

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

    二、通过汇编探索C++虚函数在g++中的实现

    先写个例子

    g++如何实现虚函数的动态绑定?

    vtbl在何时被创建?vptr又是在何时被初始化?

    在Linux中运行的C++程序虚拟存储器中,vptr、vtbl存放在虚拟存储的什么位置?

    总结


    一、虚函数表解析

    前言

    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; 的虚函数表如下:

    我们可以看到下面几点:

    • 虚函数按照其声明顺序(C++所有初始化顺序都是按照声明的顺序)放于表中。
    • 父类的虚函数在子类的虚函数前面
    • (还有很重要的一点就是在子类的内存布局中,父类和子类共用一个虚表,在多继承中子类和第一个父类共享)

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

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

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

    为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数: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++。这篇文章也不例外。通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。水可载舟,亦可覆舟。下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。

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

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

    Base1 *b1 = new Derive();
    b1->f1();  //编译出错

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

    2、访问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++中那些危险的东西。不然,这是一种搬起石头砸自己脚的编程语言。

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

    我们可以在VC的IDE环境中的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++虚函数在g++中的实现

    本文是我在追查一个诡异core问题的过程中收获的一点心得,把公司项目相关的背景和特定条件去掉后,仅取其中通用的C++虚函数实现部分知识记录于此。

    在开始之前,原谅我先借用一张图黑一下C++:

    “无敌”的C++

    “无敌”的C++

    如果你也在写C++,请一定小心…至少,你要先有所了解:当你在写虚函数的时候,g++在写什么?

     

    先写个例子

    为了探索C++虚函数的实现,我们首先编写几个用来测试的类,代码如下:

    #include <iostream>
    using namespace std;
    class Base1
    {
    public:
        virtual void f() {
            cout << "Base1::f()" << endl;
        }
    };
    
    class Base2
    {
    public:
        virtual void g() {
            cout << "Base2::g()" << endl;
        }
    };
    
    class Derived : public Base1, public Base2
    {
    public:
        virtual void f() {
            cout << "Derived::f()" << endl;
        }
    
        virtual void g() {
            cout << "Derived::g()" << endl;
        }
    
        virtual void h() {
            cout << "Derived::h()" << endl;
        }
    
    };
    
    int main(int argc, char *argv[])
    {
        Derived ins;
        Base1 &b1 = ins;
        Base2 &b2 = ins;
        Derived &d = ins;
    
        b1.f();
        b2.g();
        d.f();
        d.g();
        d.h();
    }

    代码采用了多继承,是为了更多的分析出g++的实现本质,用UML简单的画一下继承关系:

    示例代码UML图

    示例代码UML图

    代码的输出结果和预期的一致,C++实现了虚函数覆盖功能,代码输出如下:

    Derived::f()
    Derived::g()
    Derived::f()
    Derived::g()
    Derived::h()

    我写这篇文章的重点是尝试解释g++编译在底层是如何实现虚函数覆盖和动态绑定的,因此我假定你已经明白基本的虚函数概念以及虚函数表(vtbl)和虚函数表指针(vptr)的概念和在继承实现中所承担的作用,如果你还不清楚这些概念,建议你在继续阅读下面的分析前先补习一下相关知识,陈皓的《C++虚函数表解析》系列是一个不错的选择。开始分析!

    通过本文,我将尝试解答下面这三个问题:

    1. g++如何实现虚函数的动态绑定?
    2. vtbl在何时被创建?vptr又是在何时被初始化?
    3. 在Linux中运行的C++程序虚拟存储器中,vptr、vtbl存放在虚拟存储的什么位置?

    首先是第一个问题:

    g++如何实现虚函数的动态绑定?

    这个问题乍看简单,大家都知道是通过vptr和vtbl实现的,那就让我们刨根问底的看一看,g++是如何利用vptr和vtbl实现的。

    第一步,使用 -fdump-class-hierarchy 参数导出g++生成的类内存结构:

    Vtable for Base1
    
    Base1::_ZTV5Base1: 3u entries
    
    0     (int (*)(...))0
    
    4     (int (*)(...))(& _ZTI5Base1)
    
    8     Base1::f
    
    
    
    Class Base1
    
       size=4 align=4
    
       base size=4 base align=4
    
    Base1 (0xb6acb438) 0 nearly-empty
    
        vptr=((& Base1::_ZTV5Base1) + 8u)
    
    
    
    Vtable for Base2
    
    Base2::_ZTV5Base2: 3u entries
    
    0     (int (*)(...))0
    
    4     (int (*)(...))(& _ZTI5Base2)
    
    8     Base2::g
    
    
    
    Class Base2
    
       size=4 align=4
    
       base size=4 base align=4
    
    Base2 (0xb6acb474) 0 nearly-empty
    
        vptr=((& Base2::_ZTV5Base2) + 8u)
    
    
    
    Vtable for Derived
    
    Derived::_ZTV7Derived: 8u entries
    
    0     (int (*)(...))0
    
    4     (int (*)(...))(& _ZTI7Derived)
    
    8     Derived::f
    
    12    Derived::g
    
    16    Derived::h
    
    20    (int (*)(...))-0x000000004
    
    24    (int (*)(...))(& _ZTI7Derived)
    
    28    Derived::_ZThn4_N7Derived1gEv
    
    
    
    Class Derived
    
       size=8 align=4
    
       base size=8 base align=4
    
    Derived (0xb6b12780) 0
    
        vptr=((& Derived::_ZTV7Derived) + 8u)
    
      Base1 (0xb6acb4b0) 0 nearly-empty
    
          primary-for Derived (0xb6b12780)
    
      Base2 (0xb6acb4ec) 4 nearly-empty
    
          vptr=((& Derived::_ZTV7Derived) + 28u)

    如果看不明白这些乱七八糟的输出,没关系(当然能看懂更好),把上面的输出转换成图的形式就清楚了:

    vptr和vtbl

    vptr和vtbl

    其中有几点尤其值得注意:

    1. 我用来测试的机器是32位机,所有vptr占4个字节,每个vtbl中的函数指针也是4个字节
    2. 每个类的主要(primal)vptr放在类内存空间的起始位置(由于我没有声明任何成员变量,可能看不清楚)
    3. 在多继承中,对应各个基类的vptr按继承顺序依次放置在类内存空间中,且子类与第一个基类共用同一个vptr
    4. 子类中声明的虚函数除了覆盖各个基类对应函数的指针外,还额外添加一份到第一个基类的vptr中(体现了共用的意义)

    有了内存布局后,接下来观察g++是如何在这样的内存布局上进行动态绑定的。

    g++对每个类的指针或引用对象,如果是其类声明中虚函数,使用位于其内存空间首地址上的vptr寻找找到vtbl进而得到函数地址。如果是父类声明而子类未覆盖的虚函数,使用对应父类的vptr进行寻址。

    先来验证一下,使用 objdump -S 得到 b1.f() 的汇编指令:

        b1.f();
    
    8048734:       8b 44 24 24             mov    0x24(%esp),%eax    # 得到Base1对象的地址
    
    8048738:       8b 00                   mov    (%eax),%eax        # 对对象首地址上的vptr进行解引用,得到vtbl地址
    
    804873a:       8b 10                   mov    (%eax),%edx        # 解引用vtbl上第一个虚函数的地址
    
    804873c:       8b 44 24 24             mov    0x24(%esp),%eax
    
    8048740:       89 04 24                mov    %eax,(%esp)
    
    8048743:       ff d2                   call   *%edx              # 调用函数

    其过程和我们的分析完全一致,聪明的你可能发现了,b2怎么办呢?Derived类的实例内存首地址上的vptr并不是Base2类的啊!答案实际上是因为g++在引用赋值语句 Base2 &b2 = ins 上动了手脚:

        Derived ins;
    
    804870d:       8d 44 24 1c             lea    0x1c(%esp),%eax
    
    8048711:       89 04 24                mov    %eax,(%esp)
    
    8048714:       e8 c3 01 00 00          call   80488dc <_ZN7DerivedC1Ev>
    
        Base1 &b1 = ins;
    
    8048719:       8d 44 24 1c             lea    0x1c(%esp),%eax
    
    804871d:       89 44 24 24             mov    %eax,0x24(%esp)
    
        Base2 &b2 = ins;
    
    8048721:       8d 44 24 1c             lea    0x1c(%esp),%eax   # 获得ins实例地址
    
    8048725:       83 c0 04                add    $0x4,%eax         # 添加一个指针的偏移量
    
    8048728:       89 44 24 28             mov    %eax,0x28(%esp)   # 初始化引用
    
        Derived &d = ins;
    
    804872c:       8d 44 24 1c             lea    0x1c(%esp),%eax
    
    8048730:       89 44 24 2c             mov    %eax,0x2c(%esp)

    虽然是指向同一个实例的引用,根据引用类型的不同,g++编译器会为不同的引用赋予不同的地址。例如b2就获得一个指针的偏移量,因此才保证了vptr的正确性。

    PS:我们顺便也证明了C++中的引用的真实身份就是指针…

    接下来进入第二个问题:

    vtbl在何时被创建?vptr又是在何时被初始化?

    既然我们已经知道了g++是如何通过vptr和vtbl来实现虚函数魔法的,那么vptr和vtbl又是在什么时候被创建的呢?

    vptr是一个相对容易思考的问题,因为vptr明确的属于一个实例,所以vptr的赋值理应放在类的构造函数中。g++为每个有虚函数的类在构造函数末尾中隐式的添加了为vptr赋值的操作

    同样通过生成的汇编代码验证:

    class Derived : public Base1, public Base2
    
    {
    
    80488dc:       55                      push   %ebp
    
    80488dd:       89 e5                   mov    %esp,%ebp
    
    80488df:       83 ec 18                sub    $0x18,%esp
    
    80488e2:       8b 45 08                mov    0x8(%ebp),%eax
    
    80488e5:       89 04 24                mov    %eax,(%esp)
    
    80488e8:       e8 d3 ff ff ff          call   80488c0 <_ZN5Base1C1Ev>
    
    80488ed:       8b 45 08                mov    0x8(%ebp),%eax
    
    80488f0:       83 c0 04                add    $0x4,%eax
    
    80488f3:       89 04 24                mov    %eax,(%esp)
    
    80488f6:       e8 d3 ff ff ff          call   80488ce <_ZN5Base2C1Ev>
    
    80488fb:       8b 45 08                mov    0x8(%ebp),%eax
    
    80488fe:       c7 00 48 8a 04 08       movl   $0x8048a48,(%eax)
    
    8048904:       8b 45 08                mov    0x8(%ebp),%eax
    
    8048907:       c7 40 04 5c 8a 04 08    movl   $0x8048a5c,0x4(%eax)
    
    804890e:       c9                      leave
    
    804890f:       c3                      ret
    

    可以看到在代码中,Derived类的构造函数为实例的两个vptr赋初值,可是,这两个初值居然是立即数!立即数!立即数!这说明了vtbl的生成并不是运行时的,而是在编译期就已经确定了存放在这两个地址上

    这个地址不出意料的属于.rodata(只读数据段),使用 objdump -s -j .rodata 提取出对应的内存观察:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    80489e0 03000000 01000200 00000000 42617365  ............Base

    80489f0 313a3a66 28290042 61736532 3a3a6728  1::f().Base2::g(

    8048a00 29004465 72697665 643a3a66 28290044  ).Derived::f().D

    8048a10 65726976 65643a3a 67282900 44657269  erived::g().Deri

    8048a20 7665643a 3a682829 00000000 00000000  ved::h()........

    8048a30 00000000 00000000 00000000 00000000  ................

    8048a40 00000000 a08a0408 34880408 68880408  ........4...h...

    8048a50 94880408 fcffffff a08a0408 60880408  ............`...

    8048a60 00000000 c88a0408 08880408 00000000  ................

    8048a70 00000000 d88a0408 dc870408 37446572  ............7Der

    8048a80 69766564 00000000 00000000 00000000  ived............

    8048a90 00000000 00000000 00000000 00000000  ................

    8048aa0 889f0408 7c8a0408 00000000 02000000  ....|...........

    8048ab0 d88a0408 02000000 c88a0408 02040000  ................

    8048ac0 35426173 65320000 a89e0408 c08a0408  5Base2..........

    8048ad0 35426173 65310000 a89e0408 d08a0408  5Base1..........

    由于程序运行的机器是小端机,经过简单的转换就可以得到第一个vptr所指向的内存中的第一条数据为0x08048834,如果把这个数据解释为函数地址到汇编文件中查找,会得到:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    08048834 <_ZN7Derived1fEv>:

    };

     

    class Derived : public Base1, public Base2

    {

    public:

        virtual void f() {

    8048834:       55                      push   %ebp

    8048835:       89 e5                   mov    %esp,%ebp

    8048837:       83 ec 18                sub    $0x18,%esp

    Bingo!g++在编译期就为每个类确定了vtbl的内容,并且在构造函数中添加相应代码使vptr能够指向已经填好的vtbl的地址

    这也同时为我们解答了第三个问题:

    在Linux中运行的C++程序虚拟存储器中,vptr、vtbl存放在虚拟存储的什么位置?

    直接看图:

    虚函数在虚拟存储器中的位置

    虚函数在虚拟存储器中的位置

    图中灰色部分应该是你已经熟悉的,彩色部分内容和相关联的箭头描述了虚函数调用的过程(图中展示的是通过new在堆区创建实例的情况,与示例代码有所区别,小失误,不要在意):当调用虚函数时,首先通过位于栈区的实例的指针找到位于堆区中的实例地址,然后通过实例内存开头处的vptr找到位于.rodata段的vtbl,再根据偏移量找到想要调用的函数地址,最后跳转到代码段中的函数地址执行目标函数

    总结

    研究这些问题的起因是因为公司代码出现了非常奇葩的行为,经过追查定位到虚函数表出了问题,因此才有机会脚踏实地的对虚函数实现进行一番探索。

    也许你会想,即使我不明白这些底层原理,也一样可以正常的使用虚函数,也一样可以写出很好的面相对象的代码啊?

    这一点儿也没有错,但是,C++作为全宇宙最复杂的程序设计语言,它提供的功能异常强大,无异于武侠小说中锋利无比的屠龙宝刀。但武功不好的菜鸟如果胡乱舞弄宝刀,却很容易反被其所伤。只有了解了C++底层的原理和机制,才能让我们把C++这把屠龙宝刀使用的更加得心应手,变化出更加华丽的招式,成为真正的武林高手。

    展开全文
  • 虚函数和纯虚函数详解

    千次阅读 2018-09-09 23:05:44
    虚函数和普通的函数实际上是存储在不同的区域的,虚函数所在的区域是可被覆盖(也称复写override)的,每当子类定义相同名称的虚函数时就将原来基类的版本给覆盖了,另一侧面也说明了为什么基类声明的虚函数在后代...

    https://mp.weixin.qq.com/s?__biz=MzAxNzYzMTU0Ng==&mid=2651289202&idx=1&sn=431ffd1fae4823366a50b68aed2838d4&chksm=80114627b766cf31f72018ef5f1fe29591e9f6f4bd72018e7aea849342ca6f0a271fb38465ae#rd

    打开链接看。转载文章,注明出处

                    <p>学习C++的多态性,你必然听过虚函数的概念,你必然知道有关她的种种语法,但你未必了解她为什么要那样做,未必了解她种种行为背后的所思所想。深知你不想在流于表面语法上的蜻蜓点水似是而非,今天我们就一起来揭开挡在你和虚函数(女神)之间的这一层窗户纸。</p><p style="white-space: normal;"><br></p><p style="white-space: normal;text-align: center;"><img class="" data-ratio="0.040690505548705305" data-src="http://mmbiz.qpic.cn/mmbiz_png/d0YYOX6ZlbwxEuFWMbayZLMTA3E4icqQ0LTQYboqGxvVCAF7yj9TOJjRgYTJjc8eichlRgco5icXaofHEwThf73MQ/0?wx_fmt=png" data-type="png" data-w="811" title="分割线" style="display: inline; width: 547px !important; height: auto !important; visibility: visible !important;" _width="547px" src="http://mmbiz.qpic.cn/mmbiz_png/d0YYOX6ZlbwxEuFWMbayZLMTA3E4icqQ0LTQYboqGxvVCAF7yj9TOJjRgYTJjc8eichlRgco5icXaofHEwThf73MQ/640?wx_fmt=png&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" crossorigin="anonymous" data-fail="0"></p><p style="white-space: normal;text-align: center;"><br></p><p style="white-space: normal;">首先,我们要搞清楚女神的所作所为,即语法规范。然后再去探究她背后的逻辑道理。她的语法说来也不复杂,概括起来就这么几条:</p><p style="white-space: normal;"><br></p><ol class="list-paddingleft-2" style="list-style-type: decimal;"><li><p style="white-space: normal;">在类成员方法的声明(不是定义)语句前面加个单词:<span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>virtual</strong></span>,她就会摇身一变成为虚函数。</p></li><li><p style="white-space: normal;">在虚函数的声明语句末尾中加个 <strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">=0</span></strong> ,她就会摇身一变成为纯虚函数。</p></li><li><p style="white-space: normal;">子类可以重新定义基类的虚函数,我们把这个行为称之为<strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">复写</span></strong>(<span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>override</strong></span>)。</p></li><li><p style="white-space: normal;">不管是虚函数还是纯虚函数,<strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">基类都可以为提供他们的实现</span></strong>(<span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>implementation</strong></span>),如果有的话子类可以调用基类的这些实现。</p></li><li><p style="white-space: normal;">子类可<strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">自主选择</span></strong>是否要提供一份属于自己的个性化虚函数实现。</p></li><li><p style="white-space: normal;">子类<strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">必须提供</span></strong>一份属于自己的个性化纯虚函数实现。</p><p style="white-space: normal;"><br></p></li></ol><section class="" data-tools="135编辑器" data-id="90142" style="border-width: 0px; border-style: none; border-color: initial; box-sizing: border-box; --darkreader-inline-border-top: initial; --darkreader-inline-border-right: initial; --darkreader-inline-border-bottom: initial; --darkreader-inline-border-left: initial;" data-darkreader-inline-border-top="" data-darkreader-inline-border-right="" data-darkreader-inline-border-bottom="" data-darkreader-inline-border-left=""><section style="text-align:center;"><section style="display:inline-block;"><img class=" __bg_gif" data-ratio="0.6470588235294118" data-src="http://mmbiz.qpic.cn/mmbiz_gif/d0YYOX6Zlby3pIrMZzOlNLBFgYEWJI3P7oPYfGhH71NFibcIASiadaicc9qC3RlIwvibOaTxaAczEGYC5fND4qZibaQ/0?wx_fmt=gif" data-type="gif" data-w="34" style="display: inline; width: 34px !important; height: auto !important; visibility: visible !important;" title="音符动态简约分割线" _width="34px" src="http://mmbiz.qpic.cn/mmbiz_gif/d0YYOX6Zlby3pIrMZzOlNLBFgYEWJI3P7oPYfGhH71NFibcIASiadaicc9qC3RlIwvibOaTxaAczEGYC5fND4qZibaQ/0?wx_fmt=gif&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1" data-order="0" data-fail="0"></section></section></section><p><br></p><p>语法都列出来了,背后的逻辑含义是什么呢?我们用一个生动的例子来说明,虚函数是如何实现多态性的。</p><p><br></p><p>假设我们要设计关于飞行器的类,并且提供类似加油、飞行的实现代码,考虑具体情况,飞行器多种多样,有民航客机、歼击机、轰炸机、直升机、热气球、火箭甚至窜天猴、孔明灯、纸飞机!</p><p>假设我们有一位牛得一比的飞行员,他能给各式各样的飞行器加充不同的燃料,也能驾驶各式各样的飞行器。下面我们来看看这些类可以怎么设计。</p><p style="text-align: center;"><img data-s="300,640" data-type="jpeg" data-src="https://mmbiz.qpic.cn/mmbiz_jpg/d0YYOX6Zlby3pIrMZzOlNLBFgYEWJI3PT70rvCkl3ZpWOwS6SAQTj2G6leoucU40ly2BI7f7kUBlSXvFMQkMxg/0?wx_fmt=jpeg" data-copyright="0" style="width: 164px !important; height: auto !important; visibility: visible !important;" class="" data-ratio="1.397887323943662" data-w="284" _width="164px" src="https://mmbiz.qpic.cn/mmbiz_jpg/d0YYOX6Zlby3pIrMZzOlNLBFgYEWJI3PT70rvCkl3ZpWOwS6SAQTj2G6leoucU40ly2BI7f7kUBlSXvFMQkMxg/640?wx_fmt=jpeg&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" crossorigin="anonymous" data-fail="0"></p><p>首先,飞行器。由于我们假设所有的飞行器都有两种行为:<span style="color: rgb(217, 33, 66); --darkreader-inline-color:#d86677;" data-darkreader-inline-color=""><strong>加油</strong><span style="color: rgb(0, 0, 0); --darkreader-inline-color:#e9e3d5;" data-darkreader-inline-color="">和</span><strong>飞行</strong></span>。因此我们可以将这两种行为抽象到一个基类中,并由它来派生具体的某款飞行器。</p><p><br></p><p><em><span style="font-size: 14px; color: rgb(178, 178, 178); --darkreader-inline-color:#c3b49e;" data-darkreader-inline-color="">这是一个描述飞行器的基类,提供了两个基本的功能:加油和飞行</span></em></p><blockquote><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">class aircraft</span></strong></p><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">{</span></strong></p><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">&nbsp; &nbsp; void <span style="color: rgb(217, 33, 66); --darkreader-inline-color:#d86677;" data-darkreader-inline-color="">refuel</span>(); </span></strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">// 加燃油,普通虚函数</span></p><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">&nbsp; &nbsp; void <span style="color: rgb(217, 33, 66); --darkreader-inline-color:#d86677;" data-darkreader-inline-color="">fly</span>()=0; </span></strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">&nbsp;// 飞行,纯虚函数</span></p><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">};</span></strong></p></blockquote><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><br></span></strong></p><p style="line-height: normal;"><em><span style="font-size: 14px; color: rgb(178, 178, 178); --darkreader-inline-color:#c3b49e;" data-darkreader-inline-color="">这是一个普通虚函数,意味着基类希望子类提供自己的个性化实现代码,但基类同时也提供一个缺省的虚函数实现版本,在子类不复写该虚函数的情况下作为备选方案</span></em></p><blockquote><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">void aircraft::<span style="color: rgb(217, 33, 66); --darkreader-inline-color:#d86677;" data-darkreader-inline-color="">refuel</span>()</span></strong></p><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">{</span></strong></p><p style="line-height: normal;"><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">&nbsp; &nbsp; // 加充通用型燃油</span></p><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">}</span></strong></p></blockquote><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><br></span></strong></p><p style="line-height: normal;"><em><span style="font-size: 14px; color: rgb(178, 178, 178); --darkreader-inline-color:#c3b49e;" data-darkreader-inline-color="">这是一个纯虚函数,意味着基类强制子类必须提供自己的个性化版本,否则编译将失败。但让人惊奇的是,C++仍然保留了基类提供该纯虚函数代码实现的权利,这也许是给千变万化的实际情况留下后路</span></em></p><blockquote><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">void aircraft::<span style="color: rgb(217, 33, 66); --darkreader-inline-color:#d86677;" data-darkreader-inline-color="">fly</span>()</span></strong></p><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">{</span></strong></p><p style="line-height: normal;"><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">&nbsp; &nbsp; // 一种不应该被使用的缺省飞行方案</span></p><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">}</span></strong></p></blockquote><p><br></p><p>有了基类aircraft,我们就可以潇洒地派生出各式各样的飞行器了,比如<strong><span style="color: rgb(123, 12, 0); --darkreader-inline-color:#f19385;" data-darkreader-inline-color="">轰炸机</span></strong>和<strong><span style="color: rgb(123, 12, 0); --darkreader-inline-color:#f19385;" data-darkreader-inline-color="">直升机</span></strong>:<br></p><p style="text-align: center;"><img data-s="300,640" data-type="png" data-src="https://mmbiz.qpic.cn/mmbiz_png/d0YYOX6Zlby3pIrMZzOlNLBFgYEWJI3PfMEezwpiaooicVBfbqUtZqmHjibqIJHwU7s6qP8Yj1mGD0kpq4OOS5ZiaA/0?wx_fmt=png" data-copyright="0" style="width: 220px !important; height: auto !important; visibility: visible !important;" class="" data-ratio="0.49290060851926976" data-w="493" _width="220px" src="https://mmbiz.qpic.cn/mmbiz_png/d0YYOX6Zlby3pIrMZzOlNLBFgYEWJI3PfMEezwpiaooicVBfbqUtZqmHjibqIJHwU7s6qP8Yj1mGD0kpq4OOS5ZiaA/640?wx_fmt=png&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" crossorigin="anonymous" data-fail="0"><img data-s="300,640" data-type="jpeg" data-src="https://mmbiz.qpic.cn/mmbiz_jpg/d0YYOX6Zlby3pIrMZzOlNLBFgYEWJI3PEyKgERCNSia86eKWflCtaFxCFcqtSTSibHHSicl90kRs9SI68XkFxTn5w/0?wx_fmt=jpeg" data-copyright="0" class="" data-ratio="0.3764705882352941" data-w="425" style="width: 251px !important; height: auto !important; visibility: visible !important;" _width="251px" src="https://mmbiz.qpic.cn/mmbiz_jpg/d0YYOX6Zlby3pIrMZzOlNLBFgYEWJI3PEyKgERCNSia86eKWflCtaFxCFcqtSTSibHHSicl90kRs9SI68XkFxTn5w/640?wx_fmt=jpeg&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" crossorigin="anonymous" data-fail="0"></p><p><em><span style="font-size: 14px; color: rgb(178, 178, 178); --darkreader-inline-color:#c3b49e;" data-darkreader-inline-color="">轰炸机类定义,复写了加油和飞行</span></em></p><blockquote><p style="line-height: normal;"><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>class&nbsp;</strong></span><strong><span style="color: rgb(123, 12, 0); --darkreader-inline-color:#f19385;" data-darkreader-inline-color="">bomber</span></strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong> : public aircraft</strong></span></p><p style="line-height: normal;"><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>{</strong></span></p><p style="line-height: normal;"><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>&nbsp; &nbsp; void <span style="color: rgb(216, 79, 169); --darkreader-inline-color:#d270ac;" data-darkreader-inline-color="">refuel</span>(){}</strong> // 加充轰炸机的特殊燃油!</span></p><p style="line-height: normal;"><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>&nbsp; &nbsp; void</strong></span> <span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong><span style="color: rgb(216, 79, 169); --darkreader-inline-color:#d270ac;" data-darkreader-inline-color="">fly</span></strong><strong>(){}</strong> // 轰炸机实弹飞行!</span></p><p style="line-height: normal;"><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>};</strong></span></p></blockquote><p><br></p><p><em><span style="font-size: 14px; color: rgb(178, 178, 178); --darkreader-inline-color:#c3b49e;" data-darkreader-inline-color="">直升机类定义,复写了飞行代码,但没有复写加油</span></em></p><blockquote><p style="line-height: normal;"><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>class&nbsp;</strong></span><strong><span style="color: rgb(123, 12, 0); --darkreader-inline-color:#f19385;" data-darkreader-inline-color="">copter</span></strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>: public aircraft</strong></span></p><p style="line-height: normal;"><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>{</strong></span></p><p style="line-height: normal;"><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>&nbsp; &nbsp; void </strong></span><strong><span style="color: rgb(216, 79, 169); --darkreader-inline-color:#d270ac;" data-darkreader-inline-color="">fly</span></strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>(){}</strong> // 直升机盘旋!</span></p><p style="line-height: normal;"><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>};</strong></span></p></blockquote><p><br></p><p>以上代码可以看到,直升机类(copter)没有自己的加油方式,直接使用了基类提供的缺省加油的方式。此时我们来定义一个能驾驭多机型的王牌飞行员类:</p><p><br></p><p><em><span style="font-size: 14px; color: rgb(178, 178, 178); --darkreader-inline-color:#c3b49e;" data-darkreader-inline-color="">一个能王牌飞行员</span></em></p><blockquote><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">class pilot</span></strong></p><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">{</span></strong></p><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">&nbsp; &nbsp; void <span style="color: rgb(217, 33, 66); --darkreader-inline-color:#d86677;" data-darkreader-inline-color="">refuelPlane</span>(aircraft *p);</span></strong></p><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">&nbsp; &nbsp; void <span style="color: rgb(217, 33, 66); --darkreader-inline-color:#d86677;" data-darkreader-inline-color="">dirvePlane</span>(aircraft *p);</span></strong></p><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">};</span></strong></p></blockquote><p><br></p><p><em><span style="font-size: 14px; color: rgb(178, 178, 178); --darkreader-inline-color:#c3b49e;" data-darkreader-inline-color="">给我什么飞机我就加什么油</span></em></p><blockquote><p style="white-space: normal;line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">void pilot::refuelPlane(<span style="color: rgb(217, 33, 66); --darkreader-inline-color:#d86677;" data-darkreader-inline-color="">aircraft *p</span>)</span></strong></p><p style="white-space: normal;line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">{</span></strong></p><p style="white-space: normal;line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">&nbsp; &nbsp; p-&gt;refuel();</span></strong></p><p style="white-space: normal;line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">}</span></strong></p></blockquote><p style="white-space: normal;"><br></p><p style="white-space: normal;"><em><span style="font-size: 14px; color: rgb(178, 178, 178); --darkreader-inline-color:#c3b49e;" data-darkreader-inline-color="">给我什么飞机我就怎么飞</span></em></p><blockquote><p style="white-space: normal;line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">void pilot::dirvePlane(<span style="color: rgb(217, 33, 66); --darkreader-inline-color:#d86677;" data-darkreader-inline-color="">aircraft *p</span>)</span></strong></p><p style="white-space: normal;line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">{</span></strong></p><p style="white-space: normal;line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">&nbsp; &nbsp; p-&gt;fly();</span></strong></p><p style="white-space: normal;line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">}</span></strong></p></blockquote><p><br></p><p>很明显,我们接下来要给这位很浪的飞行员表演一下操纵各种飞行器的机会,我们来定义各种飞机然后丢给他去处理<br></p><p><br></p><p><em><span style="font-size: 14px; color: rgb(178, 178, 178); --darkreader-inline-color:#c3b49e;" data-darkreader-inline-color="">定义两架飞机,一架轰6K,一架武直10</span></em></p><blockquote><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">aircraft *<span style="color: rgb(217, 33, 66); --darkreader-inline-color:#d86677;" data-darkreader-inline-color="">H6K </span>= new bomber;</span></strong></p><p style="line-height: normal;"><strong><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color="">aircraft *<span style="color: rgb(217, 33, 66); --darkreader-inline-color:#d86677;" data-darkreader-inline-color="">WZ10&nbsp;</span>= new copter;</span></strong></p></blockquote><p><br></p><p><em><span style="font-size: 14px; color: rgb(178, 178, 178); --darkreader-inline-color:#c3b49e;" data-darkreader-inline-color="">来一个王牌飞行员,给H6K加油(加的是轰炸机特殊燃油),并且按照H6K的特点飞行</span></em></p><blockquote><p style="line-height: normal;"><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>pilot <span style="color: rgb(217, 33, 66); --darkreader-inline-color:#d86677;" data-darkreader-inline-color="">Jack</span>;</strong></span></p><p style="line-height: normal;"><span style="color: rgb(217, 33, 66); --darkreader-inline-color:#d86677;" data-darkreader-inline-color=""><strong>Jack</strong></span><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>.refuelPlane(H6K); &nbsp;// 加充轰炸机燃油</strong></span></p><p style="line-height: normal;"><span style="color: rgb(217, 33, 66); --darkreader-inline-color:#d86677;" data-darkreader-inline-color=""><strong>Jack</strong></span><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>.flyPlane(H6K); // 轰炸机实弹飞行</strong></span></p></blockquote><p><br></p><p><em><span style="font-size: 14px; color: rgb(178, 178, 178); --darkreader-inline-color:#c3b49e;" data-darkreader-inline-color="">给WZ10加油(加的是基类提供的通用燃油),按照WZ10的特点飞行</span></em></p><blockquote><p style="white-space: normal;"><span style="color: rgb(217, 33, 66); --darkreader-inline-color:#d86677;" data-darkreader-inline-color=""><strong>Jack</strong></span><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>.refuelPlane(WZ10); // 加充通用型燃油</strong></span></p><p style="white-space: normal;"><span style="color: rgb(217, 33, 66); --darkreader-inline-color:#d86677;" data-darkreader-inline-color=""><strong>Jack</strong></span><span style="color: rgb(0, 82, 255); --darkreader-inline-color:#4578e5;" data-darkreader-inline-color=""><strong>.flyPlane(WZ10); // 直升机盘旋</strong></span></p></blockquote><p style="white-space: normal;"><br></p><p style="white-space: normal;">上述代码体现了最经典的所谓多态的场景,给Jack不同的飞机,就能表现不同的结果。虚函数和纯虚函数都能做到这一点,区别是,子类如果不提供虚函数的实现,那就会自动调用基类的缺省方案。而子类如果不提供纯虚函数的实现,则编译将会失败。基类提供的纯虚函数实现版本,无法通过指向子类对象的基类类型指针或引用来调用,因此不能作为子类相应虚函数的备选方案。下面给出总结。</p><p style="white-space: normal;"><br></p><section class="" data-tools="135编辑器" data-id="91269" style="border-width: 0px; border-style: none; border-color: initial; box-sizing: border-box; --darkreader-inline-border-top: initial; --darkreader-inline-border-right: initial; --darkreader-inline-border-bottom: initial; --darkreader-inline-border-left: initial;" data-darkreader-inline-border-top="" data-darkreader-inline-border-right="" data-darkreader-inline-border-bottom="" data-darkreader-inline-border-left=""><section style="text-align:center;"><img class="" data-ratio="0.0598669623059867" data-src="http://mmbiz.qpic.cn/mmbiz_png/d0YYOX6Zlby3pIrMZzOlNLBFgYEWJI3PDaKcmftE3HfjOkwXxpjTG32TUXD90sTrwrqtBhSmsLpK7VicOFgonrg/0?wx_fmt=png" data-type="png" data-w="451" style="display: inline; width: 300px !important; height: auto !important; visibility: visible !important;" title="欧式古典 分割线" _width="300px" src="http://mmbiz.qpic.cn/mmbiz_png/d0YYOX6Zlby3pIrMZzOlNLBFgYEWJI3PDaKcmftE3HfjOkwXxpjTG32TUXD90sTrwrqtBhSmsLpK7VicOFgonrg/640?wx_fmt=png&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" crossorigin="anonymous" data-fail="0"></section></section><p><br></p><p>第一,<strong>当基类的某个成员方法,在大多数情形下都应该由子类提供个性化实现,但基类也可以提供一个备选方案的时候,请将其设计为虚函数</strong>。例如飞行器的加油动作,每种不同的飞行器原则上都应该有自己的个性化的加充然后的方式,但也不免可以有一种通用的然后和加充方式。</p><p><br></p><p>第二,<strong>当基类的某个成员方法,必须由子类提供个性化实现的时候,请将其设计为纯虚函数</strong>。例如飞行器的飞行动作,逻辑上每种飞行器都必须提供为其特殊设计的个性化飞行行为,而不应该有任何一种“通用的飞行方式”。</p><p><br></p><p>第三,<strong>使用一个基类类型的指针或者引用,来指向子类对象,进而调用经由子类复写了的个性化的虚函数,这是C++实现多态性的一个最经典的场景</strong>。</p><p><br></p><p>第四,基类提供的纯虚函数的实现版本,并非为了多态性考虑,因为指向子类对象的基类指针和引用无法调用该版本。<strong>纯虚函数在基类中的实现跟多态性无关,它只是提供了一种语法上的便利,在变化多端的应用场景中留有后路</strong>。</p><p><br></p><p>第五,<strong>虚函数和普通的函数实际上是存储在不同的区域的</strong>,虚函数所在的区域是可被覆盖(也称复写override)的,每当子类定义相同名称的虚函数时就将原来基类的版本给覆盖了,另一侧面也说明了为什么基类中声明的虚函数在后代类中不需要另加声明一律自动为虚函数,因为它所存储的位置不会发生改变。而普通函数的存储区域不会覆盖,每个类都有自己独立的区域互不相干。</p><p><br></p><p>最后附一幅草图以供参考</p><p><img data-s="300,640" data-type="jpeg" data-src="https://mmbiz.qpic.cn/mmbiz_jpg/d0YYOX6Zlby3pIrMZzOlNLBFgYEWJI3PN6GG3sY9otrSozjIaZfxneKRHRUOCpKQGgBcXx3NCl5OPwia8bpxwBA/0?wx_fmt=jpeg" data-copyright="0" style="width: 677px !important; height: auto !important; visibility: visible !important;" class="" data-ratio="0.5625" data-w="1280" _width="677px" src="https://mmbiz.qpic.cn/mmbiz_jpg/d0YYOX6Zlby3pIrMZzOlNLBFgYEWJI3PN6GG3sY9otrSozjIaZfxneKRHRUOCpKQGgBcXx3NCl5OPwia8bpxwBA/640?wx_fmt=jpeg&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" crossorigin="anonymous" data-fail="0"></p><p><br></p><section class="" data-source="bj.96weixin.com" style="white-space: normal;"><section class="" data-ele="imb" style="border-width: 0px;border-style: none;"><section style="margin-top: 10px;box-sizing: border-box;font-family: 微软雅黑;"><section class="" style="padding: 10px; background-color: rgb(182, 228, 253); text-align: justify; --darkreader-inline-bgcolor:#16445b;" data-darkreader-inline-bgcolor=""><p style="overflow-wrap: break-word;line-height: 1.5em;"><span style="color: inherit; font-size: 14px; --darkreader-inline-color: inherit;" data-darkreader-inline-color="">识别下面二维码进入&nbsp;</span><span style="font-size: 16px; color: rgb(171, 25, 66); font-weight: bold; --darkreader-inline-color:#df7d93;" data-darkreader-inline-color="">微店</span><span style="color: inherit; font-size: 14px; --darkreader-inline-color: inherit;" data-darkreader-inline-color=""><span style="font-size: 16px;"><strong><span style="color: rgb(171, 25, 66); --darkreader-inline-color:#df7d93;" data-darkreader-inline-color="">●</span></strong><strong><span style="color: rgb(171, 25, 66); --darkreader-inline-color:#df7d93;" data-darkreader-inline-color="">秘籍酷&nbsp;</span></strong></span>瞅一眼呗!也许有你喜欢的东西</span></p></section><section class="" data-wxsrc="http://mmbiz.qpic.cn/mmbiz_png/p6Vlqvia1Uicx4P4GotoIy3EibRibkEgicfCyW3ntgWjeGSa0qhpNRn7pLzBdTBYzJ6XOPPINUTaIczxAP6zFfMM9FQ/0?wx_fmt=png" style="height: 20px;background-image: url(&quot;http://mmbiz.qpic.cn/mmbiz_png/d0YYOX6ZlbxAMtYaYL4el9dAzOJGcj02HD6Aw7JbAdVclKtZOyrsU1ricnWH4H7wc8J0FnyNajFZmwIkEyd9DOg/0?wx_fmt=png&quot;);background-repeat: no-repeat;background-size: 50% 100%;"><br></section></section></section></section><section class="" data-source="bj.96weixin.com" style="white-space: normal;line-height: 25.6px;"><section class="" data-width="320px" style="margin-right: auto;margin-left: auto;border-style: none;width: 320px;clear: both;overflow: hidden;"><section data-width="100%" style="padding-right: 0.1em;width: 320px;float: left;"><p><img class=" __bg_gif" data-ratio="0.6583333333333333" data-src="http://mmbiz.qpic.cn/mmbiz/d0YYOX6ZlbzEJj7MCYjbiaJpFRSnbYNq9hhibppd5ib2OYnibsbFGiaskrdGqxGwmGdQhPtC09E0yvQaGtmic7xf4kTw/0?wx_fmt=gif" data-type="gif" data-w="600" data-width="319px" height="190" width="295" style="width: 295px !important; height: auto !important; visibility: visible !important;" _width="295px" src="http://mmbiz.qpic.cn/mmbiz/d0YYOX6ZlbzEJj7MCYjbiaJpFRSnbYNq9hhibppd5ib2OYnibsbFGiaskrdGqxGwmGdQhPtC09E0yvQaGtmic7xf4kTw/0?wx_fmt=gif&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1" data-order="1" data-fail="0"><br></p><section style="margin-top: -10.5em; margin-left: 0.2em; padding: 0.2em 1em; float: left; font-size: 18px; text-align: center; color: rgb(255, 255, 255); opacity: 0.95; --darkreader-inline-color:#fbf7f1;" data-darkreader-inline-color=""><img class="" data-src="https://mmbiz.qpic.cn/mmbiz_jpg/d0YYOX6ZlbwxEuFWMbayZLMTA3E4icqQ0CLoyEht9RTIJgDMyicz8AAxbhtsomhiclSMficf1XlEY1ykKsVWAQXt3w/0?wx_fmt=jpeg" data-width="130px" title="" vspace="0" border="0" data-type="jpeg" data-cropselx1="0" data-cropselx2="130" data-cropsely1="0" data-cropsely2="130" data-copyright="0" data-ratio="1" data-w="210" style="width: 130px !important; height: auto !important; visibility: visible !important;" _width="130px" src="https://mmbiz.qpic.cn/mmbiz_jpg/d0YYOX6ZlbwxEuFWMbayZLMTA3E4icqQ0CLoyEht9RTIJgDMyicz8AAxbhtsomhiclSMficf1XlEY1ykKsVWAQXt3w/640?wx_fmt=jpeg&amp;tp=webp&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" crossorigin="anonymous" data-fail="0"></section></section></section></section>
                </div>
    
    展开全文
  • C++虚函数重载

    千次阅读 2017-02-19 12:23:13
    在一次修改代码过程踩的坑,下来研究了一下,发现C++中虚函数重载后会产生很多有意思的情况,在这里总结了一下。 C++有重载(overload)和重写(override)以及重定义这几个概念,1 overload:指的是相同作用域...

    在一次修改代码过程中踩的坑,下来研究了一下,发现C++中虚函数重载后会产生很多有意思的情况,在这里总结了一下。

    C++中有重载(overload)和重写(override)以及重定义这几个概念,1 overload:指的是相同作用域中的两个函数的函数名相同,但参数列表的个数、顺序、类型不同。而override指的是子类中重新定义的父类的虚函数。
    2 override:overload要求两个函数的参数列表必须不同,但是不要求这两个函数必须是虚函数。而override要求必须是虚函数且父类的虚函数必须有virtual关键字,函数的参数列表和返回值也必须相同。子类中override的虚函数的访问修饰符可以不同。
    3 重定义也是描述分别位于父类与子类中的同名函数的,但返回值可以不同。如果参数列表不同,这时子类中重定义的函数不论是否有virtual关键字,都会隐藏父类的同名函数。
    如果参数列表相同,但父类中的同名函数没有virtual关键字修饰,此时父类中的函数仍然被隐藏。

    虚函数的典型用法是:

    #include <iostream>
    using namespace std; 
    
    class Base{
    public:
        virtual void f();
    
    };
    
    void Base::f(){
        cout << "Base::f()" << endl;
    }
    
    class Derived:public Base{
    public:
        virtual void f();
    };
    
    void Derived::f(){
        cout << "Derived::f()" << endl;
    }
    
    int main() {
        Base *p1 = new Base;
        Base *p2 = new Derived;
        p1->f();
        p2->f();
        
        delete p1;
        delete p2;
    
        return 0;
    }
    

    程序的输出是:

    Base::f()
    Derived::f()
    

    那么如果虚函数之间又发生了overload,会出现什么情况?
    我们先看最简单的情况:

    #include <iostream>
    using namespace std; 
    
    class Base{
    public:
        virtual void f(int);
    };
    
    void Base::f(int a){
        cout << "Base::f(int) " << a << endl;
    }
    
    class Derived:public Base{
    public:
        virtual void f();
    };
    
    void Derived::f(){
        cout << "Derived::f()" << endl;
    }
    
    int main() {
        Base *p1 = new Base;
        p1->f(1);
        
        Base *p2 = new Derived;
        p2->f();
        
        delete p1;
        delete p2;
        
        return 0;
    }
    

    编译上面的代码,会发生如下错误:

    test.cpp: In function ‘int main()’:
    test.cpp:28:11: error: no matching function for call to ‘Base::f()’
         p2->f();
               ^
    test.cpp:10:6: note: candidate: virtual void Base::f(int)
     void Base::f(int a){
          ^
    test.cpp:10:6: note:   candidate expects 1 argument, 0 provided
    

    这就是因为父类中虚函数的参数列表已经发生变化,这时不论子类中重定义的函数不论是否有virtual关键字,都会隐藏父类的同名函数。这时子类中只是重定义了一个自己的函数virtual void f(),而并没有override父类中对应的虚函数。
    p2是一个指向Base类型的指针,根据虚函数的特性,对p2→f();的处理取决于是否override了父类的虚函数,如果没有,仍然会调用调用父类中被override的虚函数,但是现在父类中的函数已经成为了virtual void f(int),因此在执行p2→f()时会由于缺少输入参数而出现上述错误。
    为了证明上述论断,可以在执行p2→f()时传入参数来判断:

    #include <iostream>
    using namespace std; 
    
    class Base{
    public:
        virtual void f(int);
    };
    
    void Base::f(int a){
        cout << "Base::f(int) " << a << endl;
    }
    
    class Derived:public Base{
    public:
        virtual void f();
    };
    
    void Derived::f(){
        cout << "Derived::f()" << endl;
    }
    
    int main() {
        Base *p1 = new Base;
        p1->f(1);
        
        Base *p2 = new Derived;
        p2->f(2);
        
        delete p1;
        delete p2;
        
        return 0;
    }
    

    这时可以通过编译,执行结果为

    Base::f(int) 1
    Base::f(int) 2
    

    可以看到,子类自己定义的virtual void f()其实是父类的virtual void f(int)的一个重定义的函数,这时尽管p2实际指向了一个Derived对象,但由于没有override父类对应的虚函数,在执行 p2→f(2)时将执行父类的virtual void f(int)。
    也可以这样修改:

    #include <iostream>
    using namespace std; 
    
    class Base{
    public:
        virtual void f(int);
        virtual void f();
    };
    
    void Base::f(int a){
        cout << "Base::f(int) " << a << endl;
    }
    
    void Base::f(){
        cout << "Base::f() " << endl;
    }
    
    class Derived:public Base{
    public:
        virtual void f();
    };
    
    void Derived::f(){
        cout << "Derived::f()" << endl;
    }
    
    int main() {
        Base *p1 = new Base;
        p1->f(1);
        
        Base *p2 = new Derived;
        p2->f();
        
        delete p1;
        delete p2;
        
        return 0;
    }
    

    因为父类中定义了可被子类override的函数,所以这时执行p2→f()又会重新执行子类的virtual void f():

    Base::f(int) 1
    Derived::f()
    

    我们甚至还可以这样验证:

    #include <iostream>
    using namespace std; 
    
    class Base {
    public:
        virtual void f(int);
        virtual void f();
    };
    
    void Base::f(int a) {
        cout << "Base::f(int) " << a << endl;
    }
    
    void Base::f() {
        cout << "Base::f() " << endl;
    }
    
    class Derived:public Base {
    public:
        virtual void f(float);
    };
    
    void Derived::f(float a) {
        cout << "Derived::f(float)" << showpoint << a << endl;
    }
    
    
    int main() {
        Base *p1 = new Base;
        p1->f(1);
        
        Base *p2 = new Derived;
        p2->f(**2.0**);
        
        delete p1;
        delete p2;
        
        return 0;
    }
    

    这时输出仍然为

    Base::f(int) 1
    Base::f(int) 2
    

    这说明如果通过指向父类的指针,调用虚函数时,如果子类重定义了该虚函数(参数列表发生变化),则实际调用的仍是父类中的虚函数。
    上面都是通过指向父类的指针来调用虚函数的,那么如果通过指向子类的指针调用虚函数会发生什么:

    #include <iostream>
    using namespace std; 
    
    class Base {
    public:
        virtual void f(int);
        virtual void f();
    
    };
    
    void Base::f(int a) {
        cout << "Base::f(int) " << a << endl;
    }
    
    void Base::f() {
        cout << "Base::f() " << endl;
    }
    
    class Derived:public Base {
    public:
        virtual void f(float);
    };
    
    void Derived::f(float a) {
        cout << "Derived::f(float)" << showpoint << a << endl;
    }
    
    int main() {
        Base *p1 = new Base;
        p1->f(1);
        
        Derived *p2 = new Derived;
        p2->f(2.0);
        p2->f(3);
        
        delete p1;
        delete p2;
        
        return 0;
    }
    

    这时输出就变为了:

    Base::f(int) 1
    Derived::f(float)2.00000
    Derived::f(float)3.00000
    

    这说明,如果通过指向子类的指针调用虚函数,并且子类重定义了父类的虚函数,这时实际调用的就将是子类中的虚函数。


    展开全文
  • 简述虚函数

    千次阅读 2019-04-04 18:00:28
    前段时间我在博客简单地说了下C++的虚函数,所谓虚函数,就是C++实现多态性的方法。那么编译器是如何识别虚函数的呢?据百度百科描述,C++并未规定用何种方法实现虚函数,但是大部分编译器厂商都选择使用虚函数表...
  • C++虚函数.docx

    2019-11-07 18:50:43
    C++通过虚函数实现动多态。虚函数的本质就是通过基类访问派生类定义的函数。每一个含有虚函数的类,其实例对象内部都有一个虚函数表指针,该虚函数表指针被初始化为本类的虚函数表的内存地址。对虚函数实现机制...
  • 虚函数的指针访问

    2015-06-06 07:45:41
    资源描述了含有虚函数的类、派生类对象的存储结构,以及使用成员函数指针访问虚函数的方法
  • 在面向对象的设计思想虚函数的作用是实现多态性。如何实现多态呢?下面看C++虚函数表的解析来理解。 虚函数表 http://blog.csdn.net/haoel/article/details/1948051/ 对C++ 了解的人都应该知道虚函数(Virt
  • C++重载与重写函数区别及虚函数

    千次阅读 2018-07-11 21:50:21
    C++虚函数(virtual function) 1.简介 虚函数是C++用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。假设我们有下面的类层次:classA{public: virtual void foo() { cout &...
  • C++——虚函数与抽象类

    千次阅读 2018-12-28 10:57:37
    虚函数 虚函数:(virtual function)允许函数调用与函数体间的联系在运行时才建立,是实现动态联编的基础。...如果派生类没有重新定义虚函数,基类虚函数充当派生类的虚函数。 要由成员函数、指针或引...
  • 深入理解多态虚函数--虚函数表解析

    千次阅读 2018-02-09 15:02:13
    C++虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。...
  • c++虚基类表和虚函数表的布局

    万次阅读 2017-04-30 17:47:44
    本文涉及到C++对象的内存布局知识,若无该方面基础建议先阅读haoel(陈皓)专栏的C++对象内存布局的博客: http://blog.csdn.net/haoel/article/details/3081328
  • C++虚函数实现机制

    千次阅读 2019-03-02 19:58:42
    C++虚函数实现机制 C++程序的内存格局通常分为四个区:全局数据区,代码区,栈区,和堆区(即自由存储区)。 全局数据区存放全局变量,静态数据和常量;所有类成员函数和非成员函数代码存放在代码区;为运行函数而...
  • C++面试题之虚函数(表)实现机制

    千次阅读 多人点赞 2018-04-27 20:01:25
    前言大家都应该知道C++的精髓是虚函数吧? 虚函数带来的好处就是: 可以定义一个基类的指针, 其指向一个继承类, 当通过基类的指针去调用函数时, 可以在运行时决定该调用基类的函数还是继承类的函数. 虚函数是实现多态...
  • 今天我们来谈一谈面试 C++ 工程师时经常被谈到的一个问题:为什么析构函数必须是虚函数?为什么默认的析构函数不是虚函数? 首先,我们看一下百度百科对虚函数是怎么定义的: 在某基类声明为 virtual并在一个或多个...
  • qt虚函数

    万次阅读 2015-12-31 14:56:14
     虚函数是C++用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。假设我们有下面的类层次: class A { public:  virtual void foo() { cout }; class B
  • 最近再读一遍C++Primer Plus,在虚函数描述(中文版P501)有以下描述:  如果数组成员指向的是Brass对象,则调用Brass::ViewAcct();如果指向的的是BrassPlus对象,则调用BrasPlus::ViewAcct()。如果Brass::...
  • C++ 虚函数实现机制

    千次阅读 2019-02-07 19:40:18
    转 C++面试题之虚函数(表)实现机制 前言大家都应该知道C++的精髓是虚函数吧? 虚函数带来的好处就是: 可以定义一个基类的指针, 其指向一个继承类, 当通过基类的指针去调用函数时, 可以在运行时决定该调用基类的...
  • c++为了兼容c保留了struct类型,但是c++的struct和c有明显的区别,c++的struct可以继承,可以有成员函数,但是在c却不行,在c++struc... 首先没有虚函数的类其内存布局和c的struct没有什么根本的区别,其实...
  • 这篇文章主要介绍了c++中虚函数和纯虚函数的作用与区别,需要的朋友可以参考下 虚函数为了重载和多态的需要,在基类是有定义的,即便定义是空,所以子类可以重写也可以不写基类的此函数! 纯虚函数在...
  • c++、虚函数、强制类型转换、多态

    千次阅读 2018-08-13 20:52:58
    多态:如果子类的成员函数对父类的成员进行了覆盖,当一个指向中子类的父类指针或引用了子类的父类引用,当使用它调用虚函数,然后根据实际的调用对象调用子类的覆盖函数,而不是父类虚函数,这种语法...
  • 最近被问到一个关于多继承虚函数表的问题,当时回答是可能存在多个虚函数表,应该是顺序排列的,但具体怎么排列还是有些疑惑的,回答的时候到有点儿心虚。之后查了资料,做了简单的实验,可以确定的是对于继承了多个...
  • 【c++】虚函数描述符override

    千次阅读 2014-05-10 12:45:25
    C++11为了帮助程序员写继承结构复杂的类型,引入了虚函数描述符override 重载虚函数 就加上关键字override 这样编译器可以辅助检查是不是正确重载,如果没加这个关键字 也没什么严重的error 只是少了编译器检查的...
  • C++ 虚函数和函数重载

    千次阅读 2014-07-23 19:34:59
    函数重载 如何讲函数重载: What——函数重载是什么? why——为什么要用函数重载,没有函数重载会怎样? how——举例说明怎么使用函数重载 ***********************************************************...
  • C++虚函数表,虚表指针,内存分布

    万次阅读 多人点赞 2018-07-10 20:16:47
    虚函数表和内存分布那一块转载自:https://blog.twofei.com/496/虚函数效率转载自:https://www.cnblogs.com/rollenholt/articles/2023364.html前言大家都应该知道C++的精髓是虚函数吧? 虚函数带来的好处就是: 可以...
  • //虚函数是怎么实现的 //虚函数的说明 //虚析构函数 //构造函数和析构函数的调用顺序 //纯虚函数与抽象类 //为什么要引用抽象基类和纯虚函数 //虚函数与纯虚函数的区别 什么是多态 多态性是指发出的消息被...
  • 虚函数和纯虚函数及虚函数

    千次阅读 2015-05-08 10:42:35
    虚函数为了重载和多态的需要,在基类是有定义的,即便定义是空,所以子类可以重写也可以不写基类的此函数! 纯虚函数在基类是没有定义的,必须在子类加以实现,很像java的接口函数! 虚函数 ...
  • c++构造函数,虚函数相关问题

    千次阅读 2019-05-03 14:33:05
    1.c++内存分布的特点: 全局区(静态区):全局变量,static修饰的局部变量和全局变量,编译时期已经确定 常量区:const修饰的全局变量(const...一般的成员变量和虚函数指针一般是分配在堆或栈。 一般const...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 64,796
精华内容 25,918
关键字:

关于虚函数的描述中