核查表并不是任务清单。它的用途是帮助你回忆起可能忘记的事情,而不是来约束你。如果只是盲目地按照核查表的要求按部就班地做,到头来可能还是会忘记一些事情。知道这一点后,请看下面关于定义类时候要弄清楚的一些问题。这些问题没有确切的答案,关键是要提醒你思考它们,并确认你所作的事情是出于有意识的决定,而不是偶然事件。
核查表需要核查的内容:
- 类需要构造函数吗?
- 数据成员是私有的吗?例如在vector中求size的问题,length成员变量的处理。
- 你的类需要一个无参的构造函数吗?如果一个类已经有了构造函数,而你想声明该类的对象可以不必显示地初始化它们,则必须显式地写一个无参的构造函数——例如:这里我们定义了一个有一个构造函数的类。除非这个类有一个不需要参数的构造函数,否则下面的语句就是非法的: Point p;//错误:没有初始化.因为这里没有指出怎么样初始化对象p。当然,可能正体现了设计的意图,但必须得是有意识的。此外请牢记,如果一个类需要一个显式构造函数,如上面的Point类一般,则试图生成该类对象的数据是非法的:Point pa[100];即使想要把你的类的所有实例化都初始化,也应该考虑所付出的代价,是否值得为此禁止对象数据。
class Point { public: point(int P,int q): x(P),y(q){} private: int x,y; };
- 是不是每个构造函数初始化所有的数据成员?构造函数的用途就是用一种明确定义的状态来设置对象。对象的状态由对象的数据成员进行反映。因此,每个构造函数都要负责为所有的数据成员设置经过明确定义的数值。如果构造函数没有做到这一点,就很可能导致错误。当然,这种说法也未必总是正确的。有时,类会有一些数据成员,它们只在它们的对象存在了一定时间之后才有意义。
- 类需要析构函数吗?不是所有的构造函数的类都需要析构函数。例如,标识复数的类即使有构造函数也不可能需要析构函数。如果深入考虑一个类要做些什么,那么该类是否需要析构函数的问题就显得十分明显了。应该问一问该类是否分配了资源,而这些资源又不会由成员函数自动释放。特别是那些构造函数里包含了new表达式的类,通常要在析构函数中加上相应的delete表达式,所以需要一个析构函数。
- 类需要一个虚析构函数吗?有些类需要析构函数只是为了声明它们的析构函数是虚的。当然,绝不会用作基类的类是不需要析构函数的:任何虚函数只在继承的情况下才有用。而且虚析构函数通常是空的。
- 类需要复制构造函数吗?关于在于复制该类的对象是否相当于复制其数据成员和基类对象。如果并不想当,就需要复制构造函数。如果你的类在构造函数内分配资源,则可能需要一个显示的复制构造函数来管理资源。有析构函数(除了空的虚析构函数外)的类通常是用析构函数来释放构造函数分配的资源,这通常也说明需要一个复制构造函数。一个典型的例子就是类String:下面定义需要一个析构函数,因为它的数据成员指向了必须由对应的对象释放的被动态分配的内存。出于同样的原因,它还需要一个显式的复制构造函数;没有的话,复制String对象就会以复制它的data成员的形式隐式地定义。复制完后,两个对象的data成员将指向同样的内存;当这两个对象被销毁时候,这个内存会被释放两次。如果不想用户能够复制类的对象,就声明复制构造函数(可能还有赋值操作符)为私有的。
class String { public: String(); String(const char*s); //其他成员函数 private: char *data; };
- 你的类需要一个赋值操作符吗?如果需要复制构造函数,同理多半也会需要一个赋值操作符。如果不想用户能够设置类的对象,就将赋值操作符私有化。类X的赋值由X::operator=来定义。通常,operator=应该返回一个X&,并且由return *this;结束以保证与内建的复制操作符一致。
- 你的赋值操作符能正确地将对象赋值给对象本身吗?自我赋值尝尝被错误地应用,以至于不止一本C++书把它弄错了。赋值总是用新值取代目标对象的旧值。如果原对象和目标对象是同一个,而我们又奉行“先释放旧值,再复制”的行事规程,那么就可能在还没有实施复制之前就把原对象销毁了。避免这个问题的最简单的方法就是显式地加以预防:
String& String::operator=(const String& s) { if(&s != this) { delete[]data; data = new char[strlen(s.data)+1]; strcpy(data,s.data); } return *this; } //另一个可行的办法就是将旧的目标值保存起来,直到将源复制完成: String& String::operator=(const String& s) { char* newdata = new char[strlen(s.data)+1]; strcpy(newdata,s.data); delete []data; data = newdata; return *this; }
- 你的类需要定义关系操作符吗?由于C++支持模板,所以通用库也逐渐开始包含容器了,这些提供了关于诸如列表、集合和图等数据结构的泛型定义。这些容器依赖于它们所包含的元素类型的操作。通常要求容器能够判断两个值是否相等。还常常需要容器具有判断一个值是否大于或小于另一个值的能力。因此,如果你的类逻辑上支持相等操作,那么提供operator==和operator!=就可能会存在很多好处。类似地,如果你的类的值有某种排序关系,那就可能会想提供余下的关系操作符。即使不希望用户直接使用关系操作符,也可能需要这些关系操作符。只要它们想创建您的类型的有序集合,你就必须提供关系操作符。
- 删除数组时候你记住用delete[]吗? [ ]这个奇怪的语法之所以存在,是因为c++希望在保持与c兼容的同时关注效率。c程序员们希望在他们写函数时使用malloc分配内存,然后返回给c++函数。之后他们希望c++函数能够使用delete来释放那些内存。c++系统不想占用现有C系统的malloc函数,因此必须利用原来的这个malloc直接实现new,而不能另起炉灶。因此c++库在释放数组时不一定要清楚数组的大小。即使malloc把长度值存储到某个位置上,c++库在释放数组时不一定要清楚数组的大小。即使malloc把长度值存储到某个位置上,c++库也没法在保证可移植性的前提下找到这个值。因此,作为一种折中方案,c++要求用户告知要被删除的是不是数组。如果是,该实现就可能会提供另一个地方来存储长度,因为与数组所需的内存量相比,这个常数的开销会小很多。尽管有些c++实现只在数组中的对象有特殊的析构函数时才要这样做,在删除任何类型的数组时使用[]格式仍然是一种很好的习惯。
- 记得在复制构造函数和赋值操作符的参数类型中加上const了吗?有些早起的c++著作建议类X的复制构造函数应为X::X(X&)。这种建议是不正确的:复制构造函数应该是像X::X(const X&)这样。毕竟复制对象不会改变原对象!实际上,由于绑定一个非const引用到一个临时的对象是非法的,使用X::X(X&)作为复制构造函数不会允许复制任何特殊表达式的结构。同样的道理也适用于赋值:使用X::operator=(const X&),而不是X::operator=(X&)。
- 如果函数有引用参数,它们应该是const引用吗?只有当函数想改变参数时,它才应该有不用const声明的引用参数。所以,例如,不应该用Complex operator+(complex& x,complex& y);而应该总用Complex operator+(const Complex& x,const Complex& y );除非像允许增加两个Complex对象来改变它们的值!否则,由于x+y不是左值,不能绑定一个非const 引用到自身,所以类似于x+y+z的表达式就变得不可能了。
- 记得适当声明成员函数为const的了吗?如果确信一个成员函数不用修改它的对象,就可以声明它为const,这样就把他用于const对象了。变通的方法让编译器指出什么时候应该有一个虚析构函数、什么时候要自动提供析构函数。另外,问题在于要精确定义何时生成这样的析构函数。如果定义不完善,程序员就得进行检查。与其事后检查,还不如一开始就定义虚析构函数。