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");
       }
   }
}
相关文章
|
存储 自然语言处理 编译器
C陷阱与缺陷
C陷阱与缺陷
71 0
C陷阱与缺陷
|
28天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
21天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
7月前
|
测试技术
常见测试陷阱
常见测试陷阱
|
7月前
|
存储 程序员 编译器
C陷阱与缺陷:语法陷阱
C陷阱与缺陷:语法陷阱
64 0
|
编译器 C语言
源于《C陷阱与缺陷》----研究程序死循环问题
所以最后答案应该就是打印了12次xiao tao,然后越界访问出现错误,使arr[10]=0,arr[11]=0了 但最后答案却不是这样。
126 0
|
存储 人工智能 自然语言处理
【C缺陷与陷阱】----语义“陷阱”
那获得该下标为0的元素的指针,如果给这个指针加1,就能得到指向该数组中下一个元素的指针。也就是指针+一个整数得到的还是指针,只不过指针的位置发生改变
113 0
|
自然语言处理 编译器 程序员
【C陷阱与缺陷】----语法陷阱
由于一个程序错误可以从不同层面采用不同方式进行考察,而根据程序错误与考察程序的方式之间的相关性,可以将程序错误进行划分为各种陷阱与缺陷
130 0
|
自然语言处理 编译器 C语言
|
自然语言处理 编译器 C语言