天天看點

《深度探索C++對象模型》學習筆記 — 構造、析構、拷貝語義學(The Semantics of Construction,Destruction,and copy)一、抽象類和純虛函數二、無繼承下的對象構造三、繼承體系下的對象構造四、對象複制語義學五、析構語義學

《深度探索C++對象模型》學習筆記 — 構造、析構、拷貝語義學(The Semantics of Construction,Destruction,and copy)

  • 一、抽象類和純虛函數
  • 二、無繼承下的對象構造
    • 1、POD
  • 三、繼承體系下的對象構造
    • 1、構造順序
    • 2、虛繼承和最底層類
    • 3、vptr構造
    • 4、代碼驗證
  • 四、對象複制語義學
    • 1、拷貝構造與虛繼承
    • 2、拷貝指派與虛繼承
  • 五、析構語義學
    • 1、析構順序
    • 2、代碼驗證

一、抽象類和純虛函數

如果我們嘗試在抽象類中聲明資料成員,那麼我們至少應該提供protected權限的初始化資料成員的構造函數;相應的,如果這些成員需要手動釋放或解鎖,我們需要提供析構函數執行相應操作。一般而言,我們會選擇将析構函數聲明為純虛函數,然後提供一份該函數的聲明:

#include <iostream>
using namespace std;

class CLS_Base
{
public:
	virtual ~CLS_Base() = 0;

protected:
	CLS_Base(size_t size = 5)
	{
		m_pCh = new char(size);
	}

private:
	char* m_pCh;
};

CLS_Base::~CLS_Base()
{
	delete m_pCh;
	cout << "~CLS_DerCLS_Baseived()" << endl;
}

class CLS_Derived : public CLS_Base
{
public:
	virtual ~CLS_Derived()
	{
		cout << "~CLS_Derived()" << endl;
	}
};

int main()
{
	CLS_Derived derived;
}
           
《深度探索C++對象模型》學習筆記 — 構造、析構、拷貝語義學(The Semantics of Construction,Destruction,and copy)一、抽象類和純虛函數二、無繼承下的對象構造三、繼承體系下的對象構造四、對象複制語義學五、析構語義學

二、無繼承下的對象構造

1、POD

POD表示Plain Old Data(我看了下C++标準,并沒有提及書中寫的Plain Ol’ Data)。這種資料類型能夠和C語言相容,所有需要編譯器合成的構造、析構、拷貝函數都是trivial的。如下:

typedef struct
{
	float x, y, z;
} Point;
           

這類結構的trivial成員我們認為根本沒有被定義或調用。這裡有一種例外,就是全局變量。對于全局變量,編譯器負責對其執行預設初始化。

三、繼承體系下的對象構造

1、構造順序

(1)所有虛基類的構造函數必須被調用,從左到右,從最深到最淺:

         a.如果基類位于初始化清單中,那麼任何顯式指定的參數都應該傳遞過去;否則,如果該基類有預設函數,則調用該函數。

         b.類中的每個虛基類子對象的偏移量,必須在執行期可被存取。

         c.如果class object是最底層(most-derived)的類,其構造函數可能被調用;某些用以支援這一行為的機制必須被放進來。

(2)所有上一層基類的構造函數必須被調用(層層遞歸調用),以基類的聲明順序為順序:

         a.如果基類位于初始化清單中,那麼任何顯式指定的參數都應該傳遞過去。

         b.否則,如果該基類有預設函數,則調用該函數。

         c.如果基類是多重繼承中的第二或後繼的基類,this指針必須有所調整。

(3)如果類對象有vptr,它們必須被設定初值指向适當的虛函數表。

(4)初始化清單中的成員對象初始化操作會被放進構造函數體,并且按照成員的聲明順序調用。

(5)如果一個成員沒有出現在初始化清單中,但是它有預設構造函數,則該函數必須被調用。

2、虛繼承和最底層類

我們知道當類結構中使用虛繼承時,這些類是希望共享基類資源的。這就涉及到一個問題,共享的部分該由誰初始化呢?最底層類。以如下的繼承體系為例:

《深度探索C++對象模型》學習筆記 — 構造、析構、拷貝語義學(The Semantics of Construction,Destruction,and copy)一、抽象類和純虛函數二、無繼承下的對象構造三、繼承體系下的對象構造四、對象複制語義學五、析構語義學

這裡最底層類指的就是CLS_Vertex3DDerived。結合代碼分析下:

class CLS_Point 
{
public:
	CLS_Point()
	{
	}
};

class CLS_Point2D : virtual public CLS_Point {};
class CLS_Point3D : public CLS_Point2D {};
class CLS_Vertex : virtual public CLS_Point {};

class CLS_Vertex3D : public CLS_Point3D, public CLS_Vertex 
{
public:
	CLS_Vertex3D() :
		CLS_Point3D(),
		CLS_Vertex()
	{
	}
};

