设为首页 加入收藏

TOP

谈VC++对象模型(四十六)
2012-11-02 08:51:57 】 浏览:25212
Tags:对象 模型

    正如你猜想的,当继承关系中存在虚基类时,强制转化的开销会比较大。具体说来,和访问虚基类成员变量的开销相当。
   
    I* pi;
   
    (G*)pi; // (G*)pi;
   
    (H*)pi; // (H*)(pi pi + dIH : 0);
   
    (C*)pi; // (C*)(pi (pi+dIGvbptr + (*(pi+dIGvbptr)) ) : 0);
   
    译者注:pi是指向I对象的指针,G,H是I的基类,C是G,H的虚基类。
   
    强制转化pi为G*时,由于G*和I*的地址相同,不需要计算;
   
    强制转化pi为H*时,只需要考虑一个常量偏移;
   
    强制转化pi为C*时,所作的计算和访问虚基类成员变量的开销相同,首先得到G的虚基类表指针,再从虚基类表的第二项中取出G到虚基类C的偏移量,最后根据pi、虚基类表偏移和虚基类C与虚基类表指针之间的偏移计算出C*.
   
    一般说来,当从派生类中访问虚基类成员时,应该先强制转化派生类指针为虚基类指针,然后一直使用虚基类指针来访问虚基类成员变量。这样做,可以避免每次都要计算虚基类地址的开销。见下例。
   
    /* before: */             … pi->c1 … pi->c1 …
   
    /* faster: */ C* pc = pi; … pc->c1 … pc->c1 …
   
    译者注:前者一直使用派生类指针pi,故每次访问c1都有计算虚基类地址的较大开销;后者先将pi转化为虚基类指针pc,故后续调用可以省去计算虚基类地址的开销。
   
    5 成员函数
   
    一个C++(www.cppentry.com)成员函数只是类范围内的又一个成员。X类每一个非静态的成员函数都会接受一个特殊的隐藏参数--this指针,类型为X* const.该指针在后台初始化为指向成员函数工作于其上的对象。同样,在成员函数体内,成员变量的访问是通过在后台计算与this指针的偏移来进行。
   
    struct P {
   
    int p1;
   
    void pf(); // new
   
    virtual void pvf(); // new
   
    };
   
    P有一个非虚成员函数pf(),以及一个虚成员函数pvf()。很明显,虚成员函数造成对象实例占用更多内存空间,因为虚成员函数需要虚函数表指针。这一点以后还会谈到。这里要特别指出的是,声明非虚成员函数不会造成任何对象实例的内存开销。现在,考虑P::pf()的定义。
   
    void P::pf() { // void P::pf([P *const this])
   
    ++p1;   // ++(this->p1);
   
    }
   
    这里P:pf()接受了一个隐藏的this指针参数,对于每个成员函数调用,编译器都会自动加上这个参数。同时,注意成员变量访问也许比看起来要代价高昂一些,因为成员变量访问通过this指针进行,在有的继承层次下,this指针需要调整,所以访问的开销可能会比较大。然而,从另一方面来说,编译器通常会把this指针缓存到寄存器中,所以,成员变量访问的代价不会比访问局部变量的效率更差。
   
    译者注:访问局部变量,需要到SP寄存器中得到栈指针,再加上局部变量与栈顶的偏移。在没有虚基类的情况下,如果编译器把this指针缓存到了寄存器中,访问成员变量的过程将与访问局部变量的开销相似。
   
    5.1 覆盖成员函数
   
    和成员变量一样,成员函数也会被继承。与成员变量不同的是,通过在派生类中重新定义基类函数,一个派生类可以覆盖,或者说替换掉基类的函数定义。覆盖是静态(根据成员函数的静态类型在编译时决定)还是动态(通过对象指针在运行时动态决定),依赖于成员函数是否被声明为"虚函数".
   
    Q从P继承了成员变量和成员函数。Q声明了pf(),覆盖了P::pf()。Q还声明了pvf(),覆盖了P::pvf()虚函数。Q还声明了新的非虚成员函数qf(),以及新的虚成员函数qvf()。
   
    struct Q : P {
   
    int q1;
   
    void pf();  // overrides P::pf
   
    void qf();  // new
   
    void pvf(); // overrides P::pvf
   
    virtual void qvf(); // new
   
    };
   
    对于非虚的成员函数来说,调用哪个成员函数是在编译时,根据"->"操作符左边指针表达式的类型静态决定的。特别地,即使ppq指向Q的实例,ppq->pf()仍然调用的是P::pf(),因为ppq被声明为"P*".(注意,"->"操作符左边的指针类型决定隐藏的this参数的类型。)
   
    P p; P* pp = &p; Q q; P* ppq = &q; Q* pq = &q;
   
    pp->pf();  // pp->P::pf();  // P::pf(pp);
   
    ppq->pf(); // ppq->P::pf(); // P::pf(ppq);
   
    pq->pf();  // pq->Q::pf();  // Q::pf((P*)pq); (错误!)
   
    pq->qf();  // pq->Q::qf();  // Q::qf(pq);
   
    译者注:标记"错误"处,P*似应为Q*.因为pf非虚函数,而pq的类型为Q*,故应该调用到Q的pf函数上,从而该函数应该要求一个Q* const类型的this指针。
   
    对于虚函数调用来说,调用哪个成员函数在运行时决定。不管"->"操作符左边的指针表达式的类型如何,调用的虚函数都是由指针实际指向的实例类型所决定。比如,尽管ppq的类型是P*,当ppq指向Q的实例时,调用的仍然是Q::pvf()。
   
    pp->pvf();  // pp->P::pvf();  // P::pvf(pp);
   
    ppq->pvf(); // ppq->Q::pvf(); // Q::pvf((Q*)ppq);
   
    pq->pvf();  // pq->Q::pvf();  // Q::pvf((P*)pq); (错误!)
   
    译者注:标记"错误"处,P*似应为Q*.因为pvf是虚函数,pq本来就是Q*,又指向Q的实例,从哪个方面来看都不应该是P*.
   
    为了实现这种机制,引入了隐藏的vfptr成员变量。一个vfptr被加入到类中(如果类中没有的话),该vfptr指向类的虚函数表(vftable)。类中每个虚函数在该类的虚函数表中都占据一项。每项保存一个对于该类适用的虚函数的地址。因此,调用虚函数的过程如下:取得实例的vfptr;通过vfptr得到虚函数表的一项;通过虚函数表该项的函数地址间接调用虚函数。也就是说,在普通函数调用的参数传递、调用、返回指令开销外,虚函数调用还需要额外的开销。
   
    回头再看看P和Q的内存布局,可以发现,VC++(www.cppentry.com)编译器把隐藏的vfptr成员变量放在P和Q实例的开始处。这就使虚函数的调用能够尽量快一些。实际上,VC++(www.cppentry.com)的实现方式是,保证任何有虚函数的类的第一项永远是vfptr.这就可能要求在实例布局时,在基类前插入新的vfptr,或者要求在多重继承时,虽然在右边,然而有vfptr的基类放到左边没有vfptr的基类的前面。
   
    许多C++(www.cppentry.com)的实现会共享或者重用从基类继承来的vfptr.比如,Q并不会有一个额外的vfptr,指向一个专门存放新的虚函数qvf()的虚函数表。Qvf项只是简单地追加到P的虚函数表的末尾。如此一来,单继承的代价就不算高昂。一旦一个实例有vfptr了,它就不需要更多的vfptr.新的派生类可以引入更多的虚函数,这些新的虚函数只是简单地在已存在的,"每类一个"的虚函数表的末尾追加新项。
   

                  

首页 上一页 43 44 45 46 47 下一页 尾页 46/47/47
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇用VC++制作DLL经验 下一篇VC++数据类型

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目