【总结】C++ 多态详解--------静态动态\虚函数与虚函数表(内存布局)

https://blog.csdn.net/lixungogogo/article/details/51138493(大牛总结)

可参考内存布局:https://blog.csdn.net/woalss/article/details/79060077

静态和动态

1.多态体现

这里写图片描述

2.静态动态联编

那么静态联编和动态联编分别是什么呢
联编的作用是:程序调用函数,编译器决定使用哪个可执行代码块。

也就是确定调用的具体对象。

静态联编其实就是类似上面我们提到的,函数重载和运算符重载,它是在编译过程汇总进行的联编,又称早期联编。
动态联编是在程序运行过程中才动态的确定操作对象。

3.静态动态类型

在C++Primer一书中,讲到了静态类型和动态类型。
  静态类型和动态类型可用于变量或表达式。
  表达式的静态类型在编译时总是已知的,它是在变量声明时的类型或表达式生成的类型。
  动态类型则是变量或表达式表示的内存中的对象的类型,直到运行时才可知。
  其实静态类型和动态类型与静态联编,动态联编是与指针和引用有着很大关系的。
  原因如下:
  实际上一个非指针非引用的变量,在声明时已经确定了它自己的类型,不会再后面改变。
  而指针或引用可以进行类型转换的原因,就是下面要好好分析的。
  下面我要问一些问题。
  1.什么有两种类型的联编?
  2.既然动态联编如此好,为什么不将他设置成默认的?
  3.动态联编是如何工作的?
  现在我对上面的问题解答一下。
 为什么有两种类型的联编以及为什么默认为静态联编?

  原因有两个——效率和概念模型。

       1.效率:为了使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开秀,所以,在派生类不需要重新定义基类方法的情况下,静态联编的效率更高

       2.内存和存取时间,这点在后面虚函数的介绍中会提及

 

4.指针和引用类型兼容性

继承详解——派生类成员函数详解(函数隐藏、构造函数与兼容覆盖规则)中,提到过赋值兼容覆盖规则,其实就是这里的指针和引用类型兼容性。
在C++中,动态联编与指针和引用调用的方法相关。其实从某种程度上说,这是由继承控制的。在公有继承中,建立is-a关系的一种方法是如何处理指向对象的指针和引用
一般情况下,C++是不允许将一种类型的地址赋给另一种类型的指针,也不允许一种类型的引用指向另一种类型。

赋值兼容转换规则中,指向基类的引用或指针可以引用派生类对象,而不必进行显示类型转换。

1.向上强制转换
  将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasting)

  这使得公有继承不需要进行显示类型转换。这也是is-a规则的一部分
  因为公有继承中是接口继承,即基类中的成员派生类中都有,所以发生向上强制转换的时候,势必担心出现问题的。
  将指向派生类对象的指针作为函数参数时,也是如此。

2.向下强制转换

  与向上强制转换相反,将基类指针或引用转换为派生类指针或引用成为向下强制转换。
  如果不使用显示类型转换,向下强制转换是不允许的,因为is-a关系是不可逆的。

    Fruit b;
    Banana d;
    Banana *pb = &b;//隐式向下强制转换
    //报错
    Banana *p = (Banana*)&b;//显式类型转换,不会报错

 

虚函数和虚函数表

1.虚函数定义

定义:实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法。

要说明的是:

  如果在基类中定义了虚函数,那么派生类中的同名函数将自动变为虚函数,但是我们可以在派生类同名函数前也加上virtual关键字,这样会增加程序的可读性。
总结:
  如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚拟的。这样程序将根据对象类型而不是引用或指针的类型来选择方法也就是函数的版本

注意:这里一定要注意什么时候用虚函数,必须是用指针或引用调用方法的时候用虚函数,因为如果是通过对象调用方法,那么编译的时候就知道应该用哪个方法了。

2.工作原理

C++规定了虚函数的行为,但将实现方法留给了编译器。

