不可忽视的 C 语言陷阱!_warning #187-d use of "=" where "==" may have be

对于switch…case语句,从概率论上说,绝大多数程序一次只需执行一个匹配的case语句,而每一个这样的case语句后都必须跟一个break。

去复杂化大概率事件,这多少有些不合常情。

  1. 不能乱加的break

break关键字用于跳出最近的那层循环语句或者switch语句,但程序员往往不够重视这一点。

1990年1月15日,AT&T电话网络位于纽约的一台交换机宕机并且重启,引起它邻近交换机瘫痪,由此及彼,一个连着一个,很快,114型交换机每六秒宕机重启一次,六万人九小时内不能打长途电话。

当时的解决方式:工程师重装了以前的软件版本。。。事后的事故调查发现,这是break关键字误用造成的。

《C专家编程》提供了一个简化版的问题源码:

  1. network code()
  2. {
  3. switch(line)
    
  4.  {
    
  5.     case  THING1:
    
  6.  {
    
  7.         doit1();
    
  8.      } break;
    
  9.     case  THING2:
    
  10.  {
    
  11.         if(x==STUFF)
    
  12.          {
    
  13.             do_first_stuff();
    
  14.             if(y==OTHER_STUFF)
    
  15.                 break;
    
  16.             do_later_stuff();
    
  17.         }  /*代码的意图是跳转到这里… …*/  
    
  18.         initialize_modes_pointer();
    
  19.  } break;
    
  20.     default :
    
  21.         processing();
    
  22. } /*… …但事实上跳到了这里。*/  
    
  23. use_modes_pointer(); /*致使modes_pointer未初始化*/  
    
  24. }

那个程序员希望从if语句跳出,但他却忘记了break关键字实际上跳出最近的那层循环语句或者switch语句。

现在它跳出了switch语句,执行了use_modes_pointer()函数。但必要的初始化工作并未完成,为将来程序的失败埋下了伏笔。

1.4 意想不到的八进制

将一个整形常量赋值给变量,代码如下所示:

  1. int a=34, b=034;

变量a和b相等吗?

答案是不相等的。

我们知道,16进制常量以’0x’为前缀,10进制常量不需要前缀,那么8进制呢?

它与10进制和16进制表示方法都不相同,它以数字’0’为前缀,这多少有点奇葩:三种进制的表示方法完全不相同。

如果8进制也像16进制那样以数字和字母表示前缀的话,或许更有利于减少软件Bug,毕竟你使用8进制的次数可能都不会有误使用的次数多!

下面展示一个误用8进制的例子,最后一个数组元素赋值错误:

  1. a[0]=106; /十进制数106/
  2. a[1]=112; /十进制数112/
  3. a[2]=052; /实际为十进制数42,本意为十进制52/
1.5指针加减运算

**指针的加减运算是特殊的。**下面的代码运行在32位ARM架构上,执行之后,a和p的值分别是多少?

  1. int a=1;
  2. int *p=(int *)0x00001000;
  3. a=a+1;
  4. p=p+1;

对于a的值很容判断出结果为2,但是p的结果却是0x00001004。

指针p加1后,p的值增加了4,这是为什么呢?

原因是指针做加减运算时是以指针的数据类型为单位。

p+1实际上是按照公式p+1*sizeof(int)来计算的。

不理解这一点,在使用指针直接操作数据时极易犯错。

某项目使用下面代码对连续RAM初始化零操作,但运行发现有些RAM并没有被真正清零。

  1. unsigned int *pRAMaddr; //定义地址指针变量
  2. for(pRAMaddr=StartAddr;pRAMaddr<EndAddr;pRAMaddr+=4)
  3. {
  4.  *pRAMaddr=0x00000000;   //指定RAM地址清零  
    
  5. }

通过分析我们发现,由于pRAMaddr是一个无符号int型指针变量,所以pRAMaddr+=4代码其实使pRAMaddr偏移了4*sizeof(int)=16个字节,

所以每执行一次for循环,会使变量pRAMaddr偏移16个字节空间,但只有4字节空间被初始化为零。

其它的12字节数据的内容,在大多数架构处理器中都会是随机数。

1.6关键字sizeof

不知道有多少人最初认为sizeof是一个函数。

其实它是一个关键字,其作用是返回一个对象或者类型所占的内存字节数,对绝大多数编译器而言,返回值为无符号整形数据。

需要注意的是,使用sizeof获取数组长度时,不要对指针应用sizeof操作符,比如下面的例子:

  1. void ClearRAM(char array[])
  2. {
  3. int i ;
    
  4. for(i=0;i<sizeof(array)/sizeof(array[0]);i++)     //这里用法错误,array实际上是指针  
    
  5. {
    
  6.     array[i]=0x00;
    
  7. }
    
  8. }
  9. int main(void)
  10. {
  11. char Fle[20];
    
  12. ClearRAM(Fle);          //只能清除数组Fle中的前四个元素  
    
  13. }

我们知道,对于一个数组array[20],我们使用代码sizeof(array)/sizeof(array[0])可以获得数组的元素(这里为20),但数组名和指针往往是容易混淆的,

有且只有一种情况下数组名是可以当做指针的,那就是**数组名作为函数形参时,数组名被认为是指针,同时,它不能再兼任数组名。

**注意只有这种情况下,数组名才可以当做指针,但不幸的是这种情况下容易引发风险。

在ClearRAM函数内,作为形参的array[]不再是数组名了,而成了指针。

sizeof(array)相当于求指针变量占用的字节数,在32位系统下,该值为4,sizeof(array)/sizeof(array[0])的运算结果也为4。

