C++对象的内存模型

看了C++ under the hood之后,C++对象的内存模型是这样的:涉及到虚的,就需要额外的存储空间。虚这里指的是虚函数和虚继承,额外的存储是对象内部需要存储虚函数表指针和虚基类表指针(VS专有)。

注意特性与特性的实现,是两回事。C++中通过虚函数来达到多态,多态的实现通常都是通过虚函数表来实现,当然还有其他可能的实现方式,这在标准中并未规定。意即说到虚函数,不一定非得有虚函数表。

一个类的内存的分布在VC下是这样的:先是非虚基类子对象(包括非虚基类的虚函数表指针和成员),接着是虚基类表指针,接着才是该类本身扩展的成员,然后是虚基类子对象。该类的虚函数如果在基类中出现,会在对应的虚函数表中的位置对基类的进行替代。而不曾在基类中出现的虚函数,会加到最前边的非虚基类子对象的虚函数表的尾部,所以如果没有非虚基类,则在起始位置新建一张虚函数表,用来保存这些多余的虚函数的位置。


虚函数表中的内容包括:

1、基类子对象在派生类对象中的偏移量,由于一般虚函数表指针总是在对象头,也即是改虚函数表指针偏移对象起始地址的大小,英文topoffset,dynamic_cast 时能从基类子对象调整到完整的派生类对象。

2、指向 typeinfo 类型信息相关的指针。

3、各个虚函数指针。

虚基类表内容:

1、虚基类表指针偏移对象起始地址的量,类似虚函数表中的第一项。

2、虚基类表指针偏移虚基类子对象的量。由于虚基类子对象通常在对象的最末尾,对于B->A, C->A,  D->B,D->C 这种菱形继承,比如C* ptr = new D, 需要通过ptr访问D的数据时,就需要改偏移量。


一般的,初始化的顺序是按继承的顺序,从上到下,从左到右调用基类的构造函数(虚继承子对象会破坏这个规则,最先调用),然后是成员对象的构造函数,然后是虚函数表和虚基类表指针的初始化,最后是其他成员的初始化。另外,对于虚继承,虚继承子对象如果没有默认构造函数,在每一层的每一个派生类中都要显示的调用其构造函数,此时如果其基类的构造函数初始化列表中已经调用了虚继承子对象的构造函数,在派生类的构造函数的初始化列表中,仍然要显示的调用,其基类的构造函数中对虚继承子对象的构造函数的调用将忽略。


class A;    //A假如没有默认的构造函数
class B:virtual public A
class C:virtual public A
class D:public B, virtual public C    //在D的初始化列表中仍然要显示的调用A的构造函数(B、C构造函数中对A构造函数的调用将忽略)


也就是说,须基类的构造函数,在每一层的派生类的构造函数中都会被调用(A没有默认的构造函数,需要手写调用;有默认的,编译器安插A的构造函数在D的构造中);也就是说,上边D的构造函数中,仍然会调用A的构造函数,而不是将A的非虚继承中,包裹在B中或者C中,那样D只需要调用B的和C的就行;但是虚继承,D的还是会调用A的,而忽略在B和C中对A的调用。

利用这个原理,如果A的构造函数为private,假设B是A的友元,同时,B虚继承于A;那么,B将是一个无法被继承的类;因为如果C继承于B,那么C的构造函数中会去调用A的构造函数,这是不允许的。而且这里B可以正常在栈上构造,而普通的防止被继承的方法实际上使用的是单例的模式。

这就是C++中的黑科技。


这里也回顾一下C++的访问控制:public 是面向公众的,protected 是留给孩子的,private 是对朋友开放的(当然从内存上来看)。需要说明的是,孩子又有三种,当然从内存上来看,无论哪种孩子,派生类对象的内存里都有一个完成的基类子对象。默认情况下,三种孩子对于基类中的public 和 protected,继承过来知道,public 孩子变成自己的public 和 protected, protected 孩子都变成自己的protected,private 孩子都变成自己的private ;当然可以用using 关键字改变这种默认行为。

另外,如果不是为了多态,在派生类和基类中使用相同的函数名是个糟糕的设计;是虚函数而且必须函数签名相同才能是多态。其他情况下,基类的指针(或者引用)指向派生类,永远只能调用基类中定义过的函数,当然是函数名相同,签名不同,仍然是不同的函数。所谁的指针调用谁的函数。派生类的指针将无法调用与派生类中定义的同名的基类中的函数--> 这是个糟糕的设计,当然任何在访问权限内,总是可以用域运算符来访问基类的(所有的访问前都可以加域运算符,只是不简洁)。

为了无心造成的这个设计,C++11推出两个新的关键字:final 和 override;final修饰类是,表示这个类不能被继承。final 在基类中修饰虚函数,表示派生类中不能再重写这个虚函数,否则会出错;override 在派生类中修饰虚函数,表示这个虚函数是对基类的重写,否则会出错。


另外,关于 static 变量的存储,是在首次执行到 static 变量的语句时,才创建和分配内存的,意即对于局部的 static 变量,如果声明语句没有被执行到,永远不会被创建。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值