纯虚函数
可以静态地调用纯虚函数,不能经过虚拟机制。
Abstract_base::pure_function(); //合法调用
HINT:纯虚析构函数必须定义,因为每一个派生类对象在析构的时候,都会自下而上地调用继承链中的每一个基类的析构函数。
如果只声明而不定义纯虚析构函数,那么将会出现链接错误undefined reference to ...
;
所以替代方案:不要将虚析构函数声明为pure。
5.1 无继承情况下的对象构造
在C中,未初始化的全局对象一般被视为临时性定义,因为没有显式初始化操作,存储在 BSS 段。
C++不支持临时性定义,C++所有全局对象都被以初始化过的数据来对待,所以C++代码中的 BSS 段并不重要。
抽象数据类型
观念上类会合成并调用默认拷贝构造、拷贝赋值和析构,但是实际上,在某些符合位逐次拷贝的类中,它们都是 trivial 的,根本没有产生。
继承
虚函数的引入,会导致代码膨胀:
- 我们自己定义的构造函数被附加了代码,将 vptr 初始化。
- 合成一个拷贝构造和一个拷贝赋值运算符,而且其操作不再是 trivial 的。即不再 bitwise ,而是真正产生了调用。编译器会将对象的连续内容拷贝到另一个对象上,而不是精确的“以每个成员为最小单位的颗粒化”。类似于
memcpy()
。
设想,如果一个 Base class 被初始化或以一个派生类赋值,那么 bitwise 方式会带来vptr的非法设定。this->__vptr_Point = __vtbl__Point;
当许多函数都需要传值的方式传回一个局部变量,那么最好提供一个拷贝构造。它的出现会触发NRV优化,NRV优化后将不再需要调用拷贝构造,因为直接在被直接传回的object本体上进行计算。
继承体系下的对象构造
当定义一个对象时:T object
;构造函数的调用伴随了如下扩充操作:
- 成员初始化列表中的 data members 的初始化操作会被放到构造函数体内部,并按声明顺序;
- 如果一个成员没有出现在成员初始化列表中,但是它有一个默认构造函数,则该默认构造函数会被调用;
- 如果类对象有 vptr,那么必须在构造函数开始处设定初值,指向合适的 vtbl ;
- 在本体构造之前,所有基类构造函数都要被调用,按照声明顺序。
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;}
protected:
float _x,_y;
};
class Line{
private:
Point _begin,_end;
public:
Line(float = 0.0, float = 0.0, float = 0.0, float = 0.0);
Line(const Point&, const Point&);
};
由于Point
声明了拷贝构造、拷贝赋值和析构函数,所以内含Point的类中会合成显式拷贝构造、拷贝复制和析构函数。
虚继承
当出现虚继承:
class Point3d:public virtual Point{
public:
Point3d(float x = 0.0, float y = 0.0, float z = 0.0):
Point(x,y), _z(z){}
Point3d(const Point3d& rhs):
Point(rhs), _z(rhs._z){}
~Point3d();
Point3d& operator=(const Point3d&);
virtual float z(){return _z;}
private:
float _z;
};
传统的扩充构造函数并不会出现,因为此处是虚继承,共享副本。
试想这一种继承体系:
Vertex 的构造函数也需要调用 Point 的构造函数,但是当Point3d
和Vertex
同为Vertex3d
的subobject 时,它们两个不可以调用Point构造函数。
转而由底层的Vertex3d
完成对Point
的初始化;再往后的继承中,则由PVertex
来完成被共享的subobject的构造。
为了支持这项特性,需要扩充 constructor ,测试传入的参数并决定是否调用相关的虚基类构造函数:
在Vertex3d
的构造函数起始位置判断是否调用虚基类构造函数,并将__most_derived
变量设置为false,从而阻断了Point3d
和Vertex
对虚基类构造函数的调用。
vptr 初始化语意学
在一个构造函数中经由构造中的对象调用虚函数,其函数实例应该是在此 class 中有作用的那个。
创建一个继承链中最底端的对象,构造函数从上往下调用,在没有调用对应构造函数之前,该class和它底下的部分都没有被创建出来,所以无法调用相应的虚函数。
所以编译器必须保证有函数实例可以被调用,在构造函数中调用的函数以静态方式调用,不介入虚机制。Point3d
可以调用它作用域内的虚函数。
👆都是扯皮
其实实现起来还是通过虚机制,在 base class 构造函数调用后,在程序员代码之前,设定vptr的值。
5.3 对象复制语意学
不要允许一个虚基类的拷贝操作,或者,不要再任何虚基类中声明数据。