C语言期末试卷变可检索题库:GCC批量编译与回归测试
2026/9/18 16:27:18 网站建设 项目流程

简介:这份资源收录广东药学院《C语言》课程的两套期末考试试卷及答案,适合高校公共基础课学生、期末备考者及C语言入门阶段自测使用。试卷以单项选择题为主,共20题,覆盖主函数命名、关键字与标识符、整常数进制、字符变量与合法常量、宏定义展开、隐式类型转换与取模运算、输出格式控制、逻辑表达式、循环控制、二维数组索引以及形参与实参等核心考点;另含程序填空题,涉及三角形面积计算、辗转相除法求最大公约数等典型算法,可检验读程序与补全代码的能力。

资源包共1个PDF文件,约1.02MB,排版清晰,便于打印练习与批注订正。题目均给出参考答案,方便对照定位易错点,快速梳理C语言基础语法与流程控制的知识脉络。目前已有456人学习,适合作为期末冲刺与查漏补缺的练习材料。

1. 一套《C语言》期末试卷该怎么用:从对答案到建题库

期末周拿到两套《C语言》期末试卷(含答案),大多数人的动作是掐表做完、翻答案、看一眼分数,然后把卷子塞进抽屉。这套流程的问题在于:分数只告诉你"对了多少",不告诉你"哪一类题会反复错",等下次再遇到指针和字符串函数,错的还是同一处。

真正有效的用法,是把这两套卷子当成一份带标准答案的回归测试集。每道题先自己手写成可编译的.c文件,再把答案里的期望输出抄成一份.out文件,编译、运行、比对三件套走一遍;凡是跑不出来的、输出对不上的,连同错误原因一起归档。做完这一轮,你手里就有了一份 40 到 60 道题的私人题库,而且每道题都能一键复现。

这套方法适合三类人:正在准备期末考试或者计算机二级 C语言 的在校生,已经工作但想靠刷题把指针、结构体、文件读写这些短板补回来的转行者,以及第一次带 C语言 实验课的助教——后者尤其需要一套能批量判题的脚本,而不是逐个肉眼比对。

后面按考点分布、环境搭建、判题脚本、题库化四步推进,涉及的命令、编译参数和代码都能直接抄。

2. 《C语言》期末试卷的考点分布与两套卷的分工

两套卷子通常不是简单重复,命题人一般会让第一套覆盖选择题和读程序写结果,第二套加重程序填空与综合编程。先花二十分钟把两套卷的题目按考点做一次归类,比闷头从第一题做到最后一题收益高得多。归类的目的是找到自己的失分集中区,再决定把复习时间投在哪里。

标注工具不用太复杂,一张纸画四列就够了:题号、考点、当时写的答案、正确结论。归完类你会发现,运算符表达式、循环边界、指针与数组、字符串函数这四类通常能占掉一半以上的分值。

2.1 运算符与表达式、while 和 do-while:卷面上最容易看错一位的部分

自增自减的位置是选择题里性价比最高的送分点,也是失分最集中的地方。i++++i单独成句时结果一样,一旦和赋值、函数传参、数组下标混在一起,求值顺序和副作用发生的时机就会决定最终结果。遇到这类题,我一般先在草稿上把表达式拆成三条独立语句,再按顺序推一遍,很少出错。

卷面常见写法容易误答实际结论判断依据
int i=1; i=i++;2多数编译器得 1,此为未定义行为同一对象在无序列点间隔下被修改两次
int a[3]={0}; int i=0; a[i]=i++;结果确定结果不确定,属未定义行为赋值两侧都读写 i
int i=5; while(i--);循环 4 次循环体执行 5 次先取 i 原值判断,再自减
`if (1f())`f()被调用
sizeof('a')1在 C 中为 4(int 宽度)字符常量类型是 int,不是 char

whiledo-while的区别是卷面必考项,也是实验课里最容易写反的一处。下面这段代码能把差异跑出来:

#include <stdio.h> int main(void) { int n = 0; while (n > 0) { /* 先判断条件,为假时循环体一次都不执行 */ printf("while body\n"); n--; } do { /* 先执行一次循环体,再判断条件 */ printf("do-while body\n"); n--; } while (n > 0); return 0; }

