《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];

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2.4 为赌博而生的期望值理论

期望值理论 人们在风险决策时&#xff0c;会把数学期望值最大的可能选项作为自己的最终选择。 期望值&#xff1a;指无数次相同的风险决策的最终平均值或加权平均数它往往以货币或财产的数量为表现形式。又称期望货币值。 期望值的计算 圣彼得堡论&#xff1a;为什么人们不愿…

FPGA高端项目:FPGA基于GS2971的SDI视频接收+HLS多路视频融合叠加,提供1套工程源码和技术支持

目录 1、前言免责声明 2、相关方案推荐本博已有的 SDI 编解码方案本方案的SDI接收转HDMI输出应用本方案的SDI接收图像缩放应用本方案的SDI接收纯verilog图像缩放纯verilog多路视频拼接应用本方案的SDI接收HLS图像缩放Video Mixer多路视频拼接应用本方案的SDI接收OSD动态字符叠加…

《AI歌手:音乐产业的未来之音?》

引言 随着人工智能技术的快速发展,AI歌手作为一种新兴的演艺模式逐渐走进了人们的视野。AI歌手以其独特的魅力和无限的潜力引发了人们对于音乐产业未来的思考。本文将围绕AI歌手的音乐呈现、市场认可、替代性以及其他类似AI应用等方面展开讨论,探究AI歌手是否有望成为音乐产…

R语言读取大型NetCDF文件

失踪人口回归&#xff0c;本篇来介绍下R语言读取大型NetCDF文件的一些实践。 1 NetCDF数据简介 先给一段Wiki上关于NetCDF的定义。 NetCDF (Network Common Data Form) is a set of software libraries and self-describing, machine-independent data formats that support…

【强化学习的数学原理-赵世钰】课程笔记(七)时序差分方法

目录 一.内容概述 二.激励性实例&#xff08;Motivating examples&#xff09;-随机问题&#xff08;stochastic problems&#xff09; 三.估计 state value 的 TD 算法&#xff08;TD learning of state values&#xff09; 四.估计 action value 的 TD 算法&#xff08;TD…

【c语言 】 函数入门

&#x1f388;个人主页&#xff1a;豌豆射手^ &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;C语言 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、交流进步&…

Spring循环依赖的成因与破局

一、Spring注入类型 Spring 核心功能之一依赖注入&#xff0c;依赖注入是使用 Spring 框架的基本手段&#xff0c;通过他获取各种类型的 bean&#xff0c;但使用不同的依赖注入类型时经常会遇到循环依赖的问题。Spring 依赖注入类型&#xff1a; 字段注入&#xff0c;这是最常…

酷开科技发力研发酷开系统,让家庭娱乐生活更加丰富多彩

在这个快节奏的社会&#xff0c;家庭娱乐已成为我们日常生活中不可或缺的一部分&#xff0c;为了给家庭带来更多欢笑与感动&#xff0c;酷开科技发力研发出拥有丰富内容和技术的智能电视操作系统——酷开系统&#xff0c;它集合了电影、电视剧、综艺、游戏、音乐等海量内容&…

01-分析同步通讯/异步通讯的特点及其应用

同步通讯/异步通讯 微服务间通讯有同步和异步两种方式 同步通讯: 类似打电话场景需要实时响应(时效性强可以立即得到结果方便使用),而且通话期间不能响应其他的电话(不支持多线操作)异步通讯: 类似发邮件场景不需要马上回复并且可以多线操作(适合高并发场景)但是时效性弱响应…

Unmanaged PowerShell

简介 在渗透测试当中经常会使用到PowerShell来执行脚本, 但是直接使用PowerShell.exe是一个非常敏感的行为, EDR等产品对PowerShell.exe进程的创建监控的很密切, 并且随着PowerShell的渗透测试工具的普及, 越来越多的EDR会利用微软提供的AMSI接口对PS脚本进行扫描, 但是对于低…

[短文]不同空白字符导致程序执行失败问题

屏幕显示的一个空白字符&#xff0c;对于编程者来说&#xff0c;并无差异&#xff0c;但底层截然不同的表示方法&#xff0c;极大可能导致程序执行失败&#xff01; 今天博主就遇到一个空格字符的问题&#xff0c;大概情况是前端编写SQL传入&#xff0c;后端有时可以执行&…

Uninty 鼠标点击(摄像机发出射线-检测位置)

平面来触发碰撞&#xff0c;胶囊用红色材质方便观察。 脚本挂载到胶囊上方便操作。 目前实现的功能&#xff0c;鼠标左键点击&#xff0c;胶囊就移动到那个位置上。 using System.Collections; using System.Collections.Generic; using UnityEngine;public class c6 : MonoBe…

DNS服务

简介 DNS&#xff1a;domain name service 域名服务 作用&#xff1a;为客户机提供域名解析 dns 监听端口&#xff1a;53端口 分为 udp&#xff08;只负责向外发&#xff09; 和 tcp&#xff08;确保正常发送至对端&#xff0c;对端发送数据包表示已经收到&#xff09; 搭建…

解密QQ盗号诈骗APP:逆向溯源,探寻幕后黑色操作

逆向溯源qq盗号诈骗app 起因 专注于web漏洞挖掘、内网渗透、免杀和代码审计&#xff0c;感谢各位师傅的关注&#xff01;网安之路漫长&#xff0c;与君共勉&#xff01; 分析该app是源于朋友被盗号了&#xff0c;对方发了个app想盗号&#xff0c;这怎么能惯着他&#xff1f;这不…

day16_购物车(添加购物车,购物车列表查询,删除购物车商品,更新选中商品状态,完成购物车商品的全选,清空购物车)

文章目录 购物车模块1 需求说明2 环境搭建3 添加购物车3.1 需求说明3.2 远程调用接口开发3.2.1 ProductController3.2.2 ProductService 3.3 openFeign接口定义3.3.1 环境搭建3.3.2 接口定义3.3.3 降级类定义 3.4 业务后端接口开发3.4.1 添加依赖3.4.2 修改启动类3.4.3 CartInf…

MyBatisPlus理解

MyBatisPlus是mybatis的增强&#xff0c;mybatis是数据库持久化的框架&#xff0c;但mybatisplus并不是替代mybatis&#xff0c;而是相辅相成的关系 MyBatisPlus不会对以前使用mybatis开发的项目进行影响&#xff0c;引入后仍然正常运行。 使用方法&#xff1a; 1.在引入了对…

[力扣 Hot100]Day48 路径总和 III

题目描述 给定一个二叉树的根节点 root &#xff0c;和一个整数 targetSum &#xff0c;求该二叉树里节点值之和等于 targetSum 的 路径 的数目。 路径 不需要从根节点开始&#xff0c;也不需要在叶子节点结束&#xff0c;但是路径方向必须是向下的&#xff08;只能从父节点到…

再谈 Cookie 和 Session

文章目录 1. 核心方法&#xff1a;HttpServletRequest 类中HttpServletResponse 类中HttpSession类中Cookie类中 2.实现登录界面 Cookie 是浏览器在本地持久化存储数据的一种机制。 Cookie 的数据从哪里来&#xff1f; 是服务器返回给浏览器的。 Cookie 的数据长什么啥样&#…

Chapter20-Ideal gases-CIE课本要点摘录、总结

20.1 Particles of a gas Brownian motion Fast modules 速率的数值大概了解下&#xff1a; average speed of the molecules:400m/s speed of sound:approximately 330m/s at STP&#xff08;standard temperature and pressure&#xff09; Standard Temperature and Pres…

不同路径 不同路径 II 整数拆分

62.不同路径 力扣题目链接(opens new window) 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish” &#xff09;。…