OOP存在重载(overloading)、重写(overriding)、隐藏(hiding)3个概念。他们非常相似,也容易混淆。本实用经验将详细讲述他们之间的区别。
重载(overloading)
也许你听说过函数重载,运算符重载。也许现在你已经可熟练使用重载了。重载其实就是一个名字在不同的形势下,存在不同的实现。先来看重载的官方定义:重载指同一作用域的不同函数使用相同的函数名,但是函数的参数或类型不同。简单的将就是不同的函数使用同样的标示符。并且这些函数位于同一个作用域。
在C++中,普通的全局函数可支持重载,类的成员函数也可支持重载。而且类的成员函数重载作用域仅在类中。下面看一个取整函数声明:
inline float ceil(float X);
inline long double ceil(long double X);
inline double ceil(double X);
这3个全局函数ceil(float X)、ceil(double X)和ceil(long double X)互为重载。都使用了函数标识符ceil函数标示符名称。而且都位于全局作用域中。我想现在你比较关注的应该是编译器在调用时怎么区分它们的?是的,这正是重载实现的关键所在。
重载的条件:
(1)至少有两个以上的函数具有相同的标示符名称。
(2)函数的形参个数或类型不同或是形参的顺序不同。
(3)函数的返回值相同与否,不能用于判断两个函数是否互为重载函数。
(4)互为重载的函数必须具备相同的作用域。不同作用域内的同名函数通过作用域来区分不形成重载。
同样类成员函数也支持重载,在实现机制方面他们和全局函数没有太大的区别。唯一的区别是一个是全局作用域,一个是类作用域;需要明确的是父类和派生类中的同名函数不形成重载,因为他们不在一个作用域。
// 打印机类
class CPrinter
{
public:
void Print(int iData);
void Print(float fData);
void Print(char *pszData, int iDatalength);
};
// 字符串打印机
class CStrPrinter : public CPrinter
{
void Print(char *pszData);
}
CStrPrinter strprinter;
Strprinter.print(2.0);
上述代码,无法通过编译,因为void Print(float fData)被void Print(char *pszData)覆盖了。此例可有效的验证上述论断。但是如果你真的想让基类中的print和派生类中的print形成重载,亦可行。见如下代码:
// 打印机类
class CPrinter
{
public:
void Print(int iData);
void Print(float fData);
void Print(char *pszData, int iDatalength);
}
// 字符串打印机
class CStrPrinter : public CPrinter
{
Using CPrinter::Print;
void Print(char *pszData);
}
CStrPrinter strprinter;
Strprinter.print(2.0);
上述代码可以通过编译,因为void Print(float fData)与void Print(char *pszData)互为重载。基类的成员函数与派生类的成员函数不互为重载。因为他们分属于不同的作用域。通过函数声明可将基类的声明引入到派生类中,只有在此前提下才可能存在重载。
关于重载最后需要说明的是:编译器在实现重载时,编译器是以静态绑定的方式处理的,关于实现机制你可参考实用经验 48相关论述。
重写(overriding)
如果你了解virtual函数,那你应该也知道重写(overriding)。基类和派生类之间的多态性,一般通过派生类重写基类中的同名且同参的虚函数实现。
// CFile文件操作类,完成文件的操作动作。
class CFile
{
public:
CFile();
virtual ~CFile();
// 文件打开操作
virtual Open(char *pszFileName, int iOpenType);
// 文件关闭操作
virtual Close();
// 文件读操作
virtual Read(void *pBuffer, int iBufferLength);
// 文件写操作
virtual Write(void *pBuffer, int iBufferLength);
};
// Text文件操作类,完成Text文件的操作动作。
class CTextFile: public CFile
{
public:
CTextFile();
virtual ~ CTextFile ();
// 文件打开操作
virtual Open(char *pszFileName, int iOpenType);
// 文件关闭操作
virtual Close();
// 文件读操作
virtual Read(void *pBuffer, int iBufferLength);
// 文件写操作
virtual Write(void *pBuffer, int iBufferLength);
};
CTextFile中Open,Close()及Read就是分别对CFile类中Open,Close()及Read函数的重写。一般重写需要遵守下述准则:
- 重写函数不能是static,且必须是virtual函数,即使父类未声明为virtual,那祖先也必须声明为virtual。
- 重写函数必须有相同的类型,名称和参数列表 (即相同的函数原型)。
- 重写函数的访问修饰符可以不同。父类声明virtual虚函数可见性为private,派生类重写改public,protected亦可。
- const可能会导致虚函数成员重写失败。这是因为基类成员函数和派生类成员函数的标签不一样。
// Text文件操作类,完成Text文件的操作动作。
class CTextFile: public CFile
{
public:
CTextFile();
~CTextFile();
// Text 文件打开操作。
virtual int Open(char *pszFileName, int iOpenType) const;
….
};
派生类CTextFile中的Open带了const,而基类CFile没有带const,因为具有不同的函数标签,所以派生类CTextFile中的Open函数,并没有重写CTextFile中的Open函数。
隐藏(hiding)
隐藏,就是派生类中的函数屏蔽了基类中的同名函数的非虚函数。派生类的同名非虚函数会隐藏基类中的同名函数。只要派生类中的函数和基类中的函数同名即可,而不需要两者具有相同的参数列表。
重定义后子类调用的函数是子类自己的函数,父类的函数会被隐藏。如果想调用父类的该同名函数,需要加上父类作用域来指定调用的函数。
因为编译器在实现函数调用时,首先会查找本类中函数,只要本类中有函数名称满足要求即停止查找。如果查找不到在沿着继承链逐级向上查找,直到查找到函数名称为止。由于在派生链中派生类属于下级,存在同名函数时,编译器最终选择派生类中的函数。从而导致基类的同名函数就被屏蔽了。
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); // 错误的
pd->Base::f(10); // 正确的。
}
语句pd->f(10)的本意是想调用函数Base::f(int),但是Base::f(int)不幸被Derived::f(char *)隐藏。由于数字10不能被隐式地转化为字符串,所以导致编译报错。
综上可知,重载、重写和隐藏所关注的主要是作用域,函数名,参数,返回值,有无virtual等。下面就以这些为切入点,总结三者的区别和联系,如表11-2所示。
对比项 | 作用域 | Virtual | 函数名 | 参数列表 | 返回值 |
---|---|---|---|---|---|
overloading | 相同 | 可有可无 | 相同 | 不同 | 可同,可不同 |
overriding | 不同 | 有 | 相同 | 相同 | 相同 |
Hiding | 不同 | 可有可无 | 相同 | 可相同,可不同 | 可同,可不同 |
请谨记
- 在C++中,重载(overloading)、重写(overriding)、隐藏(hiding)三者极为相似。所以极易混淆,尤其是初学者。三者的差异可从作用域,函数名,参数,返回值,有无virtual等方面来理解。