通常,编译器处理虚函数的方法是:
  给每个对象添加一个隐藏成员

  虚函数指针:隐藏成员中保存了一个指向函数地址数组的指针。

       虚指针是从父类继承而来(如图),在创建子类对象时先调用父类的构造函数,是虚指针指向父类的虚函数表;然后在调用子类自身的构造函数,再使虚指针指向子类的虚函数表。
  其实这里的函数地址数组指的就是虚函数表(virtual function table),vtbl。
  虚函数表中存储了为类对象进行声明的虚函数的地址。

  例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表派生类对象将包含一个指向独立地址表的指针。
  如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址,如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的地址。
  如果派生类定义了新的虚函数,则该函数的地址也将被添加到vtbl中,注意,无论类中包含的虚函数是一个还是多个,都只需要在对象中添加一个地址成员,只是表的大小不同。

1. 每个对象都将增大,增大量为存储地址的空间
2. 对每个类,编译器都创建一个虚函数的地址表
3. 每个函数调用都需要执行一步额外的操作,即到表中查找地址

虽然非虚函数的执行效率比虚函数较高,但不具备动态联编功能。

虚函数的注意事项
上面我们已经讨论了虚函数的一些要点,下面我们再来看一些虚函数相关的注意事项:

1.构造函数:构造函数不能是虚函数
2.析构函数:最好把基类的析构函数定义为虚函数;(如果没有定义为虚函数,当基类指针指向派生类,并且删除指针时,会析构基类而不会析构派生类,造成内存泄漏)
3.友元:友元不能是虚函数
因为友元不是类成员,而只有成员才能是虚函数。
4.重定义,隐藏和覆盖

接下来会分别详解重载隐藏覆盖内存布局

结合执行结果可以得到如下结论:
说明Derive的虚函数表结构跟上面分析的是一致的:
d对象的首地址就是vptr指针的地址-pvptr,(所以子类对象赋给父类时,首地址也就变成了子类的首地址)
取pvptr的值就是vptr-虚函数表的地址
取vptr中[0][1][2]的值就是这三个函数的地址
通过函数地址就直接可以运行三个虚函数了。
函数表中Base::g()函数指针被Derive中的Derive::g()函数指针覆盖, 所以执行的时候是调用的Derive::g()
这里写图片描述

生成时机:

  • 拥有虚函数的类会有一个虚表,而且这个虚表存放在类定义模块的数据段中。模块的数据段通常存放定义在该模块的全局数据和静态数据区,这样我们可以把虚表看作是模块的全局数据或者静态数据
  • 类的虚表会被这个类的所有对象所共享。类的对象可以有很多,但是他们的虚表指针都指向同一个虚表,从这个意义上说,我们可以把虚表简单理解为类的静态数据成员。值得注意的是,虽然虚表是共享的但是虚表指针并不是,类的每一个对象有一个属于它自己的虚表指针。

  • 虚表中存放的是虚函数的地址。
  • 虚表指针得生成是在对象生成时确定得:

类的非静态成员函数调用时,编译器会传入一个"隐藏"的参数。 这个参数就是通常我们说的"this"指针,它的值就是对象的地址。 在上面的代码中,寄存器 ECX 保存的就是这个"this" 指 针 , 同 时 它 的 值 又 赋 给 了 寄 存 器 EAX。"??_7CD-szBase@@6B@"就是上面提到的虚表,同时它也代表了虚表的地址

接下来,虚表的地址被赋给了由寄存器 EAX 指定的内存中。由此可见,虚表的地址被存放在对象的起始位置,即对象的第一个数据成员就是它的虚表指针。 同时我们还可以注意到,虚表指针的初始化确实发生在构造函数的调用过程中, 但是在执行构造函数体之前,即进入到构造函数的"{"和"}"之前。 为了更好的理解这一问题, 我们可以把构造函数的调用过程细分为两个阶段,即:

1.进入到构造函数体之间。在这个阶段如果存在虚函数的话,虚表指针被初始化。如果存在构造函数的初始化列表的话,初始化列表也会被执行。

2.进入到构造函数体内。这一阶段是我们通常意义上说的构造函数

这也就解释了以下两点:

1、为什么虚函数表指针的类型为 void * ?

答:上面vftable的类型之所以用 void * 表示,实际上一个类中所有虚函数的地址都被放到这个表中,不同虚函数对应的

函数指针类型不尽相同,所以这个表的类型无法确定,但是在机器级里都是入口地址,即一个32位的数字(32位系

统),等到调用时,因为编译器预先知道了函数的参数类型,返回值等,可以自动做好处理。

