引言:C语言实验的重要性与学习目标

C语言作为计算机科学的基础语言,其实验环节是巩固理论知识、培养编程思维的关键。苑俊英老师的《C语言程序设计实验指导》是一本经典的实验教材,涵盖了从基础语法到复杂算法的多个层面。通过本实验指导的学习,学生能够掌握程序调试技巧、理解内存管理机制,并培养解决实际问题的能力。本文将针对该教材中的典型实验题目进行详细解答,并分享实用的实验技巧,帮助读者提升C语言编程水平。

实验一:数据类型与基本输入输出

实验目标

掌握C语言的基本数据类型(int、float、char、double)及其输入输出格式,理解不同数据类型在内存中的存储方式。

典型题目详解

题目:编写程序,从键盘输入一个整数、一个浮点数和一个字符,然后分别输出它们的值。

参考答案:

#include <stdio.h>

int main() {
    int num_int;
    float num_float;
    char ch;

    // 输入部分
    printf("请输入一个整数:");
    scanf("%d", &num_int);
    printf("请输入一个浮点数:");
    scanf("%f", &num_float);
    printf("请输入一个字符:");
    scanf(" %c", &ch);  // 注意:前面的空格用于跳过缓冲区中的空白字符

    // 输出部分
    printf("\n输入的整数是:%d\n", num_int);
    printf("输入的浮点数是:%.2f\n", num_float);  // 保留两位小数
    printf("输入的字符是:'%c'\n", ch);

    return 0;
}

代码详解:

  1. #include <stdio.h>:包含标准输入输出头文件,提供printf和scanf函数的声明。
  2. scanf(" %c", &ch):在读取字符时,前面的空格非常重要。因为之前的scanf可能会在缓冲区留下换行符,不加空格会直接读取换行符导致错误。
  3. %.2f:格式化输出浮点数,保留两位小数。

实验技巧:

  • 输入验证:在实际应用中,应检查scanf的返回值以确保输入有效。
  • 缓冲区问题:理解输入缓冲区的概念,特别是字符输入时的空白字符处理。
    
    // 改进的输入处理
    int input_int;
    printf("请输入整数:");
    while(scanf("%d", &input_int) != 1) {
      printf("输入无效,请重新输入:");
      while(getchar() != '\n'); // 清空缓冲区
    }
    

实验二:选择结构程序设计

实验目标

掌握if-else和switch语句的使用,理解条件表达式的求值规则。

典型题目详解

题目:编写程序,根据输入的成绩(0-100),输出对应的等级:A(90-100)、B(80-89)、C(70-79)、D(60-69)、E(0-59)。

参考答案:

#include <stdio.h>

int main() {
    int score;
    printf("请输入成绩(0-100):");
    scanf("%d", &score);

    if (score < 0 || score > 100) {
        printf("输入错误!成绩必须在0-100之间。\n");
    } else if (score >= 90) {
        printf("等级:A\n");
    } else if (score >= 80) {
        printf("等级:B\n");
    } else if (score >= 70) {
        printf("等级:C\n");
    } else if (score >= 100) {
        printf("等级:D\n");
    } else {
        printf("等级:E\n");
    }

    return 0;
}

代码详解:

  1. 边界检查:首先检查输入是否在有效范围内,这是良好的编程习惯。
  2. 条件顺序:if-else的判断顺序很重要,从高到低判断可以避免逻辑错误。
  3. switch版本:虽然switch不适合连续范围,但可以展示其用法:
switch(score / 10) {
    case 10:
    case 9: printf("A\n"); break;
    case 8: printf("B\n"); break;
    case 7: printf("C\n"); break;
    case 6: printf("D\n"); break;
    default: printf("E\n"); break;
}

实验技巧:

  • 避免嵌套过深:复杂的条件判断可以考虑使用switch或函数封装。
  • 使用枚举:对于固定等级,可以定义枚举类型提高可读性。
    
    enum Grade { A=90, B=80, C=70, D=60, E=0 };
    

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

实验目标

掌握for、while、do-while循环的使用,理解循环控制变量和循环嵌套。

典型题目详解

题目:编写程序,计算1到100之间所有能被3或7整除的数的和。

参考答案:

#include <stdio.h>

int main() {
    int sum = 0;
    for (int i = 1; i <= 100; i++) {
        if (i % 3 == 0 || i % 7 == 0) {
            sum += i;
        }
    }
    printf("1-100之间能被3或7整除的数的和是:%d\n", sum);
    return 0;
}

代码详解:

  1. 循环变量初始化int i = 1,从1开始循环。
  2. 条件判断i <= 100,循环到100为止。
  3. 取模运算%运算符用于判断整除关系。
  4. 累加操作sum += i等价于sum = sum + i

扩展版本:

// 使用while循环实现
int i = 1, sum = 0;
while (i <= 100) {
    if (i % 3 == 0 || i % 7 == 0) {
        sum += i;
    }
    i++;
}

实验技巧:

  • 循环优化:可以考虑步长优化,但要注意可读性。
  • 调试技巧:在循环体内加入printf调试输出。
    
    for (int i = 1; i <= 100; i++) {
      if (i % 3 == 0 || i % 7 == 0) {
          printf("找到符合条件的数:%d\n", i);
          sum += i;
      }
    }
    

实验四:数组与字符串

实验目标

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

典型题目详解

题目:编写程序,输入10个整数,找出其中的最大值、最小值和平均值。

参考答案:

#include <stdio.h>

int main() {
    int arr[10];
    int max, min, sum = 0;
    float avg;

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

    // 初始化最大值和最小值
    max = min = arr[0];
    sum = arr[0];

    // 遍历数组
    for (int i = 1; i < 10; i++) {
        if (arr[i] > max) max = arr[i];
        if (arr[i] < min) min = arr[i];
        sum += arr[i];
    }

    avg = (float)sum / 10;

    printf("最大值:%d\n", max);
    printf("最小值:%d\n", 10);
    printf("平均值:%.2f\n", avg);

    return 0;
}

代码详解:

  1. 数组定义int arr[10]定义了包含10个整数的数组。
  2. 初始化技巧:将数组第一个元素同时赋值给max、min和sum,避免使用魔法数字。
  3. 类型转换:计算平均值时,将sum强制转换为float,避免整数除法截断。
  4. 边界处理:循环从i=1开始,因为第一个元素已经初始化。

字符串处理示例:

// 字符串长度计算
int strlen_custom(const char *str) {
    int len = 0;
    while (str[len] != '\0') {
        len++;
    }
    return len;
}

// 字符串复制
void strcpy_custom(char *dest, const char *src) {
    int i = 0;
    while ((dest[i] = src[i]) != '\0') {
        i++;
    }
}

实验技巧:

  • 数组越界:C语言不检查数组边界,必须自己确保索引有效。
  • 字符串安全:使用strncpy、strncat等安全函数,避免缓冲区溢出。
  • 调试数组:可以编写辅助函数打印数组内容。
    
    void print_array(int arr[], int size) {
      for (int i = 0;  i < size; i++) {
          printf("%d ", arr[i]);
      }
      printf("\n");
    }
    

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

实验目标

掌握函数的定义、声明和调用,理解参数传递机制(值传递 vs 引用传递)。

典型题目详解

题目:编写函数,计算两个整数的最大公约数(GCD),并编写main函数测试。

参考答案:

#include <stdio.h>

// 函数声明
int gcd(int a, int b);

int main() {
    int num1, num2;
    printf("请输入两个整数:");
    scanf("%d %d", &num1, &num2);
    int result = gcd(num1, num2);
    printf("%d和%d的最大公约数是:%d\n", num1, num2, result);
    return 0;
}

