文章目录


前言

C语⾔是结构化的程序设计语⾔,这⾥的结构指的是顺序结构、选择结构、循环结构,C语⾔是能够实现这三种结构的,其实我们如果仔细分析,我们⽇常所⻅的事情都可以拆分为这三种结构或者这三种结构的组合。
我们可以使⽤ if 、 switch 实现分⽀结构,使⽤ for 、 while 、 do while 实现循环结构。


一、循环与分支的意义

在C语言中,循环和分支语句是程序控制流的核心组成部分,它们赋予了程序决策能力和重复执行能力,使程序不再只是简单地顺序执行。
几乎所有算法(排序、搜索、遍历等)都依赖于循环和分支语句的实现。
没有循环和分支,程序将只是简单的指令序列,无法应对复杂的现实需求。 它们是使程序真正"活"起来的关键要素。

二、循环与分支的介绍

1.if语句

1.1 if

if 语句的语法形式如下:

if ( 表达式 )
 语句

在C语⾔中,0为假,⾮0表⽰真,也就是表达式的结果如果是0,则语句不执⾏,表达式的结果如果不
是0,则语句执⾏。
例⼦:输⼊⼀个整数,判断是否为奇数

#include <stdio.h>
int main()
{
 int num = 0;
 scanf("%d", &num);
 if(num % 2 == 1)
 printf("%d 是奇数\n", num);
 return 0;
}

流程图如下:

开始 ↓
计算条件表达式 ↓
条件为真? ──是──> 执行if代码块
↓否
执行else代码块(如果有) ↓
结束

1.2 else

如果⼀个数不是奇数,那就是偶数了,如果任意⼀个整数,我们要清楚的判断是奇数还是偶数怎么表
⽰呢?
这⾥就需要 if…else… 语句了,语法形式如下:

if ( 表达式 )
    语句1
else
	语句2

例⼦:输⼊⼀个整数,判断是否为奇数,如果是奇数打印是奇数,否则打印偶数。

#include <stdio.h>
int main()
{
 int num = 0;
 scanf("%d", &num);
 if(num % 2 == 1)
 printf("%d 是奇数\n", num);
 else
 printf("%d 是偶数\n", num);
 return 0;
}

1.3 分支中包含多条语句

默认在 if 和 else 语句中默认都只控制⼀条语句,⽐如:

#include <stdio.h>
int main()
{
 int age = 0;
 scanf("%d", &age);
 if(age >= 18)
 	printf("成年了\n");
	printf("应该独立了\n");
 return 0;
}

上⾯的代码,你会发现输⼊的值不管是>=18 还是<18,“应该独立了” 都会打印在屏幕上。
这是因为 if 语句只能控制⼀条语句,就是 printf(“成年了\n”); ,if语句为真,则打印成年了,
if语句为假,则不打印,对于 printf(“可以谈恋爱了\n”); 是独⽴存在的,不管if语句的条件的真
假,都会被执⾏。那如果我们要if语句同时控制2条语句,怎么办呢?那就要使⽤ {} 将代码括起来,
else 后也可以跟上⼤括号。如下:

#include <stdio.h>
int main()
{
 int age = 0;
 scanf("%d", &age);
 if(age >= 18) //if 后使⽤{} 控制多条语句-这个块也叫:程序块,或者复合语句
 {
 printf("成年了\n");
 printf("应该独立了\n");
 }
 return 0;
}
#include <stdio.h>
int main()
{
 int age = 0;
 scanf("%d", &age);
 if(age >= 18) //if 后使⽤{} 控制多条语句-这个块也叫:程序块,或者复合语句
 {
printf("成年了\n");
 printf("应该独立了\n");
 }
 else //else 后使⽤{}控制多条语句-这个块也叫:程序块,或者复合语句
 {
 printf("未成年\n");
 printf("可以尝试自己独立做事\n");
 }
 return 0;
}

1.4 嵌套if

在 if else 语句中, else 可以与另⼀个 if 语句连⽤,构成多重判断。

if (外层条件) {
    // 外层条件为真时执行
    if (内层条件) {
        // 内外层条件都为真时执行
    } else {
        // 外层为真,内层为假时执行
    }
} else {
    // 外层条件为假时执行
}

⽐如:要求输⼊⼀个整数,判断输⼊的整数是0,还是正数或者负数。请看如下代码:

#include <stdio.h>
int main()
{
 int num = 0;
 scanf("%d", &num);
 if(num == 0)
 printf("输⼊的数字是0\n");
 else if(num > 0) //这⾥的if 相当于嵌套在else语句中,形成了嵌套结构
 printf("输⼊的数字是正数\n");
 else
 printf("输⼊的数字是负数\n");
 
 return 0;
}

