我在设计一个类时最先考虑的是对象的生命周期和上下文. 简单来说就是: 对象由谁在什么时候new出来, 又由谁在什么时候delete掉. 因为C++没有垃圾回收, 所以不得不考虑这些问题. 虽说拿Java来做就没有这些烦恼, 但思考这些问题会让我更明白这个类的代表着什么, 如何更好的设计.
假如, 现在要设计一个显示器类, 类的名字就要Display好了. 其基本功能是开关显示器, 查询显示器的参数, 例如尺寸大小, 分辨率, 点距等等.
实现如下:
1: class Display {
2: public:
3: Display(const char* display_name);
4: ~Display();
5:
6: // contorl functions
7: void turnOn();
8: void turnOff();
9:
10: // info functions
11: int pixelWidth() const;
12: int pixelHeight() const;
13: // ...
14: };
控制接口和查询接口的意义都很明确, 但是上面构造函数和析构函数就有歧义了.
构造函数接受一个显示器的名字来创建显示器(比如你有多个显示器或者远程显示器).
1: Display* d = new Display("0");
2: // ...
3: delete d;
"new"出一个显示器是什么意思. 显示器就在那干嘛要"new", "new"一个显示器我会多一个显示器吗? ''delete"显示器也说不通吧.
好吧, 有人会说我这是咬文嚼字, 但正是这种咬文嚼字让我以另一个方式来理解: 把Display对象当做一个可以操作显示器的"句柄"对象. "new"只是创建一个用于操作显示器的句柄而已, 显示器还是显示器.
说到句柄, 首先想到的肯定是Windows里面的句柄. 一样地, 句柄是对设备资源的引用, 通过句柄可以操作设备, 但不能通过句柄"delete"设备. 这点比指针安全多了.
so, 我们要设计的显示器类要分成两部分了, 一个是操作的句柄DisplayHandle, 另一个是真正对应显示器的类DisplayDevice. DisplayDevice是一直都存在的, DisplayHandle构造出来后就连接上一个DisplayDevice.
1: class DisplayHandle {
2: public:
3: DisplayHandle(const char* display_name)
4: : m_device(DisplayDevice::GetDisplay(display_name)) {
5: }
6:
7: // control functions
8: void turnOn() {
9: m_device->turnOn();
10: }
11: void turnOff() {
12: m_device->turnOff();
13: }
14:
15: // ...
16:
17: private:
18: DisplayDevice* m_device;
19: };
20:
21: class DisplayDevice {
22: public:
23: static DisplayDevice* GetDisplay(const char* display_name);
24:
25: private:
26: DisplayDevice();
27: ~DisplayDevice();
28: };
DisplayDevice是内部的类, 不会暴露出来, 用户也拿不到DisplayDevice的指针. 这比之前Display的概念就清楚多了: "new"出一个Display(DisplayHandle)只是连接了显示器设备, 获得了控制权, 这个对象的生死和显示器没有任何关系.
另一个关于对象生死的例子就是窗口控件.
首先, 控件对象创建出来, 窗口就应该有(假设不隐藏). 窗口关闭, 控件对象就应该销毁(复杂点).
再者, 父控件销毁了, 子控件也应该全部销毁. 原因是父窗口关闭了, 子窗口也应该都关闭, 自然地子控件控件都要销毁.
所以说, 设计窗口控件时一定不能让控件对象在栈上创建; 因为控件对象的生命周期和窗口一样, 其生命是交由用户来决定的, 而不是靠栈来管理的.
很多窗口控件库都是这样设计的, 这里就不举例了.