C陷阱与缺陷——第7章可移植缺陷

C程序能够方便地在不同编程环境中移植,但是C语言实现可能有细微差别,会导致可移植问题。可移植性主题内容很多,可以参考《How to Write Portable Software in C》

1. 应对C语言标准变更

早期兼容的函数定义

double
square(x)
    double x;
{
    return x*x;
}

2. 标志符名称的限制

早期限制:C实现必须能够区别出前6个字符不同的外部名称

3. 整数的大小

关于short、int和long型C语言的限制如下:

  • short型整数容纳的值肯定能够被int整数容纳,int型整数容纳的值也肯定能够被long型整数容纳;
  • 一个普通整数(int类型)足够大以容纳任何数组下标;
  • 字符长度由硬件特性决定

为了可移植性,除了声明变量为long型,最好的方式是定义一个新的类型,如下:

typedef long tenmil;

4. 字符是有符号整数还是无符号整数

如果c是一个字符变量,使用(unsigned) c实际上会将字符c转换为无符号整数时,c将首先被转换为int型整数,而此时可能得到非预期的结果。

正确的方式是使用语句(unsigned char) c这才会直接进行转换

5. 移位运算符

  • 向右移位时,空出的位是由0填充,还是由符号位的副本填充

如果被移位的对象是无符号数,那么空出的位将被0填充。如果被移位的对象是由符号数,那么C语言实现既可以用0填充空出的位,也可以用符号位的副本填充空出的位。

  • 移位计数(即移位操作的位数)允许的取值范围是什么?

如果被移位的对象长度是n位,那么移位计数必须大于等于0,而严格小于n

用移位运算符代替除法运算,将可能提升程序运行速度

6. 内存位置0

NULL指针并不指向任何对象,因此,除非是用于赋值或比较运算,其他的目的使用NULL都是非法的。

但具体的处理可能有所不同,要检查出这类问题的最简单办法就是,把程序移到不允许读取内存位置0的机器人运行。

7. 除法运算时发生的截断

C语言标准保证的除法定义:

  • q*b + r == a
  • 当a>0且b>0时,保证|r|<|b|以及r>=0

8. 随机数的大小

比如rand函数的实现,有些返回范围是0-2^15-1,有些返回范围是0-2^31-1

9. 大小写转换

比如大小写转换的宏实现:

#define toupper(c) ((c)+'A'-'a')
#define tolower(c) ((c)+'a'-'A')

当输入不是大小写字母时,它们返回的将是无用信息

修改版本:

#define toupper(c) ((c) >= 'a' && (c) <= 'z' ? (c) + 'A' - 'a' : (c))
#define tolower(c) ((c) >= 'A' && (c) <= 'Z' ? (c) + 'a' - 'A' : (c))

上述实现的缺点是可能导致c被求值1到3次

函数实现

int
toupper (int c)
{
    if (c >= 'a' && c <= 'z')
        return c + 'A' - 'a';
    return c;
}

缺点是函数调用有额外开销

10. 首先释放,然后重新分配

C语言实现realloc时,调用realloc函数时,需要把指向一块已分配内存的区域指针以及这块内存新的大小作为参数传入,然后可调整这块内存区域为新的大小,这个过程有可能涉及内存的拷贝。

但是unix实现过程不太一样:Realloc函数把指针ptr所指向内存块的大小调整为size字节,返回一个指向调整后内存块的指针,假定这块内存原来大小为oldsize,新的大小为newsize,这两个数之间较小者为min(oldsize, newsize),那么内存块中min(oldsize, newsize)部分存储的内容将保持不变,如果prt所指向的是一块最近一次调用malloc,realloc或calloc分配的内存,即使这块内存已被释放,realloc仍然可以正常工作。

11. 可移植性问题的一个例子

以下程序的作用是把给出的long型整数转换为其10进制表示,并且对10进制表示中的每个字符都调用函数指针所指向的函数:

void
printnum(long n, void(*p)())
{
    if (n < 0)
    {
        (*p)('-');
        n = -n;
    }
    if (n >= 10)
        printnum(n/10, p);
    (*p)((int)(n % 10) + '0');
}

这个实现对于ASCII字符集和ANSI的C实现是正确的,但是对某些机器可能出错,要避免这个问题,解决办法是使用一张代表数字的字符表,修改如下:

void
printnum(long n, void(*p)())
{
    if (n < 0)
    {
        (*p)('-');
        n = -n;
    }
    if (n >= 10)
        printnum(n/10, p);
    (*p)("0123456789"[n % 10]);
}

还有一个问题是负数可能溢出,因为C语言基于2点补码实现允许的负数取值范围要大于正数的取值范围,修改如下,用函数printneg专门处理负数,printnum则只负责判断正负

void
printneg (long n, void(*p)())
{
    if (n <= -10)
        printneg(n/10, p);
    (*p)("0123456789"[-(n % 10)]);
}

void
printnum (long n, void(*p)())
{
    if (n < 0)
    {
        (*p)('-');
        printneg(n, p);
    }
    else
    {
        printneg(-n, p);
    }
}

以上实现的问题是当n为负数时,n%10完全有可能是一个正数,此时-(n % 10)就是一个负数,索引范围不在数字数组里面。最终修改版本为:

void 
printneg (long n, void (*p)())
{
    long q;
    int r;
    
    q = n / 10;
    r = n % 10;
    if (r > 0)
    {
        r -= 10;
        q++;
    }
    if (n <= -10)
        printneg(q, p);
    (*p)("0123456789"[-r]);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值