1.5 悬空else问题

如果有多个 if 和 else ,可以记住这样⼀条规则, else 总是跟最接近的 if 匹配。
我们⾸先从下⾯代码开始

#include <stdio.h>
int main()
 {
 int a = 0;
 int b = 2;
 if(a == 1)
 		if(b == 2)
 			printf("hehe\n");
 else
 		printf("haha\n");
 return 0;
 }

程序运⾏的结果是啥?
很多初学者,上来以判断 a 是 0,不等于 1 ,那就执⾏ else ⼦句,打印 haha
但是当你去运⾏代码,输出的结果是:啥都不输出。
为什么呢?
这就是悬空 else 的问题,如果有多个 if 和 else ,可以记住这样⼀条规则, else 总是跟最接近
的 if 匹配。
上⾯的代码排版,让 else 和第⼀个 if 语句对⻬,让我们以为 else 是和第⼀个if匹配的,当 if语句不成⽴的时候,⾃然想到的就是执⾏ else ⼦句,打印 haha ,但实际上 else 是和第⼆个 if进⾏匹配的,这样后边的 if…else 语句是嵌套在第⼀个 if 语句中的,如果第⼀个 if 语句就不成⽴,嵌套 if 和 else 就没机会执⾏了,最终啥都不打印。

// 这样写容易误解
if (a > 0)
    if (b > 0)
        printf("Both positive\n");
else
    printf("你以为这个else属于外层if?其实属于内层if!\n");

// 想要else属于外层if,必须用花括号
if (a > 0) {
    if (b > 0)
        printf("Both positive\n");
} else {
    printf("现在这个else明确属于外层if\n");
}

如果代码改成下⾯这样就更加容易理解了

#include <stdio.h>
int main()
{
 int a = 0;
 int b = 2;
 if(a == 1)
 {
 if(b == 2)
 printf("hehe\n");
 else
 printf("haha\n");
 }
 return 0;
}

只要带上适当的⼤括号,代码的逻辑就会更加的清晰,所以⼤家以后在写代码的时候要注意括号的使
⽤,让代码的可读性更⾼。

if (condition1) {
    if (condition2) {
        // ...
    } else {
        // 明确属于内层if
    }
} else {
    // 明确属于外层if
}

2. 关系操作符

C 语⾔⽤于⽐较的表达式,称为 “关系表达式”(relational expression),⾥⾯使⽤的运算符就称
为“关系运算符”(relational operator),主要有下⾯6个。

2.1 基本关系操作符

操作符 含义 示例 结果
== 等于 5 == 5 1 (真)
!= 不等于 5 != 3 1 (真)
> 大于 5 > 3 1 (真)
< 小于 5 < 3 0 (假)
>= 大于等于 5 >= 5 1 (真)
<= 小于等于 5 <= 3 0 (假)

2.2 基本用法

#include <stdio.h>

int main() {
    int a = 10, b = 20;
    
    printf("a == b: %d\n", a == b);  // 0
    printf("a != b: %d\n", a != b);  // 1
    printf("a < b:  %d\n", a < b);   // 1
    printf("a > b:  %d\n", a > b);   // 0
    printf("a <= b: %d\n", a <= b);  // 1
    printf("a >= b: %d\n", a >= b);  // 0
    
    return 0;
}

2.3 在if语句中的应用

#include <stdio.h>

int main() {
    int score = 85;
    
    if (score >= 60 && score <= 100) {
        printf("成绩合格\n");
    }
    
    if (score == 100) {
        printf("满分!\n");
    }
    
    return 0;
}

2.4 常见注意事项

混淆 ===

注意:相等运算符 == 与赋值运算符 = 是两个不⼀样的运算符,不要混淆。有时候,可能会不⼩⼼写
出下⾯的代码,它可以运⾏,但很容易出现意料之外的结果。

int x = 5;

// 错误!总是为真,因为这是赋值不是比较
if (x = 10) {
    printf("这总是会执行\n");
}

// 正确
if (x == 10) {
    printf("这才是在比较\n");
}
浮点数比较
#include <math.h>

double a = 0.1 + 0.2;
double b = 0.3;

// 错误:浮点数精度问题
if (a == b) {
    printf("这可能不会执行\n");
}

// 正确:使用容差比较
if (fabs(a - b) < 0.000001) {
    printf("这在容差范围内相等\n");
}
多个关系运算符不宜连用
i < j < k

上面示例中,连续使用两个小于运算符。这是合法表达式,不会报错,但是通常达不到想要的结果,即不是保证变量 j 的值在 i 和 k 之间。因为关系运算符是从左到右计算,所以实际执⾏的是下⾯的
表达式。

(i < j) < k

