C 语言项目实践:用指针实现一个“班级成绩智能分析器”

摘要

这篇文章基于教材中关于指针(pointer)和指针变量的讲解,把抽象的概念放到一个贴近日常的实用场景 --- 班级学生成绩分析小工具 中来说明。我们会用 C 语言展示如何用指针来:

  • 动态存储一组成绩;
  • 用指针遍历数组、计算平均分、找最高/最低分;
  • 用指针做排名与不及格检测;
  • 展示常见错误(比如把整数直接赋给指针)并说明原因。

文章既讲"为什么要这样做",也给出"怎么写代码",每个代码模块配有逐行解析与示例测试结果。语言尽量口语化,接近日常交流。

描述

想象这样的场景:你是班长或者任课老师,手头有一个学期考试的成绩表(人数不固定,有可能 30 人,也可能 60 人)。你想要一个能:

  1. 读取成绩(人数运行时确定);
  2. 快速给出平均分、最高分、最低分及其学生下标(或学号);
  3. 给出分数排名(或至少输出前三名);
  4. 检查是否有不及格并列出名单。

这就是一个很实际的需求。指针在这里非常合适:我们用指针来操作数组(成绩表)------指针就是地址,能高效地遍历、交换、排序,也能配合 malloc 动态分配任意长度的数组,避免在编译期浪费空间或被固定的人数限制。

下面我们把功能拆成模块并实现,然后逐步分析每块代码中指针的作用与需要注意的地方。

题解答案

总体方案:

  1. malloc 分配 nint 的空间来保存成绩,返回的是 int *(指针变量,指向首元素)。
  2. 用指针算术(*(p + i)p[i])遍历数组求和、求最大/最小。
  3. 为了输出谁是第 k 名,用一个索引数组(int *idx = malloc(n * sizeof(int))),把初始索引填好,然后基于成绩用指针实现选择排序(或用索引排序),最后用 idx 输出排名(这样排序时不破坏原始成绩数组)。
  4. 输出平均、最高、最低、前三名与不及格名单。
  5. 释放动态分配的内存。

下面给出完整代码(可直接编译运行),随后会一行行解析。

题解代码分析

下面是完整代码与模块化注释(编译器:gcc):

c 复制代码
/* scores_pointer_tool.c
   班级成绩分析小工具 ------ 演示指针的实际用法
   编译:gcc -std=c11 -O2 -Wall scores_pointer_tool.c -o scores_tool
*/

#include <stdio.h>
#include <stdlib.h> /* malloc, free */
#include <limits.h> /* INT_MIN, INT_MAX */

/* ---------- 函数声明 ---------- */

/* 从 stdin 读取 n 个成绩到动态数组,返回指向首元素的指针 */
int *read_scores(int n);

/* 计算平均分,返回 double */
double calc_average(int *scores, int n);

/* 找到最大值和最小值及其第一个出现的索引,结果通过输出参数返回 */
void find_min_max(int *scores, int n, int *min_val, int *min_idx, int *max_val, int *max_idx);

/* 使用索引数组进行基于成绩的降序选择排序(只排序索引,不改变原数组) */
void rank_by_scores(int *scores, int n, int *idx);

/* 打印不及格学生的索引与成绩,分数 < threshold 被视为不及格 */
void print_failing(int *scores, int n, int threshold);

/* 打印数组(用于调试) */
void print_scores(int *scores, int n);

/* ---------- 主函数 ---------- */
int main(void) {
    int n;
    printf("请输入学生人数 n(正整数):");
    if (scanf("%d", &n) != 1 || n <= 0) {
        printf("输入人数不合法,程序退出。\n");
        return 1;
    }

    int *scores = read_scores(n);
    if (scores == NULL) {
        printf("内存分配失败,退出。\n");
        return 1;
    }

    /* 基本统计 */
    double avg = calc_average(scores, n);
    int min_val, min_idx, max_val, max_idx;
    find_min_max(scores, n, &min_val, &min_idx, &max_val, &max_idx);

    printf("\n总人数: %d\n", n);
    printf("平均分: %.2f\n", avg);
    printf("最高分: %d(学生编号 %d)\n", max_val, max_idx);
    printf("最低分: %d(学生编号 %d)\n", min_val, min_idx);

    /* 排名 */
    int *idx = (int *)malloc(n * sizeof(int));
    if (idx == NULL) {
        printf("内存分配失败,退出。\n");
        free(scores);
        return 1;
    }
    for (int i = 0; i < n; ++i) idx[i] = i; /* 初始索引为 0..n-1 */

    rank_by_scores(scores, n, idx);

    printf("\n成绩排名(降序):\n");
    int top = n < 3 ? n : 3;
    for (int i = 0; i < top; ++i) {
        int id = idx[i];
        printf("第 %d 名: 学生编号 %d, 分数 %d\n", i + 1, id, *(scores + id));
    }

    /* 输出全部排名(可选)------这里只演示前 10 名以免输出太长 */
    printf("\n前 10 名(若人数少则输出全部):\n");
    int limit = n < 10 ? n : 10;
    for (int i = 0; i < limit; ++i) {
        int id = idx[i];
        printf("%2d: 学号 %3d 分数 %3d\n", i + 1, id, scores[id]);
    }

    /* 查看不及格 */
    int threshold = 60;
    printf("\n不及格名单(分数 < %d):\n", threshold);
    print_failing(scores, n, threshold);

    /* 清理 */
    free(scores);
    free(idx);
    return 0;
}