所以在main函数中调用ClearRAM(Fle),也只能清除数组Fle中的前四个元素了。

1.7增量运算符’++’和减量运算符‘–‘

增量运算符”++”和减量运算符”–“既可以做前缀也可以做后缀。

**前缀和后缀的区别在于值的增加或减少这一动作发生的时间是不同的。

**作为前缀是先自加或自减然后做别的运算,作为后缀时,是先做运算,之后再自加或自减。

许多程序员对此认识不够,就容易埋下隐患。

下面的例子可以很好的解释前缀和后缀的区别。

  1. int a=8,b=2,y;
  2. y=a++±-b;

代码执行后,y的值是多少?

这个例子并非是挖空心思设计出来专门让你绞尽脑汁的C难题(如果你觉得自己对C细节掌握很有信心,做一些C难题检验一下是个不错的选择。

那么,《The C Puzzle Book》这本书一定不要错过),你甚至可以将这个难懂的语句作为不友好代码的例子。

但是它也可以让你更好的理解C语言。根据运算符优先级以及编译器识别字符的贪心法原则,第二句代码可以写成更明确的形式:

  1. y=(a++)+(–b);

当赋值给变量y时,a的值为8,b的值为1,所以变量y的值为9;赋值完成后,变量a自加,a的值变为9,千万不要以为y的值为10。

这条赋值语句相当于下面的两条语句:

  1. y=a+(–b);
  2. a=a+1;
1.8逻辑与’&&’和逻辑或’||’的陷阱

为了提高系统效率,逻辑与和逻辑或操作的规定如下:

**如果对第一个操作数求值后就可以推断出最终结果,第二个操作数就不会进行求值!

**比如下面代码:

  1. if((i>=0)&&(i++ <=max))
  2. {
  3.    //其它代码  
    
  4. }

在这个代码中,只有当i>=0时,i++才会被执行。这样,i是否自增是不够明确的,这可能会埋下隐患。逻辑或与之类似。

1.9结构体的填充

结构体可能产生填充,因为对大多数处理器而言,访问按字或者半字对齐的数据速度更快,

当定义结构体时,编译器为了性能优化,可能会将它们按照半字或字对齐,这样会带来填充问题。比如以下两个个结构体:

第一个结构体:

  1. struct {
  2. char  c;
    
  3. short s;
    
  4. int   x;
    
  5. }str_test1;

第二个结构体:

  1. struct {
  2. char  c;
    
  3. int   x;
    
  4. short s;
    
  5. }str_test2;

这两个结构体元素都是相同的变量,只是元素换了下位置,那么这两个结构体变量占用的内存大小相同吗?

其实这两个结构体变量占用的内存是不同的,

对于Keil MDK编译器,默认情况下第一个结构体变量占用8个字节,第二个结构体占用12个字节,差别很大。第一个结构体变量在内存中的存储格式如图2-1所示:

图2-1:结构体变量1内存分布

第二个结构体变量在内存中的存储格式如图2-2所示。

对比两个图可以看出MDK编译器是是怎么将数据对齐的,这其中的填充内容是之前内存中的数据,是随机的,所以不能在结构之间逐字节比较;

另外,合理的排布结构体内的元素位置,可以最大限度减少填充,节省RAM。

图2-2 :结构体变量2内存分布

2-不可轻视的优先级

C语言有32个关键字,却有34个运算符。要记住所有运算符的优先级是困难的。稍不注意,你的代码逻辑和实际执行就会有很大出入。

比如下面将BCD码转换为十六进制数的代码:

  1. result=(uTimeValue>>4)*10+uTimeValue&0x0F;

这里uTimeValue存放的BCD码,想要转换成16进制数据,实际运行发现,如果uTimeValue的值为0x23,按照我设定的逻辑,result的值应该是0x17,但运算结果却是0x07。

经过种种排查后,才发现’+’的优先级是大于’&’的,相当于(uTimeValue>>4)*10+uTimeValue与0x0F位与,结果自然与逻辑不符。

符合逻辑的代码应该是:

  1. result=(uTimeValue>>4)*10+(uTimeValue&0x0F);

不合理的#define会加重优先级问题,让问题变得更加隐蔽。

  1. #define READSDA IO0PIN&(1<<11) //读IO口p0.11的端口状态
  2. if(READSDA==(1<<11)) //判断端口p0.11是否为高电平
  3. {
  4. //其它代码  
    
  5. }

编译器在编译后将宏带入,原代码语句变为:

  1. if(IO0PIN&(1<<11) ==(1<<11))
  2. {
  3. //其它代码   
    
  4. }

运算符’==‘的优先级是大于’&'的,代码IO0PIN&(1<<11) ==(1<<11))等效为IO0PIN&0x00000001:判断端口P0.0是否为高电平,这与原意相差甚远。

因此,使用宏定义的时候,最好将被定义的内容用括号括起来。

按照常规方式使用时,可能引起误会的运算符还有很多,如表2-1所示。C语言的运算符当然不会只止步于数目繁多!

有一个简便方法可以避免优先级问题:不清楚的优先级就加上”()”,但这样至少有会带来两个问题:

  • 过多的括号影响代码的可读性,包括自己和以后的维护人员
  • 别人的代码不一定用括号来解决优先级问题,但你总要读别人的代码

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Go语言工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Go语言全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
img

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

图片转存中…(img-OVjndMTO-1713007816828)]
[外链图片转存中…(img-gMc4YsWq-1713007816828)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-FyCJ6a2O-1713007816829)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值