上⾯式⼦中, i < j 返回 0 或 1 ,所以最终是 0 或 1 与变量 k 进⾏⽐较。如果想要判断变量 j的值是否在 i 和 k 之间,应该使用下面的写法。

i < j && j < k

⽐如:我们输⼊⼀个年龄,如果年龄在18岁~36岁之间,我们输出⻘年。如果我们这样写

#include <stdio.h>
int main()
{
 int age = 0;
 scanf("%d", &age);
 if(18<=age<=36)
 {
 printf("⻘年\n");
 }
 return 0;
}

当我们输⼊10的时候,依然输出⻘年
这是因为,我们先拿18和age中存放的10⽐较,表达式18<=10为假, 18<=age 的结果是0,再拿0和
36⽐较,0<=36为真,所以打印了 ⻘年 ,所以即使当age是10的时候,也能打印 ⻘年 ,逻辑上是有
问题,这个代码应该怎么写呢?

#include <stdio.h>
int main()
{
 int age = 0;
 scanf("%d", &age);
 if(age>=18 && age<=36)
 {
 printf("⻘年\n");
 }
 return 0;
}

以上就是关于操作符,我们需要掌握的,剩下的只要按照字⾯意思理解使用就行,没有特别注意的。

2.5 结合逻辑操作符

int age = 25;
int score = 85;

// 多个条件组合
if (age >= 18 && score >= 60) {
    printf("成年人且成绩合格\n");
}

if (age < 18 || score < 60) {
    printf("未成年或成绩不合格\n");
}

返回值:关系操作符返回 1 (真) 或 0 (假)

3. 条件操作符

在C语言中,条件操作符(也称为三元操作符)是唯一一个需要三个操作数的操作符。

基本语法

条件表达式 ? 表达式1 : 表达式2

工作原理

  1. 先计算条件表达式的值
  2. 如果条件为(非0),则计算表达式1的值,并作为整个表达式的结果
  3. 如果条件为(0),则计算表达式2的值,并作为整个表达式的结果

示例代码

#include <stdio.h>

int main() {
    int a = 10, b = 20;
    
    // 基本用法:找出两个数中的较大值
    int max = (a > b) ? a : b;
    printf("较大值是:%d\n", max);  // 输出:20
    
    // 判断奇偶数
    int num = 15;
    char* result = (num % 2 == 0) ? "偶数" : "奇数";
    printf("%d 是 %s\n", num, result);  // 输出:15 是 奇数
    
    // 赋值给不同变量
    int x = 5, y = 8;
    int z = (x > y) ? x : y;
    printf("z = %d\n", z);  // 输出:8
    
    return 0;
}

嵌套条件操作符

#include <stdio.h>

int main() {
    int a = 10, b = 20, c = 15;
    
    // 找出三个数中的最大值
    int max = (a > b) ? ((a > c) ? a : c) : ((b > c) ? b : c);
    printf("最大值是:%d\n", max);  // 输出:20
    
    // 成绩等级判断
    int score = 85;
    char grade = (score >= 90) ? 'A' : 
                 (score >= 80) ? 'B' : 
                 (score >= 70) ? 'C' : 
                 (score >= 60) ? 'D' : 'F';
    printf("成绩等级:%c\n", grade);  // 输出:B
    
    return 0;
}

与if-else语句的对比

使用条件操作符:

int abs_value = (x >= 0) ? x : -x;

等效的if-else语句:

int abs_value;
if (x >= 0) {
    abs_value = x;
} else {
    abs_value = -x;
}

注意事项

  1. 类型兼容性:表达式1和表达式2的类型应该兼容
  2. 可读性:嵌套过多会降低代码可读性
  3. 副作用:注意条件操作符中的副作用
#include <stdio.h>

int main() {
    int x = 1, y = 2;
    
    // 注意:只有被选中的表达式才会被执行
    int result = (x > 0) ? (printf("x>0 "), x) : (printf("x<=0 "), y);
    printf("result = %d\n", result);  // 输出:x>0 result = 1
    
    return 0;
}

优点

  • 简洁:适合简单的条件赋值
  • 表达式:可以在更大的表达式中使用
  • 效率:编译器可以更好地优化

缺点

  • 可读性:复杂逻辑时不如if-else清晰
  • 调试:调试时不如if-else方便

条件操作符是C语言中非常有用的工具,特别适合简单的条件赋值场景,但要注意适度使用以保持代码的可读性。

4. 逻辑操作符:&& , || , !

逻辑操作符是编程和逻辑运算中用于连接多个布尔表达式(其值为 True 或 False),并最终返回一个布尔结果的核心工具。掌握它们对于编写条件判断、循环控制等至关重要。

4.1 三大基本逻辑操作符

