C++ 内存布局:深入理解C++内存布局

1、虚函数简介

虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数。典型情况下,这一信息具有一种被称为vptr(virtual table pointer,虚函数表指针)的指针的形式。vptr 指向一个被称为 vtbl(virtual table,虚函数表)的函数指针数组,每一个包含虚函数的类都关联到
vtbl。当一个对象调用了虚函数,实际的被调用函数通过下面的步骤确定:找到对象的 vptr 指向的 vtbl,然后在 vtbl 中寻找合适的函数指针。

虚拟函数的地址翻译取决于对象的内存地址,而不取决于数据类型(编译器对函数调用的合法性检查取决于数据类型)。如果类定义了虚函数,该类及其派生类就要生成一张虚拟函数表,即vtable。而在类的对象地址空间中存储一个该虚表的入口,占4个字节,这个入口地址是在构造对象时由编译器写入的。所以,由于对象的内存空间包含了虚表入口,编译器能够由这个入口找到恰当的虚函数,这个函数的地址不再由数据类型决定了。故对于一个父类的对象指针,调用虚拟函数,如果给他赋父类对象的指针,那么他就调用父类中的函数,如果给他赋子类对象的指针,他就调用子类中的函数(取决于对象的内存地址)。

2、C++中含有虚函数的内存分布

涉及到虚函数的内存分布往往比较复杂,除了考虑其本身所带来的额外的内存开销,还要考虑继承等所带来的问题。针对这一方面,我们按照如下的步骤逐一解决。

1)、单个含有虚函数的类

2)、基类含有虚函数,使用普通继承,派生类中不含虚函数

3)、基类含有虚函数,使用普通继承,派生类中含有虚函数

4)、基类不含有虚函数,使用虚继承,派生类中不含虚函数

5)、基类不含虚函数,使用虚继承,派生类中含有虚函数

6)、基类含有虚函数,使用虚继承,派生类中不含虚函数

7)、基类含有虚函数,使用虚继承,派生类中含有虚函数

8)、基类含有虚函数,使用虚继承,向下派生多次

9)、基类含有虚函数,多继承

2.1 含有虚函数的单个类

#include <iostream>

template<typename T>
class CPoint
{
public:
	CPoint()
	{
		_x = 0;
		_y = 0;
		_z = 0;
	}

	virtual void setX(T newX)
	{
		//std::cout << "CPoint setX" << std::endl;
		_x = newX;
	}
	virtual void setY(T newY)
	{
		_y = newY;
	}
	virtual void setZ(T newZ = 0)
	{
		_z = newZ;
	}

	virtual T getX() const
	{
		return _x;
	}

	virtual T getY() const
	{
		return _y;
	}

	virtual T getZ() const
	{
		return _z;
	}

protected:
	T _x;
	T _y;
	T _z;
};
void main()
{
	CPoint<double> m_Point;
	std::cout <<"CPoint:"<<	sizeof(m_Point) << std::endl;
	std::cin.get();
}

上面的程序输出结果如下:

上述的代码输出为32,一方面和内存布局有关,另一方面还和内存对齐有关。类模板实例化为double,构建一个对象,对象中有三个数据成员,每个数据成员占8字节。

m_Point对象的内存布局如上图所示,可以看到m_Point内部除了三个成员变量之外,还有一个_vfptr,_vfptr是一个虚函数表的指针,保存的是虚函数表的地址。m_Point内部一共有5个虚函数,所以对应的虚函数表中便有5个与虚函数对应得地址。

由于虚函数表指针占据4个字节,并且处于类的内存地址起始处,所以整个类一共占据32个字节。

2.2基类含有虚函数,使用普通继承,派生类中不含虚函数

修改上面的代码,得到如下的内容

#include <iostream>

template<typename T>
class CPoint
{
public:
	CPoint()
	{
		_x = 0;
		_y = 0;
		_z = 0;
	}

	virtual void setX(T newX)
	{
		//std::cout << "CPoint setX" << std::endl;
		_x = newX;
	}
	virtual void setY(T newY)
	{
		_y = newY;
	}
	virtual void setZ(T newZ = 0)
	{
		_z = newZ;
	}

	virtual T getX() const
	{
		return _x;
	}

	virtual T getY() const
	{
		return _y;
	}

	virtual T getZ() const
	{
		return _z;
	}

protected:
	T _x;
	T _y;
	T _z;
};

template<typename T>
class CPoint2D :  public CPoint<T>
{
public:
	CPoint2D()
	{
		_x = 0;
		_y = 0;
		_z = 0;
	}

	CPoint2D(T x, T y, T z = 0)
	{
		_x = x;
		_y = y;
		_z = z;
	}

	CPoint2D(const CPoint2D &point2D)
	{
		_x = point2D.getX();
		_y = point2D.getY();
		_z = point2D.getZ();
	}

	const CPoint2D& operator = (const CPoint2D& point2D)
	{
		if (this == &point2D)
			return *this;

		_x = point2D.getX();
		_y = point2D.getY();
		_z = point2D.getZ();
	}

	void operator +(const CPoint2D& point2D)
	{
		_x += point2D.getX();
		_y += point2D.getY();
		_z += point2D.getZ();
	}

	void operator -(const CPoint2D &point2D)
	{
		_x -= point2D.getX();
		_y -= point2D.getY();
		_z -= point2D.getZ();
	}
};
<pre name="code" class="cpp">void main()
{
	CPoint<double> m_Point;

	CPoint2D<double> m_Point2D(0.0,0.0);
	std::cout <<"CPoint:"<<	sizeof(m_Point) << std::endl;
	std::cout <<"CPoint2D:"<< sizeof(m_Point2D)<< std::endl;
	std::cout <<"CPoint2D::getZ:"<< sizeof(&CPoint2D<double>::getZ) << std::endl;

	std::cin.get();
}

上面的代码输出得到如下的内容

最后一个输出的是一个函数指针的大小,在没有虚继承的情况下,在X86(Win 32Debug)系统上输出是4.

整个类的大小为32字节,我们看一下内存分布就明白了

可以看到m_Point2D的内存布局和m_Point的内存布局很类似。一个虚函数表指针,然后三个成员变量。虚函数表中的内容和m_Point中的一摸一样。这是因为CPoint2D
是从CPoint继承过来的。

2.3基类含有虚函数,使用普通继承,派生类中含有虚函数

继续修改上面的代码,得到如下的内容

#include <iostream>

template<typename T>
class CPoint
{
public:
	CPoint()
	{
		_x = 0;
		_y = 0;
		_z = 0;
	}

	virtual void setX(T newX)
	{
		//std::cout << "CPoint setX" << std::endl;
		_x = newX;
	}
	virtual void setY(T newY)
	{
		_y = newY;
	}
	virtual void setZ(T newZ = 0)
	{
		_z = newZ;
	}

	virtual T getX() const
	{
		return _x;
	}

	virtual T getY() const
	{
		return _y;
	}

	virtual T getZ() const
	{
		return _z;
	}

protected:
	T _x;
	T _y;
	T _z;
};

template<typename T>
class CPoint2D :  public CPoint<T>
{
public:
	CPoint2D()
	{
		_x = 0;
		_y = 0;
		_z = 0;
	}

	CPoint2D(T x, T y, T z = 0)
	{
		_x = x;
		_y = y;
		_z = z;
	}

	CPoint2D(const CPoint2D &point2D)
	{
		_x = point2D.getX();
		_y = point2D.getY();
		_z = point2D.getZ();
	}

	const CPoint2D& operator = (const CPoint2D& point2D)
	{
		if (this == &point2D)
			return *this;

		_x = point2D.getX();
		_y = point2D.getY();
		_z = point2D.getZ();
	}

	void operator +(const CPoint2D& point2D)
	{
		_x += point2D.getX();
		_y += point2D.getY();
		_z += point2D.getZ();
	}

	void operator -(const CPoint2D &point2D)
	{
		_x -= point2D.getX();
		_y -= point2D.getY();
		_z -= point2D.getZ();
	}

	virtual T getZ() const
	{
		std::cout << "CPoint2D:"<<sizeof(CPoint2D<T>::getZ()) << std::endl;
		return 0;
	}

