一致性——《C++编程风格》读书笔记(三)

来源:互联网 发布:mac bombshell试色 编辑:程序博客网 时间:2024/05/29 19:46

       对于一个类,可以通过类的接口类的实现这两方面来进行观察。类的接口由类的公有成员定义,它决定了这个类所创建的对象能够为程序其它部分中的客户代码提供什么样的服务;类的实现是帮助完成这些服务的功能,它们通常是作为类的私有成员被封装起来,客户代码是无法进行反问的。设计一个类时,应注意两点:接口必须能够代表一致的抽象,而实现则必须使得对象在行为上与这个抽象保持一致。

      状态:任何时候一个对象都是处于某种状态。这种状态是由对象中所有数据的共同成员决定的。我们可以通过接口将对象从一种状态驱动到另一种状态,而类的实现必须能够帮助维持这个状态。

      接口和实现可以通过不同的模型来表示对象的状态,这也分别称之为逻辑状态物理状态。多个物理状态可以对应一个逻辑状态。

 

1.示例:Mstring类

   初步程序代码如下:

程序输出:

The wheel that squeaks the loudest
Is the one that gets the grease
Josh Billings

 

 


Process returned 0 (0x0)   execution time : 0.016 s
Press any key to continue.

 

2.代码改进

2.1明确定义的状态

     下面两个构造函数存在同样的问题:

      Mstring () { s = new char[80]; len = 80;}
      Mstring (int n) {s = new char[n]; len = n;}

     如果在创建Mstring对象时调用了上述任一构造函数之一,那么这个Mstring对象的初始状态是未定义的。例如下面的代码将出错:

                          Mstring x,y(128); x.print();y.print();

      构造函数是为了初始化对象,因此一个构造函数至少应该使得对象处于明确定义的状态。

原则:

      a.构造函数应该使得对象处于明确定义的状态。

      对初始状态最稳妥的处理是使用空字符串,定义一个带有默认实参的构造函数可以代替前面的两个构造函数,因此可以做如下改进:

                  Mstring(int n = 80){ s = new char[n];s[0] = '/0'; len = n;}

原则:

      b.我们应该考虑使用默认参数的形式来代替函数重载的形式。

2.2物理状态的一致性

      在Mstring类的第三个构造函数中将对函数的字符串参数进行复制。它对s进行了初始化,但对len的处理方式与前两个构造函数不同。在前两个构造函数中,len是所分配的字符数组的长度,在第三个中,len是字符串的长度。两者相差1。

      通过分析,Mstring对象的状态对于数据成员len的含义并没有一致的定义。

2.3类不变性

      对于每个类,我们都可以写出一组类不变性(class invariant)条件,在类的每个对象生存期类,这些条件应该是成立的,例如,Mstring类的一个不变性条件是:len == strlen(s).

原则:

       c.用一致的方式来定义对象的状态--这需要识别出类不变性。

       如果一个c++程序员在头脑中始终记着循环不变性,那么他在编写循环时就有两种选择:在编写完循环后,他可以将不变性作为循环代码的注释;或者将不变性作为一个断言整合到代码中(通过在标准头文件assert.h中定义的assert宏,我们可以很容易的在代码中增加断言)。将不变性作为类的注释和将不变性作为循环的注释一样简单,然而将类的不变性作为断言整合到类的代码中则困难些。
2.4 动态内存一致性

     动态内存主要考虑两点:首先,是否足够大以容纳将要存储的信息?其次,是否所有的动态内存都是可以回收的。

    对于第一点:assign()没考虑目标字符数组的长度或大小。concat()则总是去分配。这两个函数的表现行为上存在不一致。

原则:

     d.类的借口定义应该是一致的--避免产生困惑。

    对于第二点:内存泄露,当不是所有的new来分配的内存都使用delete来进行释放时,就会发生内存泄露。Mstring类的内存泄露既不在构造函数中,也不在析构函数中,而是在concat中。因为在函数中分配新的数组时,并没有释放对象中当前的数组。当concat()执行下面的语句时:

     s = new char[len + 1];

     s马上被一个新的指针值覆盖,而前一个指针则被抛弃了,这就使得前一个指针所指向字符数组成为一块垃圾内存,这就形成了内存泄露。

下面的代码不足以解决concat中的问题:

 

要改正该问题,delete语句只能被增加在新的字符串创建之后:

 

原则:

      e.对于每个new操作,都应该有相应的delete操作。

3.改进后的代码


 

4.编程风格示例:第二种方法

 

 

 

4.1冗余

    _length的长度信息并没有被用到过,当需要字符串的长度信息时,这个长度信息却从没有被用到过。例如在Strdup()这个函数中每次都是调用strlen()来得到字符串的长度。

原则:

f.避免对从不使用的状态信息进行存储和计算。

 

4.2动态内存及operator=

    如果在赋值符两边是同一个SimpleString对象,且调用operator=(const SimpleString&),那么表达式的结果将是未定义的。可作如下改进:

 

SimpleString& SimpleString::operator= (const SimpleString& s)

{

       If(this = &s)

              return *this;

    delete [] _string;

    _string = s._string ? Strdup (s._string) : 0;

    _length = _string ? s._length : 0;

    return *this;

}

 

 

原则:

g.在定义operator=时,我们应该考虑到x=x这种情况。

    将上述原则退而广之,对于operator=(const char*),如果函数的参数指针等于被删除的字符串指针,那么函数中利用到参数指针内容的部分将会出现未定义的结果。

 

SimpleString& SimpleString::operator= (const char* s)

{

    char* prev_string = _string;

    _string = s ? Strdup (s) : 0;

_length = s ? strlen (s) : 0;

delete *prev_string;

    return *this;

}

 

    就像我们在String类中用一个带有默认参数的构造函数来代替最初的两个构造函数一样,我们没有理由为同样的代码维护两份拷贝,因此我们应该在一个运算符函数中调用另一个运算符函数

 

原则:

h.用一个通用的函数来代替重复的表达式序列。

注意到上面这些讨论后修改后的程序为:

 

原创粉丝点击