引言:为什么实验四如此重要?

在湖北理工学院的C语言程序设计课程中,实验四通常是一个关键的转折点。这个实验旨在帮助学生从简单的顺序执行和单层逻辑跨越到更复杂的编程结构——循环嵌套函数调用。这两个概念是编程的核心,掌握它们不仅能轻松应对大学的编程作业,更是未来学习数据结构、算法以及大型项目开发的基础。

很多同学在面对实验四的题目时,常常感到困惑:循环为什么要嵌套?函数调用到底怎么传参?为什么程序运行的结果总是不对?别担心,本篇文章将从最基础的概念讲起,通过详细的步骤、完整的代码示例和实际应用场景,带你彻底攻克实验四的难点,实现从入门到精通的飞跃。


第一部分:循环嵌套(Nested Loops)—— 打印图形与处理矩阵的利器

1.1 什么是循环嵌套?

简单来说,循环嵌套就是在一个循环语句的循环体内部,再包含另一个循环语句。最常见的是两层嵌套,即双重循环,通常用于处理二维数据,比如矩阵、表格或打印各种图形。

核心逻辑:外层循环每执行一次,内层循环就会完整地从头到尾执行一遍。

1.2 经典案例:打印九九乘法表

九九乘法表是学习循环嵌套的“Hello World”。它完美展示了外层循环控制行数,内层循环控制列数的概念。

题目要求:在屏幕上打印出如下格式的九九乘法表:

1*1=1
1*2=2  2*2=4
1*3=3  2*3=6  3*3=9
...

代码实现与详解

#include <stdio.h>

int main() {
    // 定义两个整型变量 i 和 j,分别用于控制外层和内层循环
    int i, j;

    // 外层循环:控制行数,从1循环到9
    for (i = 1; i <= 9; i++) {
        
        // 内层循环:控制列数,每一行的列数不超过当前的行数
        // 例如第3行,只打印到3*3=9,所以 j <= i
        for (j = 1; j <= i; j++) {
            
            // 打印乘法表达式,\t是制表符,用于对齐
            // %-4d 表示左对齐,占用4个字符宽度,使输出更整齐
            printf("%d*%d=%-4d", i, j, i * j);
        }
        
        // 每打印完一行后,必须换行,否则所有内容会挤在一行
        printf("\n");
    }

    return 0;
}

代码执行流程分析

  1. i = 1:进入第一行。
    • j = 1,满足 j <= i,打印 1*1=1
    • j++ 变为2,不满足 j <= i,内层循环结束。
    • 执行 printf("\n"),换行。
  2. i = 2:进入第二行。
    • j = 1,满足 j <= i,打印 1*2=2
    • j++ 变为2,满足 j <= i,打印 2*2=4
    • j++ 变为3,不满足 j <= i,内层循环结束。
    • 执行 printf("\n"),换行。
  3. …以此类推,直到 i=9 循环结束。

1.3 进阶案例:打印菱形图案

打印菱形是实验四中难度较高的题目,它需要结合数学逻辑(对称性)和三层循环(甚至更多)。

题目要求:输入一个整数 n(例如5),打印出高度为 2n-1 的菱形。 例如 n=5 时:

    *
   ***
  *****
 *******
*********
 *******
  *****
   ***
    *

代码实现与详解

#include <stdio.h>

int main() {
    int n, i, j, k;
    
    printf("请输入菱形上半部分的行数(半径): ");
    scanf("%d", &n);

    // --- 上半部分(包括中间行) ---
    // 外层循环控制行数,从1到n
    for (i = 1; i <= n; i++) {
        
        // 第一层内层循环:打印空格
        // 空格数 = n - i
        for (j = 1; j <= n - i; j++) {
            printf(" ");
        }
        
        // 第二层内层循环:打印星号
        // 星号数 = 2 * i - 1
        for (k = 1; k <= 2 * i - 1; k++) {
            printf("*");
        }
        
        printf("\n");
    }

    // --- 下半部分 ---
    // 下半部分的行数是 n-1
    for (i = 1; i <= n - 1; i++) {
        
        // 打印空格,数量逐渐增加
        // 空格数 = i
        for (j = 1; j <= i; j++) {
            printf(" ");
        }
        
        // 打印星号,数量逐渐减少
        // 星号数 = 2 * (n - i) - 1
        for (k = 1; k <= 2 * (n - i) - 1; k++) {
            printf("*");
        }
        
        printf("\n");
    }

    return 0;
}