class CLS_Vertex3DDerived : public CLS_Vertex3D
{
public:
	CLS_Vertex3DDerived() :
		CLS_Vertex3D()
	{
	}
};

int main()
{
	CLS_Vertex3DDerived obj;
}
           

結合我們前面檢視反彙編的知識,我們可以看到在CLS_Vertex3DDerived執行構造函數之前,執行了這樣一段彙編代碼:

{
00EE10D0  push        ebp  
00EE10D1  mov         ebp,esp  
00EE10D3  push        ecx  
00EE10D4  mov         dword ptr [this],ecx  
00EE10D7  cmp         dword ptr [ebp+8],0  
00EE10DB  je          CLS_Vertex3DDerived::CLS_Vertex3DDerived+2Bh (0EE10FBh)  
00EE10DD  mov         eax,dword ptr [this]  
00EE10E0  mov         dword ptr [eax],offset CLS_Vertex3D::`vbtable' (0EE2100h)  
00EE10E6  mov         ecx,dword ptr [this]  
00EE10E9  mov         dword ptr [ecx+4],offset CLS_Point2D::`vbtable' (0EE20F8h)  
00EE10F0  mov         ecx,dword ptr [this]  
00EE10F3  add         ecx,8  
00EE10F6  call        CLS_Point::CLS_Point (0EE1000h)  
};
           

這保證了繼承體系中共享的部分将會是最先被構造的。

3、vptr構造

前面我們學習過,如果在構造函數或者析構函數中調用虛函數,該調用将會被靜态決議,而非通過vptr進行調用。這樣做很合理,因為vptr隻是子對象部分的vptr。那麼如果我們在虛函數中再調用虛函數呢?被調用的虛函數如何知道此次調用來自構造函數還是外部呢?我們可以考慮模仿虛基類構造函數的調用,在每次調用前在棧中放置一個參數,控制是否需要通過虛拟機制調用,但這樣會大大降低函數的效率。在msvc中,考慮下面的代碼:

#include <iostream>
using namespace std;

class CLS_Base
{
public:
	CLS_Base()
	{
		test();
	}

	virtual void test()
	{
		cout << "CLS_Base::test" << endl;
		testInner();
	}

	virtual void testInner()
	{
		cout << "CLS_Base::testInner" << endl;
	}
};

class CLS_Derived : public CLS_Base
{
};

int main()
{
	CLS_Derived obj;
}
           

檢視反彙編:

testInner();
00221047  mov         ecx,dword ptr [this]  
0022104A  mov         edx,dword ptr [ecx]  
0022104C  mov         ecx,dword ptr [this]  
0022104F  mov         eax,dword ptr [edx+4]  
00221052  call        eax  
           

我們可以看出在微軟的編譯器中是直接通過vptr編譯的。這就要求我們在進入構造函數體之前要把vptr放置好。更具體來講,我們應該在任何基類構造函數調用之後,在成員變量初始化之前設定好vptr。

是以,如果在初始化清單中調用虛函數,當其調用于成員變量初始化時,從虛函數表的初始化角度來說,這是個安全的行為。然而,從語義上講,這未必安全,因為虛函數中可能會使用未初始化的成員變量。

4、代碼驗證

#include <iostream>
using namespace std;

class CLS_CommonBase1
{
public:
	CLS_CommonBase1(void* ptr, string str = "")
	{
		cout << "CLS_CommonBase1::CLS_CommonBase1 str = " << str << " typeid(*this).name() = " << typeid(*this).name() << endl;
		cout << "ptr = " << ptr << " this = " << this << endl;
	}

	virtual void test() {}
};

class CLS_CommonBase2
{
public:
	CLS_CommonBase2(void* ptr, string str = "")
	{
		cout << "CLS_CommonBase2::CLS_CommonBase2 str = " << str << " typeid(*this).name() = " << typeid(*this).name() << endl;
		cout << "ptr = " << ptr << " this = " << this << endl;
	}

	virtual void test() {}
};

class CLS_VirtualBase1
{
public:
	CLS_VirtualBase1(void* ptr, string str = "")
	{
		cout << "CLS_VirtualBase1::CLS_VirtualBase1 str = " << str << " typeid(*this).name() = " << typeid(*this).name() << endl;
		cout << "ptr = " << ptr << " this = " << this << endl;
	}

	virtual void test() {}
};

class CLS_VirtualBase2
{
public:
	CLS_VirtualBase2(void* ptr, string str = "")
	{
		cout << "CLS_VirtualBase2::CLS_VirtualBase2 str = " << str << " typeid(*this).name() = " << typeid(*this).name() << endl;
		cout << "ptr = " << ptr << " this = " << this << endl;
	}

	virtual void test() {}
};

