C陷阱与缺陷第二章

简介:

@[TOC]
前言:

博主实力有限,博文有什么错误请你斧正,非常感谢!

这是《c陷阱与缺陷》专栏第二章。

词法”陷阱“

语法”陷阱“

要理解一个 C程序,仅仅理解组成该程序的”符号“是不够的。还需要我们理解这些“符号”是如何组成声明,表达式,语句与程序的。本文主要讨论一些与人们直觉相悖,容易引起混淆的地方。

理解函数声明

1.变量的声明:

定义

任何c的变量声明都由两部分组成:“类型”以及一组类型表达式的“声明符”以及最后的”分号“,且声明符可以像表达式一样,任意的使用括号

变量声明的意义:

  • 建立变量符号表
通过声明变量,编译器可以建立变量符号表,对于程序用什么变量,编译器都非常清楚。从而帮助程序员远离疏忽而将变量名写错的情况
  • 为变量分配内存
声明变量的同时,编译器为其分配内存,

exp:

char 1字节,int 4字节,float 4字节,

double 8字节

  • 变量的类型声明,说明了编译器看内存的角度
exp:

二进制补码:11111111

对于声明为char 型,在内存中存的是十进制的==-1==;

对于声明为 unsigned char 型,在内存中存的是十进制的255

  • 声明类型,确定变量的取值范围
char :-128~127

unsigned char: 0~255

....

  • 不同数据类型有不同算法操作

除法/:

对于2边为整数,实行整形除法。

对于一边为浮点型,实现浮点除法。

image-20210829145941533

取余%

只能用于整形变量运算。浮点型不行。

2.类型转换

定义:

将声明的变量名去掉,去掉”分号“,加上"括号"

exp1:

float a =0;

声明float变量a;

类型转换:

(float)

exp2:

void (*p)(int );

声明函数指针。

类型转换:

(void (*)(int))

3.分析表达式(相对绕):

((void ( )())0)()

1.假设fp是一个函数指针,怎么调用其所指向的函数。

void (*fp)(int )

调用形式:(*fp)(int )

ANSI C 标准允许将上式写成:fp(int ),

表达式( fp)(int )中的 fp2侧的()很重要。

单目运算符*的优先级低于()。

因此:对于fp(),编译器会理解为: ( ( * fp)()),先调用,后解引用,逻辑上有严重问题。

2.对fp进行类型转换

void (*)()

3.分析(*0)()

*只能用于指针,但是0不是,因此我们需要对0进行类型转换,将其转化为函数指针。

因此:

( ( void ()() ) 0 ) ()
4.实质:

将0强制转换为void(*)()指针,指向返回值为void类型的函数的指针

5.typedef形式

typedef void (*fun)();

((void ( )())0)():

(*(fun)0)

运算符的优先级问题

讨论运算符,我们必然需要知道其优先级问题,这样对于一些程序,我们才不会凭直觉看程序。
类型 运算符 结合性
最高级别的操作符(并不是真的运算符),相对优先级从左到右依次递减 ()[],->, , 自左到右
单目运算符,相对优先级从左到右依次递减 ~++---(type)*&sizeof 自右到左
双目运算符---算术运算符 */,%,+,- 自左到右
双目运算符---移位运算符 <<,>> 自左到右
双目运算符---关系运算符,相对优先级从左到右依次递减 <,<=,>,>=,==,!= 自左到右
双目运算符---逻辑运算符,相对优先级从左到右依次递减 &^,**\ ,&&,\ \ ** 自左到右
三目运算符---条件运算符 自右到左
双目运算符---赋值运算符(assignments)相对优先级从左到右依次递减 =,*=,/=,%=,+=,-= 自右到左
最低优先级---逗号运算符(顺序求值运算符) , 自左到右

1.优先级规律总结

  • 优先级最高的是操作符,最低为,逗号运算符
  • 优先级顺序:
操作符 > 单目运算符 >算术运算符 >移位运算符 >

