多态性与虚函数
原文链接:http://www.cnblogs.com/CaiNiaoZJ/archive/2011/08/11/2134673.html
面向对象的程序设计中的多态性是指向不同的对象发送同一消息,不同对象对同一消息产生不同行为。在程序中消息就是调用函数,不同的行为就是指不同的实现方法,对应不同的函数体。换种说法也就是“一个接口,多种方法”。
从实现的角度来讲,多态性可以分为两类:编译时的多态性和运行时的多态性。前者是通过静态联编来实现的,比如C++中函数的重载和运算符的重载。后者则是通过动态联编来实现,在C++中运行时的多态性主要是通过虚函数来实现的。
1、不过在说虚函数之前,先介绍一个有关于基类和派生类对象之间的复制兼容关系的内容。它也是之后学习虚函数的基础。有时候会把整数型赋值给双精度类型的变量。在赋值之前,先把整形数据转换为双精度的,再把它赋值给双精度类型的变量。这种不同类型数据之间的自动转换和赋值称之为赋值兼容。同样,在基类和派生类之间也存在着赋值兼容关系,它是指需要基类对象的任何地方都可以使用公有派生类来代替。为什么只有公有继承的才可以呢,因为在公有继承中派生类保留了基类中除了构造和析构函数之外的所有成员,基类的公有或保护成员的访问权限都按原样保留下来,在派生类外可以调用基类的公有函数来访问基类的私有成员。因此基类实现的功能,派生类都可以。
那么它们具体是如何实现的呢? (1)派生类对象直接向基类赋值,赋值效果,基类数据成员和派生类中数据成员的值相同;(2)派生类对象可以初始化基类对象的引用;(3)派生类对象的地址可以赋值给基类对象的指针;(4)函数形参是基类对象或基类对象的引用,在调用函数时,可以用派生类的对象作为实参;
1 #include "stdafx.h" 2 #include<iostream> 3 #include<string> 4 5 class ABCBase 6 { 7 private: 8 std::string ABC; 9 public: 10 ABCBase(std::string abc) 11 { 12 ABC=abc; 13 } 14 void showABC(); 15 }; 16 17 void ABCBase::showABC() 18 { 19 std::cout<<"字母ABC=>"<<ABC<<std::endl; 20 } 21 22 class X:public ABCBase 23 { 24 public: 25 X(std::string x):ABCBase(x){} 26 }; 27 28 void function(ABCBase &base) 29 { 30 base.showABC(); 31 } 32 33 34 int main() 35 { 36 ABCBase base("A"); 37 base.showABC(); 38 39 X x("B"); 40 base=x; 41 base.showABC(); 42 43 ABCBase &base1=x; 44 base1.showABC(); 45 46 ABCBase *base2=&x; 47 base2->showABC(); 48 49 function(x); 50 51 return0; 52 }
运行结果:
要注意的是:第一,在基类和派生类对象的赋值时,该派生类必须是公有继承的。第二,只允许派生类对象向基类对象赋值,反过来则不行;
2. 接下来讲一下虚函数,它允许函数调用与函数体之间的联系在运行时才建立,即在运行时才决定如何动作。虚函数声明的格式:
virtual 返回类型 函数名(形参表)
{
函数体
}
那么定义虚函数有什么用呢?让我们用下面的这个示例来说明一下:
1 #include "stdafx.h" 2 #include <iostream> 3 #include <string> 4 5 6 class Graph 7 { 8 protected: 9 double x; 10 double y; 11 public: 12 Graph(double x,double y); 13 void showArea(); 14 }; 15 16 Graph::Graph(double x,double y) 17 { 18 this->x=x; 19 this->y=y; 20 } 21 22 void Graph::showArea() 23 { 24 std::cout<<"计算图形面积"<<std::endl; 25 } 26 27 class Rectangle:public Graph 28 { 29 public: 30 Rectangle(double x,double y):Graph(x,y){}; 31 void showArea(); 32 }; 33 34 void Rectangle::showArea() 35 { 36 std::cout<<"矩形面积为:"<<x*y<<std::endl; 37 } 38 39 class Triangle:public Graph 40 { 41 public: 42 Triangle(double d,double h):Graph(d,h){}; 43 void showArea(); 44 }; 45 46 void Triangle::showArea() 47 { 48 std::cout<<"三角形面积为:"<<x*y*0.5<<std::endl; 49 } 50 51 class Circle:public Graph 52 { 53 public: 54 Circle(double r):Graph(r,r){}; 55 void showArea(); 56 }; 57 58 void Circle::showArea() 59 { 60 std::cout<<"圆形面积为:"<<3.14*x*y<<std::endl; 61 } 62 63 int main() 64 { 65 Graph *graph; 66 67 Rectangle rectangle(10,5); 68 graph=&rectangle; 69 graph->showArea(); 70 71 Triangle triangle(5,2.4); 72 graph=▵ 73 graph->showArea(); 74 75 Circle circle(2); 76 graph=&circle; 77 graph->showArea(); 78 79 return0; 80 }
运行结果:
结果似乎和我们想象的不一样,既然Graph类的对象graph指针分别指向了Rectangle类对象,Triangle类对象,以及Circle类对象,那么就应该执行它们自己所对应的成员函数,为何结果不是这样呢?当基类对象指针指向公有派生对象时,它只能访问从基类继承下来的成员,而不能访问派生类中定义的成员。但是使用动态指针就是为了表达一种动态调用的性质即当指针指向哪个对象,就调用那个对象对应类的成员函数。那要怎么解决呢,这是就体现出了虚函数的作用。其实我们只需要针对上一个示例代码中所有类里出现的showArea()函数声明之前加一个关键字virtual:
1 1 #include "stdafx.h" 2 2 #include <iostream> 3 3 #include <string> 4 4 5 5 6 6 class Graph 7 7 { 8 8 protected: 9 9 double x; 10 10 double y; 11 11 public: 12 12 Graph(double x,double y); 13 13 voidvirtual showArea();//定义为虚函数或virtual void showArea() 14 14 }; 15 15 16 16 Graph::Graph(double x,double y) 17 17 { 18 18 this->x=x; 19 19 this->y=y; 20 20 } 21 21 22 22 void Graph::showArea() 23 23 { 24 24 std::cout<<"计算图形面积"<<std::endl; 25 25 } 26 26 27 27 class Rectangle:public Graph 28 28 { 29 29 public: 30 30 Rectangle(double x,double y):Graph(x,y){}; 31 31 virtualvoid showArea();//定义为虚函数 32 32 }; 33 33 34 34 void Rectangle::showArea() 35 35 { 36 36 std::cout<<"矩形面积为:"<<x*y<<std::endl; 37 37 } 38 38 39 39 class Triangle:public Graph 40 40 { 41 41 public: 42 42 Triangle(double d,double h):Graph(d,h){}; 43 43 virtualvoid showArea();//定义为虚函数 44 44 }; 45 45 46 46 void Triangle::showArea() 47 47 { 48 48 std::cout<<"三角形面积为:"<<x*y*0.5<<std::endl; 49 49 } 50 50 51 51 class Circle:public Graph 52 52 { 53 53 public: 54 54 Circle(double r):Graph(r,r){}; 55 55 virtualvoid showArea();//定义为虚函数 56 56 }; 57 57 58 58 void Circle::showArea() 59 59 { 60 60 std::cout<<"圆形面积为:"<<3.14*x*y<<std::endl; 61 61 } 62 62 63 63 int main() 64 64 { 65 65 Graph *graph; 66 66 67 67 Rectangle rectangle(10,5); 68 68 graph=&rectangle; 69 69 graph->showArea(); 70 70 71 71 Triangle triangle(5,2.4); 72 72 graph=▵ 73 73 graph->showArea(); 74 74 75 75 Circle circle(2); 76 76 graph=&circle; 77 77 graph->showArea(); 78 78 79 79 return0; 80 80 }
其他代码原封不动,就可以得到我们想要的运行结果:
在基类中的某成员函数被声明为虚函数后,在之后的派生类中可以重新来定义它。但定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,都必须和基类中的原型完全相同。其实在上述修改后的代码里,只要在基类中显式声明了虚函数,那么在之后的派生类中就不需要用virtual来显式声明了,可以略去,应为系统会根据其是否和基类中虚函数原型完全相同来判断是不是虚函数。因此,上述派生类中的虚函数如果不显式声明也还是虚函数。最后对虚函数做几点补充说明:(1)因为虚函数使用的基础是赋值兼容,而赋值兼容成立的条件是派生类之从基类公有派生而来。所以使用虚函数,派生类必须是基类公有派生的;(2)定义虚函数,不一定要在最高层的类中,而是看在需要动态多态性的几个层次中的最高层类中声明虚函数;(3)虽然在上述示例代码中main()主函数实现部分,我们也可以使用相应图形对象和点运算符的方式来访问虚函数,如:rectangcle.showArea(),但是这种调用在编译时进行静态联编,它没有充分利用虚函数的特性。只有通过基类对象来访问虚函数才能获得动态联编的特性;(4)一个虚函数无论配公有继承了多少次,它仍然是虚函数;(5)虚函数必须是所在类的成员函数,而不能是友元函数,也不能是静态成员函数。因为虚函数调用要靠特定的对象类决定该激活哪一个函数;(6)内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的即使虚函数在类内部定义,编译时将其看作非内联;(7)构造函数不能是虚函数,但析构函数可以是虚函数;