[GeekBand] C++继承关系下虚函数内存分布

本文参考文献:GeekBand课堂内容,授课老师:侯捷

                 :深度探索C++对象模型(侯捷译)

                 :网络资料,如:http://blog.csdn.net/sanfengshou/article/details/4574604

 

    说明:由于条件限制,仅测试了Windows平台下的VS2013 IDE。其余平台结果可能不同,但原理都类似。建议读者自己在其他平台进行测试。

1、什么是虚函数?

虚函数是类的非静态成员函数,在类中的基本形式如下:virtual 函数返回值类型 虚函数名(形参表)

如:virtual void process()

2、虚函数的作用,为什么采用虚函数?

    虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数重新定义(形式也是:virtual 函数返回值类型 虚函数名(形参表){ 函数体 }),在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。

3、正文

  首先,假设有一个Fruit类,如下所示:

//基类
class Fruit
{
public:
    //构造函数
    Fruit(const int no_,const double weight_,const char key_) :no(no_), weight(weight_), key(key_){};
    //打印变量内存地址
    void print() 
    {
        cout << "no :" << no << "           |    " << " Memory Address no: " << (void*)&no << endl;
        cout << "weight :" << weight << "       |    " << "  Memory Address weight: " << (void*)&weight << endl;
        cout << "key :" << key << "          |    " << "  Memory Address key: " << (void*)&key << endl;
    }
    //虚函数的影响
    virtual void process()
    { 
        cout << "the Process function of Base Fruit is called!" << endl;
    }
private:
    int no;
    double weight;
    char key;
};

 

我们知道,int类型为4个字节,double类型为8个字节,char类型为1个字节,又知道Class中存在着字节对齐的说法。这里有流传比较广的三原则:

1、偏移地址和成员占用大小均需要对齐;

2、结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)

3、结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补齐.

由此可以推断出,此时数据段大小应为24字节

如果数据更换位置了呢?比如,感兴趣的朋友自己验证下:

private:
    char key;
    int no;
    double weight;

 

最后,相信大家都知道 #pragma pack(),这个函数。这里面又有着效率等问题,以后再详细的分析它,由于与此标题无关,就不展开说了。

 

测试代码如下所示:

 

#include "stdafx.h"
#include"Object.h"
#include"iostream"
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
    //基类(水果):测试数据
    cout << "------------------------------------------------------------------" << endl;
    cout << "The Memory Test Data of Fruit Class !"<< endl;
    cout << "------------------------------------------------------------------" << endl;
    Fruit TestFruit(0,0.0,'b');
    cout << "Fruit Class Size : " << sizeof(Fruit) << endl;                               //类的大小
    cout << "Fruit Memory Layout FirstAddress: " << (void*)&TestFruit << endl;            //类的首地址
    //虚函数表的地址存在在前四个字节中
    cout << "Fruit Virtual Function Table Address:" << (int*)(&TestFruit) << endl;       //虚函数表地址
    //打印类的信息
    TestFruit.print();                                                                    //类中成员的地址信息
    cout << "Fruit Virtual process Function Address: " << (int*)*(int*)&TestFruit + 0 << endl;  //虚函数表中函数process函数首地址
    // 通过函数指针调用函数,验证正确性
    typedef void(*func_pointer)(void);
    func_pointer fptr = NULL;
    fptr = (func_pointer)*((int*)*(int*)&TestFruit + 0); // v_func1()
    fptr(); //process 
}

 

结果如下所示:

 

根据显示结果,所以我们可以大致的画出内存分布图:

其中

1、Fruit类为类,大小为 sizeof(Fruit) = 32,

2、vptr 为虚指针,指向了虚函数表

3、int类型为4字节,为了对齐,所以填充了4个字节。同理,char填充了7个字节。

          

如果派生类和基类有共同的虚函数时内存如何分布呢?

子类代码如下,注意此时子类和基类都有virtual void process()函数


#ifndef  _OBJECT_H_
#define  _OBJECT_H_
#include"iostream"
using namespace std;
//基类
class Fruit
{
public:
    //构造函数
    Fruit(const int no_,const double weight_,const char key_) :no(no_), weight(weight_), key(key_){};
    //打印变量内存地址
    void print() 
    {
        cout << "no :" << no << "           |    " << " Memory Address no: " << (void*)&no << endl;
        cout << "weight :" << weight << "       |    " << "  Memory Address weight: " << (void*)&weight << endl;
        cout << "key :" << key << "          |    " << "  Memory Address key: " << (void*)&key << endl;
    }
    //虚函数的影响
    virtual void process()
    { 
        cout << "the Process function of Base Fruit is called!" << endl;
    }
private:
    int no;
    double weight;
    char key;
};

//派生类
//这里考虑自己本身的虚函数,及基类的虚函数
class Apple : public Fruit
{
public:
    //构造函数
    Apple(const Fruit& fruit_,const int size_,const char type_) :size(size_), type(type_),Fruit(fruit_){};
    //打印成员数据
    void save() 
    {
        cout << "size :" << size << "   |    " << "   Apple Memory Address no: " << (void*)&size << endl;
        cout << "type :" << type << "   |    " << "   Apple Memory Address weight: " << (void*)&type << endl;
    }
    virtual void process()
    {
        cout << "the Process function of Derived Apple is called!" << endl;
    }
private:
    int size;
    char type;
};