难点解析

  • 找规律:菱形的核心在于找空格和星号的数量关系。上半部分,随着行数 i 增加,空格减少(n-i),星号增加(2i-1)。
  • 对称性:下半部分是上半部分的镜像。下半部分的行数 i 从1开始,空格数 i 逐渐增加,星号数 2*(n-i)-1 逐渐减少。
  • 三层循环:这里实际上用了两个并列的内层循环(一个管空格,一个管星号),逻辑上属于两层嵌套,但代码结构上看起来像三层,这是图形打印的常用技巧。

第二部分:函数调用(Function Call)—— 模块化编程的基石

2.1 为什么要用函数?

在没有函数之前,所有的代码都写在 main 函数里。当代码量超过50行,逻辑就会变得非常混乱。函数的作用就是将特定的功能封装起来,实现代码复用,让程序结构更清晰。

函数的定义格式

返回值类型 函数名(参数类型 参数名, 参数类型 参数名) {
    // 函数体
    return 返回值; // 如果返回值类型是void,则不需要return
}

2.2 案例:编写一个判断素数的函数

题目要求:编写一个函数 isPrime(int n),如果 n 是素数(质数),返回1;否则返回0。在主函数中调用该函数,输出100以内的所有素数。

代码实现与详解

#include <stdio.h>

// 函数声明(也叫函数原型)
// 告诉编译器有这个函数的存在,参数是什么类型,返回值是什么类型
int isPrime(int n);

int main() {
    int i;
    printf("100以内的素数有:\n");
    
    // 遍历2到100的数
    for (i = 2; i <= 100; i++) {
        // 调用函数,如果返回1,说明是素数,打印
        if (isPrime(i) == 1) {
            printf("%d ", i);
        }
    }
    printf("\n");
    return 0;
}

// 函数定义
// 功能:判断一个数是否为素数
int isPrime(int n) {
    int i;
    // 优化:只需要判断到 sqrt(n) 即可,但为了初学者易懂,这里写到 n-1
    // 只要能被 2 到 n-1 之间的任意一个数整除,就不是素数
    for (i = 2; i < n; i++) {
        if (n % i == 0) {
            return 0; // 不是素数,返回0
        }
    }
    return 1; // 循环结束都没被整除,是素数,返回1
}

关键点解析

  1. 函数声明:在 main 函数之前或内部声明 isPrime 是非常必要的,否则编译器会报错(除非把函数定义写在 main 之前)。
  2. 参数传递isPrime(i) 中的 i 是实参,函数定义中的 n 是形参。调用时,实参的值会赋给形参。
  3. 逻辑封装:判断素数的复杂逻辑被隐藏在 isPrime 函数内部,main 函数只需要关心“调用它”和“得到结果”,这就是模块化的好处。

2.3 实验四常见题型:成绩统计系统

很多学校的实验四会要求结合循环和函数做一个小系统,比如统计班级成绩。

题目要求

  1. 编写函数 float findMax(float arr[], int n):求数组中的最高分。
  2. 编写函数 float calculateAvg(float arr[], int n):求平均分。
  3. main 函数中输入5个学生的成绩,调用上述函数并输出结果。

完整代码示例

#include <stdio.h>

// 函数声明
float findMax(float arr[], int n);
float calculateAvg(float arr[], int n);

int main() {
    // 定义数组存储成绩
    float scores[5];
    int i;

    // 1. 输入部分:利用循环输入数据
    printf("请输入5位同学的成绩:\n");
    for (i = 0; i < 5; i++) {
        printf("第%d位: ", i + 1);
        scanf("%f", &scores[i]);
    }

    // 2. 调用函数部分
    float maxScore = findMax(scores, 5);
    float avgScore = calculateAvg(scores, 5);

    // 3. 输出结果
    printf("\n--- 统计结果 ---\n");
    printf("最高分: %.1f\n", maxScore);
    printf("平均分: %.1f\n", avgScore);

    return 0;
}

// 函数定义:求最大值
float findMax(float arr[], int n) {
    float max = arr[0]; // 假设第一个元素最大
    for (int i = 1; i < n; i++) {
        if (arr[i] > max) {
            max = arr[i]; // 发现更大的,更新max
        }
    }
    return max;
}

// 函数定义:求平均值
float calculateAvg(float arr[], int n) {
    float sum = 0.0;
    for (int i = 0; i < n; i++) {
        sum += arr[i]; // 累加
    }
    return sum / n; // 求平均
}

代码深度剖析

  • 数组作为参数:注意函数定义中 float arr[],在C语言中,数组作为参数传递给函数时,传递的是数组的首地址。这意味着在函数内部修改 arr 会影响外部的数组(虽然本例中没有修改,只是读取)。
  • 循环与函数的结合main 函数里的 for 循环负责数据采集,而函数内部的 for 循环负责数据处理。这就是典型的“输入-处理-输出”模型。

