C++ 对象的内存布局(上)

对象的影响因素

简而言之,我们一个类可能会有如下的影响因素:

1)成员变量

2)虚函数(产生虚函数表)

3)单一继承(只继承于一个类)

4)多重继承(继承多个类)

5)重复继承(继承的多个父类中其父类有相同的超类)

6)虚拟继承(使用virtual方式继承,为了保证继承后父类的内存布局只会存在一份)

上述的东西通常是C++这门语言在语义方面对对象内部的影响因素,当然,还会有编译器的影响(比如优化),还有字节对齐的影响。在这里我们都不讨论,我们只讨论C++语言上的影响。

本篇文章着重讨论下述几个情况下的C++对象的内存布局情况。

1)单一的一般继承(带成员变量、虚函数、虚函数覆盖)

2)单一的虚拟继承(带成员变量、虚函数、虚函数覆盖)

3)多重继承(带成员变量、虚函数、虚函数覆盖)

4)重复多重继承(带成员变量、虚函数、虚函数覆盖)

5)钻石型的虚拟多重继承(带成员变量、虚函数、虚函数覆盖)

我们的目标就是,让事情越来越复杂。

知识复习

我们简单地复习一下,我们可以通过对象的地址来取得虚函数表的地址,如:

 typedef void(*Fun)(void);

            Base b;

            Fun pFun = NULL;

            cout << "虚函数表地址:" << (int*)(&b) << endl;
            cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;

            // Invoke the first virtual function
            pFun = (Fun)*((int*)*(int*)(&b));
            pFun();

我们同样可以用这种方式来取得整个对象实例的内存布局。因为这些东西在内存中都是连续分布的,我们只需要使用适当的地址偏移量,我们就可以获得整个内存对象的布局。

本篇文章中的例程或内存布局主要使用如下编译器和系统:

1)Windows 7 和 VC++ 2010

2)Cygwin 和 G++ 3.4.4

单一的一般继承

下面,我们假设有如下所示的一个继承关系:

请注意,在这个继承关系中,父类,子类,孙子类都有自己的一个成员变量。而了类覆盖了父类的f()方法,孙子类覆盖了子类的g_child()及其超类的f()。

我们的源程序如下所示:

#include <iostream>
using namespace std;

class Parent {
public:
	int iparent;
	Parent ():iparent (10) {}
	virtual void f() { cout << " Parent::f()" << endl; }
	virtual void g() { cout << " Parent::g()" << endl; }
	virtual void h() { cout << " Parent::h()" << endl; }

};

class Child : public Parent {
public:
	int ichild;
	Child():ichild(100) {}
	virtual void f() { cout << "Child::f()" << endl; }
	virtual void g_child() { cout << "Child::g_child()" << endl; }
	virtual void h_child() { cout << "Child::h_child()" << endl; }
};

class GrandChild : public Child{
public:
	int igrandchild;
	GrandChild():igrandchild(1000) {}
	virtual void f() { cout << "GrandChild::f()" << endl; }
	virtual void g_child() { cout << "GrandChild::g_child()" << endl; }
	virtual void h_grandchild() { cout << "GrandChild::h_grandchild()" << endl; }
};

int _tmain(int argc, _TCHAR* argv[])
{
	typedef void(*Fun)(void);

	GrandChild gc;

	Fun pFun=NULL;

	int** pVtab = (int**)&gc;

	cout << "[0] GrandChild::_vptr->" << endl;
	/*for (int i=0; (Fun)pVtab[0][i]!=NULL; ++i){*/
	//本人注释 测试环境win7+VS2010 经过调试发现此环境下虚函数并不是以NULL结尾,所以原文修改为如下:
	for (int i=0; i<6; ++i){
		pFun = (Fun)pVtab[0][i];
		cout << "    ["<<i<<"] ";
		pFun();

	}
	cout << "[1] Parent.iparent = " << (int)pVtab[1] << endl;
	cout << "[2] Child.ichild = " << (int)pVtab[2] << endl;
	cout << "[3] GrandChild.igrandchild = " << (int)pVtab[3] << endl;

	return 0;
}

使用图片表示如下:(红色部分为本人修改)

可见以下几个方面:

1)虚函数表在最前面的位置。

2)成员变量根据其继承和声明顺序依次放在后面。

3)在单一的继承中,被overwrite的虚函数在虚函数表中得到了更新。

多重继承

