思维导图
一、注释、空白与代码块
1.1 注释的本质与艺术
两种形式详解:
1. 单行注释
语法: //
行为: 从双斜杠开始,直到该行结束的所有字符都被忽略。
适用场景: 简短的变量说明、逻辑解释或暂时屏蔽某行代码。
代码示例:
int salary = 5000; // 定义基本工资,单位:元
// int bonus = 200; // 暂时注释掉奖金,下个月再发
2. 多行注释
语法: /* ... */
行为: 编译器一旦遇到 /*,就会贪婪地寻找最近的 */ 来结束注释,中间的所有换行符、代码都会被吞噬。
致命陷阱:不支持嵌套!
许多初学者试图用 /* */ 去注释掉包含现有注释的一大段代码,这会导致编译错误。
错误示例:
/*
开始注释一段逻辑
int a = 10; /* 这里有一个内层注释 */
// 上面的 */ 会与第一行的 /* 匹配,导致这里变成代码,引发报错
int b = 20;
*/
正确做法: 使用预处理指令 #if 0 ... #endif 来屏蔽大段代码。
1.2 空白的解析逻辑
核心概念: 在 C 语言中,空格、制表符、换行符 统称为空白符。 编译器在进行词法分析时,通常将连续的多个空白符视为一个分隔符。这意味着代码的排版完全是为了服务于人类的阅读体验,而非机器。
代码对比: 写法 A (机器视角,合法但难以阅读):
int main(){int x=1;if(x>0){printf("Hi");}return 0;}
写法 B (人类视角,推荐):
int main() {
int x = 1;
if (x > 0) {
printf("Hi");
}
return 0;
}
解析: 写法 A 和 B 编译后的机器码完全一致。但写法 B 遵循了缩进规范,清晰展示了层级结构。
1.3 代码块与作用域
核心概念:
代码块是由一对花括号 { } 包围的语句集合。它在逻辑上扮演了“单一语句”的角色。
内存视角:
代码块不仅仅是把代码包起来,它定义了变量的生命周期 和 可见性。在块内定义的变量(局部变量),通常存储在栈 (Stack) 上。当程序执行流离开右花括号 } 时,这些变量会被自动销毁,内存被回收。
代码示例:
#include <stdio.h>
int main() {
int x = 100; // main 函数作用域
{ // 开启一个新的内层代码块
int y = 50; // y 仅在此块内存活
printf("块内: x=%d, y=%d\n", x, y); // x 在此处依然可见
} // 此时,变量 y 被销毁
// printf("%d", y); // 错误!y 已经不存在了
printf("块外: x=%d\n", x);
return 0;
}
二、变量与常量
2.1 变量声明、定义与初始化
核心概念: 变量是内存中一段已命名的存储空间。
未初始化的危险
如果定义局部变量时不初始化,它的值是什么? 答案是:垃圾值。这块内存之前被谁用过,留下了什么数据,现在就是什么。直接使用未初始化的变量是 C 语言中最常见的 Bug 来源之一。
代码示例:
void variable_demo() {
int a; // 定义了变量 a,但没给初值,a 的值是随机的(危险!)
int b = 10; // 定义并初始化(安全)
int c, d = 5; // 注意:只有 d 被初始化为 5,c 依然是随机值
a = 20; // 赋值操作:覆盖原有的内存内容
printf("a = %d, b = %d\n", a, b);
}
2.2 变量命名规则
只能包含字母、数字、下划线。
不能以数字开头(编译器解析词法时会混淆数字常量和标识符)。
区分大小写:Score 和 score 是两个完全不同的变量。
不能是关键字:如 int, if, return 等。
2.3 const 的语义与最佳实践
核心概念:
const 关键字用于修饰变量,将其转变为“只读变量”。
vs #define (宏定义):
#define MAX 100:预处理阶段的文本替换。没有类型检查,调试困难,容易产生副作用。const int MAX = 100;:编译阶段的变量。有明确的类型 (int),编译器会检查类型匹配,调试器能显示其值。 最佳实践: 现代 C 语言开发中,优先推荐使用const替代简单的数值宏。
代码示例:
void const_demo() {
const double PI = 3.1415926; // 定义只读变量
double radius = 5.0;
// PI = 3.14; // 编译错误!试图修改只读变量
double area = PI * radius * radius;
printf("面积: %f\n", area);
}
三、基本运算符
3.1 算术运算符
涉及符号:+, -, *, /, %
难点剖析 1:整数除法
在 C 语言中,如果除号 / 两边都是整数,执行的是整除。结果的小数部分会被直接丢弃(截断),而不是四舍五入。
代码示例:
int a = 5;
int b = 2;
double result1 = a / b; // 结果是 2.000。原因:5/2 算出整数 2,再转为 double
double result2 = a / 2.0; // 结果是 2.500。原因:2.0 是浮点数,触发隐式转换
难点剖析 2:取模运算
% 运算求余数。
限制: 操作数必须是整数。5.0 % 2 会导致编译错误。
符号规则: 结果的符号通常与被除数(左边的数)一致。
代码示例:
printf("%d\n", 5 % 2); // 输出 1
printf("%d\n", -5 % 2); // 输出 -1
printf("%d\n", 5 % -2); // 输出 1
3.2 关系与逻辑运算符
真与假的本质:
非 0 值 为 真 True 0 值 为 假 False
关系运算的结果一定是整数 1 (真) 或 0 (假)。
短路求值—— 重要特性
逻辑与 &&:Expr1 && Expr2。若 Expr1 为假,Expr2 完全不会执行。
逻辑或 ||:Expr1 || Expr2。若 Expr1 为真,Expr2 完全不会执行。
代码示例 (利用短路特性防止崩溃):
int x = 0;
// 如果不利用短路,10/x 会导致除零错误 (Crash)
// 但因为 x!=0 为假,后面的除法被安全跳过
if (x != 0 && (10 / x) > 1) {
printf("Valid");
} else {
printf("Skipped calculation");
}
2.3.3 自增自减与常见坑
前缀 vs 后缀:
++i (前缀):先自增,再取值。口诀:先己后人。
i++ (后缀):先取值,再自增。口诀:先人后己。
代码示例:
int i = 10;
int a = ++i; // i 变为 11,a 得到 11
int b = i++; // b 得到 11,i 随后变为 12
printf("i=%d, a=%d, b=%d", i, a, b);
高危警告:不要在同一表达式中多次修改同一变量!
诸如 i = i++ + ++i; 这样的代码属于未定义行为。不同的编译器、不同的优化等级会产生完全不同的结果。严禁编写此类代码。
四、表达式与语句
4.1 表达式语句
核心概念:
表达式是计算值的公式,如 a + 3。
语句是执行动作的指令,通常以分号 ; 结尾。
任何表达式后面加上分号,就构成了一条语句。
代码示例:
3 + 4; // 合法语句,计算了 7,但随即丢弃,无副作用(无意义)
i++; // 常用语句,利用了自增的副作用
x = y + 5; // 赋值语句
; // 空语句 (Null Statement)
4.2 复合语句
即前文提到的代码块 { ... }。
在 if、while 等控制结构中,如果想执行多条指令,必须使用复合语句。
常见错误示例:
if (score > 60)
printf("通过!\n");
printf("恭喜!\n"); // 缩进误导!这行代码其实在 if 外面,无论是否及格都会打印
修正: 加上 { }。
五、输入输出基础
5.1 printf 格式化输出
语法: printf("格式控制串", 参数列表...);
常用占位符:
%d: 十进制有符号整数 (int)%u: 十进制无符号整数 (unsigned int)%f: 浮点数 (float/double) - 默认保留6位小数%.2f: 浮点数,强制保留2位小数%c: 单个字符%s: 字符串
5.2 scanf 格式化输入与缓冲区陷阱
基本用法:
scanf 从标准输入流 (stdin) 读取数据。
关键点:取地址符 &
读取基本类型变量时,必须告诉 scanf 变量存储在哪里,所以要加 &。
int age;
scanf("%d", &age); // 正确
// scanf("%d", age); // 错误!会导致程序崩溃
深入理解:缓冲区与换行符残留
这是初学者最容易崩溃的地方。
当用户输入 100 并按下回车时,输入流中实际包含:1 0 0 \n (换行符)。
1.
scanf("%d", &num)读走了100。 2.\n仍然停留在缓冲区中。
- 如果下一行代码是
scanf("%c", &ch),它意图读取一个字符。它会立即读取残留的\n,而不会等待用户输入新的字符。
代码复现与解决方案:
int num;
char ch;
printf("请输入数字: ");
scanf("%d", &num);
printf("请输入字符: ");
// 错误写法:scanf("%c", &ch); // 会直接读到上次的回车
// 正确写法 1:在格式串前加空格,指示跳过所有空白符(含换行)
scanf(" %c", &ch);
// 正确写法 2:手动吃掉缓冲区里的换行符
// getchar();
// scanf("%c", &ch);
printf("数字: %d, 字符: %d (ASCII)\n", num, ch);
返回值检查:
scanf 返回成功匹配并赋值的参数个数。
if (scanf("%d", &num) != 1) {
printf("错误:您输入的不是数字!\n");
}
六、 练习题
题目 1: 下面代码能否通过编译?
int /* 注释1 */ a = 10;
int b = 20; // 注释2 \
这种续行注释写法合法吗?
题目 2: 选出所有非法的变量名:
A. _sys_val B. 2Pack C. sizeof D. Int E. total-sum
题目 3: 下列代码输出什么?(假设编译器不优化)
void func() {
int x;
int y = x + 10;
printf("%d", y);
}
题目 4: 指出错误行。
const int LIMIT = 50; // Line 1
int arr[LIMIT]; // Line 2 (在 C89 标准下)
LIMIT = 60; // Line 3
题目 5: 表达式 (double)(10 / 4) 的值是多少?
A. 2.5
B. 2.0
C. 2
D. 0
题目 6: 10 % 3,10 % -3,-10 % 3 的结果分别是多少?
题目 7: 分析输出结果。
int a = 5;
int b = a++ + 10;
printf("a=%d, b=%d", a, b);
题目 8: 分析输出结果。
int a = 5;
int b = ++a + 10;
printf("a=%d, b=%d", a, b);
题目 9: C 语言中,表达式 5 和表达式 !!5 的逻辑真假性是否一致?值是否一致?
题目 10: 下面代码执行后,x 的值是多少?
int a = 0;
int x = 10;
if (a != 0 && (x++) > 10) {
// do nothing
}
题目 11: 下面代码会打印什么?
int a = 5;
if (a = 0) {
printf("True");
} else {
printf("False");
}
题目 12: printf("%.2f", 3.14159); 的输出是什么?
题目 13: 要读取一个 double 类型的变量 d,格式控制符应该用什么?
题目 14: 连续执行 scanf("%d", &a); scanf("%d", &b);。如果用户输入 10 20(中间有空格)并回车,程序能正确读到两个数吗?
题目 15: ;; 是合法的 C 语言代码吗?
七、 解析
题 1 解析 答案: 合法。
题 2 解析 答案: B, C, E。 详解:
B (
2Pack): 不能以数字开头。 C (sizeof): 是 C 语言的关键字(运算符)。 E (total-sum): 减号-是运算符,不能出现在变量名中,应使用下划线total_sum。 D (Int): 合法,因为 C 区分大小写,int是关键字,但Int只是普通标识符(虽然不推荐这么命名)。
题 3 解析 答案: 垃圾值(不可预测)。 详解:
局部变量
x未初始化,其内存中的值是随机的。x + 10也是随机的。在某些严格的安全检查模式下,程序可能会直接报错崩溃。
题 4 解析 答案: Line 3 错误。Line 2 在 C89 错误,C99 正确。 详解:
Line 3:
LIMIT是const只读变量,不能被赋值。 Line 2: 在旧标准 C89 中,数组大小必须是字面常量(#define或直接写数字),const变量不行。但在 C99 引入变长数组 (VLA) 后,Line 2 变得合法。
题 5 解析 答案: B (2.0) 详解:
这是一个极易犯错的点。括号内的优先级最高,先计算
10 / 4。因为是两个整数相除,结果截断为整数2。然后(double)将整数2转换为浮点数2.0。如果想得到 2.5,必须写成10.0 / 4。
题 6 解析 答案: 1, 1, -1。 详解:
10 % 3 = 110 % -3 = 1-10 % 3 = -1在 C 语言中,取模结果的符号与左操作数(被除数)相同。
题 7 解析 答案: a=6, b=15。 详解:
a++是后缀自增。计算b时,使用a的旧值 (5),所以b = 5 + 10 = 15。表达式结束后,a自增变为 6。
题 8 解析 答案: a=6, b=16。 详解:
++a是前缀自增。先将a加 1 变为 6。然后参与计算b = 6 + 10 = 16。
题 9 解析 答案: 真假性一致(都是真),但数值不同(5 和 1)。 详解:
5是非零值,逻辑为真。!5变为0(假)。!!5变为1(真)。 注意:所有逻辑运算符的结果只有 0 或 1。
题 10 解析 答案: x = 10。 详解:
考察短路求值。表达式
a != 0为假。逻辑与&&只要左边为假,右边(x++) > 10就被完全跳过,根本不会执行。所以x没有发生自增。
题 11 解析 答案: False。 详解:
if (a = 0)中是一个赋值表达式,不是==。 1.a被赋值为 0。 2.表达式的值即为 0。 3.if(0)判定为假。
题 12 解析 答案: 3.14。 详解:
%.2f指示四舍五入保留两位小数。
题 13 解析
答案: %lf (long float)。
详解:
printf:%f可以同时打印 float 和 double(因为 float 会被提升为 double)。scanf: 非常严格。float必须用%f,double必须用%lf。用错会导致数据错乱。
题 14 解析 答案: 能。 详解:
scanf的%d格式符会自动跳过所有的空白字符(空格、Tab、换行符),直到找到数字为止。所以输入中的空格不会造成阻碍,反而充当了分隔符。
题 15 解析 答案: 合法。 详解:
这是两条空语句。虽然什么都不做,但在语法上完全正确。常用于占位,例如
while(wait_for_flag()) ;(空循环体)。