贵阳建设网站培训,企鹅媒体平台,成都口碑最好的家装公司,电脑培训学校学费多少第五条#xff1a;再谈重载、覆盖和隐藏 在C中#xff0c;无论在类作用域内还是外#xff0c;两个#xff08;或多个#xff09;同名的函数#xff0c;可能且仅可能是以下三种关系#xff1a;重载#xff08;Overload#xff09;、覆盖#xff08;Override#xff0…第五条再谈重载、覆盖和隐藏 在C中无论在类作用域内还是外两个或多个同名的函数可能且仅可能是以下三种关系重载Overload、覆盖Override和隐藏Hide因为同名区分这些关系则是根据参数是否相同、是否带有const成员函数性质、是否有virtual关键字修饰以及是否在同一作用域来判断。在第四条中我们曾提到了一些关于重载、覆盖的概念但只是一带而过也没有提到隐藏这一篇我们将详细讨论。 1、首先说的是重载有一个前提必须要弄清楚的是如果不在类作用域内进行讨论两个或多个同名函数之间的关系只可能是重载或隐藏这里先说重载。考虑以下事实 1 int foo(char c){...}
2 void foo(int x){...} 这两个函数之间的关系是重载overload即相同函数名但参数不同并注意返回类型是否相同并不会对重载产生任何影响。 也就是说如果仅仅是返回类型不相同而函数名和参数都完全相同的两个函数不能构成重载编译器会告知ambiguous二义性等词以表达其不满 1 //Cant 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点统称“函数特征标”不同 4virtual关键字可有可无不产生影响因为第1点已经指出这是在同一个类中 即“相同的范围特征标不同当然同名是肯定的发生重载“。 2、覆盖override真正的多态行为通过虚函数来实现所以编译器根据以下依据来进行判定两个注意只可能是两个即使在继承链中也只是最近两个为一组函数之间的关系是覆盖 1不同的范围即使用域两个函数分别位于基类和派生类中 2函数名字相同基本前提 3函数参数也相同基本前提第2和第3点统称“函数特征标”相同 4基类函数必须用virtual关键字修饰 即“不同的范围特征标相同且基类有virtual声明发生覆盖“。 3、隐藏Hide即 1如果派生类函数与基类函数同名但参数不同特征标不同此时无论是否有virtual关键字基类的所有同名函数都将被隐藏而不会重载因为不在同一个类中 2如果派生类函数与基类函数同名且参数也相同特征标相同但基类函数没有用virtual关键字声明则基类的所有同名函数都将被隐藏而不会覆盖因为没有声明为虚函数。 即“不同的范围特征标不同当然同名是肯定的发生隐藏”或不同的范围特征标相同但基类没有virtual声明发生隐藏“。 可见有两种产生隐藏的情况分别对应不能满足重载和覆盖条件的情况。 另外必须要注意的是在类外讨论时也可能发生隐藏如在名字空间中如下述代码所示 1 #include iostream2 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 a7 {8 void foo(void) { cout a::foo() endl; }9 void callFoo(void)
10 { foo();
11 // foo(10); Cant 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 iostream2 using namespace std;3 4 class Other5 {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 // Cant 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被提升为float8 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虚函数的地址。 写到这里不知道读者是否已经明白这些绕来绕去的关系。在实际代码运用中可能并不会写出含有这么多“陷阱”的测试代码我们只要弄清楚重载、覆盖和隐藏的具体特征并头脑清醒地知道我现在需要的是哪一种功能通常也不会需要隐藏就能写出清析的代码。上面的代码其实是一个糟糕的例子因为在这个例子中重载、覆盖、隐藏并存我们编写代码就是要尽可能防止这种含混不清的情况发生。 记住一个原则每一个方法功能和职责尽可能单一否则尝试将它拆分成为多个方法。转载于:https://www.cnblogs.com/ccdev/archive/2012/12/26/2833884.html