C++:多态浅析

1.多态

在C++中由两种多态性:

• 编译时的多态性:通过函数的重载运算符的重载来实现的

• 运行时的多态性:通过类继承关系虚函数来实现的

特别注意:

a.运行时的多态性是指程序执行前,无法根据函数名和函数的参数来确定调用哪一个函数,必须在程序执行的过程中,根据执行的具体情况来动态地确定。其目的是追求程序的通用性,建立一种通用的程序

b.运行时的多态,简而言之就是用父类型的指针或引用指向其子类的实例,然后通过父类的指针或引用调用实际子类的成员函数,从而使父类的指针或引用拥有“多种形态”。这是一种泛型技术(如:模版技术、RTTI技术),其目的是使用不变的代码来实现可变的算法

示例:

 1 #include<iostream>
 2 using namespace std;
 3 class Animal{  //基类 
 4 public:
 5     virtual void eat(){ //虚函数 
 6         cout<<"Animal eat"<<endl; 
 7     }
 8     virtual void sleep(){
 9         cout<<"Animal sleep"<<endl; 
10     }
11 }; 
12 
13 class Person:public Animal{ //子类1
14 public:
15     void eat(){
16         cout<<"Person eat"<<endl;
17     } 
18     void sleep(){
19         cout<<"Person sleep"<<endl;
20     }
21 }; 
22 
23 class Dog:public Animal{ //子类2
24 public:
25     void eat(){
26         cout<<"Dog eat"<<endl;
27     } 
28     void sleep(){
29         cout<<"Dog sleep"<<endl;
30     }
31 };
32 
33 class Bird:public Animal{ //子类3
34 public:
35     void eat(){
36         cout<<"Bird eat"<<endl;
37     } 
38     void sleep(){
39         cout<<"Bird sleep"<<endl;
40     }
41 };
42 
43 void func(Animal &a){ //函数,注意参数是基类的引用 
44     a.eat();
45     a.sleep();
46 }
47 
48 int main(){
49     Person p; //Person类的对象实例 
50     Dog d; //Dog类的对象实例 
51     Bird b; //Bird类的对象实例
52     func(p);
53     cout<<"-----------分界线---------------"<<endl; 
54     func(d);
55     cout<<"-----------分界线---------------"<<endl; 
56     func(b);
57     return 0;
58 } 

2.虚函数

2.1 虚函数的定义

虚函数是一个类的成员函数,它的定义语法如下:

语法:virtual 返回值类型 函数名(参数表);

特别注意:

a.当一个类的一个成员函数被定义为虚函数时,则由该类派生出来的所有派生类中,该函数始终保持虚函数的特征

b.当在派生类中重新定义虚函数时,不必加关键字virtual。但重新定义时不仅要求函数同名,而且要求函数的参数列表与返回值类型也必须和基类中的虚函数相同,否则编译器会报错

c.虚函数可以在先在类内进行声明,然后在类外定义。但在类内声明时需要在返回值类型之前加上关键字virtual,在类外定义时则不需要在添加关键字virtual

2.2 虚函数使用的注意事项

1.派生类中重定义虚函数时,虚函数的函数名必须与其基类中的虚函数的函数名相同,除此之外要求参数列表和函数的返回值类型也必须相同

[特例]:当基类中的虚函数的返回值类型是基类类型的指针时,允许在派生类中重定义该虚函数时将返回值类型改写为派生类类型的指针

 1 #include<iostream>
 2 using namespace std;
 3 class Animal{
 4 public:
 5     int value;
 6     Animal():value(0){}
 7     Animal(int v):value(v){}
 8     virtual Animal* show(){ //返回值类型是Animal类型的指针 
 9         cout<<"Animal类中的value值是:"<<value<<endl;
10         return this;
11     } 
12 };
13 
14 class Person:public Animal{ 
15 public:
16     int value;
17     Person():value(0){}
18     Person(int v):value(v){}
19     Person* show(){ //返回值类型是Person类型的指针  
20         cout<<"Person类中的value值是:"<<value<<endl;
21         return this; 
22     }
23 };
24 
25 int main(){
26     Animal animal(10);
27     Person person(20);
28     animal.show();
29     cout<<"------------分界线----------------"<<endl;
30     person.show();
31     return 0;
32 }

2.只有类中的成员函数才有资格成为虚函数。这是因为虚函数仅适用于有继承关系的类对象(建议成员函数尽可能地设置为虚函数)

3.类中的静态成员函数是该类所有对象所共有的,不受限于某个对象个体,因此不能作为虚函数

4.内联(inline)函数不能是虚函数,因为内联函数不能在运行中动态确定位置。即使虚函数在类的内部定义,但是在编译的时候系统仍然将它看做是非内联的

5.类中的析构函数可以作为虚函数,但构造函数不能作为虚函数。这是因为在调用构造函数时对象还没有完成实例化,而调用析构函数时对象已经完成了实例化

