4.5 C++重载、覆盖和遮蔽
参考:http://www.weixueyuan.net/view/6375.html
总结:
函数签名包括函数名和函数参数的个数、顺序以及参数数据类型。
需要注意的是函数签名并不包含函数返回值部分,如果两个函数仅仅只有函数返回值不同,那么系统是无法区分这两个函数的,此时编译器会提示语法错误。
函数重载是指两个函数具有相同的函数名,但是函数参数个数或参数类型不同。函数重载多发生在顶层函数之间或者同一个类中,函数重载不需要构成继承关系。
函数重载是编译期绑定,它并不是多态。
覆盖构成条件和多态构成条件是相同的,覆盖是一种函数间的表现关系,而多态描述的是函数的一种性质,二者所描述的其实是同一种语法现象。
覆盖首先要求有继承关系,其次是要求构成继承关系的两个类中必须具有相同函数签名的成员函数,并且这两个成员函数必须是虚成员函数,具备这两个条件后,派生类中的虚成员函数则会覆盖基类中的同名的虚成员函数。如果我们通过基类指针或引用来调用虚成员函数,则会形成多态。
函数覆盖属于运行期绑定,但是要注意如果函数不是虚函数,则无论采用什么方法调用函数均为编译期绑定。
函数遮蔽同样要求构成继承关系,构成继承关系的两个类中具有相同函数名的函数,如果这两个函数不够成覆盖关系,则就构成了遮蔽关系。(注意不是相同函数签名,只需要相同函数名就可以了)
覆盖要求的是函数签名相同,而遮蔽只需要函数名相同。
一般来讲,函数名相同通常会用在以下几种情况中:
- 顶层函数的函数重载。对于程序设计人员而言,实现功能相同但所处理数据类型不同的函数时,采用函数重载的方式将会带来极大的方便。例如设计一个求绝对值函数,针对整型和double类型各设及一个abs函数,调用时而无需关注参数类型,这样的设计是很方便的。
- 类中的成员函数的重载,这种函数重载和顶层函数重载同样能给我们的程序带来方便。
- 类中的构造函数重载,设计多个构造函数,用于不同的初始化对象方式。
- 在继承层次中为了使用多态特性而采用相同函数签名。
除此之外函数名相同还会导致继承层次中的函数遮蔽,而函数遮蔽这一特性通常会使得程序难以理解,因此建议谨慎使用函数遮蔽机制。
------------------------------------
多态函数是指在运行期才将函数入口地址与函数名绑定的函数,仅有虚函数才是多态。但是除了虚函数以外,重载和遮蔽同样具有函数名相同的特征,在此做一下区分。为了说明方便,我们引入函数签名这一概念。函数签名包括函数名和函数参数的个数、顺序以及参数数据类型。
例1:
void f( ) void g( ) void f(int)
例2:
void f( int) void f(double)
例3:
void f(double, int) void f(int, double)
为了理解函数签名的含义,我们先来看一下上面的三个例子。例1中函数f()和函数g()函数名不同,因此这两个函数的函数签名不同,f()函数和f(int)函数一个有参数,一个没有参数,函数签名同样不同,g()函数和f(int)函数函数名不同并且函数参数个数也不同,因此这两个函数的函数签名也是不相同的。例2中两个函数函数名相同,函数参数个数相同,但是函数参数的类型不同,因此这两个函数的函数签名也不是相同的。例3中的两个函数,函数名相同,函数参数个数相同,函数参数类型也是相同的,都是一个double类型和一个int类型的,只不过函数参数的顺序是不相同,如此一来这两个函数的函数签名同样是不相同的。
需要注意的是函数签名并不包含函数返回值部分,如果两个函数仅仅只有函数返回值不同,那么系统是无法区分这两个函数的,此时编译器会提示语法错误。
例4:
int f(int, double) void f(int, double)
在本例中,两个函数的函数名相同,函数参数个数相同,函数参数类型相同,函数参数顺序相同,如此一来两个函数的函数签名是相同的。但是这两个函数的返回值不同,仅凭函数返回值,编译器无法区分这两个函数,编译器提示语法错误。
了解了函数签名的含义之后我们再来看一下重载、覆盖和遮蔽。
1) 重载
函数重载是指两个函数具有相同的函数名,但是函数参数个数或参数类型不同。函数重载多发生在顶层函数之间或者同一个类中,函数重载不需要构成继承关系。
例5:
class base { public : base(); base(int a); base(int a, int b); base( base &); int fun(int a); int fun(double a); int fun(int a, int b); private: int x; int y; }; int g(int a); int g(double a); int g(int a, int b);
在本例中,我们列出了几种函数重载的情形。首先是函数的构造函数重载,我们在类中声明了四个构造函数,这四个函数构成重载的关系,前面三个函数之间只是函数参数数目不同,第四个构造函数为拷贝构造函数,该函数与前面的默认构造函数和两个带参构造函数参数类型不同。类中的成员函数同样可以进行重载,如本例中base类的三个fun函数。这两种情况是类内部的函数重载,在类外部顶层函数也同样能够成函数重载关系,如本例中的g函数,这三个函数都是顶层函数,由于函数名相同,但是函数参数不同,构成函数重载关系。
函数重载是编译期绑定,它并不是多态。
2) 覆盖
覆盖构成条件和多态构成条件是相同的,覆盖是一种函数间的表现关系,而多态描述的是函数的一种性质,二者所描述的其实是同一种语法现象。
覆盖首先要求有继承关系,其次是要求构成继承关系的两个类中必须具有相同函数签名的成员函数,并且这两个成员函数必须是虚成员函数,具备这两个条件后,派生类中的虚成员函数则会覆盖基类中的同名的虚成员函数。如果我们通过基类指针或引用来调用虚成员函数,则会形成多态。
例6:
#include<iostream> using namespace std; class base { public : virtual void vir1(){} virtual void vir2(){} }; class derived : public base { public: void vir1(){} void vir2(){} }; int main() { base * p; p = new derived; p->vir1(); p->vir2(); delete p; return 0; }
本例是一个非常简单的多态的示例程序,base类和derived类构成继承关系,在这两个类中成员函数vir1和vir2同名,并且这两个同名函数都被声明为了虚函数。如此一来则构成了函数覆盖,派生类中的vir1函数覆盖了基类中的vir1函数,派生类中的vir2函数覆盖了基类中的vir2函数。在主函数中通过基类指针调用vir1和vir2虚函数,构成多态,这两个函数的运行为运行期绑定。
函数覆盖属于运行期绑定,但是要注意如果函数不是虚函数,则无论采用什么方法调用函数均为编译期绑定。如果我们将例6中的基类中的两个virtual关键字去掉,则主函数中调用vir1和vir2函数属于编译期绑定,无论p指向的是派生类对象或者是基类对象,执行的都将会是基类的vir1和vir2函数。
3) 遮蔽
函数遮蔽同样要求构成继承关系,构成继承关系的两个类中具有相同函数名的函数,如果这两个函数不够成覆盖关系,则就构成了遮蔽关系。遮蔽理解起来很简单,只要派生类与基类中具有相同函数名(注意不是相同函数签名,只需要相同函数名就可以了)并且不构成覆盖关系即为遮蔽。
遮蔽可以分为两种情况,一种是非虚函数之间,另一种则是虚函数之间。我们通过程序示例来分别介绍这两种遮蔽情况。
例7:
#include<iostream> using namespace std; class base { public : void vir1(){cout<<"base vir1"<<endl;} void vir2(){cout<<"base vir2"<<endl;} }; class derived : public base { public: void vir1(){cout<<"derived vir1"<<endl;} void vir2(int){cout<<"derived vir2"<<endl;} }; int main() { base * p; p = new derived; p->vir1(); p->vir2(); delete p; derived d; d.vir1(); d.vir2(5); d.base::vir1(); d.base::vir2(); return 0; }
在本例中没有虚函数,base类和derived类构成继承关系,因为构成继承关系的两个类中有同名函数,因此构成了函数遮蔽。派生类中的vir1函数遮蔽了基类中的vir1函数,派生类中的vir2函数遮蔽了基类中的vir1函数。需要注意的是虽然派生类中的vir2函数和基类中的vir2函数的函数签名不同,但是只需要函数名相同就构成函数遮蔽。我们接着来分析一下主函数,主函数中我们先是定义了基类类型的指针,指针指向的是基类对象,然后通过指针调用函数vir1和vir2,这个时候因为并不构成多态,因此调用的还是基类的vir1和vir2函数。之后定义了一个派生类对象d,通过该对象调用vir1和vir2函数,因为派生类中的vir1和vir2遮蔽了基类中的vir1和vir2函数,因此直接调用的将会是派生类中的vir1和vir2函数。如果需要通过派生类对象调用被遮蔽的基类中的函数,则需要通过域解析操作符来处理,在本例的最后d.base::vir1();和d.base::vir2()就是这么做的。这个程序的最终运行结果如下:
base vir1
base vir2
derived vir1
derived vir2
base vir1
base vir2
如果构成继承关系的两个类中包含同名的虚函数,则情况非常复杂,当然要判断还是非常简单,还是那个原则:如果没有构成覆盖则为遮蔽。覆盖要求的是函数签名相同,而遮蔽只需要函数名相同。
例8:
#include<iostream> using namespace std; class base { public : virtual void vir1(){cout<<"base vir1"<<endl;} virtual void vir2(){cout<<"base vir2"<<endl;} }; class derived : public base { public: virtual void vir1(){cout<<"derived vir1"<<endl;} virtual void vir2(int){cout<<"derived vir2"<<endl;} }; int main() { base * p; p = new derived; p->vir1(); p->vir2(); delete p; derived d; d.vir1(); d.vir2(5); d.base::vir1(); d.base::vir2(); return 0; }
在这个程序中,定义了两个类,base类和derived类,这两个类构成继承关系,派生类和基类中包含同名的函数,并且同名的函数均为虚函数。针对这两个同名函数,我们一个一个来分析一下,首先来看一下vir1,基类和派生类中的vir1函数的函数签名是相同的,而且又是虚函数,构成了函数覆盖关系。再来看一下vir2函数,基类中的vir2函数和派生类中的vir2函数函数名相同,但函数参数不同,则它们的函数签名不同,因此派生类中的vir2函数和基类中的vir1函数不构成函数覆盖,既然函数名相同,那么可以构成函数遮蔽。
接着我们同样来看一下主函数,在主函数中,我们定义了一个基类类型的指针,指针指向派生类对象,之后通过该指针分别调用vir1和vir2函数。由于vir1是构成函数覆盖,因此通过基类指针调用vir1构成多态,由于p指针指向的是派生类对象,故调用的vir1函数是派生类中的vir1函数。派生类中的vir2函数和基类中的vir2函数只构成函数遮蔽,因此通过基类类型指针调用vir2函数并不会形成多态,最终调用的是基类中的vir2函数。之后定义了派生类对象d,通过派生类对象d调用的函数只能是派生类中的函数,当然也包括从基类中继承来的函数。d.vir1()和d.vir2(5)这两个函数调用语句调用的都是派生类中新增的成员函数,派生类中的vir1函数虽然和基类中的vir1函数构成覆盖关系,但是由于没有通过基类指针或引用来调用,因此也没有构成多态,如此一来,如果需要通过对象来调用从基类中继承过来的vir1函数,同样是需要域解析操作符。派生类中的vir2函数和基类中vir2函数构成遮蔽,因此通过对象和成员选择符调用的仍是派生类中新增的vir2函数,如果想调用基类中的vir2函数,则需要通过域解析操作符。例8程序运行结果如下:
derived vir1
base vir2
derived vir1
derived vir2
base vir1
base vir2
以上总结了函数名相同的所有情况,函数名相同利用的好可以为程序设计带来较大的便利,使用的不好则容易误导程序设计人员。一般来讲,函数名相同通常会用在以下几种情况中:
- 顶层函数的函数重载。对于程序设计人员而言,实现功能相同但所处理数据类型不同的函数时,采用函数重载的方式将会带来极大的方便。例如设计一个求绝对值函数,针对整型和double类型各设及一个abs函数,调用时而无需关注参数类型,这样的设计是很方便的。
- 类中的成员函数的重载,这种函数重载和顶层函数重载同样能给我们的程序带来方便。
- 类中的构造函数重载,设计多个构造函数,用于不同的初始化对象方式。
- 在继承层次中为了使用多态特性而采用相同函数签名。
除此之外函数名相同还会导致继承层次中的函数遮蔽,而函数遮蔽这一特性通常会使得程序难以理解,因此建议谨慎使用函数遮蔽机制。