类设计的核查表

类设计的核查表

选自C++沉思录

类需要构造函数吗?

有些类很简单,它们的结构就是它们的接口,此刻不需要构造函数,但是,我们要关心足够复杂的类,它们需要构造函数来隐藏它们内部的工作方式。

数据成员是私有的吗?

通常使用共有的数据成员不是什么好事,因为类设计中无法控制何时访问这些成员;

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进行操作之前才检测刚才的操作有没有改变长度,而如果使用成员函数,用户只能通过调用函数来改变长度,这样用户每次改变长度,我们都知道。

例如在vector中求size的问题,length成员变量的处理。

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

如果你想声明该类的对象可以不必显式地初始化它们,则必须显式地写一个无参的构造函数
例如:

class Point
{
public:
    point(int P,int q): x(P),y(q){}
private:
    int x,y;
};

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

Point p;   //错误:没有初始化.

因为这里没有指出怎么样初始化对象p。当然,可能正体现了设计的意图,但必须得是有意识的。此外请牢记,如果一个类需要一个显式构造函数,如上面的Point类一般,则试图生成该类对象的数组是非法的:

Point pa[100];  //错误

即使想要把你的类的所有实例化都初始化,也应该考虑所付出的代价,是否值得为此禁止对象数据。

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

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

类需要析构函数吗?

不是所有的构造函数的类都需要析构函数。例如,表示复数的类即使有构造函数也不可能需要析构函数。如果深入考虑一个类要做些什么,那么该类是否需要析构函数的问题就显得十分明显了。应该问一问该类是否分配了资源,而这些资源又不会由成员函数自动释放。特别是那些构造函数里包含了new表达式的类,通常要在析构函数中加上相应的delete表达式,所以需要一个析构函数

类需要一个虚析构函数吗?

声明一个派生类的对象的同时也自动声明了一个基类的对象。派生类的对象也可以认为是其基类的对象,但是反之不然。因此,C++允许一个基类对象的指针指向其派生类的对象(派生类包含基类的成员函数),但不允许一个派生类对象的指针指向其基类的对象(基类不一定包含派生类的成员函数)

所以,当基类和派生类都有相同的成员函数时,基类指针无论是指向基类对象还是指向派生类对象,调用的都是基类的成员函数。(当然派生类指针指向派生类对象,调用的是派生类的成员函数)

有些类需要析构函数只是为了声明它们的析构函数是虚的。当然,绝不会用作基类的类是不需要析构函数的:任何虚函数只在继承的情况下才有用。
而且虚析构函数通常是空的

类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有一个虚析构函数,
                     //否则将调用错误的析构函数
}

这里,即使B没有虚成员函数,甚至根本没有任何成员函数,也必须有一个虚析构函数,否则delete会出错:
//类B应改成

class B{
    
    
public:   
        int m_Blength;
        virtual ~B() {
    
    }  //虚析构函数通常是空的
}
  1. 类需要复制构造函数吗?
    关于在于复制该类的对象是否相当于复制其数据成员和基类对象。如果并不想当,就需要复制构造函数如果你的类在构造函数内分配资源,则可能需要一个显示的复制构造函数来管理资源。有析构函数(除了空的虚析构函数外)的类通常是用析构函数来释放构造函数分配的资源,这通常也说明需要一个复制构造函数。
    一个典型的例子就是类String:
class String
{
public:
    String();
    String(const char*s);
    //其他成员函数
private:
    char *data;
};

下面定义需要一个析构函数,因为它的数据成员指向了必须由对应的对象释放的被动态分配的内存。出于同样的原因,它还需要一个显式的复制构造函数;没有的话,复制String对象就会以复制它的data成员的形式隐式地定义。复制完后,两个对象的data成员将指向同样的内存;当这两个对象被销毁时候,这个内存会被释放两次(深拷贝与浅拷贝问题)
如果不想用户能够复制类的对象,就声明复制构造函数(可能还有赋值操作符)为私有的

