二、C语言基本语法与程序结构

0 阅读12分钟

思维导图

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

一、注释、空白与代码块

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 变量命名规则

只能包含字母、数字、下划线。 不能以数字开头(编译器解析词法时会混淆数字常量和标识符)。 区分大小写Scorescore 是两个完全不同的变量。 不能是关键字:如 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 复合语句

即前文提到的代码块 { ... }。 在 ifwhile 等控制结构中,如果想执行多条指令,必须使用复合语句。

常见错误示例:

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 仍然停留在缓冲区中。

  1. 如果下一行代码是 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 % 310 % -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: LIMITconst 只读变量,不能被赋值。 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 = 1 10 % -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 必须用 %fdouble 必须用 %lf。用错会导致数据错乱。

题 14 解析 答案: 能。 详解:

scanf%d 格式符会自动跳过所有的空白字符(空格、Tab、换行符),直到找到数字为止。所以输入中的空格不会造成阻碍,反而充当了分隔符。

题 15 解析 答案: 合法。 详解:

这是两条空语句。虽然什么都不做,但在语法上完全正确。常用于占位,例如 while(wait_for_flag()) ;(空循环体)。