C++ 面向对象的三个特点--多态性(一)
-
C++的多态性定义
所谓多态性就是不同对象收到相同的消息产生不同的动作。通俗的说,多态性是指一个名字定义不同的函数,这些函数执行不同但又类似的操作,即用同样的接口访问功能不同的函数,从而实现“一个接口,多种方法”。
多态性又分为两种:一种是编译时的多态性,主要通过函数重载和运算符重载实现。一种是运行时的多态性,主要通过继承和虚函数来实现的。
这一部分,我们主要讲函数重载和继承与虚函数,运算符的重载我准备单独做一部分来记载。
-
函数重载
在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形参(参数的个数、类型或者顺序)必须不同,这就是函数重载。函数重载通常被用来实现功能类似而所处理的数据类型不同的问题。
1 // base class 2 class Base 3 { 4 public: 5 void print(int a, int b) { 6 printf("a = %d, b = %d\n", a, b); 7 } 8 9 void print(int c) { 10 printf("c = %d\n", c); 11 } 12 }; 13 // main 14 int _tmain(int argc, _TCHAR* argv[]) 15 { 16 Base base; 17 base.print(1); 18 base.print(2, 3); 19 20 system("pause"); 21 return 0; 22 }
输出结果:
c = 1
a = 2, b = 3
几个注意点:
a) 函数的类型不在参数匹配检查之列。若两个函数除了返回类型不同,其他的全部相同,这是非法的。如:
int Print(int a);
double Print(int a);
虽然这两个函数的返回类型不同,但是由于参数个数和参数类型完全相同,编译程序将无法区分这两个函数。因为在确定调用哪一个函数之前,返回类型是不知道的。
b) 函数的重载与带默认值的函数一起使用时,需要注意有可能会引起二义性。如:
void Print(int a = 0, int b = 0);
void Print(int a);
当我们调用Print(1)时,程序无法确定调用的是哪一个。
c) 函数调用时,注意输入的实参要与形参的类型相符,否则容易造成编译器自动做类型转换是导致的错误。
-
隐藏(基类函数的隐藏)
网上看到很多博客和文章提到这一个概念,在有的书上面,也发现这一概念被称作为在基类和派生类中的函数重载。不过这里为了和函数重载区分,我们还是把它叫做隐藏。
我们来根据两个例子来说明:
一:派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字(有关键字virtual的话就是虚函数了,这就是后面要说的重写(覆盖)的概念了)。此时,基类的函数被隐藏。
1 // 基类 2 class Base 3 { 4 public: 5 // 不能有关键字virtual 6 void print(int a) { 7 printf("Base::a = %d\n", a); 8 } 9 }; 10 // 派生类 11 class Derivel : 12 public Base 13 { 14 public: 15 // 隐藏 16 void print(int a) { 17 printf("Derivel::a = %d\n", a); 18 } 19 }; 20 // main 21 int _tmain(int argc, _TCHAR* argv[]) 22 { 23 Base base; 24 base.print(1); 25 26 Derivel derivel; 27 derivel.print(1); // 隐藏了基类的print函数,调用的是派生类中的print函数 28 29 system("pause"); 30 return 0; 31 }
输出结果:
Base::a = 1
Derivel::a = 1
说明:
派生类Derivel公有继承了基类Base,对于它的公有函数print。派生类的对象derivel是可以直接访问的。但是派生类中自己也定义了一个同名且同参数的print函数,我们可以看到对象derivel调用的print函数时派生类自己定义的print函数,而不是基类继承下来的print函数,由此可知,基类的print函数被隐藏了。
二:派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏。
1 // 基类 2 class Base 3 { 4 public: 5 // 关键字virtual有无不影响 6 virtual void print(int a) { 7 printf("Base::a = %d\n", a); 8 } 9 }; 10 // 派生类 11 class Derivel : 12 public Base 13 { 14 public: 15 // 隐藏 16 void print(int a,int b) { 17 printf("Derivel::a = %d, b = %d\n", a, b); 18 } 19 }; 20 // main 21 int _tmain(int argc, _TCHAR* argv[]) 22 { 23 Base base; 24 base.print(1); 25 26 Derivel derivel; 27 // derivel.print(1); // 错误,基类的print已经被隐藏 28 derivel.print(1,2); // 正确 29 30 system("pause"); 31 return 0; 32 }
输出结果:
Base::a = 1
Derivel::a = 1, b = 2
说明:这里与上面的不同是派生类中定义了一个名字相同,参数不同的函数,这个函数同样把继承的print函数给隐藏了。同时,由于参数不同,就不用考虑基类的print函数有没有virtual关键字定义了。
延伸:
重载与隐藏的区别:
(1) 范围不同,重载的函数是在一个类里面,隐藏和被隐藏的函数是在基类和派生类两个类里面。
(2) 参数的区别,重载的函数之间参数肯定是不一样的,隐藏和被隐藏的函数参数可以一样也可以不一样。
-
虚函数和函数重写(覆盖)
上面说的函数重载和隐藏以及没有说明的运算符重载都是编译时的多态,也被称为静态多态。下面要说的虚函数和重写就是多态的另外一种,运行时的多态,也被称为动态多态。
要了解重写,我们首先得了解虚函数的概念。
虚函数是函数重载的另一种表现形式,这是一种动态的重载方式,它提供了一种更为灵活的多态性机制。这里也就离不开派生类的对象指针了,我们来看一个例子。
1 // 基类 2 class Base 3 { 4 public: 5 void print(int a) { 6 printf("Base::a = %d\n", a); 7 } 8 }; 9 // 派生类 10 class Derivel : 11 public Base 12 { 13 public: 14 void print(int a) { 15 printf("Derivel::a = %d", a); 16 } 17 18 void print1() { 19 printf("Derivel::print1"); 20 } 21 }; 22 // main 23 int _tmain(int argc, _TCHAR* argv[]) 24 { 25 Base base; 26 Derivel derivel; 27 Base *b; 28 29 b = &base; 30 b->print(1); 31 b = &derivel; 32 b->print(1); 33 34 system("pause"); 35 return 0; 36 }
输出结果:
Base::a = 1
Base::a = 1
对于对象指针,应该注意几个问题:
(1). 声明为指向基类对象的指针可以指向它的公有派生类对象,但不允许指向它的私有派生类对象和保护派生类对象。
(2). 允许将一个声明为指向基类对象的指针指向它的派生类对象,但是不能将一个指向派生类对象的指针指向它的基类对象。
(3). 声明为指向基类对象的指针,当其指向公有派生类对象时,只能用它来直接访问派生类从基类继承来的成员,而不能直接访问公有派生类中自己定义的成员。如上面例子中如果你写这句话:b->print1();,那它将报错。正确的应该写成((Derivel*)b)->print1();。
从输出结果可以看出,虽然b以及指向了派生类对象derivel,但是它所调用的print函数还是基类对象的print函数。这就是我们的问题,不管指针指向那个对象(基类对象或者派生类对象),它所调用的print函数一直都是基类的print函数。原因是普通成员函数的调用是在编译时静态联编的。在这种情况下,若要调用派生类中的成员函数,必须采用显示的调用方法,如:derivel.print(1),或者指针强制类型转换((Derivel*)derivel)->print();,我们使用对象指针的目的就是为了表达一种动态的特性,即指针指向的对象不同时执行的操作不同。如果采用以上的两种显示调用方法也就起不到这种作用了。其实真正的解决方法就是把基类的print函数说明为虚函数,这就是我们为什么要引入虚函数的原因了。
所以,如果我们把上面那段代码里面基类中的void print(int a)函数前面加上一个virtual关键字,把它声明为虚函数,那么输出结果就是这样:
Base::a = 1
Derivel::a = 1
在基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或者是多个派生类中被重新定义。在派生类中重新定义时,其函数原型,包括返回类型,函数名,参数个数,参数类型和参数顺序都必须和基类完全相同。
虚函数定义的几点说明:
(1). 在基类中,用virtual关键字可以将其public或者protected部分的成员函数声明为虚函数,建议在public部分声明。
(2). 在派生类对基类声明的虚函数进行重新定义的时候,关键字virtual是可写可不写的,但是不写的时候有的情况下容易引起混乱,所以建议在派生类重新定义的是时候也写上virtual关键字。
(3). 虚函数在派生类中被重新定义时,其函数的原型与基类中的函数原型必须完全相同。
(4). 一个虚函数无论被公有继承多少次,他仍然保持他的虚函数特性。
(5). 虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数。
(6). 构造函数不能被声明为虚函数,但是析构函数可以是虚函数。
通过一个例子,我们来最后总结一下虚函数下的重写(覆盖)。
1 // 基类 2 class Base 3 { 4 public: 5 virtual void print(int a) { 6 printf("Base::a = %d\n", a); 7 } 8 }; 9 // 基类1 10 class Base1 11 { 12 public: 13 void print(int a) { 14 printf("Base1::a = %d\n", a); 15 } 16 }; 17 // 派生类 18 class Derivel : 19 public Base,public Base1 20 { 21 public: 22 void print(int a) { 23 printf("Derivel::a = %d\n", a); 24 } 25 }; 26 // main 27 int _tmain(int argc, _TCHAR* argv[]) 28 { 29 Derivel derivel; // 定义派生类的对象derivel 30 Base *b; // 定义基类base的指针b 31 Base1 *b1; // 定义基类base1的指针b1 32 33 b = &derivel; 34 b->print(1); // 基类Base中print是虚函数,派生类重写了print函数 35 // 因此此处调用派生类的print函数 36 b1 = &derivel; 37 b1->print(1); // 基类Base1中print不是虚函数,b1是Base1的指针 38 // 因此此处调用基类Base1的print函数 39 40 system("pause"); 41 return 0; 42 }
输出结果:
Derivel::a = 1
Base1::a = 1
延伸:
重写和重载的区别:
1) .范围不同,重载的函数是在一个类里面,重写的函数是在基类和派生类两个类里面。
2) .参数的区别,重载的函数之间参数肯定是不一样的,重写的函数的函数类型(函数名,函数参数,参数个数,参数顺序)必须与基类的函数完全相同。
3) .virtual修饰的区别,重载函数virtual修饰可有可无,重写的基类函数必须有virtual修饰。
重写和隐藏的区别:
1) .参数的区别,隐藏和被隐藏的的函数之间参数是可以一样也可以不一样,重写函数的函数类型(函数名,函数参数,参数个数,参数顺序)必须与基类函数完全相同。
2) .virtual修饰的区别,当隐藏和被隐藏的函数之间参数一样时,不能有virtual(有virtual修饰其实就是重写了),参数不一样是,virtual可以可无。重写的基类函数必须有virtual修饰。