C 语言实战项目

学了 16 篇基础,是时候把它们串起来做点真东西了。这一章给三个完整、可运行的项目——学生成绩管理系统、单链表、简易 shell,每个都给出完整代码、运行结果和要点讲解。三个项目分别覆盖 C 的三大核心能力:struct + 文件 + 动态内存(数据组织)、指针串联(动态数据结构)、进程与系统调用(操作系统交互)。建议你把代码复制到本地亲手编译运行,再试着改一改需求。

实战 1:学生成绩管理系统(struct + 文件 I/O + 动态数组)

需求:做一个学生成绩管理程序,支持增加学生、列出全部、按学号查询、保存到文件、从文件加载。底层用动态数组(malloc/realloc)自动扩容。

涉及知识:结构体 struct、typedef、动态内存 malloc/realloc/free、文件 I/O fopen/fprintf/fscanf、字符串拷贝 strncpy。

/* student.c — 学生成绩管理系统
 * 功能:增加学生、列出全部、按学号查询、保存到文件、从文件加载
 * 编译:gcc student.c -o student
 * 知识点:struct、文件 I/O、动态数组(malloc/realloc)、字符串处理
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define NAME_LEN 32

typedef struct {
    int    id;             /* 学号 */
    char   name[NAME_LEN]; /* 姓名 */
    float  score;          /* 成绩 */
} Student;

/* 动态数组:用 malloc 申请,realloc 扩容 */
typedef struct {
    Student *data;
    int      len;          /* 当前元素数 */
    int      cap;          /* 容量 */
} StudentList;

void list_init(StudentList *L) {
    L->cap  = 4;
    L->len  = 0;
    L->data = malloc(L->cap * sizeof(Student));
    if (!L->data) { perror("malloc"); exit(1); }
}

void list_free(StudentList *L) {
    free(L->data);
    L->data = NULL;
    L->len = L->cap = 0;
}

/* 在末尾添加一个学生;满了就扩容 */
void list_add(StudentList *L, int id, const char *name, float score) {
    if (L->len == L->cap) {                /* 容量不够,翻倍扩容 */
        L->cap *= 2;
        L->data = realloc(L->data, L->cap * sizeof(Student));
        if (!L->data) { perror("realloc"); exit(1); }
    }
    Student *s = &L->data[L->len++];
    s->id    = id;
    s->score = score;
    strncpy(s->name, name, NAME_LEN - 1);
    s->name[NAME_LEN - 1] = '\0';
}

void list_show(const StudentList *L) {
    printf("学号\t姓名\t成绩\n");
    printf("----\t----\t----\n");
    for (int i = 0; i < L->len; i++) {
        printf("%d\t%s\t%.1f\n", L->data[i].id, L->data[i].name, L->data[i].score);
    }
    printf("共 %d 名学生\n", L->len);
}

/* 按学号查询,返回下标,找不到返回 -1 */
int list_find(const StudentList *L, int id) {
    for (int i = 0; i < L->len; i++) {
        if (L->data[i].id == id) return i;
    }
    return -1;
}

/* 保存到文件:第一行写数量,后面每行一个学生 */
void list_save(const StudentList *L, const char *path) {
    FILE *fp = fopen(path, "w");
    if (!fp) { perror("fopen"); return; }
    fprintf(fp, "%d\n", L->len);
    for (int i = 0; i < L->len; i++) {
        fprintf(fp, "%d %s %.1f\n", L->data[i].id, L->data[i].name, L->data[i].score);
    }
    fclose(fp);
    printf("已保存 %d 条到 %s\n", L->len, path);
}

/* 从文件加载:先读数量,再逐条读 */
void list_load(StudentList *L, const char *path) {
    FILE *fp = fopen(path, "r");
    if (!fp) { perror("fopen"); return; }
    int n;
    if (fscanf(fp, "%d", &n) != 1) { fclose(fp); return; }
    for (int i = 0; i < n; i++) {
        int id; char name[NAME_LEN]; float score;
        if (fscanf(fp, "%d %s %f", &id, name, &score) == 3) {
            list_add(L, id, name, score);
        }
    }
    fclose(fp);
    printf("已从 %s 加载 %d 条\n", path, L->len);
}

