那谁的技术博客

感兴趣领域:高性能服务器编程,存储,算法,Linux内核
随笔 - 210, 文章 - 0, 评论 - 1183, 引用 - 0
数据加载中……

探索C++的秘密之二:重载,覆盖,和隐藏

这几个概念都有一个共同点:函数名称相同,所以不免让人混淆,大致的区别如下:

重载(overload):
必须在一个域中,函数名称相同但是函数参数不同,重载的作用就是同一个函数有不同的行为,因此不是在一个域中的函数是无法构成重载的,这个是重载的重要特征

覆盖(override):
覆盖指的是派生类的虚拟函数覆盖了基类的同名且参数相同的函数,既然是和虚拟函数挂钩,说明了这个是一个多态支持的特性,所谓的覆盖指的是用基类对象的指针或者引用时访问虚拟函数的时候会根据实际的类型决定所调用的函数,因此此时派生类的成员函数可以"覆盖"掉基类的成员函数.
注意唯有同名且参数相同还有带有virtual关键字并且分别在派生类和基类的函数才能构成虚拟函数,这个也是派生类的重要特征.
而且,由于是和多态挂钩的,所以只有在使用类对象指针或者引用的时候才能使用上.
总之一句话:覆盖函数都是虚函数,反之不然~~

隐藏(hide):
指的是派生类的成员函数隐藏了基类函数的成员函数.隐藏一词可以这么理解:在调用一个类的成员函数的时候,编译器会沿着类的继承链逐级的向上查找函数的定义,如果找到了那么就停止查找了,所以如果一个派生类和一个基类都有同一个同名(暂且不论参数是否相同)的函数,而编译器最终选择了在派生类中的函数,那么我们就说这个派生类的成员函数"隐藏"了基类的成员函数,也就是说它阻止了编译器继续向上查找函数的定义....
回到隐藏的定义中,前面已经说了有virtual关键字并且分别位于派生类和基类的同名,同参数函数构成覆盖的关系,因此隐藏的关系只有如下的可能:
1)必须分别位于派生类和基类中
2)必须同名
3)参数不同的时候本身已经不构成覆盖关系了,所以此时是否是virtual函数已经不重要了
  当参数相同的时候就要看时候有virtual关键字了,有的话就是覆盖关系,没有的时候就是隐藏关系了

上面的解说大体把三者的区别给说清楚了,但是还有一些疑惑的地方,以下以代码例子说明.

很多人分辨不清隐藏和覆盖的区别,因为他们都是发生在基类和派生类之中的.但是它们之间最为重要的区别就是:
覆盖的函数是多态的,是存在于vtbl之中的函数才能构成"覆盖"的关系,而隐藏的函数都是一般的函数,不支持多态,在编译阶段就已经确定下来了.


class  Base
{
public :
virtual   void  f( float  x) {cout << " Base::f(folat) " << x << endl;}
        
void  g( float  x) {cout << " Base::g(float) " << x << endl;}    
}
;

class  Derived: public  Base
{
public  :
    
virtual   void  f( float  x) {cout << " Derived::f(float) " << x << endl;}
            
void  g( int  x) {cout << " Deriver::g(int) " << x << endl;}
}
;

int  main()
{
    Derived d;
    Base 
* pb =& d;
    Derived 
* pd =& d;
    pb
-> f( 3.14f );
    pd
-> f( 3.14f );
    pb
-> g( 3.14f );    // 输出结果:Base::g(float)3.14
    pd -> g( 3.14f );    // 输出结果:Dervied::g(int)3
     return   0 ;
}



在调用f函数的时候,派生类Derived的f函数覆盖了基类Base的f函数,而派生类Derived的g函数隐藏了基类Base的g函数.
为什么?理由很简单,f函数是virtual函数,但是g函数不是.我们可以把Base类和Derived类看成这样的一个struct:
struct Base
{
    
void          (*g)(float);  // Base类型的函数指针,不可变
    struct VTABLE  *__vptr;     // 虚拟函数指针数组,可变
}
;