下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类只overwrite了父类的f()函数,而还有一个是自己的函数(我们这样做的目的是为了用g1()作为一个标记来标明子类的虚函数表)。而且每个类中都有一个自己的成员变量:

我们的类继承的源代码如下所示:父类的成员初始为10,20,30,子类的为100

#include <iostream>
using namespace std;

class Base1 {
public:
	int ibase1;
	Base1():ibase1(10) {}
	virtual void f() { cout << "Base1::f()" << endl; }
	virtual void g() { cout << "Base1::g()" << endl; }
	virtual void h() { cout << "Base1::h()" << endl; }

};

class Base2 {
public:
	int ibase2;
	Base2():ibase2(20) {}
	virtual void f() { cout << "Base2::f()" << endl; }
	virtual void g() { cout << "Base2::g()" << endl; }
	virtual void h() { cout << "Base2::h()" << endl; }
};

class Base3 {
public:
	int ibase3;
	Base3():ibase3(30) {}
	virtual void f() { cout << "Base3::f()" << endl; }
	virtual void g() { cout << "Base3::g()" << endl; }
	virtual void h() { cout << "Base3::h()" << endl; }
};

class Derive : public Base1, public Base2, public Base3 {
public:
	int iderive;
	Derive():iderive(100) {}
	virtual void f() { cout << "Derive::f()" << endl; }
	virtual void g1() { cout << "Derive::g1()" << endl; }
};

int _tmain(int argc, _TCHAR* argv[])
{
	typedef void(*Fun)(void);

	Derive d;

	Fun pFun=NULL;

	int** pVtab = (int**)&d;

	cout << "[0] Base1::_vptr->" << endl;
	pFun = (Fun)pVtab[0][0];
	cout << "     [0] ";
	pFun();

	pFun = (Fun)pVtab[0][1];
	cout << "     [1] ";pFun();

	pFun = (Fun)pVtab[0][2];
	cout << "     [2] ";pFun();

	pFun = (Fun)pVtab[0][3];
	cout << "     [3] "; pFun();

	pFun = (Fun)pVtab[0][4];
	cout << "     [4] "; cout<<pFun<<endl;

	cout << "[1] Base1.ibase1 = " << (int)pVtab[1] << endl;

	int s = sizeof(Base1)/4;

	cout << "[" << s << "] Base2::_vptr->"<<endl;
	pFun = (Fun)pVtab[s][0];
	cout << "     [0] "; pFun();

	pFun = (Fun)pVtab[s][1];
	cout << "     [1] "; pFun();

	pFun = (Fun)pVtab[s][2];
	cout << "     [2] "; pFun();

	pFun = (Fun)pVtab[s][3];
	cout << "     [3] ";
	cout<<pFun<<endl;

	cout << "["<< s+1 <<"] Base2.ibase2 = " << (int)pVtab[s+1] << endl;

	s = s + sizeof(Base2)/4;

	cout << "[" << s << "] Base3::_vptr->"<<endl;
	pFun = (Fun)pVtab[s][0];
	cout << "     [0] "; pFun();

	pFun = (Fun)pVtab[s][1];
	cout << "     [1] "; pFun();

	pFun = (Fun)pVtab[s][2];
	cout << "     [2] "; pFun();

	pFun = (Fun)pVtab[s][3];
	cout << "     [3] ";
	cout<<pFun<<endl;

	s++;
	cout << "["<< s <<"] Base3.ibase3 = " << (int)pVtab[s] << endl;
	s++;
	cout << "["<< s <<"] Derive.iderive = " << (int)pVtab[s] << endl;

	return 0;
}

我们通过上面的程序来查看子类实例的内存布局:上面程序中,注意我使用了一个s变量,其中用到了sizof(Base)来找下一个类的偏移量。(因为我声明的是int成员,所以是4个字节,所以没有对齐问题。关于内存的对齐问题,大家可以自行试验,我在这里就不多说了)

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

我们可以看到:

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

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

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

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

时间: 2024-11-02 19:24:16

C++ 对象的内存布局(上)的相关文章

C++ 对象的内存布局(下)

原文地址:http://blog.csdn.net/haoel/article/details/3081385 (注:看本文的时候由于宿舍快断电了,来不及细看,所以怕自己忘记,先贴出来.不排除文章有错误,大家自己测试一下.) 重复继承 下面我们再来看看,发生重复继承的情况.所谓重复继承,也就是某个基类被间接地重复继承了多次. 下图是一个继承图,我们重载了父类的f()函数. 其类继承的源代码如下所示.其中,每个类都有两个变量,一个是整形(4字节),一个是字符(1字节),而且还有自己的虚函数,自己o