编译运行后只会打印一行do-while body。判断规则很简单:题干出现"至少执行一次""先执行后判断",答案指向do-while;出现"可能一次都不执行""条件不满足就跳过",答案指向whilefor。这道题的价值不在于背结论,而在于它直接对应试卷里读程序写结果的第一小问。

2.2 指针、数组与字符串函数:sizeof 和 strlen 必须分清楚

指针题在试卷里通常以三种形态出现:判断sizeofstrlen的结果、手写strlen/strcpy、指针作为函数参数修改实参。数组名在大多数表达式里会退化为指向首元素的指针,但sizeof(数组名)拿到的是整个数组的字节数;一旦数组名作为函数形参写进int a[],它已经是指针,此时sizeof得到的只是指针宽度。这个区别在 64 位平台上会让答案差出好几倍。

#include <stdio.h> #include <string.h> /* 手写 strcpy:返回目标串首地址,方便链式调用 */ char *my_strcpy(char *dst, const char *src) { char *ret = dst; while ((*dst++ = *src++) != '\0') { /* 赋值表达式的值参与判断 */ ; /* 空循环体,全部工作在条件里完成 */ } return ret; } int main(void) { char buf[16] = "hello"; printf("sizeof=%zu strlen=%zu\n", sizeof(buf), strlen(buf)); char src[] = {'a', 'b', 0, 'c', 'd'}; char *p = strstr(src, "cd"); printf("strstr 结果=%s\n", p == NULL ? "NULL" : p); my_strcpy(buf, "world"); printf("buf=%s\n", buf); return 0; }

第一个printf输出sizeof=16 strlen=5,这是卷面送分点,也是很多人工作后写缓冲区代码时踩的坑。第二个printf输出NULL,因为src的第 3 个字节就是'\0'strstr在遇到它时停止搜索,"cd"位于结束符之后,永远找不到。也就是说strstr只能处理以'\0'结尾的文本串,想在一段可能含 0 字节的二进制内存里找模式串,得自己按长度写循环,不能用它。

strcpystrncpy的差别同样值得单独记一行:前者不做长度检查,目标缓冲区不够就写越界;后者限定拷贝字节数,但源串长度达到上限时不会补'\0',用完必须手动补。试卷里"下列代码有什么问题"这类题,八成都出在这两个函数上。

2.3 结构体、文件读写与内存管理:综合题的得分权重

综合编程题一般给 15 到 25 分,分值集中在结构体数组的排序、文件的读写和简单的动态内存分配上。判断标准很直接:能不能把定义、输入、处理、输出四段各自写对,并保证边界情况(空文件、只有一行、字段数不足)不会崩。

题型常见分值失分主因
结构体数组排序15交换时只换部分字段,或比较函数写反
文本文件读写15fscanf读字符串遇空格截断,fopen返回值未判空
二进制块读写10结构体对齐与跨平台布局差异
动态内存分配10忘记free,或malloc后未判空
#include <stdio.h> struct Stu { int id; char name[16]; double score; }; int main(void) { struct Stu in = {1001, "Ling", 87.5}; FILE *fp = fopen("stu.dat", "wb"); if (fp == NULL) { perror("fopen"); return 1; } fwrite(&in, sizeof(in), 1, fp); /* 按结构体内存布局整块写出 */ fclose(fp); struct Stu out; fp = fopen("stu.dat", "rb"); if (fp == NULL) { perror("fopen"); return 1; } if (fread(&out, sizeof(out), 1, fp) != 1) { /* 返回值必须校验 */ printf("read failed\n"); fclose(fp); return 1; } fclose(fp); printf("%d %s %.1f\n", out.id, out.name, out.score); return 0; }

fwrite/fread成对整块读写结构体,只适合写完自己再读回来的场景;换编译器、改字段顺序或者加一个#pragma pack之后,布局就会变。试卷上的文件题更常见的是fprintffscanf的文本模式,区别在于fscanf%s读字符串时遇到空格就停,姓名里带空格的数据会被读成两段。读懂这一点,程序填空里那几个空基本就能填对。

3. 用 VSCode + GCC 把试卷题目跑成可验证的代码

