C++必知系列(三)——对象内存模型

    C++最初只是一个带类的C,后来给类加了继承功能,有了继承,自然就发展出多态的概念。那么当定义一个C++类对象时,它的内存模型是怎样的呢?了解它的内存模型应该会让我们在编程时心中更有数。下面就依C++的自然发展顺序来简单探讨一下其类对象的内存模型。

1. 无继承,无多态

    这是最简单的情况,在C里有结构体,只是一个多数据类型的集合,C++的类最初做的就是将数据和其操作封装在一起,如下类和结构体申明:

                     class entry1 {                              struct entry2 {

                           int a;                                             int a;

                           int b;                                             int b;

                           void SetA(int a);                     }

                           void SetB(int b);

                      };

entry1相比entry2只是将两个函数申明封装进来,那么他们定义的变量占用的内存模型有什么差别吗?实际上是没有任何差别,占用同样的大小及相同的内存结构。那么entry1里申明的两个成员函数怎么处理?C++一贯的原则是简单,实用,效率及尽量与C兼容,这里实际是entry1定义的所有对象共享同样的成员函数,也就是这些成员函数的定义只有一份,他们几乎与普通的函数无区别,那么成员函数如何感知是在不同的对象上操作呢?这要归功于编译器了,编译器在碰到成员函数申明时,会给它增加一个隐含的参数,这个参数的类型就是封装这个成员函数的类类型指针,而当调用成员函数时编译器会把相应对象指针作为隐含参数传递进去,这样就能区别不同的对象了,例如:

                  entry1 a;

                  a.SetA(10); 

经编译后等价如下调用: SetA(&a , 10);

   类成员有访问权限的控制,那么申明在不同访问权限区段内的成员在内存中存放有什么限制呢?如下类定义:

                 class CA {

                 public:

                     int a;

                     int b;

                 private:

                    int c;

                    int d;

                protected:

                   int e;

                   int f; 

                };

C++标准只规定位于同一访问权限区段的成员声明在内存里后申明的占较高的地址,而各区段之间可自由排列,那么在程序里我们可以确定&b > &a,&d>&c,...,但不能假定&e > &a,&d > &b,...;考虑到简单性与效率,目前大部分编译器都是按照申明顺序在内存里由低到高排列。

   类里除了可以有普通成员外,还可以有静态成员,静态成员属于类,整个类只有静态成员的独一无二的一份,因此它并不与对象一起存放,而是位于全局数据区,在编译时就已经分配好。

2. 继承

   继承是一个很好的设计机制及代码复用机制,面向对象设计的重要特征之一。继承其实可以有很多的实现模型,那么在C++里是如何实现的呢,或者说一个有继承的类对象的内存模型是怎样的呢?C++依然将效率放在了第一位,牺牲的可能就是一些弹性,对基类的静态依赖性,C++支持单继承,多继承及复杂的虚拟继承,考虑到虚继承较复杂,而且平时很少用到,这里就不多说,感兴趣的可以参考《深度探索C++对象模型》。 C++里派生类对象里直接包括所有基类对象的所有成员,C++标准规定派生类对象里各基类对象成分内存结构与一个单独的基本对象结构完全一致,但基类成分出现的位置并无强制要求,但目前大部分编译器将基类成分放在首位,如果是多重继承,则按照基类申明的顺序依次排列,但有些特殊情况编译器为了优化会进行一些调整,一种可能的调整情况就是,第一个基类没有虚函数,而后续基类有虚函数,派生类有自己的虚函数,那么编译器很可能就会把带虚函数的基类向前调整,以减少一个虚表指针空间及提高虚函数访问的效率,所以编程时不要太依赖于这种顺序关系。下面举例说明,如下类定义:

             class Base1 {           class Base2  {          class Derive : public Base1, public Base2 {        

                 int a;                            int c;                           char e;                                    

                 char b;                         int d;                           char f;

            };                                };                                      char g;

                                                                               };

 

 derive类对象一种常见的内存结构与如下类等价:

            class equal {

                Base1 base1;

                Base2 base2;

                char e;

                char f;

                char g;

           };

但请注意与如下类定义并不等价:

         class noequal {

             int a;

             int b;

             int c;

             int d;

             int e;

             char e;

             char f;

             char g;

         };

由于内存对齐等问题,noequal类与equal类对象的内存结构并不等价,导致noequal类丧失了基类成分应有的一致性。

3. 多态

      以上所讲的都没有考虑到多态的情况,有了继承,多态似乎成了一个必须的重要机制,通过多态程序可以只依赖于抽象接口,而无需依赖具体实现其实C++标准本身并没有规定多态的具体实现,而是规定一些外在的行为,对于编程需要记住的就是:将一个派生类对象付给基类引用,或派生类指针付给基类指针,然后由基类引用或基类指针调用虚函数时会发生多态行为,具体调用的函数是运行时确定的,但目前所有编译器基本都是基于虚表指针与虚表数组的机制来实现,所以也几乎成为了一个虚函数实现的标准。

     对于一个有虚函数的类对象,编译时编译器会在对象的某个位置(一般为起始位置)安排一个虚表指针,指向该类的虚表数组的起始位置,虚表数组里存放虚函数的起始地址,类的虚表数组是静态确定的,也就是编译完成后就确定,程序运行过程中是无法修改的,虚表指针的初始化(指向该类的虚表数组)是在每个类的构造和析构函数里在进入用户编写的代码前初始化的,这是由编译器自动插入的代码,那么对于有虚函数的类会付出额外额外虚表指针空间的代价,如下类定义:

       class CA {

            int a;

           virtual void test() {...}

       };

编译过后实际可能的结构等价于:

      class CA2 {

          int* vptr;

          int a;

      };

这里一个需要注意的是对于一个有虚函数的类对象,谨慎采用memset,ZeroMemory等内存操作函数,防止覆盖了虚表指针等隐式数据,从而可能导致运行时的崩溃。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值