【原创】Performanced C++ 经验规则 第五条:再谈重载、覆盖和隐藏

第五条:再谈重载、覆盖和隐藏

在C++中,无论在类作用域内还是外,两个(或多个)同名的函数,可能且仅可能是以下三种关系:重载(Overload)、覆盖(Override)和隐藏(Hide),因为同名,区分这些关系则是根据参数是否相同、是否带有const成员函数性质、是否有virtual关键字修饰以及是否在同一作用域来判断。在第四条中,我们曾提到了一些关于重载、覆盖的概念,但只是一带而过,也没有提到隐藏,这一篇我们将详细讨论。

1、首先说的是重载,有一个前提必须要弄清楚的是,如果不在类作用域内进行讨论,两个(或多个)同名函数之间的关系只可能是重载或隐藏,这里先说重载。考虑以下事实:

1 int foo(char c){...}
2 void foo(int x){...}

这两个函数之间的关系是重载(overload),即相同函数名但参数不同,并注意返回类型是否相同并不会对重载产生任何影响

也就是说,如果仅仅是返回类型不相同,而函数名和参数都完全相同的两个函数,不能构成重载,编译器会告知"ambiguous"(二义性)等词以表达其不满:

1 //Can't be compiled!
2 
3 int fooo(char c){...}
4 void fooo(char c){...}
5 
6 char c = 'A';
7 fooo(c); // Which one? ambiguous

在第四条中,已经讲述过,重载是编译期绑定的静态行为,不是真正的多态性,那么,编译器是根据什么来进行静态绑定呢?又是如何确定两个(或多个)函数之间的关系是重载呢?

有以下判定依据:

(1)相同的范围:即作用域,这里指在同一个类中,或同一个名字空间,即C++的函数重载不支持跨越作用域进行(读者可再次对比Java在这问题上的神奇处理,既上次Java给我们提供了未卜先知的动态绑定能力后,Java超一流的意识和大局观再次给Java程序员提供了跨类重载的能力,如有兴趣可详细阅读《Thinking in Java》的相关章节,其实对于学好C++来讲,去学一下Java是很有帮助的,它会告诉你,同样或类似的问题,为什么Java要做这样的改进),这也是区别重载和隐藏的最重要依据。

关于“C++不能支持跨类重载”,稍后笔者会给出代码来例证这一点。

(2)函数名字相同(基本前提)

(3)函数参数不同(基本前提,否则在同一作用域内有两个或多个同名同参数的函数,将产生ambiguous,另外注意,对于成员函数,是否是const成员函数,即函数声明之后是否带有const标志, 可理解为“参数不同“),第(2)和第(3)点统称“函数特征标”不同

(4)virtual关键字可有可无不产生影响(因为第(1)点已经指出,这是在同一个类中)

“相同的范围,特征标不同(当然同名是肯定的),发生重载“

 

2、覆盖(override),真正的多态行为,通过虚函数来实现,所以,编译器根据以下依据来进行判定两个(注意只可能是两个,即使在继承链中,也只是最近两个为一组)函数之间的关系是覆盖:

(1)不同的范围:即使用域,两个函数分别位于基类和派生类中

(2)函数名字相同(基本前提)

(3)函数参数也相同(基本前提),第(2)和第(3)点统称“函数特征标”相同

(4)基类函数必须用virtual关键字修饰

“不同的范围,特征标相同,且基类有virtual声明,发生覆盖“

 

3、隐藏(Hide),即:

(1)如果派生类函数与基类函数同名,但参数不同(特征标不同),此时,无论是否有virtual关键字,基类的所有同名函数都将被隐藏,而不会重载,因为不在同一个类中;

(2)如果派生类函数与基类函数同名,且参数也相同(特征标相同),但基类函数没有用virtual关键字声明,则基类的所有同名函数都将被隐藏,而不会覆盖,因为没有声明为虚函数。

“不同的范围,特征标不同(当然同名是肯定的),发生隐藏”,或"不同的范围,特征标相同,但基类没有virtual声明,发生隐藏“

可见有两种产生隐藏的情况,分别对应不能满足重载和覆盖条件的情况。