[注]:在基类中及其派生类中都动态分配内存空间时,必须把析构函数定义为虚函数,从而实现“销毁”对象时的多态性。例如在C++中用new运算符建立临时对象时,若基类中有析构函数并且同时定义了一个指向该基类的指针变量,但指针变量指向的对象却是该基类的派生类对象,那么在程序执行delete操作时,系统只会执行基类中的析构函数,而不会执行派生类中的析构函数,从而造成内存泄漏

 1 #include<iostream>
 2 using namespace std;
 3 class Father{
 4 public:
 5     Father()=default;
 6     ~Father(){
 7         cout<<"调用Father类的析构函数"<<endl;
 8     }
 9 };
10 class Son:public Father{
11 public:
12     Son()=default;
13     ~Son(){
14         cout<<"调用Son类的析构函数"<<endl;
15     }
16 };
17 
18 int main(){
19     Father* ptr=new Son;
20     delete ptr;
21     return 0;
22 }

这是因为这里的指针本质上指向的其实是派生类对象中隐藏包含的基类子对象。将基类的析构函数定义为虚函数可以解决这个问题:

 1 #include<iostream>
 2 using namespace std;
 3 class Father{
 4 public:
 5     Father()=default;
 6     virtual ~Father(){
 7         cout<<"调用Father类的析构函数"<<endl;
 8     }
 9 };
10 class Son:public Father{
11 public:
12     Son()=default;
13     ~Son(){
14         cout<<"调用Son类的析构函数"<<endl;
15     }
16 };
17 
18 int main(){
19     Father* ptr=new Son;
20     delete ptr;
21     return 0;
22 }

当基类中的析构函数为虚函数时,无论指针指向的是同一类族中的哪一个对象,系统都会采用动态关联,调用相应的析构函数,对该对象进行清理工作。因此最好将基类中的析构函数声明为虚函数,这将使该基类的所有派生类的析构函数自动成为虚函数

6.使用虚函数会使程序的运行速度降低,这是因为为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现,但程序的通用性会变得更高

7.如果虚函数的定义放在类外,virtual关键字只加在函数的声明的前面,不能再添加在函数定义的前面。正确的定义必须不包括关键字virtual

2.3 虚表(参考来源:http://www.xuebuyuan.com/1485876.html

2.3.1 什么是虚表

在C++中,虚函数是通过虚表(Virtual Table)来实现的虚表本质上是一个类的虚函数的地址表,它解决了继承、覆盖的问题,保证其容量真实反映实际的函数。而对虚表的利用,往往需要通过指向虚表的指针来实现在C++中,编译器必须保证虚表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量),这样我们便可以通过对象实例的地址得到虚表,然后利用指向虚表的指针遍历虚表中的函数指针,并调用相应的函数

示例:

 1 #include<iostream>
 2 using namespace std;
 3 class Father{
 4 public:
 5     virtual void show(){ //虚函数1 
 6         cout<<"调用Father类的成员方法show()"<<endl;
 7     }
 8     virtual void func(){ //虚函数2 
 9         cout<<"调用Father类的成员方法func()"<<endl; 
10     }
11     void print(){ //普通成员函数 
12         cout<<"调用Father类的成员方法print()"<<endl;
13     } 
14 };
15 
16 class Son:public Father{ 
17 public:
18     void show(){ //虚函数1 
19         cout<<"调用Son类的成员方法show()"<<endl;
20     }
21     void func(){ //虚函数2 
22         cout<<"调用Son类的成员方法func()"<<endl; 
23     }
24     virtual void list(){ //虚函数3 
25         cout<<"调用Son类的成员方法list()"<<endl;
26     }
27 };
28 
29 int main(){
30     Father father;
31     father.show();
32     father.func();
33     father.print();
34     cout<<"-----------分界线------------"<<endl;
35     Son son;
36     son.show();
37     son.func();
38     son.print();
39     son.list();
40     return 0;
41 }

图解说明:

当Father类创建对象father后,其内存分布大致如下:

Father类中的成员函数print()是普通成员函数,因此其不再虚表中。

当Son类创建对象son后,其内存分布大致如下:

2.3.2 虚表的四种情况

2.3.2.1 一般继承(无虚函数覆盖)

如上图所示,在这个继承关系中,子类Derive没有重定义任何父类Base的虚函数,而是在其继承父类Base的基础上添加了三个新的虚函数,其代码主要如下(摘要):

 1 class Base{//基类
 2 public:
 3     virtual void f();
 4     virtual void g();
 5     virtual void h(); 
 6 }; 
 7 
 8 class Derive{ //子类
 9 public:
10     virtual void f1();
11     virtual void g1();
12     virtual void h1(); 
13 };

此时对于子类的实例(Derive d;)来说,其虚表结构大致如下:

从中我们可以看出:

• 虚函数按照其声明顺序放于虚表中

• 父类的虚函数在子类的虚函数的前面

 2.3.2.2 一般继承(由虚函数覆盖)

如上图所示,在这个继承关系中,子类Derive重定义了部分父类Base的虚函数,其代码主要如下(摘要):

 1 class Base{//基类
 2 public:
 3     virtual void f();
 4     virtual void g();
 5     virtual void h(); 
 6 }; 
 7 
 8 class Derive{ //子类
 9 public:
10     void f(); //重定义基类中的虚函数
11     virtual void g1();
12     virtual void h1(); 
13 };

