C++学习之多态篇(虚函数和虚析构函数的实现原理--虚函数表)

来源:互联网 发布:srt软件下载 编辑:程序博客网 时间:2024/05/15 23:45

通过下面的代码来说明:

#include <iostream>

#include <stdlib.h>
#include <string>
using namespace std;


/**
 *  定义动物类:Animal
 *  成员函数:eat()、move()
 */
class Animal
{
public:
    // 构造函数
    Animal(){cout << "Animal" << endl;}
    // 析构函数
   virtual  ~Animal(){cout << "~Animal" << endl;}
    // 成员函数eat()
void eat(){cout << "Animal -- eat" << endl;}
    // 成员函数move()
     void move(){cout << "Animal -- move" << endl;}
     
    //   int m_iLegs;


    //  short m_hands;
    //  char m_face;
    //  int m_iHead;
     
};


class A
{
    public:
    A(){cout<<"A"<<endl;}
   virtual  ~A(){cout<<"~A"<<endl;}
};
/**
 * 定义狗类:Dog
 * 此类公有继承动物类
 * 成员函数:父类中的成员函数
 */
class Dog : public Animal,public A
{
public:
    // 构造函数
Dog(int legs)
    {
        cout << "Dog" << endl;
        m_iLegs = legs;
    }
    // 析构函数
~Dog(){cout << "~Dog" << endl;}
    // 成员函数eat()
     void eat(){cout << "Dog -- eat" << endl;}
    // 成员函数move()
void move(){cout << "Dog -- move" << endl;}
public:
     int m_iLegs;
};


int main(void)
{
    // 通过父类对象实例化狗类
//   Animal * p = new Dog;
    //  Animal animal;
    //  cout<<sizeof(animal)<<endl;
    //  int * q = (int * )&animal;
    //  cout<<*(q)<<endl;
    
    Dog dog(4);
    cout<<sizeof(dog)<<endl;
    
    int * p = (int*)&dog;
    cout<<&dog<<endl;
    cout<<p<<endl;
    p++;
    cout<<p<<endl;
    p++;
   
    cout<<"p++: "<<p<<endl;
    cout<<(unsigned int)(*p)<<endl;
    // cout<< dog.m_iLegs<<endl;
    // cout<< (int *)p<<endl;
    // 调用成员函数
    //  p->eat();
    //  p->move();
    // 释放内存
    //  delete p;
    //  p = NULL;
    
return 0;

}

当两个virtual都不加的时候,输出结果:

AnimalADog40x7ffc948870600x7ffc948870600x7ffc94887064p++: 0x7ffc948870682491969640~Dog~A~Animal
分析:此时,由于函数不占用对象内存大小,有专门的程序代码区来存放程序的二进制代码,因此Dog对象dog的内存大小只是其成员变量的大小,此时有一个int型的成员变量m_iLegs,因此此时输出的sizeof(dog)的大小是4个字节

当加上其中一个virtual以后,比如Animal的析构函数成了虚析构函数,输出结果:

AnimalADog160x7fffaf8dd8800x7fffaf8dd8800x7fffaf8dd884p++: 0x7fffaf8dd8884~Dog~A~Animal
分析:此时,Dog从Animal继承,因为虚析构函数的特性是可以继承的,所以dog的析构函数也是虚析构函数,虽然没写virtual,但是编译器会自动为其加上virtual。这样,Dog的对象的内存中除了存放了自己的成员变量以为,还存放了一个虚函数表指针,在64位机器上,指针大小为8个字节,又因为高位对齐原则,所以这一次输出的sizeof(dog)的值得大小是16个字节,(8(指针)+4(int)+4空内存)

如果再将类A的virtual加上,其实不光是析构函数,声明其他函数为虚函数也一样,这时,输出的结果如下:

AnimalADog240x7ffe2707d3100x7ffe2707d3100x7ffe2707d314p++: 0x7ffe2707d3184198544~Dog~A~Animal
分析:此时Dog类的对象中将含有两个虚函数表,输出的结果将是两个虚函数指针加整型成员变量+4个空内存的大小=24个字节了。

这里注意两点:

(1)高位对齐!就是输出的大小都是高位的整数倍,这个例子中都是8的整数倍;