2、为什么虚函数表前要加 const ?

答:因为虚函数表是一个常量表,在编译时,编译器会自动生成,并且不会改变,所以如果有多个B类的实例,每个实

 

3.普通情况

class Base
{
public:
    int b;
    virtual void Fun1()
    {
        cout << "Base::Fun1()" << endl;
    }
    virtual void Fun2()
    {
        cout << "Base::Fun2()" << endl;
    }
};
class Derive: public Base
{
public:
    int d;  
};
int main()
{
    Base b;
    b.b = 1;
    Derive d;
    d.b = 1;
    d.d = 2;
    return 0;
}

编译器会给每个对象添加一个隐藏成员vptr,vptr中存储的是虚表地址,进入虚表中查看一下虚表中存储了什么:

基类:

这里写图片描述

可以看到虚表占了十二个字节,最后的四个字节均为0,其实这是编译器给虚表最后都会加四个字节的0,意义是NULL,表示虚表已经结束。

这里写图片描述

派生类:派生类仅仅是继承了基类的虚函数,没有自己重定义也没有自己新增函数。

后面的cccccccc是为了区分d和其他东西的不属于d

  由上面的图我们可以看到,派生类d在内存中是十二个字节,前四个字节依然是编译器给的vptr,后面紧跟的是基类成员,然后是自己新增的成员d,那么我们进入d的虚表看看。

这里写图片描述

这里写图片描述

 

4.函数重载

