引言

C语言作为一门经典的编程语言,以其高效、灵活和接近硬件的特性,在系统编程、嵌入式开发以及算法竞赛中占据重要地位。上机实验是学习C语言不可或缺的环节,通过实际编写和调试代码,可以加深对语法、数据结构和算法的理解。本文精选了从基础语法到复杂算法的典型上机实验题目,提供详细的解析和完整的代码示例,帮助读者进行实战演练。每个题目都包含问题描述、解题思路、代码实现和关键点分析,旨在提升编程能力和调试技巧。

1. 基础语法实验:输入输出与算术运算

1.1 题目:计算两个整数的和、差、积、商和余数

问题描述:编写一个C程序,从键盘输入两个整数a和b,计算并输出它们的和(a+b)、差(a-b)、积(a*b)、商(a/b)和余数(a%b)。注意处理除数为零的情况。

解题思路

  • 使用scanf函数读取输入。
  • 使用条件语句检查b是否为零,如果是,则输出错误信息。
  • 计算并使用printf输出结果,格式化输出以提高可读性。
  • 关键点:整数除法会截断小数部分,余数运算仅适用于整数。

代码实现

#include <stdio.h>

int main() {
    int a, b;
    printf("请输入两个整数(用空格分隔):");
    scanf("%d %d", &a, &b);

    if (b == 0) {
        printf("错误:除数不能为零!\n");
        return 1;
    }

    printf("和:%d\n", a + b);
    printf("差:%d\n", a - b);
    printf("积:%d\n", a * b);
    printf("商:%d\n", a / b);
    printf("余数:%d\n", a % b);

    return 0;
}

关键点分析

  • scanf("%d %d", &a, &b):从标准输入读取两个整数,&操作符用于获取变量地址。
  • if (b == 0):防止除零错误,这是编程中的常见安全检查。
  • 输出使用换行符\n分隔,便于阅读。运行示例:输入3 2,输出和5、差1、积6、商1、余数1。

1.2 题目:判断素数(质数)

问题描述:编写程序输入一个正整数n,判断它是否为素数。素数是大于1且只能被1和自身整除的数。

解题思路

  • 从2到sqrt(n)遍历,检查是否有因子。
  • 如果有因子,则不是素数;否则是素数。
  • 优化:只需检查到sqrt(n),因为如果n有大于sqrt(n)的因子,必有小于sqrt(n)的对应因子。
  • 关键点:处理n<=1的特殊情况。

代码实现

#include <stdio.h>
#include <math.h>

int main() {
    int n, i;
    int isPrime = 1;  // 假设是素数

    printf("请输入一个正整数:");
    scanf("%d", &n);

    if (n <= 1) {
        isPrime = 0;
    } else {
        for (i = 2; i <= sqrt(n); i++) {
            if (n % i == 0) {
                isPrime = 0;
                break;
            }
        }
    }

    if (isPrime) {
        printf("%d 是素数。\n", n);
    } else {
        printf("%d 不是素数。\n", n);
    }

    return 0;
}

关键点分析

  • #include <math.h>:使用sqrt函数计算平方根,需要链接数学库(编译时加-lm)。
  • isPrime标志变量:简化逻辑,避免多层if嵌套。
  • break:一旦找到因子,立即退出循环,提高效率。示例:输入17,输出是素数;输入9,输出不是素数。

2. 控制结构实验:循环与条件

2.1 题目:计算1到100的偶数和

问题描述:使用循环计算1到100之间所有偶数的和,并输出结果。

解题思路

  • 使用for循环遍历1到100。
  • 通过if判断是否为偶数(n % 2 == 0),累加到sum。
  • 替代方案:直接循环偶数(i=2; i<=100; i+=2)。
  • 关键点:初始化sum为0,避免垃圾值。

代码实现

#include <stdio.h>

int main() {
    int sum = 0;
    int i;

    for (i = 1; i <= 100; i++) {
        if (i % 2 == 0) {
            sum += i;
        }
    }

    printf("1到100的偶数和为:%d\n", sum);
    return 0;
}

关键点分析

  • sum += i:等价于sum = sum + i,是累加的标准写法。
  • 循环变量i从1开始,确保覆盖所有数。运行结果:2450。
  • 扩展:如果使用while循环,可改为i=1; while(i<=100){... i++;},展示不同循环结构。

2.2 题目:斐波那契数列前n项

问题描述:输入n,输出斐波那契数列的前n项。斐波那契数列:F(1)=1, F(2)=1, F(n)=F(n-1)+F(n-2)(n>2)。

