现在的位置: 首页 > 综合 > 正文

深入理解成员函数的重载、隐藏、覆盖

2013年09月15日 ⁄ 综合 ⁄ 共 4137字 ⁄ 字号 评论关闭

  重载的特征:
  1、处在相同的空间中,即相同的范围内。
  2、函数名相同。
  3、参数不同,即参数个数不同,或相同位置的参数类型不同。
  4、virtual关键字对是否够成重载无任何影响。
  每个类维护一个自己的名字空间,即类域,所以派生类跟基类处于不同的空间之中,因些,虽然派生类自动继承了基类的成员变量及成员函数,但基类的函数跟派生类的函数不可能直接够成函数重载,因为它们处于两个不同的域。
  隐藏规则:
  1、派生类的函数跟基类的函数同名,但是参数不同,此时,不论有没有virtual关键字,基类函数将被隐藏。
  2、派生类的函数跟基类的函数同名,且参数也样,但基类没有virtual关键字,此时基类函数也将被隐藏。
  下面举个例子可能好理解一些:
  #include <iostream>
   class Base
   {
   public:
   void g(float x){ cout << "Base::g(float) " << x << endl; }
   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;
   // Bad : behavior depends on type of the pointer
   pb->g(3.14f); // Base::g(float) 3.14
   pd->g(3.14f); // Derived::g(int) 3 (surprise!)
   // Bad : behavior depends on type of the pointer
   pb->h(3.14f); // Base::h(float) 3.14 (surprise!)
   pd->h(3.14f); // Derived::h(float) 3.14
   }
  参照隐藏规则,可以知道,派生类的成员函数都隐藏了基类的同名函数。
  到这里可以讲一下对“隐藏”这个动词的理解了。所谓的隐藏,指的是派生类类型的对象、指针、引用访问基类和派生类都有的同名函数时,访问的是派生类的函数,即隐藏了基类的同名函数。这句话好像有点废,派生类访问的当然是派生的东西(即派生类的变量、函数)。可是再深入想一想,派生类自动继承了基类的成员,基类成员可以像派生类成员那样被直接访问,那为什么会当然访问派生类的成员?
  所以,隐藏规则的底层原因其实是C++的名字解析过程。
  在继承机制下,派生类的类域被嵌套在基类的类域中。派生类的名字解析过程如下:
  1、首先在派生类类域中查找该名字。
  2、如果第一步中没有成功查找到该名字,即在派生类的类域中无法对该名字进行解析,则编译器在外围基类类域对查找该名字的定义。
  所以,准确来说,当基类跟派生类共享一个名字时,派生类成员是“隐藏了对基类成员的直接访问”!只要加上作用域限定,还是可以访问到基类成员的。
  上面的例子中,如果派生类Derived没有自己的成员函数void g( int x ){},那么main()函数中pd->g(3.14f)访问的将是基类的成员函数,其输出将为Base::g(float) 3.14。

 

对覆盖规则的定义如下:

  (1)不同的范围(分别位于派生类与基类);
  (2)函数名字相同;
  (3)参数相同;
  (4)基类函数必须有virtual 关键字.

  这样来理解重载、隐藏、覆盖确实是有点令人困惑.其实这个(覆盖)定义就是类的虚函数的定义.即,基类中,必须有virtual关键字,派生类函数的原型必须相同.
  所谓的覆盖规则造成的调用现象,其实就是类的虚函数实现原理生成的.为了达到动态绑定(后期绑定)的目的,C++编译器通过某个表格(一般称为vtable),在执行期"间接"调用实际上欲绑定的函数.每一个内含虚函数的类,C++编译器都会为它做出一个虚函数表,表中的每一个元素都指向一个虚函数的地址.

 分析:

先看看类的成员函数:

类的成员函数的代码定义在PE文件的代码区,所以从程序加载时,就已经分配好了内存用于存放这些代码;代码运行时所需要的内存,比如栈、堆等等,则是代码运行时才分配的;对于某个类的所有对象来说,类成员函数只在内存中有一份拷贝,所有的对象都共享同一份成员函数的代码。同一个类的不同的对象之间的差异仅仅是通过成员变量来体现的。

 

    1、(一般的静态绑定(前期绑定)在编译器编译时,就已经固定了所属对象,如基类的对象和指针只能调用基类的成员,不能够调用派生类成员。虚函数并没有在编译时与对象绑定变成固定地址,而是在运行时由Vptr指向函数地址,(Vptr是编译器为每个类加的一个成员,每个包含虚函数的类都有,用来指向虚函数地址),当指向基类对象时调用基类Vptr,指向派生类对象时调用派生类Vptr,因为Vptr占用内存,当指向不同的对象时,Vptr也不同。)

    2、(当指向基类的指针指向派生类对象调用虚函数时,首先指针判断指向的对象地址内容,根据地址不同,Vptr也不同,虚函数也不同。当指向非虚函数时,非虚函数的地址已经在编译时转换为由基类调用的固定的函数地址了,它并没有存在于对象内存中(但Vptr存在于对象内存中,因为它是类的数据成员),跟所指对象以及对象内存没有关系,只跟所调用函数的对象或指针类型有关系,即指针类型是基类,不管指针指向哪个派生类,都是调用基类的非虚函数)

    3、(也就是说非虚函数编译时就确定了它属于哪个类,就应该被哪个类调用。而虚函数编译时只是根据所属对象(不同类有不同的Vptr)来确定它属于哪个类,执行时才能确定具体调用哪个函数的地址(因为所指地址不同))

    4、其实就是成员函数用this指针与调用它的对象绑定,用this直接调用成员函数。虚函数用this指向的数据成员VPTR调用虚函数。当调用虚函数时,先找Vptr,Vptr存放于类的内存地址中,所以要先找基类所指的地址是哪个内存地址,来确定是哪个Vptr以及Vptr指向的虚函数,唉,晕了半天这句才是关键精髓)

 

 

    举个例子:
    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();
--------------------------------------------------------------------------

    当派生类改写了虚函数时,虚函数表相应的被修改了:(这里我理解为其实还是名字解析过程,派生类里定义了和基类相同的函数,所以指向派生类对象的基类指针,调用相同的函数时,先从指向派生类对象内存中找到Vptr所指的虚函数表,然后找相应的虚函数地址,如果没有则由于继承关系会去基类的虚函数表里找相应的函数)
    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++虚函数表的实现原理.这样就可以比较容易的区分这两个知识点,而不用老是背隐藏规则跟覆盖规则的细微区别.

转自【http://www.builder.com.cn/2008/0323/779917.shtml

抱歉!评论已关闭.