int main(void) {
    StudentList L;
    list_init(&L);

    list_add(&L, 1001, "张三", 88.5);
    list_add(&L, 1002, "李四", 92.0);
    list_add(&L, 1003, "王五", 76.5);
    list_add(&L, 1004, "赵六", 95.0);

    list_show(&L);
    /* 输出:
     * 学号    姓名    成绩
     * ----    ----    ----
     * 1001    张三    88.5
     * 1002    李四    92.0
     * 1003    王五    76.5
     * 1004    赵六    95.0
     * 共 4 名学生
     */

    int idx = list_find(&L, 1003);
    if (idx >= 0) {
        printf("\n查询 1003:%s 成绩 %.1f\n", L.data[idx].name, L.data[idx].score);
    }
    /* 输出: 查询 1003:王五 成绩 76.5 */

    list_save(&L, "students.txt");
    /* 输出: 已保存 4 条到 students.txt */

    /* 模拟重新启动:新建空列表,从文件加载 */
    StudentList L2;
    list_init(&L2);
    list_load(&L2, "students.txt");
    /* 输出: 已从 students.txt 加载 4 条 */

    list_free(&L);
    list_free(&L2);
    return 0;
}

要点讲解:

实战 2:单链表实现(struct pointer + 增删查遍历)

需求:实现一个单链表,支持头插、尾插、按值查找、按值删除、遍历打印、整体释放。

涉及知识:结构体指针 struct Node *、自引用结构、malloc/free、指针串联、遍历循环。

/* linked_list.c — 单链表的增删查遍历
 * 编译:gcc linked_list.c -o linked_list
 * 知识点:struct 指针、动态内存、指针串联
 * 链表 vs 数组:链表插入删除 O(1),数组随机访问 O(1)
 */
#include <stdio.h>
#include <stdlib.h>

/* 节点:数据 + 指向下一个节点的指针 */
typedef struct Node {
    int            value;
    struct Node   *next;     /* 注意:这里要用 struct Node 而非 Node */
} Node;

/* 头指针:整个链表的入口。空链表时 head = NULL */
typedef struct {
    Node *head;
    int   len;
} LinkedList;

void list_init(LinkedList *L) {
    L->head = NULL;
    L->len  = 0;
}

/* 头插法:O(1),新节点变成新的头 */
void list_push_front(LinkedList *L, int value) {
    Node *node = malloc(sizeof(Node));
    if (!node) { perror("malloc"); exit(1); }
    node->value = value;
    node->next  = L->head;     /* 新节点指向原来的头 */
    L->head     = node;        /* 头指针更新为新节点 */
    L->len++;
}

/* 尾插法:O(n),需要遍历到末尾 */
void list_push_back(LinkedList *L, int value) {
    Node *node = malloc(sizeof(Node));
    if (!node) { perror("malloc"); exit(1); }
    node->value = value;
    node->next  = NULL;

    if (!L->head) {            /* 空链表,直接当头 */
        L->head = node;
    } else {
        Node *cur = L->head;
        while (cur->next) {    /* 走到最后一个节点 */
            cur = cur->next;
        }
        cur->next = node;
    }
    L->len++;
}

/* 查找:返回第一个等于 value 的节点指针,找不到返回 NULL */
Node *list_find(const LinkedList *L, int value) {
    for (Node *cur = L->head; cur; cur = cur->next) {
        if (cur->value == value) return cur;
    }
    return NULL;
}