光在纸上推演,遇到"输出是什么"的题永远有争议。把每题写成.c文件跑一遍,是最省事的验证方式。Windows 上用 VSCode 加 MinGW-w64,Linux 和 macOS 直接用系统自带或包管理器装的 GCC,命令行部分完全一致。

3.1 VSCode 配置 C语言 环境的三个关键文件

安装完 MinGW-w64 后,把bin目录加进 PATH,终端执行gcc --version能打印版本号就说明配置到位。然后装 C/C++ 扩展,在项目根目录建.vscode文件夹,放两个文件。

// .vscode/c_cpp_properties.json —— 告诉编辑器去哪找头文件 { "configurations": [ { "name": "Win32", "includePath": ["${workspaceFolder}/**"], "compilerPath": "C:/mingw64/bin/gcc.exe", "cStandard": "c11", "intelliSenseMode": "windows-gcc-x64" } ], "version": 4 }
// .vscode/tasks.json —— Ctrl+Shift+B 一键编译当前文件 { "version": "2.0.0", "tasks": [ { "label": "gcc build active file", "type": "shell", "command": "gcc", "args": [ "-std=c11", "-Wall", "-Wextra", "-g", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}.exe" ], "group": { "kind": "build", "isDefault": true } } ] }

compilerPath要换成自己机器上的实际路径,写错了只是智能提示失效,不影响编译。cStandard设为c11是因为部分老教材用的是 C89/C99 写法,用 C11 编译时会给隐式函数声明之类的告警,这些告警本身就是试卷想考的点。

3.2 编译参数怎么设:把告警当成免费的错题本

每道题都用同一组参数编译,不要图省事只敲gcc a.c。告警能提前暴露的正是试卷里最容易失分的那批问题。

参数作用对应卷面考点
-std=c11指定语言标准变量声明位置、//注释是否合法
-Wall -Wextra打开主要和额外告警未初始化变量、隐式类型转换
-Wconversion提示隐式数值转换有符号无符号比较、doubleint截断
-g生成调试信息配合 gdb 单步看指针变化
-fsanitize=address检测越界与内存泄漏数组越界、malloc后忘free
-lm链接数学库幂运算、sqrt相关题目
# 单题编译并立即运行,退出码非 0 说明程序自身崩了 gcc -std=c11 -Wall -Wextra -g -fsanitize=address exam_q17.c -o q17 && ./q17 echo "exit=$?"

-fsanitize=address在 GCC 和 Clang 上都可用,运行时会打印越界访问的调用栈,试卷里"程序运行结果是什么"的题如果确实存在越界,这个参数能直接给出证据。用了它之后二进制体积变大、运行变慢,但单题验证完全够用。

3.3 三道必刷题的完整可运行版本

3.3.1 判断浮点数相等:别写a == b

浮点数是二进制小数,0.1 + 0.20.3并不相等。试卷上常以"以下if是否成立"的形式考查。

#include <stdio.h> #include <math.h> int main(void) { double a = 0.1 + 0.2; int eq = fabs(a - 0.3) < 1e-9; /* 用绝对误差阈值代替 == */ printf("a=%.17f 相等=%d\n", a, eq); double b = 1e16, c = 1e16 + 1.0; printf("大数相等=%d\n", fabs(b - c) < 1e-9); /* 绝对阈值在大数上失效 */ printf("相对误差判定=%d\n", fabs(b - c) <= 1e-9 * fmax(fabs(b), fabs(c))); return 0; }

第一行输出显示a0.30000000000000004,判定为相等。第三行说明绝对阈值在数量级很大的时候不管用,因为1e16加 1 在double里根本表示不出来,这时候要用相对误差。这道题连着考了浮点表示、误差阈值和数量级三件事。

