前一段时间再次拜读《Inside the C++ Object Model》 深入探索C++对象模型,有了进一步的理解,因此我也写了四篇博文算是读书笔记:

Program Transformation Semantics (程序转换语义学)

The Semantics of Copy Constructors(拷贝构造函数之编译背后的行为)

The Semantics of Constructors: The Default Constructor (默认构造函数什么时候会被创建出来)

The Semantics of Data: Data语义学 深入探索C++对象模型

这些文章都获得了很大的浏览量,虽然类似的博文原来都有,可能不容易被现在仍活跃在CSDN Blog的各位同仁看到吧。因此萌生了接着将这这本书读完的同时,再接着谈一下我的理解,或者说读书笔记。

关于C++虚函数,很多博文从各个角度来探究虚函数是如何实现的,或者说编译器是如何实现虚函数的。比较经典的文章有陈皓先生的《C++虚函数表解析》和《C++对象内存布局》。本文通过GDB来从另外一个角度来理解C++
object的内存布局,一来熟悉语言背后编译器为了实现语言特性为我们做了什么;二来熟悉使用GDB来调试程序。

同时,本文也将对如何更好的理解C++语言提供了一个方法:使用GDB,可以很直观的理解编译器的实现,从根本上掌握C++!我们不单单只会开车,还应该知道车的内部的构造。

2、带有虚函数的单一继承

class Parent
{
public:
  Parent():numInParent(1111)
  {}
  virtual void Foo(){
  };
  virtual void Boo(){
  };
private:
  int numInParent;
}; class Child: public Parent
{
public:
  Child():numInChild(2222){}
  virtual void Foo(){
  }
  int numInChild;
};

编译时不要忘记-g,使得gdb可以把各个地址映射成函数名。

(gdb) set p obj on
(gdb) p *this
$2 = (Child) {<Parent> = {_vptr.Parent = 0x400a30, numInParent = 1111}, numInChild = 2222}
(gdb) set p pretty on
(gdb) p *this
$3 = (Child) {
<Parent> = {
_vptr.Parent = 0x400a30,
numInParent = 1111
},
members of Child:
numInChild = 2222
}
(gdb) p /a (*(void ***)this)[0]@3
$4 = {0x4008ec <Child::Foo()>, 0x4008b4 <Parent::Boo()>, 0x6010b0 <_ZTVN10__cxxabiv120__si_class_type_infoE@@CXXABI_1.3+16>}

解释一下gdb的命令:

set p obj <on/off>: 在C++中,如果一个对象指针指向其派生类,如果打开这个选项,GDB会自动按照虚方法调用的规则显示输出,如果关闭这个选项的话,GDB就不管虚函数表了。这个选项默认是off。 使用show print object查看对象选项的设置。

set p pertty <on/off>: 按照层次打印结构体。可以从设置前后看到这个区别。on的确更容易阅读。

p /a (*(void ***)this)[0]@3

就是打印虚函数表了。因为知道是两个,可以仅仅打印2个元素。为了知道下一个存储了什么信息,我们打印了3个值。实际上后几个元素存储了Parent 和Child的typeinfo name和typeinfo。

总结:

对于单一继承,

1. vptr存储到了object的开始。

2. 在vptr之后,从Parent开始的data member按照声明顺序依次存储。

3. 多重继承,包含有相同的父类

对应的C++codes:

class Point2d{
public:
virtual void Foo(){}
virtual void Boo(){}
virtual void non_overwrite(){}
protected:
float _x, _y;
}; class Vertex: public Point2d{
public:
virtual void Foo(){}
virtual void BooVer(){}
protected:
Vertex *next;
}; class Point3d: public Point2d{
public:
virtual void Boo3d(){}
protected:
float _z;
}; class Vertex3d: public Vertex, public Point3d{
public:
void test(){}
protected:
float mumble;
};