	virtual void setZ(T newZ = 0)
	{
		//std::cout << "CPoint2D:" << sizeof(CPoint2D::setZ()) << std::endl;
		_z = 0;
	}
};
void main()
{
	CPoint<double> m_Point;

	CPoint2D<double> m_Point2D(0.0,0.0);
	std::cout <<"CPoint:"<<	sizeof(m_Point) << std::endl;
	std::cout <<"CPoint2D:"<< sizeof(m_Point2D)<< std::endl;
	std::cout <<"CPoint2D::getZ:"<< sizeof(&CPoint2D<double>::getZ) << std::endl;

	std::cin.get();
}

上面的代码输出内容如下所示:

内存布局如下:

输出的内容和之前派生类中没有虚函数的一样,但是内存布局发生了变化。变化体现在_vfptr中,_vfptr中有4个地址是和CPoint中的一样,2个不一样,这是因为在CPoint2D中,重写了CPoint中的两个虚函数,从而派生类中的虚函数覆盖了父类中的虚函数。这地方的重写不仅仅是函数名相同,还要保证函数的参数类型,参数个数,函数的返回形式也和基类中的一致。

从上面的例子中我们可以得出以下的结论:

1)、类中一旦出现虚函数,编译器便会给其分配一个虚函数表,虚函数表指针的大小和编译器有关。

2)、派生类中如果对父类的虚函数进行了重写,那么派生类中的虚函数会覆盖父类的虚函数,体现在上图的虚函数表中的地址发生了变化。

3)、虚函数表指针总是处于类的地址的开始处,所以在计算类的大小时要注意这一点。

2.4基类不含有虚函数,使用虚继承,派生类中不含虚函数

这一次使用前一章节的代码,对前一章节的代码进行修改,得到如下的内容

#include <iostream>
using namespace std;

class CBase
{
	//public
public:
	CBase()
	{

	}
};

class CBaseClass
{
	//private members
private:
	int nCount;

	//public members
public:

	//private member funcs
private:
	CBaseClass(const CBaseClass &base)
	{

	}

	CBaseClass &operator = (const CBaseClass& base)
	{
		return *this;
	}

	//public members
public:
	CBaseClass(int count = 0)
	{
		nCount = count;
	}
	~CBaseClass()
	{

	}
};

class CBaseClassNew
{
	//private members
private:
	int nCount;

	//public members
public:
	int nNewCount;
	//private member funcs
private:
	CBaseClassNew(const CBaseClassNew &base)
	{

	}

	CBaseClassNew &operator = (const CBaseClassNew& base)
	{
		return *this;
	}

	//public members
public:
	CBaseClassNew(int count = 0)
	{
		nCount = count;
	}
	~CBaseClassNew()
	{

	}
};

class CDerivedClass : virtual public CBaseClass
{
	//private members:
private:
	int nDeriveCount;

	//public members
public:
	int nCurrentNum;

	//private member funcs
private:
	CDerivedClass(const CDerivedClass& derived)
	{

	}

	CDerivedClass & operator = (const CDerivedClass &derived)
	{
		return *this;
	}
	//public member funcs
public:
	CDerivedClass(int nDerived = 0)
	{
		nDeriveCount = nDerived;
		nCurrentNum = 0;
	}

};
void main()
{
	CBase base;
	cout << "base Size:" << sizeof(base) << endl;

	CBaseClass baseClass(10);
	cout << "baseClass Size:" << sizeof(baseClass) << endl;

	CDerivedClass derivedClass(12);
	cout << "derivedClass Size:" << sizeof(derivedClass) << endl;

	cin.get();

}

上述代码的输出内容如下

CBase
中只有一个构造函数,所以占一个字节

CBaseClass中有一个成员变量,为int型,所以占4个字节

CDerivedClass中自身的2个成员变量和基类中的1个成员变量均是int型,一共12个字节。CDerivedClass使用的是虚继承,这导致在派生类中会产生一个指针指向基类,所以派生类的大小为14字节。

其内存分布如下图所示:

因为篇幅太长,剩下的内容后面再说了。

时间: 2024-08-27 17:00:22

C++ 内存布局:深入理解C++内存布局的相关文章

android:布局、绘制、内存泄露、响应速度、listview和bitmap、线程优化以及一些优化的建议!

1.布局优化 首先删除布局中无用的控件和层级,其次有选择地使用性能较低的viewgroup,比如布局中既可以使用RelativeLayout和LinearLayout,那我们就采用LinearLayout,因为RelativeLayout的功能比较复杂,它的布局需要花费 风度哦的CPU实际. 布局优化的另一个手段就是采用<include>,<merge>,<viewstub>标签.<include>主要用于布局重用,<include>,<m