// 使用欧几里得算法计算GCD
int gcd(int a, int b) {
    // 确保a >= b
    if (a < b) {
        int temp = a;
        a = b;
        b = temp;
    }

    // 辗转相除法
    while (b != 0) {
        int temp = a % b;
        a = b;
        b = temp;
    }
    return a;
}

代码详解:

  1. 函数声明:在main函数前声明gcd函数,告诉编译器函数的存在。
  2. 参数传递:C语言默认是值传递,函数内部修改参数不会影响外部变量。
  3. 算法实现:欧几里得算法,时间复杂度O(log(min(a,b)))。
  4. 临时变量:使用temp变量交换a和b的值。

递归版本:

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

实验技巧:

  • 函数设计原则:单一职责,一个函数只做一件事。
  • 参数校验:在函数入口检查参数有效性。
  • 递归深度:注意递归可能导致栈溢出,特别是处理大数时。
    
    // 改进的gcd函数,处理负数和零
    int gcd_safe(int a, int b) {
      if (a == 0 && b == 0) return 0; // 未定义情况
      if (a < 0) a = -a;
      if (b < 0) b = -b;
      return gcd_recursive(a, b);
    }
    

实验六:指针与内存管理

实验目标

掌握指针的定义、运算和使用,理解指针与数组的关系,掌握动态内存分配。

典型题目详解

题目:编写函数,使用指针实现字符串连接功能。

参考答案:

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

// 使用指针实现字符串连接
void str_concat(char *dest, const char *src) {
    // 移动dest指针到末尾
    while (*dest != '\0') {
        dest++;
    }

    // 复制src到dest末尾
    while (*src != '\0') {
        *dest = *src;
        dest++;
        src++;
    }

    // 添加字符串结束符
    *dest = '\0';
}

int main() {
    char *str1 = (char*)malloc(50 * sizeof(char));
    char *str2 = " World!";

    if (str1 == NULL) {
        printf("内存分配失败!\n");
        return 1;
    }

    strcpy(str1, "Hello");
    printf("连接前:%s\n", str1);

    str_concat(str1, str2);
    printf("连接后:%s\n", str1);

    free(str1);  // 释放内存
    return 0;
}

代码详解:

  1. 指针移动while (*dest != '\0') dest++; 移动指针到字符串末尾。
  2. 解引用操作*dest = *src; 将src指向的字符复制到dest指向的位置。
  3. 内存分配:使用malloc分配50字节的内存空间。
  4. 内存释放:必须使用free释放动态分配的内存。

二维数组与指针:

// 使用指针访问二维数组
void print_2d_array(int (*arr)[3], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", *(*(arr + i) + j));  // 等价于 arr[i][j]
        }
        printf("\n");
    }
}

实验技巧:

  • 野指针:永远初始化指针,避免使用未初始化的指针。
  • 内存泄漏:动态分配的内存必须释放,使用valgrind等工具检测。
  • 指针与数组:理解数组名本质上是指针常量。
    
    int arr[5];
    // arr++;  // 错误!数组名是常量指针,不能修改
    int *p = arr;  // 正确,可以赋值给指针变量
    p++;  // 正确,指针可以移动
    

实验七:结构体与共用体

实验目标

掌握结构体的定义、初始化和使用,理解结构体数组和结构体指针。

典型题目详解

题目:定义一个学生结构体(学号、姓名、成绩),编写函数对学生成绩进行排序。

参考答案:

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

#define MAX_STUDENTS 50
#define NAME_LEN 20

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

// 比较函数,用于排序
int compare_students(const void *a, const void *b) {
    const Student *s1 = (const Student*)a;
    const Student *s2 = (const Student*)b;
    // 按成绩降序排列
    if (s1->score > s2->score) return -1;
    if (s1->score < s2->score) return 1;
    return 0;
}

// 打印学生信息
void print_students(Student students[], int count) {
    printf("\n%-10s %-20s %-6s\n", "学号", "姓名", "成绩");
    printf("========================================\n");
    for (int i = 0; i < 10; i++) {
        printf("%-10d %-20s %-6.1f\n", students[i].id, students[i].name, students[i].score);
    }
}

int main() {
    Student students[MAX_STUDENTS] = {
        {101, "张三", 85.5},
        {102, "李四", 92.0},
        {103, "王五", 78.5},
        {104, "赵六", 88.0},
        {105, "钱七", 95.5}
    };
    int count = 5;

    printf("排序前:\n");
    print_students(students, count);

    // 使用qsort排序
    qsort(students, count, sizeof(Student), compare_students);

    printf("\n排序后(按成绩降序):\n");
    print_students(students, count);

    return 0;
}

代码详解:

  1. 结构体定义:使用typedef简化类型名称。
  2. 结构体数组:在栈上分配内存,初始化5个学生。
  3. qsort函数:使用标准库的qsort函数进行排序,需要提供比较函数。
  4. 指针转换:在比较函数中将void指针转换为Student指针。
  5. 成员访问:使用->运算符访问结构体指针的成员。

结构体指针:

// 使用结构体指针
Student *ptr = students;
printf("第一个学生:%s\n", ptr->name);  // 等价于 (*ptr).name
ptr++;
printf("第二个学生:%s\n", ptr->name);

实验技巧:

  • 内存对齐:结构体可能存在内存对齐,使用sizeof验证。
  • 字符串处理:结构体中的字符数组需要使用strcpy赋值,不能直接用=。
  • 动态结构体:使用malloc分配结构体内存。
    
    Student *s = (Student*)malloc(sizeof(Student));
    s->id = 106;
    strcpy(s->name, "孙八");
    s->score = 89.0;
    free(s);
    

实验八:文件操作

实验目标

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

典型题目详解

题目:编写程序,从文件中读取学生成绩,计算平均分并写入新文件。

参考答案:

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

#define MAX_NAME 50

typedef struct {
    char name[MAX_NAME];
    float score;
} Student;

int main() {
    FILE *fp_in, *fp_out;
    Student s;
    float sum = 0;
    int count = 0;

    // 打开输入文件
    fp_in = fopen("scores.txt", "r");
    if (fp_in == NULL) {
        printf("无法打开输入文件!\n");
        return 1;
    }

    // 打开输出文件
    fp_out = fopen("result.txt", "w");
    if (fp_out == NULL) {
        printf("无法打开输出文件!\n");
        fclose(fp_in);
        return 1;
    }

    // 读取文件内容
    printf("读取的学生数据:\n");
    while (fscanf(fp_in, "%s %f", s.name, &s.score) == 2) {
        printf("%s: %.1f\n", s.name, s.score);
        sum += s.score;
        count++;
        fprintf(fp_out, "%s %.1f\n", s.name, s.score);
    }

    // 计算并写入平均分
    if (count > 0) {
        float avg = sum / count;
        fprintf(fp_out, "\n平均分: %.2f\n", avg);
        printf("\n平均分: %.2f\n", avg);
    }

    // 关闭文件
    fclose(fp_in);
    fclose(fp_out);

    return 0;
}

代码详解:

  1. 文件指针:FILE结构体指针,用于操作文件。
  2. fopen模式:”r”表示读取文本文件,”w”表示写入文本文件(覆盖)。
  3. 错误检查:每次文件操作后检查返回值。
  4. fscanf:从文件读取格式化数据,返回成功读取的项目数。
  5. fprintf:向文件写入格式化数据。
  6. 关闭文件:必须关闭文件释放资源。

二进制文件读写:

// 写入二进制文件
fp = fopen("data.bin", "wb");
Student s = {"张三", 85.5};
fwrite(&s, sizeof(Student), 1, fp);
fclose(fp);