void __Baseg(float)
{
    cout
<<"Base::g(folat)"<<x<<endl;
}


struct Derived
{
    
void          (*g)(float);  // Derived类型的函数指针,不可变
    struct VTABLE  *__vptr;     // 虚拟函数指针数组,可变
}
;

void __Derivedg(float)
{
    cout
<<"Deriver::g(int)"<<x<<endl;
}


struct VTABLE
{
    
void          (*f)(float);  // 函数指针
}
;

void __Basef(float)
{
    cout
<<"Base::f(folat)"<<x<<endl;
}


void __Derivedf(float)
{
    cout
<<"Deriver::f(int)"<<x<<endl;
}



在程序编译的时候,函数指针f就已经是确定的了,但是__vptr根据不同的而有分别,而这个变化是运行期动态决定的.
也就是说:f的地址不可变,__vptr可变.
回到上面的例子中,Base *pb=&d;的时候只是用Derived类对象d的__vptr修改了Base类pb的__vptr指针,但是当Base类成员建立的
时候f函数指针就是不能改变的.

当函数被声明为virtual的时候,就激活了多态机制,程序在运行的时候会根据类型的实际类型到VTABLE中查找函数指针,因此对函数g的调用就是这样子的:
pb->__vptr->g();
而对f的调用就是一般的类成员函数指针的调用了:pb->f(),因为这个类型在程序编译的时候已经确认了,所以在程序运行的时候是不能发生改变的.

综上,可以把
Derived d;
Base *pb=&d;
的过程分解为:
d.g = __Derivedg;
d.__vptr->f = __Derivedf;
pb->g = __Baseg;            // 这里根据指针的真正类型确定函数指针
pb->__vptr = d.__vptr;      // 这里只是简单的指针赋值,因此访问到的就是Derived的函数了
最后在调用:
pb->f(3.14f);
pb->g(3.14f);
实际上是:
pb->__vptr->__Derivedf(3.14f);
__Baseg(3.14f);
这么写就明白最后在调用的时候为什么会用那样的结果了,可以看出多了一个__vptr这个间接层实现了所谓的"动态绑定".

最后,需要说明的一点是:实际上在c++中,非static和非virtual的函数指针并不会在一个class中保存它的函数指针,上面把函数g的指针写在struct里面只是为了方便说明这样的问题:在编译阶段这个函数就已经是确定的不可改变的了.特此说明一下.

posted on 2006-03-24 20:39 那谁 阅读(3118) 评论(6)  编辑 收藏 引用 所属分类: C\C++

评论

# re: 探索C++的秘密之二:重载,覆盖,和隐藏   回复  更多评论   

重载,重写,和隐藏会不会更好理解一些。
2006-03-24 21:12 | 沐枫

# re: 探索C++的秘密之二:重载,覆盖,和隐藏   回复  更多评论   

我看到很多地方都是写的覆盖,这个讲究的地方在哪里呢?兄台给我讲一下,谢啦~~
2006-03-24 22:08 | 创系

# re: 探索C++的秘密之二:重载,覆盖,和隐藏   回复  更多评论   

没什么讲究啦,就是看什么词容易让人理解,就是什么词好。
2006-03-27 13:27 | 沐枫

# re: 探索C++的秘密之二:重载,覆盖,和隐藏   回复  更多评论   

写的很好! 谢谢!
2008-06-11 16:51 | anonymous

# re: 探索C++的秘密之二:重载,覆盖,和隐藏 [未登录]  回复  更多评论   

里面有很多不一致的地方哦
2010-07-29 07:33 | haha

# re: 探索C++的秘密之二:重载,覆盖,和隐藏 [未登录]  回复  更多评论   

谢谢分享,我还一直没弄明白,这下明白了。
2011-12-20 19:36 | Cloud

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理