本文是《Inside the C++ Object Model》第三章的读书笔记。主要讨论C++ data member的内存布局。这里的data member 包含了class有虚函数时的vptr和vtable的布局情况。

1. 开头几个小问题

1.  首先回答一个问题: 一个空类,sizeof是多少?答案是1。因为编译器会生成一个隐晦的1bytes,用于区分,当该类多个对象时,各个对象都能在内存分配唯一地址。

2.  还有虚函数表的指针vptr,可能在类的开始,也可能在类的结尾。通常是类的结尾。(注:比较新的VC++和GCC都是在开头。不知道是否所有的版本都是)。

3.  关于成员变量的内存对齐,例如一个类只有char a一个属性; 但是它的大小是4(32位。64位机器是8?但是我是用GCC的sizeof仍然是1。熟悉汇编应该知道,这个地址应该不会存其他的内容了,因此说sizeof是4/8也可理解)。虽然char的大小是1。

4.  属性的内存顺序和声明顺序是一致的。不同级别(public、protected和private)属性的排列顺序是相对一致的,就是说可能不连续,但是必须符合较晚出现的属性存在较高的地址。

2. vptr值的不同存储方式

以下的图来自http://blog.csdn.net/hherima同学。非常感谢hherima同学的图。我将使用hherima同学的图,加上我自身的理解来彻底巩固并且分享给各位可爱的程序猿们。

下图演示单一继承并含有虚函数情况下的数据布局。Point2d 和Point3d是继承关系。Point2d含有虚函数,而Point3d自身没有虚函数。

注意:vptr放在类的末尾。这种方式在刚开始被很多编译器采用,因为可以保存base c struct的内存布局。

但是到了C++2.0,开始支持虚拟继承和抽象基类;并且由于OO的兴起,某些编译器开始把vptr放到class object的起头处。比如微软的第一个C++编译器就是采用这种方法。



   前端存放的好处就是编译器可以直接访问虚函数表而不需要通过offset。当然代价就是与C的struct不再兼容。但是谁会从一个C struct派生出具有虚函数的C++ class呢?

如果是前端存放,还存在一个问题:如果基类没有虚函数,派生类有虚函数,那么单一继承的自然多态就会被打破。如果要将派生类转换成基类,必须编译器的介入。但是这种情况也比较少,因此多态就是为了继承,谁会设计出这种继承呢?既然这不是大多数的case,采用vptr在开头,那么就具有很好的意义。这种conventional实际上很利于编译器将C++编译到汇编,而且汇编也比较容易读。否则,放在结尾的话,每个class的data member数量是不一样的,因此vptr存储的offset也不一样。而放到头上,那么0号位置存的就是vptr,1号位置存的就是第一个data
member,这样不单利于编译代码,也便于我们阅读反汇编的汇编代码。

3. 数据成员(data member)的内存布局

在上一小节中我们讨论了vptr的不同存放方式。编译器需要通过设置offset来存取vptr和data member。在98页关于对一个nonstatic data member的存取操作描述,feel confused:作者的意思是如果是直接取对象的第一个data member,那么需要在对象的地址+1。我不是太明白。如果是存取对象的第一个成员,那么对象的地址应该就是指向第一个成员的,它可能是vptr,也可能是第一个data member。那么如果是汇编,那么直接取该地址的内容,该地址的内容有可能是成员的值,也可能存的仍是地址(指针),那么offset+1没有意义。如果是C++的code,那么本身不需要这么麻烦,谁会直接将对象所在的地址进行解释,而不是通过C++的方式?当然某些高性能编程可能是,但是我实在想不出有任何理由要这样去做。

C++语言保证“出现在派生类中的基类对象,有其完整性”,这么做是为了在位拷贝的时候,能够拷贝正确。一般每个成员都会独占一个地址,意思是在32位机器上,每个数据成员至少占用4个B。当然为了内存对齐,比如有一下class:

class data{
char a;
char b;
int c;
};

那么a和b可能会share一个地址单元,即sizeof(data) = 8;但是子类,父类的数据成员可以为了空间效率share一个地址单元吗?

假如Concrete1 和Concere2都有一个char的属性,而且Concere2继承自Concrete1。那么如果这两个数据成员share一个地址单元会有什么问题?那么我们思考一下以下的赋值能符合我们的预期吗?

Concrete1 *pc1_1, pc1_2;
Concrete2 c2;
pc1_1 = &c2;
//memory allocate for pc1_2
*pc1_2 = *pc1_1;

注意,从pc1_1到pc1_2的memberwise复制(复制一个一个的member)时,pc1_1的char b就被抹掉了。那么pc1_1就丢掉了派生类的信息。而这个复制很显然不是我们需要的!



这也是为什么C++语言保证“出现在派生类中的基类对象,有其完整性”!

3. 多重继承(Multiple Inheritance)