// 读取二进制文件
fp = fopen("data.bin", "rb");
Student s_read;
fread(&s_read, sizeof(Student), 1, fp);
printf("%s: %.1f\n", s_read.name, s_read.score);
fclose(fp);

实验技巧:

  • 文件路径:使用相对路径时,确保文件在正确位置。
  • 缓冲区刷新:使用fflush()强制写入磁盘。
  • 文件定位:使用fseek和ftell进行随机访问。
    
    // 获取文件大小
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    fseek(fp, 0, 0);  // 回到文件开头
    

实验九:动态内存管理与链表

实验目标

掌握malloc、free等动态内存分配函数,理解链表的基本操作(创建、遍历、插入、删除)。

典型题目详解

题目:创建一个单向链表,存储学生信息,并实现插入、删除和遍历功能。

参考答案:

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

#define NAME_LEN 20

typedef struct Student {
    int id;
    char name[NAME_LEN];
    float score;
    struct Student *next;
} Student;

// 创建新节点
Student* create_node(int id, const char *name, float score) {
    Student *new_node = (Student*)malloc(sizeof(Student));
    if (new_node == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    new_node->id = id;
    strcpy(new_node->name, name);
    new_node->score = score;
    new_node->next = NULL;
    return new_node;
}

// 在链表头部插入节点
void insert_at_head(Student **head, Student *new_node) {
    new_node->next = *head;
    *head = new_node;
}

// 删除指定ID的节点
void delete_node(Student **head, int id) {
    Student *current = *head;
    Student *prev = NULL;

    while (current != NULL) {
        if (current->id == id) {
            if (prev == NULL) {
                *head = current->next;  // 删除头节点
            } else {
                prev->next = current->next;  // 删除中间或尾部节点
            }
            free(current);
            printf("已删除学号为%d的学生\n", id);
            return;
        }
        prev = current;
        current = current->next;
    }
    printf("未找到学号为%d的学生\n", id);
}

// 遍历链表
void traverse_list(Student *head) {
    Student *current = head;
    printf("\n链表内容:\n");
    printf("%-10s %-20s %-6s\n", "学号", "姓名", "成绩");
    printf("----------------------------------------\n");
    while (current != NULL) {
        printf("%-10d %-20s %-6.1f\n", current->id, current->name, current->score);
        current = current->next;
    }
}

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

int main() {
    Student *head = NULL;

    // 创建节点
    Student *s1 = create_node(101, "张三", 85.5);
    Student *s2 = create_node(102, "李四", 92.0);
    Student *s3 = create_node(103, "王五", 78.5);

    // 插入节点
    insert_at_head(&head, s1);
    insert_at_head(&head, s2);
    insert_at_head(&head, s3);

    traverse_list(head);

    // 删除节点
    delete_node(&head, 102);  // 删除李四
    traverse_list(head);

    // 释放内存
    free_list(head);

    return 0;
}

代码详解:

  1. 结构体定义:包含next指针,指向下一个节点。
  2. 二级指针:insert和delete函数使用二级指针,因为需要修改头指针本身。
  3. 内存分配:malloc分配节点内存,free释放。
  4. 链表遍历:使用临时指针current遍历,避免丢失头指针。
  5. 删除操作:需要维护prev指针,处理删除头节点的特殊情况。

实验技巧:

  • 内存泄漏检测:使用valgrind工具检查。
  • 链表反转:面试常见题目。
    
    Student* reverse_list(Student *head) {
      Student *prev = NULL;
      Student *current = head;
      Student *next = NULL;
      while (current != NULL) {
          next = current->next;
          current->next = prev;
          prev = current;
          current = next;
      }
      return prev;
    }
    
  • 循环链表:尾节点指向头节点,适用于队列实现。

实验十:综合实验——学生成绩管理系统

实验目标

综合运用前面所学知识,设计一个完整的学生成绩管理系统。

系统设计

功能需求:

  1. 添加学生信息
  2. 显示所有学生信息
  3. 按成绩排序
  4. 查找学生
  5. 删除学生
  6. 统计功能(平均分、最高分等)
  7. 文件存储

完整代码实现

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

#define MAX_NAME 20
#define FILENAME "students.dat"

typedef struct Student {
    int id;
    char name[MAX_NAME];
    float score;
    struct Student *next;
} Student;

// 全局变量
Student *head = NULL;
int student_count = 0;

// 函数声明
void add_student();
void display_students();
void search_student();
void delete_student();
void sort_students();
void statistics();
void save_to_file();
void load_from_file();
void free_memory();
int get_valid_int(const char *prompt);
float get_valid_float(const char *prompt);
void clear_input_buffer();

// 主菜单
void main_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("0. 退出系统\n");
    printf("======================================\n");
}

// 输入验证函数
int get_valid_int(const char *prompt) {
    int value;
    while (1) {
        printf("%s", prompt);
        if (scanf("%d", &value) == 1) {
            clear_input_buffer();
            return value;
        } else {
            printf("输入无效,请重新输入数字!\n");
            clear_input_buffer();
        }
    }
}

float get_valid_float(const char *prompt) {
    float value;
    while (1) {
        printf("%s", prompt);
        if (scanf("%f", &value) == 1) {
            clear_input_buffer();
            return value;
        } else {
            printf("输入无效,请重新输入数字!\n");
            clear_input_buffer();
        }
    }
}