关系运算符 >逻辑运算符 >条件运算符(三目运算符)> 赋值运算符

  • 除了单目运算符赋值运算符三目运算符,逗号运算符结合性自右到左,其它都是==自左到右==.
  • 在算术运算符中,*,/,%优先级相同,+,-优先级相同。因此同级算术运算时的规则是:从左到右依次计算,
exp:3/2*4

编译器的含义是;(3/2)*4;

  • 关系运算符的相对优先级自左到右依次递减
exp:

a<b == c<d

编译器的含义:

(a<b)==(c<d)

2.分析表达式

x = y > 4000 && z<5 ?: 3.5 : 2.0;
  • 优先级: < , > ,&& , ? , = ;
  • 因此:( x= ( ( y>4000 ) && ( z < 5 ) ) ) ? 3.5 : 2.0;
  • 实质:判断完&&后的真假后的1/0,之和进行`条件运算符的运算,最后进行赋值运算

image-20210829164444436

*p++;
  • 优先级:++,*;
  • 编译器含义;*(p++);
while(c=getc(in)!=EOF)

{

putc(c,out);

}//非本意代码
  • 优先级:!=,=
  • 因此getc(in)返回一个临时变量,后与文件结尾标志`EOF比较,最后将1/0赋值给c,作为while的判断条件
while((c=getc(in))!=EOF)

{

putc(c,out);

}//正确代码

  • 优先级;(),!=,=
  • 因此将getc(in)的临时变量赋值给c后与EOF比较,作为while判断条件
if((t=BTYPE(PT1->aty)==STRTY)||t==UNTONTY)
{
   
}
  • 优先级:(),==,||,=
  • 先判断BTYPE(PT1->aty)与STRTY是否相等,真假的1/0发赋值给t.然后t与UNTONTY判断是否相等,最后||。

语句结束标志——

分号的不当使用,可能成为隐藏的,极其不易查找的Bug。

情形1

if(x[i]>y);
y=x[i];
因为 的问题,y=x[i]不再是if(x[i]>y)的函数体

因此为避免这种情况,每个if后面紧接{},就可以极大避免这种问题。

情形2

if(n<3)
return 
a=x[0];
b=x[1];
因为return后面遗漏 ,但是对于该程序,他仍然会顺利进行而不报错。如果返回函数的返回值的类型是 void,那么编译器会因为返回值类型不一致而报错。

但是对于函数声明时省略返回值类型,编译器会默认返回值类型是int。那么对于上面的程序,来说,其不会报错,但确实一个难以发现的Bug。

情形3.

struct time
{
int date ;
int time;

}
main()
{

...

}
对于这个程序,因为声明结构体类型,缺少 ,编译器会认为 main函数的返回值类型是结构体类型。

switch语句

c语言的switch语句的控制流程能够依次通过并执行各个case 部分。根据需要,通过break可以结束,c语言依次执行的机制.因此在使用switch时要格外注意break的使用。

#include <stdio.h>
int main()
{
   int day = 0;
   switch(day)
  {
       case 1:
       case 2:
       case 3:
       case 4:
       case 5:
           printf("weekday\n");
           break;
       case 6:
       case 7:
           printf("weekend\n");
           break;
  }
   return 0;
}

break语句的实际效果是把语句列表划分为不同的部分

“悬挂”else引发的问题

#define _CRT_SECURE_NO_WARNINGS
#include <string.h>
#include <stdio.h>
#include <stdlib.h> 

int main()
{
   int x = 2;
   int y = 2;
   if (2 == x)
       if ( 1== y)printf("haha\n");
      else
      {

           printf("hehe\n");
       }

}



image-20210829200216463

结果是“hehe”

  • else 与最近的if匹配。
  • 因此else 与if(1==y)配对
  • 好的解决方法if后面及时的{}
#define _CRT_SECURE_NO_WARNINGS
#include <string.h>
#include <stdio.h>
#include <stdlib.h> 

