《深度探索C++对象模型》学习笔记 — 构造、析构、拷贝语义学(The Semantics of Construction,Destruction,and copy)
一、抽象类和纯虚函数
如果我们尝试在抽象类中声明数据成员,那么我们至少应该提供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的调整和成员对象的析构不需要严格的顺序要求。
|