小结:C语言易错点

文章目录
  1. 1. 易错点速查
  2. 2. Ch3 数据和 C
    1. 2.1. 转义序列 ‘\num’ 的 num 看成八进制数而不是十进制
    2. 2.2. 四舍五入与去尾
  3. 3. Ch4 字符串和格式化输入/输出
    1. 3.1. 两个xxxxf()函数的返回值
    2. 3.2. 易错:双精度浮点型 (double) 的转换说明
    3. 3.3. C Primer Plus 给出的易错提示·节选
    4. 3.4. printf() 求具体值的顺序
    5. 3.5. printf() 中的修饰符
  4. 4. Ch5 运算符、表达式和语句
    1. 4.1. 整数相除即为整除
    2. 4.2. 例题一则:运算符的优先级问题
    3. 4.3. 逻辑运算符:本是同根生,相煎何太急
  5. 5. Ch6 C 控制语句:循环
    1. 5.1. 逗号运算符
  6. 6. Ch7 C 控制语句:分支和跳转
    1. 6.1. 摘抄
      1. 6.1.1. 7.3.2 优先级
      2. 6.1.2. 7.3.3 求值顺序
  7. 7. Ch12 存储类别、链接和内存管理
    1. 7.1. 静态变量的声明(包括赋初值的声明)只执行一次
  8. 8. 后面的章节
    1. 8.1. malloc() 前面要显式转换指针类型
    2. 8.2. 这个CC就是逊啦:C语言没有连等判断
    3. 8.3. 在逻辑判断中偷懒
    4. 8.4. 没有赋值,白算一遭
    5. 8.5. switch … case 结构不加 break;
    6. 8.6. 欢乐的指针们
  9. 9. 附录:手写稿
  10. 10. 附录:方法论
    1. 10.1. 把指针这种衍生类型看作基本类型

酱瓜正在激情复习C语言中,在这里随便记一下看到的一些易错点。本文按照《C Primer Plus(第6版)》的章节顺序编排。

易错点速查

  • 整数除法即为整除
  • switch…case 语句漏 break;

Ch3 数据和 C

转义序列 ‘\num’ 的 num 看成八进制数而不是十进制

1
2
3
4
5
6
7
#include <stdio.h>
int main() {
char a = '\65';
char b = 65;
printf("%d %c\n", a, a);
printf("%d %c", b, b);
}

Output:

1
2
53 5
65 A

当然,最标准的写法当然是:

1
2
3
char best0 = '\0oo';	//ASCII码为八进制数oo
char best1 = '\00o'; //ASCII码为八进制数o
char maybe_worse = '\0o'; //ASCII码为八进制数o

四舍五入与去尾

简而言之就是小数->小数就是四舍五入,小数->整数就是直接去尾(截断)。

1
2
3
4
5
#include <stdio.h>
int main() {
float a = 3.54159;
printf("%.3f %d", a, (int)a);
}

Output:

1
3.542 3

Ch4 字符串和格式化输入/输出

两个xxxxf()函数的返回值

printf() 返回实际输出字符串的长度(不包括'\0')。

scanf() 返回成功读取的数据个数。

易错:双精度浮点型 (double) 的转换说明

printf() 中输出一个 double 型用 %f 即可,而 scanf() 读入一个 double 型却要用 %lf

C Primer Plus 给出的易错提示·节选

  1. 对于 scanf() ,一定要记得在变量名前加上地址运算符(&)。
  2. scanf() 函数中的转换说明为 %s 时,可读取一个单词

printf() 求具体值的顺序

C 语言在执行 printf() 时。对函数中表达式表列的处理顺序是从后往前……对于 printf("%d %d %d", n, n++, n--); 先处理 n-- ,再处理 n++ ,最后处理 n ……

某 C语言试题集

printf() 中的修饰符

下表来自《C Primer Plus(第6版)》, P83

修饰符 含义
.数字 精度
对于 %s 转换,表示待打印字符的最大数量
对于整型转换,表示待打印数字的最小位数
对于 %e、%E、%f 转换,表示小数点右边数字的位数
对于 %g、%G 转换,表示有效数字的最大位数

Ch5 运算符、表达式和语句

整数相除即为整除

这个就很经典了,整数除法只能出整数(趋零截断),浮点数除法(只要一个运算数是浮点数即可)才是平时我们做的除法。不过说归说,一旦这个含除法运算符的表达式太长,有可能忘掉这一点。

例题一则:运算符的优先级问题

p 为指针(int* p;),试解析语句 a = *p++;

【解析】

a = *p++;

a = *(p++);

1
2
a = *p;
p += 1;

逻辑运算符:本是同根生,相煎何太急

逻辑与 && 的优先级要高于逻辑或 ||

Ch6 C 控制语句:循环

逗号运算符

逗号运算符把两个表达式连接成一个表达式,并保证最左边的表达式最先求值。逗号运算符通常在 for 循环头的表达式中用于包含更多的信息。整个逗号运算符的值是逗号右侧表达式的值。

《C Primer Plus(第6版)》, P158

虽然「整个逗号运算符的值是逗号右侧表达式的值」,但不代表在那种「人脑Build & Run」题目里面看到逗号运算符就直接看最右边的表达式,因为前面几个表达式可能改变了某些变量的值!比如:

1
x = (++z * y, y++, z % y);

执行到 z % y 一步就有:

1
2
z == z_original + 1;
y == y_original;

Ch7 C 控制语句:分支和跳转

摘抄

7.3.2 优先级

! 运算符的优先级很高,比乘法运算符还高,与递增运算符的优先级相同,只比圆括号的优先级低。&& 运算符的优先级比 || 运算符高,但是两者的优先级都比关系运算符低,比赋值运算符高。

