新普京网站-澳门新普京 > 前端 > 澳门新普京:虚函数和虚继承浅析,虚函数与虚继承小结

澳门新普京:虚函数和虚继承浅析,虚函数与虚继承小结

2019/12/29 21:07

本文针对C++里的虚函数,虚世袭表现和法则进行一些大致深入分析,有畸形的地点请提议。上面都以以VC贰零壹零编写翻译器对这三种机制内部落到实处为例。

   
  虚函数的功力正是落实多态性,通过指向派生类的基类指针或引用,访谈派生类中同名覆盖成员函数;落成情势正是在函数重回值早先增加关键字“virtual”;如下:  

虚函数

以下是百度百科对于虚函数的批注:

概念:在某基类中扬言为 virtual 并在二个或七个派生类中被另行定 义的成员函数[1]

语法:virtual 函数重临类型 函数名(参数表) { 函数体 }

用途达成多态性,通过指向派生类的基类指针,访谈派生类中同名覆盖成员函数

函数证明和定义和平时的类成员函数相仿,只是在再次回到值此前参预了严重性字“virtual”注脚为虚函数。而虚函数是促成多态的重要花招,意思是独有对虚函数的调用技艺动态调整调用哪一个函数,那是相对于日常成员函数来讲的,普通的成员函数在编写翻译阶段就会分明调用哪两个函数。举个栗子:

#include <stdio.h>

class A {
public:
    void fn() { printf("fn in An"); }
    virtual void v_fn() { printf("virtual fn in An"); }
};

class B : public A {
public:
    void fn() { printf("fn in Bn"); }
    virtual void v_fn() { printf("virtual fn in Bn"); }
};

int main() {
    A *a = new B();
    a->fn();
    a->v_fn();
    return 0;
}

基类A有三个分子函数fn和v_fn,派生类B世袭自基类A,相仿完毕了四个函数,然后在main函数中用A的指针指向B的实例(向上转型,也是贯彻多态的至关重要花招),然后分别调用fn和v_fn函数。结果是“fn in A”和”virtual fn in B”。那是因为fn是常常成员函数,它是因此类A的指针调用的,所以在编写翻译的时候就鲜明了调用A的fn函数。而v_fn是虚函数,编写翻译时无法鲜明,而是在运维时再通过有个别编制来调用指针所指向的实例(B的实例)中的v_fn函数。要是派生类B中尚无达成(完全意气风发致,不是重载)v_fn这几个函数,那么照旧会调用基类类A中的v_fn;假使它实现了,就可以说派生类B覆盖了基类A中的v_fn那么些虚函数。那就是虚函数的表现和利用,唯有因而虚函数,才干促成面向对象语言中的多态性。