解题思路

  • 使用迭代方法,避免递归的低效。
  • 初始化前两项为1,然后循环计算后续项。
  • 关键点:n较小时直接输出,n较大时注意整数溢出(使用long long)。

代码实现

#include <stdio.h>

int main() {
    int n;
    long long a = 1, b = 1, temp;

    printf("请输入n(n>=1):");
    scanf("%d", &n);

    if (n <= 0) {
        printf("n必须大于0!\n");
        return 1;
    }

    printf("斐波那契数列前%d项:\n", n);
    if (n >= 1) printf("%lld ", a);
    if (n >= 2) printf("%lld ", b);

    for (int i = 3; i <= n; i++) {
        temp = a + b;
        printf("%lld ", temp);
        a = b;
        b = temp;
    }
    printf("\n");

    return 0;
}

关键点分析

  • 使用long long防止大数溢出,斐波那契增长很快。
  • temp变量用于交换a和b,模拟递推关系。
  • 示例:n=5,输出1 1 2 3 5。迭代比递归高效,避免栈溢出。

3. 数组与字符串实验

3.1 题目:求数组最大值和最小值

问题描述:定义一个整型数组,输入5个元素,找出最大值和最小值并输出。

解题思路

  • 使用scanf输入数组元素。
  • 初始化max和min为数组第一个元素,然后遍历更新。
  • 关键点:数组下标从0开始,遍历时从1开始比较。

代码实现

#include <stdio.h>

int main() {
    int arr[5];
    int max, min;
    int i;

    printf("请输入5个整数:\n");
    for (i = 0; i < 5; i++) {
        scanf("%d", &arr[i]);
    }

    max = min = arr[0];
    for (i = 1; i < 5; i++) {
        if (arr[i] > max) max = arr[i];
        if (arr[i] < min) min = arr[i];
    }

    printf("最大值:%d\n", max);
    printf("最小值:%d\n", min);

    return 0;
}

关键点分析

  • arr[5]:固定大小数组,C语言中数组大小必须在编译时确定。
  • 循环从i=1开始,避免与自身比较。示例输入:3 5 1 8 2,输出max=8, min=1。

3.2 题目:字符串反转

问题描述:输入一个字符串,将其反转后输出。例如,输入”hello”,输出”olleh”。

解题思路

  • 使用字符数组存储字符串。
  • 双指针法:一个从头,一个从尾,交换字符直到相遇。
  • 关键点:字符串以’\0’结束,反转时不包括它。

代码实现

#include <stdio.h>
#include <string.h>

int main() {
    char str[100];
    int len, i;
    char temp;

    printf("请输入一个字符串:");
    gets(str);  // 注意:gets不安全,实际中用fgets

    len = strlen(str);
    for (i = 0; i < len / 2; i++) {
        temp = str[i];
        str[i] = str[len - 1 - i];
        str[len - 1 - i] = temp;
    }

    printf("反转后:%s\n", str);
    return 0;
}

关键点分析

  • strlen(str):获取字符串长度,不包括’\0’。
  • 交换使用临时变量temp。示例:输入”world”,输出”dlrow”。
  • 安全提示:gets易导致缓冲区溢出,生产环境用fgets(str, 100, stdin)

4. 函数与递归实验

4.1 题目:计算阶乘(递归实现)

问题描述:编写递归函数计算n的阶乘(n! = n(n-1)…*1),输入n输出结果。

解题思路

  • 递归基:n=0或1时返回1。
  • 递归式:n! = n * factorial(n-1)。
  • 关键点:防止栈溢出,n不宜过大(通常n<20)。

代码实现

#include <stdio.h>

long long factorial(int n) {
    if (n < 0) return -1;  // 错误处理
    if (n == 0 || n == 1) return 1;
    return n * factorial(n - 1);
}

int main() {
    int n;
    printf("请输入n:");
    scanf("%d", &n);

    long long result = factorial(n);
    if (result == -1) {
        printf("n不能为负数!\n");
    } else {
        printf("%d! = %lld\n", n, result);
    }

    return 0;
}

关键点分析

  • 函数返回long long,因为阶乘增长快(20! ≈ 2.4e18)。
  • 递归深度为n,示例:n=5,输出120。递归简洁但效率低,迭代版本可作为对比。

4.2 题目:最大公约数(GCD)和最小公倍数(LCM)

问题描述:输入两个正整数a和b,计算GCD和LCM。GCD用欧几里得算法(辗转相除法)。