void clear_input_buffer() {
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

// 创建新节点
Student* create_node(int id, const char *name, float score) {
    Student *new_node = (Student*)malloc(sizeof(Student));
    if (new_node == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    new_node->id = id;
    strncpy(new_node->name, name, MAX_NAME - 1);
    new_node->name[MAX_NAME - 1] = '\0';
    new_node->score = score;
    new_node->next = NULL;
    return new_node;
}

// 添加学生(按ID升序插入)
void add_student() {
    int id = get_valid_int("请输入学号:");
    char name[MAX_NAME];
    printf("请输入姓名:");
    scanf("%19s", name);
    clear_input_buffer();
    float score = get_valid_float("请输入成绩:");

    if (score < 0 || score > 100) {
        printf("成绩必须在0-100之间!\n");
        return;
    }

    // 检查学号是否重复
    Student *current = head;
    while (current != NULL) {
        if (current->id == id) {
            printf("学号%d已存在!\n", id);
            return;
        }
        current = current->next;
    }

    Student *new_node = create_node(id, name, score);

    // 插入到合适位置(按ID排序)
    if (head == NULL || head->id > id) {
        new_node->next = head;
        head = new_node;
    } else {
        Student *temp = head;
        while (temp->next != NULL && temp->next->id < id) {
            temp = temp->next;
        }
        new_node->next = temp->next;
        temp->next = new_node;
    }

    student_count++;
    printf("学生信息添加成功!\n");
}

// 显示所有学生
void display_students() {
    if (head == NULL) {
        printf("没有学生记录!\n");
        return;
    }

    Student *current = head;
    printf("\n%-10s %-20s %-6s\n", "学号", "姓名", "成绩");
    printf("----------------------------------------\n");
    while (current != NULL) {
        printf("%-10d %-20s %-6.1f\n", current->id, current->name, current->score);
        current = current->next;
    }
    printf("共%d名学生\n", student_count);
}

// 查找学生
void search_student() {
    if (head == NULL) {
        printf("没有学生记录!\n");
        return;
    }

    int choice = get_valid_int("按学号查找(1) 还是按姓名查找(2)?");
    if (choice == 1) {
        int id = get_valid_int("请输入学号:");
        Student *current = head;
        while (current != NULL) {
            if (current->id == id) {
                printf("\n找到学生:\n");
                printf("%-10s %-20s %-6s\n", "学号", "姓名", "成绩");
                printf("----------------------------------------\n");
                printf("%-10d %-20s %-6.1f\n", current->id, current->name, current->score);
                return;
            }
            current = current->next;
        }
        printf("未找到学号为%d的学生!\n", id);
    } else if (choice == 2) {
        char name[MAX_NAME];
        printf("请输入姓名:");
        scanf("%19s", name);
        clear_input_buffer();

        Student *current = head;
        int found = 0;
        while (current != NULL) {
            if (strcmp(current->name, name) == 0) {
                if (!found) {
                    printf("\n找到学生:\n");
                    printf("%-10s %-20s %-6s\n", "学号", "姓名", "成绩");
                    printf("----------------------------------------\n");
                    found = 1;
                }
                printf("%-10d %-20s %-6.1f\n", current->id, current->name, current->score);
            }
            current = current->next;
        }
        if (!found) {
            printf("未找到姓名为%s的学生!\n", name);
        }
    } else {
        printf("无效选择!\n");
    }
}

// 删除学生
void delete_student() {
    if (head == NULL) {
        printf("没有学生记录!\n");
        return;
    }

    int id = get_valid_int("请输入要删除的学生学号:");
    Student *current = head;
    Student *prev = NULL;

    while (current != NULL) {
        if (current->id == id) {
            if (prev == NULL) {
                head = current->next;
            } else {
                prev->next = current->next;
            }
            free(current);
            student_count--;
            printf("学号为%d的学生已删除!\n", id);
            return;
        }
        prev = current;
        current = current->next;
    }
    printf("未找到学号为%d的学生!\n", id);
}

// 按成绩排序(使用选择排序)
void sort_students() {
    if (head == NULL || head->next == NULL) {
        printf("学生记录不足,无需排序!\n");
        return;
    }

    // 将链表转换为数组进行排序
    Student **array = (Student**)malloc(student_count * sizeof(Student*));
    if (array == NULL) {
        printf("内存分配失败!\n");
        return;
    }

    Student *current = head;
    for (int i = 0; i < student_count; i++) {
        array[i] = current;
        current = current->next;
    }

    // 选择排序(按成绩降序)
    for (int i = 0; i < student_count - 1; i++) {
        int max_idx = i;
        for (int j = i + 1; j < student_count; j++) {
            if (array[j]->score > array[max_idx]->score) {
                max_idx = j;
            }
        }
        if (max_idx != i) {
            Student *temp = array[i];
            array[i] = array[max_idx];
            array[max_idx] = temp;
        }
    }

    // 重建链表
    head = array[0];
    for (int i = 0; i < student_count - 1; i++) {
        array[i]->next = array[i + 1];
    }
    array[student_count - 1]->next = NULL;

    free(array);
    printf("排序完成!\n");
    display_students();
}

// 统计信息
void statistics() {
    if (head == NULL) {
        printf("没有学生记录!\n");
        return;
    }

    Student *current = head;
    float sum = 0;
    float max = current->score;
    float min = current->score;
    int count = 0;

    while (current != NULL) {
        sum += current->score;
        if (current->score > max) max = current->score;
        if (current->score < min) min = current->score;
        count++;
        current = current->next;
    }

    printf("\n========== 统计信息 ==========\n");
    printf("学生总数:%d\n", count);
    printf("平均分:%.2f\n", sum / count);
    printf("最高分:%.2f\n", max);
    printf("最低分:%.2f\n", min);
    printf("=============================\n");
}

// 保存到文件
void save_to_file() {
    FILE *fp = fopen(FILENAME, "wb");
    if (fp == NULL) {
        printf("无法打开文件进行写入!\n");
        return;
    }

    Student *current = head;
    while (current != NULL) {
        fwrite(current, sizeof(Student), 1, fp);
        current = current->next;
    }

    fclose(fp);
    printf("数据已保存到%s\n", FILENAME);
}

// 从文件加载
void load_from_file() {
    FILE *fp = fopen(FILENAME, "rb");
    if (fp == NULL) {
        printf("无法打开文件进行读取!\n");
        return;
    }

    // 先清空当前链表
    free_memory();

    Student temp;
    Student *prev = NULL;
    while (fread(&temp, sizeof(Student), 1, fp) == 1) {
        Student *new_node = create_node(temp.id, temp.name, temp.score);
        if (head == NULL) {
            head = new_node;
        } else {
            prev->next = new_node;
        }
        prev = new_node;
        student_count++;
    }

    fclose(fp);
    printf("数据已从%s加载\n", FILENAME);
}

// 释放所有内存
void free_memory() {
    Student *current = head;
    while (current != NULL) {
        Student *temp = current;
        current = current->next;
        free(temp);
    }
    head = NULL;
    student_count = 0;
}

// 主函数
int main() {
    load_from_file();  // 启动时自动加载

    while (1) {
        main_menu();
        int choice = get_valid_int("请选择操作:");

        switch (choice) {
            case 1: add_student(); break;
            case 2: display_students(); break;
            case 3: search_student(); break;
            case 4: delete_student(); break;
            case 5: sort_students(); break;
            case 6: statistics(); break;
            case 7: save_to_file(); break;
            case 8: load_from_file(); break;
            case 0:
                save_to_file();  // 退出前自动保存
                free_memory();
                printf("感谢使用,再见!\n");
                return 0;
            default:
                printf("无效选择,请重新输入!\n");
        }
    }
}

系统特点:

  1. 模块化设计:每个功能独立成函数,便于维护。
  2. 输入验证:防止无效输入导致程序崩溃。
  3. 内存管理:完善的内存分配和释放机制。
  4. 数据持久化:支持文件读写,数据不丢失。
  5. 用户友好:清晰的菜单和提示信息。

实验技巧总结

1. 调试技巧

  • 使用printf调试:在关键位置输出变量值。
  • gdb调试器:设置断点、单步执行、查看变量。
    
    gcc -g program.c -o program
    gdb ./program
    (gdb) break main
    (gb) run
    (gdb) next
    (gdb) print variable
    
  • 编译器警告:使用-Wall -Wextra选项,将警告视为错误。

2. 性能优化

  • 避免重复计算:在循环外计算不变表达式。
  • 使用寄存器变量register int i;(现代编译器通常自动优化)。
  • 内联函数static inline减少函数调用开销。

3. 安全编程

  • 边界检查:始终检查数组索引和指针范围。
  • 输入验证:验证所有用户输入,防止缓冲区溢出。
  • 使用安全函数:如strncpy代替strcpy,snprintf代替sprintf。

4. 代码风格

  • 命名规范:使用有意义的变量名和函数名。
  • 注释:解释复杂逻辑,但不要过度注释简单代码。
  • 缩进和空格:保持一致的代码格式。

5. 常见错误避免

  • 未初始化变量:使用-Wuninitialized检查。
  • 内存泄漏:使用valgrind检测。
  • 悬空指针:释放内存后将指针置为NULL。
  • 格式化字符串:使用正确的格式说明符,如%f vs %lf

结语

通过苑俊英老师的实验指导,我们系统地学习了C语言的各个方面。从基础语法到高级特性,每个实验都构建在前一个实验的基础上。掌握这些实验内容不仅有助于通过考试,更重要的是培养了编程思维和解决实际问题的能力。

记住,编程是一门实践性很强的技能。理论知识必须通过大量的编码练习才能真正掌握。建议读者在理解每个实验的基础上,尝试修改题目要求,添加新功能,或者用不同的方法实现相同的功能,这样才能真正融会贯通。

最后,保持良好的编程习惯:代码清晰、注释适当、测试充分、文档完善。这些习惯将使你在未来的编程道路上走得更远。# C语言程序设计实验指导苑俊英答案详解与实验技巧分享

引言:C语言实验的重要性与学习目标

C语言作为计算机科学的基础语言,其实验环节是巩固理论知识、培养编程思维的关键。苑俊英老师的《C语言程序设计实验指导》是一本经典的实验教材,涵盖了从基础语法到复杂算法的多个层面。通过本实验指导的学习,学生能够掌握程序调试技巧、理解内存管理机制,并培养解决实际问题的能力。本文将针对该教材中的典型实验题目进行详细解答,并分享实用的实验技巧,帮助读者提升C语言编程水平。

实验一:数据类型与基本输入输出

实验目标

掌握C语言的基本数据类型(int、float、char、double)及其输入输出格式,理解不同数据类型在内存中的存储方式。

典型题目详解

题目:编写程序,从键盘输入一个整数、一个浮点数和一个字符,然后分别输出它们的值。

参考答案:

#include <stdio.h>

int main() {
    int num_int;
    float num_float;
    char ch;

    // 输入部分
    printf("请输入一个整数:");
    scanf("%d", &num_int);
    printf("请输入一个浮点数:");
    scanf("%f", &num_float);
    printf("请输入一个字符:");
    scanf(" %c", &ch);  // 注意:前面的空格用于跳过缓冲区中的空白字符

    // 输出部分
    printf("\n输入的整数是:%d\n", num_int);
    printf("输入的浮点数是:%.2f\n", num_float);  // 保留两位小数
    printf("输入的字符是:'%c'\n", ch);

    return 0;
}

代码详解:

  1. #include <stdio.h>:包含标准输入输出头文件,提供printf和scanf函数的声明。
  2. scanf(" %c", &ch):在读取字符时,前面的空格非常重要。因为之前的scanf可能会在缓冲区留下换行符,不加空格会直接读取换行符导致错误。
  3. %.2f:格式化输出浮点数,保留两位小数。

实验技巧:

  • 输入验证:在实际应用中,应检查scanf的返回值以确保输入有效。
  • 缓冲区问题:理解输入缓冲区的概念,特别是字符输入时的空白字符处理。
    
    // 改进的输入处理
    int input_int;
    printf("请输入整数:");
    while(scanf("%d", &input_int) != 1) {
      printf("输入无效,请重新输入:");
      while(getchar() != '\n'); // 清空缓冲区
    }
    

实验二:选择结构程序设计

实验目标

掌握if-else和switch语句的使用,理解条件表达式的求值规则。

典型题目详解

题目:编写程序,根据输入的成绩(0-100),输出对应的等级:A(90-100)、B(80-89)、C(70-79)、D(60-69)、E(0-59)。

参考答案:

#include <stdio.h>

int main() {
    int score;
    printf("请输入成绩(0-100):");
    scanf("%d", &score);

    if (score < 0 || score > 100) {
        printf("输入错误!成绩必须在0-100之间。\n");
    } else if (score >= 90) {
        printf("等级:A\n");
    } else if (score >= 80) {
        printf("等级:B\n");
    } else if (score >= 70) {
        printf("等级:C\n");
    } else if (score >= 100) {
        printf("等级:D\n");
    } else {
        printf("等级:E\n");
    }

    return 0;
}

代码详解:

  1. 边界检查:首先检查输入是否在有效范围内,这是良好的编程习惯。
  2. 条件顺序:if-else的判断顺序很重要,从高到低判断可以避免逻辑错误。
  3. switch版本:虽然switch不适合连续范围,但可以展示其用法:
switch(score / 10) {
    case 10:
    case 9: printf("A\n"); break;
    case 8: printf("B\n"); break;
    case 7: printf("C\n"); break;
    case 6: printf("D\n"); break;
    default: printf("E\n"); break;
}

实验技巧:

  • 避免嵌套过深:复杂的条件判断可以考虑使用switch或函数封装。
  • 使用枚举:对于固定等级,可以定义枚举类型提高可读性。
    
    enum Grade { A=90, B=80, C=70, D=60, E=0 };
    

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

实验目标

掌握for、while、do-while循环的使用,理解循环控制变量和循环嵌套。

典型题目详解

题目:编写程序,计算1到100之间所有能被3或7整除的数的和。

参考答案:

#include <stdio.h>

int main() {
    int sum = 0;
    for (int i = 1; i <= 100; i++) {
        if (i % 3 == 0 || i % 7 == 0) {
            sum += i;
        }
    }
    printf("1-100之间能被3或7整除的数的和是:%d\n", sum);
    return 0;
}

代码详解:

  1. 循环变量初始化int i = 1,从1开始循环。
  2. 条件判断i <= 100,循环到100为止。
  3. 取模运算%运算符用于判断整除关系。
  4. 累加操作sum += i等价于sum = sum + i

扩展版本:

// 使用while循环实现
int i = 1, sum = 0;
while (i <= 100) {
    if (i % 3 == 0 || i % 7 == 0) {
        sum += i;
    }
    i++;
}

实验技巧:

  • 循环优化:可以考虑步长优化,但要注意可读性。
  • 调试技巧:在循环体内加入printf调试输出。
    
    for (int i = 1; i <= 100; i++) {
      if (i % 3 == 0 || i % 7 == 0) {
          printf("找到符合条件的数:%d\n", i);
          sum += i;
      }
    }
    

实验四:数组与字符串

实验目标

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

典型题目详解

题目:编写程序,输入10个整数,找出其中的最大值、最小值和平均值。

参考答案:

#include <stdio.h>

int main() {
    int arr[10];
    int max, min, sum = 0;
    float avg;

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

    // 初始化最大值和最小值
    max = min = arr[0];
    sum = arr[0];

    // 遍历数组
    for (int i = 1; i < 10; i++) {
        if (arr[i] > max) max = arr[i];
        if (arr[i] < min) min = arr[i];
        sum += arr[i];
    }

    avg = (float)sum / 10;

    printf("最大值:%d\n", max);
    printf("最小值:%d\n", 10);
    printf("平均值:%.2f\n", avg);

    return 0;
}

代码详解:

  1. 数组定义int arr[10]定义了包含10个整数的数组。
  2. 初始化技巧:将数组第一个元素同时赋值给max、min和sum,避免使用魔法数字。
  3. 类型转换:计算平均值时,将sum强制转换为float,避免整数除法截断。
  4. 边界处理:循环从i=1开始,因为第一个元素已经初始化。

字符串处理示例:

// 字符串长度计算
int strlen_custom(const char *str) {
    int len = 0;
    while (str[len] != '\0') {
        len++;
    }
    return len;
}

// 字符串复制
void strcpy_custom(char *dest, const char *src) {
    int i = 0;
    while ((dest[i] = src[i]) != '\0') {
        i++;
    }
}

实验技巧:

  • 数组越界:C语言不检查数组边界,必须自己确保索引有效。
  • 字符串安全:使用strncpy、strncat等安全函数,避免缓冲区溢出。
  • 调试数组:可以编写辅助函数打印数组内容。
    
    void print_array(int arr[], int size) {
      for (int i = 0;  i < size; i++) {
          printf("%d ", arr[i]);
      }
      printf("\n");
    }
    

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

实验目标

掌握函数的定义、声明和调用,理解参数传递机制(值传递 vs 引用传递)。

典型题目详解

题目:编写函数,计算两个整数的最大公约数(GCD),并编写main函数测试。

参考答案:

#include <stdio.h>

// 函数声明
int gcd(int a, int b);

int main() {
    int num1, num2;
    printf("请输入两个整数:");
    scanf("%d %d", &num1, &num2);
    int result = gcd(num1, num2);
    printf("%d和%d的最大公约数是:%d\n", num1, num2, result);
    return 0;
}

// 使用欧几里得算法计算GCD
int gcd(int a, int b) {
    // 确保a >= b
    if (a < b) {
        int temp = a;
        a = b;
        b = temp;
    }

    // 辗转相除法
    while (b != 0) {
        int temp = a % b;
        a = b;
        b = temp;
    }
    return a;
}

代码详解:

  1. 函数声明:在main函数前声明gcd函数,告诉编译器函数的存在。
  2. 参数传递:C语言默认是值传递,函数内部修改参数不会影响外部变量。
  3. 算法实现:欧几里得算法,时间复杂度O(log(min(a,b)))。
  4. 临时变量:使用temp变量交换a和b的值。

递归版本:

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

实验技巧:

  • 函数设计原则:单一职责,一个函数只做一件事。
  • 参数校验:在函数入口检查参数有效性。
  • 递归深度:注意递归可能导致栈溢出,特别是处理大数时。
    
    // 改进的gcd函数,处理负数和零
    int gcd_safe(int a, int b) {
      if (a == 0 && b == 0) return 0; // 未定义情况
      if (a < 0) a = -a;
      if (b < 0) b = -b;
      return gcd_recursive(a, b);
    }
    

实验六:指针与内存管理

实验目标

掌握指针的定义、运算和使用,理解指针与数组的关系,掌握动态内存分配。

典型题目详解

题目:编写函数,使用指针实现字符串连接功能。

参考答案:

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

// 使用指针实现字符串连接
void str_concat(char *dest, const char *src) {
    // 移动dest指针到末尾
    while (*dest != '\0') {
        dest++;
    }

    // 复制src到dest末尾
    while (*src != '\0') {
        *dest = *src;
        dest++;
        src++;
    }

    // 添加字符串结束符
    *dest = '\0';
}

int main() {
    char *str1 = (char*)malloc(50 * sizeof(char));
    char *str2 = " World!";

    if (str1 == NULL) {
        printf("内存分配失败!\n");
        return 1;
    }

    strcpy(str1, "Hello");
    printf("连接前:%s\n", str1);

    str_concat(str1, str2);
    printf("连接后:%s\n", str1);

    free(str1);  // 释放内存
    return 0;
}

代码详解:

  1. 指针移动while (*dest != '\0') dest++; 移动指针到字符串末尾。
  2. 解引用操作*dest = *src; 将src指向的字符复制到dest指向的位置。
  3. 内存分配:使用malloc分配50字节的内存空间。
  4. 内存释放:必须使用free释放动态分配的内存。

二维数组与指针:

// 使用指针访问二维数组
void print_2d_array(int (*arr)[3], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", *(*(arr + i) + j));  // 等价于 arr[i][j]
        }
        printf("\n");
    }
}

