[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、通过反汇编观察,这点还不是很熟,不过以后要加强调试经验。

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

时间: 2024-10-12 13:26:04

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

含有虚函数菱形的虚拟继承(没有对虚函数进行重写)

在VS2013编程,调试 问题 :  菱形继承会引来,二义性 1.源代码 </pre><pre name="code" class="cpp">#include <iostream> using namespace std; class Base { public: virtual void FunTest() { cout << "Base::FunTest () " << endl;

谈谈c++中继承中的虚函数

c++继 承中的虚函数 c++是一种面向对象的编程语言的一个很明显的体现就是对继承机制的支持,c++中继承分很多种,按不同的分类有不同分类方法,比如可以按照基类的个数分为多继承和单继承,可以按照访问权限分为public继承.protected继承和private继承,按照是否是虚拟继承可以分为virtual继承和non-virtual继承.当然这里的分类标准都是有重叠的部分,比如,non-virtual继承又可以分为单继承和多继承.这里要讨论的是虚函数,因此主要从virtual和non-virt

C++ 虚函数内存分配

本文重点参考了<C++ 虚函数表解析>一文(链接:http://blog.csdn.net/haoel/article/details/1948051/),陈皓前辈此文讲解清晰,读后受益匪浅.只是代码中存在一些问题,例如涉及到本文重点虚函数表的地方,写到 Base b; cout << "虚函数表地址:" << (int*)(&b) << endl; 但是,实际上(int*)(&b)并非虚函数表地址,而是对象b的地址,*(

静态联编,动态联编,类指针之间的关系,虚函数与多态性,纯虚函数,虚析构函数

1.静态联编,是程序的匹配,连接在编译阶段实现,也称为早期匹配.重载函数使用静态联编. 2.动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编.switch语句和if语句是动态联编的例子. #include<iostream> void go(int num) { } void go(char *str) { } //class //::在一个类中 class A { public: void go(int num) { } void go(char *str) { } }; void

继承中的虚函数、纯虚函数、普通函数

一.虚函数 被virtual关键字修饰的类成员函数就是虚函数.虚函数的作用就是实现运行时的多态性,将接口与实现分离.简单理解就是相同函数有着不同的实现,但因个体差异而采用不同的策略. 基类中提供虚函数的实现,为派生类提供默认的函数实现.派生类可以重写基类的虚函数以实现派生类的特殊化.如下: class Base{ public: virtual void foo() { cout<<"Base::foo() is called"<<endl; } }; clas

C++继承后的虚函数访问权限

今天在写代码时发现对继承后的函数访问权限不太清楚,于是自己做了个测试: 1.头文件(test.h) 1 #include <iostream> 2 using namespace std; 3 4 class A{ 5 private: 6 void print(){ 7 cout << "this is A" << endl; 8 } 9 }; 10 11 class B:public A{ }; A为基类,B为A的子类. 2.源文件(test.c

VC++ 之 多态性与虚函数

多态性是面向对象程序设计的关键技术之一.利用多态性技术,可以调用同一个函数名的函数,实现完全不同的功能.若程序设计语言不支持多态性,不能称为面向对象的语言. 在C++中有两种多态性: 编译时的多态性:通过函数的重载和运算符的重载来实现的. 运行时的多态性:在程序执行前,无法根据函数名和参数来确定该调用哪一个函数,必须在程序执行过程中,根据具体情况来动态地确定.它是通过类继承关系和虚函数来实现的,目的也是建立一种通用的程序. 虚函数的定义 ◆ 1.定义格式虚函数是一个类的成员函数,定义格式如下: 

记录:C++类内存分布(虚继承与虚函数)

工具:VS2013 先说一下VS环境下查看类内存分布的方法: 先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存布局,如果写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局. 编译后,输出如图的内存布局: 测试代码 #include <iostream> using namespace std; typedef void(*FUN)(); cl

看到的关于虚函数继承及虚继承解释比较好的文章的复制

(来源于:http://blog.chinaunix.net/uid-25132162-id-1564955.html) 1.空类,空类单继承,空类多继承的sizeof #include <iostream> using namespace std; class Base1 { }; class Base2 { }; class Derived1:public Base1 { }; class Derived2:public Base1, public Base2 { }; int main(