解题思路

  • GCD:a % b == 0 ? b : gcd(b, a % b)。
  • LCM:a * b / gcd(a, b)。
  • 关键点:确保a >= b,否则交换。

代码实现

#include <stdio.h>

int gcd(int a, int b) {
    if (b == 0) return a;
    return gcd(b, a % b);
}

int lcm(int a, int b) {
    return a * b / gcd(a, b);
}

int main() {
    int a, b;
    printf("请输入两个正整数:");
    scanf("%d %d", &a, &b);

    if (a <= 0 || b <= 0) {
        printf("输入必须为正整数!\n");
        return 1;
    }

    int g = gcd(a, b);
    int l = lcm(a, b);

    printf("GCD:%d\n", g);
    printf("LCM:%d\n", l);

    return 0;
}

关键点分析

  • 欧几里得算法高效,时间复杂度O(log min(a,b))。
  • 示例:a=12, b=18,GCD=6, LCM=36。递归实现简洁,但可改为迭代以避免递归开销。

5. 指针实验

5.1 题目:使用指针交换两个变量的值

问题描述:编写函数使用指针交换两个整数的值。

解题思路

  • 函数参数为指针,通过解引用交换。
  • 关键点:指针必须指向有效内存,避免空指针。

代码实现

#include <stdio.h>

void swap(int *p, int *q) {
    int temp = *p;
    *p = *q;
    *q = temp;
}

int main() {
    int a = 5, b = 10;
    printf("交换前:a=%d, b=%d\n", a, b);
    swap(&a, &b);
    printf("交换后:a=%d, b=%d\n", a, b);
    return 0;
}

关键点分析

  • &a获取地址,*p解引用访问值。
  • 示例:输出交换后a=10, b=5。指针函数常用于修改原变量。

5.2 题目:动态分配数组并求和

问题描述:使用malloc动态分配数组,输入n个元素,求和后释放内存。

解题思路

  • malloc分配空间,检查是否成功。
  • 输入元素,求和,free释放。
  • 关键点:内存泄漏检查,使用free

代码实现

#include <stdio.h>
#include <stdlib.h>

int main() {
    int n, *arr, sum = 0;

    printf("请输入元素个数:");
    scanf("%d", &n);

    arr = (int*)malloc(n * sizeof(int));
    if (arr == NULL) {
        printf("内存分配失败!\n");
        return 1;
    }

    printf("请输入%d个整数:\n", n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &arr[i]);
        sum += arr[i];
    }

    printf("和:%d\n", sum);
    free(arr);  // 释放内存

    return 0;
}

关键点分析

  • malloc(n * sizeof(int)):分配n个int的空间。
  • 检查NULL防止崩溃。示例:n=3, 输入1 2 3, 输出6。动态数组适合大小不确定时。

6. 结构体与文件操作实验

6.1 题目:定义学生结构体,存储并输出信息

问题描述:定义学生结构体(姓名、学号、成绩),输入信息,输出平均成绩。

解题思路

  • 使用struct定义结构体。
  • 输入到结构体数组,计算平均。
  • 关键点:字符串成员用字符数组。

代码实现

#include <stdio.h>

struct Student {
    char name[50];
    int id;
    float score;
};

int main() {
    struct Student stu[3];
    float avg = 0;

    for (int i = 0; i < 3; i++) {
        printf("输入学生%d的姓名、学号、成绩:", i+1);
        scanf("%s %d %f", stu[i].name, &stu[i].id, &stu[i].score);
        avg += stu[i].score;
    }

    avg /= 3;
    printf("平均成绩:%.2f\n", avg);

    return 0;
}

关键点分析

  • .操作符访问成员。
  • 示例:输入3个学生,输出平均。结构体组织相关数据。

6.2 题目:文件读写:写入并读取整数

问题描述:将1到10写入文件,然后读取并求和。

解题思路

  • fopen打开文件,fprintf写入,fscanf读取。
  • 关键点:检查文件打开成功,关闭文件。

代码实现

#include <stdio.h>

int main() {
    FILE *fp;
    int i, num, sum = 0;

    // 写入
    fp = fopen("numbers.txt", "w");
    if (fp == NULL) {
        printf("无法打开文件写入!\n");
        return 1;
    }
    for (i = 1; i <= 10; i++) {
        fprintf(fp, "%d\n", i);
    }
    fclose(fp);

    // 读取
    fp = fopen("numbers.txt", "r");
    if (fp == NULL) {
        printf("无法打开文件读取!\n");
        return 1;
    }
    while (fscanf(fp, "%d", &num) == 1) {
        sum += num;
    }
    fclose(fp);

    printf("文件中1到10的和:%d\n", sum);
    return 0;
}