此时对于子类的实例(Derive d;)来说,其虚表结构大致如下:

从中我们可以看出:

• 派生类中重定义的虚函数(如void f())会被放到虚表中原来父类虚函数的位置

• 没有被重定义的虚函数保持原样

[注]:正因如此,当程序执行语句:

1 Base *b=new Derive();
2 b->f();

由于虚表中Base::f()的位置已经被Derive::f()函数地址所取代,因此指针b此时调用的函数f()是Derive::f(),而不是Base::f()

2.3.2.3 多重继承(无虚函数覆盖)

如上图所示,在这个继承关系中,子类Derive没有重定义任何父类中的虚函数,而是在其继承所有父类的基础上添加了两个新的虚函数,其代码主要如下(摘要):

 1 class Base1{//基类1
 2 public:
 3     virtual void f();
 4     virtual void g();
 5     virtual void h(); 
 6 }; 
 7 
 8 class Base2{//基类2
 9 public:
10     virtual void f();
11     virtual void g();
12     virtual void h(); 
13 };
14  
15 class Base3{//基类3
16 public:
17     virtual void f();
18     virtual void g();
19     virtual void h(); 
20 }; 
21 
22 class Derive{ //子类
23 public:
24     virtual void f1();
25     virtual void g1();
26 };

此时对于子类的实例(Derive d;)来说,其虚表结构大致如下:

从中我们可以看出:

• 每一个父类都有自己的虚表

• 子类中新增加的虚函数会被添加在第一个父类的虚表的后面(所谓的第一个父类时按照声明的顺序来判断的),这样做的目的是为了使不同父类类型的指针在指向同一个子类的实例时都能够调用到实际的函数

 

2.3.2.4 多重继承(由虚函数覆盖)

如上图所示,在这个继承关系中,子类Derive重定义了部分父类中的虚函数,其代码主要如下(摘要):

 1 class Base1{//基类1
 2 public:
 3     virtual void f();
 4     virtual void g();
 5     virtual void h(); 
 6 }; 
 7 
 8 class Base2{//基类2
 9 public:
10     virtual void f();
11     virtual void g();
12     virtual void h(); 
13 };
14  
15 class Base3{//基类3
16 public:
17     virtual void f();
18     virtual void g();
19     virtual void h(); 
20 }; 
21 
22 class Derive{ //子类
23 public:
24     void f();
25     virtual void g1();
26 };

此时对于子类的实例(Derive d;)来说,其虚表结构大致如下:

从中我们可以看到:

• 派生类中重定义虚函数(如void f())时,其所有父类的虚表中的相应位置都会被替换

• 没有被重定义的虚函数保持原样

3.纯虚函数

3.1 什么时纯虚函数

纯虚函数(pure virtual function)是指被标明为不具体实现的虚拟成员函数。通常情况下,纯虚函数常用在这种情况:定义一个基类时,基类中虚函数的具体实现由于必须依赖派生类的具体情况从而无法在基类中确切定义,此时可以把这个虚函数定义为纯虚函数

语法:virtual 返回值类型 函数名(参数表)=0;

3.2 使用纯虚函数的注意事项

含有纯虚函数的基类是不能用来定义对象的。纯虚函数没有实现部分,不能产生对象,所以含有纯虚函数的类时抽象类

定义纯虚函数时,不需要定义函数的实现部分(因为没有意义,即使定义了函数的实现部分,编译器也不会对这部分代码进行编译)

• “=0”表明程序员将不定义该函数,函数声明是为派生类保留一个位置。“=0”的本质是将指向函数体的指针定位NULL

派生类必须重定义基类中的所有纯虚函数,少一个都不行,否则派生类中由于仍包含纯虚函数(从基类中继承而来),系统会仍将该派生类当成一个抽象类而不允许其实例化对象

示例:

 1 #include<iostream>
 2 using namespace std;
 3 class Animal{ //基类,抽象类
 4 public:
 5     virtual void eat()=0; //纯虚函数 
 6     virtual void sleep()=0; 
 7 };
 8 
 9 class Person:public Animal{ //子类1
10 public:
11     void eat(){
12         cout<<"Person eat"<<endl;
13     } 
14     void sleep(){
15         cout<<"Person sleep"<<endl;
16     }
17 }; 
18 
19 class Dog:public Animal{ //子类2
20 public:
21     void eat(){
22         cout<<"Dog eat"<<endl; 
23     } 
24     void sleep(){
25         cout<<"Dog eat"<<endl;
26     }
27 };
28 
29 void func(Animal &a){
30     a.eat();
31     a.sleep();
32 }
33 int main(){
34     Person person;
35     func(person);
36     cout<<"------分界线-----------"<<endl;
37     Dog dog;
38     func(dog);
39     return 0;
40 }

原文地址:https://www.cnblogs.com/duwenxing/p/7500281.html