使用GDB打印的对象内存布局:

 <Vertex> = {
<Point2d> = {
_vptr.Point2d = 0x400ab0,
_x = 5.88090213e-39,
_y = 0
},
members of Vertex:
next = 0x0
},
<Point3d> = {
<Point2d> = {
_vptr.Point2d = 0x400ae0,
_x = -nan(0x7fe180),
_y = 4.59163468e-41
},
members of Point3d:
_z = 0
},
members of Vertex3d:
mumble = 0
}

可见v3d有两个vptr,指向不同的vtable。首先看一下第一个:

(gdb) p /a (*(void ***)this)[0]@5
$9 = {0x4008be <Vertex::Foo()>,
0x4008aa <Point2d::Boo()>,
0x4008b4 <Point2d::non_overwrite()>,
0x4008c8 <Vertex::BooVer()>,
0xffffffffffffffe8}
(gdb) p /a (*(void ***)this)[0]@6
$10 = {0x4008be <Vertex::Foo()>,
0x4008aa <Point2d::Boo()>,
0x4008b4 <Point2d::non_overwrite()>,
0x4008c8 <Vertex::BooVer()>,
0xffffffffffffffe8,
0x400b00 <_ZTI8Vertex3d>}
(gdb) info addr _ZTI8Vertex3d
Symbol "typeinfo for Vertex3d" is at 0x400b00 in a file compiled without debugging.

你可以注意到了,vtable打印分行了,可以使用 set p array on将打印的数组分行,以逗号结尾。

注意到该虚函数表以

0xffffffffffffffe8

结尾。在单一继承中是没有这个结束标识的。

接着看第二个vtable:

(gdb) p /a (*(void ***)this)[1]@5
$11 = {0x4008b2 <Point2d::Boo()>,
0x4008bc <Point2d::non_overwrite()>,
0x4008d0 <Vertex::BooVer()>,
0xffffffffffffffe8,
0x400b00 <_ZTI8Vertex3d>}
(gdb) info addr _ZTI8Vertex3d
Symbol "typeinfo for Vertex3d" is at 0x400b00 in a file compiled without debugging.

当然这个只是为了举个例子。现实中很少有人这么干吧。比如访问Foo,下面的code将会导致歧义性错误:

v3d.Boo();

error: request for member Boo is ambiguous

multiInheritance.cpp:8: error: candidates are: virtual void Point2d::Boo()

只能指定具体的subobject才能进行具体调用:

v3d.::Vertex::Boo();

4. 虚拟继承

C++ codes:

class Point2d{
public:
virtual void Foo(){}
virtual void Boo(){}
virtual void non_overwrite(){}
protected:
float _x, _y;
}; class Vertex: public virtual Point2d{
public:
virtual void Foo(){}
virtual void BooVer(){}
protected:
Vertex *next;
}; class Point3d: public virtual Point2d{
public:
virtual void Boo3d(){}
protected:
float _z;
}; class Vertex3d: public Vertex, public Point3d{
public:
void test(){}
protected:
float mumble;
};

继承关系图:

使用gdb打印object的内存布局:

(gdb) p *this
$10 = (Vertex3d) {
<Vertex> = {
<Point2d> = {
_vptr.Point2d = 0x400b70,
_x = 0,
_y = 0
},
members of Vertex:
_vptr.Vertex = 0x400b18,
next = 0x4009c0
},
<Point3d> = {
members of Point3d:
_vptr.Point3d = 0x400b40,
_z = 5.87993804e-39
},
members of Vertex3d:
mumble =  0
}

gdb打印的vptr相关:

(gdb) p /a (*(void ***)this)[0]@60
$25 = {0x400870 <Vertex::Foo()>,
0x40087a <Vertex::BooVer()>,
0x10,
0xfffffffffffffff0,
0x400c80 <_ZTI8Vertex3d>, #"typeinfo for Vertex3d"
0x400884 <Point3d::Boo3d()>,
0x0,
0x0,
0xffffffffffffffe0,
0xffffffffffffffe0,
0x400c80 <_ZTI8Vertex3d>, #"typeinfo for Vertex3d"
0x400866 <_ZTv0_n24_N6Vertex3FooEv>, #"virtual thunk to Vertex::Foo()"
0x400852 <Point2d::Boo()>,
0x40085c <Point2d::non_overwrite()>,
0x0,
0x0,
0x0,
0x20,
0x0,
0x400cc0 <_ZTI6Vertex>, #"typeinfo for Vertex"
0x400870 <Vertex::Foo()>,
0x40087a <Vertex::BooVer()>,
0x0,
0x0,
0xffffffffffffffe0,
0xffffffffffffffe0,
0x400cc0 <_ZTI6Vertex>, #"typeinfo for Vertex"
0x400866 <_ZTv0_n24_N6Vertex3FooEv>, #"virtual thunk to Vertex::Foo()"
0x400852 <Point2d::Boo()>,
0x40085c <Point2d::non_overwrite()>,
0x0,
0x0,
0x0,
0x10,
0x0,
0x400d00 <_ZTI7Point3d>, #"typeinfo for Point3d"
0x400884 <Point3d::Boo3d()>,
0x0,
0x0,
0x0,
0xfffffffffffffff0,
0x400d00 <_ZTI7Point3d>, #"typeinfo for Point3d"
0x400848 <Point2d::Foo()>,
0x400852 <Point2d::Boo()>,
0x40085c <Point2d::non_overwrite()>,
0x6020b0 <_ZTVN10__cxxabiv121__vmi_class_type_infoE@@CXXABI_1.3+16>,
0x400d28 <_ZTS8Vertex3d>,
0x200000002,
0x400cc0 <_ZTI6Vertex>, #"typeinfo for Vertex"
0x2,
0x400d00 <_ZTI7Point3d>, #"typeinfo for Point3d"
0x1002,
0x0,
0x6020b0 <_ZTVN10__cxxabiv121__vmi_class_type_infoE@@CXXABI_1.3+16>,
0x400d32 <_ZTS6Vertex>,
0x100000000,
0x400d40 <_ZTI7Point2d>,
0xffffffffffffe803,
0x0,
0x0}

有兴趣的话可以看一下反汇编的vtable的构成。

参考:

1. http://stackoverflow.com/questions/6191678/print-c-vtables-using-gdb

2. http://stackoverflow.com/questions/18363899/how-to-display-a-vtable-by-name-using-gdb

尊重原创,转载请注明出处: anzhsoft http://blog.csdn.net/anzhsoft/article/details/18600163

Linux Debugging(四): 使用GDB来理解C++ 对象的内存布局(多重继承,虚继承)的更多相关文章

  1. 深入理解 Java 对象的内存布局

    对于 Java 虚拟机,我们都知道其内存区域划分成:堆.方法区.虚拟机栈等区域.但一个对象在 Java 虚拟机中是怎样存储的,相信很少人会比较清楚地了解.Java 对象在 JVM 中的内存布局,是我们 ...

  2. 深入理解JVM(三) -- 对象的内存布局和访问定位

    一 对象的内存布局: 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding). HotSpot的对 ...

  3. 理解Java对象:要从内存布局及底层机制说起,话说….

    前言 大家好,又见面了,今天是JVM专题的第二篇文章,在上一篇文章中我们说了Java的类和对象在JVM中的存储方式,并使用HSDB进行佐证,没有看过上一篇文章的小伙伴可以点这里:<类和对象在JV ...

  4. 深入理解Java虚拟机02--Java内存区域与内存溢出异常

    一.概述 我们在进行 Java 开发的时候,很少关心 Java 的内存分配等等,因为这些活都让 JVM 给我们做了.不仅自动给我们分配内存,还有自动的回收无需再占用的内存空间,以腾出内存供其他人使用. ...

  5. [深入理解Java虚拟机]<自动内存管理>

    Overview 走近Java:介绍Java发展史 第二部分:自动内存管理机制 程序员把内存控制的权利交给了Java虚拟机,从而可以在编码时享受自动内存管理.但另一方面一旦出现内存泄漏和溢出等问题,就 ...

  6. 深入理解java虚拟机(二)HotSpot Java对象创建,内存布局以及访问方式

    内存中对象的创建.对象的结构以及访问方式. 一.对象的创建 在语言层面上,对象的创建只不过是一个new关键字而已,那么在虚拟机中又是一个怎样的过程呢? (一)判断类是否加载.虚拟机遇到一条new指令的 ...

  7. Linux Debugging(三): C++函数调用的参数传递方法总结(通过gdb+反汇编)

    上一篇文章<Linux Debugging:使用反汇编理解C++程序函数调用栈>没想到能得到那么多人的喜爱,因为那篇文章是以32位的C++普通函数(非类成员函数)为例子写的,因此只是一个特 ...

  8. Linux Debugging(五): coredump 分析入门

    作为工作几年的老程序猿,肯定会遇到coredump,log severity设置的比较高,导致可用的log无法分析问题所在. 更悲剧的是,这个问题不好复现!所以现在你手头唯一的线索就是这个程序的尸体: ...

  9. Linux Debugging(二): 熟悉AT&T汇编语言

    没想到<Linux Debugging:使用反汇编理解C++程序函数调用栈>发表了收到了大家的欢迎.但是有网友留言说不熟悉汇编,因此本书列了汇编的基础语法.这些对于我们平时的调试应该是够用 ...