因此,表达式

1
2
a > b && b > c || b > d;
>

相当于

1
2
((a > b) && (b > c)) || (b > d);
>

《C Primer Plus(第6版)》, P192

7.3.3 求值顺序

……C通常不保证先对复杂表达式中哪部分先求值。例如,下面的语句,可能先对表达式 5 + 3 求值,也可能先对表达式 9 + 6 求值:

1
2
apples = (5 + 3) * (9 + 6);
>

……但是,逻辑运算符是个例外,C 保证逻辑表达式的求值顺序是从左到右。&&|| 运算符都是序列点,所以程序在从一个计算对象执行到下一个运算对象之前,所有的副作用都会生效。而且,C 保证一旦发现某个元素让整个表达式无效,便立即停止求值。(酱瓜注:参见在逻辑判断中偷懒)……

《C Primer Plus(第6版)》, P192

Ch12 存储类别、链接和内存管理

静态变量的声明(包括赋初值的声明)只执行一次

下面两个声明很相似:

1
2
3
int fade = 1;
static int stay = 1;
>

……第 2 条声明实际上并不是 trystat() 函数的一部分。如果逐步调试该程序会发现,程序似乎跳过了这条声明。这是因为静态变量在程序载入内存时已执行完毕。把这条声明放在 trystat() 函数中是为了告诉编译器只有 trystat() 函数才能看到该变量。这条声明并未在运行时执行

《C Primer Plus(第6版)》, P382

上面那一段引用其实是在围绕下面这个例程说的(这个例程主函数之类的部分省略,在书P381页可以找到,trystat() 函数在主函数中被调用了 3 次):

1
2
3
4
5
6
7
void trystat(void)
{
int fade = 1;
static int stay = 1;

printf("fade = %d and stay = %d\n", fade++, stay++);
}

Output:

1
2
3
fade = 1 and stay = 1
fade = 1 and stay = 2
fade = 1 and stay = 3

后面的章节

这里散落着酱瓜在高速狂刷往年卷的时候收集的易错点。

malloc() 前面要显式转换指针类型

1
2
3
4
5
struct node *wrong, *correct;
// 错误
wrong = malloc(sizeof(struct node));
// 正确
correct = (struct node*) malloc(sizeof(struct node));

这个CC就是逊啦:C语言没有连等判断

这一条是之前帮同学 debug 的时候发现的。

C语言:

1
2
if (0==0==0) printf("True");
else printf("False");

Output:

1
False

Python:

1
2
3
4
if (0==0==0):
print("True")
else:
print("False")

Output:

1
True

在逻辑判断中偷懒

有时候会说成「编译器的优化」。

比如这道题:

设m, n, a, b, c, d 均为 0,执行 (m=a==b) || (n=c==d) 后,m, n 的值分别为?

因为 || 左边那个 (m=a==b) 已经是 True 了,无论右边是啥,整条表达式都肯定是 True,所以程序干脆就不执行 (n=c==d) 了。故此题答案:m 为 1,n 为 0。

没有赋值,白算一遭

像什么写个 k % 2; 让你误以为是 k = k % 2; 或者 k %= 2;

注:

上面一行格式很不好看……对,题目就是在不好看的代码里面给插一个坑,更难发现有坑。好看一点的话上面应该写成这样——

像什么写个

1
k % 2;

让你误以为是

1
k = k % 2;

或者

1
k %= 2;

switch … case 结构不加 break;

不细说了,经典易错点,很容易看漏。

欢乐的指针们

下文把 int 型指针(基本类型 int 的衍生类型)看作名为 int* 的基本类型。

1
2
3
4
int (*p)(int n);	// p 是一个函数指针(指向函数的指针)
int* p(int n); // p 是一个函数,函数返回一个 int*
int* p[10]; // p 是一个长度为10的 int* 型数组
int (*p)[10]; // p 是一个指向 长度为10的数组 的指针(JonbguaScript: int[10]* p)

附录:手写稿

Page 1 Page 2 Page 3

附录:方法论

把指针这种衍生类型看作基本类型

尤其在涉及“函数隔离变量”考点的题目里面特别管用。比如这题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
void swap1(int *a, int *b) {
int *t;
t = a; a = b; b = t;
}

void swap2(int *a, int *b) {
int t;
t = *a;
*a = *b;
*b = t;
}

void swap3(int *a, int **b) {
int *t;
t = a;
a = *b;
*b = t;
}

int main() {
int a = 3, b = 4, *pa = &a, *pb = &b;
swap1(&a, &b);
printf("%d, %d\n", a, b);
/*
首先,&a 不是左值
其次,由于函数的变量隔离,即便swap1(pa, pb)也是白给
*/

swap2(pa, pb);
printf("%d, %d\n", a, b);
/*
设 SWAP 可以把 a, b 对调
swap2(pa, pb)等价于:
SWAP(a, b);
*/

swap3(pa, &pb);
printf("%d, %d, %d, %d\n", a, b, *pa, *pb);
/*
等价于:
pb = pa;
*/

return 0;
}

若是把 int *a 看作 int* a; ,下面就不容易看乱了。

此题的输出为:

1
2
3
3, 4
4, 3
4, 3, 4, 4

注:

对于这个技巧,更为通用的做法是按照下面这种编码格式去理解:

* 和指针名之间的空格可有可无。通常,程序员在声明时使用空格,在解引用变量时省略空格。

《C Primer Plus(第6版)》, P271

比如:

1
2
3
4
int * p;
void interchange(int * u, int * v);

*p = 1;