C 语言学习坚持百日基本功-循环结构-18
# C语言循环结构深度解析(附完整代码示例)
循环结构是C语言三大流程控制(顺序、分支、循环)的核心之一,它允许程序在满足特定条件时重复执行一段代码块,是实现批量处理、迭代计算、逻辑复用的基础。本文将从循环结构的底层原理、语法规则、核心类型、进阶用法、实战案例、性能优化及常见陷阱等维度,全面解析C语言循环结构,覆盖从基础到高阶的所有核心知识点。
## 一、循环结构概述
### 1.1 循环的本质与价值
程序的核心价值在于“自动化重复执行”,而循环结构正是为解决这一需求而生。在无循环的场景下,若需执行100次相同操作,需编写100段重复代码,不仅效率低下,且维护成本极高;而循环仅需编写1段核心逻辑,通过“条件判断+迭代控制”即可实现任意次数的重复执行。
从底层实现来看,循环的本质是编译器将循环语法转换为**条件跳转指令**(如汇编中的`jmp`、`je`、`jne`等),通过修改程序计数器(PC)的值,让CPU反复执行指定内存地址的指令块。例如,`for (int i=0; i<5; i++)`会被编译为:
```asm
mov eax, 0 ; 初始化i=0
LABEL_LOOP:
cmp eax, 5 ; 条件判断i<5
jge LABEL_END ; 不满足则跳转到结束
; 循环体代码
inc eax ; i++
jmp LABEL_LOOP ; 跳回循环开始
LABEL_END:
; 后续代码
```
### 1.2 循环的核心要素
任何完整的循环结构都包含三个不可缺少的要素,缺失任一要素都会导致循环异常(如死循环、不执行):
1. **初始化**:设置循环的起始状态(如循环变量初始值、计数器清零);
2. **条件判断**:定义循环的终止条件(布尔表达式,非0为真,0为假);
3. **迭代更新**:修改循环状态,使其逐步趋近于终止条件(如循环变量自增/自减)。
### 1.3 C语言循环的分类
C语言提供三种原生循环结构,适配不同的应用场景:
| 循环类型 | 语法特征 | 执行顺序 | 适用场景 |
|----------|----------|----------|----------|
| `while` | 先判断后执行 | 条件判断 → 循环体 → 迭代更新 | 未知循环次数,仅知终止条件 |
| `for` | 结构化整合三要素 | 初始化 → 条件判断 → 循环体 → 迭代更新 | 已知循环次数,或需精准控制迭代 |
| `do-while` | 先执行后判断 | 循环体 → 条件判断 → 迭代更新 | 循环体必须至少执行一次(如输入验证) |
此外,C语言还支持通过`break`、`continue`、`goto`等语句控制循环流程,进一步扩展循环的灵活性。
## 二、while循环:基础条件循环
### 2.1 语法规则与执行流程
#### 2.1.1 标准语法
```c
// 完整格式
[初始化语句]
while (条件表达式) {
循环体代码块;
迭代更新语句; // 核心:必须修改循环状态,避免死循环
}
```
#### 2.1.2 执行步骤
1. 执行**初始化语句**(仅一次,通常在`while`外定义循环变量);
2. 计算**条件表达式**的值:
- 若值为非0(真):执行循环体代码块 → 执行迭代更新语句 → 回到步骤2;
- 若值为0(假):跳出循环,执行后续代码。
#### 2.1.3 核心规则
- 条件表达式可以是任意可转换为布尔值的表达式(整数、浮点数、指针、逻辑表达式等);
- 循环体若只有一条语句,可省略`{}`,但**强烈不建议**(易引发逻辑错误);
- 迭代更新语句必须修改循环变量,否则条件表达式始终为真,陷入死循环;
- 空循环体(`while(条件);`)表示“仅判断条件,不执行任何操作”,常用于延时或等待。
### 2.2 基础示例
#### 2.2.1 累加计算(1~100的和)
```c
#include <stdio.h>
int main() {
// 初始化:循环变量i=1,累加和sum=0
int i = 1;
int sum = 0;
// 条件判断:i <= 100
while (i <= 100) {
// 循环体:累加
sum += i;
// 迭代更新:i自增
i++;
}
printf("1~100的累加和:%d\n", sum); // 输出5050
return 0;
}
```
#### 2.2.2 遍历字符串(统计字符个数)
```c
#include <stdio.h>
int main() {
char str[] = "Hello, C Language!";
int count = 0;
int index = 0;
// 条件:未到字符串结束符'\0'
while (str[index] != '\0') {
count++;
index++;
}
printf("字符串长度:%d\n", count); // 输出16(不含'\0')
return 0;
}
```
### 2.3 死循环与退出机制
`while(1)`是C语言中最常见的死循环写法,需通过`break`、`return`、`goto`或外部信号(如`SIGINT`)退出,适用于持续运行的程序(如服务器、嵌入式系统)。
#### 2.3.1 基于break的退出
```c
#include <stdio.h>
int main() {
int input;
printf("输入整数(输入-1退出):\n");
while (1) { // 死循环
scanf("%d", &input);
if (input == -1) {
printf("退出程序!\n");
break; // 跳出循环
}
printf("你输入的是:%d\n", input);
}
return 0;
}
```
#### 2.3.2 基于标志位的退出
```c
#include <stdio.h>
int main() {
int running = 1; // 标志位:1=运行,0=退出
int count = 0;
while (running) {
count++;
printf("程序运行中,计数:%d\n", count);
if (count >= 5) {
running = 0; // 修改标志位,退出循环
}
}
printf("程序结束\n");
return 0;
}
```
### 2.4 while循环的常见陷阱
1. **遗漏迭代更新**:
```c
// 错误示例:无i++,死循环
int i = 1;
while (i <= 10) {
printf("%d ", i);
}
```
2. **浮点精度导致的死循环**:
```c
// 错误示例:0.1无法精确表示,i永远无法等于1.0
double i = 0.0;
while (i != 1.0) {
printf("%.1f ", i);
i += 0.1;
}
```
解决方案:使用整数替代浮点,或设置精度阈值(`while (fabs(i - 1.0) > 1e-6)`)。
3. **条件表达式的副作用**:
```c
// 风险:getchar()会读取输入缓冲区,可能导致意外行为
while (getchar() != '\n');
```
## 三、for循环:结构化迭代循环
`for`循环将“初始化、条件判断、迭代更新”整合在一行,是C语言中最简洁、最常用的循环结构,尤其适合**已知循环次数**的场景,代码可读性和结构化程度远高于`while`。
### 3.1 语法规则与执行流程
#### 3.1.1 标准语法
```c
for (初始化表达式; 条件表达式; 更新表达式) {
循环体代码块;
}
```
#### 3.1.2 执行步骤
1. 执行**初始化表达式**(仅一次,可定义循环变量,如`int i=0`);
2. 计算**条件表达式**:
- 非0:执行循环体 → 执行更新表达式 → 回到步骤2;
- 0:跳出循环;
3. 循环结束,执行后续代码。
#### 3.1.3 灵活的表达式省略
`for`循环的三个表达式均可省略,但分号必须保留,适配不同场景:
- 省略初始化:循环变量在外部定义(兼容C89);
- 省略条件:视为恒真(死循环,`for(;;)`等价于`while(1)`);
- 省略更新:迭代逻辑放在循环体内。
示例(省略所有表达式的死循环):
```c
for (;;) {
printf("死循环运行中...\n");
// 需break退出
}
```
### 3.2 基础示例
#### 3.2.1 固定次数循环(打印1~10)
```c
#include <stdio.h>
int main() {
// 初始化:i=1;条件:i<=10;更新:i++
for (int i = 1; i <= 10; i++) {
printf("%d ", i); // 输出1 2 3 ... 10
}
printf("\n");
return 0;
}
```
#### 3.2.2 多变量控制循环
通过逗号运算符(`,`)可在初始化/更新表达式中操作多个变量,适用于双指针、对称遍历等场景:
```c
#include <stdio.h>
int main() {
// 初始化:i=0(左指针),j=9(右指针);更新:i++,j--
for (int i = 0, j = 9; i <= j; i++, j--) {
printf("i=%d, j=%d\n", i, j);
}
// 输出:
// i=0, j=9
// i=1, j=8
// ...
// i=4, j=5
// i=5, j=4
return 0;
}
```
#### 3.2.3 倒序循环(10~1)
```c
#include <stdio.h>
int main() {
for (int i = 10; i >= 1; i--) {
printf("%d ", i); // 输出10 9 8 ... 1
}
printf("\n");
return 0;
}
```
### 3.3 嵌套for循环
嵌套`for`循环是处理二维数据(如矩阵、表格)的核心手段,外层循环控制“行”,内层循环控制“列”,需注意嵌套层次不宜超过3层(否则可读性大幅下降)。
#### 3.3.1 打印99乘法表
```c
#include <stdio.h>
int main() {
// 外层:行(1~9)
for (int i = 1; i <= 9; i++) {
// 内层:列(1~i)
for (int j = 1; j <= i; j++) {
printf("%d×%d=%d\t", j, i, i*j);
}
printf("\n"); // 换行
}
return 0;
}
```
#### 3.3.2 打印矩形(5行8列*)
```c
#include <stdio.h>
int main() {
// 外层:行数
for (int row = 1; row <= 5; row++) {
// 内层:列数
for (int col = 1; col <= 8; col++) {
printf("*");
}
printf("\n");
}
return 0;
}
```
### 3.4 for循环的进阶用法
#### 3.4.1 循环变量的作用域(C99特性)
C99允许在`for`初始化表达式中定义变量,变量作用域仅限于循环体,避免命名冲突:
```c
#include <stdio.h>
int main() {
// i仅在for循环内有效
for (int i = 0; i < 5; i++) {
printf("i = %d\n", i);
}
// 错误:i未定义
// printf("%d", i);
return 0;
}
```
#### 3.4.2 基于数组长度的循环
利用`sizeof`计算数组长度,实现通用的数组遍历:
```c
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
// 数组长度 = 总字节数 / 单个元素字节数
int len = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < len; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
return 0;
}
```
### 3.5 for vs while:场景选择
| 场景 | 推荐循环 | 原因 |
|---------------------|----------|--------------------------|
| 已知循环次数 | for | 结构化强,代码简洁 |
| 未知循环次数(仅知终止条件) | while | 初始化/更新灵活 |
| 循环变量在外部复用 | while | 避免变量作用域限制 |
| 多变量控制 | for | 逗号运算符整合多变量 |
| 死循环 | for(;;) | 比while(1)更简洁(习惯问题) |
## 四、do-while循环:后判断循环
`do-while`是唯一一种“先执行、后判断”的循环结构,确保循环体**至少执行一次**,适用于“必须先执行逻辑,再判断是否继续”的场景(如用户输入验证、菜单循环)。
### 4.1 语法规则与执行流程
#### 4.1.1 标准语法
```c
[初始化语句]
do {
循环体代码块;
迭代更新语句;
} while (条件表达式); // 分号不可省略!
```
#### 4.1.2 执行步骤
1. 执行初始化语句(仅一次);
2. 执行循环体代码块;
3. 执行迭代更新语句;
4. 计算条件表达式:
- 非0:回到步骤2;
- 0:跳出循环。
#### 4.1.3 核心规则
- `while`后的分号**必须保留**(最易遗漏的语法错误);
- 条件表达式规则与`while`一致(非0为真);
- 循环体即使只有一条语句,也建议保留`{}`。
### 4.2 基础示例
#### 4.2.1 用户输入验证
```c
#include <stdio.h>
int main() {
int age;
// 必须先输入,再验证
do {
printf("请输入你的年龄(1~120):");
scanf("%d", &age);
if (age < 1 || age > 120) {
printf("年龄非法,请重新输入!\n");
}
} while (age < 1 || age > 120); // 验证不通过则重新输入
printf("你的年龄是:%d\n", age);
return 0;
}
```
#### 4.2.2 菜单循环
```c
#include <stdio.h>
int main() {
int choice;
do {
// 打印菜单(至少执行一次)
printf("\n===== 菜单 =====\n");
printf("1. 查看信息\n");
printf("2. 修改信息\n");
printf("0. 退出\n");
printf("请选择:");
scanf("%d", &choice);
// 分支处理
switch (choice) {
case 1:
printf("查看信息功能\n");
break;
case 2:
printf("修改信息功能\n");
break;
case 0:
printf("退出菜单\n");
break;
default:
printf("选择错误,请重新输入!\n");
}
} while (choice != 0); // 选择0则退出
return 0;
}
```
### 4.3 do-while的常见陷阱
1. **遗漏末尾分号**:
```c
// 错误示例:无分号,编译报错
int i = 0;
do {
printf("%d", i);
i++;
} while (i < 5)
```
2. **误判执行次数**:
```c
// 即使条件为假,仍执行一次
int i = 10;
do {
printf("%d\n", i); // 输出10
i++;
} while (i < 5);
```
3. **循环体为空**:
```c
// 无意义:执行空循环体,仅判断条件
do ; while (i < 5);
```
### 4.4 do-while vs while:核心差异
| 特性 | do-while | while |
|--------------|-------------------------|-------------------------|
| 执行次数 | 至少1次 | 可能0次 |
| 语法 | 末尾必须加分号 | 末尾无分号 |
| 适用场景 | 输入验证、菜单循环 | 通用场景,优先选择 |
| 代码可读性 | 条件在末尾,需跳转查看 | 条件在开头,直观 |
## 五、循环控制语句:break/continue/goto
`break`、`continue`、`goto`是控制循环流程的关键语句,可灵活调整循环的执行路径,是实现复杂循环逻辑的核心工具。
### 5.1 break:跳出当前循环
`break`的核心作用是**立即终止当前所在的循环**(`while`/`for`/`do-while`)或`switch`语句,嵌套循环中仅跳出当前层循环。
#### 5.1.1 单层循环跳出
```c
#include <stdio.h>
int main() {
// 查找1~100中第一个能被17整除的数
for (int i = 1; i <= 100; i++) {
if (i % 17 == 0) {
printf("第一个能被17整除的数:%d\n", i); // 输出17
break; // 找到后立即跳出
}
}
return 0;
}
```
#### 5.1.2 嵌套循环跳出(需标志位)
```c
#include <stdio.h>
int main() {
int found = 0; // 标志位:1=找到,0=未找到
// 嵌套循环:查找二维数组中的值
int arr[3][3] = {{1,2,3}, {4,5,6}, {7,8,9}};
for (int i = 0; i < 3; i++) {
if (found) break; // 外层循环退出
for (int j = 0; j < 3; j++) {
if (arr[i][j] == 5) {
printf("找到5,位置:(%d,%d)\n", i, j);
found = 1;
break; // 内层循环退出
}
}
}
return 0;
}
```
### 5.2 continue:跳过当前迭代
`continue`的核心作用是**跳过当前循环的剩余代码,直接进入下一次迭代的条件判断**,不会终止循环,仅跳过当前轮次。
#### 5.2.1 跳过偶数(打印奇数)
```c
#include <stdio.h>
int main() {
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) {
continue; // 跳过偶数
}
printf("%d ", i); // 输出1 3 5 7 9
}
printf("\n");
return 0;
}
```
#### 5.2.2 跳过空行(读取文件)
```c
#include <stdio.h>
int main() {
char buf[1024];
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
perror("fopen failed");
return 1;
}
while (fgets(buf, sizeof(buf), fp)) {
// 跳过空行(仅含换行符)
if (buf[0] == '\n') {
continue;
}
printf("%s", buf);
}
fclose(fp);
return 0;
}
```
### 5.3 goto:无条件跳转
`goto`是C语言中最具争议的语句,可无条件跳转到函数内的指定标签(`标签名:`),常被诟病“破坏结构化设计”,但在跳出多层循环、错误处理时效率极高。
#### 5.3.1 跳出多层循环
```c
#include <stdio.h>
int main() {
// 嵌套循环:查找值为10的元素
int arr[4][4] = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12},
{13,14,15,16}
};
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (arr[i][j] == 10) {
printf("找到10,位置:(%d,%d)\n", i, j);
goto loop_end; // 直接跳转到循环结束
}
}
}
loop_end: // 标签
printf("循环结束\n");
return 0;
}
```
#### 5.3.2 错误处理(资源释放)
```c
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *fp1 = NULL, *fp2 = NULL;
int *buf = NULL;
// 分配资源
fp1 = fopen("a.txt", "r");
if (fp1 == NULL) {
perror("fopen a.txt failed");
goto cleanup;
}
fp2 = fopen("b.txt", "w");
if (fp2 == NULL) {
perror("fopen b.txt failed");
goto cleanup;
}
buf = malloc(1024);
if (buf == NULL) {
perror("malloc failed");
goto cleanup;
}
// 业务逻辑...
printf("资源分配成功\n");
cleanup: // 统一释放资源
if (fp1) fclose(fp1);
if (fp2) fclose(fp2);
if (buf) free(buf);
return 0;
}
```
### 5.4 控制语句的使用原则
1. **break**:仅用于“提前终止循环”,避免滥用;
2. **continue**:仅用于“跳过当前迭代”,避免循环体嵌套过深;
3. **goto**:
- 允许场景:跳出多层循环、统一错误处理/资源释放;
- 禁止场景:正向跳转(破坏顺序执行)、跨函数跳转;
4. 嵌套循环中优先使用“标志位+break”,而非`goto`(提升可读性)。
## 六、循环结构的实战案例
### 6.1 案例1:素数判断(高效版)
素数(质数)是只能被1和自身整除的大于1的整数,循环优化核心是“仅判断到√n”,减少循环次数。
```c
#include <stdio.h>
#include <math.h>
// 判断素数:是返回1,否返回0
int is_prime(int n) {
if (n < 2) return 0; // 小于2不是素数
if (n == 2) return 1; // 2是唯一偶素数
if (n % 2 == 0) return 0; // 偶数直接排除
// 优化:仅判断奇数,且到√n
for (int i = 3; i <= sqrt(n); i += 2) {
if (n % i == 0) {
return 0;
}
}
return 1;
}
int main() {
int num;
printf("请输入整数:");
scanf("%d", &num);
if (is_prime(num)) {
printf("%d是素数\n", num);
} else {
printf("%d不是素数\n", num);
}
// 拓展:打印1~100的素数
printf("\n1~100的素数:");
for (int i = 2; i <= 100; i++) {
if (is_prime(i)) {
printf("%d ", i);
}
}
printf("\n");
return 0;
}
```
### 6.2 案例2:斐波那契数列(循环版)
斐波那契数列:1,1,2,3,5,8,13...,用循环实现比递归更高效(无栈溢出风险)。
```c
#include <stdio.h>
int main() {
int n;
printf("请输入斐波那契数列项数:");
scanf("%d", &n);
if (n < 1) {
printf("输入非法!\n");
return 1;
}
printf("斐波那契数列前%d项:", n);
int a = 1, b = 1;
for (int i = 1; i <= n; i++) {
if (i == 1 || i == 2) {
printf("%d ", 1);
} else {
int c = a + b;
printf("%d ", c);
a = b;
b = c;
}
}
printf("\n");
return 0;
}
```
### 6.3 案例3:数组排序(冒泡排序)
冒泡排序是基于循环的经典排序算法,核心是通过嵌套循环反复交换相邻元素,将最大值“冒泡”到末尾。
```c
#include <stdio.h>
void bubble_sort(int arr[], int len) {
// 外层:控制排序轮数(len-1轮)
for (int i = 0; i < len - 1; i++) {
int swapped = 0; // 优化:标记是否交换
// 内层:每轮比较次数递减
for (int j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j+1]) {
// 交换元素
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
swapped = 1;
}
}
if (!swapped) break; // 无交换,提前退出
}
}
int main() {
int arr[] = {5, 2, 9, 1, 5, 6};
int len = sizeof(arr) / sizeof(arr[0]);
printf("排序前:");
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
bubble_sort(arr, len);
printf("\n排序后:");
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
```
## 七、循环的性能优化
循环是程序性能的核心瓶颈(尤其是高频执行的循环),以下优化手段可显著提升循环效率:
### 7.1 减少循环内的计算
将循环内不变的计算移到循环外,避免重复计算:
```c
// 优化前:每次循环计算strlen(s)(O(n))
char s[] = "Hello, World!";
for (int i = 0; i < strlen(s); i++) {
// ...
}
// 优化后:仅计算一次(O(1))
int len = strlen(s);
for (int i = 0; i < len; i++) {
// ...
}
```
### 7.2 循环展开
对于固定次数的小循环,手动展开循环体,减少条件判断和迭代更新的开销:
```c
// 优化前:4次循环,4次条件判断
for (int i = 0; i < 4; i++) {
printf("%d ", i);
}
// 优化后:无循环,直接执行
printf("0 ");
printf("1 ");
printf("2 ");
printf("3 ");
```
### 7.3 使用前置自增(++i)替代后置自增(i++)
`++i`直接修改变量,无临时变量开销;`i++`需生成临时变量存储原值,效率略低:
```c
// 优化前
for (int i = 0; i < 1000000; i++) { ... }
// 优化后
for (int i = 0; i < 1000000; ++i) { ... }
```
### 7.4 避免循环内的内存分配/释放
`malloc`/`free`、`fopen`/`fclose`等系统调用开销大,应移到循环外:
```c
// 优化前:每次循环分配/释放内存
for (int i = 0; i < 100; i++) {
int *buf = malloc(1024);
// 使用buf...
free(buf);
}
// 优化后:一次分配,多次使用
int *buf = malloc(1024);
for (int i = 0; i < 100; i++) {
// 使用buf...
}
free(buf);
```
### 7.5 利用编译器优化
开启编译器优化(如GCC的`-O2`/`-O3`),编译器会自动进行循环展开、常量传播、死代码消除等优化:
```bash
# 编译时开启O2优化
gcc -O2 loop.c -o loop
```
## 八、循环结构的最佳实践
### 8.1 代码可读性
1. **缩进规范**:嵌套循环每层缩进4个空格,避免“面条代码”;
2. **变量命名**:循环变量用`i`/`j`(简单循环)或`row`/`col`/`index`(复杂循环);
3. **注释关键逻辑**:对循环的终止条件、迭代规则添加注释;
4. **拆分复杂循环**:嵌套超过3层时,拆分为独立函数。
### 8.2 鲁棒性
1. **边界检查**:循环条件避免`i <= len`(数组越界),改用`i < len`;
2. **空循环保护**:对可能为空的数组/字符串,先判断长度再循环;
3. **浮点循环替代**:避免用浮点数作为循环变量,改用整数;
4. **资源释放**:循环内分配的资源(如文件、内存)必须在循环结束前释放。
### 8.3 可维护性
1. **避免魔法数字**:用宏定义替代循环次数、边界值(如`#define MAX_LEN 100`);
2. **统一循环风格**:项目内统一使用`for`/`while`(如已知次数用`for`);
3. **复用循环逻辑**:将通用循环(如数组遍历、累加计算)封装为函数。
## 九、总结
循环结构是C语言实现“自动化重复执行”的核心,三种原生循环(`while`/`for`/`do-while`)各有适用场景:
- `for`:结构化最强,优先用于已知循环次数的场景;
- `while`:灵活性最高,适用于未知循环次数的通用场景;
- `do-while`:确保至少执行一次,适用于输入验证、菜单循环。
掌握循环的关键在于:
1. 理解循环的三要素(初始化、条件判断、迭代更新);
2. 灵活运用`break`/`continue`/`goto`控制流程;
3. 规避死循环、边界溢出、精度丢失等常见陷阱;
4. 兼顾性能与可读性,通过优化提升效率,通过规范提升可维护性。
从简单的累加计算到复杂的排序算法,从控制台程序到系统级应用,循环结构始终是C语言程序的核心骨架。遵循本文的最佳实践,可写出更健壮、高效、易维护的循环代码。
更多推荐



所有评论(0)