现在的位置: 首页 > 综合 > 正文

C++多态与虚函数

2018年05月18日 ⁄ 综合 ⁄ 共 3453字 ⁄ 字号 评论关闭

多态(polymorphism)

多态(polymorphism)可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。

C++多态性表现为:同一个函数的调用,在不同的运行环境中响应的代码不同,他支持两种多态性:编译时多态和运行时多态。

 

多态性/特征 实现方式 绑定(联编)方式 绑定时间

编译时多态 函数重载、模板等 静态绑定 程序编译时

运行时多态 虚函数与继承 动态绑定 程序运行时

 

封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。多态性是将接口与实现进行分离。也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。因为没有多态性,函数调用的地址将是一定的,而固定的地址将始终调用到同一个函数,这就无法实现一个接口,多种方法的目的了。

 

虚函数

虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态覆盖(Override

 

虚函数的定义与派生类中的重定义方式

class 类名{

public:

       virtual 成员函数说明;

}

 

class 类名:基类名{

   public:

          virtual 成员函数说明;

}

 

虚函数在不同的派生类中可能存在不同的实现,通过重载基类的虚函数,可以生成特定的派生类版本,如果派生类中无重载该虚函数,则使用基类版本,而且无论虚函数重定义是否使用virtual关键字,都还是虚函数。虚函数可以是友元函数但不能是静态成员。

 

用虚函数实现动态连接在编译期间,C++编译器根据程序传递给函数的参数或者函数返回类型来决定程序使用那个函数,然后编译器用正确的的函数替换每次启动。这种基于编译器的替换被称为静态链接,他们在程序运行之前执行。另一方面,当程序执行多态性时,替换是在程序执行期进行的,这种运行期间替换被称为动态连接。

 

来看一段简单的代码

class A{

public:

void print(){ cout<<This is A<<endl;}

 

};

 

class B : public A{

public:

void print(){ cout<<This is B<<endl;}

};

 

int main(){  

A a;

B b;

a.print();

b.print();

}

 

通过class Aclass Bprint()这个接口,可以看出这两个class因个体的差异而采用了不同的策略,输出的结果分别是This is AThis is B。但这是否真正做到了多态性呢?No,多态还有个关键之处就是一切用指向基类的指针或引用来操作对象。

 

现在就把main()处的代码改一改。

int main(){  

A a;

B b;

A* p1 = &a;

A* p2 = &b;

p1->print();

p2->print();

}

结果是两个This is A

p2明明指向的是class B的对象但却是调用的class Aprint()函数。这是因为当使用基类指针(或引用)调用实函数时,c++将选择该函数的基类版本调用,而如果使用的是派生类指针或引用调用实函数时,c++就会调用该函数的派生类版本。

 

下面将代码修改一下,把函数改成虚函数:

#include<iostream>

using namespace std;

 

class A

{

public:

void print()

cout<< "This is A" <<endl;

}

virtual void vprint()

cout<<"This is vA"<<endl;

}

};

class B : public A{

public:

void print()

cout<< "This is B" <<endl;

}

void vprint()

cout<< "This is vB" <<endl;

}

};

int main(){  

A a;

B b;

A* p = &a;

p->print();

p->vprint();

p = &b;

p->print();

p->vprint();

return 0;

}

 

class A的成员函数vprint()已经成了虚函数,那么class Bvprint()也成了虚函数。我们只需在把基类的成员函数设为virtual,其派生类的相应的函数也会自动变为虚函数。所以,class Bprint()也成了虚函数。

 

输出的结果:

This is A

This is vA

This is A

This is vB

 

p = &a本身是基类指针,指向的又是基类对象,调用的都是基类本身的函数。p = &b则是基类指针指向子类对象,体现的是多态的用法。p->print()由于指针是个基类指针,指向是一个固定偏移量的函数,因此此时指向的就只能是基类的print()函数的代码了。而p->vprint()指针是基类指针,指向的vprint是一个虚函数,由于每个虚函数都有一个虚函数列表,此时p调用vprint()并不是直接调用函数,而是通过虚函数列表找到相应的函数的地址,因此根据指向的对象不同,函数地址也将不同,这里将找到对应的子类的vprint()函数的地址。

 

总的来说就是,指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。

 

先讲虚函数表的概念:

类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址

注意的是,编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。类的每个虚成员占据虚函数表中的一行。如果类中有N个虚函数,那么其虚函数表将有N*4字节的大小。

虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

 

虚函数是如何做到因对象的不同而调用其相应的函数的呢?现在我们就来剖析虚函数

 

Class A{
  public:
 virtual void fun(){cout<<1<<endl;} 
       virtual void fun2(){cout<<2<<endl;} 
}; 
class B : public A{
 public:
 void fun(){cout<<3<<endl;}
 void fun2(){cout<<4<<endl;} 
};

 

由于这两个类中有虚函数存在,所以编译器就会为他们两个分别插入vptr指针,并为他们分别创建虚函数表。每个类都有自己的虚函数表,虚函数表的作用就是保存自己类中虚函数的地址,我们可以把虚函数表形象地看成一个数组,这个数组的每个元素存放的就是虚函数的地址,请看图

 

可以看到这两个vtbl分别为class Aclass B服务。现在有了这个模型之后,我们来分析下面的代码

A *p=new A;

p->fun();

毫无疑问,调用了A::fun(),但是A::fun()是如何被调用的呢?首先取出vptr的值,这个值就是vtbl的地址,再根据这个值来到vtbl这里,由于调用的函数A::fun()是第一个虚函数,所以取出vtbl第一个 slot里的值,这个值就是A::fun()的地址了,最后调用这个函数。现在我们可以看出来了,只要vptr不同,指向的vtbl就不同,而不同的 vtbl里装着对应类的虚函数地址,所以这样虚函数就可以完成它的任务。

抱歉!评论已关闭.