如上只是虚函数的表现和用项,上边来搜求它的兑现机制。在此以前,先来看八个主题素材,依然以上的代码,基类A的深浅为多少,也便是“printf(“%dn”, sizeof(AState of Qatar卡塔尔(قطر‎;”的输出会是稍微啊?A中二个成员变量都未曾,有人大概会说是0。额,0是纯属大谬不然的,因为在C++中,即时是空类,它的朗朗上口也为1,这是此外的话题,不在本文探讨。当然1也是分外的,实际结果是4(三十几人系统),4正巧是二个int,叁个指针(三十几人)的深浅,派生类B的轻重相仿为4。那八个字节和落到实处多态,虚函数的体制有着很关键的涉及。

实际上用VC2010调护诊疗上面代码的时候,就能开采指针a所针没有错实力中有三个成员常量(const),它的名字称为vftable,全称大概叫做virtual function table(虚函数表)。它实质上指向了四个数组,数组里面保存的是意气风发层层函数指针,而地点的次序中,那么些表独有生龙活虎项,它正是派生类B中的v_fn函数入口地址。假如大家用三个A的指针指向四个A的实例呢?它风华正茂律有一个vftable,而它指向的表中也唯有生龙活虎项,那项保存的基类的v_fn函数入口地址。那用代码表示,就近似于上面那样:

void* vftable_of_A[] = {
    A::v_fn,
    ...
};

class A {
    const void* vftable = vftable_of_A;
    virtual void v_fn() {}
};

void* vftable_of_B[] = {
    B::v_fn,
    ...
};

class B {
    const void *vftable = vftable_of_B;
    vritual void v_fn() {}
};

地方vftable的项目之所以用void*代表,实际上叁个类中有所虚函数的地点都被停放这几个表中,不一样虚函数对应的函数指针类型不尽相像,所以这一个表用C++的门类不佳表述,不过在机器级里都以进口地址,即二个叁拾一人的数字(叁九人系统),等到调用时,因为编写翻译器预先领悟了函数的参数类型,重返值等,能够活动做好管理。

如此那般大家就能够越来越好的精晓虚函数和多态了。第一个代码中,a指针即便是A*品类的,不过它却调用了B中的v_fn,因为无论是是A类,依然A的基类,都会有二个变量vftable,它指向的虚函数表中保存了不错的v_fn入口。所以a->v_fn(卡塔尔实际做的劳作便是从a指向的实例中抽取vftable的值,然后找到虚函数表,再从表中去的v_fn的入口,进行调用。不管a是指向A的实例,照旧指向B的实例,a->fn(卡塔尔所做的步骤都以上边说的意气风发致,只是A的实例和B的实例有着差异的虚函数表,虚函数表里也保留着恐怕两样的虚函数入口,所以最后将跻身分裂的函数调用中。通过表来达到不用判定项目,亦可达成多态的效果。还应该有少数指的指示的是,因为虚函数表是二个常量表,在编写翻译时,编写翻译器会自动生成,并且不会转移,所以如果有八个B类的实例,每一种实例中都会有一个vftable指针,不过它们照准的是同叁个虚函数表。

下面生龙活虎段中说起了,A和B的实例有着不一样的虚函数表,然而虚函数表中只是或许保留着分化的v_fn,这是因为C++允许派生类不隐讳基类中的虚函数,意思就是只要派生类B中尚无兑现v_fn那么些函数(不是重载),那么B的实例的虚函数表会保存着基类A中v_fn的进口地址。也正是说B类不贯彻v_fn函数,可是它生机勃勃律提供了这么些接口,实际上是调用基类A中的v_fn。假设某些类只是一个抽象类,抽象出一些列接口,不过又不能完成这么些接口,而要有派生类来达成,那么就足以把那一个接口表明为纯虚函数,包罗有纯虚函数的类称为抽象类。纯虚函数是风流罗曼蒂克类特别的虚函数,它的扬言方式如下:

class A {

public:

  virtual 返回值 函数名(参数表)= 0;

};

在虚函数注明方式后加四个“=0”,况兼不提供达成。抽象类分歧意实例化(那样做编写翻译器会报错,因为有成员函数未达成,编译器不通晓怎么调用)。纯虚函数的兑现机制和虚函数相符,只是供给派生类类必需自身实现一个(也能够不完成,可是派生类也会是个抽象类,无法实例化)。

顺带提一下,java中的每叁个成员函数都得以以明白为C++中的virtual函数,不用显式申明都足以兑现重载,多态。而java的接口相似于C++中的抽象类,需求完结里面包车型大巴接口。

#include <stdio.h>
class A {
public:
    void fn() { printf("fn in An"); }
    virtual void v_fn() { printf("virtual fn in An"); }
};
class B : public A {
public:
    void fn() { printf("fn in Bn"); }
    virtual void v_fn() { printf("virtual fn in Bn"); }
};
int main() {
    A *a = new B();
    a->fn();
    a->v_fn();
    return 0;
}

虚继承

C++援助多种继承,那和现实生活很相通,任何二个实体都不可能单豆蔻梢头的归于某多少个体系。如同马,第风流倜傥想开的正是它派生自动物这么些基类,然而它在某系地点可不得以说也派生自交通工具这二个基类呢?所以C++的千门万户世袭很有用,可是又引进了二个标题(专门的学业术语叫做菱形继承?)。动物和畅行工具都以从最根本的基类——“事物”继续而来,事物包含了两个最核心的质量,体量和质量。那么动物和畅行工具都保留了基类成员变量——体量和品质的别本。而马有世襲了那多少个类,那么马就有两份体量和品质,这是不创立的,编写翻译器不可能明显使用哪二个,所以就能报错。JAVA中海市蜃楼这么的标题,因为JAVA不容多数种世袭,它只大概落成四个接口,而接口里面只含有部分函数注明不带有成员变量,所以也空头支票这里样的标题。

本条标题用实际代码表述如下所示:

class A {
public:
    int a;
};

class B : public A {
};

class C : public A {
};

class D : public B, public C {
};

int main() {
    D d;
    d.a = 1;
    return 0;
}

这些代码会报错,因为d中保存了两份A的副本,即有三个成员变量a,平日不会报错,可是若是对D中的a使用,就能报二个“对a的拜望不明了”。虚世襲就能够化解这些主题素材。在搜求虚函数在此之前,先来二个sizeof的标题。

#include <stdio.h>

class A {
public:
    int a;
};

class B : virtual public A {
};

int main() {
    printf("%dn", sizeof(B));
    return 0;
}

B的深浅是?首先回答0的是纯属错的,理由作者前面都在说了。1也是错的,不解释。4也是错的,借使B不是虚世袭自A的,那么4正是对的。精确答案是8,B虚世袭A驾驭后,比预期中的多了4个字节,那是怎么回事呢?这些通过调节和测量试验是看不出来的,因为看不到相通于vftable的分子变量(实际上编写翻译器生成了二个周边的事物,然则调节和测量试验时看不到,但是在观察反汇编的时候,能够看来vbtable的字样,应该是virtual base table的野趣)。

虚继承的提议就是为领悟决多种世襲时,可能会保留两份别本的难题,也等于说用了虚世襲就只保留了豆蔻梢头份别本,不过那一个别本是被多种世袭的基类所分享的,该怎么贯彻这些机制吗?编写翻译器会在派生类B的实例中保存八个A的实例,然后在B中参预二个变量,那么些变量是A的实例在实际B实例中的偏移量,实际上B中并不直接保存offset的值,而是保存的两个指针,那么些指针指向三个表vbtable,vbtable表中保留着全部虚世襲的基类在实例中的offset值,多少个B的实例分享这一个表,每一个实例有个单身的指针指向这几个表,那样就很好领悟为何多了4个字节了。用代码表示仿佛上面那样。

class A {
public:
    ...
};

int vbtable_of_B[] = {
  offset(B::_a),
    ...
};

class B :virtual public A{
private:
    const int* vbtable = vbtable_of_B;
    A _a;
};

每三个A的虚派生类,都会有和好的vbtable表,这么些派生类的具有实例分享那一个表,然后各类实例各自笔者保护存了贰个对准vbtable表的指针。即便还应该有三个类C虚世襲了A,那么编写翻译器就能为它自动生成七个vbtable_of_C的表,然后C的实例都会有一个针对性这些vbtable表的指针。

假如有生龙活虎种类的虚世袭会发生什么状态,如同上边这段代码同样:

#include <stdio.h>

class A {
public:
    int a;
};

class B : virtual public A {
public:
  int b;
};

class C : virtual public B {
};

int main() {
    printf("%dn", sizeof(C));
    return 0; 
}

程序运行的结果是16,根据事情未发生前的论战,大约会如此想。基类A里有1个变量,4个字节。B类虚世袭了A,所以它有二个A的别本和一个vbtable,还应该有温馨的三个变量,那便是12字节。然后C类又虚世袭了B类,那么它有二个B的别本,三个vbtable,16字节。但实在通过调治和反汇编开掘,C中保留分别保存了A和B的别本(不包涵B类的vbtable),8字节。然后有叁个vbtable指针,4字节,表里面富含了A别本和B别本的偏移量。最后还应该有一个不行的4字节(?),意气风发共16字节。不唯有是那般,每经过后生可畏层的虚世袭,便会多出4字节。这一个多出去的四字节在反汇编中没开掘实际上用场,所以那个有待探寻,不管是编写翻译器相当不足智能,依然有待其余成效,虚世襲和体系世袭都应有小心使用。

仍然以上面的例子,假设C类是平昔接轨B类,并不是行使虚继承,那么C类的分寸为12字节。它里面是平素保存了A和B的别本(不含有B的vbtable),然后还应该有一个谈得来的vbtable指针,所以黄金时代共12字节,未有了上风姿洒脱段所说的最终的4个字节。

可是就算想上面风流倜傥种持续,会是怎么样处境?

#include <stdio.h>

class A {
public:
    int a;
};

class B : virtual public A {
};

class C : virtual public A {
};

class D : public B, public C{
};

int main() {
    printf("%dn", sizeof(D));
    return 0; 
}

D从B,C类派生出来,而B和C又相同的时候虚世袭了A。输出的构造是12,实际调节和测量试验反汇编的时候开掘,D中世袭了B和C的vbtable,这正是8字节,而还要还保留了一个A的别本,4字节,总共12字节。它和方面包车型客车数不完虚世袭例子里的12字节是不均等的。从前多个例证中独有三个vbtable,四个A的实例,末尾还可能有五个茫然的4字节。而以此例子中是有多个仅挨着的vbtable(都有效)和二个A的实例。

 

出口结果为:澳门新普京 1

  基类A有多个成员函数fn和v_fn,派生类B世袭自基类A,相近完成了三个函数,然后在main函数中用A的指针指向B的实例(向上转型,也是兑现多态的必要花招),然后分别调用fn和v_fn函数。结果是“fn in A”和”virtual fn in B”。

  那是因为fn是常常成员函数,它是透过类A的指针调用的,所以在编写翻译的时候就鲜明了调用A的fn函数。而v_fn是虚函数,编写翻译时不能够明确,而是在运维时再通过某些建制来调用指针所指向的实例(B的实例)中的v_fn函数。假诺派生类B中绝非贯彻(完全等同,不是重载)v_fn这几个函数,那么照旧会调用基类类A中的v_fn;固然它完毕了,就可以说派生类B覆盖了基类A中的v_fn这一个虚函数。这便是虚函数的变现和行使,独有由此虚函数,工夫兑现面向对象语言中的多态性。

  以上只是虚函数的突显和用场,上边来查究它的实现机制。早前,先来看叁个难题,依然以上的代码,基类A的尺寸为多少,也正是“printf(“%dn”, sizeof(A卡塔尔国State of Qatar;”的输出会是微微啊?A中三个成员变量都未有,有人恐怕会说是0。额,0是纯属大谬不然的,因为在C++中,即时是空类,它的抑扬顿挫也为1,那是此外的话题,不在本文商量。当然1也是反常的,实际结果是4(三十三位系统),4适逢其会是二个int,三个指针(三十二位)的深浅,派生类B的轻重雷同为4。那五个字节和贯彻多态,虚函数的体制有着很入眼的涉及。

澳门新普京 2

  在有个别变量中发掘指针a所指向的实力中有一个成员常量(const),它的名字称为vftable,全称大约叫做virtual function table(虚函数表)。它实质上指向了三个数组,数组里面保存的是意气风发雨后春笋函数指针,而地点的主次中,那几个表独有意气风发项,它就是派生类B中的v_fn函数入口地址。即使大家用二个A的指针指向三个A的实例呢?它朝气蓬勃律有贰个vftable,而它指向的表中也唯有生机勃勃项,那项保存的基类的v_fn函数入口地址。那用代码表示,就就如于下边那样:

void* vftable_of_A[] = {
    A::v_fn,
    ...
};
class A {
    const void* vftable = vftable_of_A;
    virtual void v_fn() {}
};
void* vftable_of_B[] = {
    B::v_fn,
    ...
};
class B {
    const void *vftable = vftable_of_B;
    vritual void v_fn() {}
};
上一篇:11新特性之Lambda表达式 下一篇:读书笔记,编程风格指南