- 1、忘记定义变量
- 2、输入输出的数据的类型与用户指定的输入输出格式声明不一致
- include
- 4、在使用输入函数
scanf
时,忘记用变量的地址符&
。 - 5、输入数据的格式与要求不符【格式化输入】
- 6、在用
scanf
函数向字符数组输入数据时,在数组名前面多加&
。 - 7、在用
scanf
函数向数值型数组输入数据时,用数值型数组名 - 8、语句后面漏分号
- 9、把预处理指令当作 C 语句,在行末加了分号
- 10、在不该加分号的地方加了分号
- 11、对应该有花括号的复合语句,忘记加花括号
- include
- 12、括号不配对
- 13、在用标识符时,混淆了大写字母和小写字母的区别。
- 14、误把“=”作为“等于”运算符
- 15、引用数组元素时误用了圆括号
- include
- define N 5
- 16、在定义数组时,将定义的“元素个数”误认为是“可使用的最大下标值”
- 17、对二维或多维数组的定义和引用的方法不对
- include
- 18、误以为数组名代表数组中全部元素
- 21、switch 语句的各分支中漏写 break 语句
- include
1、忘记定义变量
错误代码:
#include <stdio.h>
int main() {
x = 3; // 错误
y = 5; // 错误
printf("%d\n", x, y);
return 0;
}
编译运行:
:::danger
b12@PC:~/chapter13$ gcc -Wall ./src/defineVar.c -o ./bin/defineVar
./src/defineVar.c: In function ‘main’:
./src/defineVar.c:4:5: error: ‘x’ undeclared (first use in this function)
4 | x = 3;
| ^
./src/defineVar.c:4:5: note: each undeclared identifier is reported only once for each function it appears in
./src/defineVar.c:5:5: error: ‘y’ undeclared (first use in this function)
5 | y = 5;
| ^
:::
错误原因:C 要求对程序中用到的每一个变量都必须定义其类型,上面程序中没有对 x,y
进行定义。应在函数体的开头加 int x, y;
#include <stdio.h>
int main() {
int x, y; // declare
x = 3; // 错误
y = 5; // 错误
printf("%d\n", x, y);
return 0;
}
2、输入输出的数据的类型与用户指定的输入输出格式声明不一致
错误代码:例如,若 a
已定义为整型,b
已定义为实型:
#include <stdio.h>
int main() {
int a = 6;
float b = 4.5;
printf("%f, %d\n", a, b); // 错误格式化
return 0;
}
编译运行:
:::warning
b12@PC:~/chapter13$ gcc -Wall ./src/mismatchFormat.c -o ./bin/mismatchFormat
./src/mismatchFormat.c: In function ‘main’:
./src/mismatchFormat.c:5:14: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=]
5 | printf(“%f, %d\n”, a, b); // 错误格式化
| ~^ ~
| | |
| double int
| %d
./src/mismatchFormat.c:5:18: warning: format ‘%d’ expects argument of type ‘int’, but argument 3 has type ‘double’ [-Wformat=]
5 | printf(“%f, %d\n”, a, b); // 错误格式化
| ~^ ~
| | |
| int double
| %f
b12@PC:~/chapter13$ ./bin/mismatchFormat
4.500000, 6
:::
错误原因:发出警告,数据类型与指定的输出格式不匹配!并且尝试输出发生非常奇怪的事情??怎么值不一样?
在这种情况下,并不是按照赋值的规则进行转换(如把 b
的值 4.5
转换成 4
,然后输出 4
),而是将数据在存储单元中的形式按格式符的要求组织输出。
- 如
a
是整数,按整数的存储方式存储,今要它按浮点数输出,系统把此数在内存中存放的形式按浮点数解释,组织输出。 b
是浮点数,按浮点数的存储方式存储,现在系统把这个数在内存中存放的形式按整数解释,把它直接作为某一整数输出。 :::tips 即是说格式化输出不是类型转换,而是直接按给定格式化字符解释。这一点需要和(强制)类型转换弄明白。 ::: ```cinclude
int main() { int a = 6.5; // 隐式类型转换 float b = (float)4; // 强制类型转换 printf(“%d, %f\n”, a, b); // 正确格式化 return 0; }
**编译运行**:gcc 不会给出“隐式类型转换”的警告
:::success
b12@PC:~/chapter13$ gcc -Wall ./src/mismatchFormat.c -o ./bin/mismatchFormat<br />b12@PC:~/chapter13$ ./bin/mismatchFormat<br />6, 4.000000
:::
<a name="NsRmY"></a>
# 3、未注意 `int` 和 `short` 数据的数值范围
**错误代码**:不知道整型赋值会发生截断
```c
#include <stdio.h>
#include <limits.h>
int main() {
printf("sizeof(short):%lu, SHORT_MAX:%d, SHORT_MIN:%d\n",
sizeof(short), SHRT_MAX, SHRT_MIN);
printf("sizeof(int):%lu, INT_MAX:%d, INT_MIN:%d\n",
sizeof(int), INT_MAX, INT_MIN);
printf("sizeof(long):%lu, LONG_MAX:%ld, LONG_MIN:%ld\n",
sizeof(long), LONG_MAX, LONG_MIN);
int a = 89101;
short b = a; // overflow
printf("a=%d, b=%d\n", a, b);
a = 196607; // 10 1111 1111 1111 1111
b = a; // minus?
printf("a=%d, b=%d\n", a, b);
return 0;
}
编译运行:(注意:书上 198607 二进制给的是错误的!)
:::success
b12@PC:~/chapter13$ gcc -Wall ./src/intRange.c -o ./bin/intRange
b12@PC:~/chapter13$ ./bin/intRange
sizeof(short):2, SHORT_MAX:32767, SHORT_MIN:-32768
sizeof(int):4, INT_MAX:2147483647, INT_MIN:-2147483648
sizeof(long):8, LONG_MAX:9223372036854775807, LONG_MIN:-9223372036854775808
a=89101, b=23565
a=196607, b=-1
:::
与预期相反原因:整型的存储空间有限,发生截断高位!
89101: 0000 0000 0000 0001 0101 1100 0000 1101
高位32>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>低位0
赋值short 👇<---------------->
23565: ---- ---- ---- ---- 0101 1100 0000 1101
89101: 0000 0000 0000 0010 1111 1111 1111 1111
高位32>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>低位0
赋值short 👇<---------------->
-1 : ---- ---- ---- ---- 1111 1111 1111 1111
因此发生上述截断过程就导致最终 short
结果与预期相差很大,但是有没有一种好的办法可以检测呢?比如直接抛出异常等。
:::tips
注意类型转换和检测溢出不是一回事,前者发生的可能是截断(编译期),而后者是在算术运算出现的(运行时),因此后者更难以检测到。
:::
4、在使用输入函数 scanf
时,忘记用变量的地址符 &
。
错误代码:传参类型错误(Excepted:pointer)
#include <stdio.h>
int main() {
printf("Please input two number(a b):");
int a, b;
scanf("%d %d", a, b); // Error
printf("a=%d,b=%d\n", a, b);
return 0;
}
编译运行:
:::danger
b12@PC:~/chapter13$ gcc -Wall ./src/scanfPtr.c -o ./bin/scanfPtr
./src/scanfPtr.c: In function ‘main’:
./src/scanfPtr.c:6:13: warning: format ‘%d’ expects argument of type ‘int ’, but argument 2 has type ‘int’ [-Wformat=]
6 | scanf(“%d %d”, a, b); // Error
| ~^ ~
| | |
| int int
./src/scanfPtr.c:6:16: warning: format ‘%d’ expects argument of type ‘int ’, but argument 3 has type ‘int’ [-Wformat=]
6 | scanf(“%d %d”, a, b); // Error
| ~^ ~
| | |
| int int
./src/scanfPtr.c:6:5: warning: ‘a’ is used uninitialized in this function [-Wuninitialized]
6 | scanf(“%d %d”, a, b); // Error
| ^~~~~~~~
./src/scanfPtr.c:6:5: warning: ‘b’ is used uninitialized in this function [-Wuninitialized]
b12@PC:~/chapter13$ ./bin/scanfPtr
Please input two number(a b):99 66
Segmentation fault (core dumped)
:::
出错原因:函数设计模式可以理解为防止变量的覆盖与多次调用,因此传入参数永远都是一份拷贝。但是全局变量认可在函数“可见范围”内修改,但是这种行为是不被建议的。因此想要设计出一个符合函数拷贝而达到修改其本身值,那么就是指针的存在。通过传入地址参数,然后访问地址内的内容从而修改变量值。(而不是把变量当作参数拷贝后直接传入函数处理)。
这是许多初学者刚学习 C 语言时一个常见的疏忽,在其他语言中在输入时只须写出变量名即可,而 C 语言要求指明“向哪个地址所标识的单元送值”。应写成 scanf("%d %d", &a, &b);
#include <stdio.h>
int main() {
printf("Please input two number(a b):");
int a, b;
scanf("%d %d", &a, &b); // pointer
printf("a=%d,b=%d\n", a, b);
return 0;
}
编译运行:
:::success
b12@PC:~/chapter13$ gcc -Wall ./src/scanfPtr.c -o ./bin/scanfPtr
b12@PC:~/chapter13$ ./bin/scanfPtr
Please input two number(a b):99 66
a=99,b=66
:::
5、输入数据的格式与要求不符【格式化输入】
错误示例1:用 scanf
函数输入数据,应注意如何组织输入数据。关于 scanf
函数,回顾如下:
第三章、最简单的 C 程序设计——顺序程序设计
假如有 scanf
函数代码:
#include <stdio.h>
int main() {
printf("Please input two number(a b):");
int a, b;
int n = scanf("%d%d", &a, &b);
printf("a=%d,b=%d,successfully assgin %d elements\n", a, b, n);
return 0;
}
对以上程序编译运行:
:::success
b12@PC:~/chapter13$ gcc -Wall ./src/scanfFormat.c -o ./bin/scanfFormat
b12@PC:~/chapter13$ ./bin/scanfFormat
Please input two number(a b):3,4 # 以逗号间隔输入
a=3,b=-544663136,successfully assgin 1 elements # 成功 1 个
b12@PC:~/chapter13$ ./bin/scanfFormat
Please input two number(a b):3 # 以 \n 间隔输入
4
a=3,b=4,successfully assgin 2 elements # 成功 2 个
b12@PC:~/chapter13$ ./bin/scanfFormat
Please input two number(a b):3 4 # 以 \t 间隔输入
a=3,b=4,successfully assgin 2 elements # 成功 2 个
:::
从上面看到格式化字符 %d
可以自动过滤掉空白符( \n\t\f\v
和空格),但是只要不是非空白符就认为赋值结束,撒手不管了!!同时可以检测函数返回值(赋值成功个数)即可知道有效的赋值是否进行!
错误示例2:用 scanf
必须原样输入“要求字符”
#include <stdio.h>
int main() {
printf("Please input two number(a b):");
int a, b;
int n = scanf("%d,%d", &a, &b); // must comma separate
printf("a=%d,b=%d,successfully assgin %d elements\n", a, b, n);
return 0;
}
编译运行:
:::success
b12@PC:~/chapter13$ gcc -Wall ./src/scanfFormat.c -o ./bin/scanfFormat
b12@PC:~/chapter13$ ./bin/scanfFormat
Please input two number(a b):6,9 # 以逗号间隔输入
a=6,b=9,successfully assgin 2 elements # 成功 2 个
b12@PC:~/chapter13$ ./bin/scanfFormat
Please input two number(a b):6 9 # 以空格间隔输入
a=6,b=-495391584,successfully assgin 1 elements # 成功 1 个
b12@PC:~/chapter13$ ./bin/scanfFormat
Please input two number(a b):6 # 想以回车输入两个,却不给我机会
a=6,b=-206766144,successfully assgin 1 elements # 成功 1 个
b12@PC:~/chapter13$ ./bin/scanfFormat
Please input two number(a b):6 9 # 以 \t 间隔输入
a=6,b=-844522608,successfully assgin 1 elements # 成功 1 个
b12@PC:~/chapter13$ ./bin/scanfFormat
Please input two number(a b):1, 666 # 以 “数字,” + 无限空格 + 数字输入
a=1,b=666,successfully assgin 2 elements # 成功 2 个
:::
如上,对于格式字符串中“原样字符”,在进行 IO
时,也必须一模一样输入,这是硬性要求,它不像格式字符 %d
可以过滤掉一些不满足的字符(一般都会从它们缓冲区扔出去)。
:::tips
在使用 scanf
时,建议使用空格间隔作为原样字符,在大多数情况下满足人们输入习惯。或者有必要提醒用户必须以什么样间隔方式输入等。但是现阶段基本很少人乐意在控制台黑框内输入,而习惯“哪里不会点哪里”。
必须明确 scanf
的巨坑:所有 IO
操作,都是你能“看得到”的字符 + “看不到”的(且是你最后按下回车 '\n'
)组成,即送到缓冲区内的字符是: ?\n
。那么这个 \n
就可能会做怪!!尤其是在使用 getchar()
或者 scanf("%c", &ch)
时,一定要明确调用函数之前,缓冲区内有没有“垃圾”!
:::
6、在用 scanf
函数向字符数组输入数据时,在数组名前面多加 &
。
错误示例:用 scanf
函数输入数据,赋值对象应该是(有效的)指针!
#include <stdio.h>
#define N 20
int main() {
char name[N];
printf("Please input your name(<=%d):", N - 1);
int n = scanf("%s", &name); // pointer to arr:char (*)[N]
printf("Hello %s, n = %d\n", name, n);
return 0;
}
编译运行:
:::warning
b12@PC:~/chapter13$ gcc -Wall ./src/scanfPtr.c -o ./bin/scanfPtr
./src/scanfPtr.c: In function ‘main’:
./src/scanfPtr.c:7:21: warning: format ‘%s’ expects argument of type ‘char ’, but argument 2 has type ‘char ()[20]’ [-Wformat=]
7 | int n = scanf(“%s”, &name); // pointer to arr: ()[N]
| ~^ ~
| | |
| | char ()[20]
| char
b12@PC:~/chapter13$ ./bin/scanfPtr
Please input your name(<=19):Bob
Hello Bob, n = 1
:::
如上,编译器发出警告,但是我们任然可以运行!似乎因为 scanf
传参存在隐式类型转换使得 `char ()[20](数组指针)传过去的转变为
char 的地址,也就是它们虽然是不同类型的指针,但是实际上存的值是相等的(好比二维数组
arr[2][3]的“行指针”
arr[0]和“列指针”
arr[0][0]),因此对原
name` 地址进行赋值成功。*但是非常不建议这样做!
上面程序代码任然不构健壮,因为存在用户输入过多字符导致数组溢出的风险!
:::danger
b12@PC:~/chapter13$ ./bin/scanfPtr
Please input your name(<=19):dassssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss
Hello dassssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss, n = 1
stack smashing detected : terminated
Aborted (core dumped)
:::
即 printf("%s")
不断超出数组容量范围去找到一个 \0
结束输出!很幸运的是本例它找到了并输出答案!但是这是非常危险的行为!栈溢出!
为了增强程序健壮性,可有两点供参考!使用格式控制 width
限制输入!或 fgets(buffer, N, stdin)
#include <stdio.h>
#define N 20
int main() {
char name[N];
printf("Please input your name(<=%d):", N - 1);
int n = scanf("%19s", name); // N-1 char + \0
printf("Hello %s, n = %d\n", name, n);
return 0;
}
编译运行:
:::success
b12@PC:~/chapter13$ gcc -Wall ./src/scanfPtr.c -o ./bin/scanfPtr
b12@PC:~/chapter13$ ./bin/scanfPtr
Please input your name(<=19):assdgafkasyqriyqowriyoqwtorqwtq
Hello assdgafkasyqriyqowr, n = 1
:::
#include <stdio.h>
#define N 20
int main() {
char name[N];
printf("Please input your name(<=%d):", N - 1);
fgets(name, N, stdin);
printf("Hello %s -end\n", name);
return 0;
}
编译运行:
:::success
b12@PC:~/chapter13$ gcc -Wall ./src/scanfPtr.c -o ./bin/scanfPtr
b12@PC:~/chapter13$ ./bin/scanfPtr
Please input your name(<=19):123456789012345678901 # total char: 21 + \n
Hello 1234567890123456789 -end
b12@PC:~/chapter13$ ./bin/scanfPtr
Please input your name(<=19):123456789012345678 # total char: 18 + \n
Hello 123456789012345678
-end
b12@PC:~/chapter13$ ./bin/scanfPtr # total char: 10 + \n
Please input your name(<=19):1234567890
Hello 1234567890
-end
:::
7、在用 scanf
函数向数值型数组输入数据时,用数值型数组名
错误示例:用 scanf
函数妄想一次性输入全部整型数组!
#include <stdio.h>
#define N 10
int main() {
int nums[N];
printf("Please input %d numbers:", N);
int n = scanf("%d", nums); // just assign nums[0]
printf("Successfully assign %d elements\n", n);
for (int i = 0; i < N; i++) {
printf("%d ", nums[i]);
}
printf("\n");
return 0;
}
编译运行:
:::success
b12@PC:~/chapter13$ gcc -Wall ./src/scanfNums.c -o ./bin/scanfNums
b12@PC:~/chapter13$ ./bin/scanfNums
Please input 10 numbers:1 2 3 4 5 6 7 8 9 10
Successfully assign 1 elements
1 32699 1304990304 32699 0 0 1304989888 32699 -198585648 32767
:::
为什么它不能和字符串数组一样使用 scanf("%s", str);
达到同样的效果呢?可以这样理解为何字符数组存在的意义:
每个字符串数组最后都有一个 \0
,这就表明在 printf
和 scanf
专门为人设计的字符格式输出 %s
能知道该字符串数组容量大小,即遇到 \0
就代表结束。简单理解就是字符串相当于在末尾标记结束,因此 for
循环就知道数组容量大小,所以“一般情况下”就不会非法访问。
如果使用 %s
来输入整型数组元素会发生什么??按一个字节赋值码?
:::warning
b12@PC:~/chapter13$ gcc -Wall ./src/scanfNums.c -o ./bin/scanfNums
./src/scanfNums.c: In function ‘main’:
./src/scanfNums.c:6:21: warning: format ‘%s’ expects argument of type ‘char ’, but argument 2 has type ‘int ’ [-Wformat=]
6 | int n = scanf(“%s”, nums); // just assign nums[0]
| ~^ ~~~~
| | |
| | int
| char
| %ls
b12@PC:~/chapter13$ ./bin/scanfNums
Please input 10 numbers:1 2 3 4 5 6 7 8 9 10 # 输入1:间隔空格输入
Successfully assign 1 elements
-51314639 32741 -51039648 32741 0 0 -51040064 32741 -47894176 32767
b12@PC:~/chapter13$ ./bin/scanfNums # 输入2:不间隔空格输入
Please input 10 numbers:123456789
Successfully assign 1 elements
875770417 943142453 10289209 32683 0 0 10313920 32683 -972457104 32767
b12@PC:~/chapter13$ ./bin/scanfNums # 输入3:测试 %s 按字节赋值(这里有溢出风险 \0
)
Please input 10 numbers:1111000011110000111100001111000011110000
Successfully assign 1 elements
825307441 808464432 825307441 808464432 825307441 808464432 825307441 808464432 825307441 808464432
b12@PC:~/chapter13$ ./bin/scanfNums # 输入4:比输入3 少一字节,测试 \0 也按字节赋值
Please input 10 numbers:111100001111000011110000111100001111000
Successfully assign 1 elements
825307441 808464432 825307441 808464432 825307441 808464432 825307441 808464432 825307441 3158064
:::
输入1:在空格后发生截断
-51314639 1111 1100 1111 0001 0000 0000 0011 0001
'1'(49) ---- ---- ---- ---- ---- ---- 0011 0001
' '(32) ---- ---- ---- ---- ---- ---- 0010 0000
输入2:按字符 ASCII 码连续赋值一个字节
875770417 0011 0100 0011 0011 0011 0010 0011 0001
'1'(49) ---- ---- ---- ---- ---- ---- 0011 0001
'2'(50) ---- ---- ---- ---- 0011 0010 ---- ----
'3'(51) ---- ---- 0011 0011 ---- ---- ---- ----
'4'(52) 0011 0100 ---- ---- ---- ---- ---- ----
943142453 0011 1000 0011 0111 0011 0110 0011 0101
'5'(53) ---- ---- ---- ---- ---- ---- 0011 0101
'6'(54) ---- ---- ---- ---- 0011 0110 ---- ----
'7'(55) ---- ---- 0011 0111 ---- ---- ---- ----
'8'(56) 0011 1000 ---- ---- ---- ---- ---- ----
输入3:验证按字符 ASCII 码连续赋值一个字节
875770417 0011 0001 0011 0001 0011 0001 0011 0001
'1'(49) 0011 0001|0011 0001|0011 0001|0011 0001
875770417 0011 0000 0011 0000 0011 0000 0011 0000
'0'(48) 0011 0000|0011 0000|0011 0000|0011 0000
输入4:%s 输入确实会在数组最后加入 \0 字符
3158064 0000 0000 0011 0000 0011 0000 0011 0000
'\0'(0) 0000 0000 ---- ---- ---- ---- ---- ----
'0'(48) |0011 0000|0011 0000|0011 0000
若同样实现 %d
输入整个数组,那么怎么知道这个数组究竟多大呢?到哪里结束才能保证有效?因此这么大的疑难问题还不如交给程序员本身去解决!(甩锅就完事了!)
正确赋值方式:循环给每个元素赋值
#include <stdio.h>
#define N 10
int main() {
int nums[N];
printf("Please input %d numbers:", N);
for (int i = 0; i < N; i++) {
scanf("%d", nums + i); // assign each elements
printf("%d ", nums[i]);
}
printf("\n");
return 0;
}
编译运行:
:::success
b12@PC:~/chapter13$ gcc -Wall ./src/scanfNums.c -o ./bin/scanfNums
b12@PC:~/chapter13$ ./bin/scanfNums
Please input 10 numbers:1 2 3 4 5 6 7 8 9 10 # 此处输入是在缓冲区,所以不会出现输入一个数马上输出
1 2 3 4 5 6 7 8 9 10
:::
8、语句后面漏分号
C 语言规定语句末尾必须有分号。分号是 C
语句不可缺少的一部分。这也是和其他语言不同的。有的初学者往往忘记写这一分号。
#include <stdio.h>
#define N 10
int main() {
int a, b;
a = 3 // forget the semicolon
b = 4;
printf("a:%d, b%d\n", a, b);
return 0;
}
编译运行:
:::danger
b12@PC:~/chapter13$ gcc -Wall ./src/semicolon.c -o ./bin/semicolon
./src/semicolon.c: In function ‘main’:
./src/semicolon.c:6:10: error: expected ‘;’ before ‘b’
6 | a = 3 // forget the semicolon
| ^
| ;
7 | b = 4;
| ~
:::
出错原因:在程序编译时,编译系统在“a = 3
”后面未发现分号,就接着检查下一行有无分号。b = 4
也作为上一行的语句的一部分,这就出现语法错误。
修改方法:在 a = 3
后面加上 ;
。
:::info
Q:为什么 C 语言要设计分号作为语句结束呢?为何 python 就是冒号等?
A:语句分隔符/终止符:可以一行写无数条语句,编译器把 ;
作为一条语句分隔(理论上一切 C 程序都可写成一行,但是可读性巨差!写注释也不会降低程序执行效率,但易于他人阅读)
参考帖子:A Brief History of the Semicolon in Programming
:::
9、把预处理指令当作 C 语句,在行末加了分号
错误示例:
#include <stdio.h>; // error
#define N 10
int main() {
printf("Good Morning!\n");
return 0;
}
编译运行:
:::danger
b12@PC:~/chapter13$ gcc -Wall ./src/includeTest.c -o ./bin/includeTest
./src/includeTest.c:1:19: warning: extra tokens at end of #include directive
1 | #include
| ^
:::
错误原因:#include<stdio.h>;
预处理指令(包括常用的 #include, #define
指令)不是 C 语句,在指令后面不应加分号。预处理指令不能直接被编译,必须先由预处理器对之加工处理,成为能被编译系统识别和编译的 C 程序,才真正进行编译。
10、在不该加分号的地方加了分号
错误示例:
#include <stdio.h>
int main() {
int a = 2, b = 1;
if (a > b); // 1
printf("a is great than b\n");
for (int i = 0; i < 10; i++); // 2
{
scanf("%d", &a);
printf("%d\n", a);
}
return 0;
}
编译运行:
:::warning
b12@PC:~/chapter13$ gcc -Wall ./src/semicolon.c -o ./bin/semicolon
./src/semicolon.c: In function ‘main’:
./src/semicolon.c:5:5: warning: this ‘if’ clause does not guard… [-Wmisleading-indentation]
5 | if (a > b); // 1
| ^~
./src/semicolon.c:6:9: note: …this statement, but the latter is misleadingly indented as if it were guarded by the ‘if’
6 | printf(“a is great than b\n”);
| ^~
./src/semicolon.c:7:5: warning: this ‘for’ clause does not guard… [-Wmisleading-indentation]
7 | for (int i = 0; i < 10; i++); // 2
| ^~~
./src/semicolon.c:8:5: note: …this statement, but the latter is misleadingly indented as if it were guarded by the ‘for’
8 | {
| ^
b12@PC:~/chapter13$ ./bin/semicolon
a is great than b
9 # 只允许输入一次
9
:::
结果不一致原因:
- 由于在
if (a > b)
后加了分号,if
语句到分号结束。即当为真时,执行一个空语句。因此后面的
printf
语句不再属于if
分支内而另起一句。所以无论如何都输出! for (int i = 0; i < 10; i++);
就是一个空循环语句,没有任何作用,{}
的复合语句不属于其分支内,因此待循环结束后才执行一次。 :::tips 现在编译器已经非常人性化,可以帮你检测到很多不规范的代码,效率大大提高! :::11、对应该有花括号的复合语句,忘记加花括号
错误示范:缺少花括号(while
只能跟一条语句)死循环! ```cinclude
int main() { int sum = 0, i = 1; while (i <= 100) // sum = sum + i; i++; printf(“sum:%d\n”, sum = sum + i); i++; return 0; }
**编译运行:**
:::warning
(base) b12@PC:~/chapter13$ gcc -Wall ./src/curly.c -o ./bin/curly<br />./src/curly.c: In function ‘main’:<br />./src/curly.c:5:5: warning: this ‘while’ clause does not guard... [-Wmisleading-indentation]<br /> 5 | while (i <= 100)<br /> | ^~~~~<br />./src/curly.c:7:44: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the ‘while’<br /> 7 | printf("sum:%d\n", sum = sum + i); i++;<br /> | ^<br />(base) b12@PC:~/chapter13$ ./bin/curly<br />sum:1<br />............<br />sum:9<br />............
:::
**错误原因**:可见是死循环,不断累加 `sum` ,但是 `i` 的值根本没有变啊!原因就是 `while,if,for` 等流程控制语句,可以不加 `{}` 而接一条语句(即认最近的一次 `;` ),因此出现上面 `i++;` 实际上是在循环体之外的语句。所以 `i = 1` 永远使循环体不断执行下去。<br />**正确做法**:用 `{}` 将内层两个语句包裹起来变成“一条”语句。
```c
#include <stdio.h>
int main() {
int sum = 0, i = 1;
while (i <= 100) {
sum = sum + i;
i++;
}
printf("sum:%d, i:%d\n", sum, i);
return 0;
}
编译运行:
:::success
(base) b12@PC:~/chapter13$ gcc -Wall ./src/curly.c -o ./bin/curly
(base) b12@PC:~/chapter13$ ./bin/curly
sum:5050, i:101
:::
Q:为什么
C
语言要设计花括号复合语句呢?为何python
就是依靠缩进呢? A:作为语句块存在,方便书写和阅读。但是带来一些作用域上(C 语言)和强制缩进(python 语言)问题! 参考帖子:A Brief History of the Curly Brace in Programming 总之代码可读性第一,不要追求行数过少,没有用{}
的习惯的人永远在debug
路上!千万别学书上的排版格式!!
12、括号不配对
错误示范:手误容易导致右括号漏写
#include <stdio.h>
int main() {
char ch;
while (ch = getchar() != '\n') {
putchar(ch);
}
putchar(ch); // newline
return 0;
}
编译运行:
:::success
(base) b12@PC:~/chapter13$ gcc -Wall ./src/missParentheses.c -o ./bin/missParentheses
./src/missParentheses.c: In function ‘main’:
./src/missParentheses.c:5:12: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
5 | while (ch = getchar() != ‘\n’) {
| ^~
(base) b12@PC:~/chapter13$ ./bin/missParentheses
abcdef
:::
为什么输出一堆不认识的东西?因为上面 ch = getchar() != '\n'
因为关系运算符比赋值运算符优先级高,即 ch = (getchar() != '\n')
,所以当输入 abcdef\n
后,前面的都被赋值为 1
,而 ASCII 码的 1
就是看不到的字符。这一点可以用 printf("%d\n", ch);
验证。
因此我们就需要改变括号优先级,使得 ch
先被赋值,然后再判断 \n
。那么就要利用赋值表达式进行。
#include <stdio.h>
int main() {
char ch;
while ((ch = getchar() != '\n') { // miss right parenthese
putchar(ch);
}
putchar(ch); // newline
return 0;
}
编译运行:
:::danger
(base) b12@PC:~/chapter13$ gcc -Wall ./src/missParentheses.c -o ./bin/missParentheses
./src/missParentheses.c: In function ‘main’:
./src/missParentheses.c:5:36: error: expected ‘)’ before ‘{’ token
5 | while ((ch = getchar() != ‘\n’) {
| ~ ^~
| )
./src/missParentheses.c:10:1: error: expected expression before ‘}’ token
10 | }
| ^
:::
因此特别是这种括号嵌套括号的,非常容易漏掉右括号。不过现在的编译器都能敏锐地察觉到大部分错误。从而提高效率。
#include <stdio.h>
int main() {
char ch;
while ((ch = getchar()) != '\n') {
putchar(ch);
}
putchar(ch); // newline
return 0;
}
编译运行:
:::success
(base) b12@PC:~/chapter13$ gcc -Wall ./src/missParentheses.c -o ./bin/missParentheses
(base) b12@PC:~/chapter13$ ./bin/missParentheses
123456
123456
:::
13、在用标识符时,混淆了大写字母和小写字母的区别。
错误示范:Case Sensitive
#include <stdio.h>
int main() {
char ch;
while (ch = getchar() != '\n') {
putchar(ch);
}
putchar(ch); // newline
return 0;
}
编译运行:
:::danger
(base) b12@PC:~/chapter13$ gcc -Wall ./src/caseSensitive.c -o ./bin/caseSensitive
./src/caseSensitive.c: In function ‘main’:
./src/caseSensitive.c:5:5: error: ‘a’ undeclared (first use in this function)
5 | a = 2;
| ^
./src/caseSensitive.c:5:5: note: each undeclared identifier is reported only once for each function it appears in
./src/caseSensitive.c:6:5: error: ‘b’ undeclared (first use in this function)
6 | b = 3;
| ^
./src/caseSensitive.c:7:5: error: ‘c’ undeclared (first use in this function)
7 | c = a + b;
| ^
./src/caseSensitive.c:4:15: warning: unused variable ‘C’ [-Wunused-variable]
4 | int A, B, C;
| ^
./src/caseSensitive.c:4:12: warning: unused variable ‘B’ [-Wunused-variable]
4 | int A, B, C;
| ^
./src/caseSensitive.c:4:9: warning: unused variable ‘A’ [-Wunused-variable]
4 | int A, B, C;
| ^
:::
错误原因:C 语言是大小写敏感的语言,即不同标识符大小写不一样就是不同的。因此出现上面变量 a, b, c
未定义。修改方式就是统一改为小写或者大写。
#include <stdio.h>
int main() {
int a, b, c;
a = 2;
b = 3;
c = a + b;
printf("%d + %d = %d\n", a, b, c);
return 0;
}
编译运行:
:::success
(base) b12@PC:~/chapter13$ gcc -Wall ./src/caseSensitive.c -o ./bin/caseSensitive
(base) b12@PC:~/chapter13$ ./bin/caseSensitive
2 + 3 = 5
:::
14、误把“=”作为“等于”运算符
错误示范:常见语法错误,把赋值运算符认为是关系运算符中的判断相等与赋值表达式的锅
#include <stdio.h>
int main() {
int riddle = 6, guess;
printf("Please guess a number:");
scanf("%d", &guess);
if (guess = riddle) { // error
printf("Congratulations!\n");
} else {
printf("Sorry, you fail.\n");
}
return 0;
}
编译运行:
:::warning
(base) b12@PC:~/chapter13$ gcc -Wall ./src/equalSymbol.c -o ./bin/equalSymbol
./src/equalSymbol.c: In function ‘main’:
./src/equalSymbol.c:7:9: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
7 | if (guess = riddle) {
| ^~~~~
(base) b12@PC:~/chapter13$ ./bin/equalSymbol
Please guess a number:6
Congratulations!
(base) b12@PC:~/chapter13$ ./bin/equalSymbol
Please guess a number:-9 # why?
Congratulations!
:::
与预期结果不一致原因:如上编译器警告,建议在 if ()
中使用真值进行,即任何“非空”都是 1
即为真,否则都是 0
即为假。但是这个警告也和上面出错的真正原因不一致。其主要原因是如下两点
- 程序设计语言中的
=
和数学上的等式的=
是不一致的。这一点通常在学习初期容易犯错。- 程序语言中的判断相等确是
==
(有的语言还花哨,比如 JavaScript 中的==
会发生隐式类型转换导致不一定完全相等,而===
才是严格的相等)。 - 而
=
在大多数程序语言中都是赋值(R 语言中还有<-
让人直呼内行),等号左边叫lvalue
必须是一个 可写的内存空间,右侧交rvalue
,没有太大要求,只要能给出值就行。
- 程序语言中的判断相等确是
C 语言中
=
不仅仅是语法要求,它是一个运算符(其它语言里如 Python 中它不是运算符!),这就导致了一些垃圾代码while ((ch = getchar()) != '\n');
的出现,虽然方便,但是可读性差!简单说就是=
组成了赋值表达式,对变量进行赋值后返回表达式的结果,即变量最终被赋上的值。 :::tips tips:学 C 的人都有一个习惯,就是if (condition)
表达式书写时,常量判断值永远写在==
左侧。比如if (6 == a)
。它有如下好处:若不留神写成
if (6 = a)
,根据赋值运算符的要求,=
左侧必须是左值,但是现在6
是常量,编译器里面给出警告!error: lvalue required as left operand of assignment- 如果写成上面
if (a = 6)
,够你 DEBUG 半年才发现这个弱智错误。不管怎么样写,只要你自己知道程序运行流程都行,只是上面书写方式就能让别人看出你是深受毒害的人,😝 :::15、引用数组元素时误用了圆括号
错误示范:语法问题 ```cinclude
define N 5
int main() { int arr[N]; printf(“Please input %d number:”, N); for (int i = 0; i < N; i++) { scanf(“%d”, &arr(i)); } printf(“You have input following numbers:\n”); for (int i = 0; i < N; i++) { printf(“%d “, arr[i]); } printf(“\n”); return 0; }
**编译运行**:
:::success
b12@PC:~/chapter13$ gcc -Wall ./src/arrayReference.c -o ./bin/arrayReference<br />./src/arrayReference.c: In function ‘main’:<br />./src/arrayReference.c:8:22: error: called object ‘arr’ is not a function or function pointer<br /> 8 | scanf("%d", &arr(i));<br /> | ^~~<br />./src/arrayReference.c:5:9: note: declared here<br /> 5 | int arr[N];<br /> | ^~~
:::
**错误原因**:数组引用必须是 `[]` ,这是语法规定;而 `()` 几乎都是函数调用。
```c
#include <stdio.h>
#define N 5
int main() {
int arr[N];
printf("Please input %d number:", N);
for (int i = 0; i < N; i++) {
scanf("%d", &arr[i]); // 1. & + [] = pointer
// scanf("%d", arr + i); // 2. arr + i = pointer
}
printf("You have input following numbers:\n");
for (int i = 0; i < N; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
编译运行:
:::success
(base) b12@PC:~/chapter13$ gcc -Wall ./src/arrayReference.c -o ./bin/arrayReference
(base) b12@PC:~/chapter13$ ./bin/arrayReference
Please input 5 number:5 4 3 2 1
You have input following numbers:
5 4 3 2 1
:::
总结:在数组中 &
和 []
可以认为是正负抵消(去引用和引用关系)的东西,熟悉指针的都知道,数组名就是第一元素的地址,但是它不等于指针变量。一定要区分数组名,指针(地址)和指针变量(就是变量)的区别。
16、在定义数组时,将定义的“元素个数”误认为是“可使用的最大下标值”
错误示例:超界访问
#include <stdio.h>
int main () {
int nums[10] = {1, 2, 3, 4};
for (int i = 0; i <= 10; i++) {
printf("%d ", nums[i]);
}
return 0;
}
编译运行:编译采用内存检测工具参数:Address Sanitizer
(base) b12@PC:~/chapter13$ gcc ./src/arrayReference.c -o ./bin/arrayReference
(base) b12@PC:~/chapter13$ ./bin/arrayReference
=================================================================
==172==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffff77c98e8 at pc 0x7f3f6abaa49f bp 0x7ffff77c9870 sp 0x7ffff77c9860
READ of size 4 at 0x7ffff77c98e8 thread T0
#0 0x7f3f6abaa49e in main src/arrayReference.c:6
#1 0x7f3f69f470b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)
#2 0x7f3f6abaa1ad in _start (/home/b12/chapter13/bin/arrayReference+0x11ad)
Address 0x7ffff77c98e8 is located in stack of thread T0 at offset 88 in frame
#0 0x7f3f6abaa278 in main src/arrayReference.c:3
This frame has 1 object(s):
[48, 88) 'nums' (line 4) <== Memory access at offset 88 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow src/arrayReference.c:6 in main
Shadow bytes around the buggy address:
0x10007eef12c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10007eef12d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10007eef12e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10007eef12f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10007eef1300: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x10007eef1310: 00 00 f1 f1 f1 f1 f1 f1 00 00 00 00 00[f3]f3 f3
0x10007eef1320: f3 f3 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10007eef1330: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10007eef1340: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10007eef1350: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10007eef1360: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==172==ABORTING
错误原因:虽然也看不太明白上面具体意思,只知道它栈溢出,即本题中的数组访问越界了(编译器越来越智能!再也不用肉眼 DEBUG)。C 语言中规定在定义数组时用 nums[10]
,表示 nums
数组有 10
个元素,而不是可以用的最大下标值为 10
。在 C 语言中数组的下标是从 0
开始的,因此,数组 nums
只包括 nums[0]-nums[9]
这 10
个元素,想引用 nums[10]
,就超界了。
:::info
Q:为什么很多程序都是以 0
作为索引起点?
A:C 语言中是具体而言是 offset
,而不是索引,因为数组名就是第一个元素的地址,所以以 0
为起点使得 offset=0
即本身。
具体参考:
- Why Do Arrays Start With Index 0?
- Why numbering should start at zero
:::
17、对二维或多维数组的定义和引用的方法不对
错误示例:语法错误 ```cinclude
int main () { int nums[3, 4] = {{9, 8, 7, 6}}; return 0; }
**编译运行**:编译采用内存检测工具参数:Address Sanitizer
:::danger
(base) b12@PC:~/chapter13$ gcc ./src/2DarrayReference.c -o ./bin/2DarrayReference<br />./src/2DarrayReference.c: In function ‘main’:<br />./src/2DarrayReference.c:4:12: error: expected ‘]’ before ‘,’ token<br /> 4 | int nums[3, 4] = {{9, 8, 7, 6}};<br /> | ^<br /> | ]
:::
**错误原因**:数学上的矩阵和 C 语言有很大不同,在 C 语言中要遵从语法规则,使用方括号 `[]` 作为维度的区分,并且要求是常量大写。如上应该改为 `int nums[3][4]` 。
错误示范 2:二维数组索引错误
```c
#include <stdio.h>
int main () {
int nums[3][4] = {{9, 8, 7, 6}};
printf("%d\n", nums[1,3+1]);
return 0;
}
编译运行:
:::warning
(base) b12@PC:~/chapter13$ gcc ./src/2DarrayReference.c -o ./bin/2DarrayReference
./src/2DarrayReference.c: In function ‘main’:
./src/2DarrayReference.c:5:23: warning: left-hand operand of comma expression has no effect [-Wunused-value]
5 | printf(“%d\n”, nums[1,3+1]);
| ^
./src/2DarrayReference.c:5:11: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘int ’ [-Wformat=]
5 | printf(“%d\n”, nums[1,3+1]);
| ~^ ~~~
| | |
| int int
| %ls
(base) b12@PC:~/chapter13$ ./bin/2DarrayReference
-171803536
:::
错误原因:上面明显解释为 ,
运算符,因此最终返回的是 nums[4]
(这里没有报内存错误!)返回的是一个指针,与打印结果不同。因此对多维数组的引用也不能用 nums[1,3+1]
。计算机是通过机械工程实现的,不是数学上的抽象概念,因此 offset
就好比物理上的位置关系,多维数组都是线性的,只能一个维度一个维度来,即 nums[1][4]
。
18、误以为数组名代表数组中全部元素
错误示范:企图通过“变量赋值方式”对整个数组赋值元素
#include <stdio.h>
int main () {
int nums1[4] = {1, 2, 3, 4}; // initial
int nums2[4];
nums2 = nums1; // Error
for (int i = 0; i < 4; i++) {
printf("%d ", nums2[i]);
}
return 0;
}
编译运行:
:::danger
(base) b12@PC:~/chapter13$ gcc ./src/arrayAssignment.c -o ./bin/arrayAssignment
./src/arrayAssignment.c: In function ‘main’:
./src/arrayAssignment.c:6:8: error: assignment to expression with array type
6 | nums2 = nums1; // Error
| ^
:::
错误原因:数组不支持这种直接赋值做法,这一点一定要和数组“定义式初始化”划分界限。
- 定义式初始化:只要出现
int [] = {}
类似形式,所有数组内的元素都挨个把右侧{}
内的元素填入其内。若有空缺,一律填0
。(非常容易理解,不就是列举法) - 为什么数组不可以通过此种方式赋值?
int main () { printf(“Please input your score:”); int score; // 66.5 也是 60-70 区间 scanf(“%d”, &score); switch (score / 10) { case 10: case 9: printf(“A\n”); case 8: printf(“B\n”); case 7: printf(“C\n”); case 6: printf(“D\n”); default: printf(“Fail!\n”); } return 0; }
**编译运行**:
:::success
(base) b12@PC:~/chapter13$ gcc ./src/switchbreak.c -o ./bin/switchbreak<br />(base) b12@PC:~/chapter13$ ./bin/switchbreak<br />Please input your score:100<br />A<br />B<br />C<br />D<br />Fail!
:::
与预期不同的原因: `switch-case` 就是一个下巴漏的东西,或者说特别贪,想通吃。如果某个 `case` 不进行 `break` ,那么它一直进行下去,因此出现“一漏到底”的输出。<br />**解决办法**:在每个满足条件的 `case` 后面自动加上 `break` ,最后一个 `case` 或者 `default` 可加可不加,因为没有下一个了。
```c
#include <stdio.h>
int main () {
printf("Please input your score:");
int score; // 66.5 也是 60-70 区间
scanf("%d", &score);
switch (score / 10) {
case 10: case 9:
printf("A\n");
break;
case 8:
printf("B\n");
break;
case 7:
printf("C\n");
break;
case 6:
printf("D\n");
break;
default:
printf("Fail!\n");
}
return 0;
}
编译运行:
:::success
(base) b12@PC:~/chapter13$ gcc ./src/switchbreak.c -o ./bin/switchbreak
(base) b12@PC:~/chapter13$ ./bin/switchbreak
Please input your score:100
A
(base) b12@PC:~/chapter13$ ./bin/switchbreak
Please input your score:60
D
(base) b12@PC:~/chapter13$ ./bin/switchbreak
Please input your score:4
Fail!
:::
拓展延申:
其实这个 switch-case
设计的很怪,完全可以用 if-else
替代。尤其是 default
放置的位置和 break
容易漏写等原因!
#include <stdio.h>
int main () {
printf("Please input your score:");
int score; // 66.5 也是 60-70 区间
scanf("%d", &score);
switch (score / 10) {
default:
printf("Fail!\n");
// break;
case 10: case 9:
printf("A\n");
break;
case 8:
printf("B\n");
break;
case 7:
printf("C\n");
break;
case 6:
printf("D\n");
}
return 0;
}
编译运行:
:::success
(base) b12@PC:~/chapter13$ gcc ./src/switchbreak.c -o ./bin/switchbreak
(base) b12@PC:~/chapter13$ ./bin/switchbreak
Please input your score:55
Fail!
A # 为什么没跳出?
(base) b12@PC:~/chapter13$ ./bin/switchbreak
Please input your score:66
D
:::
上面的案例说明
- 最后的
case
即使不加break
也是见底了,如果满足就会跳出switch-case
控制体 - 其实
default
是最终所有case
都没有出口后再执行。其本质上也是一个“万能条件”的case
,依然准从“往下漏”的原则,如上不break
,依然输出A
。如果再其后添加break
就不会输出A
其次 switch-case
语句和之前循环语句和 if
语句不太相同地方就是 case
后面不管你多少条语句都可以!为什么有这个区别呢?我认为因为 case
不能单独存在且其一定在 switch {case 1:... case 2: ..}
内,因此只需要检测 {}
内相邻 case
就知道某个 case
分支语句数量多少了。