void* memchr( void *pv, unsigned char ch, size_t size )

来源:互联网 发布:淘宝diy u盘好 编辑:程序博客网 时间:2024/05/21 21:40

节选自 《Write Clean Code》,很多地方看了以后很有感触,但是觉得其中提到的某些要点,日常似乎也不太好应用上,贴一节自己觉得经典的吧。

下面给出一种实现 memchr函数的代码:

void* memchr( void *pv, unsigned char ch, size_t size )

{

    unsigned char *pch = (unsigned char * )pv;

    unsigned char *pchEnd = pch + size;

    while( pch<pchEnd && *pch != ch )

        pch ++;

    return( ( pch < pchEnd ) ? pch : NULL );

}

再与下面的memchr版本做一下比较:

    void* memchr( void *pv, unsigned char ch, size_t size )

    {

        unsigned char *pch = ( unsigned char * )pv;

        unsigned char *pchEnd = pch + size;

        while( pch < pchEnd )

        {

            if( *pch == ch )

                return ( pch );

            pch ++ ;

        }

        return( NULL );

    }

哪个看上去更好一些呢?第一个版本要比较pch和pchEnd两次,第二个版本只比较一次。哪个更清楚呢?更重要的是哪个第一次执行就可能正确呢?

由于第二个版本只在while条件中进行块范围检查,所以它更容易理解并且准确地实现了函数的功能。第一个版本的唯一长处是当需要将程序打印出来时,可以节省一些纸。

上面给出的memchr两个版本正确吗?你是否看出这两个版本具有同样一个细小错误?提示一下:当pv指向存储区的最后72个字节,并且size也是72时,memchr将要查找存储区的什么范围呢?如果答案是“存储区的全部范围,反复不断地查找。”那么你的回答就是对的。由于在程序中使用了有风险的惯用语,memchr陷入了无限的循环之中。

有风险的惯用语就是这样一些短语或表达式,它们看上去似乎能够正确地工作,但实际上在某些特殊场合下,它们并不能正确执行。C语言就是具有这样一些惯用语的语言,最好的办法是:无论什么时候,只要有可能就尽量避免使用这些惯用语。在memchr中有风险的惯用语是:

pchEnd = pch + size;

while( pch < pchEnd )

其中,pchEnd指向存储区中被查找的最后一个字符的下一个位置,因此它可用在while表达式中。程序员觉得这样使用很方便,如果所指的存储位置存在的话,则该程序会工作得很好,但是如果恰好查找到存储器的结尾处,那么所指的位置就不存在了(一个例外情况是:如果使用 ANSI C,总可以计算出某个数组之外的第一个元素的地址。ANSI C支持这个性能)。

作为改正错误的第一步尝试,将上面的代码改写为如下所示:

pchEnd = pch + size – 1;

while ( pch <= pchEnd )

但是,这还不能正确工作。还记得前面讲过的BuildToLowerTable中UCHAR_MAX上溢错误吗?这里也有同样的错误。现在pchEnd可能指向一个合法的存储位置,但是,由于每一次pch增加到pchEnd + l时都要上溢,因此循环将不会终止。

当你可用指针也可用计数器时,使用计数器作为控制表达式是覆盖一个范围的安全方法:

void *memchr( void *pv, unsigned char ch, size_t size )

{

    unsigned char *pch = ( unsigned char * )pv;

    while( size -- > 0 )

    {

        if( *pch == ch )

            return( pch );

        pch ++;

    }

    return( NULL );

}

上面的代码不仅正确,而且所产生的代码可能比前面各个版本产生的代码都要好,因为它不必初始化pchEnd。由于 size在减1之前必须先复制,以便与 0进行比较,所以人们通常认为size--版本比pchEnd版本要大一些并且要慢一些。可是,实际上对于许多编译程序来说,size--版本恰巧更快些、小些。这取决于编译程序是如何使用寄存器的。对于80x86编译程序言,还要取决于所使用的是哪种存储模型。不管怎样,在大小和速度方面,size--版本与pchEnd版本的差别很小,并不值得引起注意。

下面给出另一个惯用语,实际上在前面已经提过了。有些程序员可能会极力主张重写循环表达式,用--size代替size--:

