《成员函数的重载、覆盖与隐藏》
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
成员函数的重载、覆盖(override)与隐藏很容易混淆,C++程序员必须要搞清楚
概念,否则错误将防不胜防。
8.2.1 重载与覆盖
1.成员函数被重载的特征:
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
2.覆盖是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
/**************************************************************************/
示例8-2-1 中,函数Base::f(int)与Base::f(float)相互重载,而Base::g(void)
被Derived::g(void)覆盖。
#include
class Base
{
public:
void f(int x){ cout << "Base::f(int) " << x << endl; }
void f(float x){ cout << "Base::f(float) " << x << endl; }
virtual void g(void){ cout << "Base::g(void)" << endl;}
};
class Derived : public Base
{
public:
virtual void g(void){ cout << "Derived::g(void)" << endl;}
};
void main(void)
{
Derived d;
Base *pb = &d;
pb->f(42); // Base::f(int) 42
pb->f(3.14f); // Base::f(float) 3.14
pb->g(); // Derived::g(void)
}
示例8-2-1 成员函数的重载和覆盖
/**************************************************************************/
8.2.2 令人迷惑的隐藏规则
本来仅仅区别重载与覆盖并不算困难,但是C++的隐藏规则使问题复杂性陡然增加。
这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual
关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual
关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
/**************************************************************************/
示例程序8-2-2(a)中:
(1)函数Derived::f(float)覆盖了Base::f(float)。
(2)函数Derived::g(int)隐藏了Base::g(float),而不是重载。
(3)函数Derived::h(float)隐藏了Base::h(float),而不是覆盖。
#include
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; }
};
示例8-2-2(a)成员函数的重载、覆盖和隐藏
/**************************************************************************/
据作者考察,很多C++程序员没有意识到有“隐藏”这回事。由于认识不够深刻,
“隐藏”的发生可谓神出鬼没,常常产生令人迷惑的结果。
/**************************************************************************/
示例8-2-2(b)中,bp 和dp 指向同一地址,按理说运行结果应该是相同的,可事
实并非这样。
void main(void)
{
Derived d;
Base *pb = &d;
Derived *pd = &d;
// Good : behavior depends solely on type of the object
pb->f(3.14f); // Derived::f(float) 3.14
pd->f(3.14f); // Derived::f(float) 3.14
// 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
}
示例8-2-2(b) 重载、覆盖和隐藏的比较
/**************************************************************************/
8.2.3 摆脱隐藏
隐藏规则引起了不少麻烦。
/**************************************************************************/
示例8-2-3 程序中,语句pd->f(10)的本意是想调用函
数Base::f(int),但是Base::f(int)不幸被Derived::f(char *)隐藏了。由于数字10
不能被隐式地转化为字符串,所以在编译时出错。
class Base
{
public:
void f(int x);
};
class Derived : public Base
{
public:
void f(char *str);
};
void Test(void)
{
Derived *pd = new Derived;
pd->f(10); // error
}
示例8-2-3 由于隐藏而导致错误
/**************************************************************************/
从示例8-2-3 看来,隐藏规则似乎很愚蠢。但是隐藏规则至少有两个存在的理由:
写语句pd->f(10)的人可能真的想调用Derived::f(char *)函数,只是他误将参数
写错了。有了隐藏规则,编译器就可以明确指出错误,这未必不是好事。否则,编
译器会静悄悄地将错就错,程序员将很难发现这个错误,流下祸根。
假如类Derived 有多个基类(多重继承),有时搞不清楚哪些基类定义了函数f。如
果没有隐藏规则,那么pd->f(10)可能会调用一个出乎意料的基类函数f。尽管隐
藏规则看起来不怎么有道理,但它的确能消灭这些意外。
示例8-2-3 中,如果语句pd->f(10)一定要调用函数Base::f(int),那么将类
Derived 修改为如下即可。
class Derived : public Base
{
public:
void f(char *str);
void f(int x) { Base::f(x); }
};
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
首先,我们来看一个非常简单的例子,理解一下什么叫函数隐藏hide。
#include
using namespace std;
class Base{
public:
void fun() { cout << "Base::fun()" << endl; }
};
class Derive : public Base{
public:
void fun(int i) { cout << "Derive::fun()" << endl; }
};
int main()
{
Derive d;
//下面一句错误,故屏蔽掉
//d.fun();error C2660: 'fun' : function does not take 0 parameters
d.fun(1);
Derive *pd =new Derive();
//下面一句错误,故屏蔽掉
//pd->fun();error C2660: 'fun' : function does not take 0 parameters
pd->fun(1);
delete pd;
return 0;
}
/*在不同的非命名空间作用域里的函数不构成重载,子类和父类是不同的两个作用域。
在本例中,两个函数在不同作用域中,故不够成重载,除非这个作用域是命名空间作用域。*/
在这个例子中,函数不是重载overload,也不是覆盖override,而是隐藏hide。
接下来的5个例子具体说明一下什么叫隐藏
例1
#include
using namespace std;
class Basic{
public:
void fun(){cout << "Base::fun()" << endl;}//overload
void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
Derive d;
d.fun();//正确,派生类没有与基类同名函数声明,则基类中的所有同名重载函数都会作为候选函数。
d.fun(1);//正确,派生类没有与基类同名函数声明,则基类中的所有同名重载函数都会作为候选函数。
return 0;
}
例2
#include
using namespace std;
class Basic{
public:
void fun(){cout << "Base::fun()" << endl;}//overload
void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
//新的函数版本,基类所有的重载版本都被屏蔽,在这里,我们称之为函数隐藏hide
//派生类中有基类的同名函数的声明,则基类中的同名函数不会作为候选函数,即使基类有不同的参数表的多个版本的重载函数。
void fun(int i,int j){cout << "Derive::fun(int i,int j)" << endl;}
void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
Derive d;
d.fun(1,2);
//下面一句错误,故屏蔽掉
//d.fun();error C2660: 'fun' : function does not take 0 parameters
return 0;
}
例3
#include
using namespace std;
class Basic{
public:
void fun(){cout << "Base::fun()" << endl;}//overload
void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
//覆盖override基类的其中一个函数版本,同样基类所有的重载版本都被隐藏hide
//派生类中有基类的同名函数的声明,则基类中的同名函数不会作为候选函数,即使基类有不同的参数表的多个版本的重载函数。
void fun(){cout << "Derive::fun()" << endl;}
void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
Derive d;
d.fun();
//下面一句错误,故屏蔽掉
//d.fun(1);error C2660: 'fun' : function does not take 1 parameters
return 0;
}
例4
#include
using namespace std;
class Basic{
public:
void fun(){cout << "Base::fun()" << endl;}//overload
void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
using Basic::fun;
void fun(){cout << "Derive::fun()" << endl;}
void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
Derive d;
d.fun();//正确
d.fun(1);//正确
return 0;
}
/*
输出结果
Derive::fun()
Base::fun(int i)
Press any key to continue
*/
例5
#include
using namespace std;
class Basic{
public:
void fun(){cout << "Base::fun()" << endl;}//overload
void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
using Basic::fun;
void fun(int i,int j){cout << "Derive::fun(int i,int j)" << endl;}
void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
Derive d;
d.fun();//正确
d.fun(1);//正确
d.fun(1,2);//正确
return 0;
}
/*
输出结果
Base::fun()
Base::fun(int i)
Derive::fun(int i,int j)
Press any key to continue
*/
如果基类有某个函数的多个重载(overload)版本,而你在派生类中重写(override)了基类中的一个或多个函数版本,或是在派生类中重新添加了新的函数版本(函数名相同,参数不同),则所有基类的重载版本都被屏蔽,在这里我们称之为隐藏hide。所以,在一般情况下,你想在派生类中使用新的函数版本又想使用基类的函数版本时,你应该在派生类中重写基类中的所有重载版本。你若是不想重写基类的重载的函数版本,则你应该使用例4或例5方式,显式声明基类名字空间作用域。
事实上,C++编译器认为,相同函数名不同参数的函数之间根本没有什么关系,它们根本就是两个毫不相关的函数。只是C++语言为了模拟现实世界,为了让程序员更直观的思维处理现实世界中的问题,才引入了重载和覆盖的概念。重载是在相同名字空间作用域下,而覆盖则是在不同的名字空间作用域下,比如基类和派生类即为两个不同的名字空间作用域。在继承过程中,若发生派生类与基类函数同名问题时,便会发生基类函数的隐藏。当然,这里讨论的情况是基类函数前面没有virtual 关键字。在有virtual 关键字关键字时的情形我们另做讨论。
继承类重写了基类的某一函数版本,以产生自己功能的接口。此时C++编绎器认为,你现在既然要使用派生类的自己重新改写的接口,那我基类的接口就不提供给你了(当然你可以用显式声明名字空间作用域的方法,见[C++基础]重载、覆盖、多态与函数隐藏(1))。而不会理会你基类的接口是有重载特性的。若是你要在派生类里继续保持重载的特性,那你就自己再给出接口重载的特性吧。所以在派生类里,只要函数名一样,基类的函数版本就会被无情地屏蔽。在编绎器中,屏蔽是通过名字空间作用域实现的。
所以,在派生类中要保持基类的函数重载版本,就应该重写所有基类的重载版本。重载只在当前类中有效,继承会失去函数重载的特性。也就是说,要把基类的重载函数放在继承的派生类里,就必须重写。
这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,具体规则我们也来做一小结:
1 如果派生类的函数与基类的函数同名,但是参数不同。此时,若基类无virtual关键字,基类的函数将被隐藏。(注意别与重载混淆,虽然函数名相同参数不同应称之为重载,但这里不能理解为重载,因为派生类和基类不在同一名字空间作用域内。这里理解为隐藏)
2 如果派生类的函数与基类的函数同名,但是参数不同。此时,若基类有virtual关键字,基类的函数将被隐式继承到派生类的vtable中。此时派生类vtable中的函数指向基类版本的函数地址。同时这个新的函数版本添加到派生类中,作为派生类的重载版本。但在基类指针实现多态调用函数方法时,这个新的派生类函数版本将会被隐藏。
3 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏。(注意别与覆盖混淆,这里理解为隐藏)。
4 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数有virtual关键字。此时,基类的函数不会被“隐藏”。(在这里,你要理解为覆盖哦^_^)。
好了,我们先来一个小小的总结重载与覆盖两者之间的特征
重载overload的特征:
1 相同的范围(在同一个类中);
2 函数名相同参数不同;
3 virtual 关键字可有可无。
覆盖override是指派生类函数覆盖基类函数,覆盖的特征是:
1 不同的范围(分别位于派生类与基类);
2 函数名和参数都相同;
3 基类函数必须有virtual 关键字。(若没有virtual 关键字则称之为隐藏hide