class CLS_MemberObject1
{
public:
	CLS_MemberObject1(string _str)
	{
		cout << "CLS_MemberObject1::CLS_MemberObject1 _str = " << _str << endl;
	}

	virtual void test() {}
};

class CLS_MemberObject2
{
public:
	CLS_MemberObject2(string _str = "")
	{
		cout << "CLS_MemberObject2::CLS_MemberObject2 _str = " << _str << endl;
	}

	virtual void test() {}
};

class CLS_Derived : public CLS_CommonBase1, virtual public CLS_VirtualBase1, virtual public CLS_VirtualBase2, public CLS_CommonBase2
{
public:
	CLS_Derived() :
		CLS_CommonBase1(this),
		CLS_VirtualBase1(this),
		CLS_VirtualBase2(this),
		CLS_CommonBase2(this, typeid(*this).name()),
		obj1(typeid(*this).name())
	{

	}

private:
	CLS_MemberObject1 obj1;
	CLS_MemberObject2 obj2;
};

int main()
{
	CLS_Derived obj;
}
           
《深度探索C++對象模型》學習筆記 — 構造、析構、拷貝語義學(The Semantics of Construction,Destruction,and copy)一、抽象類和純虛函數二、無繼承下的對象構造三、繼承體系下的對象構造四、對象複制語義學五、析構語義學

從這個輸出中,我們可以與上面的構造過程中的順序相對應。這裡,我們在調用過程中隻給CLS_CommonBase2傳遞了typeid.name() 的參數。這是因為在前面的基類構造過程中,該name還不可用。

四、對象複制語義學

1、拷貝構造與虛繼承

與構造函數類似,拷貝構造函數采用了相同的方式以防止共享成分的多次拷貝:

#include <iostream>
using namespace std;

class CLS_VirtualBase
{
public:
	CLS_VirtualBase() {};

	CLS_VirtualBase(const CLS_VirtualBase& other)
	{
		cout << "CLS_VirtualBase(const CLS_VirtualBase&)" << endl;
	}
};

class CLS_Derived1 : virtual public CLS_VirtualBase
{
public:
	CLS_Derived1() {};

	CLS_Derived1(const CLS_Derived1& other) :
		CLS_VirtualBase(other)
	{
		cout << "CLS_Derived1(const CLS_Derived1&)" << endl;
	}
};

class CLS_Derived2 : virtual public CLS_VirtualBase
{
public:
	CLS_Derived2() {};

	CLS_Derived2(const CLS_Derived2& other):
		CLS_VirtualBase(other)
	{
		cout << "CLS_Derived2(const CLS_Derived2&)" << endl;
	}
};

class CLS_DerivedMost : public CLS_Derived1, public CLS_Derived2
{
public:
	CLS_DerivedMost() {};

	CLS_DerivedMost(const CLS_DerivedMost& other) :
		CLS_Derived1(other),
		CLS_Derived2(other)
	{
		cout << "CLS_DerivedMost(const CLS_DerivedMost&)" << endl;
	}
};

int main()
{
	CLS_DerivedMost obj;
	CLS_DerivedMost objCopy(obj);
}
           
《深度探索C++對象模型》學習筆記 — 構造、析構、拷貝語義學(The Semantics of Construction,Destruction,and copy)一、抽象類和純虛函數二、無繼承下的對象構造三、繼承體系下的對象構造四、對象複制語義學五、析構語義學

反彙編:

008E1180  push        ebp  
008E1181  mov         ebp,esp  
008E1183  push        ecx  
008E1184  mov         dword ptr [this],ecx  
008E1187  cmp         dword ptr [ebp+0Ch],0  
008E118B  je          CLS_DerivedMost::CLS_DerivedMost+2Bh (08E11ABh)  
008E118D  mov         eax,dword ptr [this]  
008E1190  mov         dword ptr [eax],offset CLS_DerivedMost::`vbtable' (08E31E8h)  
008E1196  mov         ecx,dword ptr [this]  
008E1199  mov         dword ptr [ecx+4],offset CLS_Derived1::`vbtable' (08E31E0h)  
008E11A0  mov         ecx,dword ptr [this]  
008E11A3  add         ecx,8  
008E11A6  call        CLS_VirtualBase::CLS_VirtualBase (08E1000h)
           

2、拷貝指派與虛繼承

然而上述的政策并不适用于拷貝指派。一方面,拷貝指派運算符并不支援初始化清單,那麼我們就沒法通過參數控制是否隻初始化非共享部分。另一方面,我們可以通過函數指針調用拷貝指派函數。

#include <iostream>
using namespace std;

class CLS_VirtualBase
{
public:
	CLS_VirtualBase() {};

	CLS_VirtualBase& operator=(const CLS_VirtualBase& other)
	{
		cout << "CLS_VirtualBase& operator=(const CLS_VirtualBase& other)" << endl;
		return *this;
	}
};