jvm 深入理解自动内存分配与垃圾回收

要想了解jvm自动内存分配,首先必须了解jvm的运行时数据区域,否则如何知道在哪里进行自动内存分配,如何进行内存分配,回收哪里的垃圾对象? jvm运行时数据区:程序计数器,虚拟机栈,本地方法栈,方法区,堆 程序计数器:由于程序指令是一条一条顺序执行,一条执行完之后必须知道下一条该执行那条指令,那么程序计数器就是来记录下一条指令的地址,如果调用本地方法,则程序计数器记录空值,还有由于java线程由cpu调度并发执行,所以程序计数器也有助于线程状态的恢复,程序计数器如果线程共享,在频繁的线程调度下保

你应该这样理解JVM内存管理

在进行Java程序设计时,一般不涉及内存的分配和内存回收的相关代码,此处引用一句话: Java和C++之间存在一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外的人想进去,墙里面的人想出来 ,个人从这两句话中,捕获到了 两个点 . java的自动内存管理机制,极大的节省了开发人员的精力,避免了易错且复杂的内存管理设计,相对于手动的内存管理这是极大的飞跃.java自动内存管理机制,其不能根据具体的场景提供最优的内存管理,其只提供普适的内存管理机制.想比于C++的手动内存管理,灵活性不够,存在制约系

深入理解C++内存管理机制

关于C++的内存处理,可分为三大块,分别是: (一)内存管理机制 (二)内存泄露处理 (三)内存回收机制 这篇文章将就(一)内存管理机制 进行深入探讨,如有错误欢迎大家指正. C++的内存管理也可细分为 1. 程序内存布局 2. 内存的分配方式 3. 常见内存错误及对策 ---------------------------------------------------------------------------- 一. 程序内存布局 查了相关资料,明白了一点: memory layout

深度理解div+css布局嵌套盒子

1. 网页布局概述 网页布局的概念是把即将出现在网页中的所有元素进行定位,而CSS网页排版技术有别于传统的网页排版方法,它将页面首先在整体上使用<div>标记进行分块,然后对每个快进行CSS定位以及设置显示效果,最后在每个块中添加相应的内容.利用CSS排版方法更容易地控制页面每个元素的效果,更新也更容易,甚至页面的拓扑结构也可以通过修改相应的CSS属性来重新定位.  2. 盒子模型 盒子模型是CSS控制页面元素的一个重要概念,只有掌握了盒子模型,才能让CSS很好地控制页面上每一个元素,达到我们

深入理解Java内存模型(四)——volatile

volatile的特性 当我们声明共享变量为volatile后,对这个变量的读/写将会很特别.理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步.下面我们通过具体的示例来说明,请看下面的示例代码: class VolatileFeaturesExample { //使用volatile声明64位的long型变量 volatile long vl = 0L; public void set(long l) { vl = l;

深入理解Java内存模型(1 ) -- 基础(转载)

原文地址:http://www.infoq.com/cn/articles/java-memory-model-1 并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体).通信是指线程之间以何种机制来交换信息.在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递. 在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信.在消息传递的并发模型里,线程之间没有公共状态,线

深入理解Java内存模型-volatile

volatile的特性 当我们声明共享变量为volatile后,对这个变量的读/写将会很特别.理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个监视器锁对这些单个读/写操作做了同步.下面我们通过具体的示例来说明,请看下面的示例代码: class VolatileFeaturesExample { volatile long vl = 0L; //使用volatile声明64位的long型变量 public void set(long l) { vl =

Java内存泄露的理解与解决

Java内存管理机制 在C++语言中,如果需要动态分配一块内存,程序员需要负责这块内存的整个生命周期.从申请分配.到使用.再到最后的释放.这样的过程非常灵活,但是却十分繁琐,程序员很容易由于疏忽而忘记释放内存,从而导致内存的泄露.Java语言对内存管理做了自己的优化,这就是垃圾回收机制.Java的几乎所有内存对象都是在堆内存上分配(基本数据类型除外),然后由GC(garbage collection)负责自动回收不再使用的内存. 上面是Java内存管理机制的基本情况.但是如果仅仅理解到这里,我们