
简介这份资源收录广东药学院《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 i1; ii;2多数编译器得 1此为未定义行为同一对象在无序列点间隔下被修改两次int a[3]{0}; int i0; a[i]i;结果确定结果不确定属未定义行为赋值两侧都读写 iint i5; while(i--);循环 4 次循环体执行 5 次先取 i 原值判断再自减if (1f())f()被调用sizeof(a)1在 C 中为 4int 宽度字符常量类型是 int不是 charwhile和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输出sizeof16 strlen5这是卷面送分点也是很多人工作后写缓冲区代码时踩的坑。第二个printf输出NULL因为src的第 3 个字节就是\0strstr在遇到它时停止搜索cd位于结束符之后永远找不到。也就是说strstr只能处理以\0结尾的文本串想在一段可能含 0 字节的二进制内存里找模式串得自己按长度写循环不能用它。strcpy和strncpy的差别同样值得单独记一行前者不做长度检查目标缓冲区不够就写越界后者限定拷贝字节数但源串长度达到上限时不会补\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之后布局就会变。试卷上的文件题更常见的是fprintf和fscanf的文本模式区别在于fscanf用%s读字符串时遇到空格就停姓名里带空格的数据会被读成两段。读懂这一点程序填空里那几个空基本就能填对。3. 用 VSCode GCC 把试卷题目跑成可验证的代码光在纸上推演遇到输出是什么的题永远有争议。把每题写成.c文件跑一遍是最省事的验证方式。Windows 上用 VSCode 加 MinGW-w64Linux 和 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 —— CtrlShiftB 一键编译当前文件 { version: 2.0.0, tasks: [ { label: gcc build active file, type: shell, command: gcc, args: [ -stdc11, -Wall, -Wextra, -g, ${file}, -o, ${fileDirname}/${fileBasenameNoExtension}.exe ], group: { kind: build, isDefault: true } } ] }compilerPath要换成自己机器上的实际路径写错了只是智能提示失效不影响编译。cStandard设为c11是因为部分老教材用的是 C89/C99 写法用 C11 编译时会给隐式函数声明之类的告警这些告警本身就是试卷想考的点。3.2 编译参数怎么设把告警当成免费的错题本每道题都用同一组参数编译不要图省事只敲gcc a.c。告警能提前暴露的正是试卷里最容易失分的那批问题。参数作用对应卷面考点-stdc11指定语言标准变量声明位置、//注释是否合法-Wall -Wextra打开主要和额外告警未初始化变量、隐式类型转换-Wconversion提示隐式数值转换有符号无符号比较、double转int截断-g生成调试信息配合 gdb 单步看指针变化-fsanitizeaddress检测越界与内存泄漏数组越界、malloc后忘free-lm链接数学库幂运算、sqrt相关题目# 单题编译并立即运行退出码非 0 说明程序自身崩了 gcc -stdc11 -Wall -Wextra -g -fsanitizeaddress exam_q17.c -o q17 ./q17 echo exit$?-fsanitizeaddress在 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一个 .outc_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, -stdc11, -Wall, -Wextra, -Werror, str(src), -o, str(exe)], capture_outputTrue, textTrue, ) 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)], inputstdin_data, capture_outputTrue, textTrue, timeout5, ) if proc.returncode ! 0: return False, fRUNTIME_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, fWRONG\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把告警升级成错误逼着自己处理掉隐式转换和未使用变量timeout5防止某道题写成死循环后把整个脚本挂住比对用的是strip()后的全文相等不忽略空格因为试卷上的输出格式空格也算分。退出码非 0 的写法方便挂到 CI 或者写成 git 的 pre-commit 钩子。4.3 错题归档一张表把错误类型收敛到五类以内跑通脚本只是第一步真正涨分的是把 FAIL 的原因归类。我一般只分五类多了记不住。错误类型典型现象处理动作语法与告警COMPILE_FAIL未声明变量查-Wall的第一条告警别从最后一条看起边界条件WRONG且只差最后一行补n0、n1两个测试用例类型与精度WRONG浮点只差末位改用误差阈值比较指针与内存RUNTIME_FAIL段错误用-fsanitizeaddress重编译定位文件与 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; }assert在NDEBUG宏被定义时会被整体去掉所以它只放在验证代码里不要放进提交给老师或者上线运行的程序。把每道易错题的关键中间量写成三五行断言改动代码后重跑一次就能知道有没有把原来对的改坏——这比对着答案一行行看输出快得多也是这套题库化流程里最值得长期保留的一步。本文还有配套的精品资源点击获取