《深度探索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() 的参数。这是因为在前面的基类构造过程中,vptr 还未构造好,该 name 还不可用。
如果我们将 CLS_Derived 构造函数改为:
CLS_Derived() :
CLS_CommonBase1(this, typeid(*this).name()),
obj1(typeid(*this).name()),
CLS_VirtualBase1(this),
CLS_VirtualBase2(this),
CLS_CommonBase2(this)
{
}
运行出错。
改为:
CLS_Derived() :
CLS_CommonBase1(this),
obj1(typeid(*this).name()),
CLS_VirtualBase1(this),
CLS_VirtualBase2(this),
CLS_CommonBase2(this, typeid(*this).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的调整和成员对象的析构不需要严格的顺序要求。