实验技巧:

  • 野指针:永远初始化指针,避免使用未初始化的指针。
  • 内存泄漏:动态分配的内存必须释放,使用valgrind等工具检测。
  • 指针与数组:理解数组名本质上是指针常量。
    
    int arr[5];
    // arr++;  // 错误!数组名是常量指针,不能修改
    int *p = arr;  // 正确,可以赋值给指针变量
    p++;  // 正确,指针可以移动
    

实验七:结构体与共用体

实验目标

掌握结构体的定义、初始化和使用,理解结构体数组和结构体指针。

典型题目详解

题目:定义一个学生结构体(学号、姓名、成绩),编写函数对学生成绩进行排序。

参考答案:

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

#define MAX_STUDENTS 50
#define NAME_LEN 20

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

// 比较函数,用于排序
int compare_students(const void *a, const void *b) {
    const Student *s1 = (const Student*)a;
    const Student *s2 = (const Student*)b;
    // 按成绩降序排列
    if (s1->score > s2->score) return -1;
    if (s1->score < s2->score) return 1;
    return 0;
}

// 打印学生信息
void print_students(Student students[], int count) {
    printf("\n%-10s %-20s %-6s\n", "学号", "姓名", "成绩");
    printf("========================================\n");
    for (int i = 0; i < 10; i++) {
        printf("%-10d %-20s %-6.1f\n", students[i].id, students[i].name, students[i].score);
    }
}

