《深度探索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;
}
二、無繼承下的對象構造
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、虛繼承和最底層類
我們知道當類結構中使用虛繼承時,這些類是希望共享基類資源的。這就涉及到一個問題,共享的部分該由誰初始化呢?最底層類。以如下的繼承體系為例:
這裡最底層類指的就是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;
}
從這個輸出中,我們可以與上面的構造過程中的順序相對應。這裡,我們在調用過程中隻給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);
}
反彙編:
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);
}
從結果我們可以看出,共享部分的拷貝指派函數确實被調用了兩次。事實上,從編譯器的角度,這個問題基本上是無法解決的。從語義的角度上講,我們可以先調用非共享基類的拷貝指派函數,然後調用共享基類的拷貝指派函數。但這樣并不能解決多次拷貝的問題。作者提到,最好的解決辦法就是不要在虛基類中聲明資料(那還要虛繼承幹嘛呢?)
五、析構語義學
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;
}
構造時,vptr的調整在成員對象的構造之前可以解釋為,成員對象的構造支援傳參,而參數可能由虛函數傳回。是以要保證虛函數調用的正确性。然而,對于析構函數,我個人認為vptr的調整和成員對象的析構不需要嚴格的順序要求。