3.3.2 冒泡排序与快速排序:复杂度写在注释里
#include <stdio.h> /* 冒泡排序:稳定,平均 O(n^2),适合小规模且要求稳定的场景 */ void bubble_sort(int *a, int n) { for (int i = 0; i < n - 1; i++) { int swapped = 0; for (int j = 0; j < n - 1 - i; j++) { if (a[j] > a[j + 1]) { int t = a[j]; a[j] = a[j + 1]; a[j + 1] = t; swapped = 1; } } if (!swapped) break; /* 一趟没交换说明已有序,提前退出 */ } } /* 快速排序:平均 O(n log n),最坏 O(n^2),不稳定 */ static void qsort_range(int *a, int lo, int hi) { if (lo >= hi) return; int pivot = a[(lo + hi) / 2], i = lo, j = hi; while (i <= j) { while (a[i] < pivot) i++; while (a[j] > pivot) j--; if (i <= j) { int t = a[i]; a[i] = a[j]; a[j] = t; i++; j--; } } qsort_range(a, lo, j); qsort_range(a, i, hi); } int main(void) { int a[] = {5, 2, 9, 1, 5, 6}; int n = sizeof(a) / sizeof(a[0]); bubble_sort(a, n); for (int i = 0; i < n; i++) printf("%d ", a[i]); printf("\n"); int b[] = {5, 2, 9, 1, 5, 6}; qsort_range(b, 0, n - 1); for (int i = 0; i < n; i++) printf("%d ", b[i]); printf("\n"); return 0; }

冒泡里的swapped标志是常考的优化点,加上它以后最好情况降到 O(n)。快排的基准取中间位置而不是首元素,是为了避开"输入已经有序"时退化成 O(n^2) 的情况。两者都不需要额外数组空间,试卷上要求"原地排序"时都可以用。

3.3.3 文件读写:统计文本文件的行数与单词数
#include <stdio.h> #include <ctype.h> int main(int argc, char *argv[]) { if (argc < 2) { printf("usage: %s file\n", argv[0]); return 1; } FILE *fp = fopen(argv[1], "r"); if (fp == NULL) { perror("fopen"); return 1; } int ch, prev = ' ', lines = 0, words = 0; while ((ch = fgetc(fp)) != EOF) { /* fgetc 返回 int,才能和 EOF 比较 */ if (ch == '\n') lines++; if (isspace(ch) && !isspace(prev)) words++; prev = ch; } if (prev != '\n') lines++; /* 最后一行没有换行符也要计入 */ fclose(fp); printf("lines=%d words=%d\n", lines, words); return 0; }

fgetc的返回值必须用int接,用char接会导致 0xFF 被当成 EOF,这是文件题最隐蔽的错误。单词判定用的是"前一个字符是空白、当前字符不是"的边沿检测,比逐个读%s更可靠,也不会被空行和连续空格搞乱。

4. 把试卷当回归测试集:批量编译与结果比对

一题一题手动敲命令,做到第十题就会烦。把整个复习过程脚本化,改完代码直接跑一遍,才知道自己有没有把前面做对的题又改错——这在同时维护几十个.c文件时特别重要。

4.1 目录约定:每题一个 .c,一个 .in,一个 .out

c_exam/ ├── questions/ │ ├── q01_operator.c │ ├── q01_operator.in │ ├── q01_operator.out │ ├── q17_strfunc.c │ └── ... └── run_tests.py

文件名里的编号对应试卷题号,方便回头翻原卷。没有输入输出的纯选择题就写一个空的.in,把答案里的输出粘进.out。命名统一之后,脚本才能按前缀匹配。

4.2 用 Python 脚本批量编译并比对

#!/usr/bin/env python3 """批量编译 questions/ 下的 .c 文件,用 .in 作输入、.out 作期望输出比对""" import subprocess import sys from pathlib import Path ROOT = Path(__file__).parent QDIR = ROOT / "questions" def run_one(src: Path) -> tuple[bool, str]: exe = src.with_suffix(".exe") # 编译阶段:任何告警都按错误处理,强制自己写干净代码 build = subprocess.run( ["gcc", "-std=c11", "-Wall", "-Wextra", "-Werror", str(src), "-o", str(exe)], capture_output=True, text=True, ) if build.returncode != 0: return False, "COMPILE_FAIL\n" + build.stderr[:400] in_file = src.with_suffix(".in") stdin_data = in_file.read_text() if in_file.exists() else "" proc = subprocess.run( [str(exe)], input=stdin_data, capture_output=True, text=True, timeout=5, ) if proc.returncode != 0: return False, f"RUNTIME_FAIL rc={proc.returncode}\n{proc.stderr[:400]}" expect = src.with_suffix(".out").read_text().strip() actual = proc.stdout.strip() if actual != expect: return False, f"WRONG\n期望:\n{expect}\n实际:\n{actual}" return True, "OK" def main() -> int: failed = 0 for src in sorted(QDIR.glob("*.c")): ok, msg = run_one(src) print(f"[{'PASS' if ok else 'FAIL'}] {src.name}") if not ok: failed += 1 print(" " + msg.replace("\n", "\n ")) print(f"合计 {failed} 道未通过") return 1 if failed else 0 if __name__ == "__main__": sys.exit(main())