随机推荐

  1. 线性回归(Linear Regression)均方误差损失函数最小化时关于参数theta的解析解的推导(手写)

    第一页纸定义了损失函数的样子, theta, X 和 y 的 shape, 以及最终的损失函数向量表现形式. 第二页纸抄上了几个要用到的矩阵求导公式,以及推导过程和结果. 要说明的是:推导结果与the ...

  2. 排序算法的C语言实现(上 比较类排序:插入排序、快速排序与归并排序)

    总述:排序是指将元素集合按规定的顺序排列.通常有两种排序方法:升序排列和降序排列.例如,如整数集{6,8,9,5}进行升序排列,结果为{5,6,8,9},对其进行降序排列结果为{9,8,6,5}.虽然 ...

  3. 利用Filter和拦截器,将用户信息动态传入Request方法

    前言: 在开发当中,经常会验证用户登录状态和获取用户信息.如果每次都手动调用用户信息查询接口,会非常的繁琐,而且代码冗余.为了提高开发效率,因此就有了今天这篇文章. 思路: 用户请求我们的方法会携带一 ...

  4. ubuntu重装指定版本的mysql

    查看错误log cat /var/log/mysql/error.log 首先彻底删除mysql,比如版本5.5 apt-get autoremove --purge mysql-server-5.5 ...

  5. dynamic initializer和全局变量

    "慎用全局变量,包括全局静态变量" 是众所周知的原则,因为全局变量除了会增加程序的维护成本. 如果全局变量是个复杂的对象,并且还使用其他的全局变量,那情况就变得复杂的多.因为全局变 ...

  6. 豌豆夹Redis解决方案Codis安装使用

    豌豆夹Redis解决方案Codis安装使用 1.安装 1.1 Golang环境 Golang的安装非常简单,因为官网被墙,可以从国内镜像如studygolang.com下载. [root@vm roo ...

  7. Ajax原理学习

    一.AJAX 简介 AJAX即"Asynchronous Javascript And XML"(异步JavaScript和XML),是指一种创建交互式网页应用的网页开发技术. A ...

  8. hive中的NULL(hive空值处理)

    HIVE表中默认将NULL存为\N,可查看表的源文件(hadoop fs -cat或者hadoop fs -text),文件中存储大量\N, 这样造成浪费大量空间.而且用java.python直接进入 ...

  9. [Matlab]技巧笔记

    1.将字符串作为Matlab命令执行 md = 'dir'; eval(md); 2.将字符串作为系统命令执行 md = 'dir'; system(md); 3.使显示图像的坐标轴使用相同的度量单位 ...

  10. C++ ifstream,ofstream读写二进制文件

    为什要吧数据存为二进制 这个嘛,是我个人习惯,一般,我们会把日志文件存为文本文件.数据文件存成二进制文件. 其实,我们接触的文件,比如图像.视频都是以二进制的形式存储的,要想查看这类数据,必须知道数据 ...