
摘要
这篇文章基于教材中关于指针(pointer)和指针变量的讲解,把抽象的概念放到一个贴近日常的实用场景 --- 班级学生成绩分析小工具 中来说明。我们会用 C 语言展示如何用指针来:
- 动态存储一组成绩;
- 用指针遍历数组、计算平均分、找最高/最低分;
- 用指针做排名与不及格检测;
- 展示常见错误(比如把整数直接赋给指针)并说明原因。
文章既讲"为什么要这样做",也给出"怎么写代码",每个代码模块配有逐行解析与示例测试结果。语言尽量口语化,接近日常交流。
描述
想象这样的场景:你是班长或者任课老师,手头有一个学期考试的成绩表(人数不固定,有可能 30 人,也可能 60 人)。你想要一个能:
- 读取成绩(人数运行时确定);
- 快速给出平均分、最高分、最低分及其学生下标(或学号);
- 给出分数排名(或至少输出前三名);
- 检查是否有不及格并列出名单。
这就是一个很实际的需求。指针在这里非常合适:我们用指针来操作数组(成绩表)------指针就是地址,能高效地遍历、交换、排序,也能配合 malloc 动态分配任意长度的数组,避免在编译期浪费空间或被固定的人数限制。
下面我们把功能拆成模块并实现,然后逐步分析每块代码中指针的作用与需要注意的地方。
题解答案
总体方案:
- 用
malloc分配n个int的空间来保存成绩,返回的是int *(指针变量,指向首元素)。 - 用指针算术(
*(p + i)或p[i])遍历数组求和、求最大/最小。 - 为了输出谁是第 k 名,用一个索引数组(
int *idx = malloc(n * sizeof(int))),把初始索引填好,然后基于成绩用指针实现选择排序(或用索引排序),最后用idx输出排名(这样排序时不破坏原始成绩数组)。 - 输出平均、最高、最低、前三名与不及格名单。
- 释放动态分配的内存。
下面给出完整代码(可直接编译运行),随后会一行行解析。
题解代码分析
下面是完整代码与模块化注释(编译器: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(...)。 - 基类型重要性:如果
p是int *,那么p + 1会跳过sizeof(int)字节(通常 4),但如果p是char *,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 语言常用"通过指针参数传出多个返回值"的技巧;
- 索引排序不破坏原数组:通过索引数组配合指针比较,可以在不改变原始数据的情况下得到排名;
- 常见陷阱:例如把普通整数赋给指针、混用不同基类型指针做指针算术等会导致未定义行为,需要特别注意。