美文网首页
深入了解C++虚函数

深入了解C++虚函数

作者: Sky_Mao | 来源:发表于2018-12-05 22:35 被阅读0次
    一、认识虚函数

    虚函数(Virtual Function):在基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数。
    作用:
    C++ “虚函数”的存在是为了实现面向对象中的“多态”,即父类类别的指针(或者引用)指向其子类的实例,然后通过父类的指针(或者引用)调用实际子类的成员函数。通过动态赋值,实现调用不同的子类的成员函数(动态绑定)。正是因为这种机制,把析构函数声明为“虚函数”可以防止内存泄露。

    简单的示例:

    class bass
    {
    public:
        bass() {};
        virtual void Func() { std::cout << "bass func" << std::endl; };
    };
    
    class derived : public bass
    {
    public:
        derived() {};
        virtual void Func() { std::cout << "derived func" << std::endl; };
    };
    
    int main()
    {
        bass * pB = new derived();
        pB->Func();
        return 0;
    }
    

    输出结果:


    二、虚函数表

    虚函数的调用是通过虚函数表(vitrual tables)和指向这张虚函数表的指针(virtual table pointers)来确定调用的是哪一个对象的函数,此二者通常被简写为vtbls和vptrs。
    程序中每一个class凡声明(或继承)虚函数者,都有自己的一个vtbls,而其中的条目就是该class的各个虚函数实现体的指针。

    凡是声明有虚函数的class,其对象都含有一个隐藏的数据成员,用来指向该class的vtbl。这个隐藏的数据成员就是vptr,effective C++中的描述是:这个vptr被编译器加入对象的内某个唯有编译器才知道的位置,网上搜的资料说这个数据成员会被放在对象内存布局的第一个位置。具体的可以试验一下!
    先假设放在第一个位置。
    例如这样一个类:

    class c1
    {
    public:
        c1() {};
        virtual void f1() {};
        virtual void f2() {};
        virtual void f3() {};
    };
    

    c1的vtbl看起来应该是这样的:

    &c1


    下面试验一下,vptr是否在对象内存布局的第一个位置。
    在f1函数打印一条信息:

    virtual void f1() { std::cout << "test func pos"; };
    

    在main函数内添加这些代码:

    int main()
    {
        c1 * pc = new c1();
        typedef void (*Func)(void);
        Func  pFun = (Func)*((int*)*(int*)(pc) + 0);
        pFun();
        return 0;
    }
    
     (Func)*((int*)*(int*)(pc) + 0); 
    

    这行代码可能理解起来比较吃力,我有的时候看起来也比较费劲,这一堆都是啥 玩意儿啊?
    我们可以拆开来理解
    首先把c1指针类对象强转为int*

    int * nPc = (int*)(pc);
    

    然后把类对象的首地址的所指物给强转成int*,首地址存放的应该是虚表指针 vtbls

    int * vtabls = (int*)*(nPc);
    

    最后把虚表指针第1个虚函数(从0开始的),转成Func

    Func pFun = (Func)*(vtabls + 0);
    

    这样的话是不是好理解多了。
    我使用的环境是VS2017

    输出结果:



    从输出结果上来看,在vs里指向虚函数表的指针,是存放在对象内存布局的第一个位置,其他编译器由于没有测试不确定是否存放在第一个位置

    下边看一下发生继承关系以后,虚函数表的状态
    假如有一个类(单继承无覆盖的情况):

    class c2 : public c1
    {
    public:
        c2() {};
        virtual void f4() { std::cout << "c2::f4()" << std::endl; };
        virtual void f5() {};
    };
    

    那么c2的虚函数表看起来应该是这样的:

    &c2


    在写一段代码来测试一下:

    c1 * pc = new c2();
    typedef void (*Func)(void);
    Func pFun = (Func)*((int*)*(int*)(pc) + 0);
    Func pFun2 = (Func)*((int*)*(int*)(pc)+3);
    pFun();
    pFun2();
    

    输出结果:



    从输出结果上可以看出:
    1、虚函数按照其声明顺序放于表中。
    2、父类的虚函数在子类的虚函数前面。
    如果c2继承c1后重写基类的方法c1:f1(),那么根据之前的测试,他的虚函数表应该是这样的:

    &c2


    多重继承情况下,子类的虚函数表:
    继承关系如下:



    虚函数表应该是这样的:

    &c4


    有兴趣的同学可以写一段测试代码进行验证一下,我这里就不写了。

    三、虚函数的成本

    从上面的分析可以看出:
    1、你必须为每一个拥有虚函数的class耗费一个vtbl空间,其大小视虚函数的个数(包括继承而来的)而定。
    2、你必须在每一个拥有虚函数的对象内付出“一个额外指针”的代价,包括继承而来的。
    3、虚函数不应该inlined,因为inline意味“在编译期,将调用端的调用动作被调用函数的函数本体取代”,而virtual则意味着“等待,直到运行时期才知道哪个函数被调用”,当编译器对某个调用动作,却无法知道哪个函数该被调用时,你就可以了解它们没有能力将该函数调用加以“inlining”了,事实上等于放弃了inlining。(如果虚函数通过对象调用,倒是可以inlined,但是大部分虚函数调用动作是通过对象的指针或references完成的,此类行为无法被inlined。由于此等调用行为是常态,所以虚函数事实上等于无法被inlined)-----摘自more effective C++。

    四、安全性

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

    class bass
    {
    public:
        bass() {};
    private:
        virtual void fc() { std::cout << "bass::fc()" << std::endl; };
    };
    int main()
    {
        typedef void(*Func)(void);
        bass *pBass = new bass();
        Func pF = (Func)*((int*)*(int*)(pBass));
        pF();
        return 0;
    }
    

    输出结果:


    五、将构造函数与非成员函数虚化(来自more effective C++ 条款25)

    第一次面对虚构造函数的时候,似乎不觉得有什么道理可言,并且还有些荒谬,但它们很有用。
    比如我有一个函数需要根据获得的输入,来构造不同类型的对象的时候。
    假设有一个链表,存储图形或者文字信息:

    class common
    {
    public:
    };
    class text : public common
    {
    public:
    };
    class graphic : public common
    {
    public:
    };
    std::list<common*> oCommonInfo;
    template<class T>
    common* readCommonInfo(T inPut)
    {
        //根据输入的信息来构造text还是graphic
    }
    

    oCommonInfo.push_back(readCommonInfo(inPut));
    思考一下,readCommonInfo做了一些什么事,它产生一个新对象,或许是text,也或许是graphic,
    视输入的数据而定,由于它产生了新对象,所以行为仿若构造函数,但它能够产生不同类型的对象,
    所以我们称它为一个virtual construction。所谓的virtual construction是某种函数,视其获得的输入,可产生不同类型的对象。

    还有一种比较特殊的virtual construction,比如virtual copy construction,常见的是类的clone
    比如:

    class a
    {
    public:
        a() {};
        virtual a* clone() const = 0;
    };
    class b : public a
    {
    public:
        b() {};
        virtual b* clone() const {};
    };
    class c : public a
    {
    public:
        c() {};
        virtual c* clone() const {};
    };
    

    虚函数在重写的时候,返回类型、函数名称、参数个数、参数类型必须相同,但是当基类虚函数返回基类指针,派生类虚函数返回派生类指针,是允许的。
    a *pa = new b或者c;
    list.push_back(a.clone());

    就像construction无法被真正虚化一样,非成员函数也是一样。
    不过可以将非成员函数的行为虚化,
    可以写一个虚函数做实际工作,在写一个什么也不做的非虚函数,只负责调用虚函数。
    当然为了避免此巧妙安排蒙受函数调用带来的成本,可以将非虚函数inline化。

    相关文章

      网友评论

          本文标题:深入了解C++虚函数

          本文链接:https://www.haomeiwen.com/subject/wmjgcqtx.html