《编写高质量代码:改善c程序代码的125个建议》——建议5:使用有严格定义的数据类型

简介:

本节书摘来自华章计算机《编写高质量代码:改善c程序代码的125个建议》一书中的第1章,建议5,作者:马 伟 更多章节内容可以访问云栖社区“华章计算机”公众号查看。

建议5:使用有严格定义的数据类型

大家都知道,C语言是一种既具有高级语言的特点,又具有汇编语言特点的程序设计语言。它既可以作为系统设计语言来编写系统应用程序,也可以作为应用程序设计语言来编写不依赖计算机硬件的应用程序。因此,它是一种可移植性很高的语言,用它所写的程序可以很方便地部署到不同的平台之上。
尽管如此,C语言在可移植性方面实际上还是存在着许多重要的问题。除了不同的系统使用的C语言标准库不同之外,预处理程序和语言本身在许多重要方面也会不尽相同。我们知道,ANSI委员会对C语言的大多数问题进行了标准化,从而使程序员可以很方便地写出可移植的代码。但是,ANSI标准却并没有准确定义像char、int和long这样的内部数据类型,而是将这些重要的实现细节留给编译程序的研制者来决定。
例如,某一个ANSI标准的编译程序可能具有32位的int和char类型,它们在默认状态下是有符号的;而另一个ANSI标准的编译程序可能有16位的int和char类型,默认状态下是无符号的。尽管如此不同,但这两个编译程序却都是严格符合ANSI标准的。为了让读者更加深入地了解这种情况,我们来看下面一段示例代码:

char ch;
ch= (char)0xff;
if(ch == 0xff)
{
}

在上面的代码中,我们先将整数0xff赋给char类型的变量ch,然后再将ch变量与整数0xff进行比较。从表面上看,语句“if(ch == 0xff)”应该返回真。但实际情况并非如此,语句“if(ch == 0xff)”的具体返回值因系统而异,也就是说它有可能返回真,也有可能返回假。或许有人会疑惑,这么明显的一个语句怎么会发生这种情况呢?
其实,原因很简单。上面我们说过,ANSI标准确并没有准确定义像char、int和long这样的内部数据类型,而是将这些重要的实现细节留给了编译程序。因此,它的结果完全依赖于编译程序。如果默认字符是无符号的,则语句“if(ch == 0xff)”的返回值肯定为真;但对字符为有符号的编译程序而言,语句“if(ch == 0xff)”的返回值却会为假。
在上面的代码中,字符ch要与整型数0xff进行比较。根据C语言的转换规则,编译程序必须首先将ch转换为整型int,待两者类型一致后再进行比较。这样,如果int是32位的,则在转换中会将其值从0xff扩充为0xffffffff。因此,语句“if(ch == 0xff)”的返回值就为
假了。
其实,对于上面的这些问题,ANSI委员会成员并非视而不见。实际上,他们考查了大量的C语言实现并得出了这样的结论:由于各编译程序之间的类型定义是如此不同,以致定义严格的标准将会使大量现存代码无效。而这就恰恰违背了他们的一个重要指导原则:“现存代码是非常重要的。”
除此之外,对类型进行严格约束也将违背委员会的另外一个指导原则:“保持C语言的活力,即使不能保证它具有可移植性,也要使其运行速度快。”因此,如果实现者感到有符号字符对给定的机器来说更有效,那么就使用有符号字符吧!同样,硬件实现者可以将int选择为16位、32位或别的位数。也就是说,在默认状态下,用户并不知道位域是有符号的还是无符号的。
当然,这种内部类型在其规格说明中存在着一个不足之处,在今后升级或改变编译程序时,或者移到新的目标环境时,或者与其他单位共享代码时,甚至在改变工作且所用编译程序的规则全部改变时,这个不足就会体现出来。但是,这并不意味着用户就不能安全地使用这些内部类型。其实,只要用户不对ANSI标准没有明确说明的类型再作假设,用户就可以安全使用内部类型。例如,对于char数据类型,只要它能提供0~127的值(即有符号字符和无符号字符域的交集),一般就是可移植的。例如下面这段代码:

int strcmp(const char *strLeft,const char *strRight)
{
    assert(strLeft!=NULL&&strRight!=NULL);
    int ret=0;
    while(!(ret= *strLeft-*strRight) && *strRight)
    {
            strLeft++,strRight++; 
    }
    if(ret<0)
    {
            ret=-1;
    }
    else if(ret>0)
    {
            ret=1;
    }
    else
    {
            ret=0;
    }
    return ret;
}

在上面代码中,strcmp函数用于比较两个字符串。如果strLeft<strRight,则返回-1;如果strLeft==strRight,则返回0;如果strLeft>strRight,则返回1。从表面上看,strcmp函数并没有什么大的问题,但如果仔细观察,你会发现strcmp函数在可移植方面存在问题。因此,我们需要将strLeft和strRight参数声明为无符号字符指针,如下面的代码所示:

int strcmp(const unsigned char *strLeft,
           const unsigned char *strRight)
{
    assert(strLeft!=NULL&&strRight!=NULL);
    int ret=0;
    while(!(ret= *strLeft-*strRight) && *strRight)
    {
            strLeft++,strRight++; 
    }
    if(ret<0)
    {
            ret=-1;
    }
    else if(ret>0)
    {
            ret=1;
    }
    else
    {
            ret=0;
    }
    return ret;
}

当然,我们也可以直接在函数里对其进行修改,如下面的代码所示:

int strcmp(const char *strLeft,const char *strRight)
{
    assert(strLeft!=NULL&&strRight!=NULL);
    int ret=0;
    while(!(ret= *(unsigned char*)strLeft
                -*(unsigned char*)strRight) && *strRight)
    {
            strLeft++,strRight++; 
    }
    if(ret<0)
    {
            ret=-1;
    }
    else if(ret>0)
    {
            ret=1;
    }
    else
    {
            ret=0;
    }
    return ret;
}

其实,面对上面的问题时,只需要记住一个简单的原则,就是不要在表达式中使用“简单的”字符。当然,位域也有同样的问题,因此也有一个类似的原则:任何时候都不要使用“简单的”位域。例如,下面的代码在任何编译程序上都可以工作,因为它没有对域作假定。

char* strcpy(char *strDst,const char *strSrc) 
{ 
    char *ret = strDst; 
    assert(strDst!=NULL&&strSrc!=NULL); 
    while((*strDst++=*strSrc++)&&strlen(strDst)!=0) 
            NULL; 
    return ret; 
}

最后,对于编写可移植程序还有这样一个问题:有些程序员可能会认为使用可移植的类型比使用“自然的”类型效率更低。例如,假定int类型的物理字长对目标硬件是最有效的。这就意味着这种“自然的”位数可能大于16位,所保持的值也可能大于32767。现在假定用户的编译程序使用的是32位的int,且要求使用0至40000的值域。那么,是为了使机器可以在int内有效地处理40000个值而使用int呢,还是坚持使用可移植类型,而用long代替int呢?
其实这要具体情况具体分析。
对于对效率要求比较高的程序。大家一致认为如果能够使用char定义的变量,就不要使用int定义的变量;能够使用int定义的变量,就不要用long 变量来定义;能不使用浮点型变量就不要使用浮点型变量。当然,在定义变量后不要让变量超过其作用范围,如果超过变量的范围赋值,C语言编译器并不会报错,但程序的运行结果却错了,而且这样的错误很难发现。
如果基于可移植性来考虑,要是机器使用的是32位int,那么也可以使用32位long,因为这两者产生的代码即使不相同也很相似,所以我们可以使用long。用户即便担心在将来必须支持的机器上使用long可能会效率低一些,那也应该坚持使用可移植类型。
总之,不论怎样,我们都应该坚持这样一个原则:那就是尽量使用严格形式定义的、可移植的数据类型,尽量不要使用与具体硬件或软件环境关系密切的变量。

相关文章