C++学习笔记--类对象、继承、多态的内存模型

来源:互联网 发布:淘宝网包邮服务 编辑:程序博客网 时间:2024/06/08 11:57

类对象模型

以前我们学习过结构体变量在内存中是如何排布的,那么对于一个类对象,内存中又是如何排布的?

在学习class关键字之前我们是使用struct关键字来创建一个类的,它与class仅有的区别就是默认的访问级别不同,struct的默认访问级别是public,class的默认访问级别是private。所以,class只是一个特殊的struct,在内存中依旧可以看做变量的集合,class遵循与struct相同的内存对齐规则,最后一个点,class的成员变量和成员函数是分开存放的,每个对象有独立的一套成员变量,所有对象共享同一套成员函数,因为数据是存放在堆栈和全局数据区的,在程序运行时可以任意添加或更改,而函数是存放在代码段的,程序运行期间无法变更。

class A{    int i;    int j;    char c;    double d;public:    void print()    {        cout << "i = " << i << ", "             << "j = " << j << ", "             << "c = " << c << ", "             << "d = " << d << endl;    }};struct B{    int i;    int j;    char c;    double d;};
定义一个结构体和一个类,类里的成员变量排布和结构体一样,并且带有成员函数,我们通过sizeof关键字来看看结构体和类的大小:

    cout << "sizeof(A) = " << sizeof(A) << endl;    // 24 bytes    cout << "sizeof(B) = " << sizeof(B) << endl;    // 24 bytes
看看输出结果;



大小都是24个字节,为什么呢?因为编译器会按照最长类型的整数倍排布。

现象说明编译器在编译时并没有将成员函数和类的成员变量放在同一个地方,实际的对象中只包含成员变量,类的成员变量的内存排布和struct的内存排布是一样的,这是在编译期间的行为。那么当程序执行起来后又会发生什么?首先运行时的对象会退化成结构体的形式,所有成员在内存中依次排布,成员变量间可能会存在内存间隙(对齐问题),并且我们可以通过内存地址直接访问成员变量,所以在运行时的访问权限关键字会失效。

    B* p = reinterpret_cast<B*>(&a);    p->i = 1;    p->j = 2;    p->c = 'c';    p->d = 3;    a.print();
通过指针可以修改成员变量的值。


那么我们为什么能够通过类对象来调用成员函数呢?那是因为C++给我们隐藏了背后的调用过程,类的成员函数位于代码段中,当对象调用成员函数时对象本身的地址作为参数隐式传递给成员函数,而成员函数又通过对象地址访问成员变量,而这个对象地址就是我们说的this指针,C++语法规则隐藏了对象地址的传递过程。

继承对象模型

子类成员是由父类成员叠加子类新成员得到的,为了证明这个事实,我们用一个 例子分析:

class Demo{protected:    int mi;    int mj;public:    void print()    {        cout << "mi = " << mi << ", "             << "mj = " << mj << endl;    }};class Derived : public Demo{    int mk;public:    Derived(int i, int j, int k)    {        mi = i;        mj = j;        mk = k;    }        void print()    {        cout << "mi = " << mi << ", "             << "mj = " << mj << ", "             << "mk = " << mk << endl;    }};
定义一个父类和一个子类如上所示,在子类中添加两个函数,我们可以尝试打印两个类的大小,调用下面语句:

    cout << "sizeof(Demo) = " << sizeof(Demo) << endl;             cout << "sizeof(Derived) = " << sizeof(Derived) << endl;
输出结果为:

sizeof(Demo) = 8
sizeof(Derived) = 12

因为父类只有两个int型成员变量,所以大小是8个字节,子类中是父类成员变量和自身成员变量的叠加,即3个int型变量,由于类的成员函数不计算在类的大小中,所以打印是12个字节。

虽然大小确定了,我们还想确认下子类的内存排布是否真是先父类成员再子类成员,定义一个下面这样的结构体:

