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

C++ 函数隐藏(函数名相同才会出现)

2013年08月16日 ⁄ 综合 ⁄ 共 4321字 ⁄ 字号 评论关闭

     看了 林锐 的  《高质量编程指南》8.2.2 令人迷惑的隐藏规则.  (这里的隐藏是指派生类的函数屏蔽了与其同名的基类函数)

 

     这一节写得很好: 1. 把出现隐藏的情况列举出来了.

                     2. 举的例子很贴切, 让人能更好的理解. 

                     3. 对出现隐藏函数情况的理解.

                     4. 提出对应的解决方案. 

  •  如果派生类的函数与基类的函数同名, 但是参数不同. 此时, 不论有无 virtual 关键字, 基类的函数将被隐藏(注意别与重载混淆).
  •  如果派生类的函数与基类的函数同名, 并且参数也相同, 但是基类函数没有 virtual 关键字. 此时, 基类的函数被隐藏(注意别与覆盖混淆). 

      就是以上两种情况导致了函数隐藏的情况出现. 看看书里的例子:

 

#include <iostream>
using namespace std;
class Base
{
    public:
    virtual void f(float x){cout << "Base::f(float) "  << x << endl;}
            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:
    virtual void f(float x){cout << "Derived::f(float) "  << x << endl;}
            void g(int x)  {cout << "Derived::g(int) "  << x << endl;}
            void h(float x){cout << "Derived::h(float) "  << x << endl;}   
};

 

int main()
{
    Derived d;
    Base *pb = &d;
    Derived *pd = &d;
   
    //没出现隐藏的情况
    pb->f(3.14f);             //Derived::f(float) 3.14
    pd->f(3.14f);             //Derived::f(float) 3.14
   
    //出现隐藏的情况 1
    pb->g(3.14f);             //Base::g(float) 3.14
    pd->g(3.14f);             //Derived::g(int)  3       (surprise!)
   
    //出现隐藏的情况 2
    pb->h(3.14f);             //Base::h(float) 3.14         (surprise!)
    pd->h(3.14f);             //Derived::h(float)  3.14

    system("pause");
    return 0;
}

 


  •  如果派生类的函数与基类的函数同名, 并且参数也相同, 但是基类函数没有 virtual 关键字. 此时, 基类的函数被隐藏(注意别与覆盖混淆). 

          //出现隐藏的情况
          pb->h(3.14f);             //Base::h(float) 3.14         (surprise!)
          pd->h(3.14f);             //Derived::h(float)  3.14

 

个人看法:

 

     如果你学过 java 的多态, 对这个结果应该很难接受.

 

     Derived 对象d 被隐式转换为 Base 对象, 那么该 Base 对象跟Derived 对象d 同名的函数被 Derived 对象d 覆盖. 所以两者的执行结果应该是一样的.

 

     但是这里是 C++, 不是 java. 对于C++ 来说, 如果 Base 类的某个函数没有 virtual 关键字, 那该函数跟 Derived 类的同名函数(参数也相同)是没有什么关系的

 

     这个请看下 《C++ Primer》501页下面的"关键概念: 名字查找和继承".

 

     pb 是 Base 类指针,  pb指针 绑定到 Derived 对象 d,  但是由于 Base 类的 h(float) 函数不是虚函数,  无论实际对象是什么类型, 都执行 Base::h(float).  

 

      程序会直接在 Base 类中寻找 h 函数; 如果没有 h 函数, 那就会去其父类中寻找 h 函数 ; 如果还是找不到 h 函数 , 那就会去其父类的上一层类中继续寻找 h 函数 ; 一次类推, 一直到找到方法A 为止; 如果最终都找不到, 你的程序应该是不能通过编译的!(这种查找方式倒是跟 java 一样)

 

      java 的函数是没有 virtual 关键字的, 但是派生类和基类只要函数名和参数相同, 那么该函数就被覆盖了. 如果反过来想, 相对于 C++, 那不是 java 的每个函数都是虚函数吗?  可能C++ 在于效率上考虑, 不想所有的函数都使用动态联编.

 


  •  如果派生类的函数与基类的函数同名, 但是参数不同. 此时, 不论有无 virtual 关键字, 基类的函数将被隐藏(注意别与重载混淆).

          //出现隐藏的情况 1
         pb->g(3.14f);             //Base::g(float) 3.14
         pd->g(3.14f);             //Derived::g(int)  3       (surprise!)

 

