第五章 构造、析构、拷贝语意学
纯虚函数的存在
在虚基类的时候一定要将析构函数声明为虚函数。编译器在调用派生类析构函数会采用静态调用的方式一层一层的调用每一个虚基类的析构函数,如果缺乏一个基类析构函数的定义会导致链接失败(Linux g++ 编译会出现 undefined reference),并且不要将虚析构函数声明为pure
的,编译器没有足够的能力合成一个pure virtual destructor
。
不要将一个抽象基类声明为const
继承体系下的对象的构造
编译器会按照继承体系去扩充构造函数,大致的扩充操作如下:
-
成员初始化列表中的数据成员会被放进构造函数体中,以成员声明的顺序为顺序。
-
如果成员没有出现在初始化列表中,但是含有默认构造函数,该默认构造函数必须被调用。
-
在以上步骤之前,如果有虚函数,一定要先初始化
vptr
. -
在以上步骤之前,上一层的基类的构造函数被调用,以基类的声明顺序为顺序:
- 如果基类处于成员初始化列表中,任何显式指定的参数必须被传递进去
- 如果基类没有处于成员初始化列表,但是含有默认构造函数就调用默认构造函数。
- 如果基类是多重继承下的第二个或者后继的基类,就要调整
this
指针的顺序。
-
在以上步骤之前,虚基类的构造函数必须被调用,按照从左到右,由最深到最浅的顺序:
- 如果基类处于成员初始化列表中,任何显式指定的参数必须被传递进去。如果基类没有处于成员初始化列表,但是含有默认构造函数就调用默认构造函数。
- 每一个虚基类的子类的偏移位置在执行期必须被存取。
下面这个例子描述了构造函数的扩充:
class Point {
public:
Point(float x = 0.0, float y = 0.0);
Point(const Point&);
Point& operator=(const Point&);
virtual ~Point();
virtual float z() { return 0.0; }
private:
float _x, _y;
};
class Line {
Point _begin, _end;
public:
Line(const Point& begin, const Point& end) : _begin(begin), _end(end) {}
};
// Line的构造函数会被编译器扩充为
Line* Line::Line(Line *this, const Point &begin, const Point &end) {
this->_begin.Point::Point(begin);
this->_end.Point::Point(end);
}
虚指针初始化语意学
虚指针在构造函数中何时被初始化?
在基类构造函数调用操作之后,但是在程序员提供的代码或是成员初始化列表所列的成员初始化操作之前。
虚指针必须被设定的两种情况:
- 当一个完整的对象被构造器起来时。
- 当一个子对象的构造函数调用一个虚函数时。
对象复制语意学
一个类的拷贝赋值运算符,在一下情况不会出现bitwise copy
语意:
- 当类内含有一个成员对象,并且其
class
有一个拷贝赋值运算符 - 当一个类的基类有一个拷贝赋值运算符
- 当一个类声明任何虚函数
- 当一个类继承自一个虚基类的时候
对于棱形继承中的拷贝,编译器需要抑制基类的重复拷贝,通过添加额外的参数most_derived
来抑制,但是尽可能的不允许一个虚基类的拷贝操作,不要在任何虚基类中声明数据。
析构语意学
如果一个类没有析构函数,只有在类内含有一个对象成员拥有析构函数的情况下编译器才会自动合成一个出来。
析构函数的执行顺序:
- 析构函数的本体首先执行
- 如果
class
有一个的成员是一个类,并且有构造函数,按照声明的顺序的相反顺序执行析构函数 - 如果一个对象含有一个虚指针,首先重设
vptr
指向基类的virtual table
- 如果有任何直接的非虚基类拥有析构函数,按照声明顺序的相反顺序被调用
- 如果有任何虚基类有析构函数,当前类按照原来构造顺序的相反顺序调用析构函数。
析构函数的底层实现策略时维护两份析构函数实例:
- 一个
complete object
实例,总是设定好vptr
, 并调用virtual base class destructors
- 一个
base class subobject
实例,除非在析构函数中调用一个虚函数,否则绝不会调用virtual base class
的析构函数并设定虚指针。