《深度探索C++对象模型》读书笔记[第五章:构造/解构/拷贝 语义学]
5.0.1 纯虚函数的存在
纯虚函数可以不经由虚拟机制静态地调用一个纯虚函数,纯虚析构函数除外,class 设计者一定得定义它[纯虚析构函数]. 因为每一个 derived class destructor 会被编译器加以扩展,以静态调用的方式调用其 "每一个 virtual base class"以及"上一层 base class"的 destructor.因此只要缺乏任何一个 base class destructors 的定义,就会导致链接失败.
#include <iostream>
#include <stdio.h>
using namespace std;
class Base
{
public:
virtual ~Base() = 0;
virtual void interface() = 0;
};
void Base::interface(){}
class Derived : public Base
{
public:
void interface() override;
};
void Derived::interface()
{
Base::interface();
}
int main()
{
Derived obj;
}
代码会链接失败
test.cpp:11: undefined reference to `Base::~Base()'
collect2: error: ld returned 1 exit status
继承体系中每一个 class object 的 destructors 都会被调用.编译器不能够压抑这个调用操作.
一个比较好的地带方案就是,:"不要把 virtual destructor 声明为 pure".
5.0.2 虚拟规格的存在
一般而言,把所有的成员函数都声明为 virtual function,然后再靠编译器的优化操作把非必要的 virtual invocation 去除,并不是一个好的设计观念.
5.0.3 虚拟规格中 const 的存在
决定一个 virtual function 是否需要 const,是一件琐碎的事情,当你不容易做决定时,不再用const就是了.
5.1 "无继承"情况下的对象构造
plain Old Data的定义与判别在<深入理解C++11>的3.6 POD类型一节有详细叙述.
在C之中,global被视为一个"临时性的定义",因为它没有明确的初始化操作.被放在程序 data segment中一个 "特别保留给未初始化之 global objects 使用"的空间.这块空间被称为BSS.
C++并不支持 "临时性定义",这是因为 class 构造行为的隐含应用之故.global 在C++中被视为完全定义(它会组织第二个或更多个定义)。C和C++的一个差异就在于,BSS data segment 在C++中相对地不重要.C++的所有全局对象都被当做"初始化过的数据"来对待.
5.1.1 抽象数据类型
explicit initialization list 在C++11中可以通过 #include<initializer_list>并实现以initialize_list
5.1.2 为继承做准备
virtual functions 除了每一个 class object 多负担一个 vptr 之外, virtual function 的引入也引发编译器对于我们的 Point clss 产生膨胀作用:
- 我们所定义的 constructor 被附加了一些码,以便将 vptr 初始化.这些码必须被附加在任何 base class constructors的调用之后,但必须在任何由使用者(程序员)供应的码之前.
- 合成一个 copy constructor 和一个 copy assignment operator,而且其操作不再是 trivial(但 implicit destructor 仍然是 trivial).如果一个 Point object 被初始化或以一个 derived class object 赋值,那么以位位基础(bitwise)的操作可能给 vptr带来非法设定.
5.2 继承体系下的对象构造
当我们定义一个 object 如下:
T object;
时,如果 T 有一个 constructor,它会被调用.伴随着 constructor的调用,Constructor可能内带大量的隐藏码,因为编译器会扩充每一个 constructor,扩充程度视 class T的继承体系而定。一般而言编译器所做的扩充操作大约如下:
-
记录 在 member initialization list 中的 data members 初始化操作会被放进 constructor的函数本身,并以 members的声明顺序为顺序.
-
如果有一个 member 并没有出现在 member initialization list 之中,但它有一个 default constructor,那么该 default constructor必须被调用.
-
在那之前,如果 class object 有virtual table pointer(s),它(们)必须被设定初值,指向适当的 virtual table(s).
-
在那之前,所有上一层的 base class constructors 必须被调用,以 base class 的声明顺序为顺序(与 member initialization list 中的顺序没关联):
- 如果 base class 被列于 member initialization list 中,那么任何明确指定的参数都应该传递过去.
- 如果 base class 没有被列于 member initialization list 中,而它有 default constructor(或 default memberwise copy constructor),那么久调用之.
- 如果 base class 是多重继承下的第二或后继的 base class,那么 this 指针必须有所调整.
-
在那之前,所有 virtual base class constructors 必须被调用,从左到右,从最深到最浅。
-
如果 class 被列于 member initialization list中, 那么如果任何明确指定的参数,都应该传递过去.若没有列于 list 之中,而 class 有一个 default constructor,也应该调用之.
-
此外, class 中的每一个 virtual base class subobject 的偏移量(offset)必须在执行期可被存取.
-
如果 class object是最底层(most-derived)的 class,其 constructors 可能被调用;某些用以支持这个行为的机制必须被放进来.
-
5.2.1 虚拟继承
传统的"constructor扩充现象"并没有用,这是因为 virtual base class的"共享性"之故.
"virtual base class constructors 的被调用"有着明确的定义:只有一个完整的 class object 被定义出来时,它才会被调用;如果 object 只是某个完整 object 的 subobject,它就不会被调用.
5.2.2 vptr初始化语义学
当我们定义一个 PVertex object 时, constructors 的调用顺序是:
Point(x,y);
Point3d(x,y,z);
Vertex(x,y,z);
Vertex3d(x,y,z);
PVertex(x,y,z);
问题:
假设这个继承体系中的每一个 class 都定义了一个 virtual function size(),该函数负责传回 class 的大小.继承体系中的每一个 constructors 内带一个调用操作 size()。当我们定义 PVertex object时,前述五个 constructors 会 如何?每一次size()调用会被决议为PVertex::size()吗(毕竟那是我们正在构造的东西)?或者每次调用会被决议为"当前正在执行之 constructor 所对应之 class"的size()函数实体?
答:
C++语言规则告诉我们,在Point3d constructor中调用的 size()函数,必须被决议为Point3d::size()而不是 PVertex::size().更一般地,在一个 class 的 constructor和destructor中,经由构造中的对象来调用一个virtual function,其函数实体应该是在此class中有作用的那个.
由于consructor尚未执行完,所以每一个调用操作要以静态方式决议,千万不能用到虚拟机制.另外,size()中又调用一个虚函数,也必须静态决议.
如何限制虚函数而使用静态决议?
vptr初始化操作需要在base class constructors调用操作之后,但是在程序员供应的码或是 "member initialization list 中所列的 members 初始化操作"之前.
constructor的执行算法通常如下:
- 在 derived class constructor 中,"所有 virtual base classes"及"上一层 base class"的 constructors 会被调用.
- 上述完成之后,对象的 vptr(s)被初始化,指向相关的 vitrual table(s).
- 如果有 member initialization list 的话,将在 constructor体内扩展开来.这必须在 vptr 被设定之后才进行,以免有一个 virtual member function 被调用.
4.最后,执行程序员所提供的码.
在 class 的 constructor 的 member initialization list 中调用该class的一个虚拟函数 总是安全的,但是语义上可能不安全.
在需要供应参数给一个 base calss constructor的情况下,在"class 的 constructor 的 member initialization list 中" 调用该 class 的虚拟函数,就不安全了,此时cptr如不是尚未被设定好,就是被设定指向错误的 class .更进一步地,该函数所存取的任何 class's data members 一定还没有被初始化.
5.3 对象复制语义学
如果 class 已经有了 bitwise copy 语义, implicit copy assignment operator 被视为毫无用处,也根本不会被合成出来.一个 class 对于默认的 copy assignment operator,在以下情况不会表现出 bitwise copy 语义:
- 当 class 内带一个 member object,而其 class 有一个 copy assignment operator时.
- 当一个 class 的baseclass 有一个 copy assignment operator时.
- 当一个 class 声明了任何 virtual functions (我们一定不能够拷贝右端 class object 的 vptr 地址,因为它可能是一个 derived class object).
- 当 class 继承自一个 virtual base class (不论此 base class 有没有copy operator)时.
有一次重申:不要在任何 virtual base class 中声明数据.
5.4 对象的功能
本节是对象构造和拷贝的效率对比.包括POD,ADT,单一继承,多重继承,虚拟继承.
5.5 解构语义学
如果 class 没有定义 destructor,那么只有在 class 内带的 member object(或是 class 自己的base class)拥有 destructor 的情况下,编译器才会自动合成出一个来.否则,destructor 会被视为不需要,也就不需要被合成.
析构执行顺序:
- 如果 object 内带一个 vptr,那么首先重设(reset) 相关的 virtual table。
- destructor 的函数本身现在被执行,也就是说 vptr 会在程序员的码执行前被重设(reset)
- 如果 class 拥有 member class objects,而后者拥有 destructors,那么它们会以其声明顺序的相反顺序被调用.
4.如果有任何直接的(上一层) nonvirtual base classes 拥有 destructor,他们会以其声明顺序的相反顺序被调用. - 如果有任何 virtual base classes 拥有 destructor, 而当前讨论的这个 class 是最尾端(most-derived)的 class.那么他们会以其原来的构造顺序的相反顺序被调用.
当我们在destructor中调用member functions时,对象的蜕变会因为 vptr 的重新设定(在每一个destructor中,在程序员所供应的码执行之前)而收到影响.
译者侯捷认为正确的顺序应该是 2、3、1、4、5。即:
-
destructor 的函数本身首先被执行;
-
如果 class 拥有 member class objects,而后者拥有 destructors,那么它们会以其声明顺序的相反顺序被调用;
-
如果 object 内带一个 vptr,则现在被重新设定,指向适当之 base class 的 virtual table;
-
如果有任何直接的(上一层)nonvirtual base classes 拥有 destructors,它们会以其声明顺序的相反顺序被调用;
-
如果有任何 virtual base classes 拥有 destructor,而当前讨论的这个 class 是最尾端 (most-derived) 的 class,那么它们会以其原来的构造顺序的相反顺序被调用。
测试代码:
// VC++ 2005
#include <stdio.h>
class Derived;
Derived* g_pDerived = NULL;
Derived* g_pDerived2 = NULL;
class Base_Base
{
public:
virtual ~Base_Base()
{
printf("/nBase_Base destructor/n");
vfunc();
}
virtual void vfunc()
{
printf("Base_Base::vfunc/n");
}
};
class Base: public virtual Base_Base
{
public:
virtual ~Base()
{
printf("/nBase destructor/n");
vfunc();
}
virtual void vfunc()
{
printf("Base::vfunc/n");
}
};
class Base2_Base
{
public:
virtual ~Base2_Base()
{
printf("/nBase2_Base destructor/n");
vfunc();
}
virtual void vfunc()
{
printf("Base2_Base::vfunc/n");
}
};
class Base2: public Base2_Base
{
public:
virtual ~Base2()
{
printf("/nBase2 destructor/n");
vfunc();
}
virtual void vfunc()
{
printf("Base2::vfunc/n");
}
};
class Base3_Base
{
public:
virtual ~Base3_Base()
{
printf("/nBase3_Base destructor/n");
vfunc();
}
virtual void vfunc()
{
printf("Base3_Base::vfunc/n");
}
};
class Base3: public Base3_Base
{
public:
virtual ~Base3()
{
printf("/nBase3 destructor/n");
vfunc();
}
virtual void vfunc()
{
printf("Base3::vfunc/n");
}
};
class Base4_Base
{
public:
virtual ~Base4_Base()
{
printf("/nBase4_Base destructor/n");
vfunc();
}
virtual void vfunc()
{
printf("Base4_Base::vfunc/n");
}
};
class Base4: virtual public Base4_Base
{
public:
virtual ~Base4()
{
printf("/nBase4 destructor/n");
vfunc();
}
virtual void vfunc()
{
printf("Base4::vfunc/n");
}
};
class Base5_Base
{
public:
virtual ~Base5_Base()
{
printf("/nBase5_Base destructor/n");
vfunc();
}
virtual void vfunc()
{
printf("Base5_Base::vfunc/n");
}
};
class Base5: virtual public Base5_Base
{
public:
virtual ~Base5()
{
printf("/nBase5 destructor/n");
vfunc();
}
virtual void vfunc()
{
printf("Base5::vfunc/n");
}
};
class MemberClass
{
public:
virtual ~MemberClass();
private:
Derived* _pDerived;
};
class MemberClass2
{
public:
virtual ~MemberClass2();
private:
Derived* _pDerived;
};
class Derived
: virtual public Base
, public Base2
, virtual public Base3
, virtual public Base5
, public Base4
{
public:
virtual ~Derived();
virtual void vfunc();
private:
MemberClass mc;
MemberClass2 mc2;
};
Derived::~Derived()
{
printf("Derived destructor/n");
vfunc();
}
void Derived::vfunc()
{
printf("Derived::vfunc/n");
}
MemberClass::~MemberClass()
{
printf("/nMemberClass destructor/n");
_pDerived = new Derived;
g_pDerived = _pDerived;
_pDerived->vfunc();
}
MemberClass2::~MemberClass2()
{
printf("/nMemberClass2 destructor/n");
_pDerived = new Derived;
g_pDerived2 = _pDerived;
_pDerived->vfunc();
}
int main()
{
Derived oDerived;
delete g_pDerived;
delete g_pDerived2;
return 0;
}
输出:
Derived destructor
Derived::vfunc
MemberClass2 destructor
Derived::vfunc
MemberClass destructor
Derived::vfunc
Base4 destructor
Base4::vfunc
Base2 destructor
Base2::vfunc
Base2_Base destructor
Base2_Base::vfunc
Base4_Base destructor
Base4_Base::vfunc
Base5 destructor
Base5::vfunc
Base5_Base destructor
Base5_Base::vfunc
Base3 destructor
Base3::vfunc
Base3_Base destructor
Base3_Base::vfunc
Base destructor
Base::vfunc
Base_Base destructor
Base_Base::vfunc
从测试结果可以看出,侯捷的说法是正确的。