引言:C语言实验教程的重要性与学习价值

C语言作为计算机科学与技术专业的基础核心课程,其实验环节对于巩固理论知识、培养编程思维具有不可替代的作用。魏英教授编写的《C语言程序设计实验教程第二版》是众多高校广泛采用的实验教材,该书通过系统化的实验项目设计,帮助学生从基础语法到复杂算法逐步掌握C语言编程技能。

本教程的特点在于:

  • 循序渐进:从简单的输入输出到复杂的数据结构应用
  • 理论与实践结合:每个实验都对应理论课的知识点
  • 项目驱动:通过实际案例让学生理解编程的实际应用价值
  • 错误处理:详细分析常见错误类型及调试方法

实验一:C语言开发环境与基础语法实验

实验目标

  1. 熟悉C语言开发环境(Visual Studio/Code::Blocks/Dev-C++)
  2. 掌握C程序的基本结构
  3. 理解变量定义、数据类型和基本输入输出
  4. 编写并调试第一个C程序

实验内容详解

1. 开发环境配置

推荐环境:Visual Studio Code + MinGW-w64 或 Visual Studio Community

配置步骤

# 1. 安装MinGW-w64编译器
# 下载地址:https://sourceforge.net/projects/mingw-w64/
# 安装时选择架构:x86_64, 线程模型:posix

# 2. 配置环境变量
# 将bin目录添加到PATH,例如:C:\mingw64\bin

# 3. 验证安装
gcc --version

2. 第一个C程序:Hello World

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

代码详解

  • #include <stdio.h>:包含标准输入输出头文件
  • int main():主函数,程序执行的入口点
  • printf():格式化输出函数
  • return 0:正常退出程序,返回状态码0

3. 变量与数据类型实验

#include <stdio.h>

int main() {
    // 整型变量
    int age = 20;
    long bigNumber = 1000000L;
    
    // 浮点型变量
    float height = 1.75f;
    double pi = 3.1415926535;
    
    // 字符型变量
    char grade = 'A';
    
    // 基本输入输出
    printf("年龄:%d\n", age);
    printf("身高:%.2f米\n", height);
    printf("等级:%c\n", grade);
    
    return 0;
}

常见错误分析

  • 错误1:忘记分号 ;
    
    int a = 5  // 编译错误:expected ';' before 'return'
    
  • 错误2:变量未初始化
    
    int x;  // x的值是随机的,可能导致程序异常
    printf("%d", x);  // 未定义行为
    
  • 错误3:格式化字符串与参数不匹配
    
    int num = 10;
    printf("数字:%f", num);  // 错误:%f用于float/double,应该用%d
    

实验技巧分享

  1. 调试技巧:使用printf打印中间变量值
  2. 代码规范:变量命名要有意义,如studentAge而不是a
  3. 版本控制:即使实验简单,也建议使用Git管理代码
  4. 注释习惯:在复杂逻辑处添加注释,解释”为什么”而不是”做什么”

实验二:分支结构程序设计

实验目标

掌握if-else语句和switch语句的使用,理解条件判断的逻辑。

实验内容详解

1. 成绩等级判断程序

#include <stdio.h>

int main() {
    int score;
    printf("请输入成绩(0-100):");
    scanf("%d", &score);
    
    if (score < 0 || score > 100) {
        printf("输入错误!\n");
    } else if (score >= 90) {
        printf("优秀\n");
    } else if (score >= 80) {
        printf("良好\n");
    } else if (score >= 70) {
        printf("中等\n");
    } else if (C >= 60) {
        printf("及格\n");
    } else {
        printf("不及格\n");
    }
    
    return 0;
}

注意:上面代码有个故意设置的错误,C >= 60应该是score >= 60,这是学生常犯的变量名拼写错误。

2. Switch语句实现计算器

#include <stdio.h>

int main() {
    char operator;
    double num1, num2, result;
    
    printf("输入表达式(如:2 + 3):");
    scanf("%lf %c %lf", &num1, &operator, &num2);
    
    switch(operator) {
        case '+':
            result = num1 + num2;
            printf("%.2lf + %.2lf = %.2lf\n", num1, num2, result);
            break;
        case '-':
            result = num1 - num2;
            printf("%.2lf - %.2lf = %.2lf\n", num1, num2, result);
            break;
        case '*':
            result = num1 * num2;
            printf("%.2lf * %.2lf = %.2lf\n", num1, num2, result);
            break;
        case '/':
            if (num2 != 0) {
                result = num1 / num2;
                printf("%.2lf / %.2lf = %.2lf\n", num1, num2, result);
            } else {
                printf("错误:除数不能为零!\n");
            }
            break;
        default:
            printf("错误:未知运算符!\n");
            break;
    }
    
    return 0;
}

3. 逻辑运算符实验