struct Test{    int mi;    int mj;    int mk;};
我们定义一个Test结构体指针,让它指向一个子类对象,通过指针修改结构体指针成员的值,并作输出进行检验:

    Derived d(1, 2, 3);    Test* p = reinterpret_cast<Test*>(&d);    d.print();    p->mi = 10;    p->mj = 20;    p->mk = 30;    d.print();
先定义一个子类对象d和一个Test结构体指针,让它指向reinter_cast关键字转换后的d对象,打印原先d对象成员的值,再通过指针修改结构体成员变量的值,最后再次输出。

结论推断:如果最后的d.print()打印出来的结果依次是10、20、30的话表示成员变量的值修改成功,也表示Test结构的成员分布与子类的成员分布是一样的,即先父类成员再子类成员,与结构体成员有一一对应的关系

输出:

mi = 1, mj = 2, mk = 3
mi = 10, mj = 20, mk = 30

答案和预期一样,证明前面的推断是正确的,子类成员是先父类成员,再子类成员叠加起来的
多态对象模型

C++中多态的实现原理是什么?

首先,多态是面向对象理论的一个概念,它和具体实现语言无关,即就是相同的行为在不同环境得到不同的结果,C++中就是用虚函数来实现这种多态性。那么虚函数内部是如何描述的。

当类中声明虚函数时,编译器会在类中自动生成一个虚函数表,用来存储类中虚函数成员的地址,它是一种数据结构,并且也由编译器维护。当编译器在编译成员函数时如果发现有virtual关键字声明的成员函数的话,编译器就会将它的地址放入虚函数表,当存在虚函数的类时,创建对象时就会自动添加一个指针指向虚函数表,即含有虚函数的类对象会多出一个指针作为成员变量,但是我们对它并不可见。当我们需要调用某个虚函数时编译器首先会通过添加进来的指针指向的虚函数表里查找函数的地址,然后进行调用。由于父类和子类各有一张虚函数表,所以在调用同名函数时只需根据对象所属类即可正确无误的执行下去,由于需要查找的缘故,调用虚函数的效率会比调用普通成员函数的效率低,这就是C++中多态的实现方式。下面我们通过程序证明虚函数指针的存在:

首先将类中的print函数设为virtual声明的虚函数,我们再次输出类的大小 看看:

    virtual void print()    {        cout << "mi = " << mi << ", "             << "mj = " << mj << endl;    }

    cout << "sizeof(Demo) = " << sizeof(Demo) << endl;    cout << "sizeof(Derived) = " << sizeof(Derived) << endl;
输出结果:

sizeof(Demo) = 12
sizeof(Derived) = 16
较之刚才都多了4,表示指针确实是存在的,那么它存在的位置是哪儿呢?是成员变量的最前面还是最后面?答案是指向虚函数表的指针放在类对象的最前面四个字节的

首先我们还是定义一个Test结构体;

struct Test{    int mi;    int mj;    int mk;};
还是按照前面的做法,定义一个结构体指针指向一个子类对象,通过修改子类中mi、mj、mk变量的值为10、20、30,输出修改操作后的结果:

mi = 1, mj = 2, mk = 3
mi = 20, mj = 30, mk = 3
看见了什么,答案将不再对应,看起来好像位置都提前了一个,正是如此,因为带有虚函数的类对象中会有一个指针放在成员变量的最前面。重新创建一个结构体,使结构的第一个成员变量是一个void指针,任何类型变量都可以作为实验,只需保证在内存排布中类普通成员变量开始排布之前有四个字节的空缺,再次做修改结构成员变量值的操作:

struct Test{    void *pp;    int mi;    int mj;    int mk;};
现在的输出是:

mi = 1, mj = 2, mk = 3
mi = 10, mj = 20, mk = 30
和预期一样,表示内存布局成功定位,也说明类对象中开始处确实存在一个成员变量,既然存在那么存在的肯定就是由于虚函数的存在而在对象创建时强加的一个指向虚函数表的指针。





原创粉丝点击