浅谈C++中派生类对象的内存布局_C 语言

主要从三个方面来讲: 1 单一继承 2 多重继承 3 虚拟继承 1 单一继承 (1)派生类完全拥有基类的内存布局,并保证其完整性. 派生类可以看作是完整的基类的Object再加上派生类自己的Object.如果基类中没有虚成员函数,那么派生类与具有相同功能的非派生类将不带来任何性能上的差异.另外,一定要保证基类的完整性.实际内存布局由编译器自己决定,VS里,把虚指针放在最前边,接着是基类的Object,最后是派生类自己的object.举个栗子: class A { int b; char c; }

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

      前一段时间再次拜读<Inside the C++ Object Model> 深入探索C++对象模型,有了进一步的理解,因此我也写了四篇博文算是读书笔记: Program Transformation Semantics (程序转换语义学) The Semantics of Copy Constructors(拷贝构造函数之编译背后的行为) The Semantics of Constructors: The Default Constructor (默认构造函数什么时候会被创建出

对象内存布局 (5)

内容概要: 满足下面3个条件时, 1. 父类有虚函数,子类也有虚函数,且子类的虚函数重写或覆盖了父类的虚函数 2. 非虚继承 3. 多重继承 类对象之内存布局 多重继承,派生类不重写基类中的虚函数. 假定各类之间的关系如下图:     代码如下: #include <iostream> using namespace std; class Base1 { public: int m_base1; inline virtual void vfBase1_1() { cout << &

对象内存布局 (14)

前言 07年12月,我写了一篇<C++虚函数表解析>的文章,引起了大家的兴趣.有很多朋友对我的文章留了言,有鼓励我的,有批评我的,还有很多问问题的.我在这里一并对大家的留言表示感谢.这也是我为什么再写一篇续言的原因.因为,在上一篇文章中,我用了的示例都是非常简单的,主要是为了说明一些机理上的问题,也是为了图一些表达上方便和简单.不想,这篇文章成为了打开C++对象模型内存布局的一个引子,引发了大家对C++对象的更深层次的讨论.当然,我之前的文章还有很多方面没有涉及,从我个人感觉下来,在谈论虚函数

对象内存布局 (15)

重复继承   下面我们再来看看,发生重复继承的情况.所谓重复继承,也就是某个基类被间接地重复继承了多次.   下图是一个继承图,我们重载了父类的f()函数.   其类继承的源代码如下所示.其中,每个类都有两个变量,一个是整形(4字节),一个是字符(1字节),而且还有自己的虚函数,自己overwrite父类的虚函数.如子类D中,f()覆盖了超类的函数, f1()和f2() 覆盖了其父类的虚函数,Df()为自己的虚函数. class B {     public:         int ib;  

C++对象布局及多态实现探索之内存布局

为了便于分析和观察对象的内存布局,我把代码生成时的结构成员对齐选项设置为1字节,默认为8字节.如果你在自己的工程下编译文中的代码,请做同样的设置.因为我写了一些函数打印对象中的布局信息,如果对象选项不是1字节,运行这些代码会出现指针异常错误. 普通类对象的内存布局 首先我们从普通类对象的内存布局开始.c000为一个空类,定义如下: struct c000 {}; 运行如下代码打印它的大小及对象中的内容. print_size_detail(c000) 结果为: the size of c000

对象内存布局 (1)

内容概要: 满足下面2个条件时, 1. 父类有虚函数,子类无虚函数(即无虚函数重写或无虚函数覆盖) 2. 非虚继承 类对象之内存布局   前述相关内容参考: 1. http://blog.csdn.net/pathuang68/archive/2009/04/20/4096088.aspx 2. http://blog.csdn.net/pathuang68/archive/2009/04/21/4096429.aspx 3. http://blog.csdn.net/pathuang68/ar

对象内存布局 (2)

内容概要: 满足下面2个条件时, 1. 父类有虚函数,子类也有虚函数,但子类并没有重写或覆盖父类的虚函数 2. 非虚继承 类对象之内存布局 如果在Derived类中增加一个下面的虚函数,会怎么样呢?Base类和Derived类之间的关系如下:    新加入的虚函数定义如下: #include <iostream> using namespace std; class Base { public: int m_base; inline virtual void vfBase_1() { cout