多态性之编译期多态和运行期多态(C++版)

多态性之编译期多态和运行期多态(C++版)


    C++中最为经典的就是多态性,多态性充分体现了面向对象的思想,并且是C++与C的最大区别之一。多态性分为编译期多态和运行期多态,也称为静态多态和动态多态,有些人也称其为编译时多态和运行时多态,不管什么称呼,万变不离其宗,一个是编译期的静态的多态,一个是运行期的动态的多态,那么它们在C++中分别体现在哪里呢?又有什么区别呢?下面将详细介绍(重点是运行期多态)。

    1. 编译期多态(静态多态)


    编译期多态,正如其名,就是在编译期确定的一种多态性。这个在C++中主要体现在函数模板,这里需要注意的是函数重载和多态无关,很多地方把函数重载也误认为是编译期多态,这是错误的。

     那么函数模板是如何体现编译期多态的呢?下面举一个简单的例子就可以明白。

[cpp]  view plain  copy
  1. // 例1:函数模板体现出编译期多态  
  2. #include <iostream>  
  3.   
  4. template <typename T>  
  5. T add(T a, T b)  
  6. {  
  7.     T c = a + b;  
  8.     return c;  
  9. }  
  10.   
  11. int main()  
  12. {  
  13.     int i1 = 1;  
  14.     int i2 = 2;  
  15.     int iResult = 0;  
  16.   
  17.     iResult = add(i1, i2);  
  18.     std::cout << "The result of integer is " << iResult << std::endl;  
  19.   
  20.     double d1 = 1.1;  
  21.     double d2 = 2.2;  
  22.     double dResult = 0;  
  23.   
  24.     dResult = add(d1, d2);  
  25.     std::cout << "The result of double  is " << dResult << std::endl;  
  26.   
  27.     return 0;  
  28. }  

    从例1中可以看到,我们定义了一个函数模板add,用来求两个数的和,这两个数的数据类型在使用时才知道。main函数中使用了两个int值的求和以及两个double值的求和,这里就体现了多态性,即在编译期,编译器根据一定的最佳匹配算法确定函数模板的参数类型到底是什么,这就体现了编译期的多种状态。

    当说到多态性的时候一般都默认指运行期多态,所以编译期多态大家只要知道是如何表现的就可以了,下面重点来讨论运行期多态。    

    2. 运行期多态(动态多态)


    运行期多态主要是指在程序运行的时候,动态绑定所调用的函数,动态地找到了调用函数的入口地址,从而确定到底调用哪个函数。在C++中,运行期多态主要通过虚函数来实现,并且一定要有继承关系,下面举一个简单的例子来讲解。

[cpp]  view plain  copy
  1. // 例2:虚函数和继承关系体现运行期多态  
  2. #include <iostream>  
  3.   
  4. class parent  
  5. {  
  6. public:  
  7.     parent() {}  
  8.   
  9.     // 父类的虚函数  
  10.     virtual void eat()  
  11.     {  
  12.         std::cout << "Parent eat." << std::endl;  
  13.     }  
  14.   
  15.     // 注意这个并不是虚函数!!!  
  16.     void drink()  
  17.     {  
  18.         std::cout << "Parent drink." << std::endl;  
  19.     }  
  20. };  
  21.   
  22. class child : public parent  
  23. {  
  24. public:  
  25.     child () {}  
  26.   
  27.     // 子类重写了父类的虚函数  
  28.     void eat()  
  29.     {  
  30.         std::cout << "Child eat." << std::endl;  
  31.     }  
  32.   
  33.     // 子类覆盖了父类的函数,注意由于父类的这个函数  
  34.     // 并不是虚函数,所以不存在继承后重写的说法  
  35.     void drink()  
  36.     {  
  37.         std::cout << "Parent drink." << std::endl;  
  38.     }  
  39.   
  40.     // 子类特有的函数  
  41.     void childLove()  
  42.     {  
  43.         std::cout << "Child love playing." << std::endl;  
  44.     }  
  45. };  
  46.   
  47. int main()  
  48. {  
  49.     parent* pa = new child();  
  50.     pa->eat();       // 运行期多态的体现!!!  
  51.     pa->drink(); // 这里调用的还是父类的drink,所以并不是多态!!!  
  52.     // pa->childLove(); // 编译出错,父类的指针不能调用父类没有的函数  
  53.   
  54.     return 0;  
  55. }  
