简介:这份资源收录广东药学院《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 (1 | f())` | f()被调用 | |
sizeof('a') | 1 | 在 C 中为 4(int 宽度) | 字符常量类型是 int,不是 char |
while和do-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;出现"可能一次都不执行""条件不满足就跳过",答案指向while或for。这道题的价值不在于背结论,而在于它直接对应试卷里读程序写结果的第一小问。
2.2 指针、数组与字符串函数:sizeof 和 strlen 必须分清楚
指针题在试卷里通常以三种形态出现:判断sizeof与strlen的结果、手写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 字节的二进制内存里找模式串,得自己按长度写循环,不能用它。
strcpy和strncpy的差别同样值得单独记一行:前者不做长度检查,目标缓冲区不够就写越界;后者限定拷贝字节数,但源串长度达到上限时不会补'\0',用完必须手动补。试卷里"下列代码有什么问题"这类题,八成都出在这两个函数上。
2.3 结构体、文件读写与内存管理:综合题的得分权重
综合编程题一般给 15 到 25 分,分值集中在结构体数组的排序、文件的读写和简单的动态内存分配上。判断标准很直接:能不能把定义、输入、处理、输出四段各自写对,并保证边界情况(空文件、只有一行、字段数不足)不会崩。
| 题型 | 常见分值 | 失分主因 |
|---|---|---|
| 结构体数组排序 | 15 | 交换时只换部分字段,或比较函数写反 |
| 文本文件读写 | 15 | fscanf读字符串遇空格截断,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之后,布局就会变。试卷上的文件题更常见的是fprintf和fscanf的文本模式,区别在于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 | 提示隐式数值转换 | 有符号无符号比较、double转int截断 |
-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.2和0.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; }第一行输出显示a是0.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==0、n==1两个测试用例 |
| 类型与精度 | WRONG,浮点只差末位 | 改用误差阈值比较 |
| 指针与内存 | RUNTIME_FAIL,段错误 | 用-fsanitize=address重编译定位 |
| 文件与 IO | WRONG,输出为空 | 检查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; }assert在NDEBUG宏被定义时会被整体去掉,所以它只放在验证代码里,不要放进提交给老师或者上线运行的程序。把每道易错题的关键中间量写成三五行断言,改动代码后重跑一次就能知道有没有把原来对的改坏——这比对着答案一行行看输出快得多,也是这套题库化流程里最值得长期保留的一步。
本文还有配套的精品资源,点击获取