(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同(返回值不判断);
(4)virtual 关键字可有可无
相信对C++有一定了解的朋友都知道函数重载的条件是:
  在同一个作用域内

这里写图片描述

virtual关键字在函数重载中可有可无:只影响函数的地址存放位置(加virtual存放在虚函数表里,不加存放在成员函数里)

这里写图片描述

成员函数是单独存储的,所以编译器在存储成员函数那寻找函数即可

class Base
{
public:
    Base(int data = 0)
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    void B()
    {
        cout << "Base::B()" << endl;
    }
    virtual void B(int b)
    {
        cout << "Base::B(int)" << endl;
    }
    //B()与B(int b)构成了函数重载
    //因为上面两个函数是在同一作用域中
    int b;
};

一个加virtual:

这里写图片描述

两个都加virtual:

这里写图片描述

 

5.函数覆盖

什么是函数覆盖呢?
  覆盖是指派生类函数覆盖基类函数,特征是
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。

 其实函数覆盖分为两种情况:

a.对象调用函数的情况

  派生类对象调用的是派生类的覆盖函数
  基类的对象调用基类的函数

class Base
{
public:
    Base(int data = 1)
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    virtual void Test1()
    {
        cout << "Base::Test1()" << endl;
    }
    virtual void Test2()
    {
        cout << "Base::Test2()" << endl;
    }
    virtual void Test3()
    {
        cout << "Base::Test3()" << endl;
    }
    int b;
};
class Derive :public Base
{
public:
    Derive(int data = 2)
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test1()
    {
        cout << "Derive::Test1()" << endl;
    }   
    void Test2()
    {
        cout << "Derive::Test2()" << endl;
    }
    int d;
};

int main()
{
    Base b;
    b.Test1();
    b.Test2();
    b.Test3();
    Derive d;
    d.Test1();
    d.Test2();
    d.Test3();
    return 0;
}

这里写图片描述

这里写图片描述

在派生类中定义了的函数,在派生类虚函数表中将基类函数覆盖了,即派生类虚函数表中绿色的部分,而派生类没有定义的函数,即Test3(),基类和派生类的函数地址完全相同。

派生类中定义了同名同参数的函数后,发生了函数覆盖。
这就更清楚的看出了,派生类中定义了同名同参数的函数后,发生了函数覆盖。

b.指针或引用调用函数的情况

class Base
{
public:
    Base(int data = 1)
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    virtual void Test()
    {
        cout << "Base::Test()" << endl;
    }
    int b;
};
class Derive :public Base
{
public:
    Derive(int data = 2)
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test()
    {
        cout << "Derive::Test()" << endl;
    }
    int d;
};

int main()
{
    Base *pb;
    Derive d;
    pb = &d;
    pb->Test();
    return 0;
}

这里写图片描述 

由内存布局可以看出,指针pb指向的虚表就是派生类对象d所拥有的虚表,所以当然调用的是派生类已经覆盖了的函数。(也就是说将子类对象赋给父类指针时,虚函数指针也传递过去了)
  所以说:
  多态的本质:不是重载声明而是覆盖。
  虚函数调用方式:通过基类指针或引用,执行时会根据指针指向的对象的类,决定调用哪个函数。

6.函数隐藏(不需要内存分析)

经过上面的分析知道,在不同的类域定义不同参数的同名函数,是无法构成函数重载的。
  那么当我们这么做的时候,会发生什么呢。
  实际上,这种情况叫做函数隐藏。
“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下

(1)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
(2)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。

7.总结

  1.函数重载必须是在同一作用域的,在继承与多态这里,在基类与派生类之间是不能进行函数重载。
  2.函数覆盖是多态的本质

  在基类中的虚函数,在派生类定义一个同名同参数的函数,就可以用派生类新定义的函数对基类函数进行覆盖。
  3.函数隐藏是发生在基类和派生类之间的,当函数同名但是不同参数的时候,不论是不是虚函数,都会发生函数隐藏。

8.虚函数的打印

通过内存以及汇编代码中分析了虚函数表,现在我就再介绍一种查看虚函数表的方法, 即打印虚函数表
如果类中一旦有了虚函数,那么编译器会自动给类中加四个字节的数据,这四个字节为指向虚函数表的指针,存放的是虚函数表在内存中的地址。

https://blog.csdn.net/lixungogogo/article/details/51144759

9.多继承的虚函数表

a.不带函数覆盖
 

class Base1
{
public:
    Base1(int data = 1)
        :b1(data)
    {
        cout << "Base1()" << endl;
    }
    ~Base1()
    {
        cout << "~Base1()" << endl;
    }
    virtual void Test1()
    {
        cout << "Base1::Test1()" << endl;
    }
    virtual void Test2()
    {
        cout << "Base1::Test2()" << endl;
    }
    virtual void Test3()
    {
        cout << "Base1::Test3()" << endl;
    }
    int b1;
};
class Base2
{
public:
    Base2(int data = 2)
        :b2(data)
    {
        cout << "Base2()" << endl;
    }
    ~Base2()
    {
        cout << "~Base2()" << endl;
    }
    virtual void Test4()
    {
        cout << "Base2::Test4()" << endl;
    }
    virtual void Test5()
    {
        cout << "Base2::Test5()" << endl;
    }
    virtual void Test6()
    {
        cout << "Base2::Test6()" << endl;
    }
    int b2;
};
class Derive :public Base1,public Base2
{
public:
    Derive(int data = 3)
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    int d;
};

内存分布:有两张虚函数表与继承来的参数

这里写图片描述

b.带函数覆盖


class Base1
{
public:
    Base1(int data = 1)
        :b1(data)
    {
        cout << "Base1()" << endl;
    }
    ~Base1()
    {
        cout << "~Base1()" << endl;
    }
    virtual void Test1()
    {
        cout << "Base1::Test1()" << endl;
    }
    virtual void Test2()
    {
        cout << "Base1::Test2()" << endl;
    }
    virtual void Test3()
    {
        cout << "Base1::Test3()" << endl;
    }
    int b1;
};
class Base2
{
public:
    Base2(int data = 2)
        :b2(data)
    {
        cout << "Base2()" << endl;
    }
    ~Base2()
    {
        cout << "~Base2()" << endl;
    }
    virtual void Test1()
    {
        cout << "Base2::Test1()" << endl;
    }
    virtual void Test2()
    {
        cout << "Base2::Test2()" << endl;
    }
    virtual void Test3()
    {
        cout << "Base2::Test3()" << endl;
    }
    virtual void Test4()
    {
        cout << "Base2::Test4()" << endl;
    }
    virtual void Test5()
    {
        cout << "Base2::Test5()" << endl;
    }
    virtual void Test6()
    {
        cout << "Base2::Test6()" << endl;
    }
    int b2;
};
class Derive :public Base1,public Base2
{
public:
    Derive(int data = 3)
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test1()
    {
        cout << "Derive::Test1()" << endl;
    }   
    void Test4()
    {
        cout << "Derive::Test4()" << endl;
    }
    int d;
};

typedef void(*VTable)();//定义函数指针
void PrintDerive(Derive &b)
{
    VTable vtb = (VTable)(*((int *)*(int *)&b));
    //打印Derive中Base1的虚函数表
    //vtb就是函数的地址
    int i = 0;
    cout << "Vtable is " << endl;
    while (vtb != NULL)
    {
        cout << "NUM " << ++i << "Function " << endl;
        cout << "------->";
        vtb();
        vtb = (VTable)*(((int*)(*(int *)&b)) + i);
        //向后偏移四个字节
    }
    cout << "End" << endl;
    /*************************/
    //打印Derive中Base2的虚函数表
    vtb = (VTable)(*((int *)*((int *)&b+2)));
    //vtb就是函数的地址
     i = 0;
    cout << "Vtable is " << endl;
    while (vtb != NULL)
    {
        cout << "NUM " << ++i << "Function " << endl;
        cout << "------->";
        vtb();
        vtb = (VTable)*(((int*)(*((int *)&b+2))) + i);
        //向后偏移四个字节
    }
    cout << "End" << endl;
}
int main()
{
    Derive d;
    PrintDerive(d);
    return 0;
}

这里写图片描述

 由结果可以得到,Test1()不仅覆盖了Base1中的Test1()函数,也覆盖了Base2类中的Test1()函数。

纯虚函数

纯虚函数相当于定义了一个接口,不同的子类必须定义自己的实现。

纯虚函数的原理其实就是:纯虚函数在类的vftable表中对应的表项被赋值为0。也就是指向一个不存在的函数。由于编译器绝对不允许有调用一个不存在的函数的可能,所以该类不能生成对象。在它的派生类中,除非重写此函数,否则也不能生成对象。(如果我们声明的一个类是一个抽象类,里面含有纯虚函数,那么编译器就会在虚表中为这个纯虚函数保留一个位置,所以如果没有实现,这个位置就是空的,所以不能进行实例化。如果派生类改写了基类的方法并且添加了基类所不具有的方法,那么编译器会在其创建的虚表中完成相关的对应,这样我们就能够正确地调用方法了,但是还不能确切的知道我们在调用哪个类的方法,这种问题可使用运行时的类型确定来解决。)

纯虚函数的作用:

为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
也就是制定标准,统一接口。

如果一个基类中有多个纯虚函数,即基类为抽象类,那么派生类只有实现所有纯虚函数的override,子类才会是能够实例化的具体类,任何一个父类的纯虚函数没有实现override,则子类也为抽象类不能实例化。

注意: 
(1)纯虚函数没有函数体; 
(2)最后面的“=0”并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是虚函数”; 
(3)这是一个声明语句,最后有分号; 
(4)纯虚函数只有函数的名字而不具备函数的功能,不能被调用。 
(5)纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对他进行定义。如果在基类中没有保留函数名字,则无法实现多态性。 
(6)如果在一个类中声明了纯虚函数,在其派生类中没有对其函数进行定义,则该虚函数在派生类中仍然为纯虚函数。

抽象类
抽象类不能声明对象,只是作为基类的派生类服务。
抽象类不能定义对象,但是可以作为指针或者引用类型使用。

1:含有纯虚函数的类成为抽象类。
      除非派生类中完全实现基类中所有的纯虚函数,否则,派生类也是抽象类,不能实例化对象。

2:只定义了protected型构造函数的类也是抽象类。因为无论是在外部还是派生类中都不能创建该对象。但是可以由其派生出新的类。
      这种能派生出新类,但是不能创建自己对象的类时另一种形式的抽象类。

抽象类为什么不能实例化?
因为抽象类中的纯虚函数没有具体的实现,所以没办法实例化。

定义:

含有纯虚函数的类称为抽象类(接口类),抽象类不能实例化出对象

注意:

(1)凡是包含纯虚函数的类都是抽象类;

(2)抽象类不能实例化出对象;

(3)纯虚函数在派生类中重新定义以后,派生类才能实例化出对象;

抽象类/接口类的作用:

抽象类的存在,使得子类必须重写虚函数才能实例化出对象;
 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值