《C缺陷和陷阱》-笔记(4)

目录

一、边界计算与不对称边界

1.栏杆错误

2.程序简化

3.编写程序

4.移动字符

5.打印元素

二、求值顺序

一、边界计算与不对称边界

在C语言中,这个数组的下标范围是从0到9。一个拥有10个元素的数组中,它的元素的下标范围是从0到n-1。

例如,让我们仔细地来看看本书导读中的一段代码:
int i,a=[10];
for(i=1;i<=10;i++)
a[i]=0;

这段代码本意是要设置数组a中所有元素为0,

在for语句的比较部分本来是i<10,却写成了i<=10,循环体内将并不存在的a[10]设置为0,这就陷入了一个死循环。

1.栏杆错误

在所有常见的程序设计错误中,最难于察觉的一类是“栏杆错误”,也常被称为“差一错误”

100英尺长的围栏每隔10英尺需要一根支撑用的栏杆,一共需要多少根栏杆呢?

(1)最“显而易见”的答案是将100除以10,得到的结果是10.当然这个答案是错误的,正确答案是11。
(2)要支撑10英尺长的围栏实际需要2根栏杆,两端各一根。

(3_除了最右侧的一段围栏,其他每一段10英尺长的围栏都只在左侧有一根栏杆;而例外的最右侧一段围栏不仅左侧有一根栏杆,右侧也有一根栏杆。

实际上提示了我们避免“栏杆错误”的两个通用原则:

(1)首先考虑最简单情况下的特例,然后将得到的结果外推,这是原则一。
(2)仔细计算边界,绝不掉以轻心,这是原则二。

例如,假定整数x满足边界条件x>=16且x<=37,那么此范围内x的可能取值个数有多少?

根据原则一,我们考虑最简单情况下的特例。这里假定整数x的取值范围上界与下界重合,即x>=16且x<=16,显然合理的x取值只有1个整数,即16。

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

造成“栏杆错误”的根源正是“h-1+1”中的“+1”。一个字符串中由下标为16到下标为37的字符元素所组成的子串,

用第一个入界点和第一个出界点来表示一个数值范围。具体而言,前面的例子我们不应说整数x满足边界条件x>=16且x<=37,而是说整数x满足边界条件x>=16且x<38。

注意:

下界是“入界点”,即包括在取值范围之中;

上界是“出界点”,即不包括在取值范围之中。

2.程序简化

对于程序设计的简化效果却足以令人吃惊:
1.取值范围的大小就是上界与下界之差。38-16的值是22,是不对称边界16和38之间所包括的元素数目。
2.如果取值范围为空,那么上界等于下界。这是第1条的直接推论。
3.即使取值范围为空,上界也永远不可能小于下界。
对于像C这样的数组下标从0开始的语言,不对称边界给程序设计带来的便利尤其明显:

这种数组的上界(即第一个“出界点”)恰是数组元素的个数!因此,如果我们要在C语言中定义一个拥有10个元素的数组,那么0就是数组下标的第一个“入界点”(指处于数组下标范围以内的点,包括边界点),而10就是数组下标中的第一个“出界点”(指不在数组下标范围以内的点,不含边界点)。正因为此,我们这样写:
int a[10], i;
for(i=0;i<10;i++)
     a[i]=0;

而不是写成下面这样:
int a[10],i:
      for(i = 0;i <= 9;i++)

a [i] = 0;

那么下面这个语句的含义究竟是什么?
for (i= 0 to 10)
a[i] = 0;

如果10是包括在取值范围内的“入界点”,那么i将取11个值,而不是10个值。如果10是不包括在取值范围内的“出界点”

另一种考虑不对称边界的方式是,把上界视作某序列中第一个被占用的元素,而把下界视作序列中第一个被释放的元素。

当处理各种不同类型的缓冲区时,这种看待问题的方式就特别有用。这种看待问题的方式就特别有用。将长度无规律的输入数据送到缓冲区(即一块能够容纳N个字符的内存)中去,每当这块内存被“填满”时,就将缓冲区的内容写出。缓冲区的声明可能是下面这个样子:
# define N 1024 
static char bufferN 

我们再设置一个指针变量,让它指向缓冲区的当前位置:
static char * bufptr;

按照“不对称边界”的惯例,我们可以这样编写语句:
* bufptr++ = c;
这个语句把输入字符c放到缓冲区中,然后指针bufptr 递增1,又指向缓冲区中第1个未占用的字符。

当指针bufptr 与&buffer [0]相等时,缓冲区存放的内容为空,因此初始化时声明缓冲区为空可以这样写:
bufptr= & buffer[0];
或者,更简洁一点,直接写成:
bufptr = buffer;

任何时候缓冲区中已存放的字符数都是bufptr -buffer ,因此我们可以通过将这个表达式与N作比较,来判断缓冲区是否已满。当缓冲区全部“填满”时,表达式bufptr -buffer 就等于N,可以推断缓冲区中未占用的字符数为N-(bufptr -buffer )。

3.编写程序

我们就可以开始编写程序了,假设这个函数的名称是bufwrite 。函数bufwrite 有两个参数,第一个参数是一个指针,指向将要写入缓冲区的第1个字符;第二个参数是一个整数,代表将要写入缓冲区的字符数。假定我们可以调用函数flushbuffer 来把缓冲区中的内容写出,而且函数flushbuffer 会重置指针bufpur ,使其指向缓冲区的起始位置。如下所示:
 void
 bufwrite( char * p, int n)
  {
          while( --n> 0) {
                   if (bufptr = & buffer[N])
                          flushbuffer();
                 * bu fptr+ + = *p++;
         }
}

重复执行表达式--n>=0只是进行n次迭代的一种方法。

我们注意到前面出现了bufptr 与&buffer [N]的比较,而buffer [N]这个元素是不存在的,数组buffer 的元素下标从0到N-1,根本不可能是N。我们用这种写法:
if (bufptr = & buffer[N)
代替了下面等效的写法:
if (bufptr & buffer[ N 1]>
原因在于我们要坚持遵循“不对称边界”的原则:比较指针bufptr 与缓冲区后第一个字符的地址,而&buffer [N]正是这个地址。

我们并不需要引用这个元素,而只需要引用这个元素的地址,

ANSIC标准明确允许这种用法:数组中实际不存在的“溢界”元素的地址位于数组所占内存之后,这个地址可以用于进行赋值和比较。

4.移动字符

假定我们有一种方法能够一次移动k个字符。即使你的C语言实现没有提供这个函数,自己写一个也很容易:
void 
memcpy( char * dest ,const *char source, int k 
{

          while( --k >= 0)
         * dest++=*source++;
}

我们现在可以让函数bufwrite 利用库函数memcpy 来一次转移一批字符到缓冲区,而不是一次仅转移一个字符。

循环中的每次迭代在必要时会刷新缓存,计算需要移动的字符数,移动这些字符,最后恰当地更新计数器。如下所示:
void 
bufwrite( char * p, int n)
{
       while( n > 0) {
                 int k, rem;
                 if (bufptr == & buffer [ N] )
                                      flushbuffer();
                 rem = N - ( bufptr - buffer);
                  k = n > rem? rem: n ;
                 memcpy (bufptr ,p,k);
                  bufptr += k;
                 p += k;
                 n -= k;
}
}

在循环的入口处,n是需要转移到缓冲区的字符数。因此,只要n还大于0,

上面的代码中,最后四行语句管理着字符转移的过程:

(1)从缓冲区中第1个未占用字符开始,复制k个字符到其中;

(2)将指针bufptr 指向的地址前移k个字符,使其仍然指向缓冲区中第1个未占用字符

(3)输入字符串的指针p前移k个字符;

(4)将n(即待转移的字符数)减去k

5.打印元素

打印当前输入数值(即当前行的最后一个元素),打印换行符以结束当前行,如果是一页的最后一行还要另起新的一页:
printnum(n);                     /打印当前行的最后一个元素*/
printr1();                          /另起新的一行*/
if = (++ row NROWS) {
            printpage ();
            row = 0 ;                    /*重置当前行号*/
             bufptr =buffer ;          /*重置指针bufptr */
}


因此,最后的print 函数看上去就像这样:
void
print( int n )
{
              if (bufptr = & buffer [ BUFSIZE] ) {
                   static int row = 0;
                    int * p;
                  for (p = buffer+ row; p < bufptr);
                        p += NRONS)
                       printnum(*p);
                       printnum(n);             /*打印当前行的最后一个元素*/
                        printnl();                   /*另起新的一行*/

                          if (++ row = NROWS) {
                          printpage();
                           row = 0               /*重置当前行序号*/
                            bufptr =buffer ;/*重置指针bufptr */
          }
        else 
                * butptr++ = n;
}

只需要编写函数flush ,它的作用是打印缓冲区中所有剩余元素,只需要将其作为内循环,在其上另外套一个外循环(作用是遍历一页中的每一行)
void
flush()
{
int row;

for (row = 0; row NROWS; row++)      {
int * p:
for (p = buffer row; p = bufptr;
                         p += NRONS)
printnum(*p);
printnl ();
}
printpage();
}

事实上,即使最后一页为空,函数flush 仍然还会全部打印出来,只不过一页全是空白而已。为了代码更加简洁,可以改为:

void
flush()
(
int row 
int k = bufptr -buffer ;            /*计算缓冲区中剩余项的数目*/
if (K >NROWS)
           K = NROWS;
if(k>0){
          for (row = 0; row < k; row++) {
int * p;
for (p = buffer + row; p< bufptr:
                        p += NROWS )
printnum(*p);
printnl();
}
printpage();
}
)

二、求值顺序

运算符优先级是关于诸如表达式
a + b * c 


应该被解释成
a + ( b * c) 


而不是
(a+b) * c

可以保证像下面的语句

if (count !=  0 && sum/ count smallaverage)
printf("average < %g\ n" , smallaverage);


即使当变量count 为0时,也不会产生一个“用0作除数”的错误。

例如,考虑下面的表达式:
a < b && c < d 


C语言的定义中说明a<b应当首先被求值。如果a确实小于b,此时必须进一步对c<d求值,以确定整个表达式的值。但是,如果a大于或等于b,则无需对c<d求值,表达式肯定为假。
另外,要对a<b求值,编译器可能先对a求值,也可能先对b求值,在某些机器上甚至有可能对它们同时并行求值。

C语言中只有四个运算符(&&、、?:和,)存在规定的求值顺序。运算符&&和运算符首先对左侧操作数求值,只在需要时才对右侧操作数求值。运算符?有三个操作数:在a?b:c中,操作数a首先被求值,根据a的值再求操作数b或c的值。而逗号运算符,首先对左侧操作数求值,然后该值被“丢弃”,再对右侧操作数求值。
C语言中其他所有运算符对其操作数求值的顺序是未定义的。特别地,赋值运算符并不保证任何求值顺序。

运算符&&和运算符对于保证检查操作按照正确的顺序执行至关重要。例如,在语句
if (y  != 0 && x/ y > tolerance)
            complain ();


中,就必须保证仅当y非0时才对xy求值。

i = 0;
while (i < n)
          y[i] = x[i++];

上面的代码假设y[i]的地址将在i的自增操作执行之前被求值,这一点并没有任何保证!

下面这种版本的写法与前类似,也不正确:
i=0;
while( i < n)
         y[i++]=x[i]:


另一方面,下面这种写法却能正确工作:
i = 0;
while (i < n) {
           y[i]=x[i];
           i++;
}


当然,这种写法可以简写为:
for (i = 0; i < n; i++)
      y[i]=x[i];

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/529390.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Buran勒索病毒通过Microsoft Excel Web查询文件进行传播

Buran勒索病毒首次出现在2019年5月&#xff0c;是一款新型的基于RaaS模式进行传播的新型勒索病毒&#xff0c;在一个著名的俄罗斯论坛中进行销售&#xff0c;与其他基于RaaS勒索病毒(如GandCrab)获得30%-40%的收入不同&#xff0c;Buran勒索病毒的作者仅占感染产生的25%的收入,…

网红老阳分享的蓝海赚钱项目,这三个真香!

在互联网经济飞速发展的当下&#xff0c;寻找蓝海项目成为了许多创业者和投资者的首要任务。近期&#xff0c;知名网红老阳分享了一些他认为具有巨大潜力的蓝海项目&#xff0c;其中包括RPO人力资源、视频号带货和Temu跨境电商。下面我们将对这三个项目进行详细解析。 老阳分享…

Python从0到100(三):Python中的变量介绍

前言&#xff1a; 零基础学Python&#xff1a;Python从0到100最新最全教程。 想做这件事情很久了&#xff0c;这次我更新了自己所写过的所有博客&#xff0c;汇集成了Python从0到100&#xff0c;共一百节课&#xff0c;帮助大家一个月时间里从零基础到学习Python基础语法、Pyth…

MUMU模拟器12连logcat的方法

大家好&#xff0c;我是阿赵。   在开发手机游戏的时候&#xff0c;在真机上会出现各种问题&#xff0c;在查询问题的时候&#xff0c;安卓手机需要用adb连接来连接手机看logcat输出分析问题。但由于连接手机比较麻烦&#xff0c;所以我都习惯在电脑用安卓模拟器来测试。   …

代码随想录day17(3)二叉树:二叉树的中序遍历(leetcode94)

题目要求&#xff1a;实现二叉树的中序遍历。 思路&#xff1a;对于二叉树的中序遍历&#xff0c;通常可以使用递归算法与非递归&#xff08;迭代&#xff09;算法两种。 对于递归算法的处理与前序、后序基本相同&#xff0c;只是本次应先访问其左节点&#xff0c;然后进行pu…

Linux报错排查-CentOS/BigCloud_Enterprise_Linux系统yum安装kvm报错

Linux运维工具-ywtool 目录 一.系统环境二.问题描述三.问题解决四.其他命令 一.系统环境 系统版本:BigCloud_Enterprise_Linux 7.1 二.问题描述 通过yum安装kvm报错提示: /usr/bin/yum install -y qemu-kvm qemu-img libvirt libvirt-python virt-manager libvirt-client …

视频占用内存太大了怎么办 如何快速又无损的压缩视频 快来学习吧

视频文件太大是很多人在使用电脑或移动设备时经常遇到的问题。如果视频文件过大&#xff0c;不仅会占用过多的存储空间&#xff0c;还会让播放和传输变得困难。为了解决这个问题&#xff0c;我们需要学会如何缩小视频文件大小。那么如何缩小储存视频的大小呢&#xff1f;下面给…

【计算机网络_应用层】https协议——加密和窃密的攻防

文章目录 1.https协议的介绍2. 加密和解密2.1 什么是加密2.2 常见的加密方式2.2.1 对称加密2.2.2 非对称加密 2.3 数据摘要&#xff08;数据指纹&#xff09;2.4 数字签名 3. https协议的加密和解密方案一&#xff1a;使用对称加密&#xff08;❌&#xff09;方案二&#xff1a…

记一次 .NET某设备监控自动化系统 CPU爆高分析

一&#xff1a;背景 1. 讲故事 先说一下题外话&#xff0c;一个监控别人系统运行状态的程序&#xff0c;结果自己出问题了&#xff0c;有时候想一想还是挺讽刺的&#xff0c;哈哈&#xff0c;开个玩笑&#xff0c;我们回到正题&#xff0c;前些天有位朋友找到我&#xff0c;说…

PaddlePaddle框架安装

提示&#xff1a;可在python环境中进行安装&#xff0c;避免环境污染&#xff0c;创建命令conda create -n xxx_name python3.9,激活conda activate xxx_name 第一步&#xff1a;查看计算机平台版本 在窗口输入查看命令&#xff0c;查看CUDA的版本 nvidia-smi 二、根据以下条件…

QML子组件圆角

效果 参考&#xff1a;解决QML中clip对圆角无效的问题

Linux--基础命令

一.pwd&#xff08;Print Working Directory&#xff09; (1)pwd:显示当前位置的绝对路径; 二.cd (Change Directory) (2)cd:切换目录,cd的参数表示要切换的位置,可以使用绝对路径或者相对路径; 三.ls (3)ls:显示目录中的文件 (l a i) ls补充: 理解使用: -A 显现除 “.”和“…