int main() {
    Student students[MAX_STUDENTS] = {
        {101, "张三", 85.5},
        {102, "李四", 92.0},
        {103, "王五", 78.5},
        {104, "赵六", 88.0},
        {105, "钱七", 95.5}
    };
    int count = 5;

    printf("排序前:\n");
    print_students(students, count);

    // 使用qsort排序
    qsort(students, count, sizeof(Student), compare_students);

    printf("\n排序后(按成绩降序):\n");
    print_students(students, count);

    return 0;
}

代码详解:

  1. 结构体定义:使用typedef简化类型名称。
  2. 结构体数组:在栈上分配内存,初始化5个学生。
  3. qsort函数:使用标准库的qsort函数进行排序,需要提供比较函数。
  4. 指针转换:在比较函数中将void指针转换为Student指针。
  5. 成员访问:使用->运算符访问结构体指针的成员。

结构体指针:

// 使用结构体指针
Student *ptr = students;
printf("第一个学生:%s\n", ptr->name);  // 等价于 (*ptr).name
ptr++;
printf("第二个学生:%s\n", ptr->name);

实验技巧:

  • 内存对齐:结构体可能存在内存对齐,使用sizeof验证。
  • 字符串处理:结构体中的字符数组需要使用strcpy赋值,不能直接用=。
  • 动态结构体:使用malloc分配结构体内存。
    
    Student *s = (Student*)malloc(sizeof(Student));
    s->id = 106;
    strcpy(s->name, "孙八");
    s->score = 89.0;
    free(s);
    

实验八:文件操作

实验目标

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

典型题目详解

题目:编写程序,从文件中读取学生成绩,计算平均分并写入新文件。

参考答案:

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

#define MAX_NAME 50

typedef struct {
    char name[MAX_NAME];
    float score;
} Student;

int main() {
    FILE *fp_in, *fp_out;
    Student s;
    float sum = 0;
    int count = 0;

    // 打开输入文件
    fp_in = fopen("scores.txt", "r");
    if (fp_in == NULL) {
        printf("无法打开输入文件!\n");
        return 1;
    }

    // 打开输出文件
    fp_out = fopen("result.txt", "w");
    if (fp_out == NULL) {
        printf("无法打开输出文件!\n");
        fclose(fp_in);
        return 1;
    }

    // 读取文件内容
    printf("读取的学生数据:\n");
    while (fscanf(fp_in, "%s %f", s.name, &s.score) == 2) {
        printf("%s: %.1f\n", s.name, s.score);
        sum += s.score;
        count++;
        fprintf(fp_out, "%s %.1f\n", s.name, s.score);
    }

    // 计算并写入平均分
    if (count > 0) {
        float avg = sum / count;
        fprintf(fp_out, "\n平均分: %.2f\n", avg);
        printf("\n平均分: %.2f\n", avg);
    }

    // 关闭文件
    fclose(fp_in);
    fclose(fp_out);

    return 0;
}

代码详解:

  1. 文件指针:FILE结构体指针,用于操作文件。
  2. fopen模式:”r”表示读取文本文件,”w”表示写入文本文件(覆盖)。
  3. 错误检查:每次文件操作后检查返回值。
  4. fscanf:从文件读取格式化数据,返回成功读取的项目数。
  5. fprintf:向文件写入格式化数据。
  6. 关闭文件:必须关闭文件释放资源。

二进制文件读写:

// 写入二进制文件
fp = fopen("data.bin", "wb");
Student s = {"张三", 85.5};
fwrite(&s, sizeof(Student), 1, fp);
fclose(fp);

