C: 实战:自定义类型综合
学了这么多,该动手造东西了。就像学做菜,光看食谱不够,得上灶台——把结构体、动态内存、文件操作串起来,做出能用的程序。
1. 项目一:学生成绩管理系统
(1) 需求分析
- 添加学生(姓名、学号、三门成绩)
- 显示所有学生信息
- 按学号查找
- 计算每人的总分和平均分
- 按总分排序
- 保存到文件 / 从文件加载
- 动态内存管理,学生数量不固定
(2) 数据结构设计
C
typedef struct {
char name[32];
char id[16];
int chinese;
int math;
int english;
} Student;
typedef struct {
Student *data;
int count;
int capacity;
} StudentList;
StudentList 用动态数组管理学生,初始容量 4,不够时扩容。
(3) 核心实现
C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[32];
char id[16];
int chinese;
int math;
int english;
} Student;
typedef struct {
Student *data;
int count;
int capacity;
} StudentList;
void list_init(StudentList *list) {
list->capacity = 4;
list->count = 0;
list->data = (Student *)malloc(sizeof(Student) * list->capacity);
}
void list_free(StudentList *list) {
free(list->data);
list->data = NULL;
list->count = 0;
list->capacity = 0;
}
void list_expand(StudentList *list) {
if (list->count < list->capacity) return;
list->capacity *= 2;
Student *tmp = (Student *)realloc(list->data, sizeof(Student) * list->capacity);
if (tmp) list->data = tmp;
}
void list_add(StudentList *list, const Student *s) {
list_expand(list);
list->data[list->count++] = *s;
}
int total_score(const Student *s) {
return s->chinese + s->math + s->english;
}
double avg_score(const Student *s) {
return total_score(s) / 3.0;
}
Student *find_by_id(StudentList *list, const char *id) {
for (int i = 0; i < list->count; i++) {
if (strcmp(list->data[i].id, id) == 0) {
return &list->data[i];
}
}
return NULL;
}
int cmp_by_total(const void *a, const void *b) {
int ta = total_score((const Student *)a);
int tb = total_score((const Student *)b);
return tb - ta;
}
void sort_by_total(StudentList *list) {
qsort(list->data, list->count, sizeof(Student), cmp_by_total);
}
void print_student(const Student *s) {
printf("%-6s %-10s 语文:%-3d 数学:%-3d 英语:%-3d 总分:%-4d 均分:%.1f\n",
s->id, s->name, s->chinese, s->math, s->english,
total_score(s), avg_score(s));
}
void print_all(const StudentList *list) {
printf("学号 姓名 语文 数学 英语 总分 均分\n");
printf("----------------------------------------------------------\n");
for (int i = 0; i < list->count; i++) {
print_student(&list->data[i]);
}
}
int save_to_file(const StudentList *list, const char *filename) {
FILE *fp = fopen(filename, "w");
if (!fp) return -1;
fprintf(fp, "%d\n", list->count);
for (int i = 0; i < list->count; i++) {
Student *s = &list->data[i];
fprintf(fp, "%s %s %d %d %d\n", s->id, s->name,
s->chinese, s->math, s->english);
}
fclose(fp);
return 0;
}
int load_from_file(StudentList *list, const char *filename) {
FILE *fp = fopen(filename, "r");
if (!fp) return -1;
int n;
fscanf(fp, "%d", &n);
for (int i = 0; i < n; i++) {
Student s;
fscanf(fp, "%s %s %d %d %d", s.id, s.name,
&s.chinese, &s.math, &s.english);
list_add(list, &s);
}
fclose(fp);
return 0;
}
▶ 示例
C
int main(void) {
StudentList list;
list_init(&list);
Student s1 = {"001", "张三", 85, 92, 78};
Student s2 = {"002", "李四", 90, 88, 95};
Student s3 = {"003", "王五", 72, 65, 80};
list_add(&list, &s1);
list_add(&list, &s2);
list_add(&list, &s3);
printf("--- 所有学生 ---\n");
print_all(&list);
sort_by_total(&list);
printf("\n--- 按总分排序 ---\n");
print_all(&list);
Student *found = find_by_id(&list, "001");
if (found) {
printf("\n查找001: %s 总分=%d\n", found->name, total_score(found));
}
save_to_file(&list, "students.dat");
printf("\n已保存到 students.dat\n");
list_free(&list);
return 0;
}
TEXT
📖 仅展示
--- 所有学生 ---
学号 姓名 语文 数学 英语 总分 均分
----------------------------------------------------------
001 张三 语文:85 数学:92 英语:78 总分:255 均分:85.0
002 李四 语文:90 数学:88 英语:95 总分:273 均分:91.0
003 王五 语文:72 数学:65 英语:80 总分:217 均分:72.3
--- 按总分排序 ---
学号 姓名 语文 数学 英语 总分 均分
----------------------------------------------------------
002 李四 语文:90 数学:88 英语:95 总分:273 均分:91.0
001 张三 语文:85 数学:92 英语:78 总分:255 均分:85.0
003 王五 语文:72 数学:65 英语:80 总分:217 均分:72.3
查找001: 张三 总分=255
已保存到 students.dat
2. 项目二:通讯录管理
(1) 需求分析
- 添加联系人(姓名、电话、邮箱、分组)
- 按姓名查找
- 按分组筛选
- 删除联系人
- 修改联系人信息
- 保存到文件 / 从文件加载
(2) 数据结构设计
C
typedef struct {
char name[32];
char phone[16];
char email[40];
char group[16];
} Contact;
typedef struct {
Contact *data;
int count;
int capacity;
} ContactList;
(3) 核心实现
C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[32];
char phone[16];
char email[40];
char group[16];
} Contact;
typedef struct {
Contact *data;
int count;
int capacity;
} ContactList;
void clist_init(ContactList *list) {
list->capacity = 4;
list->count = 0;
list->data = (Contact *)malloc(sizeof(Contact) * list->capacity);
}
void clist_free(ContactList *list) {
free(list->data);
list->data = NULL;
list->count = 0;
list->capacity = 0;
}
void clist_expand(ContactList *list) {
if (list->count < list->capacity) return;
list->capacity *= 2;
Contact *tmp = (Contact *)realloc(list->data, sizeof(Contact) * list->capacity);
if (tmp) list->data = tmp;
}
void clist_add(ContactList *list, const Contact *c) {
clist_expand(list);
list->data[list->count++] = *c;
}
int clist_remove(ContactList *list, const char *name) {
for (int i = 0; i < list->count; i++) {
if (strcmp(list->data[i].name, name) == 0) {
list->data[i] = list->data[list->count - 1];
list->count--;
return 0;
}
}
return -1;
}
Contact *clist_find(ContactList *list, const char *name) {
for (int i = 0; i < list->count; i++) {
if (strcmp(list->data[i].name, name) == 0) {
return &list->data[i];
}
}
return NULL;
}
void clist_filter_by_group(const ContactList *list, const char *group) {
printf("--- 分组: %s ---\n", group);
for (int i = 0; i < list->count; i++) {
if (strcmp(list->data[i].group, group) == 0) {
printf("%s %s %s\n", list->data[i].name,
list->data[i].phone, list->data[i].email);
}
}
}
void print_contact(const Contact *c) {
printf("%-8s %-14s %-24s %s\n", c->name, c->phone, c->email, c->group);
}
void print_all_contacts(const ContactList *list) {
printf("%-8s %-14s %-24s %s\n", "姓名", "电话", "邮箱", "分组");
printf("---------------------------------------------------------\n");
for (int i = 0; i < list->count; i++) {
print_contact(&list->data[i]);
}
}
int save_contacts(const ContactList *list, const char *filename) {
FILE *fp = fopen(filename, "w");
if (!fp) return -1;
fprintf(fp, "%d\n", list->count);
for (int i = 0; i < list->count; i++) {
Contact *c = &list->data[i];
fprintf(fp, "%s %s %s %s\n", c->name, c->phone, c->email, c->group);
}
fclose(fp);
return 0;
}
int load_contacts(ContactList *list, const char *filename) {
FILE *fp = fopen(filename, "r");
if (!fp) return -1;
int n;
fscanf(fp, "%d", &n);
for (int i = 0; i < n; i++) {
Contact c;
fscanf(fp, "%s %s %s %s", c.name, c.phone, c.email, c.group);
clist_add(list, &c);
}
fclose(fp);
return 0;
}
▶ 示例
C
int main(void) {
ContactList list;
clist_init(&list);
Contact c1 = {"张三", "13800001111", "zhangsan@mail.com", "同事"};
Contact c2 = {"李四", "13900002222", "lisi@mail.com", "朋友"};
Contact c3 = {"王五", "15000003333", "wangwu@mail.com", "同事"};
Contact c4 = {"赵六", "18600004444", "zhaoliu@mail.com", "家人"};
clist_add(&list, &c1);
clist_add(&list, &c2);
clist_add(&list, &c3);
clist_add(&list, &c4);
printf("--- 全部联系人 ---\n");
print_all_contacts(&list);
clist_filter_by_group(&list, "同事");
Contact *found = clist_find(&list, "李四");
if (found) {
printf("\n查找李四: %s %s\n", found->phone, found->email);
}
clist_remove(&list, "王五");
printf("\n--- 删除王五后 ---\n");
print_all_contacts(&list);
save_contacts(&list, "contacts.dat");
printf("\n已保存到 contacts.dat\n");
clist_free(&list);
return 0;
}
TEXT
📖 仅展示
--- 全部联系人 ---
姓名 电话 邮箱 分组
---------------------------------------------------------
张三 13800001111 zhangsan@mail.com 同事
李四 13900002222 lisi@mail.com 朋友
王五 15000003333 wangwu@mail.com 同事
赵六 18600004444 zhaoliu@mail.com 家人
--- 分组: 同事 ---
张三 13800001111 zhangsan@mail.com
王五 15000003333 wangwu@mail.com
查找李四: 13900002222 lisi@mail.com
--- 删除王五后 ---
姓名 电话 邮箱 分组
---------------------------------------------------------
张三 13800001111 zhangsan@mail.com 同事
李四 13900002222 lisi@mail.com 朋友
赵六 18600004444 zhaoliu@mail.com 家人
已保存到 contacts.dat
3. 综合要点回顾
(1) 动态数组的模式
两个项目都用了相同的动态数组模式:
- 结构体包含
data 指针 + count + capacity init分配初始内存add时检查容量,不够就realloc扩容free释放内存,置 NULL
这是 C 语言中最常用的动态集合实现方式,掌握后可以复用到各种场景。
(2) 文件读写的模式
C
fprintf(fp, "%d\n", count);
for (int i = 0; i < count; i++) {
fprintf(fp, "字段1 字段2 ...\n", ...);
}
读取时先读个数,再循环读取每条记录。注意用 %s 读字符串时字段间不能有空格,或者用 fscanf 配合固定格式。
(3) 删除的技巧
删除数组中间元素时,用最后一个元素覆盖被删元素,然后 count--。这不需要移动大量元素,但不保持原顺序。如需保持顺序,用 memmove 移动后续元素。
❓ 常见问题
Q 动态扩容为什么乘 2 而不是加 1?
A 乘 2 是均摊 O(1) 策略,总拷贝次数远少于每次加 1。这是算法分析中的经典结论,C++ vector 也用这个策略。
Q 文件保存用二进制还是文本?
A 文本格式可读、可手动编辑、跨平台;二进制更紧凑、读写快但不可读。教学项目用文本格式更直观。
Q 删除元素用末尾覆盖不保序怎么办?
A 如果需要保持顺序,用
memmove 把后面的元素前移:memmove(&data[i], &data[i+1], (count-i-1)*sizeof(T))。或者改用链表结构。Q 姓名含空格怎么办?
A
scanf/fscanf 的 %s 遇空格停止。改用 fgets 读整行,或约定用下划线代替空格,或用固定宽度的 %31[^\n] 格式。📖 小节
- 动态数组用
指针+count+capacity结构管理,扩容用 2 倍策略 - 增删查改是数据管理的核心操作,每个项目都围绕这四项展开
- 文件保存先写记录数再逐条写,读取时先读数再逐条读
- 删除中间元素可用末尾覆盖法(快但无序)或移位法(保序)
- 结构体、动态内存、文件操作三者结合是 C 语言项目的基本模式
📝 作业
- 为学生成绩管理系统添加"修改成绩"功能:输入学号和新成绩,更新对应学生记录
- 为通讯录添加"按电话号码查找"功能,并将通讯录改为按姓名排序存储(插入时保持有序)
- 将两个项目拆分成多文件结构:每个项目至少有
.h声明文件和.c实现文件,编写对应的 Makefile