#endif

 

完整测试代码如下:

 

// TestObjectSize.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include"Object.h"
#include"iostream"
using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    //基类(水果):测试数据
    cout << "------------------------------------------------------------------" << endl;
    cout << "The Memory Test Data of Fruit Class !"<< endl;
    cout << "------------------------------------------------------------------" << endl;
    Fruit TestFruit(0,0.0,'b');
    cout << "Fruit Class Size : " << sizeof(Fruit) << endl;                               //类的大小
    cout << "Fruit Memory Layout FirstAddress: " << (void*)&TestFruit << endl;            //类的首地址
    //虚函数表的地址存在在前四个字节中
    cout << "Fruit Virtual Function Table Address:" << (int*)(&TestFruit) << endl;       //虚函数表地址
    //打印类的信息
    TestFruit.print();                                                                    //类中成员的地址信息
    cout << "Fruit Virtual process Function Address: " << (int*)*(int*)&TestFruit + 0 << endl;  //虚函数表中函数process函数首地址
    // 通过函数指针调用函数,验证正确性
    typedef void(*func_pointer)(void);
    func_pointer fptr = NULL;
    fptr = (func_pointer)*((int*)*(int*)&TestFruit + 0); // v_func1()
    fptr(); //process 

    //派生类(苹果):测试数据
    cout << "------------------------------------------------------------------" << endl;
    cout << "The Memory Test Data of Apple Class  !" << endl;
    cout << "------------------------------------------------------------------" << endl;
    Apple TestApple(TestFruit, 1, 't');
    cout << "Apple Class Size : " << sizeof(Apple) << endl;                               //类的大小
    cout << "Apple Memory Layout FirstAddress: " << (void*)&TestApple << endl;            //类的首地址
    //虚函数表的地址存在在前四个字节中
    cout << "Apple Virtual Function Table Address:" << (int*)(&TestApple) << endl;       //虚函数表地址
    //查看基类Fruit类的信息
    TestApple.print();
    //打印Apple类的信息
    TestApple.save();                                                                    //类中成员的地址信息
    cout << "Apple Virtual process Function Address: " << (int*)*(int*)&TestApple + 0 << endl;  //虚函数表中函数process函数首地址
    // 通过函数指针调用函数,验证正确性
    typedef void(*func_pointer2)(void);
    func_pointer fptr2 = NULL;
    fptr2 = (func_pointer2)*((int*)*(int*)&TestApple + 0); // v_func1()
    fptr2(); //process 

    system("pause");
    return 0;
}

 

我们先看下实际结果:

 

 

通过结果,我们可以画出此时子类的内存分布图。

根据显示结果,我们可以进一步分析!

其中

1、Apple类为派生类,大小为 sizeof(Apple) = 40,

2、vptr 为虚指针,指向了虚函数表.同时,先对基类进行了内存分配,然后再对子类进行内存分配。

3、派生类中int类型为4字节,char 为1字节。为了对齐,char 类型填充了3个字节、

 

根据以上分析:整体框架如图:

         

 

4.1、进一步的思考

1、 通过以上分析,我们基本上知道了系统如何进行内存分布的,我们这里对虚函数表内存进一步的观察与思考:

打印出来的信息 : Fruit Virtual process Function Address : 001AEC78

                        Apple Virtual process Function Address :001AEDB0

                        观察得到,从内存角度出发,基类的地址(001AEC78)要小于子类(001AEDB0),这说明基类的虚函数在子类的前面。

 

2、分别在Fruit类、Apple类中添加虚函数:

 

    //测试用,非本题范围
    virtual void process_b0()
    {
        cout << "This is Base Fruit class's process_b0" << endl;
    }

 

及Apple类中添加虚函数

    //测试用,非本题范围
    virtual void process_b1()
    {
        cout << "This is Derived Apple_class's process_b1" << endl;
    }

 

3、然后修改测试代码段,将检验一次,改为三次

 

 

    // 通过函数指针调用函数,验证正确性
    //typedef void(*func_pointer2)(void);
    //func_pointer fptr2 = NULL;
    //fptr2 = (func_pointer2)*((int*)*(int*)&TestApple + 0); // v_func1()
    //fptr2(); //process 

    // 通过函数指针调用函数,验证调用问题
    typedef void(*func_pointer)(void);
    func_pointer fp = NULL;
    for (int i = 0; i<3; i++) {
    fp = (func_pointer)*((int*)*(int*)&TestApple + i);
    fp();

 

4、运行后结果如图所示:

 

通过结果进一步的思考与分析:

1、显示:The Progress function of Deviced Apple is called !同名的process,运行的是子类的process()。说明此时用子类的虚函数process()代替了父类的虚函数process()!

2、先显示:this is Base Fruit class proccess_b0 ,后显示:This is Deviced Apple class process_b1.进一步说明了基类的虚函数内存分布在子类的之前!

 

基本关系如图所示:

 

4.2、调试技巧

在VS2013 DE中有很多方便的工具,可以清晰的观察出变量等各种信息,如图通过debug模式下即可查看出很多关键信息!

 

2、通过反汇编观察,这点还不是很熟,不过以后要加强调试经验。

 

 

                                                                以上就是我个人的一些不成熟的学习笔记,望各位批评指正。谢

 

posted @ 2016-08-08 02:53  徐贺  阅读(421)  评论(0编辑  收藏  举报