大多数编程语言都包含以下三种基本逻辑操作符:

操作符 名称 含义 逻辑定义(真值表)
&& (或 AND) 逻辑与 当且仅当所有操作数都为 True 时,结果才为 True。 True && True = True
True && False = False
False && True = False
False && False = False
**` ** (或 OR`) 逻辑或
! (或 NOT) 逻辑非 一元操作符,用于反转操作数的逻辑状态。 !True = False
!False = True

通俗理解:

  • 与 (&&):类似于“并且”。条件A 并且 条件B 都满足才行。
  • 或 (||):类似于“或者”。条件A 或者 条件B 有一个满足就行。
  • 非 (!):类似于“不”。不是这个条件。

4.2 需要特别注意的事项(重中之重)

4.2.1 短路求值

这是逻辑操作符最重要的特性之一。短路求值指的是:表达式的求值顺序是从左到右,并且一旦结果能够确定,就不再继续计算剩余的操作数。

  • &&(逻辑与)的短路:如果第一个操作数为 False,那么整个表达式肯定为 False,因此不会再计算第二个操作数。

    // 示例 (Java/C++/JavaScript等)
    if (false && someFunction()) {
        // someFunction() 永远不会被调用!
    }
    
  • ||(逻辑或)的短路:如果第一个操作数为 True,那么整个表达式肯定为 True,因此不会再计算第二个操作数。

    // 示例
    if (true || someFunction()) {
        // someFunction() 永远不会被调用!
    }
    

为什么要关注短路求值?

  • 避免错误:可以利用它来避免运行时错误。
    // 在访问 obj 的成员前,先检查 obj 是否为空
    if (obj != null && obj.isValid()) {
        // 如果 obj 为 null,第一个条件为 false,会触发短路,obj.isValid() 不会被执行,从而避免了 NullPointerException。
    }
    
  • 提高效率:如果第二个操作数是一个计算量很大的函数,短路可以节省不必要的计算。
4.2.2 操作数的类型与“真值”判断

在动态语言(如 JavaScript, Python)或某些上下文中,逻辑操作符的操作数不一定是严格的布尔值。它们会对操作数进行“真值”判断。

  • 假值:在逻辑判断中会被视为 False 的值。
    拓展: * JavaScript: false, 0, "" (空字符串), null, undefined, NaN
    • Python: False, None, 0, "" (空字符串), [] (空列表), {} (空字典)等。
  • 真值:除假值以外的所有值。

注意:逻辑操作符在这些语言中返回的不一定是布尔值,而是其中一个操作数的值

  • a && b:如果 a 为假值,返回 a;否则返回 b
  • a || b:如果 a 为真值,返回 a;否则返回 b
// JavaScript 示例
console.log(0 && "Hello");    // 输出: 0 (因为 0 是假值)
console.log("Apple" && 42);   // 输出: 42 (因为 "Apple" 是真值,所以返回第二个操作数)
console.log("" || "Default"); // 输出: "Default" (因为 "" 是假值)
console.log("Hi" || "Default"); // 输出: "Hi" (因为 "Hi" 是真值,直接返回)

这个特性常被用于设置默认值

const userName = inputName || "Guest"; // 如果 inputName 为空,则使用 "Guest"
4.2.3 运算符优先级

逻辑操作符的优先级决定了表达式中哪些部分先被计算。常见的优先级从高到低是:

逻辑非 (!) > 逻辑与 (&&) > 逻辑或 (||)

boolean result = true || false && false; 
// 等价于 true || (false && false)
// => true || false
// => true

// 如果想要改变顺序,必须使用括号
boolean result2 = (true || false) && false; 
// => true && false
// => false

最佳实践:即使你记得优先级,也强烈建议使用括号 () 来明确指定计算顺序,这样可以提高代码的可读性并避免潜在的错误。

4.2.4 与位操作符的区别

不要将逻辑操作符和位操作符混淆。