#include <stdio.h>

int main() {
    int year;
    printf("请输入年份:");
    scanf("%d", &year);
    
    // 判断闰年:能被4整除但不能被100整除,或能被400整除
    if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
        printf("%d是闰年\n", year);
    } else {
        printf("%d不是闰年\n", year);
    }
    
    return 0;
}

实验技巧分享

  1. 缩进规范:使用4个空格或1个Tab,保持代码层次清晰
  2. 边界测试:测试0、负数、极大值等边界情况
  3. 避免深层嵌套:超过3层嵌套时考虑重构代码
  4. 使用括号:即使运算符优先级明确,也建议用括号明确意图

实验三:循环结构程序设计

实验目标

掌握for、while、do-while循环,理解循环控制语句(break、continue)。

实验内容详解

1. 求1到100的和

#include <stdio.h>

int main() {
    int sum = 0;
    
    // 方法1:for循环
    for (int i = 1; i <= 100; i++) {
        sum += i;
    }
    printf("1到100的和:%d\n", sum);
    
    // 方法2:while循环
    sum = 0;
    int i = 1;
    while (i <= 100) {
        sum += i;
        i++;
    }
    printf("1到100的和:%d\n", sum);
    
    // 方法3:do-while循环
    sum = 0;
    i = 1;
    do {
        sum += i;
        i++;
    } while (i <= 100);
    printf("1到100的和:%d\n", sum);
    
    return 0;
}

2. 打印九九乘法表

#include <stdio.h>

int main() {
    printf("九九乘法表\n");
    printf("================================\n");
    
    for (int i = 1; i <= 9; i++) {
        for (int j = 1; j <= i; j++) {
            printf("%d×%d=%-4d", i, j, i*j);
        }
        printf("\n");
    }
    
    return 0;
}

3. 百钱买百鸡问题

#include <stdio.h>

int main() {
    int x, y, z; // 公鸡、母鸡、小鸡
    printf("百钱买百鸡的所有方案:\n");
    
    for (x = 0; x <= 20; x++) { // 公鸡最多20只
        for (y = 0; y <= 33; y++) { // 母鸡最多33只
            z = 100 - x - y; // 小鸡数量
            if (5*x + 3*y + z/3 == 100 && z % 3 == 0) {
                printf("公鸡:%d只,母鸡:%d只,小鸡:%d只\n", x, y, z);
            }
        }
    }
    
    return 0;
}

4. break和continue的使用

#include <stdio.h>

int main() {
    // break:立即退出整个循环
    printf("break示例:\n");
    for (int i = 1; i <= 10; i++) {
        if (i == 5) {
            break; // 当i=5时,退出整个循环
        }
        printf("%d ", i);
    }
    printf("\n");
    
    // continue:跳过本次循环的剩余代码
    printf("continue示例:\n");
    for (int i = 1; i <= 10; i++) {
        if (i % 2 == 0) {
            continue; // 跳过偶数
        }
        printf("%d ", i);
    }
    printf("\n");
    
    return 0;
}

实验技巧分享

  1. 循环变量初始化:在循环开始前正确初始化循环变量
  2. 循环条件:确保循环条件最终会变为false,避免死循环
  3. 循环变量更新:在循环体内更新循环变量
  4. 调试循环:在循环内部打印关键变量值,观察循环过程

实验四:数组与字符串实验

实验目标

掌握一维数组、二维数组的定义和使用,理解字符串与字符数组的关系。

实验内容详解

1. 一维数组:统计成绩

#include <stdio.h>

int main() {
    int scores[5];
    int sum = 0;
    float average;
    
    printf("请输入5个学生的成绩:\n");
    for (int i = 0; i < 5; i++) {
        printf("学生%d:", i+1);
        scanf("%d", &scores[i]);
        sum += scores[i];
    }
    
    average = sum / 5.0;
    
    printf("\n统计结果:\n");
    printf("总分:%d\n", sum);
    printf("平均分:%.2f\n", average);
    
    // 查找最高分
    int max = scores[0];
    for (int i = 1; i < 5; i++) {
        if (scores[i] > max) {
            max = scores[i];
        }
    }
    printf("最高分:%d\n", max);
    
    return 0;
}

2. 二维数组:矩阵运算

#include <stdio.h>

#define ROWS 3
#define COLS 3

int main() {
    int matrix[ROWS][COLS];
    int transpose[COLS][ROWS];
    
    printf("请输入3×3矩阵:\n");
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            printf("matrix[%d][%d]:", i, j);
            scanf("%d", &matrix[i][j]);
        }
    }
    
    // 计算转置矩阵
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            transpose[j][i] = matrix[i][j];
        }
    }
    
    // 输出原矩阵
    printf("\n原矩阵:\n");
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            printf("%4d", matrix[i][j]);
        }
        printf("\n");
    }
    
    // 输出转置矩阵
    printf("\n转置矩阵:\n");
    for (int i = 0; i < COLS; i++) {
        for (int j = 0; j < ROWS; j++) {
            printf("%4d", transpose[i][j]);
        }
        printf("\n    ");
    }
    
    return 0;
}

