C陷阱与缺陷-疑难问题理解06

3.4 避免”举隅 [yú] 法”

“举隅法”(synecdoche)是一种文学修辞上的手段,有点类似于以微笑表示喜悦、赞许之情,或以隐喻表示指代物与被指物的相互关系。在《牛津英语辞典》 中,对“举隅法”(synecdoche)是这样解释的:“以含义更宽泛的词语来代替含 义相对较窄的词语,或者相反;例如,以整体代表部分,或者以部分代表整体, 以生物的类来代表生物的种,或者以生物的种来代表生物的类,等等。”

《牛津英语辞典》中这一词条的说明,倒是恰如其份地描述了 C语言中一个 常见的“陷阱”:混淆指针与指针所指向的数据。对于字符串的情形,编程者更是经常犯这种错误。例如:

char *p,*q;
p = "xyz";

尽管某些时候我们可以不妨认为,上面的赋值语句使得p的值就是字符串 “xyz”,然而实际情况并不是这样,记住这一点尤其重要。实际上,p的值是一个 指向由’x’、’y’、’z’和’ \0’,4个字符组成的数组的起始元素的指针。因此,如果我们执行下面的语句:

q = p;

p和q现在是两个指向内存中同一地址的指针。这个赋值语句并没有同时复制内 存中的字符。

我们需要记住的是,复制指针并不同时复制指针所指向的数据。

因此,当我们执行完下面的语句之后:

q[1] =’Y’;

q所指向的内存现在存储的是字符串’xYz’。因为p和q所指向的是同一块内存, 所以p指向的内存中存储的当然也是字符串’xYz’。

3.5 空指针并非空字符串

​ 除了一个重要的例外情况,在C语言中将一个整数转换为一个指针,最后得到的结果都取决于具体的C编译器实现。这个特殊情况就是常数0,编译器保证由0转换而来的指针不等于任何有效的指针。出于代码文档化的考虑,常数0这 个值经常用一个符号来代替:

#define NULL 0

当然无论是直接用常数0,还是用符号NULL,效果都是相同的。需要记住的重要一点是,当常数0被转换为指针使用时,这个指针绝对不能被解除引用 (dereference)。换句话说,当我们将0赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容。下面的写法是完全合法的:

if (p == (char *) 0)...

但是如果要写成这样:

if (strcmp(char *) 0) == 0)...

就是非法的了,原因在于库函数strcmp的实现中会包括査看它的指针参数所指向 内存中的内容的操作。

如果p是一个空指针,即使

printf(p);

printf("%s", p);

的行为也是未定义的。而且,与此类似的语句在不同的计算机上会有不同的效果。

3.6 边界计算与不对称边界

​ 如果一个数组有10个元素,那么这个数组下标的允许取值范围是什么昵?

在标准的 Basic语言中,声明一个拥有10个元素的数组,实际上编译器分配了 11个元素的空间,下标范围从0到10。

​ 在C语言中,这个数组的下标范围是从0到9。一个拥有10个元素的数组中, 存在下标为0的元素,却不存在下标为10的元素。C语言中一个拥有n个元素的数组,却不存在下标为n的元素,它的元素的下标范围是从0到n-1为此,由其他程序语言转而使用C语言的程序员在使用数组时特别要注意。

例如,让我们仔细地来看看本书导读中的一段代码:

int i, a[10];
for (i=1; i<=10;i++)
a[i] = 0; 

​ 这段代码本意是要设置数组a中所有元素为0,却产生了一个出人意料的“副效果”。在for语句的比较部分本来是i <10,却写成了 i <=10,因此实际上并不存在的a[10]被设置为0,也就是内存中在数组a之后的一个字(word)的内存被设置为0。如果用来编译这段程序的编译器按照内存地址递减的方式来给变量分配内存,那么内存中数组a之后的一个字(word)实际上是分配给了整型变量i。此时,本来循环计数器i的值为10,循环体内将并不存在的a[10]设置为0,实际上却是将计数器i的值设置为0.这就陷入了一个死循环。

​ 尽管C语言的数组会让新手感到麻烦,然而C语言中数组的这种特别的设计 正是其最大优势所在。要理解这一点,需要作一些解释。

​ 在所有常见的程序设计错误中,最难于察觉的一类是“栏杆错误”,也常被称为“差一错误”(off-by-one error)。

前面一段讨论了解决这个问题的两种方法,实际上提示了我们避免“栏杆错误”的两个通用原则;

(1) 首先考虑最简单情况下的特例,然后将得到的结果外推,这是原则一。

(2) 仔细计算边界,绝不掉以轻心,这是原则二。

​ 将上面总结的内容牢记在心以后,我们现在来看整数范围的计算。例如,假定整数x满足边界条件x>=16且x<=37,那么此范围内x的可能取值个数有多少? 换句话说,整数序列16, 17, …,37 一共有多少个元素?很显然,答案与37-16 (亦即21)非常接近,那么到底是20, 21还是22呢?

​ 根据原则一,我们考虑最简单情况下的特例。这里假定整数x的取值范围上 界与下界重合,即x>=16且x<=16,显然合理的x取值只有1个整数,即16。所以当上界与下界重合时,此范围内满足条件的整数序列只有1个元素。

​ 再考虑一般的情形,假定下界为1,上界为h。如果满足条件“上界与下界重合”,即l1= h,亦即h-1= 0。根据特例外推的原则,我们可以得出满足条件的整数序列有h-1+ 1个元素。在本例中,就是37-16+1,即22。

造成“栏杆错误”的根源正是“h-1+1”中的“+1”。一个字符串中由下标为16到下标为37的字符元素所组成的子串,它的长度是多少呢?稍不留意,就会得到错误的结果21。很自然地,人们会问这样一个问题:是否存在一些编程技巧,能够降低这类错误发生的可能性昵?

这个编程技巧不但存在,而且可以一言以蔽之:用第一个入界点和第一个出界点来表示一个数值范围。具体而言,前面的例子我们不应说整数x满足边界条件x>=16且x<=37,而是说整数x满足边界条件x>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

奈斯编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值