逻辑操作符 位操作符
操作对象 布尔值(或可进行真值判断的值) 整数的二进制位
操作符 &&, `
结果 布尔值(或其中一个操作数) 整数
是否有短路效应 没有
// 示例
int a = 5; // 二进制 0101
int b = 3; // 二进制 0011

// 位操作
System.out.println(a & b); // 按位与:0001 -> 1
System.out.println(a | b); // 按位或:0111 -> 7

// 逻辑操作
if (a > 0 && b > 0) { // 这里比较的是 a 和 b 的值是否大于 0
    // 执行代码
}

注意

  1. 核心三要素:掌握 && (与), || (或), ! (非) 的含义和真值表。
  2. 理解短路求值:这是最重要的特性,能帮你写出更安全、更高效的代码。
  3. 注意类型和返回值:在动态类型语言中,逻辑操作符返回的是操作数本身,而非一定是布尔值。理解“真值”和“假值”的概念。
  4. 善用括号:用 () 明确优先级,避免歧义。
  5. 区分逻辑与位操作&&& 是完全不同的东西,不要混用。

通过理解这些概念和注意事项,你就能在各种编程场景下自信且正确地使用逻辑操作符了。

5. switch 语句

C 语言中的 switch 语句是一个非常强大的多分支选择结构,但在使用时有很多重要的细节需要注意。

5.1 switch 语句的基本结构

switch 语句允许根据一个整型表达式的值,从多个代码块中选择一个来执行。

基本语法:

switch (表达式) {
    case 常量1:
        // 语句序列1
        break;
    case 常量2:
        // 语句序列2
        break;
    // ... 更多的 case 标签
    default:
        // 默认语句序列
}

执行流程:

  1. 计算 switch 后面括号内 表达式 的值。
  2. 将表达式的值与每个 case 后面的 常量 进行比较。
  3. 如果找到匹配的常量,则执行该 case 标签后面的代码块。
  4. 执行直到遇到 break 语句或 switch 语句结束。
  5. 如果没有 case 匹配,则执行 default 标签后的代码块(如果存在)。

5.2 关键点与注意事项(重中之重)

5.2.1 switch 的表达式的类型限制

这是最容易出错的地方之一。switch 的表达式必须是一个整型表达式

  • 允许的类型int, char, enum(枚举本质是整型)。
  • 不允许的类型float, double, char*(字符串) 或其它任何非整型类型。

错误示例:

float f = 3.14f;
switch (f) { // 错误!表达式不能是浮点型
    case 3.14f: // 错误!case 标签也不能是浮点常量
        // ...
}
5.2.2 case 标签的值必须是整型常量表达式
  • case 后面必须跟一个编译期可知的整型常量,不能是变量。
  • 通常使用整数、字符常量(如 'A')或枚举常量。

错误示例:

int x = 10;
int y = 20;
switch (x) {
    case y: // 错误!y 是变量,不是常量
        // ...
}

正确示例:

#define VALUE 10
const int num = 20; // 注意:在 C 中,const 修饰的变量并不总是被视为常量表达式,在某些编译器下可能报错。最好使用 #define 或枚举。

switch (x) {
    case 10:        // 正确,整型字面量
    case 'A':       // 正确,字符字面量(本质是整型)
    case VALUE:     // 正确,宏定义的常量
    // case num:    // 可能报错,避免使用
        // ...
}
5.2.3 break 语句的作用与“case 穿透”

这是 switch 语句最核心的特性,也是最重要的注意事项。

  • break 的作用立即终止整个 switch 语句。如果没有 break,程序会继续执行下一个 case 的代码块,直到遇到 breakswitch 结束。这种现象被称为 “case 穿透”

  • 故意利用 “case 穿透”
    当多个 case 需要执行相同的代码时,可以故意省略 break

    int score = 'B';
    switch (score) {
        case 'A':
        case 'B':
        case 'C':
            printf("及格\n"); // 当 score 为 'A', 'B', 或 'C' 时,都会执行这一句
            break;
        case 'D':
            printf("不及格\n");
            break;
        default:
            printf("无效成绩\n");
    }
    
  • 无意造成的 “case 穿透”(常见错误)
    如果忘记写 break,会导致逻辑错误,且编译器通常不会报错。

    int number = 1;
    switch (number) {
        case 1:
            printf("执行 case 1\n");
            // 忘记写 break!
        case 2:
            printf("执行 case 2\n"); // 即使 number 是 1,这里也会被执行!
            break;
    }
    // 输出:
    // 执行 case 1
    // 执行 case 2
    
5.2.4 default 分支是可选的,但强烈建议使用
  • default 分支用于处理所有 case 都不匹配的情况。
  • 最佳实践永远写上 default 分支,即使你认为它不会被用到。它可以捕获意外的输入,增强程序的健壮性。
  • default 的位置可以任意,但通常放在最后。如果放在中间或开头,同样要注意 break
5.2.5 casedefault 标签的顺序

casedefault 标签的顺序可以是任意的,执行流程只取决于表达式的值,而不是标签的顺序。

switch (x) {
    default: // default 在开头,记得加 break!
        printf("未知");
        break;
    case 1:
        printf("一");
        break;
    case 2:
        printf("二");
        // 注意:如果这里没有 break,会穿透到 default?不会!因为 default 在上面,已经执行过了。
}

5.3 switchif-else 的对比

特性 switch 语句 if-else
判断基础 基于单个整型表达式相等性判断 基于多个任意布尔表达式,可以是范围判断、逻辑组合等
可读性 多分支等值判断时,结构清晰 条件复杂时(如范围判断 if (x > 10 && x < 20))更合适
效率 编译器可能优化为跳转表,效率可能更高 通常需要依次判断条件

选择指南:

  • 当需要对同一个变量进行多种不同的等值判断时,优先使用 switch
  • 当判断条件涉及范围、关系运算(如 ><)或复杂的逻辑组合时,必须使用 if-else

注意

  1. 表达式类型:确保 switch 的表达式是整型int, char, enum)。
  2. case 常量case 后面必须是整型常量,不能是变量。
  3. 警惕穿透时刻检查是否每个 case 都需要 break。故意穿透要有注释说明,无意穿透是 Bug。
  4. 使用 default总是包含 default 分支以处理意外情况。
  5. 保持简洁:每个 case 的代码块不宜过长,否则应考虑封装成函数。
  6. 注释穿透:如果故意使用了 “case 穿透”,请务必添加注释(如 // fallthrough),向阅读代码的人表明这是有意为之。

通过理解这些规则和注意事项,你可以有效地利用 switch 语句来编写出更清晰、更健壮的 C 语言代码。

6. while循环

好的,我们来详细讲解 C 语言中的 while 循环。它是一种基本的入口条件循环,意味着在执行循环体之前先检查条件是否满足。

6.1 while 循环的基本结构

while 循环会在指定条件为真时,反复执行一段代码块。

基本语法:

while (条件表达式) {
    // 循环体:条件为真时重复执行的代码
}

执行流程:

  1. 计算 条件表达式 的值。
  2. 如果结果为 (在 C 语言中,任何非零值都视为真),则执行循环体内的语句。
  3. 执行完循环体后,流程再次跳转回步骤 1,重新判断条件。
  4. 如果条件结果为 (0),则循环终止,程序继续执行 while 循环之后的代码。

6.2 关键点与注意事项(重中之重)

6.2.1 循环条件必须最终能变为假

这是 while 循环最核心的注意事项。如果循环条件永远为真,循环将永远不会停止,这就构成了 无限循环

无限循环示例:

while (1) {        // 条件永远为 1(真)
    printf("这是一个无限循环!\n");
}

int count = 5;
while (count > 0) {
    printf("Count: %d\n", count);
    // 忘记写 count--; 循环条件永远不会改变,导致无限循环
}

正确示例:

int count = 5;
while (count > 0) {
    printf("Count: %d\n", count);
    count--; // 在循环体内修改循环变量,使条件最终变为假
}
// 输出:
// Count: 5
// Count: 4
// Count: 3
// Count: 2
// Count: 1
6.2.2 循环条件的求值时机

while入口条件循环,意味着它先判断,后执行。如果初始条件就为假,循环体一次都不会执行。

int number = 10;
while (number < 5) { // 初始条件 (10 < 5) 为假
    printf("这行代码永远不会被执行!\n");
}
6.2.3 循环体内必须有改变循环条件的语句

为了让循环能够结束,循环体内必须包含能影响条件表达式结果的代码。这通常通过修改一个 “循环控制变量” 来实现。

// 从用户输入读取数字,直到用户输入 0
int input = -1; // 初始化为一个非零值,确保循环至少进入一次

while (input != 0) {
    printf("请输入一个数字 (输入0退出): ");
    scanf("%d", &input); // 在循环体内改变 input 的值,影响循环条件
    printf("你输入了: %d\n", input);
}
6.2.4 边界情况与“差一错误”

在控制循环次数时,要特别注意边界条件,否则容易发生 “差一错误”

示例:打印 1 到 5

// 方法 1:计数器从 1 开始
int i = 1;
while (i <= 5) { // 包含 5
    printf("%d ", i);
    i++;
}
// 输出:1 2 3 4 5

// 方法 2:计数器从 0 开始
int j = 0;
while (j < 5) { // 不包含 5,但 j 从 0 到 4,打印时加 1
    printf("%d ", j + 1);
    j++;
}
// 输出:1 2 3 4 5

// 常见的差一错误
int k = 1;
while (k < 5) { // 条件写成 < ,导致只打印到 4
    printf("%d ", k);
    k++;
}
// 输出:1 2 3 4 (缺少 5)
6.2.5 使用 breakcontinue 控制循环流程
  • break 语句立即终止整个循环,并跳出循环体。

    int num = 0;
    while (num < 10) {
        num++;
        if (num == 5) {
            break; // 当 num 等于 5 时,立即退出循环
        }
        printf("%d ", num);
    }
    // 输出:1 2 3 4
    
  • continue 语句跳过本次循环剩余的代码,直接进入下一次循环的条件判断。

    int num = 0;
    while (num < 6) {
        num++;
        if (num == 3) {
            continue; // 当 num 等于 3 时,跳过本次循环的 printf
        }
        printf("%d ", num);
    }
    // 输出:1 2 4 5 6 (跳过了 3)
    

    注意:在使用 continue 时,要确保循环控制变量的更新不会因此被跳过,否则可能导致无限循环。

    // 错误示例
    int num = 0;
    while (num < 6) {
        if (num == 3) {
            continue; // 如果 num 为 3,会跳过 num++,导致 num 永远为 3,无限循环!
        }
        printf("%d ", num);
        num++; // 更新操作在 continue 之后,可能被跳过
    }
    
6.2.6 循环体为单条语句时的花括号

如果循环体只包含一条语句,可以省略花括号 {}。但强烈建议始终使用花括号,即使只有一条语句。

// 可以省略,但不推荐
while (condition)
    single_statement;

// 推荐:始终使用花括号
while (condition) {
    single_statement;
}

原因:使用花括号可以增强代码清晰度,并避免在后续添加语句时出错。


6.3 while 循环的常见应用场景

  1. 输入验证:反复要求用户输入,直到输入有效。

    int score;
    printf("请输入成绩 (0-100): ");
    scanf("%d", &score);
    while (score < 0 || score > 100) {
        printf("输入无效!请重新输入成绩 (0-100): ");
        scanf("%d", &score);
    }
    
  2. 处理未知数量的数据:直到遇到特定标记(如文件结束符 EOF)才停止。

    int ch;
    while ((ch = getchar()) != EOF) {
        putchar(ch);
    }
    
  3. 游戏主循环:保持游戏运行,直到玩家退出。

    int game_is_running = 1;
    while (game_is_running) {
        process_input();
        update_game_logic();
        render_graphics();
        // 在某些条件下,将 game_is_running 设置为 0 来退出游戏
    }
    

注意

  1. 避免无限循环:确保循环体内有能使条件最终变为假的语句。
  2. 明确循环变量:清楚知道哪个变量在控制循环,并确保它被正确初始化和管理。
  3. 小心边界:仔细检查循环条件,避免"差一错误"。
  4. 慎用 continue:使用 continue 时,确保循环控制变量的更新不会被意外跳过。
  5. 始终使用花括号:即使循环体只有一行代码,也使用 {}
  6. 优先选择 while:当循环次数未知,或需要先判断后执行时,while 是理想的选择。

通过遵循这些指导原则,你可以有效地利用 while 循环来构建强大而可靠的 C 语言程序。

7. for循环

C 语言中的 for 循环是功能最强大、使用最广泛的循环结构,特别适合在已知循环次数有明确循环控制变量的场景下使用。

7.1 for 循环的基本结构

for 循环将循环控制结构的三个关键部分(初始化、条件判断、更新)集中在一起,语法非常紧凑。

基本语法:

for (初始化表达式; 循环条件表达式; 更新表达式) {
    // 循环体:条件为真时重复执行的代码
}

执行流程:

  1. 初始化:首先执行初始化表达式(通常用于初始化循环计数器),且只执行一次
  2. 条件判断:计算循环条件表达式的值。如果为(非零),则执行循环体;如果为(零),则终止循环,跳转到循环体后的语句。
  3. 执行循环体:执行循环体内的所有语句。
  4. 更新:执行更新表达式(通常用于递增或递减循环计数器)。
  5. 重复步骤 2~4,直到条件表达式为假。

7.2 关键点与注意事项(重中之重)

7.2.1 三个表达式的灵活性

for 循环的三个部分都非常灵活,但需要正确使用。

标准用法(最常见):

for (int i = 0; i < 10; i++) {
    printf("%d ", i);
}
// 输出:0 1 2 3 4 5 6 7 8 9

特殊用法:

  • 初始化部分可以声明多个同类型变量(C99及以上)
    for (int i = 0, j = 10; i < j; i++, j--) {
        printf("i=%d, j=%d\n", i, j);
    }
    
  • 更新部分可以执行多个操作
    for (int i = 0; i < 10; i++, printf("更新了\n")) {
        // 循环体
    }
    
7.2.2 三个表达式都可以省略(但分号必须保留)

这是 for 循环的一个重要特性,但需要谨慎使用。

省略条件表达式:相当于条件永远为真,创建无限循环

for (int i = 0; ; i++) {  // 省略条件,无限循环
    if (i > 5) break;     // 需要其他方式退出循环
    printf("%d ", i);
}

省略初始化和更新表达式:相当于 while 循环

int i = 0;
for (; i < 5; ) {  // 省略初始化和更新
    printf("%d ", i);
    i++;           // 在循环体内更新
}
// 等同于 while (i < 5) { ... }

全部省略:经典的无限循环写法

for (;;) {  // 无限循环,相当于 while (1)
    // 需要 break 语句来退出
    if (some_condition) break;
}
7.2.3 循环变量的作用域

在 C89/C90 中:循环变量必须在 for 语句之前声明

int i;  // 循环变量在外部声明
for (i = 0; i < 5; i++) {
    // ...
}
// 此处 i 仍然可见,值为 5

在 C99 及以后:可以在初始化表达式中直接声明,此时变量作用域仅限于 for 循环

for (int i = 0; i < 5; i++) {  // i 在循环内声明
    printf("%d ", i);
}
// 此处 i 不再可见(超出作用域)

最佳实践:如果循环变量只在循环内使用,推荐在 for 语句内声明,这样可以避免变量名污染外部作用域。

7.2.4 边界情况与"差一错误"

这是 for 循环中最常见的错误类型。

经典的差一错误:

// 错误:循环 11 次而不是 10 次
for (int i = 0; i <= 10; i++) {  // 0到10是11个数
    printf("%d ", i);
}

// 正确:循环 10 次
for (int i = 0; i < 10; i++) {   // 0到9是10个数
    printf("%d ", i);
}

// 另一种正确写法(从1开始)
for (int i = 1; i <= 10; i++) {  // 1到10是10个数
    printf("%d ", i);
}
7.2.5 不要在循环体内修改循环变量

虽然语法允许,但这通常是不好的做法,容易导致逻辑错误。

不推荐的写法:

for (int i = 0; i < 10; i++) {
    if (i == 5) {
        i = 8;  // 直接修改循环变量,跳过了一些迭代
    }
    printf("%d ", i);
}
// 输出:0 1 2 3 4 8 9(跳过了5,6,7)

更好的做法:使用 continue 或调整循环逻辑

for (int i = 0; i < 10; i++) {
    if (i >= 5 && i <= 7) {
        continue;  // 明确跳过特定值
    }
    printf("%d ", i);
}
7.2.6 浮点数不适合作为循环计数器

由于浮点数的精度问题,使用浮点数作为循环变量可能导致意外结果。

错误示例:

// 可能不会精确执行 10 次
for (float f = 0.1; f < 1.0; f += 0.1) {
    printf("%f ", f);  // 浮点误差可能使 f 无法精确等于 1.0
}

正确做法:使用整数循环,在内部计算浮点值

for (int i = 1; i <= 10; i++) {
    float f = i * 0.1f;
    printf("%f ", f);
}
7.2.7 循环嵌套时的性能考虑

在嵌套循环中,将循环次数多的循环放在内层可能提高缓存命中率。

// 较好的写法:内层循环次数多
for (int i = 0; i < 10; i++) {           // 外层循环10次
    for (int j = 0; j < 1000000; j++) {  // 内层循环100万次
        // 密集计算
    }
}

7.3 for 循环的常见应用模式

7.3.1 计数循环
// 执行固定次数的循环
for (int i = 0; i < n; i++) {
    // 重复执行 n 次
}
7.3.2 数组遍历
int arr[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
    printf("arr[%d] = %d\n", i, arr[i]);
}
7.3.3 递减循环
// 从大到小遍历
for (int i = 10; i > 0; i--) {
    printf("%d ", i);
}
// 输出:10 9 8 7 6 5 4 3 2 1
7.3.4 步长不为1的循环
// 每次增加2
for (int i = 0; i < 10; i += 2) {
    printf("%d ", i);
}
// 输出:0 2 4 6 8

7.4 for vs while 选择指南

场景 推荐使用 理由
已知循环次数 for 循环控制集中,代码清晰
遍历数组/集合 for 天然适合使用索引
条件复杂,与计数器无关 while 更灵活的条件表达
至少执行一次 do-while 保证先执行后判断
无限循环 for (;;)while (1) 个人偏好,两者均可

注意

  1. 初始化就近原则:在 for 语句内声明循环变量(C99+)。
  2. 警惕差一错误:仔细检查循环边界条件。
  3. 避免修改循环变量:在循环体内不要直接修改循环计数器。
  4. 慎用浮点数:不要用浮点数作为循环控制变量。
  5. 表达式可省略但要谨慎:理解每个表达式省略的含义。
  6. 嵌套循环优化:考虑缓存友好性来安排循环顺序。
  7. 选择合适循环:根据具体场景选择 forwhiledo-while

通过掌握这些要点,你可以写出更加高效、健壮的 for 循环代码。for 循环是 C 语言中最常用且最高效的循环结构,正确使用它对编程至关重要。

总结

以上就是今天要讲的内容,谢谢各位大佬的观看

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