C++中的重载,隐藏,覆盖,虚函数,多态浅析
直到今日,才发现自己对重载的认识长时间以来都是错误的。幸亏现在得以纠正,真的是恐怖万分,雷人至极。一直以来,我认为重载可以发生在基类和派生类之间,例如:
1 class A { 2 public: 3 void test(int); 4 }; 5 class B : public A { 6 public: 7 void test(int, int); 8 }; 9 10 void main() 11 { 12 B b; 13 14 b.test(5); //错误,应该b.A::test(5); 15 }
我一直认为当类B把类A中的test函数继承之后,在类B中,类A的test函数和类B自己定义的test函数是重载关系(因为我觉得这两个函数靠形参个数区分开来了),进而,我就认为第14行会调用类A的test函数。非常雷人。现在把重载和隐藏的注意事项总结出来,供理解有误的人们参考:
重载:
在一个类内,如果存在若干个同名函数,而且这些函数之间可以用形参个数或者形参类型区分开来的时候(注意不能靠函数返回类型区分),这几个函数就互为重载函数。这时,当你通过类对象调用这几个函数时,编译器就可以通过你传递的实参个数或者类型,去匹配相应的函数,而不会发生歧义。这也就是重载函数的作用所在(让你可以使用若干个同名函数)。需要注意的是:
1.重载绝对不会发生在基类和派生类之间,如上例所示。当基类和派生类中存在同名函数时,无论同名函数的形参个数或者类型是否相同,派生类中的同名函数都会将基类中的同名函数隐藏掉,因此它们是隐藏关系,而不是重载关系。关于隐藏,后边会提到。如此以来,上例的14行在编译时就会报错,提示类B中没有test(int)类型的函数。
2.在同一个类中,重载函数之间必须依靠形参个数或者形参类型来进行区分,不能依靠返回类型。也就是说,如果同一个类中的两个同名函数形参个数和类型完全相同,但是返回值类型不同,这时候编译就会报错,因为当你通过类对象调用该同名函数时,编译器会出现二义性,不知道该选择哪个函数。记着,重载必须靠形参来区分。
3.在同一个类中,虚函数和虚函数,虚函数和普通函数之间也可以重载,规则完全同上。虚函数下边会提到。
隐藏:
隐藏只能出现在基类和派生类之间,而不能发生在同一个类内(比如上述2中,只会引起编译器出现二义性)。当基类和派生类中存在同名函数时,无论同名函数的形参个数或者类型是否相同,派生类中的同名函数都会将基类中的同名函数隐藏掉,而不会是重载关系。这时,当你通过派生类对象调用该同名函数时,只能访问派生类的该函数,如果硬要访问基类的该函数,则需要在函数名前加上类作用域,如上边代码所示。
覆盖:
覆盖也只能出现在基类和派生类之间,当派生类和基类中的存在同名函数,且参数个数和参数类型完全相同,并且基类中的该函数有virtual修饰(派生类中的该函数可有可无),则派生类的该函数覆盖掉基类的该函数。该性质用来实现多态。
虚函数:
在一个类中,用virtual关键字声明的函数都是虚函数。虚函数存在的唯一目的,就是为了实现多态(动态绑定/运行时绑定)。关于多态,后面会提到。虚函数只有在基类和派生类之间才能发挥虚特性(也就是说才能发挥虚函数的真正的目的)。在同一个类中,所有虚函数就和普通函数是一样,使用同样的重载规则(重载的第3点中提到过)。因此在同一个类中可以把虚函数看作普通函数来使用(因为其虚特性发挥不出来),使用方法和注意事项与普通函数一模一样。
多态:
多态是面向对象思想的精髓所在。说白了,就是通过基类指针或引用调用一个成员函数时,直到运行阶段在才能决定该成员函数是哪个派生类中定义的成员函数。有点抽象吧?没事,先看一段代码吧。
1 class A { 2 public: 3 virtual void test(int); 4 }; 5 6 class B : public A { 7 public: 8 void test(int); 9 }; 10 11 class C : public A { 12 public: 13 void test(int); 14 }; 15 16 void main() 17 { 18 A *a0; 19 A &a1 = b; 20 A &a2 = c; 21 B b; 22 C c; 23 24 a0 = &b; 25 a0.test(2); //调用类B的test函数 26 27 a0 = &c; 28 a0.test(3); //调用类C的test函数 29 30 a1.test(4); //调用类B的test函数 31 a2.test(5); //调用类C的test函数 32 }
我们先说什么是多态吧,随后再讲产生多态的条件。第18行,在main函数中定义了一个指向类A类型的指针变量a0,第21和22行分别定义了派生类B和C的对象b,c。第24行,将对象b的指针赋给a0,第25行a0.test将调用类B的test函数;第27行,将对象c的指针赋给a0,第28行a0.test将调用类C的test函数。这就是多态,有感觉了吗?说白了,就是当基类指针变量指向了哪个派生类对象,就可以调用哪个派生类对象的方法。类似的,引用也可以实现多态,第19-20,30-31行所展示的。
下面总结下实现多态的条件:
哪些成员函数想要以多态的形式来执行,那么这些函数必须:
1.在基类中将这些成员函数声明为虚函数,并实现(必须要实现)。
2.在派生类中也声明这些成员函数并实现(必须实现),基类和派生类的这些函数必须同名,而且其形参个数和类型,返回值类型必须与基类中的这些函数完全相同。此时,派生类中这些函数无论是否用virtual来声明,都会被自动虚化。
3.将派生类对象赋给基类的指针变量或者引用。至此,多态实现,可用基类指针或引用调用派生类的方法(符合多态条件的方法,而不是普通方法)。
实现多态的这三个条件必须完全满足,虚函数的虚特性才能发挥出来,也才能实现多态。缺少任何一个条件,虚函数的虚特性都会被打破,无法实现多态。虚特性被打破的虚函数和普通函数是一样的,因此说虚函数的唯一用途就是实现多态。
下面举一些不是多态的例子:
a.基类中声明为虚函数,派生类中也声明为虚函数,并且也同名。但是派生类中该函数的形参类型或者形参个数和基类中的不相同。此时,多态不满足,派生类和基类的这两个虚函数仅仅是隐藏关系,没有虚特性。
b.基类中声明为虚函数,派生类中也声明了一个同名函数,但没有使用virtual,并且形参类型或者形参个数和基类不相同。这时候基类中的虚函数也丢失虚特性,派生类的该函数不会被虚化,当然也就够不成多态,这两个函数也仅仅是隐藏关系。
c.基类中的函数不是虚函数,派生类中声明为虚函数,它们同名,这时也够不成多态,派生类的虚函数没有虚特性,它们也是隐藏关系。
d.基类和派生类的两个函数同名,都是虚函数,形参的个数和类型也都相同,但是返回值类型不同,这时编译会报错,因为两个虚函数在隐藏时,返回值类型发生了冲突,因此隐藏发生错误。注意,如果这两个函数不是虚函数,这不会报错,隐藏会成功;同时,如果派生类中是虚函数,基类中不是虚函数,也不过报错,隐藏也是成功的。这也说明,虚化并隐藏时,返回值类型一定要保持相同。