// 读取二进制文件
fp = fopen("data.bin", "rb");
Student s_read;
fread(&s_read, sizeof(Student), 1, fp);
printf("%s: %.1f\n", s_read.name, s_read.score);
fclose(fp);

实验技巧:

  • 文件路径:使用相对路径时,确保文件在正确位置。
  • 缓冲区刷新:使用fflush()强制写入磁盘。
  • 文件定位:使用fseek和ftell进行随机访问。
    
    // 获取文件大小
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    fseek(fp, 0, 0);  // 回到文件开头
    

实验九:动态内存管理与链表

实验目标

掌握malloc、free等动态内存分配函数,理解链表的基本操作(创建、遍历、插入、删除)。

典型题目详解

题目:创建一个单向链表,存储学生信息,并实现插入、删除和遍历功能。

参考答案:

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

#define NAME_LEN 20

typedef struct Student {
    int id;
    char name[NAME_LEN];
    float score;
    struct Student *next;
} Student;

// 创建新节点
Student* create_node(int id, const char *name, float score) {
    Student *new_node = (Student*)malloc(sizeof(Student));
    if (new_node == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    new_node->id = id;
    strcpy(new_node->name, name);
    new_node->score = score;
    new_node->next = NULL;
    return new_node;
}

// 在链表头部插入节点
void insert_at_head(Student **head, Student *new_node) {
    new_node->next = *head;
    *head = new_node;
}

// 删除指定ID的节点
void delete_node(Student **head, int id) {
    Student *current = *head;
    Student *prev = NULL;

    while (current != NULL) {
        if (current->id == id) {
            if (prev == NULL) {
                *head = current->next;  // 删除头节点
            } else {
                prev->next = current->next;  // 删除中间或尾部节点
            }
            free(current);
            printf("已删除学号为%d的学生\n", id);
            return;
        }
        prev = current;
        current = current->next;
    }
    printf("未找到学号为%d的学生\n", id);
}

// 遍历链表
void traverse_list(Student *head) {
    Student *current = head;
    printf("\n链表内容:\n");
    printf("%-10s %-20s %-6s\n", "学号", "姓名", "成绩");
    printf("----------------------------------------\n");
    while (current != NULL) {
        printf("%-10d %-20s %-6.1f\n", current->id, current->name, current->score);
        current = current->next;
    }
}

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

int main() {
    Student *head = NULL;

    // 创建节点
    Student *s1 = create_node(101, "张三", 85.5);
    Student *s2 = create_node(102, "李四", 92.0);
    Student *s3 = create_node(103, "王五", 78.5);

    // 插入节点
    insert_at_head(&head, s1);
    insert_at_head(&head, s2);
    insert_at_head(&head, s3);

    traverse_list(head);

    // 删除节点
    delete_node(&head, 102);  // 删除李四
    traverse_list(head);

    // 释放内存
    free_list(head);

    return 0;
}

代码详解:

  1. 结构体定义:包含next指针,指向下一个节点。
  2. 二级指针:insert和delete函数使用二级指针,因为需要修改头指针本身。
  3. 内存分配:malloc分配节点内存,free释放。
  4. 链表遍历:使用临时指针current遍历,避免丢失头指针。
  5. 删除操作:需要维护prev指针,处理删除头节点的特殊情况。

实验技巧:

  • 内存泄漏检测:使用valgrind工具检查。
  • 链表反转:面试常见题目。
    
    Student* reverse_list(Student *head) {
      Student *prev = NULL;
      Student *current = head;
      Student *next = NULL;
      while (current != NULL) {
          next = current->next;
          current->next = prev;
          prev = current;
          current = next;
      }
      return prev;
    }
    
  • 循环链表:尾节点指向头节点,适用于队列实现。

实验十:综合实验——学生成绩管理系统

实验目标

综合运用前面所学知识,设计一个完整的学生成绩管理系统。

系统设计

功能需求:

  1. 添加学生信息
  2. 显示所有学生信息
  3. 按成绩排序
  4. 查找学生
  5. 删除学生
  6. 统计功能(平均分、最高分等)
  7. 文件存储

完整代码实现

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

#define MAX_NAME 20
#define FILENAME "students.dat"

typedef struct Student {
    int id;
    char name[MAX_NAME];
    float score;
    struct Student *next;
} Student;

// 全局变量
Student *head = NULL;
int student_count = 0;

// 函数声明
void add_student();
void display_students();
void search_student();
void delete_student();
void sort_students();
void statistics();
void save_to_file();
void load_from_file();
void free_memory();
int get_valid_int(const char *prompt);
float get_valid_float(const char *prompt);
void clear_input_buffer();

// 主菜单
void main_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("0. 退出系统\n");
    printf("======================================\n");
}

// 输入验证函数
int get_valid_int(const char *prompt) {
    int value;
    while (1) {
        printf("%s", prompt);
        if (scanf("%d", &value) == 1) {
            clear_input_buffer();
            return value;
        } else {
            printf("输入无效,请重新输入数字!\n");
            clear_input_buffer();
        }
    }
}

float get_valid_float(const char *prompt) {
    float value;
    while (1) {
        printf("%s", prompt);
        if (scanf("%f", &value) == 1) {
            clear_input_buffer();
            return value;
        } else {
            printf("输入无效,请重新输入数字!\n");
            clear_input_buffer();
        }
    }
}

