构造、析构、拷贝语义学 (Semantics of Construction,Destruction,and Copy)
我们先看到下面这个例子:
class Abstract_base {
public:
virtual ~Abstract_base() = 0;
virtual void interface() const = 0;
virtual const char*
mumble() const { return _mumble; }
protected:
char* _mumble;
};
虽然这个 class 被设计为一个抽象类(abstract class),但是它仍然需要一个显式的构造函数以初始化其 data member _mumble。如果没有这个初始化操作,那么该类的派生类的局部对象 _mumble 将无法被设定初值。
一般而言,member 应该被初始化,并且只在 constructor 中或是在 class 的其他 member function 中指定初值。其他任何操作都将破坏封装性质,使 class 的维护和修改更加困难。
书上还提到一种观点:将接口(interface)和实现(implementation)分离。在 java 中,这是常规的做法。但是,将共享的数据放在同一个类中,也是合理的。所以,我们是将 _mumble 从上面的类中进行抽离,还是为这个抽象类提供一个有参构造函数呢?我们接着往下看吧。
在C++中我们可以通过静态调用来调用基类中的纯虚函数(pure virtual function),但是不能由虚拟机制调用。
inline void Abstract_base::interface() const
{
};
inline void Concrete_derived::interface() const
{
Abstract_base::interface();
}
这这种调用在C++中是被允许的。但是同时我们需要注意的是 pure virtual destructor :class 设计者必须得定义它。因为每一个 derived class destructor 不被编译器加以扩张,以静态调用的方式调用其“每一个 virtual base class ”以及“上一层 base class”的destructor。因此,只要缺乏任何一个 base class destructor 的定义,就会导致链接失败。
C++中保证在一个继承体系中的每一个 class object 的 destructor 都会被调用。并且编译器并会为一个类中的 pure virtual destructor 拓展其定义,所以,在设计一个类的时候,我们应该不要把virtual destructor 申明为 pure。
我们再看一下Abstract_base::mumble()
,它的原型是这样的
virtual const char* Abstract_base::mumble() const;
我们将它设计成一个virtual function了,但是这个函数定义内容跟这个类的类型并没有关联,所以,在后续的继承中,我们几乎不会将这个函数进行重写,同时,这个函数的一个实现是一个 inline函数,看到上一个代码。所以,如果调用这个函数,就会引起一些不必要的操作,比如:在虚函数表中进行寻址,然后,对这个函数进行展开,由于这是个虚函数,所以需要绑定类型,在编译阶段我们并不能确定具体的调用对象,这就涉及到一系列的问题(我也没有完全搞清楚)。随然,编译器能够靠优化操作,帮我们将不必要的 virtual invocation去除,但是,并不是很好的设计理念。所以,我们在声明一个 virtual function 之前,应该先考虑该该 function 在后续的继承体系中会不会被重写。
接着我们看到这个接口函数
virtual void /* (const) */ interface() const = 0;
现在我们看到 const
关键字,可以看到这个函数一前一后都有一个 const
,第一个 cosnt
修饰该函数的返回值,不允许后续的操作需改其返回值,第二个 const
是修改调用的对象,不允许在调用中将对象修改。但是我们很多时候,并不能知道在该类的继承体系下还会不会对这个类中的成员数据进行修改。所以,书上也给出了很明确的答案,不使用 const
进行修饰,针对后面那个 const
。
那现在看看比较合理的设计
class Abstract_base {
public:
virtual ~Abstract_base();
virtual void interface() = 0;
const char* mumble() const { return _mumble; }
protected:
char* _mumble;
};
// 对比一下
class Abstract_base {
public:
virtual ~Abstract_base() = 0;
virtual void interface() const = 0;
virtual const char*
mumble() const { return _mumble; }
protected:
char* _mumble;
};
现在我们进入本章的正题。
无继承 情况下的对象构造
我们考虑下面这种情况:
// 这种声明就是C++标准中所说的Plain OI' Data标签
typedef struct Point
{
float _x, _y, _z;
} Point;
Point global; // 全局变量
Point foobar()
{
Point local; // 局部变量,生命周期跟函数的调用的周期相同
Point* heap = new Point; // 堆变量,由程序员手动控制和释放
*heap = local;
delete heap;
return local;
}
当我们以C++来编译这段代码时,编译器会为上面的结构体贴上一个Plain OI' Data
标签。
当编译器遇到这样的标签时:
Point global
观念上 Point 的 trivial constructor 和 destructor 都会产生并被调用,constructor 是在程序起始(startup)被调用,而destructor 实在程序的exit()处被调用。
在C中,global 被视为已给“临时性的定义”,因为它没有显式的初始化操作。一个“临时性的定义”可以在程序中发生多次。那些实例会被链接器折叠起来,只留下一个单独实例,被放在程序 data segment 中一个“特别留给未初始化的global objects 使用”的空间。由于历史的缘故,这块空间被称为 BSS,这是 Block Started by Symbol 的缩写。
C++并不支持“临时性定义”,这是因为 class 构造行为的隐式应用的缘故。**全局变量在C++中被视为完全定义。(它会阻止第二个或跟多的定义)。**C和C++的一个差异就在于,BSS data segment 在C++中相对地不重要。C++中所有全局对象都被以初始化过的数据来对待。
但是,我们看到Point local
,在C++中,如果一个局部变量,只是声明了,并没有被定义,那么编译器是不会管的,使用的时候可能存在隐患。比如上面:
Point local;
Point* heap = new Point;
*heap = local;
// 编译器会将上面的代码拓展为
Point *heap = __new(sizeof(Point));
*heap = local; // 这出现问题了,因为local这块内存是没有被初始化过的。
上面的代码的最后一个赋值操作是通过逐个字节逐个字节进行拷贝的,所以,在后续使用 heap
的时候就会出问题。
-
抽象数据类型(Abstract Data Type)
看下面这种
Point
的定义class Point { public: Point( float x = 0.0, float y = 0.0, float z = 0.0 ) : _x(x), _y(y), _z(z) {} // 没有拷贝构造、赋值运算符和析构函数 }
现在对于一个全局变量
Point global; // 实施 Point::Point(0.0,0..0,0.0);
由于他被定义在全局范围内,所以他的初始化将延迟到程序运行才开始。
如果需要将类中的所有成员设置为常量初值,那么给予一个显式的初值化列表会比较有效率些。因为当函数的激活列表(activation recode)被放进程序堆栈时,显式初值化列表中的常量就可以被放进变量的内存中了。
但是,这种显式初值化列表有三个缺点:
- 只有当 class members 都是 public ,这个方法才能奏效
- 只能指定常量,因为它们在编译时期就可以被计算出来
- 由于编译器并没有自动施行他,所以初始化的行为的失败的可能性会高一些。
还剩下一个小节,我们明天再讲吧。