第三部分:实验四中的常见错误与调试技巧

在完成实验四时,你可能会遇到以下问题,这里提供解决方案:

3.1 循环相关的错误

  1. 死循环(Infinite Loop)

    • 现象:程序运行后停不下来,或者输出大量重复内容。
    • 原因:循环变量没有正确更新。例如 while(i < 10) 但循环体内没有 i++
    • 解决:仔细检查循环变量的初值、条件和步长。
  2. 图形打印不对称

    • 现象:打印的三角形、菱形歪了。
    • 原因:空格和星号的数量公式写错,或者内层循环的条件写反(比如写成了 j < n 而不是 j < n-i)。
    • 解决:在纸上画出行和列,手动推导公式,或者在循环内部加入 printf 打印调试信息。

3.2 函数相关的错误

  1. “undefined reference to” 错误

    • 原因:函数声明了但没有定义,或者定义的函数名拼写与声明不一致。
    • 解决:检查函数名拼写,确保函数定义存在且在 main 函数之前(或者有声明)。
  2. 参数传递错误

    • 现象:函数计算结果总是0或乱码。
    • 原因:数组传递时忘记传递长度 n,导致函数内部不知道数组有多大,越界访问了内存。
    • 解决:养成好习惯,数组作为参数时,一定要同时传递数组长度。
  3. 返回值类型不匹配

    • 现象:函数声明为 int,但内部没有 return 语句,或者返回了 float
    • 解决:确保函数的返回类型与 return 后面的数据类型一致。

第四部分:综合实战——约瑟夫环问题

为了彻底掌握循环嵌套与函数调用,我们来看一个经典的进阶题目:约瑟夫环(Josephus Problem)

题目描述:N个人围成一圈,从第一个人开始报数,数到M的人出圈,然后从下一个人重新开始报数,直到所有人都出圈。求出圈的顺序。

解题思路

  1. 这是一个典型的循环问题,需要使用数组模拟圆圈。
  2. 需要双重循环:外层循环控制出圈的人数(直到所有人都出圈),内层循环控制报数过程。
  3. 可以封装成函数来处理逻辑。

代码实现

#include <stdio.h>
#define N 10 // 假设10个人
#define M 3  // 假设数到3出圈

// 函数:模拟约瑟夫环过程
// 参数:n是总人数,m是报数上限
void josephus(int n, int m) {
    int i, j, k;
    int count = 0; // 记录当前报数
    int out_count = 0; // 记录已经出圈的人数
    int person[N]; // 0表示在圈内,1表示已出圈

    // 初始化:所有人都在圈内
    for (i = 0; i < n; i++) {
        person[i] = 0;
    }

    // 循环直到所有人都出圈
    while (out_count < n) {
        // 遍历所有人
        for (i = 0; i < n; i++) {
            // 如果这个人已经出圈,跳过
            if (person[i] == 1) continue;

            // 如果这个人还在圈内,报数
            count++;
            
            // 判断是否数到M
            if (count == m) {
                person[i] = 1; // 标记为出圈
                out_count++;   // 出圈人数+1
                printf("第%d个出圈的人是: %d\n", out_count, i + 1); // i+1是因为数组下标从0开始
                count = 0;     // 重置报数,下一个人从1开始报
            }
        }
    }
}

int main() {
    printf("总人数: %d, 报数到 %d 出圈\n", N, M);
    josephus(N, M);
    return 0;
}

代码解析

  • 模拟技巧:使用数组 person 标记状态,这是解决这类问题的常用技巧。
  • 循环逻辑while 循环控制总进度,内部的 for 循环模拟每一轮报数。continue 语句在这里非常关键,用于跳过已经出圈的人。
  • 函数封装:将复杂的模拟过程封装在 josephus 函数中,main 函数非常简洁。

总结

实验四的核心在于逻辑思维的抽象化

  1. 循环嵌套教会你如何处理二维数据和重复的重复操作,它是图形打印、矩阵运算的基础。
  2. 函数调用教会你如何将大问题拆解为小问题,它是模块化编程、团队协作的基础。

当你遇到难题时,不要急着写代码。先在纸上画出流程图,理清输入是什么,输出是什么,中间需要几步处理。如果步骤复杂,就尝试把这些步骤拆分成一个个小函数。

希望这篇详解能帮助你顺利完成湖北理工C语言实验四,不仅是为了完成作业,更是为了真正掌握编程的精髓!加油!