C++对象模型
2025-05-08 21:58 一只老老老菜鸟 阅读(32) 评论(0) 收藏 举报单个类对象:
在C++中,有两种 class data members:static 和 nonstatic,以及三种 classmember functions:static、nonsiatic 和 virtual.
已知下面这个 class Point 声明:
class Point { public: Point() :_x(1.0) {} virtual ~Point() {} double x() const {} static int PointCount() {} protected: virtual void p() const {} double _x; static int _point_conut; };
Point pt;
这个 class Point 在机器中将会被怎么样表现呢?也就是说,我们如何模塑(modeling)出各种data members 和 function members 呢?
Nonstatic data members 被配置于每一个 class object 之内,static data members 则被存放在所有的 class object 之外。Static 和 nonstatic function members 也被放在所有的 class object 之外,Virtual functions 则以两个步骤支持之:
1.每一个 class 产生出一堆指向 virtual functions 的指针,放在表格之中。这个表格被称为 virtual table(vtbl)
2.每一个 class object 被添加了一个指针,指向相关的 virtual table.通常这个指针被称为 vptr。vptr 的设定(setting)和重置(resetting)都由每一个 class 的 constructor、destructor 和 copy assignment 运算符自动完成,每一个class 所关联的 type_info object(用以支持 runtime type identification,RTTI)也经由 virtual table 被指出来,通常是放在表格的第一个 slot处。
成员类型 | 存储位置 | 对象内存占用 | 共享性 |
---|---|---|---|
non-static 成员变量 | 对象内存布局 | ✔️ | 对象独享 |
static 成员变量 | 全局/静态数据区 | ❌ | 所有对象共享 |
non-static 成员函数 | 代码区 | ❌ | 共享 |
static 成员函数 | 代码区 | ❌ | 共享 |
virtual 成员函数 | 代码区(通过虚表访问) | ❌ | 共享 |
pt对象实例的内存布局(32位编译器)16byte:
偏移量 | 内容 | 大小(字节) |
---|---|---|
0 | 虚表指针(vptr) | 4 |
4 | 对齐填充 |
4 |
8 |
double _x |
8 |
16 | (结束) |
需要多少内存才能够表现一个 class object?一般而言要有:
1.其 nonstatic data members 的总和大小;
2.加上任何由于 alignment(内存对齐)的需求而填补(padding)上去的空间(可能存在于 members之间,也可能存在于集合体边界)
3.加上为了支持 virtual 而由内部产生的任何额外负担(overhead)
继承:
在 C++ 继承模型中,一个 derived class object 所表现出来的东西,是其自己的members 加上其 base class(es)members 的总和。至于 derived class members和 base class(es)members 的列次序并未在 C++ Standard 中强制指定;理论上编译器可以自由安排之。在大部分编译器上头,base class members 总是先出现但属于 virtual base class 的除外(一般而言,任何一条规则一旦碰上 virtual baseclass 就没辄儿,这里亦不例外).
C++ 语言保证“出现在 derived class中的 base class subobject 有其完整原样性”.子类会完全继承父类的对象模型。
为什么子类会设计这种膨胀空间的方式呢?优点:父子类间copy很方便恰当,不会出错。否则会出现如下错误。
继承加多态:
程序会生成一个虚表(virtual table),用来存放它所声明的每一个virtual functions的地址,再加上一个slots(用以支持 runtimetype identification)
在每一个 class object 中导入一个 虚表指针vptr,指向虚表,提供执行期的链接,使每一个object 能够找到相应的 virtual table.
把 vptr 放在 class object 的前端,对于“在多重继承之下,通过指向 classmembers 的指针调用 virtual function”,会带来一些帮助。
否则,不仅“从 class object 起始点开始量起”的 ofset 必须在执行期备妥,甚至与 class vptr 之间的 offset 也必须备妥.
多重继承:
多重继承的问题主要发生于 derived ciass objects 和其第二或后继的 baseclass objects 之间的转换:不论是直接转换或是经由其所支持的 virtual function 机制做转换
对一个多重派生对象,将其地址指定给“最左端(也就是第一个)base class 的指针”,情况将和单一继承时相同,因为二者都指向相同的起始地址。需付出的成本只有地址的指定操作而已,至于第二个或后继的 base class 的地址指定操作,则需要将地址修改过:加上(或减去,如果downcast 的话)介于中间的 base class subobject(s)大小,例如:
虚拟继承:
暂略
引申:
C++不允许一个对象的大小为0,不同对象的地址不能具有相同的地址。
这是因为new需要分配不同的内存地址,不能分配内存大小为0的空间,避免除以sizeof(T)引发除零异常。
所以一个没有数据成员的空类,编译器会为其分配1字节的内存空间,即空类的大小为1。
空基类被继承后,如果派生类有自己的数据成员,那么空基类这1个字节不会添加到派生类中。
一个指向 Zoo4nimal 的指针是如何地与一个指向整数的指针或一个指向 template Array(如下,与一个 Sring 一并产生)的指针有所不同呢?
ZooAnimal *px;
int *pi;
Array< string > *pta;
以内存需求的观点来说,没有什么不同!它们三个都需要有足够的内存来放置一个机器地址.
“指向不同类型之各指针”间的差异,既不在其指针表示法不同,也不在其内容(代表一个地址)不同,而是在其所寻址出来的 object 类型不同。
也就是说,“指针类型”会教导编译器如何解释某个特定地址中的内存内容及其大小
嗯,那么,一个指向地址1000而类型为void*的指针,将涵盖怎样的地址空间呢?
是的,我们不知道!这就是为什么一个类型为void*的指针只能够含有一个地址,而不能够通过它操作所指之 object 的缘故,
所以,转型(cast)其实是一种编译器指令。大部分情况下它并不改变一个指针所含的真正地址,它只影响“被指出之内存的大小和其内容”的解释方式,