个人看法: 

 

  这个其实也不能说是隐藏, 因为 g(float) 和 g(int) 是不同的函数, C++编译后在符号库中的名字分别是 _g_float 和 _g_int.即使他们都有 virtual 关键字, 但是因为是分别存在与派生类和基类中的不同函数, 所以在不存在覆盖的关系(重载更不可能).

 

     pb 是 Base 类指针,  pb指针 绑定到 Derived 对象 d, Base 类根本就没有 g(int) 函数, 所以 pd 指针是总不可能去调用 Derived::g(int) 函数的.

 

      pb->g(3.14f);  程序在 Base 类中找到匹配的函数 Base::g(float) , 然后调用这个函数.

 

      pd->g(3.14f);             //Derived::g(int)  3       (surprise!)

           

      编译先在 Derived 类中查找匹配 g(3.14f) 的函数,  他找到了 g(int) , 并且在 Derived 类中只有一个函数匹配. 即使 g(int) 是 virtual 的, 但pd 指针指向的 Derived 对象 d 的 g(int) 函数跟 Derived 类的 g(int) 函数是一样的, 调用的都是 Derived::g(int) 函数, 所以不存在多态, 也就无需动态联编了. (需要动态联编的条件请看《C++ Primer》15.2.4 "virtual 与其他成员函数" 开头部分, 这里之所以无需动态联编, 是因为不满足动态联编的第二个条件).

 

      即使 Base 类有匹配的函数virtual g(float x), 但是virtual g(float x) 是存放在 Derived 对象 d 的虚函数表(virtual function table, vtbl, plus 13.4.4) 中的, 如果不进行动态联编, 程序不会去 vtbl 中查找对应的函数地址, vtbl 中的函数地址是不会被引用到的, 也就不会被调用了.

   

      所以把 Base 类的 g(float x) 加上 virtual 关键字, 结果不会改变; 再把 Derived 类的 g(int)  加上 virtual 关键字, 结果也是不变的.

 

       如果 Derived 类添加一个函数 virtual void g(float x){cout << "Derived::g(float) "  << x << endl;}, 把 Base 类的 g(float x) 加上 virtual 关键字.

 

       那结果就是

                      pb->g(3.14f);             //Derived::g(float) 3.14
                      pd->g(3.14f);             //Derived::g(float) 3.14  

 

        pb->g(3.14f)

 

        pb 是 Base 类指针,  pb指针 绑定到 Derived 对象 d. 由于 Base 类的 g(float) 函数的 virtual 的, 并且是 Base 类指针调用  g(float) 函数, pb指针绑定的对象 d 的静态类型是 Derived 类, Derived 类的 g(float) 函数也是 virtual 的, 通常只有在运行程序时才能确定对象的动态类型.  所以编译器对 虚函数 g(float) 使用动态联编.

 

         因为 Derived 类提供了虚函数 g(float) 的新定义, 所以在 Derived 对象 d 的虚函数表(vtbl) 中g(float) 函数的地址保存为 Derived::g(float) 函数的地址. pb 指针调用虚函数 g(float) 时候, 程序到 Derived 对象 d 的虚函数表(vtbl) 中查找 g(float) 函数的地址, 然后就执行该地址的函数. 所以 pb->g(3.14f)  执行了 Derived::g(float) 函数. 

 

 


 

 

 

 

      说起来, 子类要重载父类的方法, 还真是麻烦呢, 难道要全部方法copy 过来? 其实也不必要呢, 如果是子类对象能隐式转换父类对象, 但是子类自有的方法, 对于基类对象来说是不存在的, 基类对象当然也不能调用这些方法了. 所以呢, 子类不必要重载父类的方法, 建一个属于自己的方法还更好!  

 

       virtual 关键字, 好像就是在告诉你, 我这个函数可以给派生类同名字同参数的函数覆盖; 纯虚函数更是直接告诉派生类, 你一定要写一个同名字同参数的函数覆盖我,  哈哈!

 

     

重要查考: 《C++ Primer》第480页 "关键概念:C++ 中的多态性".

               《C++ Primer plus》13.4.4 虚拟成员函数和动态联编.

               《C++ Primer plus》第 449 页 "虚函数的工作原理".

               《C++ Primer》15.2.4 virtual 与其他成员函数.

抱歉!评论已关闭.