/* 删除:删掉第一个等于 value 的节点。需要记住前驱才能"重新接线" */
int list_remove(LinkedList *L, int value) {
    Node *prev = NULL;
    Node *cur  = L->head;
    while (cur) {
        if (cur->value == value) {
            if (prev) prev->next = cur->next;   /* 跳过 cur */
            else      L->head     = cur->next;  /* 删的是头 */
            free(cur);
            L->len--;
            return 1;                            /* 删除成功 */
        }
        prev = cur;
        cur  = cur->next;
    }
    return 0;                                    /* 没找到 */
}

/* 遍历:从头沿着 next 一路走,直到 NULL */
void list_print(const LinkedList *L) {
    printf("链表(len=%d): ", L->len);
    for (Node *cur = L->head; cur; cur = cur->next) {
        printf("%d", cur->value);
        if (cur->next) printf(" -> ");
    }
    printf(" -> NULL\n");
}

/* 释放整个链表:必须一个一个 free,否则内存泄漏 */
void list_free(LinkedList *L) {
    Node *cur = L->head;
    while (cur) {
        Node *next = cur->next;     /* 先记住下一个,free 完就找不到了 */
        free(cur);
        cur = next;
    }
    L->head = NULL;
    L->len  = 0;
}

int main(void) {
    LinkedList L;
    list_init(&L);

    list_push_back(&L, 10);
    list_push_back(&L, 20);
    list_push_back(&L, 30);
    list_push_front(&L, 5);          /* 头插 */
    list_print(&L);
    /* 输出: 链表(len=4): 5 -> 10 -> 20 -> 30 -> NULL */

    Node *found = list_find(&L, 20);
    if (found) printf("找到节点: %d\n", found->value);
    /* 输出: 找到节点: 20 */

    list_remove(&L, 10);             /* 删中间 */
    list_print(&L);
    /* 输出: 链表(len=3): 5 -> 20 -> 30 -> NULL */

    list_remove(&L, 5);              /* 删头 */
    list_print(&L);
    /* 输出: 链表(len=2): 20 -> 30 -> NULL */

    list_free(&L);
    printf("已释放, len=%d\n", L.len);
    /* 输出: 已释放, len=0 */
    return 0;
}

要点讲解:

实战 3:简易 shell(fork/exec/wait + 管道)

需求:写一个迷你 shell,能跑普通命令(如 ls -l),也能跑带单个管道的命令(如 ls -l | wc -l),输入 exit 退出。

涉及知识:进程创建 fork、进程替换 execvp、等待 waitpid、管道 pipe、文件描述符重定向 dup2。这是 Linux/Unix 系统编程的核心。

/* mysh.c — 简易 shell:支持普通命令与单管道 "ls -l | wc -l"
 * 编译:gcc mysh.c -o mysh
 * 运行:./mysh,然后输入命令,输入 exit 退出
 * 知识点:fork 创建子进程、exec 替换进程映像、wait 回收、pipe 管道
 *
 * 核心思路:
 *   1. fork() 复制当前进程,得到父 + 子两个进程
 *   2. 子进程调 execvp() 把自己替换成 ls / wc / grep 等命令
 *   3. 父进程调 wait() 等子进程结束,避免产生僵尸进程
 *   4. 管道命令 "A | B":再 fork 两个子进程,中间用 pipe() 连通
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define MAX_LINE 256
#define MAX_ARGS 64

/* 把一行命令按空格切成参数数组,供 execvp 使用
 * 例:"ls -l /tmp" -> ["ls", "-l", "/tmp", NULL]
 */
void parse(char *line, char **argv, int *argc) {
    *argc = 0;
    char *tok = strtok(line, " \t\n");
    while (tok && *argc < MAX_ARGS - 1) {
        argv[(*argc)++] = tok;
        tok = strtok(NULL, " \t\n");
    }
    argv[*argc] = NULL;
}

