@[TOC]
前言:
博主实力有限,博文有什么错误请你斧正,非常感谢!这是《c陷阱与缺陷》专栏第二章。
词法”陷阱“
语法”陷阱“
要理解一个
C
程序,仅仅理解组成该程序的”符号“是不够的。还需要我们理解这些“符号”是如何组成声明,表达式,语句与程序的。本文主要讨论一些与人们直觉相悖,容易引起混淆的地方。
理解函数声明
1.变量的声明:
定义任何c的变量声明都由两部分组成:
“类型”
以及一组类型表达式的“声明符”
以及最后的”分号“
,且声明符可以像表达式一样,任意的使用括号
变量声明的意义:
- 建立变量符号表
通过声明变量,编译器可以建立变量符号表,对于程序用什么变量,编译器都非常清楚。从而帮助程序员远离疏忽而将变量名写错的情况
- 为变量分配内存
声明变量的同时,编译器为其分配内存,exp:
char 1字节,int 4字节,float 4字节,
double 8字节
- 变量的类型声明,说明了编译器看内存的角度
exp:二进制补码:11111111
对于声明为char 型,在内存中存的是十进制的==-1==;
对于声明为 unsigned char 型,在内存中存的是十进制的255
- 声明类型,确定变量的取值范围
char :-128~127unsigned char: 0~255
....
- 不同数据类型有不同算法操作
除法/:
对于2边为整数,实行整形除法。对于一边为浮点型,实现浮点除法。
取余
%
只能用于整形变量运算。浮点型不行。
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
,之和进行`条件运算符
的运算,最后进行赋值运算*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"); } }
结果是“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"); } } }