class CLS_Derived1 : virtual public CLS_VirtualBase
{
public:
	CLS_Derived1() {};

	CLS_Derived1& operator=(const CLS_Derived1& other)
	{
		this->CLS_VirtualBase::operator=(other);
		cout << "CLS_Derived1& operator=(const CLS_Derived1& other)" << endl;
		return *this;
	}
};

class CLS_Derived2 : virtual public CLS_VirtualBase
{
public:
	CLS_Derived2() {};

	CLS_Derived2& operator=(const CLS_Derived2& other)
	{
		this->CLS_VirtualBase::operator=(other);
		cout << "CLS_Derived2& operator=(const CLS_Derived2& other)" << endl;
		return *this;
	}
};

class CLS_DerivedMost : public CLS_Derived1, public CLS_Derived2
{
public:
	CLS_DerivedMost() {};

	CLS_DerivedMost& operator=(const CLS_DerivedMost& other)
	{
		this->CLS_Derived1::operator=(other);
		this->CLS_Derived2::operator=(other);
		cout << "CLS_DerivedMost& operator=(const CLS_DerivedMost& other)" << endl;
		return *this;
	}
};

int main()
{
	CLS_DerivedMost obj;
	CLS_DerivedMost objCopy;
	objCopy = obj;

	auto pf = &CLS_DerivedMost::operator=;
	(objCopy.*pf)(obj);
}
           
《深度探索C++對象模型》學習筆記 — 構造、析構、拷貝語義學(The Semantics of Construction,Destruction,and copy)一、抽象類和純虛函數二、無繼承下的對象構造三、繼承體系下的對象構造四、對象複制語義學五、析構語義學

從結果我們可以看出,共享部分的拷貝指派函數确實被調用了兩次。事實上,從編譯器的角度,這個問題基本上是無法解決的。從語義的角度上講,我們可以先調用非共享基類的拷貝指派函數,然後調用共享基類的拷貝指派函數。但這樣并不能解決多次拷貝的問題。作者提到,最好的解決辦法就是不要在虛基類中聲明資料(那還要虛繼承幹嘛呢?)

五、析構語義學

1、析構順序

(1)執行析構函數體;

(2)執行成員函數的析構函數(與聲明順序相反);

(3)如果一個object内含一個vptr,那麼首先重設相關的虛函數表;

(4)執行非虛基類的析構函數(與聲明順序相反);

(5)執行虛基類的析構函數(與聲明順序相反)。

2、代碼驗證

#include <iostream>
using namespace std;

class CLS_CommonBase1
{
public:
	virtual ~CLS_CommonBase1()
	{
		cout << "~CLS_CommonBase1 typeid(*this).name() = "<< typeid(*this).name() << endl;
	}
};

class CLS_CommonBase2
{
public:
	virtual ~CLS_CommonBase2()
	{
		cout << "~CLS_CommonBase2 typeid(*this).name() = " << typeid(*this).name() << endl;
	}
};

class CLS_VirtualBase1
{
public:
	virtual ~CLS_VirtualBase1()
	{
		cout << "~CLS_VirtualBase1 typeid(*this).name() = " << typeid(*this).name() << endl;
	}
};

class CLS_VirtualBase2
{
public:
	virtual ~CLS_VirtualBase2()
	{
		cout << "~CLS_VirtualBase2 typeid(*this).name() = " << typeid(*this).name() << endl;
	}
};

class CLS_MemberObject1
{
public:
	~CLS_MemberObject1()
	{
		cout << "~CLS_MemberObject1" << endl;
	}
};

class CLS_MemberObject2
{
public:
	 ~CLS_MemberObject2()
	{
		cout << "~CLS_MemberObject2()"<< endl;
	}
};

class CLS_Derived : public CLS_CommonBase1, virtual public CLS_VirtualBase1, virtual public CLS_VirtualBase2, public CLS_CommonBase2
{
public:
	virtual ~CLS_Derived()
	{
		cout << "~CLS_Derived() typeid(*this).name() = " << typeid(*this).name() << endl;
	}

private:
	CLS_MemberObject1 obj1;
	CLS_MemberObject2 obj2;
};

int main()
{
	CLS_Derived obj;
}
           
《深度探索C++對象模型》學習筆記 — 構造、析構、拷貝語義學(The Semantics of Construction,Destruction,and copy)一、抽象類和純虛函數二、無繼承下的對象構造三、繼承體系下的對象構造四、對象複制語義學五、析構語義學

構造時,vptr的調整在成員對象的構造之前可以解釋為,成員對象的構造支援傳參,而參數可能由虛函數傳回。是以要保證虛函數調用的正确性。然而,對于析構函數,我個人認為vptr的調整和成員對象的析構不需要嚴格的順序要求。

繼續閱讀