现在不少大学都把C语言作为一门必学的编程语言。
C语言考试呢,并不能决定你的实践能力怎么样,他考的很多点,我们不知道,也可以在写代码时避免,我们举个最简单的例子,运算符优先级,这恐怕是必考的内容了。但实际上我们写代码并不需要背下这个,我们完全可以通过多加几个括号解决问题。但是考试它并不会管这些,考试是别人出的题,所以代码也是它的。而我们也需要一个较高的分数,那么我们应该怎么办呢?
没错,这篇博客就是为了这个而诞生的。从cggwz个人的刷题经历,总结出易错的一些点,提醒大家在考试中注意,那么废话不多说,我们开始吧!
(再强调一遍:这里大部分是出卷人可能的设误点,而不一定是你自己写代码的易错点)
运算符相关
✔ 自增(减)前后缀
我们以自增为例,前缀和后缀的差别有两点:
优先级不同,后缀优先级更高
返回值不同
虽说优先级不同,但是在两者之间也就逻辑非和按位取反,貌似不会有太大影响。
而重点是返回值,也就是说, x返回的是x 1,而x 返回的是原来的x。
而x本身的值都会在返回值以后立刻改变,也就是执行这个表达式后立即改变,比如:
#include
int main(){
int x=0,y=2,z=1;
x=z z;
printf("%d",x);
return 0;
}
这个输出结果就是3,而不是2
✔ 逻辑运算符的短路问题
C语言在计算逻辑运算符的时候是采取短路机制的,也就是说,如果已经可以判断这个表达式的最终结果,那么就不再判断接下来的表达式,相应地,那些表达式也不会被执行。而出题人则喜欢通过放置一些赋值语句在后面,某些学生会因为忽略了短路机制,而误以为会执行,从而出错,我们看个例子:
#include
int main(){
int x=3,y=2,z=1;
y=--z&&--x;
printf("%d",x);
return 0;
}
这里输出是2吗?
并不是,是3
因为--z返回的值是0,而后面的逻辑运算符是&&,也就是说无论后面一个语句真假与否,这个表达式都是假,所以就会触发短路机制,不再计算后面的 x,从而x的值不会改变。
我们来看个变式:
#include
int main(){
int x=3,y=2,z=1;
y=--z&&--x||--x;
printf("%d",x);
return 0;
}
相信你现在应该能知道结果了,输出就是2.中间的那个–x并不会因为它在中间而被执行。
✔ 赋值运算符的返回值问题
我们知道C语言中的赋值运算符是有返回值的。
它们的返回值是赋值后被赋值变量的值,而不是赋值符号右边的表达式的值。
比如int a=10;a =3;这里的 =返回的就不是3,而是13.
这也适用于类型转换问题,比如double a;a=2;这里这个赋值语句的返回值就是2.0,而不是2.
✔ 运算符优先级的问题
考试前必须要掌握的内容之一。
但是显然,我这里不会把一大张表放出来,要是需要看看课本就好了。
这里我是想总结一下这张表,让大家更容易的“记住”这张表。
我们也会顺带地总结一下结合方向。
首先第一梯队,优先级最高的,是一些指代所属关系的运算符,比如[]、->、.。在加上一个圆括号。这个应该很好理解。圆括号如同一个老大哥,它在改变运算顺序这方面具有绝对的话语权,自然会在第一梯队,而这个老大哥也是懂人情的,->、.这类符号就好像是调用父对象里的一个子对象,把人家父子关系拆散恐怕不是一个老大哥应该做的,所以它们也属于第一梯队。它们的优先级是最高的。
接下来第二梯队主要是我们的单目运算符。比如正负号、指针符号、取地址、自加自减之类。它们通常是紧紧贴着它们作用的对象的(单目),那关系亲密地如同情侣。虽然狗粮不好吃,但是不能因为嫉妒就拆散人家呀(谁说的,我就觉得可以),所以它们的优先级也是比较高的,属于第二梯队。
第三梯队就是我们的四大金刚啦——加减乘除,哦不,我们不能忘了我们除法的好兄弟——取余。所以它们五个就是我们的第三梯队,我们可以把它们叫做算术运算符。这五大金刚可是从小学就开始陪伴我们了,自然在考虑完父子情、情侣情之后要考虑它们几个了。
第四梯队是位运算的左移和右移运算符。这两个家伙虽然名气没有上面五大金刚大,但是它们也是可以改变变量的值呢,也可以算得上小金刚了。所以,第四梯队,当之无愧。
第五梯队是关系运算符,也就是表示大于小于不等这类符号。它们虽然也很早就开始陪伴我们,名气也不小,但是毕竟是比较运算的结果,自然要等前两个梯队运算完,才能比较,那就只好屈居它们后面啦。
第六梯队是位运算的按位与、或、异或。理论上它们也是改变数值的呀,为什么不在前面的梯队呢?我们可以看看它们的样子:&、|,正好是我们下面一个梯队的一半,二者的关系情同手足,成功固然美好,但是一份兄弟情,哪是成功可以换来的?它们自然愿意留在兄弟身旁,相依相助。
第七梯队是逻辑运算符,加上唯一一个三目运算符——条件运算符。它们连接的多半是关系运算符的结果,自然就得在关系运算符后面啦!
第八梯队可是一个大部队,赋值运算符。它们当然得殿后啦,它们可是把前面的运算结果保存下来的必经之路。它们就如同慈爱的父母,默默地收拾前面这些孩子算出来的各种数据,并把它们认真地保存下来。我们的父母在我们小时候,不也是这样的吗?
咦?我们好像还漏一个,没错,逗号运算符。存在感太低啦!就放在最后一个吧!(我想你们大部分人也很少用它)。
这样我们的优先级就彻底理清楚啦!
总结一下:
分量、下标运算符、圆括号>单目运算符>算数运算符(五大金刚加两个小金刚)>关系运算符>位运算符(与或异或)>逻辑运算符(含条件运算符)>赋值运算符>逗号运算符
这个总结并不严谨,因为有些运算符并不是在一起的,所以要注意对我说的故事的理解,再结合表看看背背就OK了。
下面再说一下结合方向。
这个还是很好总结的,除了后缀自加自减的所有单目运算符、条件运算符、赋值运算符是右结合,其他的符号全是左结合。
这个还是很好理解的,那些单目运算符通常写在作用对象的左边,自然是从右向左结合,赋值符号也是把右边的数值赋给左边,所以也是从右向左,条件运算符……这么特殊的一个,记一下就好了。
这样我们就成功地解决了这张令人头疼的表。怎么样?不困难吧?
✔ sizeof()的返回值问题
首先强调一下,这个不是函数,虽然它看起来很像。
它运算得到的值是括号内的东西所占的内存大小,它是一个int型的值。所以sizeof()的表达式是int型表达式。
函数相关
✔ 函数声明问题
我们知道C语言里是需要函数声明的,如果函数放在主调函数的后面,就需要在主调函数前进行原型声明。
但是不知道有没有像我一样,由于长期把函数放在主调函数的前面(少打几行字),而在考试时容易忘记声明这件事。其实网上有不少信息学竞赛题的代码,它们绝大多数都是把函数放在主调函数前面,所以这里也算是一个提醒吧。
✔ mian函数参数问题
我们知道main函数有三个参数argc,argv,envp
我们主要提示一下前面两个。
现在如果我们在命令行执行如下语句:
D:\cgg.exe how are you!
这么执行后,argv是4,而不是3,因为D:\cgg.exe也是一个参数,而它就保存在argv[0]中。
这一点千万不能忘记。
变量相关
✔ 用字符串初始化字符数组
主要是长度的问题,我们知道C中的字符串是以一个空字符作为字符串的结尾标志。所以,我们在赋值时,要注意字符数组的长度要比字符串的长度多1。比如下面这段代码是无法通过编译的:
#include
int main(){
char a[4]="abcd";
return 0;
}
而我们只需要把a的长度改为5就好了。
✔ 赋初值问题
这是一个老生常谈的问题,但是还是很容易被忽略。所以在这里再说一下。当遇到一些计算求值类的题,一定要注意把变量初始化,赋初值,比如0
✔ 转义字符问题
我们知道C语言中是有转义字符的。转义字符以\开头,所以,我们是不能在一对单引号中间直接放置右斜杠的,还有就是注意,斜杠后如果是十六进制的表示码,开头是x或X,而不是0x或0X
✔ 常用ASCII码记忆
常用的无非是大小写字母和数字,这时一定需要记住的(除非你是上机考试)。
✔ 字符串常量占用内存问题
我们知道C中的字符串一定是以空字符‘\0’结尾,而这个空字符是不显示的。所以有的出题老师会在这里设坑,比如:"abcde"占的内存大小是多少字节?虽然是五个字符,但是实际上还有一个空字符,总共是6字节。
✔ 字符变量值的问题
我们经常会遇到这样的题,问你某个字符的值是什么。
这时我们需要尤其小心,尤其是选择题。
我们举个例子来说:char a='C'
a的值是什么?
是C吗?
如果你说是,那就错了。
我们只能说a的值是‘C’或者67.
想必你应该能明白我想说什么了。
单引号是不能省去的,这一点尤其重要,缺少了单引号,它就不再是一个字符了。
这一点很容易忽略,尤其是选择题,看到C这个选项,一激动就选上了。那就惨了。
✔ 变量类型的范围计算问题
这里只提醒一点,计算范围的时候不要忘记对于有符号的数,有一位符号位。
✔ 字符串常量拼接问题
在C语言中是允许进行字符串拼接的。
也就是说,如果两对引号包含的字符串放在一起,则会被解释成一个字符串。
比如:
#include<stdio.h>
int main(){
char a[15]="hello""world";
char b[15]="hel""lo""world";
printf("%s",b);
return 0;
}
这两个字符串都是合法的,且和“helloworld”等价。
数组相关
✔ 数组下标越界
这个……恐怕是个程序员都会出过的bug,从你初学编程语言到你退休。
考试也可能会出现这个问题,多半是在for循环里出现。
二维数组中行列计算问题
我们可能会经常看到这样的题,就是另一个指针等于二维数组中的某个地址单元,然后对指针进行加一之类的操作,然后问输出的值。
而我们知道这里会有两种不同的效果,一种是行计算,一种是列计算,二者的含义是,行计算,每次加一,地址会移动一行;而列计算,每次加一,地址只会移动一格。
比如:
#include
int main(int argc,char *argv[]){
char a[3][4]={{"you"},{"I"},{"we"}};
char (*p)[4];
p=a;
printf("%s",*(p 1));
return 0;
}
这里输出的就是第二个字符串I
具体的分类为:
✔ 字符数组赋值问题
我们知道我们可以这样声明一个字符数组:
char a[12]="helloworld!";
但是,字符数组在被定义之后,是不被允许这样直接赋值的。
比如下面这段代码是不会通过编译的:
char a[12];
a="helloworld!";
结构体相关
✔ 结构体所包含的变量不可初始化
这一点在C 中是被允许的,但是C中是不允许的。
所以习惯了写C 的同学尤其需要注意。
不要忘记了。
✔ 查看结构体内部变量所占内存大小
这里我们只举个例子,比如我们有ABC这个结构体,它有变量x,
则sizeof(((struct ABC*)0)->x)得到的就是x占用的内存大小。
这里的0,只要是int型的量就可以。
库函数相关
✔ 绝对值函数
注意math.h中有两个绝对值函数,一个是abs(),另一个是fabs()
其中前者是处理整型变量的,而后者是处理浮点型变量的
scanf()和printf()的返回值问题
我们通常会把它们当作输入输出语句使用,单独成行,总是自动忽略它们的返回值,但其实它们是有返回值的。
那么它们的返回值是什么呢?
printf的返回值是输出的数据个数,而scanf的返回值则是读入的数据个数。
我们可以来看个例题:
#include
int main(){
int x;
printf("2:%d",printf("1:%d,",scanf("%d",&x)));
return 0;
}
请问它的输出结果是什么?
答案是
1:1,2:4
为什么是4?不知道你是不是想这么问。
printf中所说的数据个数,其实是把输出的每个字符都看成一个单独的数据,也就是有多少字符加上我们传入的数据,就是它的返回值。
✔ fclose()的返回值问题
和scanf、printf一样,我们经常忽略fclose的返回值,其实他也是有返回值的。
如果成功关闭流,那么返回0,如果未成功关闭则返回-1。
预编译相关
✔ 注意宏定义的替换原则
我们知道宏定义实际上只是字符串的替换,并不是函数一样的东西,所以我们在做题时也需要严格替换,替换完成之前,不要进行任何计算。
我们举个例子:
答案是多少?54?那就错了。实际上应该是48.
这里我们不能把5 1算出来6再带入,而是直接代入,这样你就会发现,5和1不是直接加在一起的。