(2)在对象的内存空间中,按照继承顺序,指向继承来的两个虚函数表的虚函数表指针将排在第一位和第二位,占据前16个字节,然后才是int型的成员变量占用四个字节,所以p要p++四次才能到达int型变量的首地址,才能输出正确的值4,p++一次前进4个字节。如下代码所示:

  Dog dog(4);
    cout<<sizeof(dog)<<endl;
    
    int * p = (int*)&dog;
    cout<<&dog<<endl;
    cout<<p<<endl;
    p++;
    cout<<p<<endl;
    p++;
      cout<<p<<endl;
    p++;
      cout<<p<<endl;
    p++;
    cout<<"p++: "<<p<<endl;
    cout<<(unsigned int)(*p)<<endl;

输出结果为:

AnimalADog240x7ffe12b781600x7ffe12b781600x7ffe12b781640x7ffe12b781680x7ffe12b7816cp++: 0x7ffe12b781704~Dog~A~Animal

1.总结虚函数的实现原理





当类中有虚函数或者虚析构函数时,在实例化类的对象时,对象内存中除了成员变量的大小,还有一个虚函数表指针,而且虚函数表指针放在内存的最前面,虚函数表指针会指向一个虚函数表,而以为Shape类中含有虚函数,这个虚函数表将于Shape类的定义同时出现,在计算机中虚函数表也是占用一定到的内存空间的,且虚函数表由于一旦产生就具有不变性,所以编译器就会经量把它放到稳定(或者说是只读)的内存区。虚函数表vtable在Linux/Unix中存放在可执行文件的只读数据段中(rodata)。

在上例中虚函数表的起始位置是0xCCFF,那么虚函数表指针的值就是0xCCFF,每个类只有一张虚函数表,所有类的对象都共用同一张虚函数表。所有对象都含有相同的虚函数表指针值0xCCFF,以确保所有的对象含有的虚函数表指针都指向正确的虚函数表。虚函数表中存放的是所有的虚函数地址。计算时,先找到虚函数表指针,通过指针的偏移找到虚函数的指针,然后就可以调用虚函数。

上例图中,Circle派生自Shape,Circle从父类派生了虚函数,于是它也有了自己的虚函数表,这两个表的起始地址是不一样的,但是表中calcArea()函数的起始地址是一样的,这也就是说,两张不同的虚函数表中的函数指针可能指向同一个函数。

当Circle类定义了自己的虚函数,如下图所示,



由于此时,Circle类自己定义了calcArea()函数,所以将会覆盖掉父类的函数。

2.总结虚析构函数的是实现原理:

虚析构函数的特点是当将父类的析构函数声明为虚析构函数以后,再用父类的指针去指向子类的对象,并用delete去销毁父类的指针的时候,不会再只调用父类的析构函数,而是会先调用子类的析构函数再调用父类的析构函数,即会释放掉子类对象了,不会再因为子类对象得不到释放而产生内存泄露。这种情况也和虚函数表有关。实现过程如下:当声明父类析构函数为虚析构函数以后,在子类和父类的虚函数表中将都出现虚析构函数的函数指针,如下两幅图所示。当用父类的指针指向子类的对象,用delete Shape释放对象时,会通过Shape指针找到子类Circle的虚函数表指针,从而找到虚函数表,从而通过偏移找到虚析构函数的地址,从而调用子类Circle的析构函数,然后也会调用父类的析构函数。


多态的实现原理如下:当用父类的指针去指向子类对象时,会拿到子类的虚函数表指针,然后找到虚函数表,通过虚函数表指针的偏移,找到要调用的虚函数的函数指针,从而实现函数的调用。注意这里的偏移必须是和父类的偏移量是一样的。

与本节内容有关,补充两个概念:函数的隐藏和覆盖

函数的隐藏:没有定义多态的情况下,即没有加virtual的前提下,如果定义了父类和子类,父类和子类出现了同名的函数,就称子类的函数把同名的父类的函数给隐藏了。

函数的覆盖:是针对多态来说的。如果定义了父类和子类,父类中定义了公共的虚函数,如果此时子类中没有定义同名的虚函数,那么在子类的虚函数表中将会写上父类的该虚函数的函数入口地址,如果在子类中定义了同名虚函数的话,那么在子类的虚函数表中将会把原来的父类的虚函数地址覆盖掉,覆盖成子类的虚函数的函数地址,这种情况就称为函数的覆盖。

1 0
原创粉丝点击