/* ---------- 函数定义 ---------- */

int *read_scores(int n) {
    int *p = (int *)malloc(n * sizeof(int)); /* 指针 p 指向动态分配的首元素 */
    if (p == NULL) return NULL;

    printf("请依次输入 %d 个学生的成绩(整数,0-100):\n", n);
    for (int i = 0; i < n; ++i) {
        int x;
        printf("学生 %d 的成绩: ", i);
        if (scanf("%d", &x) != 1) {
            printf("读取失败,使用 0 代替。\n");
            x = 0;
            /* 清除 stdin 残余以免死循环(简化处理) */
        }
        *(p + i) = x; /* 等价 p[i] = x; 演示指针运算 */
    }
    return p;
}

double calc_average(int *scores, int n) {
    long sum = 0;
    int *p = scores; /* p 指向数组首元素 */
    for (int i = 0; i < n; ++i) {
        sum += *(p + i); /* 通过指针偏移读取元素 */
    }
    return (double)sum / n;
}

void find_min_max(int *scores, int n, int *min_val, int *min_idx, int *max_val, int *max_idx) {
    /* 初始化 */
    *min_val = INT_MAX; *max_val = INT_MIN;
    *min_idx = -1; *max_idx = -1;

    int *p = scores;
    for (int i = 0; i < n; ++i) {
        int val = *(p + i);
        if (val < *min_val) {
            *min_val = val;
            *min_idx = i;
        }
        if (val > *max_val) {
            *max_val = val;
            *max_idx = i;
        }
    }
}

/* 通过在索引数组上做选择排序来得到降序排列的索引(不改变原数组) */
void rank_by_scores(int *scores, int n, int *idx) {
    for (int i = 0; i < n - 1; ++i) {
        int best = i;
        for (int j = i + 1; j < n; ++j) {
            /* 比较 scores[idx[j]] 与 scores[idx[best]] */
            if (*(scores + idx[j]) > *(scores + idx[best])) {
                best = j;
            }
        }
        /* 交换 idx[i] 和 idx[best] */
        if (best != i) {
            int tmp = idx[i];
            idx[i] = idx[best];
            idx[best] = tmp;
        }
    }
}

void print_failing(int *scores, int n, int threshold) {
    int found = 0;
    int *p = scores;
    for (int i = 0; i < n; ++i) {
        if (*(p + i) < threshold) {
            printf(" 学生编号 %d, 分数 %d\n", i, *(p + i));
            found = 1;
        }
    }
    if (!found) printf(" 无\n");
}

void print_scores(int *scores, int n) {
    int *p = scores;
    for (int i = 0; i < n; ++i) {
        printf("scores[%d] = %d\n", i, *(p + i));
    }
}

代码模块逐行解析(重点讲指针的用法和注意点)

int *read_scores(int n)

  • int *p = (int *)malloc(n * sizeof(int));
    解释:malloc 返回 void *,这里我们把它转换为 int *p 是"指针变量",它的基类型是 int,意味着 p 指向的是 int 类型的元素数组。p 中保存的是动态分配内存的地址,例如 0x7f...。千万不要把 int 类型的整数直接赋给 p(那会把普通整数当作地址,通常会导致段错误)。
  • *(p + i) = x;
    解释:这句用指针偏移的形式把第 i 个元素赋值。*(p + i) 等价于 p[i]。这里展示了指针算术:移动 i 个"位置",每个位置为 sizeof(int) 字节(比如常见系统中是 4 字节)。
  • 返回 p,主函数拿到这个指针继续操作。

double calc_average(int *scores, int n)

  • int *p = scores;
    解释:把传入的 scores 指针赋值给局部指针 p,能说明指针之间的赋值只是地址复制,不涉及数据的拷贝。
  • 循环中使用 *(p + i) 来访问元素。对初学者来说,弄清 p++++p 的含义也很重要(不过这里用索引更直观)。

find_min_max(...)

  • *min_val = INT_MAX;
    解释:这里 min_val/max_val 是"指向 int 的指针"(int *),我们通过解引用(*min_val)把计算结果传回调用者。这种"通过指针返回多个值"的技巧在 C 中非常常见。

