程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++ 內存布局:深入理解C++內存布局

C++ 內存布局:深入理解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 

template
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 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個與虛函數對應得地址。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHA+08nT2tDpuq/K/bHt1rjV69W8vt00uPbX1r3ao6yyosfStKbT2sDgtcTE2rTmtdjWt8bwyry0pqOsy/nS1NX7uPbA4NK7ubLVvL7dMzK49tfWvdqhozwvcD4KPHA+PGJyPgo8L3A+CjxwPjIuMrv5wOC6rNPQ0Om6r8r9o6zKudPDxtXNqLzMs9CjrMXJyfrA4NbQsru6rNDpuq/K/TwvcD4KPHA+0N64xMnPw+a1xLT6wuujrLXDtb3I58/CtcTE2sjdPC9wPgo8cD48cHJlIGNsYXNzPQ=="brush:java;">#include template 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 class CPoint2D : public CPoint { 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(); } };

void main()
{
	CPoint m_Point;

	CPoint2D 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::getZ) << std::endl;
	
	std::cin.get();
}



上面的代碼輸出得到如下的內容

\ 最後一個輸出的是一個函數指針的大小,在沒有虛繼承的情況下,在X86(Win 32Debug)系統上輸出是4.

整個類的大小為32字節,我們看一下內存分布就明白了

\

可以看到m_Point2D的內存布局和m_Point的內存布局很類似。一個虛函數表指針,然後三個成員變量。虛函數表中的內容和m_Point中的一摸一樣。這是因為CPoint2D 是從CPoint繼承過來的。

2.3基類含有虛函數,使用普通繼承,派生類中含有虛函數

繼續修改上面的代碼,得到如下的內容

#include 

template
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
class CPoint2D :  public CPoint
{
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:"<::getZ()) << std::endl;
		return 0;
	}
	
	virtual void setZ(T newZ = 0)
	{
		//std::cout << "CPoint2D:" << sizeof(CPoint2D::setZ()) << std::endl;
		_z = 0;
	}
};
void main()
{
	CPoint m_Point;

	CPoint2D 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::getZ) << std::endl;
	
	std::cin.get();
}
上面的代碼輸出內容如下所示:


\ 內存布局如下:

\

輸出的內容和之前派生類中沒有虛函數的一樣,但是內存布局發生了變化。變化體現在_vfptr中,_vfptr中有4個地址是和CPoint中的一樣,2個不一樣,這是因為在CPoint2D中,重寫了CPoint中的兩個虛函數,從而派生類中的虛函數覆蓋了父類中的虛函數。這地方的重寫不僅僅是函數名相同,還要保證函數的參數類型,參數個數,函數的返回形式也和基類中的一致。

從上面的例子中我們可以得出以下的結論:

1)、類中一旦出現虛函數,編譯器便會給其分配一個虛函數表,虛函數表指針的大小和編譯器有關。

2)、派生類中如果對父類的虛函數進行了重寫,那麼派生類中的虛函數會覆蓋父類的虛函數,體現在上圖的虛函數表中的地址發生了變化。

3)、虛函數表指針總是處於類的地址的開始處,所以在計算類的大小時要注意這一點。


2.4基類不含有虛函數,使用虛繼承,派生類中不含虛函數

這一次使用前一章節的代碼,對前一章節的代碼進行修改,得到如下的內容

#include 
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字節。

其內存分布如下圖所示:


因為篇幅太長,剩下的內容後面再說了。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved