zoukankan      html  css  js  c++  java
  • 虚函数与虚继承

    1.什么是虚函数?

    定义:在某基类中声明为 virtual 并在一个或多个派生类中被重新定 义的成员函数。
    语法:virtual 函数返回类型 函数名(参数表) { 函数体 }
    用途实现多态性,通过指向派生类的基类指针,访问派生类中同名覆盖成员函数。
     
    纯虚函数:纯虚函数是一类特殊的虚函数,包含有纯虚函数的类称为抽象类。它的声明方式如下:

    class A {

    public:

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

    };

    在虚函数声明方式后加一个“=0”,并且不提供实现。抽象类不允许实例化(这样做编译器会报错,因为有成员函数没有实现,编译器不知道怎么调用)。纯虚函数的实现机制和虚函数类似,只是要求派生类类必须自己实现一个(也可以不实现,但是派生类也会是个抽象类,不能实例化)。

    顺带提一下,java中的每一个成员函数都可以以理解为C++中的virtual函数,不用显式声明都可以实现重载,多态。而java的接口类似于C++中的抽象类,需要实现里面的接口。

    2. 虚继承

    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("%d ", 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("%d ", 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("%d ", 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的实例。


     

  • 相关阅读:
    UIProgressView的详细使用
    Android拍照上传代码样例
    UILabel的详细使用及特殊效果
    TextView属性android:ellipsize实现跑马灯效果
    Android中WebView实现Javascript调用Java类方法
    有效获取状态栏(StatusBar)高度
    详解iPhone Tableview分批显示数据
    TextView显示插入的图片
    ObjectiveC语法快速参考
    UISegmentedControl的详细使用
  • 原文地址:https://www.cnblogs.com/lqwh/p/7227846.html
Copyright © 2011-2022 走看看