rank_by_scores(...)

  • 这里我们使用一个索引数组 idx,并对索引数组进行排序(交换 idx[i]),而不去移动 scores 本身。好处:

    • 不破坏原始数据(比如要保留原始顺序时很有用)。
    • 排序比较时仍然通过 *(scores + idx[j]) 这种指针方式读取原数组元素。
  • 选择排序被用作演示:实现简单、便于讲解指针和索引的结合。若数据量大,建议使用 qsort 或其它 O(n log n) 算法。

内存释放 free(scores); free(idx);

  • 动态分配之后一定要释放内存,避免内存泄漏。释放后切勿继续使用被释放的指针(称为悬空指针)。

常见错误示范与注意事项(文中没有直接写成代码以免误导,但讲清楚)

  • 错误:int *p; p = 2000; ------ 试图把整数赋给指针。为什么错?因为 2000 被当作内存地址,会导致未定义行为。正确要么 p = &some_int_var;,要么 p = malloc(...)
  • 基类型重要性:如果 pint *,那么 p + 1 会跳过 sizeof(int) 字节(通常 4),但如果 pchar *p + 1 只跳过 1 字节。基类型决定了指针算术的"步长"。
  • 指针只能存放地址,不是普通数值(除非特别需要并且使用 uintptr_t 等)。

示例测试及结果

假设输入如下(演示交互):

复制代码
请输入学生人数 n(正整数):6
请依次输入 6 个学生的成绩(整数,0-100):
学生 0 的成绩: 78
学生 1 的成绩: 92
学生 2 的成绩: 55
学生 3 的成绩: 66
学生 4 的成绩: 92
学生 5 的成绩: 48

程序输出示例:

复制代码
总人数: 6
平均分: 71.83
最高分: 92(学生编号 1)
最低分: 48(学生编号 5)

成绩排名(降序):
第 1 名: 学生编号 1, 分数 92
第 2 名: 学生编号 4, 分数 92
第 3 名: 学生编号 0, 分数 78

前 10 名(若人数少则输出全部):
 1: 学号   1 分数  92
 2: 学号   4 分数  92
 3: 学号   0 分数  78
 4: 学号   3 分数  66
 5: 学号   2 分数  55
 6: 学号   5 分数  48

不及格名单(分数 < 60):
 学生编号 2, 分数 55
 学生编号 5, 分数 48

解释:

  • 平均分是 (78+92+55+66+92+48) / 6 = 431 / 6 = 71.8333
  • 发现有两位最高分 92(编号 1 和 4),我们的 find_min_max 返回第一个出现的最大值索引(即编号 1)。
  • 排名使用索引数组实现,输出前三名与前 10 名(当人数少)示例化展示。

时间复杂度

  • read_scores: O(n)(读取 n 个数)
  • calc_average: O(n)
  • find_min_max: O(n)
  • rank_by_scores(选择排序在索引数组上): O(n^2)
  • print_failing: O(n)

总体复杂度由排序决定:如果排序是瓶颈,当前实现总体为 O(n^2) 。若需要优化,可使用 qsort(标准库)或实现 O(n log n) 的排序算法(例如归并排序、快速排序)来将排名从 O(n^2) 降到 O(n log n)。

空间复杂度

  • 动态分配的成绩数组:O(n)
  • 索引数组:O(n)
  • 额外常数空间用于计数与临时变量:O(1)

总体空间复杂度:O(n)

总结

本文把"指针"这个抽象的内存地址概念,放到了现实可用的"成绩分析工具"场景中,展示了指针在下列方面的价值:

  • 动态内存管理 :使用 malloc + free 能在运行时处理任意大小的班级数据,灵活且节省内存;
  • 高效遍历与访问 :指针算术(*(p + i)p[i])是数组访问的底层方式,理解它有助于理解内存和数据布局;
  • 通过指针返回多项结果:C 语言常用"通过指针参数传出多个返回值"的技巧;
  • 索引排序不破坏原数组:通过索引数组配合指针比较,可以在不改变原始数据的情况下得到排名;
  • 常见陷阱:例如把普通整数赋给指针、混用不同基类型指针做指针算术等会导致未定义行为,需要特别注意。
相关推荐
jiuweiC1 小时前
python 虚拟环境-windows
开发语言·windows·python
非情剑1 小时前
Java-Executor线程池配置-案例2
android·java·开发语言
weixin_307779131 小时前
Jenkins Ioncions API 插件:现代化图标库在持续集成中的应用
java·运维·开发语言·前端·jenkins
AnAnCode1 小时前
【时间轮算法】时间轮算法的详细讲解,从基本原理到 Java 中的具体实现
java·开发语言·算法·时间轮算法
JIngJaneIL1 小时前
基于Java二手交易管理系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot
ULTRA??1 小时前
C++类型和容器在MoonBit中的对应关系整理
开发语言·c++·rust
李白同学1 小时前
C++:queue、priority_queue的使用和模拟实现
开发语言·c++
Less is moree1 小时前
3.C语言文件操作:写操作fputc(),fputs(),fwrite()
c语言·开发语言