对于一个多重派生对象,将其地址指定给“最左端(也就是第一个)基类的指针”,情况和单一继承时相同,因为两者都指向相同的起始地址。需要付出的成本只是地址的指定操作而已,至于第二个或后继的base class的地址指定操作,则需要进行地址修改:加上或者减去介于中间base class大小。

下图展示了多继承的关系。涉及到4个类 Point2d、Point3d、Vertex和Vertex3d(p115)

下面展示了多重继承的对象模型。

注意,多继承的情况下,drived clas可能会有两个或两个以上虚函数表指针

请看下面的表达式:

Vertex3d   v3d;
Vertex* pv;
Point2d* p2d;
Point3d * p3d;

那么这个操作 pv = &v3d  需要转换内部代码

pv = (Vertex*)(((char*)&v3d) + sizeof(Point3d))

那么如果pv是从另外一个Vertex3d的指针(比如是pv3d)拷贝过来呢?那么需要考虑空指针的情况。

pv = pv3d
?(Vertex*)(((char*)&v3d) + sizeof(Point3d))
:0;

下面这两个操作,只需要拷贝地址就行了。

p2d = &v3d;

p3d = &v3d;

以下引自陈皓先生的名著《C++ 对象的内存布局(上)》中多重继承。使用的是VC++和GCC3.4.4

使用图片表示是下面这个样子:

我们可以看到:

1)  每个父类都有自己的虚表。

2)  子类的成员函数被放到了第一个父类的表中。

3)  内存布局中,其父类布局依次按声明顺序排列。

4)  每个父类的虚表中的f()函数都被overwrite成了子类的f()。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

4.  虚拟多继承情况

下图可以表现Vertex3d 的继承体系图。左为多重继承,右为虚拟多重继承。

各个class的定义如下:

class Point2d{
...
protect:
float _x, _y;
}; class Vertex: public virtual Point2d{
...
protected:
Vertex *next;
}; class Point3d: public virtual Point2d{
...
protected:
float _z;
}; class Vertex3d: public Vertex, public Point3d{
...
protected:
float mumble;
};

不论是Vertex还是Point3d都内含一个Point2d。然而在Vertex3d的对象布局中,我们只需要单一一份Point2d就好。如何使多重继承,那么Vertex3d对象中将有两个Point2d,那么对Point2d的引用可能会有歧义。所以引入虚拟继承。然而编译器要实现虚拟继承,实在是困难度颇高。虚拟继承的原则就是:让VertexPoint3d各自维护的Point2d
折叠成一个有Vertex3d维护的单一Point2d,并且还可以保存base class 和derived class的指针之间的多台指定操作。

如果一个class含有virtual base classsubobjects, 那么,该对象将被分割为两部分:一个不变局部和一个共享局部。不变局部中的数据,不管后继如何演化,总是拥有固定的offset,所以这部分数据可以直接存取。至于共享局部(即virtual base class),这一部分的数据,其位置会因为每次的派生操作而有变化,所以他们只能被间接存取。各家编译器实现技术之间的差异就是间接存取的方法不同。

如何存取class的共享局部呢?cfront编译器会在每一个derived class中安插一个指向virtual base class的指针,这样就可以间接存取。这样的实现模型会有下面两个主要缺点:

1.每一个对象必须针对其每一个virtual base class 背负一个额外的指针。

解决方法有:第一个,Microsoft编译器引入所谓的virtual base class table。每一个class object如果有一个或多个virtual base class,就会由编译器安插一个指针,指向virtual base class table。至于真正的virtual base class 指针,当然是被放在该表格中。

请看下面的虚拟继承对象模型,如图。

红框内即所谓的“共享局部”,其位置会因每次派生操作而有所变化。虚拟破坏了base class 的对象完整型,虚拟继承会在自己类中生成一个虚函数表指针。

第二个、在virtual function table 中放置virtual base class的offset(不是地址)。

这个方法的好处是,巧妙的利用了虚函数表的结构,使得drived class 能够节省一个指针的大小。上图中蓝色曲线是offset

2.由于虚拟继承串链的加长,导致间接存取层次的增加。例如:如果我们有三层虚拟衍化,我就需要三次间接存取(经由三个virtual base class指针)。

这个问题的解决方案有:拷贝所有的virtual base class 的指针到drived class中。这样就解决了存取时间的问题,虽然会有空间的开销。

参考资料:

1. http://blog.csdn.net/haoel/archive/2008/10/15/3081328.aspx

2. http://blog.csdn.net/hherima/article/details/8888539