[cpp]  view plain  copy
  1. 运行结果:  
  2.   
  3. Child eat.  
  4. Parent drink.  

    例2写得比较完善,说明了很多问题,我们先来看主要的,即多态性的体现。注意,在C++中只能用指针或引用来实现多态,不能通过普通的对象来实现多态,例2中使用的是指针的形式。我们来仔细分析一下。

    第一,在我们的父类即parent中定义了一个虚函数(virtual关键字修饰的函数)---eat(),既然是虚函数,那么我们的子类就可以重写这个函数(注意这里强调是重写,而不是重载,也不是覆盖,重要的事情说三遍,是“重写!重写!重写!”)。我们的子类child中重写了eat()函数,至于父类和子类中的其他函数暂且先忽略,后面再讲解,这里只关注多态性相关的点。然后,在main函数中,我们定义了一个父类的指针parent,但是注意,我们虽然定义的是父类的指针,但是我们指向的是子类对象,即new的是child对象,这里涉及到向上转型,即将子类对象向上转型到了父类的指针所指。总之,一句话来说就是定义一个父类指针,指向子类对象,然后我们用父类的这个指针去调用eat()函数,这里就是多态发生的地方。从运行结果可以看到,实际上调用的是子类的eat()函数,并不是父类的eat()函数,这是因为虚函数,父类定义的是虚函数,而子类重写了这个函数,虽然我们定义的是父类指针,但是实际上指向的是子类对象,那么在运行期间,就会找到动态绑定到父类指针上的对象就是子类对象,然后实际上运行期间就是找到了子类对象中eat()函数的入口地址,然后调用了子类的eat()函数,这就是运行期多态。说了这么多,听起来十分绕口,貌似很难理解,但是大家仔细想想,总结成一句话就是:“定义父类指针并指向子类对象,此时用父类指针去调用一个特殊的函数,即父类中该函数是虚函数,而子类重写了这个虚函数,此时调用的这个函数就在运行期间动态地绑定到了指针实际所指的对象,即子类对象,从而去调用子类中的这个函数”。

    第二,说完了多态,我们来看看例2中其他需要注意的地方。我们发现在main函数中pa指针还调用了drink()函数,最终的运行结果显然调用的是父类的drink()函数,那么这里为什么没有多态呢?原因很简单,因为drink()函数不是虚函数,所以根本不存在多态这一特性,虽然父类和子类中都有drink()这个函数,但是子类仅仅是覆盖或者说隐藏了父类的drink()函数,并不是重写。而我们的指针是父类指针,所以必定要去调用父类的drink()函数。

    第三,我们来看看最后一点。细心的朋友会发现,在子类child中,有一个只有它有而父类没有的函数,即childLove()函数,该函数是子类特有的,和父类没有任何关系,所以在main函数中用pa指针去调用这个函数会出错,因为父类指针根本访问不到子类的这个函数。这也是多态性的一个缺陷,即父类的指针只能访问子类中重写了父类中的那些虚函数,而不能访问子类新增的特有的函数。

    最后我们来看一个纯虚函数实现多态性的例子。

[cpp]  view plain  copy
  1. // 例3:纯虚函数和继承关系体现运行期多态  
  2. #include <iostream>  
  3.   
  4. // 父类因为包含纯虚函数,所以该类是抽象类,即不能定义对象  
  5. class parent  
  6. {  
  7. public:  
  8.     parent() {}  
  9.   
  10.     // 父类的纯虚函数  
  11.     virtual void eat() = 0;  
  12. };  
  13.   
  14. class child : public parent  
  15. {  
  16. public:  
  17.     child () {}  
  18.   
  19.     // 子类重写了父类的纯虚函数  
  20.     void eat()  
  21.     {  
  22.         std::cout << "Child eat." << std::endl;  
  23.     }  
  24. };  
  25.   
  26. int main()  
  27. {  
  28.     // parent pa0;  // 编译期会出错,因为抽象类不能定义对象  
  29.   
  30.     // 注意抽象类虽然不能定义对象,但是可以定义指针用来指向具体类  
  31.     parent* pa = new child();  
  32.     pa->eat();       // 运行期多态的体现!!!  
  33.   
  34.     return 0;  
  35. }  

[cpp]  view plain  copy
  1. 运行结果:  
  2.   
  3. Child eat.  

    例3和例2基本上是一样的,省略了很多函数,这里多态性的体现和例2也是完全一样的,之所以举这个例子是为了解释下抽象类用来实现多态的一个理解误区。从例3中就可以看到,纯虚函数就是一个函数后面加上"=0",而没有任何实现,那么包含纯虚函数的类就自然成为了抽象类,而抽象类是不能定义对象的,这也就是为什么main函数中的第一行会出错。注意,继承抽象类的具体类必须实现抽象类中的纯虚函数,否则也会出错。这里想强调一个误区,很多人觉得既然抽象类不能定义对象,那么main函数中的parent *pa为什么没有出错,这是因为抽象类虽然不能定义对象,但是可以定义指针,用来指向具体类,从而实现多态,一定要分清楚C++中的指针、引用、对象三者之间的关系,不要一概而论。

    3. 总结


    多态性是面向对象中十分重要的一个特性,大家务必要掌握,虽然初学起来感觉很难理解,但是多思考下,看完例子后自己写一个多态性的例子就会清楚一些。平常我们所指的多态性一般都是说运行期多态,所以大家重点掌握运行期多态。本篇文章可能存在一些纰漏,欢迎大家指正,谢谢。

猜你喜欢

转载自blog.csdn.net/DP323/article/details/80281188