虚函数与多态性学习小结

   在C++中,多态有两种,一种是函数重载,一种是虚函数。函数重载发生在编译的时候,它的函数参数是不一样的。而虚函数是发生在运行的时候,它的函数原型是一样的,依靠的是指针的指向。

结果似乎和我们想象的不一样,既然Graph类(图形类)的对象graph指针分别指向了Rectangle类(矩形类)对象,Triangle类(三角类)对象,以及Circle类(圆类)对象,那么就应该执行它们自己所对应成员函数showArea(),怎么结果会是Graph类(图形类)的对象graph里的成员函数呢?这好像和我们在C++之继承与派生(2)一节里所讲到的派生类成员覆盖了基类中使用相同名称的成员(派生类对象调用同名成员函数是来自于自己类中成员函数,而非基类中上的)有所不同啊,其实当基类对象指针指向公有派生类的对象时,它只能访问从基类继承下来的成员,而不能访问派生类中定义的成员。但是使用动态指针就是为了表达一种动态调用的性质即当前指针指向哪个对象,就调用那个对象对应类的成员函数。那要怎么来解决的,这时虚函数就体现出了它的作用。其实我们只需要对上一个示例代码中所有的类里出现的showArea()函数声明之前加一个关键字virtual:

 


向对象程序设计中的多态性是指向不同的对象发送同一个消息,不同对象对应同一消息产生不同行为。在程序中消息就是调用函数,不同的行为就是指不同的实现方法,即执行不同的函数体。也可以这样说就是实现了“一个接口,多种方法”。

  从实现的角度来讲,多态可以分为两类:编译时的多态性和运行时的多态性。前者是通过静态联编来实现的,比如C++中通过函数的重载和运算符的重载。后者则是通过动态联编来实现的,在C++中运行时的多态性主要是通过虚函数来实现的,也正是今天我们要讲的主要内容。

  1.不过在说虚函数之前,我想先介绍一个有关于基类与派生类对象之间的复制兼容关系的内容。它也是之后学习虚函数的基础。我们有时候会把整型数据赋值给双精度类型的变量。在赋值之前,先把整形数据转换为双精度的,在把它赋值给双精度类型的变量。这种不同类型数据之间的自动转换和赋值,称为赋值兼容。同样的,在基类和派生类之间也存在着赋值兼容关系,它是指需要基类对象的任何地方都可以使用公有派生类对象来代替。为什么只有公有继承的才可以呢,因为在公有继承中派生类保留了基类中除了构造和析构之外的所有成员,基类的公有或保护成员的访问权限都按原样保留下来,在派生类外可以调用基类的公有函数来访问基类的私有成员。因此基类能实现的功能,派生类也可以。

  那么它们具体是如何体现的呢?(1)派生类对象直接向基类赋值,赋值效果,基类数据成员和派生类中数据成员的值相同;(2)派生类对象可以初始化基类对象引用;(3)派生类对象的地址可以赋给基类对象的指针;(4)函数形参是基类对象或基类对象的引用,在调用函数时,可以用派生类的对象作为实参;

#include
#include

class ABCBase
{
private:
 std::string ABC; 
public:
 ABCBase(std::string abc)
 {
 ABC=abc;
 }
void showABC();
};

void ABCBase::showABC()
{
 std::cout<<"字母ABC=>"<showABC();

function(x);

return 0;
}

 

虚函数与多态性学习小结_第1张图片

要注意的是:第一,在基类和派生类对象的赋值时,该派生类必须是公有继承的。第二,只允许派生类对象向基类对象赋值,反过来不允许;

  2.紧接着来讲一下虚函数,它允许函数调用与函数体之间的联系在运行时才建立,即在运行时才决定如何动作。虚函数声明的格式:

  virtual 返回类型 函数名(形参表)

  {

    函数体

  }

class 类名:基类名{

   public:

          virtual 成员函数说明;

}

 

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

 

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

来看一段简单的代码

class A{

public:

void print(){ cout<<”This is A”<

通过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

using namespace std;

 

class A

{

public:

void print()

{ 

cout<< "This is A" <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()函数的地址。

 

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

 

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

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

 

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

 

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

 

由于这两个类中有虚函数存在,所以编译器就会为他们两个分别插入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里装着对应类的虚函数地址,所以这样虚函数就可以完成它的任务。


你可能感兴趣的:(编程)