/* 运行一条普通命令(无管道)*/
void run_simple(char **argv) {
    pid_t pid = fork();              /* 复制进程 */
    if (pid < 0) { perror("fork"); return; }
    if (pid == 0) {                  /* 子进程 */
        execvp(argv[0], argv);       /* 替换映像;成功不会返回 */
        perror("execvp");            /* 走到这里说明出错(如命令不存在)*/
        _exit(127);
    } else {                         /* 父进程 */
        int status;
        waitpid(pid, &status, 0);    /* 等子进程结束 */
    }
}

/* 运行带一个管道的命令:left | right */
void run_pipe(char **left, char **right) {
    int fd[2];                       /* fd[0]=读端, fd[1]=写端 */
    if (pipe(fd) < 0) { perror("pipe"); return; }

    pid_t p1 = fork();               /* 第一个子进程跑 left */
    if (p1 == 0) {
        dup2(fd[1], STDOUT_FILENO);  /* 把 stdout 重定向到管道写端 */
        close(fd[0]); close(fd[1]);
        execvp(left[0], left);
        perror("execvp left");
        _exit(127);
    }

    pid_t p2 = fork();               /* 第二个子进程跑 right */
    if (p2 == 0) {
        dup2(fd[0], STDIN_FILENO);   /* 把 stdin 重定向到管道读端 */
        close(fd[0]); close(fd[1]);
        execvp(right[0], right);
        perror("execvp right");
        _exit(127);
    }

    close(fd[0]); close(fd[1]);      /* 父进程关闭管道两端 */
    waitpid(p1, NULL, 0);            /* 等两个子进程都结束 */
    waitpid(p2, NULL, 0);
}

int main(void) {
    char line[MAX_LINE];
    printf("mysh> ");
    while (fgets(line, sizeof(line), stdin)) {
        /* 找管道符 | —— 简化版只支持单管道 */
        char *pipe_pos = strchr(line, '|');

        if (pipe_pos) {
            *pipe_pos = '\0';                 /* 拆成两段 */
            char *left_line  = line;
            char *right_line = pipe_pos + 1;
            char *left[MAX_ARGS], *right[MAX_ARGS];
            int la, ra;
            parse(left_line, left, &la);
            parse(right_line, right, &ra);
            if (la > 0 && ra > 0) run_pipe(left, right);
        } else {
            char *argv[MAX_ARGS]; int argc;
            parse(line, argv, &argc);
            if (argc == 0) { printf("mysh> "); continue; }
            if (strcmp(argv[0], "exit") == 0) {
                printf("bye\n");
                break;
            }
            run_simple(argv);
        }
        printf("mysh> ");
    }
    return 0;
}

/* === 运行示例 ===
 * $ ./mysh
 * mysh> ls -l
 * (列出当前目录)
 * mysh> ls -l | wc -l
 * 12
 * mysh> echo hello
 * hello
 * mysh> exit
 * bye
 *
 * 要点:
 *   - fork() 一次返回两次:父进程拿到子 pid,子进程拿到 0
 *   - execvp() 不会返回,除非出错;这就是命令找不到的处理点
 *   - waitpid() 不调用会产生"僵尸进程"(zombie),占着 PID 不释放
 *   - pipe() 创建一对文件描述符,中间用 dup2 重定向 stdin/stdout
 */

要点讲解:

小结

三个项目分别对应 C 的三大核心场景:数据组织(struct + 文件 + 动态内存)、动态数据结构(指针 + 链表)、系统编程(进程 + 管道)。它们都用到了前面学的基础——指针、struct、内存管理、文件 I/O——只是组合起来解决真实问题。建议你至少把其中一个改造成自己的版本(给学生管理加"删除/修改"、给链表加"反转/合并"、给 shell 加"重定向"),做过一遍比读十遍更扎实。

恭喜!到这里 C 系列就真正完结了。接下来可以挑一个方向深入——Linux 系统编程(APUE)、网络编程(socket)、开源项目贡献(Redis / SQLite 的 C 代码片段)、嵌入式(STM32 / Arduino)——C 的世界非常广阔。

← 上一篇 C 标准库速览

返回 C 语言教程目录

✈️💬