关键点分析

  • “w”模式覆盖写入,”r”只读。
  • fclose释放资源。运行后生成numbers.txt,内容为1到10,输出和55。

7. 复杂算法实验:排序与搜索

7.1 题目:冒泡排序

问题描述:输入n个整数,使用冒泡排序升序输出。

解题思路

  • 双重循环:外层控制轮数,内层相邻比较交换。
  • 优化:如果一轮无交换,提前结束。
  • 关键点:时间复杂度O(n^2),适合小数据。

代码实现

#include <stdio.h>

void bubbleSort(int arr[], int n) {
    int i, j, swapped;
    for (i = 0; i < n - 1; i++) {
        swapped = 0;
        for (j = 0; j < n - i - 1; 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 n, arr[100];
    printf("输入n(<=100):");
    scanf("%d", &n);
    printf("输入%d个整数:\n", n);
    for (int i = 0; i < n; i++) scanf("%d", &arr[i]);

    bubbleSort(arr, n);

    printf("排序后:");
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");
    return 0;
}

关键点分析

  • swapped优化避免无谓循环。
  • 示例:输入5 3 2 4 1,输出1 2 3 4 5。冒泡稳定但慢,可扩展到快速排序。

7.2 题目:二分查找

问题描述:在已排序数组中查找元素,返回索引或-1。

解题思路

  • 二分:low=0, high=n-1, mid=(low+high)/2。
  • 比较arr[mid]与目标,调整low/high。
  • 关键点:数组必须有序,时间复杂度O(log n)。

代码实现

#include <stdio.h>

int binarySearch(int arr[], int n, int target) {
    int low = 0, high = n - 1;
    while (low <= high) {
        int mid = low + (high - low) / 2;  // 防溢出
        if (arr[mid] == target) return mid;
        else if (arr[mid] < target) low = mid + 1;
        else high = mid - 1;
    }
    return -1;
}

int main() {
    int arr[] = {1, 3, 5, 7, 9};
    int n = 5, target;
    printf("输入要查找的数:");
    scanf("%d", &target);

    int index = binarySearch(arr, n, target);
    if (index != -1) printf("找到,索引:%d\n", index);
    else printf("未找到\n");

    return 0;
}

关键点分析

  • mid = low + (high - low) / 2:防止(low+high)溢出。
  • 示例:查找5,输出索引2。二分高效,适用于大数据。

8. 综合实战:链表基础

8.1 题目:单链表创建与遍历

问题描述:创建单链表存储整数,输入数据,遍历输出。

解题思路

  • 定义节点结构体(数据+指针)。
  • 尾插法创建链表,遍历打印。
  • 关键点:动态内存分配,释放链表避免泄漏。

代码实现

#include <stdio.h>
#include <stdlib.h>

struct Node {
    int data;
    struct Node *next;
};

struct Node* createList(int n) {
    struct Node *head = NULL, *tail = NULL, *newNode;
    int val;
    for (int i = 0; i < n; i++) {
        printf("输入第%d个数据:", i+1);
        scanf("%d", &val);
        newNode = (struct Node*)malloc(sizeof(struct Node));
        newNode->data = val;
        newNode->next = NULL;
        if (head == NULL) {
            head = newNode;
            tail = newNode;
        } else {
            tail->next = newNode;
            tail = newNode;
        }
    }
    return head;
}

void printList(struct Node *head) {
    struct Node *current = head;
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

void freeList(struct Node *head) {
    struct Node *temp;
    while (head != NULL) {
        temp = head;
        head = head->next;
        free(temp);
    }
}

int main() {
    int n;
    printf("输入节点数:");
    scanf("%d", &n);
    struct Node *head = createList(n);
    printList(head);
    freeList(head);
    return 0;
}

关键点分析

  • malloc创建节点,->访问成员。
  • 示例:输入3个数1 2 3,输出1 -> 2 -> 3 -> NULL。链表是动态数据结构基础,可扩展到插入/删除。

结语

通过以上从基础到复杂的实验题目,读者可以系统练习C语言的核心概念。每个代码都可直接编译运行(使用gcc编译器,如gcc program.c -o program -lm链接数学库)。建议在实际环境中调试,遇到问题使用gdb或打印调试。持续练习将显著提升编程能力,为更高级的算法和项目打下坚实基础。如果需要更多题目或特定扩展,请提供反馈。