另外必须要注意的是,在类外讨论时,也可能发生隐藏,如在名字空间中,如下述代码所示:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 void foo(void) { cout << "global foo()" << endl; }
 5 int foo(int x) { cout << "global foo(int)" << endl; return x; }
 6 namespace a
 7 {
 8         void foo(void) { cout << "a::foo()" << endl; }
 9         void callFoo(void) 
10         { foo();
11            // foo(10); Can't be compiled! }
12 }
13 
14 int main(int argc, char** argv)
15 {
16         foo();
17         a::callFoo();
18         return 0;
19 }    

输出结果:

1 global foo()
2 a::foo()

注意,名字空间a中的foo隐藏了其它作用域(这里是全局作用域)中的所有foo名称,foo(10)不能通过编译,因为全局作用域中的int foo(int)版本也已经被a::foo()隐藏了,除非使用::foo(10)显式进行调用。

这也告诉我们,无论何时,都使用完整名称修饰(作用域解析符调用函数,或指针、对象调用成员函数)是一种好的编程习惯

 好了,上面零零散散说了太多理论的东西,我们需要一段实际的代码,来验证上述所有的结论:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class Other
 5 {
 6         void* p;
 7 };
 8 
 9 class Base
10 {
11 public:
12         int iBase;
13         Base():iBase(10){}
14         virtual void f(int x = 20){ cout << "Base::f()--" << x << endl; }
15         virtual void g(float f) { cout << "Base::g(float)--" << f << endl; }
16         void g(Other& o) { cout << "Base::g(Other&)" << endl; }
17         void g(Other& o) const { cout << "Base::g(Other&) const" << endl;}
18 };
19 
20 class Derived : public Base
21 {
22 public:
23         int iDerived;
24         Derived():iDerived(100){}
25         void f(int x = 200){ cout << "Derived::f()--" << x << endl; }
26         virtual void g(int x) { cout << "Derived::g(int)--" << x << endl; }
27 };
28 
29 int main(int argc, char** argv)
30 {
31         Base* pBase = NULL;
32         Derived* pDerived = NULL;
33         Base b;
34         Derived d;
35         pBase = &b;
36         pDerived = &d;
37         Base* pBD = &d;
38         const Base* pC = &d;
39         const Base* const pCCP = &d;
40         Base* const pCP = &d;
41 
42         int x = 5;
43         Other o;
44         float f = 3.1415926;
45 
46         b.f();
47         pBase->f();
48         d.f();
49         pDerived->f();
50         pBD->f();
51 
52         b.g(x);
53         b.g(o);
54         d.g(x);
55         d.g(f);
56         // Can't be compiled!
57         // d.g(o);
58 
59         pBD->g(x);
60         pBD->g(f);
61         pC->g(o);
62         pCCP->g(o);
63         pCP->g(o);
64 
65         return 0;
66 }

在笔者Ubuntu 12.04 + gcc 4.6.3运行结果: 

 1 Base::f()--20 //b.f(),通过对象调用,无虚特性,静态绑定
 2 Base::f()--20 //基类指针指向基类对象,虽然是动态绑定,但没有使用到覆盖
 3 Derived::f()--200 //d.f,通过对象调用,无虚特性,静态绑定
 4 Derived::f()--200 //子类指针指向子类对象,虽然是动态绑定,但没有使用到覆盖
 5 Derived::f()--20 //基类指针指向子类对象,动态绑定,子类f()覆盖基类版本。但函数参数默认值,是静态联编行为,pBD的类型是基类指针,所以使用了基类的参数默认值,注意此处!
 6 
 7 Base::g(float)--5 //通过对象调用,int被提升为float
 8 Base::g(Other&) //没什么问题,基类中三个g函数之间的关系是重载
 9 Derived::g(int)--5 //没什么问题
10 Derived::g(int)--3 //注意基类的g(float)已经被隐藏!所以传入的float参数调用的却是子类的g(int)方法!
11 
12 Base::g(float)--5 //注意!pBD是基类指针,虽然它指向了子类对象,但基类中的所有g函数版本它是可见的!所以pBD->g(5)调用到了g(float)!虽然产生了动态联编也发生了隐藏,但子类对象的虚表中,仍可以找到g(float)的地址,即基类版本!
13 Base::g(float)--3.14159 //原理同上
14 
15 //d.g(o)
16 //注意此处!再注意代码中被注释了的一行,d.g(o)不能通过编译,因为d是子类对象,在子类中,基类中定义的三个g函数版本都被隐藏了,编译时不可见!不会重载
17 
18 Base::g(Other&) const //pC是指向const对象的指针,将调用const版本的g函数
19 Base::g(Other&) const //pCCP是指向const对象的const指针,也调用const版本的g函数
20 Base::g(Other&) //pCP是指向非cosnt对象的const指针,由于不指向const对象,调用非const版本的g函数

上述结果,是否和预想的是否又有些出入呢?问题主要集中于结果的第5、12、13和15行。

第5行输出结果证明:当函数参数有默认值,又发生多态行为时,函数参数默认值是静态行为,在编译时就已经确定,将使用基类版本的函数参数默认值而不是子类的

而第12、13、15行输出结果则说明,尽管已经证明我们之前说的隐藏是正确的(因为d.g(o)不可以通过编译,确实发生了隐藏),但却可以利用基类指针指向派生类对象后,来绕开这种限制!也就是说,编译器根据参数匹配函数原型的时候,是在编译时根据指针的类型,或对象的类型来确定,指针类型是基类,那么基类中的g函数版本就是可见的;指针类型是子类,由于发生了隐藏,基类中的g函数版本就是不可见的。而到动态绑定时,基类指针指向了子类对象,在子类对象的虚函数表中,就可以找到基类中g虚函数的地址。

写到这里,不知道读者是否已经明白,这些绕来绕去的关系。在实际代码运用中,可能并不会写出含有这么多“陷阱”的测试代码,我们只要弄清楚重载、覆盖和隐藏的具体特征,并头脑清醒地知道,我现在需要的是哪一种功能(通常也不会需要隐藏),就能写出清析的代码。上面的代码其实是一个糟糕的例子,因为在这个例子中,重载、覆盖、隐藏并存,我们编写代码,就是要尽可能防止这种含混不清的情况发生。

记住一个原则:每一个方法,功能和职责尽可能单一,否则,尝试将它拆分成为多个方法

posted @ 2012-12-26 17:09  Jone Zhang  阅读(2214)  评论(4编辑  收藏  举报