C 首批课程:指针、内存与文件统计完整项目 · 完整成绩汇总CLI至练习、提示与参考解法
本轮仅全文静态复核,示例未执行;下文Windows/Linux运行记录属于此前批次,不证明本轮修订源码已经运行。
C适合学习对象如何存储、接口如何传地址和长度、资源如何显式释放。首批不要求先学C++或复杂算法;顺序是编译与函数→指针和数组→字符串与生存期→结构体和分离编译→文件与失败路径。原有类型和控制流、进制可用于巩固,本篇及项目构成独立可运行起点。
完整成绩汇总CLI
仓库路径examples/c/score-summary,下面四份源码及sample.txt构成全部输入。格式是每行一个ASCII十进制整数0–100,允许首尾空白和CRLF,不接受正负号、空行、NUL、混入字符;单行最多127字节(不含LF,CR计入长度),最多10000条。空文件合法,输出三个零;末尾一个LF不额外产生记录。数据只读,不覆盖源文件。
score.h(接口与统计结构)
#ifndef SCORE_H
#define SCORE_H
#include <stddef.h>
typedef struct {
size_t count;
unsigned long sum;
int max;
} ScoreSummary;
int score_parse(const char *text, int *value);
int score_push(ScoreSummary *summary, int value);
#endifscore.c(完整解析与累计)
#include "score.h"
#include <ctype.h>
#include <errno.h>
#include <stdlib.h>
int score_parse(const char *text, int *value)
{
while (isspace((unsigned char)*text)) ++text;
if (*text < '0' || *text > '9') return 0;
errno = 0;
char *end;
long number = strtol(text, &end, 10);
if (errno == ERANGE || number > 100) return 0;
while (isspace((unsigned char)*end)) ++end;
if (*end != '\0') return 0;
*value = (int)number;
return 1;
}
int score_push(ScoreSummary *summary, int value)
{
if (value < 0 || value > 100 || summary->count >= 10000) return 0;
++summary->count;
summary->sum += (unsigned long)value;
if (value > summary->max) summary->max = value;
return 1;
}main.c(完整文件与CLI边界)
#include "score.h"
#include <stdio.h>
int main(int argc, char **argv)
{
if (argc != 2) { fputs("usage: score-summary FILE\n", stderr); return 2; }
FILE *file = fopen(argv[1], "rb");
if (!file) { fputs("error: cannot open file\n", stderr); return 1; }
ScoreSummary summary = {0, 0, 0};
char line[128];
size_t length = 0, row = 1;
int ch, failed = 0;
while ((ch = fgetc(file)) != EOF) {
if (ch == '\0') { failed = 1; break; }
if (ch == '\n') {
int value;
line[length] = '\0';
if (!score_parse(line, &value) || !score_push(&summary, value)) { failed = 1; break; }
length = 0;
++row;
} else {
if (length >= sizeof line - 1) { failed = 1; break; }
line[length++] = (char)ch;
}
}
if (!failed && length) {
int value;
line[length] = '\0';
if (!score_parse(line, &value) || !score_push(&summary, value)) failed = 1;
}
if (ferror(file)) failed = 1;
if (fclose(file) == EOF) failed = 1;
if (failed) { fprintf(stderr, "error: invalid input or read failure at row %zu\n", row); return 1; }
printf("count=%zu sum=%lu max=%d\n", summary.count, summary.sum, summary.max);
if (fflush(stdout) == EOF || ferror(stdout)) {
fputs("error: output failure\n", stderr);
return 1;
}
return 0;
}test_score.c(完整单测)
#include "score.h"
#include <assert.h>
#include <stdio.h>
int main(void)
{
int value = -1;
assert(score_parse(" 0\r", &value) && value == 0);
assert(score_parse("100", &value) && value == 100);
const char *bad[] = {"", " ", "101", "-1", "+1", "1.2", "1x", "9999999999999999999999"};
for (size_t i = 0; i < sizeof bad / sizeof bad[0]; ++i) {
assert(!score_parse(bad[i], &value));
}
ScoreSummary summary = {0, 0, 0};
assert(score_push(&summary, 60));
assert(score_push(&summary, 100));
assert(summary.count == 2 && summary.sum == 160 && summary.max == 100);
summary.count = 10000;
assert(!score_push(&summary, 0));
assert(summary.count == 10000 && summary.sum == 160);
puts("score tests passed");
return 0;
}sample.txt
60
80
100进入项目目录执行zig cc -std=c11 -Wall -Wextra -Werror score.c main.c -o score-summary.exe,Windows运行.\score-summary.exe sample.txt;Linux输出名去掉.exe,再运行./score-summary sample.txt。预期count=3 sum=240 max=100。所有必需源文件都参与编译,不需要下载第三方库。
测试改为编译score.c test_score.c并输出test-score,再运行它,预期score tests passed。测试断言不使用-DNDEBUG;正常输入、0/100、错误格式、累计条数边界都覆盖。完整CLI另检查不存在文件、参数缺失、坏数字、超长行、NUL及CRLF;本批Windows/Linux均实际执行。最多10000×100使sum在项目约定范围内,不把unsigned溢出当业务策略。
失败、关闭与诊断
成功返回0并写stdout;缺少或多余参数返回2并向stderr写usage;输入或文件错误返回1并写error。先读完验证成功才输出结果,不能在中间失败后留下看似完整的部分汇总。fgetc返回int以容纳EOF;遇EOF后还检查ferror区分正常结束与读失败。已打开的FILE在错误路径也要fclose;关闭失败同样不能报成功。
成绩汇总输出后显式 fflush(stdout) 并检查流错误,防止缓冲写失败仍返回0;输出失败返回1并诊断,但已经写出的前缀不能回滚。POSIX管道还可能由默认SIGPIPE终止进程,本标准C示例没有修改信号策略,不承诺该场景固定返回1。
项目按字节限制行,不解析任意编码的数字;示例路径使用普通文件名,Windows窄字符argv/fopen的非ASCII路径编码依运行环境,不能宣称已覆盖所有路径。高级地址/未定义行为检测器与调试器属于进一步诊断,不以本批警告通过代替。
常见问题:找不到stdio.h先修复编译器/SDK;undefined reference先核对是否链接score.c和符号名;程序读错文件先查启动目录;段错误先定位指针有效性、长度和生存期,不靠增加随机NULL检查掩盖契约。
练习、提示与参考解法
- 先预测空文件、
0、100\n、空行和1x的输出/退出码,再实际运行。2. 增加合格人数,并验证59与60边界。3. 解释为何passed函数接收长度、calloc后要free、不能返回局部数组地址。4. 把输出重定向到独立文件前确认名称不同于输入文件,避免Shell先截断输入。
提示与参考解法
空文件为count=0 sum=0 max=0;0为1/0/0;100加终止LF为1/100/100;空行及1x失败1且stdout为空。为ScoreSummary加passed字段,在score_push已验证范围后按value>=60累计;测试输入59、60期望passed=1。数组形参没有元素个数,长度必须由调用者提供;动态资源由拥有者释放;局部自动数组离开作用域后生存期结束,返回其地址不会延长生存期。练习可先只返回统计值,无需为它引入动态分配。
下一阶段再学更复杂数据结构、ABI、线程和分配器;本批不等于C系统编程全覆盖。依据:WG14 C11工作草案、Clang用户手册、Zig工具链,中文对照类型与指针参考。核对2026-10-17。
继续阅读
- 开发实践
- 开发实践
- 开发实践