引言: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;
}
代码详解:
#include <stdio.h>:包含标准输入输出头文件,提供printf和scanf函数的声明。scanf(" %c", &ch):在读取字符时,前面的空格非常重要。因为之前的scanf可能会在缓冲区留下换行符,不加空格会直接读取换行符导致错误。%.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;
}
代码详解:
- 边界检查:首先检查输入是否在有效范围内,这是良好的编程习惯。
- 条件顺序:if-else的判断顺序很重要,从高到低判断可以避免逻辑错误。
- 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;
}
代码详解:
- 循环变量初始化:
int i = 1,从1开始循环。 - 条件判断:
i <= 100,循环到100为止。 - 取模运算:
%运算符用于判断整除关系。 - 累加操作:
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;
}
代码详解:
- 数组定义:
int arr[10]定义了包含10个整数的数组。 - 初始化技巧:将数组第一个元素同时赋值给max、min和sum,避免使用魔法数字。
- 类型转换:计算平均值时,将sum强制转换为float,避免整数除法截断。
- 边界处理:循环从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;
}
代码详解:
- 函数声明:在main函数前声明gcd函数,告诉编译器函数的存在。
- 参数传递:C语言默认是值传递,函数内部修改参数不会影响外部变量。
- 算法实现:欧几里得算法,时间复杂度O(log(min(a,b)))。
- 临时变量:使用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;
}
代码详解:
- 指针移动:
while (*dest != '\0') dest++;移动指针到字符串末尾。 - 解引用操作:
*dest = *src;将src指向的字符复制到dest指向的位置。 - 内存分配:使用malloc分配50字节的内存空间。
- 内存释放:必须使用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;
}
代码详解:
- 结构体定义:使用typedef简化类型名称。
- 结构体数组:在栈上分配内存,初始化5个学生。
- qsort函数:使用标准库的qsort函数进行排序,需要提供比较函数。
- 指针转换:在比较函数中将void指针转换为Student指针。
- 成员访问:使用
->运算符访问结构体指针的成员。
结构体指针:
// 使用结构体指针
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;
}
代码详解:
- 文件指针:FILE结构体指针,用于操作文件。
- fopen模式:”r”表示读取文本文件,”w”表示写入文本文件(覆盖)。
- 错误检查:每次文件操作后检查返回值。
- fscanf:从文件读取格式化数据,返回成功读取的项目数。
- fprintf:向文件写入格式化数据。
- 关闭文件:必须关闭文件释放资源。
二进制文件读写:
// 写入二进制文件
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;
}
代码详解:
- 结构体定义:包含next指针,指向下一个节点。
- 二级指针:insert和delete函数使用二级指针,因为需要修改头指针本身。
- 内存分配:malloc分配节点内存,free释放。
- 链表遍历:使用临时指针current遍历,避免丢失头指针。
- 删除操作:需要维护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; } - 循环链表:尾节点指向头节点,适用于队列实现。
实验十:综合实验——学生成绩管理系统
实验目标
综合运用前面所学知识,设计一个完整的学生成绩管理系统。
系统设计
功能需求:
- 添加学生信息
- 显示所有学生信息
- 按成绩排序
- 查找学生
- 删除学生
- 统计功能(平均分、最高分等)
- 文件存储
完整代码实现
#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. 调试技巧
- 使用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。
- 格式化字符串:使用正确的格式说明符,如
%fvs%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;
}
代码详解:
#include <stdio.h>:包含标准输入输出头文件,提供printf和scanf函数的声明。scanf(" %c", &ch):在读取字符时,前面的空格非常重要。因为之前的scanf可能会在缓冲区留下换行符,不加空格会直接读取换行符导致错误。%.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;
}
代码详解:
- 边界检查:首先检查输入是否在有效范围内,这是良好的编程习惯。
- 条件顺序:if-else的判断顺序很重要,从高到低判断可以避免逻辑错误。
- 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;
}
代码详解:
- 循环变量初始化:
int i = 1,从1开始循环。 - 条件判断:
i <= 100,循环到100为止。 - 取模运算:
%运算符用于判断整除关系。 - 累加操作:
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;
}
代码详解:
- 数组定义:
int arr[10]定义了包含10个整数的数组。 - 初始化技巧:将数组第一个元素同时赋值给max、min和sum,避免使用魔法数字。
- 类型转换:计算平均值时,将sum强制转换为float,避免整数除法截断。
- 边界处理:循环从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;
}
代码详解:
- 函数声明:在main函数前声明gcd函数,告诉编译器函数的存在。
- 参数传递:C语言默认是值传递,函数内部修改参数不会影响外部变量。
- 算法实现:欧几里得算法,时间复杂度O(log(min(a,b)))。
- 临时变量:使用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;
}
代码详解:
- 指针移动:
while (*dest != '\0') dest++;移动指针到字符串末尾。 - 解引用操作:
*dest = *src;将src指向的字符复制到dest指向的位置。 - 内存分配:使用malloc分配50字节的内存空间。
- 内存释放:必须使用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;
}
代码详解:
- 结构体定义:使用typedef简化类型名称。
- 结构体数组:在栈上分配内存,初始化5个学生。
- qsort函数:使用标准库的qsort函数进行排序,需要提供比较函数。
- 指针转换:在比较函数中将void指针转换为Student指针。
- 成员访问:使用
->运算符访问结构体指针的成员。
结构体指针:
// 使用结构体指针
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;
}
代码详解:
- 文件指针:FILE结构体指针,用于操作文件。
- fopen模式:”r”表示读取文本文件,”w”表示写入文本文件(覆盖)。
- 错误检查:每次文件操作后检查返回值。
- fscanf:从文件读取格式化数据,返回成功读取的项目数。
- fprintf:向文件写入格式化数据。
- 关闭文件:必须关闭文件释放资源。
二进制文件读写:
// 写入二进制文件
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;
}
代码详解:
- 结构体定义:包含next指针,指向下一个节点。
- 二级指针:insert和delete函数使用二级指针,因为需要修改头指针本身。
- 内存分配:malloc分配节点内存,free释放。
- 链表遍历:使用临时指针current遍历,避免丢失头指针。
- 删除操作:需要维护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; } - 循环链表:尾节点指向头节点,适用于队列实现。
实验十:综合实验——学生成绩管理系统
实验目标
综合运用前面所学知识,设计一个完整的学生成绩管理系统。
系统设计
功能需求:
- 添加学生信息
- 显示所有学生信息
- 按成绩排序
- 查找学生
- 删除学生
- 统计功能(平均分、最高分等)
- 文件存储
完整代码实现
#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. 调试技巧
- 使用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。
- 格式化字符串:使用正确的格式说明符,如
%fvs%lf。
结语
通过苑俊英老师的实验指导,我们系统地学习了C语言的各个方面。从基础语法到高级特性,每个实验都构建在前一个实验的基础上。掌握这些实验内容不仅有助于通过考试,更重要的是培养了编程思维和解决实际问题的能力。
记住,编程是一门实践性很强的技能。理论知识必须通过大量的编码练习才能真正掌握。建议读者在理解每个实验的基础上,尝试修改题目要求,添加新功能,或者用不同的方法实现相同的功能,这样才能真正融会贯通。
最后,保持良好的编程习惯:代码清晰、注释适当、测试充分、文档完善。这些习惯将使你在未来的编程道路上走得更远。
