以下问题都没有确切的答案,关键在于在设计类的时候要时刻提醒自己思考它们,不要等出了错再回头。
1.你的类是否有构造函数?
如果没有请关闭此文章;
有些类很简单,它们的结构就是它们的接口,我们要关心足够复杂的类,它们需要构造函数来隐藏它们内部的工作方式;
2.你的类数据成员是私有的吗?
通常使用共有的数据成员不是什么好事,因为类设计中无法控制何时访问这些成员;
templete<class T> class Vector{ public: int length; }
如果类设计者将矢量的长度当做一个成员变量,那么就必须保证这个成员变量在任何时候都能正确反映实际矢量的长度,因为没有办法知道类使用者什么时候访问这个信息;
如果这样实现:
templete<class T> class Vector{ public: int length() const; }
使用函数而不是变量,在还允许读取访问的时候能够很容易的阻止写入访问,但是如果类创建后还需要改变长度,就不行了;
我们可以通过引用来只允许使用者进行读取访问:
templete<class T> class Vector{ public: const int& length; //每个构造函数都将length绑定到true_length上; private: int true_length; } //这样做确实可以防止出错,但是仍然不如用函数实现length灵活;
即使类设计者允许用户改变Vector的长度,把长度当成共有也不是一个好办法,和复制Vector一样,也需要自动分配和回收内存,如果长度是一个用户直接设置的变量,就无法马上检测到用户所作的改变,所以,对这种改变的检测总是滞后的,很可能是在每次对Vector进行操作之前才检测刚才的操作有没有改变长度,而如果使用成员函数,用户只能通过调用函数来改变长度,这样用户每次改变长度,我们都知道。
3.你的类是否需要一个无参的构造函数?
如果一个类已经有一个构造函数,而你想声明该类的对象,然后不必显式的初始化它们,则必须显式地写一个无参的构造函数;
4.是不是每个构造函数初始化所有的数据成员?
构造函数的用途就是一种明确定义的状态来设置对象,对象的状态有对象的数据成员进行反映,因此,每个构造函数都要负责为所有的数据成员设置经过明确定义的值,如果构造函数没做到这一点,就可能产生错误,但并不绝对;
5.类需要析构函数吗?
你只要深入思考一下这个类要做什么?是否分配了资源,而这些资源又不会由成员函数自动释放;(特别是构造函数里包含了new的类,析构里要加上相应的delete)
6.类需要一个虚析构函数吗?
实际上,不会用作父类的类是不需要虚析构函数的:任何虚函数只在继承的情况下才有用。
何时需要虚析构?
类D继承与类B,只要有人可能会对实际指向D类对象的B*指针执行delete操作,你就需要给B加一个虚析构函数(即使B与D都没有虚函数),否则将调用错误的析构;
class B{ public: int m_Blength; } class D:public B{ public: int m_Dlength; } int main() { B* bp = new D; delete bp; //错误 } //类B应改成 class B{ public: int m_Blength; virtual ~B() {} //虚析构函数通常是空的 }
7.你的类需要拷贝构造函数吗?
你需要考虑的是拷贝该类的对象是否就相当于拷贝其数据成员和基类对象,如果并不想当,就需要拷贝构造函数;
(如果类在构造函数中分配资源,则需要一个显式的拷贝构造函数来管理资源)
典型的例子:
class String{ public: String(); String(const char* s); private: char* data; };
我们可以看出String类需要一个析构函数,因为它的数据成员指向了一个必须由对应的对象释放的被动态分配的内存,这样,它还需要一个显式的拷贝构造函数:如果没有,拷贝String对象就会以拷贝它的data成员的形式隐式的定义,复制完之后两个对象的data成员将指向同样的内存,当这两个对象被销毁时,这块内存会被释放两次;(深拷贝与浅拷贝问题)
class Line { public: int getLength( void ); Line( int len ); // 普通的构造函数 Line( const Line &obj); // 拷贝构造函数 ~Line(); // 析构函数 private: int *ptr; }; int main( ) { Line line1(10); //普通构造 Line line2 = line1; // 这里调用了拷贝构造函数 return 0; }
8.你的类需要一个赋值操作符吗?
如果需要拷贝构造函数,多半也会需要一个赋值操作符,如“=”;如果不想让其他人设置类的对象,就将赋值操作符私有化
类XXX的赋值由 XXX::operator=来定义,通常,operator=应该返回一个 XXX&,并且由 return *this;结束以保证与内建的拷贝操作符一致;
class Thing{ public: //... private: Thing(const Thing&); Thing& operator=(const Thing& thing); };
9.你的赋值操作符能正确的将对象赋值给对象本身吗?
赋值常常被错误的应用,有时赋值总是用新值取代目标对象的旧值,如果两个对象是同一个,我们还是“先释放,再赋值”,那么就可能在还没有赋值之前就将原对象给销毁了;
class String{ public: String& operator=(const String& s); private: char* data; } //很明显不正确的实现; String& String::operatpr=(const String& s) { delete [] data; data = new char[strlen(s.data)+1]; strcpy(data,s.data); return *this; }
一旦我们把一个String对象赋给它本身,这个方法就会彻底失败,因为s和*this都指向同样的对象;
//正确的方法1(显式的预防) String& String::operatpr=(const String& s) { if(&s != *this){ delete [] data; data = new char[strlen(s.data)+1]; strcpy(data,s.data); } return *this; } //正确的做法2(旧值保存起来,直接将源值复制) String& String::operatpr=(const String& s) { char* newdata = new char[strlen(s.data)+1]; strcpy(newdata,s.data); delete [] data; data = newdata; return *this; }
10.你的类需要定义关系操作符吗?
要看你类逻辑上是否支持“=”操作等;
11.删除数组时你记得用delete[]了吗?
[ ]这个奇怪的语法之所以存在,是因为C++希望在保持与C兼容性的同时关注效率,C++库释放数组不一定要清除数组的大小。即使malloc把长度值存储到某一个位置上,C++库也没办法在保证可移植性的前提下找到这个值,因此这是一个折中的方案,C++要求使用者告知要被删除的是不是数组,如果是,该实现就可能会提供另一个地方来存储长度,因为与数组所需要的内存量相比,这个常数的开销会小很多。
12.在拷贝构造函数和赋值操作符的参数类型上记得加上const了吗?
正确的做法:
XXX::XXX(const XXX&);
XXX::operator=(const XXX&);
实际上原因是拷贝对象不会改变原对象;
13.如果函数有引用参数,它们应该用const引用吗?
只有函数想改变参数时,它才应该不用const声明;
例如:不应该用:
Complex operator+(Complex& x,Complex& y);
而应该用:
Complex operator+(const Complex& x,const Complex& y);
除非允许增加两个Complex对象来改变它们的值,否则,由于x+y不是左值,不能绑定一个非const引用到自身,所以类似于x+y+z的表达式就变得不可能。
14.记得适当地声明成员函数为const了吗?
如果确定一个成员函数不用修改它的对象,就可以声明为const;
class Vector{ public: int length(); int length(int); }; //返回n与v的长度中较大的一个 template<class T> int padded_length(const Vector<T>& v,int n) { int k = v.length(); //不会编译,因为v是const引用 return k > n ? k : n; } //正确: class Vector{ public: int length() const; int length(int); };