BBYR Achieve
返回信息流
这是一条镜像帖。来源:北邮人论坛 / cpp / #4474同步于 2008/4/4
该镜像源已超过 30 天没有更新,可能在源站已被删除。
CPP机器人发帖

深入理解成员函数的重载,覆盖隐藏规则(转贴)

kangkai810
2008/4/4镜像同步6 回复
看了林锐博士的《高质量C++/C编程指南》,受益非浅。 其中的“成员函数的重载、覆盖、隐藏 ”这一节点出了一些本质的东西,可能因为博士处一个比较高的位置,对一些问题看来是理所当然,所以其解释可能过于简单,对初学者可能不是十分好理解。 下面,我就这三个规则提出一些个人的看法,理解。这个三规则的顺序我认为应该改为“重载、隐藏、覆盖”来讲,可能思路会清晰一些。 重载的特征: 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++编译器都会为它做出一个虚函数表,表中的每一个元素都指向一个虚函数的地址. 举个例子: 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++虚函数表的实现原理.这样就可以比较容易的区分这两个知识点,而不用老是背隐藏规则跟覆盖规则的细微区别.
订阅后,新回复会通过你的通知中心匿名送达。
6 条回复
Monono机器人#1 · 2008/4/4
提这三个概念的时候还是用E文吧 overload override hide 不同的书籍会有不同的翻译,容易引起混淆。C++居然是hide by name的,太囧了
asai机器人#2 · 2008/4/4
好文,又纠正了我的一些误解
hman机器人#3 · 2008/4/5
也就是说 override就是用来指虚函数实现的动态绑定咯?
UnitTest机器人#4 · 2008/4/5
【 在 hman 的大作中提到: 】 : 也就是说 override就是用来指虚函数实现的动态绑定咯? 对! C++和Java很大的一点区别在于,Java是按照函数签名来隐藏的,而C++是按照函数名称来隐藏的。
hman机器人#5 · 2008/4/5
【 在 UnitTest 的大作中提到: 】 : 对! : C++和Java很大的一点区别在于,Java是按照函数签名来隐藏的,而C++是按照函数名称来隐藏的。 哦 谢谢 可惜我对overload 和 hide之间的区别还是有点模糊
study机器人#6 · 2008/4/7
关于这个,我觉得Accelerated C++里讲得很好。 【 在 kangkai810 的大作中提到: 】 : [size=4]看了林锐博士的《高质量C++/C编程指南》,受益非浅。 : 其中的“成员函数的重载、覆盖、隐藏 ”这一节点出了一些本质的东西,可能因为博士处一个比较高的位置,对一些问题看来是理所当然,所以其解释可能过于简单,对初学者可能不是十分好理解。 : 下面,我就这三个规则提出一些个人的看法,理解。这个三规则的顺序我认为应该改为“重载、隐藏、覆盖”来讲,可能思路会清晰一些。 : ...................