在C++中由两种多态性:html
• 编译时的多态性:经过函数的重载和运算符的重载来实现的ios
• 运行时的多态性:经过类继承关系和虚函数来实现的程序员
特别注意:算法
a.运行时的多态性是指程序执行前,没法根据函数名和函数的参数来肯定调用哪个函数,必须在程序执行的过程当中,根据执行的具体状况来动态地肯定。其目的是追求程序的通用性,创建一种通用的程序函数
b.运行时的多态,简而言之就是用父类型的指针或引用指向其子类的实例,而后经过父类的指针或引用调用实际子类的成员函数,从而使父类的指针或引用拥有“多种形态”。这是一种泛型技术(如:模版技术、RTTI技术),其目的是使用不变的代码来实现可变的算法this
示例:spa
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 }
虚函数是一个类的成员函数,它的定义语法以下:3d
语法:virtual 返回值类型 函数名(参数表);
特别注意:指针
a.当一个类的一个成员函数被定义为虚函数时,则由该类派生出来的全部派生类中,该函数始终保持虚函数的特征code
b.当在派生类中从新定义虚函数时,没必要加关键字virtual。但从新定义时不只要求函数同名,并且要求函数的参数列表与返回值类型也必须和基类中的虚函数相同,不然编译器会报错
c.虚函数能够在先在类内进行声明,而后在类外定义。但在类内声明时须要在返回值类型以前加上关键字virtual,在类外定义时则不须要在添加关键字virtual
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
在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后,其内存分布大体以下:
如上图所示,在这个继承关系中,子类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;)来讲,其虚表结构大体以下:
从中咱们能够看出:
• 虚函数按照其声明顺序放于虚表中
• 父类的虚函数在子类的虚函数的前面
如上图所示,在这个继承关系中,子类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()
如上图所示,在这个继承关系中,子类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;)来讲,其虚表结构大体以下:
从中咱们能够看出:
• 每个父类都有本身的虚表
• 子类中新增长的虚函数会被添加在第一个父类的虚表的后面(所谓的第一个父类时按照声明的顺序来判断的),这样作的目的是为了使不一样父类类型的指针在指向同一个子类的实例时都可以调用到实际的函数
如上图所示,在这个继承关系中,子类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())时,其全部父类的虚表中的相应位置都会被替换
• 没有被重定义的虚函数保持原样
纯虚函数(pure virtual function)是指被标明为不具体实现的虚拟成员函数。一般状况下,纯虚函数经常使用在这种状况:定义一个基类时,基类中虚函数的具体实现因为必须依赖派生类的具体状况从而没法在基类中确切定义,此时能够把这个虚函数定义为纯虚函数
语法:virtual 返回值类型 函数名(参数表)=0;
• 含有纯虚函数的基类是不能用来定义对象的。纯虚函数没有实现部分,不能产生对象,因此含有纯虚函数的类时抽象类
• 定义纯虚函数时,不须要定义函数的实现部分(由于没有意义,即便定义了函数的实现部分,编译器也不会对这部分代码进行编译)
• “=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 }