3. 字符串处理

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

int main() {
    char str1[50] = "Hello";
    char str2[50] = "World";
    char str3[100];
    
    // 字符串连接
    strcpy(str3, str1);
    strcat(str3, " ");
    strcat(str3, str2);
    printf("连接结果:%s\n", str3);
    
    // 字符串长度
    printf("长度:%d\n", strlen(str3));
    
    // 字符串比较
    if (strcmp(str1, str2) < 0) {
        printf("%s 小于 %s\n", str1, str2);
    }
    
    // 字符串复制
    char copy[50];
    strcpy(copy, str3);
    printf("复制结果:%s\n", copy);
    
    // 字符串查找
    char *pos = strchr(str3, 'W');
    if (pos != NULL) {
        printf("找到字符'W',位置:%ld\n", pos - str3);
    }
    
    return 0;
}

4. 冒泡排序算法

#include <stdio.h>

void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n-1; i++) {
        for (int j = 0; j < n-1-i; j++) {
            if (arr[j] > arr[j+1]) {
                // 交换
                int temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
}

int main() {
    int arr[] = {64, 34, 25, 12, 22, 11, 90};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    printf("排序前:");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    
    bubbleSort(arr, n);
    
    printf("排序后:");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    
    return 0;
}

实验技巧分享

  1. 数组边界:始终检查数组索引,避免越界访问
  2. 字符串结束符:字符数组要预留空间给’\0’
  3. 二维数组行列:使用宏定义或常量,便于修改
  4. 数组作为函数参数:需要传递数组大小

实验五:函数与模块化编程

实验目标

掌握函数的定义、调用和参数传递,理解模块化编程思想。

实验内容详解

1. 基本函数:计算阶乘

#include <stdio.h>

// 函数声明
long factorial(int n);

int main() {
    int num;
    printf("请输入一个正整数:");
    scanf("%d", &num);
    
    if (num < 0) {
        printf("错误:请输入正整数!\n");
    } else {
        long result = factorial(num);
        printf("%d! = %ld\n", num, result);
    }
    
    return 0;
}

// 函数定义
long factorial(int n) {
    if (n == 0 || n == 1) {
        return 1;
    }
    return n * factorial(n - 1); // 递归调用
}

2. 函数参数传递:值传递与地址传递

#include <stdio.h>

// 值传递:函数内修改不影响原值
void swapByValue(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    printf("函数内交换:a=%d, b=%d\n", a, b);
}

// 地址传递:函数内修改会影响原值
void swapByAddress(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
    printf("函数内交换:a=%d, b=%d\n", *a, *b);
}

int main() {
    int x = 10, y = 20;
    
    printf("原始值:x=%d, y=%d\n", x, y);
    
    // 值传递测试
    swapByValue(x, y);
    printf("值传递后:x=%d, y=%d\n", x, y);
    
    // 地址传递测试
    swapByAddress(&x, &y);
    printf("地址传递后:x=%d, y=%d\n", x, y);
    
    return 0;
}

3. 数组作为函数参数

#include <stdio.h>

// 计算数组平均值
float calculateAverage(int arr[], int size) {
    int sum = 0;
    for (int i = 0; i < size; i++) {
        sum += arr[i];
    }
    return (float)sum / size;
}

// 查找数组最大值
int findMax(int arr[], int size) {
    int max = arr[0];
    for (int i = 1; i < size; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    return max;
}

int main() {
    int scores[] = {85, 92, 78, 96, 88};
    int size = sizeof(scores) / sizeof(scores[0]);
    
    float avg = calculateAverage(scores, size);
    int max = findMax(scores, size);
    
    printf("数组:");
    for (int i = 0; i < size; i++) {
        printf("%d ", scores[i]);
    }
    printf("\n");
    printf("平均值:%.2f\n", avg);
    printf("最大值:%d\n",  max);
    
    return 0;
}

4. 变量的作用域与存储类别

#include <stdio.h>

int globalVar = 100; // 全局变量

void testFunction() {
    static int staticVar = 0; // 静态局部变量
    int autoVar = 0; // 自动局部变量
    
    staticVar++;
    autoVar++;
    
    printf("静态变量:%d,自动变量:%d\n", staticVar, autoVar);
}

int main() {
    printf("全局变量:%d\n", globalVar);
    
    testFunction(); // 输出:静态变量:1,自动变量:1
    testFunction(); // 输出:静态变量:2,自动变量:1
    testFunction(); // 输出:静态变量:3,自动变量:1
    
    // 局部变量覆盖全局变量
    int globalVar = 200;
    printf("局部覆盖后:%d\n", globalVar);
    
    return 0;
}

实验技巧分享

  1. 函数单一职责:每个函数只做一件事,保持简洁
  2. 参数检查:在函数开始检查参数有效性
  3. 返回值处理:明确函数返回值的含义和错误处理
  4. 递归深度:注意递归可能导致栈溢出,考虑迭代替代

实验六:指针与动态内存分配

实验目标

掌握指针的基本概念、指针与数组的关系、动态内存分配。

实验内容详解

1. 指针基础

#include <stdio.h>

int main() {
    int num = 100;
    int *p = &num; // 指针p指向num的地址
    
    printf("num的值:%d\n", num);
    printf("num的地址:%p\n", &num);
    printf("指针p的值:%p\n", p);
    printf("指针p指向的值:%d\n", *p);
    
    // 通过指针修改变量
    *p = 200;
    printf("通过指针修改后,num的值:%d\n", num);
    
    return 0;
}

2. 指针与数组

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *p = arr; // 数组名是首元素地址
    
    printf("数组遍历(指针方式):\n");
    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d, 地址:%p\n", i, *(p + i), p + i);
    }
    
    // 指针运算
    printf("\n指针运算:\n");
    printf("p = %p\n", p);
    printf("p+1 = %p\n", p+1);
    printf("*(p+1) = %d\n", *(p+1));
    
    return 0;
}

3. 动态内存分配

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

int main() {
    int *arr;
    int n;
    
    printf("请输入要创建的数组大小:");
    scanf("%d", &n);
    
    // 动态分配内存
    arr = (int*)malloc(n * sizeof(int));
    if (arr == NULL) {
        printf("内存分配失败!\n");
        return 1;
    }
    
    // 初始化数组
    for (int i = 0; i < n; i++) {
        arr[i] = i * 10;
    }
    
    // 使用数组
    printf("动态数组内容:\n");
    for (int i = 0;  i < n; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }
    
    // 释放内存
    free(arr);
    arr = NULL; // 防止悬空指针
    
    return 0;
}

4. 字符串与指针

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

int main() {
    char str[] = "Hello World";
    char *p = str;
    
    printf("字符串:%s\n", str);
    printf("指针遍历:\n");
    while (*p != '\0') {
        printf("%c", *p);
        p++;
    }
    printf("\n");
    
    // 字符串数组
    char *names[] = {"Alice", "Bob", "Charlie"};
    for (int i = 0; i < 3; i++) {
        printf("%s ", names[i]);
    }
    printf("\n");
    
    return 0;
}

实验技巧分享

  1. 指针初始化:指针使用前必须初始化,避免野指针
  2. 内存释放:malloc/free必须配对使用
  3. 空指针检查:分配内存后检查是否为NULL
  4. 悬空指针:释放内存后将指针设为NULL

实验七:结构体与共用体

实验目标

掌握结构体的定义、使用,理解结构体与指针、数组的关系。

实验内容详解

1. 学生信息管理

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

// 定义结构体
struct Student {
    int id;
    char name[50];
    float score;
};

int main() {
    struct Student stu1 = {1001, "张三", 85.5};
    struct Student stu2;
    
    // 输入学生信息
    printf("请输入学生信息:\n");
    printf("学号:");
    scanf("%d", &stu2.id);
    printf("姓名:");
    scanf("%s", stu2.name);
    printf("成绩:");
    scanf("%f", &stu2.score);
    
    // 输出信息
    printf("\n学生信息:\n");
    printf("学号:%d,姓名:%s,成绩:%.1f\n", stu1.id, stu1.name, stu1.score);
    printf("学号:%d,姓名:%s,成绩:%.1f\n", stu2.id, stu2.name, stu2.score);
    
    return 0;
}

2. 结构体数组与函数

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

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

// 打印学生信息
void printStudent(struct Student stu) {
    printf("学号:%d,姓名:%s,成绩:%.1f\n", stu.id, stu.name, stu.score);
}

// 按成绩排序
void sortStudents(struct Student arr[], int n) {
    for (int i = 0; i < n-1; i++) {
        for (int j = 0; j < n-1-i; j++) {
            if (arr[j].score < arr[j+1].score) { // 降序
                struct Student temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
}

int main() {
    struct Student students[] = {
        {1001, "张三", 85.5},
        {1002, "李四", 92.0},
        {1003, "王五", 78.5}
    };
    int n = sizeof(students) / sizeof(students[0]);
    
    printf("排序前:\n");
    for (int i = 0; i < n; i++) {
        printStudent(students[i]);
    }
    
    sortStudents(students, n);
    
    printf("\n排序后(按成绩降序):\n");
    for (int i = 0; i < n; i++) {
        printStudent(students[i]);
    }
    
    return 0;
}

3. 结构体指针

#include <stdio.h>

struct Point {
    int x;
    int y;
};

// 使用结构体指针作为函数参数
void movePoint(struct Point *p, int dx, int dy) {
    p->x += dx; // 等价于 (*p).x
    p->y += dy;
}

int main() {
    struct Point pt = {10, 20};
    printf("原始点:(%d, %d)\n", pt.x, pt.y);
    
    movePoint(&pt, 5, -3);
    printf("移动后:(%d, %d)\n", pt.x, pt.y);
    
    // 结构体指针动态分配
    struct Point *p = malloc(sizeof(struct Point));
    if (p != NULL) {
        p->x = 100;
        p->y = 200;
        printf("动态结构体:(%d, %d)\n", p->x, p->y);
        free(p);
    }
    
    return 0;
}

4. 共用体(Union)

#include <stdio.h>

// 共用体:所有成员共享同一内存空间
union Data {
    int i;
    float f;
    char str[20];
};

int main() {
    union Data data;
    
    printf("共用体大小:%lu字节\n", sizeof(data));
    
    data.i = 10;
    printf("赋值整数后:i=%d\n", data.i);
    
    data.f = 220.5;
    printf("赋值浮点后:f=%.1f\n", data.f);
    printf("此时i=%d(被覆盖)\n", data.i); // 值已改变
    
    strcpy(data.str, "C Language");
    printf("赋值字符串后:%s\n", data.str);
    printf("此时f=%.1f(被覆盖)\n", data.f); // 值已改变
    
    return 0;
}

实验技巧分享

  1. 结构体对齐:使用#pragma pack控制内存对齐
  2. 结构体赋值:可以直接赋值,但数组成员需要逐个复制
  3. 共用体使用:用于节省内存,但需注意当前存储的是哪个成员
  4. 结构体大小:使用sizeof计算,考虑内存对齐

实验八:文件操作实验

实验目标

掌握文件的打开、读写、关闭操作,理解文本文件与二进制文件的区别。

实验内容详解

1. 文本文件读写

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

int main() {
    FILE *fp;
    char filename[] = "test.txt";
    char buffer[100];
    
    // 写文件
    fp = fopen(filename, "w");
    if (fp == NULL) {
        printf("无法打开文件:%s\n", filename);
        return 1;
    }
    
    fprintf(fp, "Hello, File!\n");
    fprintf(fp, "This is a test file.\n");
    fprintf(fp, "Line number: %d\n", 3);
    fclose(fp);
    printf("文件写入完成。\n");
    
    // 读文件
    fp = fopen(filename, "r");
    if (fp == NULL) {
        printf("无法打开文件:%s\n", filename);
        return 1;
    }
    
    printf("\n文件内容:\n");
    while (fgets(buffer, 100, fp) != NULL) {
        printf("%s", buffer);
    }
    fclose(fp);
    
    return 0;
}

2. 二进制文件读写

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

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

int main() {
    FILE *fp;
    struct Student stu1 = {1001, "张三", 85.5};
    struct Student stu2 = {1002, "李四", 92.0};
    struct Student stu_read;
    
    // 二进制写入
    fp = fopen("students.dat", "wb");
    if (fp == NULL) {
        printf("无法创建文件!\n");
        return 1;
    }
    
    fwrite(&stu1, sizeof(struct Student), 1, fp);
    fwrite(&stu2, sizeof(struct Student), 1, fp);
    fclose(fp);
    printf("二进制文件写入完成。\n");
    
    // 二进制读取
    fp = fopen("students.dat", "rb");
    if (fp == NULL) {
        printf("无法打开文件!\n");
        return 1;
    }
    
    printf("\n从二进制文件读取:\n");
    while (fread(&stu_read, sizeof(struct Student), 1, fp) == 1) {
        printf("学号:%d,姓名:%s,成绩:%.1f\n", 
               stu_read.id, stu_read.name, stu_read.score);
    }
    fclose(fp);
    
    return 0;
}

3. 文件定位与随机访问

#include <stdio.h>

int main() {
    FILE *fp;
    char data[] = "0123456789";
    
    // 写入数据
    fp = fopen("position.txt", "w+");
    fprintf(fp, "%s", data);
    
    // 移动文件指针
    fseek(fp, 5, SEEK_SET); // 从文件开头移动5字节
    printf("位置5的字符:%c\n", fgetc(fp));
    
    fseek(fp, -2, SEEK_END); // 从文件末尾前移2字节
    printf("倒数第2个字符:%c\n", fgetc(fp));
    
    fseek(fp, 0, SEEK_SET); // 回到开头
    printf("文件开头字符:%c\n", fgetc(fp));
    
    fclose(fp);
    return 0;
}

4. 文件错误处理

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

int main() {
    FILE *fp = fopen("nonexistent.txt", "r");
    
    if (fp == NULL) {
        printf("错误代码:%d\n", errno);
        printf("错误信息:%s\n", strerror(errno));
        return 1;
    }
    
    // 文件操作...
    fclose(fp);
    return 0;
}

实验技巧分享

  1. 文件打开模式:区分文本模式和二进制模式
  2. 检查返回值:每次文件操作后检查返回值
  3. 及时关闭:文件使用完毕立即关闭
  4. 路径问题:注意相对路径和绝对路径的区别

实验九:高级数据结构——链表

实验目标

掌握链表的创建、遍历、插入、删除等操作。

实验内容详解

1. 单链表的基本操作

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

// 链表节点结构
struct Node {
    int data;
    struct Node *next;
};

// 创建新节点
struct Node* createNode(int data) {
    struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
    if (newNode == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

// 在链表末尾添加节点
void appendNode(struct Node **head, int data) {
    struct Node *newNode = createNode(data);
    
    if (*head == NULL) {
        *head = newNode;
        return;
    }
    
    struct Node *temp = *head;
    while (temp->next != NULL) {
        temp = temp->next;
    }
    temp->next = newNode;
}

// 打印链表
void printList(struct Node *head) {
    struct Node *temp = head;
    while (temp != NULL) {
        printf("%d -> ", temp->data);
        temp = temp->next;
    }
    printf("NULL\n");
}

// 在指定位置插入节点
void insertNode(struct Node **head, int pos, int data) {
    struct Node *newNode = createNode(data);
    
    if (pos == 1) {
        newNode->next = *head;
        *head = newNode;
        return;
    }
    
    struct Node *temp = *head;
    for (int i = 1; i < pos-1 && temp != NULL; i++) {
        temp = temp->next;
    }
    
    if (temp == NULL) {
        printf("位置超出范围!\n");
        free(newNode);
        return;
    }
    
    newNode->next = temp->next;
    temp->next = newNode;
}

// 删除指定位置的节点
void deleteNode(struct Node **head, int pos) {
    if (*head == NULL) {
        printf("链表为空!\n");
        return;
    }
    
    struct Node *temp = *head;
    
    if (pos == 1) {
        *head = temp->next;
        free(temp);
        return;
    }
    
    for (int i = 1; i < pos-1 && temp != NULL; i++) {
        temp = temp->next;
    }
    
    if (temp == NULL || temp->next == NULL) {
        printf("位置超出范围!\n");
        return;
    }
    
    struct Node *toDelete = temp->next;
    temp->next = toDelete->next;
    free(toDelete);
}

// 释放链表内存
void freeList(struct Node **head) {
    struct Node *temp = *head;
    while (temp != NULL) {
        struct Node *next = temp->next;
        free(temp);
        temp = next;
    }
    *head = NULL;
}

int main() {
    struct Node *head = NULL;
    
    // 创建链表
    appendNode(&head, 10);
    appendNode(&head, 20);
    appendNode(&head, 30);
    printf("初始链表:");
    printList(head);
    
    // 插入节点
    insertNode(&head, 2, 15);
    printf("在位置2插入15:");
    printList(head);
    
    // 删除节点
    deleteNode(&head, 3);
    printf("删除位置3的节点:");
    printList(head);
    
    // 释放内存
    freeList(&head);
    
    return 0;
}

2. 链表应用:学生成绩管理

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

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

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

// 按成绩排序链表
void sortList(struct Node *head) {
    if (head == NULL) return;
    
    struct Node *i, *j;
    struct Student temp;
    
    for (i = head; i->next != NULL; i = i->next) {
        for (j = i->next; j != NULL; j = j->next) {
            if (i->data.score < j->data.score) {
                temp = i->data;
                i->data = j->data;
                j->data = temp;
            }
        }
    }
}

实验技巧分享

  1. 内存管理:每次malloc都要检查返回值
  2. 指针操作:使用二级指针修改头指针
  3. 边界检查:插入删除时检查位置有效性
  4. 调试技巧:打印链表时显示地址便于调试

实验十:综合项目——学生管理系统

实验目标

综合运用所学知识,开发一个完整的学生信息管理系统。

项目需求

  1. 学生信息包括:学号、姓名、成绩
  2. 功能:添加、删除、修改、查询、排序、统计、文件存储
  3. 使用链表存储数据
  4. 支持文件持久化

完整代码实现

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

// 学生结构体
struct Student {
    int id;
    char name[50];
    float score;
};

// 链表节点
struct Node {
    struct Student data;
    struct Node *next;
};

// 全局变量
struct Node *head = NULL;
char filename[] = "students.dat";

// 函数声明
void addStudent();
void deleteStudent();
void modifyStudent();
void searchStudent();
void sortStudents();
void displayAll();
void saveToFile();
void loadFromFile();
void freeMemory();
int menu();

int main() {
    loadFromFile();
    
    while (1) {
        switch (menu()) {
            case 1: addStudent(); break;
            case 2: deleteStudent(); break;
            case 3: modifyStudent(); break;
            case 4: searchStudent(); break;
            case 5: sortStudents(); break;
            case 6: displayAll(); break;
            case 7: saveToFile(); break;
            case 8: freeMemory(); printf("感谢使用!\n"); return 0;
            default: printf("无效选择!\n");
        }
    }
    
    return 0;
}

int menu() {
    printf("\n========== 学生管理系统 ==========\n");
    printf("1. 添加学生\n");
    printf("2. 删除学生\n");
    printf("3. 修改学生\n");
    printf("4. 查询学生\n");
    printf("5. 按成绩排序\n");
    printf("6. 显示所有\n");
    printf("7. 保存到文件\n");
    printf("8. 退出系统\n");
    printf("请选择:");
    
    int choice;
    scanf("%d", &choice);
    return choice;
}

void addStudent() {
    struct Student stu;
    printf("\n添加学生\n");
    printf("学号:"); scanf("%d", &stu.id);
    printf("姓名:"); scanf("%s", stu.name);
    printf("成绩:"); scanf("%f", &stu.score);
    
    // 检查学号是否重复
    struct Node *temp = head;
    while (temp != NULL) {
        if (temp->data.id == stu.id) {
            printf("错误:学号%d已存在!\n", stu.id);
            return;
        }
        temp = temp->next;
    }
    
    // 添加到链表末尾
    struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
    if (newNode == NULL) {
        printf("内存分配失败!\n");
        return;
    }
    newNode->data = stu;
    newNode->next = NULL;
    
    if (head == NULL) {
        head = newNode;
    } else {
        temp = head;
        while (temp->next != NULL) {
            temp = temp->next;
        }
        temp->next = newNode;
    }
    
    printf("添加成功!\n");
}

void deleteStudent() {
    int id;
    printf("\n删除学生\n");
    printf("请输入要删除的学号:");
    scanf("%d", &id);
    
    if (head == NULL) {
        printf("系统中没有学生记录!\n");
        return;
    }
    
    struct Node *temp = head;
    struct Node *prev = NULL;
    
    // 查找要删除的节点
    while (temp != NULL && temp->data.id != id) {
        prev = temp;
        temp = temp->next;
    }
    
    if (temp == NULL) {
        printf("未找到学号%d的学生!\n", id);
        return;
    }
    
    // 删除节点
    if (prev == NULL) {
        head = temp->next;
    } else {
        prev->next = temp->next;
    }
    
    free(temp);
    printf("删除成功!\n");
}

void modifyStudent() {
    int id;
    printf("\n修改学生\n");
    printf("请输入要修改的学号:");
    scanf("%d", &id);
    
    struct Node *temp = head;
    while (temp != NULL) {
        if (temp->data.id == id) {
            printf("当前信息:学号:%d,姓名:%s,成绩:%.1f\n",
                   temp->data.id, temp->data.name, temp->data.score);
            
            printf("请输入新姓名:"); scanf("%s", temp->data.name);
            printf("请输入新成绩:"); scanf("%f", &temp->data.score);
            
            printf("修改成功!\n");
            return;
        }
        temp = temp->next;
    }
    
    printf("未找到学号%d的学生!\n", id);
}

void searchStudent() {
    int id;
    printf("\n查询学生\n");
    printf("请输入要查询的学号:");
    scanf("%d", &id);
    
    struct Node *temp = head;
    while (temp != NULL) {
        if (temp->data.id == id) {
            printf("查询结果:\n");
            printf("学号:%d\n", temp->data.id);
            printf("姓名:%s\n", temp->data.name);
            printf("成绩:%.1f\n", temp->data.score);
            return;
        }
        temp = temp->next;
    }
    
    printf("未找到学号%d的学生!\n", id);
}

void sortStudents() {
    if (head == NULL) {
        printf("没有学生记录!\n");
        return;
    }
    
    // 使用选择排序
    struct Node *i, *j;
    struct Student temp;
    
    for (i = head; i->next != NULL; i = i->next) {
        for (j = i->next; j != NULL; j = j->next) {
            if (i->data.score < j->data.score) {
                temp = i->data;
                i->data = j->data;
                j->data = temp;
            }
        }
    }
    
    printf("排序完成!\n");
    displayAll();
}

void displayAll() {
    if (head == NULL) {
        printf("没有学生记录!\n");
        return;
    }
    
    printf("\n所有学生信息:\n");
    printf("学号\t姓名\t成绩\n");
    printf("----------------------------\n");
    
    struct Node *temp = head;
    while (temp != NULL) {
        printf("%d\t%s\t%.1f\n", temp->data.id, temp->data.name, temp->data.score);
        temp = temp->next;
    }
}

void saveToFile() {
    FILE *fp = fopen(filename, "wb");
    if (fp == NULL) {
        printf("无法打开文件进行保存!\n");
        return;
    }
    
    struct Node *temp = head;
    while (temp != NULL) {
        fwrite(&temp->data, sizeof(struct Student), 1, fp);
        temp = temp->next;
    }
    
    fclose(fp);
    printf("数据已保存到文件:%s\n", filename);
}

void loadFromFile() {
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) {
        printf("未找到数据文件,将创建新系统。\n");
        return;
    }
    
    struct Student stu;
    while (fread(&stu, sizeof(struct Student), 1, fp) == 1) {
        // 添加到链表
        struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
        if (newNode == NULL) {
            printf("内存分配失败!\n");
            break;
        }
        newNode->data = stu;
        newNode->next = NULL;
        
        if (head == NULL) {
            head = newNode;
        } else {
            struct Node *temp = head;
            while (temp->next != NULL) {
                temp = temp->next;
            }
            temp->next = newNode;
        }
    }
    
    fclose(fp);
    printf("数据加载完成!\n");
}

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

实验技巧分享

  1. 模块化设计:每个功能独立函数,便于维护
  2. 错误处理:每个操作都要考虑失败情况
  3. 数据持久化:使用二进制文件保存结构体数组
  4. 用户体验:提供清晰的菜单和提示信息
  5. 内存管理:程序退出前释放所有动态内存

通用实验技巧与调试方法

1. 编译错误处理

常见编译错误

  • 语法错误:缺少分号、括号不匹配
  • 类型错误:赋值类型不兼容
  • 未定义符号:忘记包含头文件或拼写错误

解决方法

  • 从第一个错误开始修正,后面的错误可能由第一个引起
  • 使用gcc -Wall开启所有警告
  • 仔细阅读错误信息,定位到具体行号

2. 运行时错误调试

常见运行时错误

  • 段错误(Segmentation Fault):访问非法内存
  • 除零错误:除数为0
  • 浮点异常:非法浮点运算

调试技巧

// 使用printf调试
int main() {
    int arr[5] = {1,2,3,4,5};
    int *p = arr;
    
    // 调试代码
    printf("数组地址:%p\n", arr);
    printf("指针值:%p\n", p);
    printf("指针指向的值:%d\n", *p);
    
    return 0;
}

3. 内存泄漏检测

工具推荐

  • Linux: Valgrind
  • Windows: Visual Studio调试器
  • macOS: Instruments

Valgrind使用示例

gcc -g program.c -o program
valgrind --leak-check=full ./program

4. 代码规范建议

// 好的命名
int studentCount;
char studentName[50];
float examScore;

// 避免的命名
int a;
char b[50];
float c;

// 函数命名
void calculateAverage(); // 动词+名词,清晰表达功能
int findMaxValue();      // 返回值类型明确

// 常量定义
#define MAX_STUDENTS 100
const int MAX_SIZE = 100;

5. 性能优化技巧

// 避免不必要的数组拷贝
void processArray(int arr[], int size) { // 传递指针,不拷贝
    // ...
}

// 使用位运算优化
// 判断奇偶
if (n & 1) { // 比 n % 2 == 1 快
    // 奇数
}

// 交换变量(无临时变量)
a ^= b;
b ^= a;
a ^= b;

常见问题解答(FAQ)

Q1: 为什么我的程序运行结果正确但评测系统报错?

A: 可能是输出格式不完全匹配,检查空格、换行、大小写。

Q2: 动态分配的内存忘记释放会怎样?

A: 程序运行时内存占用持续增加,长时间运行可能导致系统内存耗尽。

Q3: 指针和数组有什么区别?

A: 数组是连续内存空间,大小固定;指针是变量,存储地址,可以指向任意内存。

Q4: 如何处理输入缓冲区残留?

A: 使用getchar()清空缓冲区,或在scanf格式字符串前加空格。

Q5: 结构体和共用体的主要区别?

A: 结构体每个成员有独立内存,共用体所有成员共享同一内存。

总结

通过本教程的系统学习,你应该能够:

  1. 熟练掌握C语言基础语法和程序结构
  2. 理解并运用各种控制结构和函数
  3. 掌握数组、字符串、结构体等数据结构
  4. 理解指针概念并正确使用
  5. 进行文件操作和动态内存管理
  6. 开发简单的综合应用程序

记住:编程是实践性技能,多写代码、多调试、多思考是提高的关键。遇到问题时,先自己尝试解决,再查阅资料或请教他人。祝你在C语言学习中取得优异成绩!