C++对象模型(五):The Semantics of Data Data语义学的更多相关文章

  1. $.data(data , "")

    今天在二次开发的时候,看到源代码的新闻列表是Aajax获取的,点击新闻内容触发编辑,我没有看到新闻Id却能查到信息. 观看$.ajax遍历赋值过程中,$tr("<a>新闻内容&l ...

  2. [Android Tips] 10. Pull out /data/data/${package_name} files without root access

    #!/usr/bin/env bash PACKAGE_NAME=com.your.package DB_NAME=data.db rm -rf ${DB_NAME} adb shell " ...

  3. Android配置----DDMS 连接真机(己ROOT),用file explore看不到data/data文件夹的解决办法

    Android DDMS 连接真机(己ROOT),用file explore看不到data/data文件夹,问题在于data文件夹没有权限,用360手机助手或豌豆荚也是看不见的. 有以下两种解决方法: ...

  4. android DDMS 连接真机(己ROOT),用file explore看不到data/data文件夹的解决办法

    android DDMS 连接真机(己ROOT),用file explore看不到data/data文件夹的解决办法 问题是没有权限,用360手机助手或豌豆荚也是看不见的. 简单的办法是用RE文件管理 ...

  5. Android /data/data/app_file/目录下面安装apk无权限问题

    当识别SDCard的时候 String filePath = null; String state = Environment.getExternalStorageState(); if (state ...

  6. 如何在安卓/data(而不是/data/data)目录下进行文件的读写操作

    分析:Android默认是无法直接操作/data目录的,只能读写程序自己的私有目录,也就是/data/data/package name/下,默认只能操作这个目录下的文件,也就是我们想直接读写/dat ...

  7. 数据库内存泄漏——A SQLiteConnection object for database '/data/data/.../databases/....db' was leaked!

      详细异常: A SQLiteConnection object for database '/data/data/.../database/....db' was leaked!  Please ...

  8. DDMS中File Explorer无法查看data/data文件解决办法

    http://www.cnblogs.com/smyhvae/p/3881477.html  找了个连接 问题描述:最近在学习Android SQLite中的SQLiteOpenHelper,使用SQ ...

  9. Android获取文件夹路径 /data/data/

    首先内部存储路径为/data/data/youPackageName/,下面讲解的各路径都是基于你自己的应用的内部存储路径下.所有内部存储中保存的文件在用户卸载应用的时候会被删除. 一. files1 ...

  10. Libgdx实现异步加载网络图片并保存到SD卡或者data/data目录下边

    Libgdx实现异步加载网络图片并保存到SD卡或者data/data目录下边,当本地有图片的时候,直接从本地读取图片,如果本地没有图片,将从服务器异步加载图片 package com.example. ...

随机推荐

  1. Uncaught (in promise) TypeError:的错误

    1.错误 创建一个vue实例,在data定义一些变量,如activityTime. 在methods里面用了axios发送请求. 在then的回调里使用this.activityTime 报错! 2. ...

  2. 3.5 find() 判断是否存在某元素

    vector 判断是否存在某元素: if(find(A.begin(), A.end(), A[i]) != A.end()){ // 若存在 A[i] // find() 返回一个指针 }

  3. Linux下查看进程打开的文件句柄数

    ---查看系统默认的最大文件句柄数,系统默认是1024 # ulimit -n ----查看当前进程打开了多少句柄数 # lsof -n|awk '{print $2}'|sort|uniq -c|s ...

  4. south 命令学习

    south 命令学习 概述 在django某个版本之前,django自身提供一个创建数据库的命令-syncdb,它会根据model来创建相应的表,但是这个命令不好的地方在于,如果想要对model进行更 ...

  5. page1

    1.1 常用的客户端技术:HTML. CSS. 客户端脚本技术 1.2 常用的服务器端技术:CGI .ASP .PHP (一种开发动态网页技术).ASP.NET(是一种建立动态web应用程序的技术,是 ...

  6. Node.js 逐行读取

    逐行读取 稳定性: 2 - 不稳定 使用 require('readline'),可以使用这个模块.逐行读取(Readline)可以逐行读取流(比如process.stdin) 一旦你开启了这个模块, ...

  7. Docker内核能力机制

    能力机制(Capability)是 Linux 内核一个强大的特性,可以提供细粒度的权限访问控制. Linux 内核自 2.2 版本起就支持能力机制,它将权限划分为更加细粒度的操作能力,既可以作用在进 ...

  8. Gradle 1.12用户指南翻译——第四十七章. Build Init 插件

    本文由CSDN博客貌似掉线翻译,其他章节的翻译请参见: http://blog.csdn.net/column/details/gradle-translation.html 翻译项目请关注Githu ...

  9. (译)快速指南:用UIViewPropertyAnimator做动画

    翻译自:QUICK GUIDE: ANIMATIONS WITH UIVIEWPROPERTYANIMATOR 译者:Haley_Wong iOS 10 带来了一大票有意思的新特性,像 UIViewP ...

  10. How to code like a hacker

    We are coding. Are we engineers? Are we programmers? Are we coder? No, I want to be a hacker! Many g ...