C++类设计者的核查表(一)

核查表不是任务清单。它的用途是帮助你回忆起可能会忘掉的事情,比如飞行员核查表需要核查飞行过程中需要注意的事情,防止发生一些不必要的意外。下面是一些C++类的设计核查,这些问题并没有一个确切的答案,但是它会提醒你思考它们,并确定你做的事情是有意识的决定,而不是偶然事件。

  • 你的类需要一个构造函数吗?
    有些类比较简单,他的结构就是它的接口,因此不需要构造函数。但是如果是一些比较复杂的类,它们需要构造函数来隐藏其内部的工作方式。

  • 你的数据成员是私有的吗?
    通常使用公有的数据成员不是什么好事,因为类的设计者无法控制何时访问这些成员,例如下面一个支持可变长矢量的类:

template<typename T>
class Vector
{
public:
	int length;
};

如果类的设计者将矢量的长度设置为一个成员变量,那么设计者就必须保证这个变量在任何时候都可以显示矢量的实际长度,因为不知道什么时候会访问这个信息。另一方面,如果函数在类中是这样实现的,比如:

template<typename T>class Vector {
public:
	int length() const;
};

这样除非用户调用,否则vector根本不必计算长度。这个思想就很重要了。
另外,使用函数而不是变量,在还允许读取访问的时候能够容易的阻止写入访问。vector的第一个版本根本没有阻止用户改变长度的措施。原则是length可以是一个const int,但是如果创建后还要改变长度,就不能这样做了。但是我们可以通过引用来只允许用户进行读取访问:

template<typename T>class Vector {
public:
	const int& length;	//每个构造函数都将length绑定到true_length
	// ...
private:
	int true_length;
};

这样做确实可以防止以后出错,但还是不如一开始直接用函数实现length方便。这样用户不可以改变length的值,所以会返回一个实际值。

但是如果类的设计者希望用户可以改变length的值,那将其设置为public也不是个好办法,和复制vector的值一样,改变长度大致上也需要手动分配和回收内存。如果length是一个用户直接设置的变量,就无法迅速检测到用户的变化,所以对这种改变的检测总是滞后的,很可能是在操作vector时才检测到length的变化。但是使用成员函数改变length,每次改变时都会调用成员函数,这样每次用户改变length时设计者都可以察觉到。

如果有两个操作length的函数,是不是应该使用set_length和get_length来对长度进行不同的操作,而且其返回值是void还是一个值?或者返回一个bool变量,是否操作成功?还是返回一个值,变化前的值还是变化后的值?所以要规范自己的设计,对代码可读性有很大的影响。

  • 你的类需要一个无参的构造函数吗?

如果一个类已经有了构造函数,而你想让你的类不必显式的初始化它们,则必须显式的写一个无参的构造函数——例如:

class PointDemo
{
public:
	PointDemo(int p, int q) :x(p), y(q) {};
	//...
private:
	int x, y;
};

这里我们定义了一个有构造函数的类。除非这个类有一个不需要参数的构造函数,否则下面的语句就是非法的:

PointDemo p;

这里没有指出怎么初始化p。此外,如果一个类需要一个显示构造函数,比如上面的类,则试图创建该类的对象数组是非法的:

PointDemo p[100]

即使你想把所有的对象都实例化,也要考虑所付出的代价。

  • 是不是每个构造函数都需要初始化所有的数据成员?

构造函数的用途就是用一种明确定义的状态来设置对象。对象的状态由对象的数据成员进行反映。因此每个构造函数都需要负责为所有的数据成员设置经过定义的值。如果构造函数没有做到这一点,就可能导致错误。
当然,这也不是完全正确的,比如类有一些数据成员,它们只在它们对象存在的一段时间有意义。

  • 类需要析构函数吗?
    不是所有有构造函数的类都需要析构函数。例如表示复数的类即使有析构函数,但是也不需要。如果深入考虑一个类需要做什么,那么考虑其是否需要析构函数就很重要了。应该问问该类是否分配了资源,而这些资源又不会由成员函数自动释放,这就足够了。尤其是那些构造函数中包含了new的类,析构函数中还需要加一个delete来释放内存。

  • 类需要一个虚析构函数吗?
    虚析构函数在继承时有很重要的作用,因此,绝对不会当作基类的类是不需要虚析构函数的。虚析构函数会在释放资源时一步步的释放,从而实现递归释放资源,如果不是虚析构函数则会调用错误的虚构函数,这个在我以前的文章中有讲到。并且,虚析构函数通常是空的。

  • 你的类需要复制构造函数吗?
    很多时候都是不,但是有时候是需要。关键在于复制时是否相当于复制其数据成员和基类对象,如果并不相当,则需要复制构造函数。
    如果你的类在构造函数内分配资源,则可能需要一个显式的复制构造函数来管理资源。有析构函数(除了空的虚析构函数外)的类通常是用析构函数来释放构造函数分配的资源,这通常也说明需要一个复制构造函数,例如:

class String
{
public:
	String();
	String(const char* s);
private:
	char *data;
};

很明显,它需要一个析构函数,因为它的数据成员指向了必须由对应的对象释放的被动态分配的内存。同理,它还需要一个显式的复制构造函数:没有的话,复制string对象就会复制它的data成员的形式隐式的定义。复制完后,两个对象的data成员将指向同样的内存;当这两个对象被销毁时,这个内存会被释放两次。在拷贝构造函数时需要重新分配内存,而且在析构函数中需要加入判断指针是否为空的判断条件,使析构函数正确的释放分配的资源。
当然,如果不希望用户能够复制类的对象,就声明构造函数(可能还要赋值操作符)为私有的:

class String
{
public:
	//...
private:
	String(const String&);
	String &operator=(const String&);
};

如果没有其他的成员使用这些成员函数,如上声明就够了。没有必要定义它们,因为没人会调用它们,你不能,用户也不能。

这些便是我对C++类设计的一些个人见解,希望对大家有所帮助。如果有不对的地方欢迎大家批评指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章