int main()
{
   int x = 2;
   int y = 2;
   if (2 == x)
   {
       if (1 == y)printf("haha\n");
       else
       {

           printf("hehe\n");
       }
   }
}
相关文章
|
Oracle Java 关系型数据库
Linux服务器搭建Minecraft原版/Mod服务器详细教程
Linux服务器搭建Minecraft原版/Mod服务器详细教程
5195 1
|
存储 Java 关系型数据库
Springboot AOP实现指定敏感字段数据加密 (数据加密篇 二)
Springboot AOP实现指定敏感字段数据加密 (数据加密篇 二)
2201 0
Springboot AOP实现指定敏感字段数据加密 (数据加密篇 二)
|
6月前
|
机器学习/深度学习 自然语言处理 测试技术
Qwen3技术报告首次全公开!“混合推理模型”是这样炼成的
近日,通义千问Qwen3系列模型已开源,其技术报告也正式发布。Qwen3系列包含密集模型和混合专家(MoE)模型,参数规模从0.6B到235B不等。该模型引入了“思考模式”与“非思考模式”的动态切换机制,并采用思考预算机制优化推理性能。Qwen3支持119种语言及方言,较前代显著提升多语言能力,在多个基准测试中表现领先。此外,通过强到弱蒸馏技术,轻量级模型性能优异,且计算资源需求更低。所有Qwen3模型均采用Apache 2.0协议开源,便于社区开发与应用。
5069 30
|
存储 人工智能 NoSQL
【AI系统】LLVM IR 详解
本文深入探讨了LLVM IR(中间表示)的概念,解释了其在编译器中的重要性和作用。LLVM IR作为一种抽象程度适中的中间语言,不仅涵盖了源代码的大部分信息,还支持编译器进行灵活的代码优化。文章进一步解析了LLVM IR的三地址码表示及其优点,并通过具体示例展示了LLVM IR的设计原则和内存模型,帮助读者更好地理解编译器内部的工作机制。
384 5
|
机器学习/深度学习 数据挖掘 Python
【第十届“泰迪杯”数据挖掘挑战赛】B题:电力系统负荷预测分析 问题二 时间突变分析 Python实现
第十届“泰迪杯”数据挖掘挑战赛B题中对电力系统负荷预测分析进行时间突变分析的Python实现方法,包括定义绘图函数、应用阈值查找异常值、手动设置阈值、使用分位数和3Sigma原则(IQR)设定阈值,以及根据分位数找到时间突变的步骤,并提供了完整代码的下载链接。
342 0
学生0元购|低配也能畅玩!《黑神话:悟空》云电脑攻略
《黑神话:悟空》正式上市,这款备受期待的游戏对电脑配置要求不低,但通过云电脑,你无需担心硬件限制,随时随地畅玩大作。最低仅需1.2元/小时,还能利用学生福利免费畅玩。快速上手教程与省钱攻略,助你轻松征服《黑神话:悟空》!
855 8
学生0元购|低配也能畅玩!《黑神话:悟空》云电脑攻略
正则表达式 - 最常用正则表达式大全(数字、字符、特殊)
正则表达式 - 最常用正则表达式大全(数字、字符、特殊)
1284 0
|
小程序 Java 大数据
智慧校园管理平台源码(含教师端、家长端、学生端小程序)
智慧校园以互联网为基础,“大数据+云服务+云计算”为核心,融合校园教学、管理、生活软硬件平台,定义智慧校园新生活。智慧校园管理平台管理者、教师、学生、家长提供一站式智慧校园解决方案,实现校园管理智能、.校园生活一体化、校园设施数字化、课堂教学生动化、家校沟通无缝化。集成智能硬件及第三方服务,面向学校、教师、家长、学生,将校内外管理、教学等信息资源进行整合,利用微信端的交互系统实现家校互联。
814 1
|
Java 编译器
【java面试】- java反射以及优缺点
java反射以及优缺点
546 0
|
弹性计算 人工智能 物联网
【玩转AIGC系列】基于AIACC加速器快速实现Stable Diffusion生成特定物体图片
本文介绍如何使用GPU云服务器搭建Stable Diffusion模型,并基于ControlNet框架,快速生成特定物体图片。
【玩转AIGC系列】基于AIACC加速器快速实现Stable Diffusion生成特定物体图片