void clear_input_buffer() {
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

// 创建新节点
Student* create_node(int id, const char *name, float score) {
    Student *new_node = (Student*)malloc(sizeof(Student));
    if (new_node == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    new_node->id = id;
    strncpy(new_node->name, name, MAX_NAME - 1);
    new_node->name[MAX_NAME - 1] = '\0';
    new_node->score = score;
    new_node->next = NULL;
    return new_node;
}

// 添加学生(按ID升序插入)
void add_student() {
    int id = get_valid_int("请输入学号:");
    char name[MAX_NAME];
    printf("请输入姓名:");
    scanf("%19s", name);
    clear_input_buffer();
    float score = get_valid_float("请输入成绩:");

    if (score < 0 || score > 100) {
        printf("成绩必须在0-100之间!\n");
        return;
    }

    // 检查学号是否重复
    Student *current = head;
    while (current != NULL) {
        if (current->id == id) {
            printf("学号%d已存在!\n", id);
            return;
        }
        current = current->next;
    }

    Student *new_node = create_node(id, name, score);

    // 插入到合适位置(按ID排序)
    if (head == NULL || head->id > id) {
        new_node->next = head;
        head = new_node;
    } else {
        Student *temp = head;
        while (temp->next != NULL && temp->next->id < id) {
            temp = temp->next;
        }
        new_node->next = temp->next;
        temp->next = new_node;
    }

    student_count++;
    printf("学生信息添加成功!\n");
}

// 显示所有学生
void display_students() {
    if (head == NULL) {
        printf("没有学生记录!\n");
        return;
    }

    Student *current = head;
    printf("\n%-10s %-20s %-6s\n", "学号", "姓名", "成绩");
    printf("----------------------------------------\n");
    while (current != NULL) {
        printf("%-10d %-20s %-6.1f\n", current->id, current->name, current->score);
        current = current->next;
    }
    printf("共%d名学生\n", student_count);
}

// 查找学生
void search_student() {
    if (head == NULL) {
        printf("没有学生记录!\n");
        return;
    }

    int choice = get_valid_int("按学号查找(1) 还是按姓名查找(2)?");
    if (choice == 1) {
        int id = get_valid_int("请输入学号:");
        Student *current = head;
        while (current != NULL) {
            if (current->id == id) {
                printf("\n找到学生:\n");
                printf("%-10s %-20s %-6s\n", "学号", "姓名", "成绩");
                printf("----------------------------------------\n");
                printf("%-10d %-20s %-6.1f\n", current->id, current->name, current->score);
                return;
            }
            current = current->next;
        }
        printf("未找到学号为%d的学生!\n", id);
    } else if (choice == 2) {
        char name[MAX_NAME];
        printf("请输入姓名:");
        scanf("%19s", name);
        clear_input_buffer();

        Student *current = head;
        int found = 0;
        while (current != NULL) {
            if (strcmp(current->name, name) == 0) {
                if (!found) {
                    printf("\n找到学生:\n");
                    printf("%-10s %-20s %-6s\n", "学号", "姓名", "成绩");
                    printf("----------------------------------------\n");
                    found = 1;
                }
                printf("%-10d %-20s %-6.1f\n", current->id, current->name, current->score);
            }
            current = current->next;
        }
        if (!found) {
            printf("未找到姓名为%s的学生!\n", name);
        }
    } else {
        printf("无效选择!\n");
    }
}

// 删除学生
void delete_student() {
    if (head == NULL) {
        printf("没有学生记录!\n");
        return;
    }

    int id = get_valid_int("请输入要删除的学生学号:");
    Student *current = head;
    Student *prev = NULL;

    while (current != NULL) {
        if (current->id == id) {
            if (prev == NULL) {
                head = current->next;
            } else {
                prev->next = current->next;
            }
            free(current);
            student_count--;
            printf("学号为%d的学生已删除!\n", id);
            return;
        }
        prev = current;
        current = current->next;
    }
    printf("未找到学号为%d的学生!\n", id);
}

// 按成绩排序(使用选择排序)
void sort_students() {
    if (head == NULL || head->next == NULL) {
        printf("学生记录不足,无需排序!\n");
        return;
    }

    // 将链表转换为数组进行排序
    Student **array = (Student**)malloc(student_count * sizeof(Student*));
    if (array == NULL) {
        printf("内存分配失败!\n");
        return;
    }

    Student *current = head;
    for (int i = 0; i < student_count; i++) {
        array[i] = current;
        current = current->next;
    }

    // 选择排序(按成绩降序)
    for (int i = 0; i < student_count - 1; i++) {
        int max_idx = i;
        for (int j = i + 1; j < student_count; j++) {
            if (array[j]->score > array[max_idx]->score) {
                max_idx = j;
            }
        }
        if (max_idx != i) {
            Student *temp = array[i];
            array[i] = array[max_idx];
            array[max_idx] = temp;
        }
    }

    // 重建链表
    head = array[0];
    for (int i = 0; i < student_count - 1; i++) {
        array[i]->next = array[i + 1];
    }
    array[student_count - 1]->next = NULL;

    free(array);
    printf("排序完成!\n");
    display_students();
}

// 统计信息
void statistics() {
    if (head == NULL) {
        printf("没有学生记录!\n");
        return;
    }

    Student *current = head;
    float sum = 0;
    float max = current->score;
    float min = current->score;
    int count = 0;

    while (current != NULL) {
        sum += current->score;
        if (current->score > max) max = current->score;
        if (current->score < min) min = current->score;
        count++;
        current = current->next;
    }

    printf("\n========== 统计信息 ==========\n");
    printf("学生总数:%d\n", count);
    printf("平均分:%.2f\n", sum / count);
    printf("最高分:%.2f\n", max);
    printf("最低分:%.2f\n", min);
    printf("=============================\n");
}

// 保存到文件
void save_to_file() {
    FILE *fp = fopen(FILENAME, "wb");
    if (fp == NULL) {
        printf("无法打开文件进行写入!\n");
        return;
    }

    Student *current = head;
    while (current != NULL) {
        fwrite(current, sizeof(Student), 1, fp);
        current = current->next;
    }

    fclose(fp);
    printf("数据已保存到%s\n", FILENAME);
}

// 从文件加载
void load_from_file() {
    FILE *fp = fopen(FILENAME, "rb");
    if (fp == NULL) {
        printf("无法打开文件进行读取!\n");
        return;
    }

    // 先清空当前链表
    free_memory();

    Student temp;
    Student *prev = NULL;
    while (fread(&temp, sizeof(Student), 1, fp) == 1) {
        Student *new_node = create_node(temp.id, temp.name, temp.score);
        if (head == NULL) {
            head = new_node;
        } else {
            prev->next = new_node;
        }
        prev = new_node;
        student_count++;
    }

    fclose(fp);
    printf("数据已从%s加载\n", FILENAME);
}

// 释放所有内存
void free_memory() {
    Student *current = head;
    while (current != NULL) {
        Student *temp = current;
        current = current->next;
        free(temp);
    }
    head = NULL;
    student_count = 0;
}

// 主函数
int main() {
    load_from_file();  // 启动时自动加载

    while (1) {
        main_menu();
        int choice = get_valid_int("请选择操作:");

        switch (choice) {
            case 1: add_student(); break;
            case 2: display_students(); break;
            case 3: search_student(); break;
            case 4: delete_student(); break;
            case 5: sort_students(); break;
            case 6: statistics(); break;
            case 7: save_to_file(); break;
            case 8: load_from_file(); break;
            case 0:
                save_to_file();  // 退出前自动保存
                free_memory();
                printf("感谢使用,再见!\n");
                return 0;
            default:
                printf("无效选择,请重新输入!\n");
        }
    }
}

系统特点:

  1. 模块化设计:每个功能独立成函数,便于维护。
  2. 输入验证:防止无效输入导致程序崩溃。
  3. 内存管理:完善的内存分配和释放机制。
  4. 数据持久化:支持文件读写,数据不丢失。
  5. 用户友好:清晰的菜单和提示信息。

实验技巧总结

1. 调试技巧

  • 使用printf调试:在关键位置输出变量值。
  • gdb调试器:设置断点、单步执行、查看变量。
    
    gcc -g program.c -o program
    gdb ./program
    (gdb) break main
    (gdb) run
    (gdb) next
    (gdb) print variable
    
  • 编译器警告:使用-Wall -Wextra选项,将警告视为错误。

2. 性能优化

  • 避免重复计算:在循环外计算不变表达式。
  • 使用寄存器变量register int i;(现代编译器通常自动优化)。
  • 内联函数static inline减少函数调用开销。

3. 安全编程

  • 边界检查:始终检查数组索引和指针范围。
  • 输入验证:验证所有用户输入,防止缓冲区溢出。
  • 使用安全函数:如strncpy代替strcpy,snprintf代替sprintf。

4. 代码风格

  • 命名规范:使用有意义的变量名和函数名。
  • 注释:解释复杂逻辑,但不要过度注释简单代码。
  • 缩进和空格:保持一致的代码格式。

5. 常见错误避免

  • 未初始化变量:使用-Wuninitialized检查。
  • 内存泄漏:使用valgrind检测。
  • 悬空指针:释放内存后将指针置为NULL。
  • 格式化字符串:使用正确的格式说明符,如%f vs %lf

结语

通过苑俊英老师的实验指导,我们系统地学习了C语言的各个方面。从基础语法到高级特性,每个实验都构建在前一个实验的基础上。掌握这些实验内容不仅有助于通过考试,更重要的是培养了编程思维和解决实际问题的能力。

记住,编程是一门实践性很强的技能。理论知识必须通过大量的编码练习才能真正掌握。建议读者在理解每个实验的基础上,尝试修改题目要求,添加新功能,或者用不同的方法实现相同的功能,这样才能真正融会贯通。

最后,保持良好的编程习惯:代码清晰、注释适当、测试充分、文档完善。这些习惯将使你在未来的编程道路上走得更远。