while( --size >= 0 )

……

这种变化的合理一面是:上面这种表达式不产生比以前的表达式更坏的代码。在某些情况下,可能会产生稍好一点的代码。但它的唯一问题是:如果盲目奉行的话,错误将会象苍蝇见到牲畜一样向代码突袭而来。

为什么呢?

如果 size是无符号值(象memchr中的一样),根据定义,将总是大于或等于0,循环将永运执行下去,因此表达式不能正常工作。

如果size是有符号数,表达式也不能正常工作。如size是int类型并且以最小的负值INT_MIN进入循环,它先被减1,那么就会产生下溢,使得循环执行大量的次数。

相反,无论怎样声明size都能使“size-- > 0”正确工作。这是个小小的、但又很重要的差别。

程序员使用“-- size > 0”的唯一原因是想加快速度。让我们仔细看一下,如果真的存在速度问题,那么进行这种改进就好象用指甲刀剪草坪一样,可以这么做,但没有什么效果。如果不存在速度问题,那为什么又要冒这样的风险呢?这就好象没有必要让草坪的所有草叶都一样长,没有必要让每行代码效率都最优一样,要认识到最重要的是总体效果。

在某些程序员看来,放弃任何可能获得效率的机会似乎近似于犯罪。但是,当读完本书以后,你会得到这样的思想即使效率可能会稍稍低一点,也要使用安全的设计和实现来系统地减少风险性。用户不应该注意是否在某个地方又增加了一些循环,而应集中注意力来看是否在试图节省某些循环时而偶然引入了错误。用投资方面的一句术语来说就是:赢利并不能证明冒险是正确的。

使用移位操作来实现乘、除、求模2的幂是另外一种有风险的惯用语。它属于“浪费效率”类。例如,第2章给出的memset快速版本有如下几行代码:

pb = ( byte * )longfill(( long * )pb, 1, size/4 );

size = size % 4

可以肯定,有一些程序员在读到上面的代码时会想:“效率多低”。他们可能会将除操作和求模操作写成如下的形式:

pb = ( byte * )longfill(( long * )pb, 1, size >> 2 );

size = size & 3

移位操作比除法或求模要快,这在大多数机器上是对的。但是,象用2的幂去除或去求模无符号值(如size)的这样的操作,已经优化好了,即使商用计算机也是如此,没有必要再手工优化这些无符号表达式。

那么,有符号表达式又将怎样呢?显式的优化是否值得呢?既值得也不值得。

假定有如下的有符号表达式:

midpoint = ( upper + lower ) / 2;

当有符号数的值为负值时,将其移位与进行有符号除法所得的结果不同,因此二进制补码的编译程序不将除法优化为移位。如果我们知道上面表达式中的upper + lower总是正值,就可以采用移位将表达式改写成如下所示,这个代码要好一些:

midpoint = ( unsigned )( upper + lower ) >> 1

因此,优化有符号表达式是值得的。可是,移位是否是最好的方法呢?不是。下面代码所示的强制转换方法同样也很好,并且比移位法要安全得多。请在编译程序上试一下:

midpoint = ( unsigned )( upper + lower )/2;

上面的代码不是告诉编译程序要做什么,而是将需要进行优化的信息传递给编译程序。通过告诉编译程序所求得的结果是无符号的,来调知它可以进行移位。现在来比较一下两种优化,那个更容易理解?那个更具有可移植性?那个更可能在第一次执行就正确呢?

多年来,我发现了许多由于程序员使用移位来进行有符号值的除法,而有符号值又不能确保为正值而引起的错误;发现了许多方向移错了的移位错误;发现了许多移位位数错了的移位错误;甚至发现了由于不小心将表达式“a=b+c/4”转换为“a=b+c>>2”而引入的优先级错。但我却不曾发现过以键入’/’和’4’来实现除以4时会发生错误。

C语言还有许多其它的有风险的惯用语。有个最好的方法来找到自己经常使用的有风险的惯用语,这就是检查以前出现的每一个错误,再问一下自己:“怎样来避免这些错误?”然后建立个人的风险惯用语表从而避免使用这些惯用语。

原创粉丝点击