【转】c++重载、覆盖、隐藏——理不清的区别

原文网址:http://blog.sina.com.cn/s/blog_492d601f0100jqqm.html

再次把林锐博士的《高质量c++编程指南》翻出来看的时候,再一次的觉得这是一本难得的好书。实践派写的东西跟理论派和翻译派写的书有着本质的 区别,每次读这本书都觉得为什么自己读了这么多遍,还是会犯一些上面讲的小错误,编代码有时候莫名其妙又会把自己转糊涂了。这本书浅显易懂,而且提到了编 程过程中应该注意的很多细节,里面展开来讲的细节又偏偏是我觉得最为薄弱的环节,如果大家想学或者正在学习c++。建议大家用心的把这本并不长也不高深的 书好好的读几遍。

   关于c++语言中重载、覆盖、隐藏这三个容易混淆的概念,我依然以林博士书中的例子发散开来回忆一下。先列举这三个概念必须满足的条件:
 函数重载的特征:
  1)处在相同的空间中(即相同的作用范围内,比如一个类中)。
  2)函数名相同。
  3)参数不同(相同位置参数的类型不同,或者参数的个数不同)。
  4)virtual关键字可有可无。
 
 覆盖的特征:
  1)不同的范围(例如分别位于基类与派生类)。
  2)函数名相同。
  3)参数相同(参数个数与类型均相同)。
  4)基类函数必须有virtual关键字(派生类可有可无,因为基类函数被声明为虚函数,派生类同名函数一定也是虚函数)。
 
 隐藏的特征:
  1)不同的范围(例如分别位于基类与派生类)。
  2)函数名相同。
  3)参数可相同也可不同(注意此处还有两种情况)。
  4)virtual关键字可有可无。
 注意:隐藏与覆盖的区别就在于如下两条:
  1)如果派生类的函数与基类的函数同名,但是参数不同(不可能构成覆盖)。此时无论有无virual关键字,基类的函数将被隐藏。(不可能构成重载,因为重载必须在同一个类中)
  2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virual关键字(如果有virual关键字,则满足覆盖的条件)。此时基类的函数被隐藏。
 
隐藏:
下面是林博士原书中的例子:
 #include <iostream.h>
 class Base
 {
     public:
              void g(float x){cout << "Base::g(float)" << x <<endl;}       //注意c++中将在类声明中定义了实现的函数自动默认为内联函数
              void h(float x){cout << "Base::h(float)" << x <<endl;}
 }
 class Derived : public Base
 {
     public:
              void g(int x){cout << "Derived::g(int)" << x <<endl;}      
              void h(float x){cout << "Derived::h(float)" << x <<endl;}
 }
 void main(void)
 {
          Derived d
          Base* pb = &d;
          Derived* pd = &d;
          //behavior depends on type of pointer
          pb->g(3.14f);     //Base::g(float) 3.14;
          pd->g(3.14f);     //Derived::g(int) 3;
 
          pb->h(3.14f);     //Base::h(float) 3.14;
          pd->h(3.14f);     //Derived::h(float) 3.14;
 }
  参照隐藏规则,派生类的成员函数隐藏了基类的同名函数。所谓隐藏就是指派生类类型的对象、引用、指针访问基类和派生类都有的同名函数的时候,访问的是派生 类的函数,隐藏了基类同名函数。派生类既然自动继承了基类的成员,那么基类成员就可以被派生类直接访问,那么为什么访问的是派生类的成员函数呢?所以隐藏 规则实际上就是默认的c++名字解析过程。
  在继承机制下,派生类的类域被嵌套在基类的类域中,派生类的名字解析过程如下:
  1)首先在派生类中查找改名字。
  2)如果第一步未查找到,及派生类的类域对改名字无法进行解析,则编译器在外围基类类域查找改名字的定义。
  所以准确来说,当派生类和基类有同一名字的成员时,派生类成员是隐藏了对基类成员的直接访问。那么如果访问到基类同名成员呢?加上类作用域限定例如:Base::g(float)就可以访问了。
 
覆盖:
   覆盖规则造成的调用现象,其实就是类的虚函数实现原理生成的。为了达到动态绑定(后期绑定)的目的,C++编译器通过某个表格(一般称为vtable), 在执行期"间接"调用实际上欲绑定的函数。每一个内含虚函数的类,C++编译器都会为它做出一个虚函数表,表中的每一个元素都指向一个虚函数的地址。 
 
   举个例子:
    class base{
    public:
        func();
        virtual vfunc1();
        virtual vfunc2();
        virtual vfunc3();
    private:
        int _data1;
        int _data2;
    };
    base对象实例在内存中占据的空间是这样的:
     base对象实例          vtable
--------------------------------------------------------------------------
         vptr ---------> (*vfunc1)() -----------> base::vfunc1();
        _data1           (*vfunc2)() -----------> base::vfunc2();
        _data2           (*vfunc3)() -----------> base::vfunc3();
--------------------------------------------------------------------------
 
    当派生类改写了虚函数时,虚函数表相应的被修改了:
    class derived: public base{
    public:
        vfunc2();
    };
    derived对象实例              vtable
--------------------------------------------------------------------------
         vptr  ---------> (*vfunc1)() -----------> base::vfunc1()      
        _data1;           (*vfunc2)() -----------> derived::vfunc2()     ****注意,这里变了!!!***
        _data2;           (*vfunc3)() -----------> base::vfunc3()
--------------------------------------------------------------------------
 
    所以当你写下如下程序的时候:
    void main(void)
    {
        Derived d;
        Base *pb = &d;
        pb->vfunc2(); // Derived::vfunc2(void)
    } 
    就不难理解为何pb->vfunc2()调用的是derived::vfunc2()了,因为pb实际上指向派生类derived的实例,而派生类中的虚函数表已经被修改了。
 
    总结:简单来说,隐藏规则就是C++的名字解析过程,自里向外解析,这个好理解;而覆盖规则其实就是C++虚函数表的实现原理。

posted on 2015-03-10 10:45  wi100sh  阅读(217)  评论(0编辑  收藏  举报

导航