class Thing {
    
    
    public:
        //…
    private:
        Thing (const Thing&);
        Thing& operator= {
    
    const Thing&};
    }; 
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;
}
  1. 你的类需要一个赋值操作符吗?
    如果需要复制构造函数,同理多半也会需要一个赋值操作符。如果不想用户能够设置类的对象,就将赋值操作符私有化。类X的赋值由X::operator=来定义。通常,operator=应该返回一个X&,并且由return *this;结束以保证与内建的复制操作符一致。

  2. 你的赋值操作符能正确地将对象赋值给对象本身吗?
    自我赋值尝尝被错误地应用,以至于不止一本Cplusplus书把它弄错了。赋值总是用新值取代目标对象的旧值如果原对象和目标对象是同一个,而我们又奉行“先释放旧值,再复制”的行事规程,那么就可能在还没有实施复制之前就把原对象销毁了

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::operator=(const String& s)
{
    
    
   if(&s != this)
    {
    
    
        delete[]data;
        data = new char[strlen(s.data)+1];
        strcpy(data,s.data);
    }
    return *this;
}

//另一个可行的办法就是将旧的目标值保存起来,直到将源复制完成:

//正确的实现方法2
String& String::operator=(const String& s)
{
    
    
    char* newdata = new char[strlen(s.data)+1];
    strcpy(newdata,s.data);
    delete []data;
    data = newdata;
    return *this;
}
  1. 你的类需要定义关系操作符吗?
    由于Cplusplus支持模板,所以通用库也逐渐开始包含容器了,这些提供了关于诸如列表、集合和图等数据结构的泛型定义。这些容器依赖于它们所包含的元素类型的操作通常要求容器能够判断两个值是否相等。还常常需要容器具有判断一个值是否大于或小于另一个值的能力
    因此,如果你的类逻辑上支持相等操作,那么提供 operator== 和operator!= 就可能会存在很多好处。
    类似地,如果你的类的值有某种排序关系,那就可能会想提供余下的关系操作符。即使不希望用户直接使用关系操作符,也可能需要这些关系操作符。只要它们想创建您的类型的有序集合,你就必须提供关系操作符

  2. 删除数组时候你记住用delete[]吗?
    []这个奇怪的语法之所以存在,是因为Cplusplus希望在保持与C兼容的同时关注效率。C程序员们希望在他们写函数时使用malloc分配内存,然后返回给Cplusplus函数。之后他们希望Cplusplus函数能够使用delete来释放那些内存。Cplusplus系统不想占用现有C系统的malloc函数,因此必须利用原来的这个malloc直接实现new,而不能另起炉灶。因此Cplusplus库在释放数组时不一定要清楚数组的大小。即使malloc把长度值存储到某个位置上,Cplusplus库在释放数组时不一定要清楚数组的大小。即使malloc把长度值存储到某个位置上,Cplusplus库也没法在保证可移植性的前提下找到这个值。
    因此,作为一种折中方案,Cplusplus要求用户告知要被删除的是不是数组
    如果是,该实现就可能会提供另一个地方来存储长度,因为与数组所需的内存量相比,这个常数的开销会小很多。尽管有些Cplusplus实现只在数组中的对象有特殊的析构函数时才要这样做,在删除任何类型的数组时使用[]格式仍然是一种很好的习惯

  3. 记得在复制构造函数和赋值操作符的参数类型中加上const了吗?
    有些早起的c++著作建议类X的复制构造函数应为X::X(X&)。这种建议是不正确的.复制构造函数应该是像X::X(const X&)。毕竟复制对象不会改变原对象!实际上,由于绑定一个非const引用到一个临时的对象是非法的,使用X::X(X&)作为复制构造函数不会允许复制任何特殊表达式的结构。同样的道理也适用于赋值:使用X::operator=(const X&),而不是X::operator=(X&)。
    正确的做法:

XXX::XXX(const XXX&);
XXX::operator=(const XXX&);
  1. 如果函数有引用参数,它们应该是const引用吗?
    只有当函数想改变参数时,它才应该有不用const声明的引用参数。所以,例如,不应该用Complex operator+(complex& x,complex& y);而应该总用Complex operator+(const Complex& x,const Complex& y );
    除非像允许增加两个Complex对象来改变它们的值!否则,由于x+y不是左值,不能绑定一个非const 引用到自身,所以类似于x+y+z的表达式就变得不可能了。

  2. 记得适当声明成员函数为const的了吗?
    如果确信一个成员函数不用修改它的对象,就可以声明它为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);
};

变通的方法让编译器指出什么时候应该有一个虚析构函数、什么时候要自动提供析构函数。另外,问题在于要精确定义何时生成这样的析构函数。如果定义不完善,程序员就得进行检查。与其事后检查,还不如一开始就定义虚析构函数.

备注: 选自 C++沉思录

猜你喜欢

转载自blog.csdn.net/u010523811/article/details/124266572
今日推荐