脚本里三个地方值得注意。-Werror把告警升级成错误,逼着自己处理掉隐式转换和未使用变量;timeout=5防止某道题写成死循环后把整个脚本挂住;比对用的是strip()后的全文相等,不忽略空格,因为试卷上的输出格式空格也算分。退出码非 0 的写法方便挂到 CI 或者写成 git 的 pre-commit 钩子。

4.3 错题归档:一张表把错误类型收敛到五类以内

跑通脚本只是第一步,真正涨分的是把 FAIL 的原因归类。我一般只分五类,多了记不住。

错误类型典型现象处理动作
语法与告警COMPILE_FAIL,未声明变量-Wall的第一条告警,别从最后一条看起
边界条件WRONG,且只差最后一行n==0n==1两个测试用例
类型与精度WRONG,浮点只差末位改用误差阈值比较
指针与内存RUNTIME_FAIL,段错误-fsanitize=address重编译定位
文件与 IOWRONG,输出为空检查fopen模式串是否写反

同一类错误连续出现三次以上,说明不是粗心,是概念没立住,得回到第 2 章对应的小节重看一遍,而不是继续刷下一题。

5. 从两套试卷到一个可检索的 C语言 题库

做完前四步,你手上已经有几十个能编译、能验证的.c文件,接下来要做的是让它们"能被检索"。期末考完半年再回头看,翻目录找"那道考函数指针的题"效率极低,给每题加两三个标签就解决了。

5.1 用标签把题目变成可检索条目

在每题文件顶部加一行结构化注释,格式固定,方便 grep 也能被脚本解析。

// tags: 指针,函数指针,返回值为指针的函数,选择 // source: 期末试卷A 第12题 // note: 函数指针 int (*p)(int) 与指针函数 int *p(int) 的读法区别 #include <stdio.h> int add(int a, int b) { return a + b; } /* 普通函数 */ int *pick(int *arr, int n) { return &arr[n - 1]; } /* 指针函数:返回 int* */ int main(void) { int (*fp)(int, int) = add; /* 函数指针:指向函数 */ printf("%d\n", fp(3, 4)); int a[4] = {1, 2, 3, 4}; printf("%d\n", *pick(a, 4)); return 0; }

读法的判断口诀是"先看标识符左边有没有*":int *pick(int)pick先和(int)结合,说明它是个函数,返回int*int (*fp)(int,int)*fp先和指针绑定,说明它是函数指针。加完 tags 之后,grep -rn "tags: .*函数指针" questions/就能把所有同类题捞出来,比翻纸质卷子快得多。

5.2 把答案从字符串变成断言

.out文件比对能判断对错,但排错时看不到中间状态。对指针、结构体这类题目,更省事的做法是把关键中间量写成assert,让程序在第一个不符合预期的地方直接停下。

#include <assert.h> #include <string.h> #include <stdio.h> int main(void) { char buf[16] = "hello"; assert(sizeof(buf) == 16); /* 数组整体大小 */ assert(strlen(buf) == 5); /* 到 '\0' 为止的长度 */ char dst[16]; strcpy(dst, buf); assert(strcmp(dst, buf) == 0); assert(dst[5] == '\0'); /* 结尾符必须存在 */ printf("all assertions passed\n"); return 0; }

assertNDEBUG宏被定义时会被整体去掉,所以它只放在验证代码里,不要放进提交给老师或者上线运行的程序。把每道易错题的关键中间量写成三五行断言,改动代码后重跑一次就能知道有没有把原来对的改坏——这比对着答案一行行看输出快得多,也是这套题库化流程里最值得长期保留的一步。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询