简介:围绕C语言PTA理论考整理的Word练习文档,主要面向正在准备PTA平台C语言理论测评的学生,以及需要自查基础语法掌握情况的自学者,用于解决判断题、选择题易混易错、缺少标准答案对照的问题。文档按题组编号收录理论试题,每题给出参考答案,并在末尾附上分条知识点详解,覆盖编译预处理命令、main函数与程序执行起点、语句分号、变量与存储单元、单目运算符、赋值与算术运算符优先级、if条件中的赋值误用、scanf与printf的格式匹配及精度控制、for循环三个表达式的执行时机、复合语句与空语句、累加与阶乘计算、整数除法与浮点累加等高频考点,可逐题核对并回溯原理。压缩包内共1个docx文件,约345KB,纯文本排版,便于打印或检索关键字复习。目前已有1897人学习下载,适合作为考前突击与错题归因的对照材料。
1. 一份 WORD 题库真正省下的,是反复试错的成本
带过几届 C 语言课的人都碰过同一个场景:学生把 PTA 理论考的判断题截图存进手机,考前翻两遍,考场上题目换个问法立刻翻车。原因不复杂——题库只给 T 和 F,不给「为什么是 F」。这份《PTA 理论考部分.docx》的价值不在那一列答案,而在于它按 HW02、HW03、HW04 三个模块把题攒在了一起,题干覆盖编译预处理、数据类型、运算符、输入输出、循环、分支、字符与 switch,本身就是一份能逐条拆解的语法清单。准备理论考的学生能用,想把 C 语言边角语法重新过一遍的开发者也能用。下面按归类、验证、排查的顺序,把它拆成一套能照着跑的流程。
2. 把判断题拆成三类语法清单:先归类比背答案快
判断题的错误率从来不是均匀分布的。把这份文档里近六十道判断题摊开看,翻车点密集出现在三个区域:预处理与程序结构、运算符与表达式、输入输出格式。按这三类归完,你会发现要记的不是六十条结论,而是十来个判据。归类的意义在于,考场上遇到没见过的题,你能顺着判据推,而不是靠记忆碰运气。
归类还有第二个好处:同一类题的错因往往共享同一个编译期检查。比如「格式符与变量类型不匹配」这一类,gcc 加上-Wall之后会直接给警告,你根本不用背。所以这一章先做分类,下一章再用编译器把分类结果坐实。
2.1 预处理与程序结构:答案藏在编译模型里
第一道容易错的题是#include <stdio.h>; 是编译预处理命令。很多人第一反应是「对啊,它就是预处理指令」,然后打 T。问题出在末尾那个分号上。预处理指令以换行结束,不以分号结束;写成#include <stdio.h>;之后,分号会被当成一条独立的空语句留在源文件里,多数编译器会直接报错或者给出警告。这道题的判据是:预处理指令的行尾不能有分号。答案是 F。
第二道是关于 main 函数的:任何一个程序都必须有而且只能有一个 main() 函数。教材语境下这题打 T,因为课程范围内讨论的都是可执行的控制台程序。但如果你在工程里待过,就知道库文件、启动代码、某些嵌入式入口根本没有 main。这里要区分「考试语境」和「标准语境」——题库问的是课程模型,不是 C 标准的完整定义。类似的还有C 语言程序是从源文件的第一条语句开始执行的,答案是 F,执行入口是 main,不是文件第一行;C 语言中的所有语句都必须以分号结束,答案也是 F,复合语句、控制结构的头部、预处理指令都不吃分号。
再往下是复合语句和空语句这两道:用一对大括号{}括起来的多条语句称为复合语句,复合语句在语法上被认为是一条语句,T;仅由一个分号构成的语句称为空语句,它什么也不做,T。这两条看着平淡,但它们是后面 for 循环题的基础——循环体要不要加大括号,取决于你把循环体当成几条语句看,而这个判断依据就是复合语句的定义。
把这一组题串起来,判据其实只有三条:预处理指令不以分号结尾、程序入口是 main、语句的边界由语法结构决定而不是由行数决定。记住这三条,这一组题基本不会错。
2.2 运算符与表达式:优先级和类型是两个独立维度
这一类题的迷惑性最强,因为题干往往写得很像那么回事。
k = 1; int k;这道,问这段代码符不符合 C 语言语法,答案是 F。原因不是「C 语言不允许在中间声明变量」——C99 之后块内任意位置都能声明——而是声明必须早于第一次使用。这里 k 在声明之前就被赋值了,编译器会直接报'k' undeclared。这道题的设计意图就是让人把「C99 放宽了声明位置」和「可以先使用后声明」混为一谈。
若变量定义为 double x;,则 x % 2 是符合 C 语言语法的表达式,答案是 F。%的操作数必须是整型,浮点取模要走fmod()。若变量定义为 int fahr;,则 5(fahr-32)/9 是符合 C 语言语法的表达式,答案同样是 F,因为5(之间缺了乘号,编译器会把它当成函数调用去解析,然后报「被调用的对象不是函数」。
若变量定义为 int n;,当 n 的绝对值大于 1 时,则表达式 1/n 的值恒为 0,这道是 T。整数除法向零截断,1/2、1/(-3)的结果都是 0。这条判据在后面循环累加的题里反复出现,是最高频的失分点之一。x + y = 22是 F,赋值号左边必须是左值,x + y是一个右值表达式。
最有意思的是那道假设题:假设赋值运算符的优先级比算术运算符高,执行 n = 10 + 2; 后 n 的值为 10。答案是 T。如果赋值先结合,n = 10先执行,剩下的+ 2变成一个孤立的表达式语句,n 停在 10。这道题考的不是真实优先级,而是「你能不能按假设的规则重新推一遍」。同类的还有fahr++ 与 fahr + 1 等价,答案是 F——前者会改写 fahr,后者不会,副作用是这两个表达式的本质区别。
这一组归下来,判据是四条:声明早于使用、%只吃整型、整数除法截断、赋值左侧必须是左值。至于优先级,真实规则记不住也没关系,编译器会替你检查。
2.3 输入输出格式:scanf 与 printf 必须成对核对
这一组题的错法高度雷同,几乎都是格式符和变量类型对不上。
double x; scanf("%d", &x);然后输入 10 输出 10.00,答案是 F。%d要求的是int*,传进去一个double*属于未定义行为,实际跑出来的值不可预测,可能是个垃圾数,也可能碰巧对。紧接着那道scanf("%f", &x)配 double,答案也是 F——float用%f,double在 scanf 里必须用%lf。注意 printf 侧不一样,printf 的%f本来就接收 double(float 会被提升),这个不对称是最容易记混的地方。
scanf("x=%lf", &x);输入 30,答案是 F。格式串里有字面量x=,输入就必须老老实实敲x=30,只敲 30 匹配不上。这类「格式串含普通字符」的题在同一份文档里出现了好几次,判据是:scanf 格式串里的非格式字符必须原样出现在输入流里。
最值得逐字看的是这道:scanf("%d %lf %d ", &money, &year, &rate);输入1001 3 0.025,输出1001#3#0.025,答案 F。错的地方有两处:一是year是 int 却配了%lf,rate是 double 却配了%d;二是格式串末尾那个空格,scanf 遇到空白字符会持续吞掉输入流里的空白直到读到一个非空白字符,程序会卡在那儿等下一次输入。这条末尾空格我当年是真踩过,本地跑没事、提交到判题系统直接超时,找了半天才定位到格式串结尾。
字符那几道相对简单:putchar('S')的输出是S而不是'S',引号只是源码里的界定符,不进输出流;printf用%c能输出字符,scanf用%c能读字符,所以「不能用 printf 输出字符」「不能用 scanf 输入字符」这两道都是 F。
这一组的判据收成三条:scanf 里 double 必须用%lf、格式串里的普通字符要原样输入、格式串末尾别留空格。三条都是可以当场验证的,下一章就动手。
3. 用 gcc 把题干变成可执行用例:判断题也能跑出证据
判断题最大的问题是它不给你反馈。你打了 F,对了是运气,错了也不知道错在哪。解决办法很土但有效:把每一道你觉得模糊的题改写成一段十行的 C 程序,编译一遍,让编译器告诉你答案。这一章讲环境怎么配、对照实验怎么写、选择题怎么逐项证伪。
3.1 最小验证环境:-std 与 -Wall 怎么配
不要用 IDE 的默认配置,默认配置会把警告藏起来,而这份题库里至少三分之一的题,答案就藏在警告里。一条命令足够:
# -std=c99 固定语言标准,避免不同编译器默认值不一致 # -Wall -Wextra 打开常用警告,赋值当条件、类型不匹配都会提示 # -pedantic 拒绝非标准扩展,防止「本机能过、判题机不过」 # -O0 关掉优化,保证调试信息和源码行号对得上 gcc -std=c99 -Wall -Wextra -pedantic -O0 case.c -o case参数说明:-std=c99是关键,题库里的声明位置题、for 循环初始化声明题都和标准版本强相关,用-std=c89和-std=c99编译同一段代码可能得到两种结论;-Wall覆盖了-Wparentheses(if (x = 20)会报警)、-Wformat(格式符与实参类型不符会报警),这两个正好对应题库里最容易错的两类;-pedantic用来挡掉 GCC 的私有扩展,判题系统大多用标准模式编译,本地放宽了没意义。
跑完之后别只看有没有error,逐条读warning。题库里那些「语法上能过、语义上是错的」题,比如赋值写进条件、scanf类型不匹配,全都是 warning 级别,编译器不会拦你,但会告诉你。
3.2 用两个标准对照「先使用后声明」
拿 2.2 节里那道k = 1; int k;做实验,代码这样写:
/* case_1_10.c —— 检验「先赋值后声明」到底能不能过 */ #include <stdio.h> int main(void) { k = 1; /* 使用点:此刻 k 尚未声明 */ int k; /* 声明点:C99 允许块内任意位置声明,但必须早于首次使用 */ printf("%d\n", k); return 0; }先用 C89 编一遍:
gcc -std=c89 -pedantic -O0 case_1_10.c -o case_1_10 # 预期:error: 'k' undeclared (first use in this function)再用 C99 编一遍,结果一样报错,因为问题不在标准版本,在于使用早于声明。这时候结论就明确了:这道题的 F 不是因为「C89 不让中间声明」,而是因为声明顺序。把代码改成下面这样就通了:
/* case_1_10_fixed.c —— 把声明提到使用之前 */ #include <stdio.h> int main(void) { int k; /* 先声明 */ k = 1; /* 再使用 */ printf("%d\n", k); return 0; }逻辑说明:k的声明会给它在栈上分配存储单元,在此之前对k的任何写入都没有合法目标,编译器只能报错。参数说明:这份对照实验里唯一变动的是两条语句的顺序,-std反复切换是为了排除「标准版本导致」这个干扰假设。做这类题时我一般会顺手把正确写法也跑一遍,确认自己的修法真的能过编译,而不是想当然。
同样的手法可以套到if (x = 20)那道题上:
/* case_hw031_4.c —— 赋值当条件,看 x 到底变不变 */ #include <stdio.h> int main(void) { int x = 0, y = 0; if (x = 20) { /* 少写一个 =,gcc -Wall 会报 -Wparentheses */ y = 1; } else { y = 0; } printf("x=%d y=%d\n", x, y); /* 实际输出 x=20 y=1 */ return 0; }逻辑说明:x = 20这个表达式的值是赋值后的左值 20,非零即真,所以走 if 分支,同时 x 被改成 20。原题说「x 的值不变」,跑一遍就知道是 F。参数说明:编译时务必带上-Wall,否则这条警告不会出现;如果想让这类写法直接变成错误,加-Werror=parentheses。血泪经验是,这个坑在真实项目里比在考卷上贵得多,一个=和==的差别能让条件判断永远为真,而且不加警告选项时它一声不吭。
3.3 选择题逐项证伪的固定流程
选择题不能只挑「看起来对的」,要把四个选项都编一遍,看哪个过不了编译或者跑不出预期结果。以 2-9 那道 switch 穿透题为例,题干是:
int x = 1, a = 0, b = 0; switch (x) { case 0: b++; case 1: a++; case 2: a++; b++; } printf("a=%d,b=%d\n", a, b);四个选项是a=2,b=1、a=1,b=1、a=1,b=0、a=2,b=2。直接跑:
/* case_2_9.c —— switch 没有 break 时的穿透路径 */ #include <stdio.h> int main(void) { int x = 1, a = 0, b = 0; switch (x) { case 0: b++; /* x=1,不匹配,整段跳过 */ case 1: a++; /* 命中入口,a 变成 1;没有 break,继续下坠 */ case 2: a++; b++; /* 穿透执行:a 变成 2,b 变成 1 */ } printf("a=%d,b=%d\n", a, b); return 0; }逻辑说明:switch 只决定「从哪个 case 进入」,不决定「到哪里结束」。进入之后一路往下执行,直到遇到 break 或者语句块结束。参数说明:把x分别改成 0、1、2、3 各跑一次,能画出四条完整路径,其中 x=3 时四个 case 全不命中,a 和 b 都停在 0——这个用例正好对应「switch 不写 default 时会静默跳过」这条判据。
固定流程建议这样走:先给每个选项标出它的输出,再逐个编译运行,对不上的直接划掉;剩下两个都跑得通的,回头看题干有没有隐含条件(比如整数除法、变量未初始化)。这套流程慢,但一次能把一整类题吃透,比对着答案抄十遍管用。
4. 循环与分支的参数化:那道 fahr 题为什么有人算成 9 次
这份题库里分值最高、争议也最多的是一道 for 循环执行次数的填空题,题干大致是:
int fahr; double celsius; for (fahr = 91; fahr <= 100; fahr++) celsius = 5.0 * (fahr - 32) / 9.0; /* 语句① */ printf("%4d%6.1f\n", fahr, celsius); /* 语句② */四个空分别是:语句①执行多少次、语句②执行多少次、循环体共执行多少次、循环结束时 fahr 的值。不少人会填 10、1、10、101,也有人填 10、10、10、101。差别就在 printf 到底在不在循环里。题干排版里 printf 缩进了一格,看着像在循环体内,但它前面没有大括号,for 的循环体只有紧跟的那一条语句,printf 在循环外。这个坑值得单独拿出来讲,因为它是「复合语句」和「缩进不参与语法」两条判据的直接应用。
4.1 for 循环的四个空:初值、条件、步长、退出值
把题干改写成可运行版本,顺手埋一个计数器:
/* case_4_2.c —— 把四个空一次算清,避免靠眼睛数 */ #include <stdio.h> int main(void) { int fahr; double celsius = 0.0; int body = 0; /* 手动计数器,统计循环体真正执行了几次 */ for (fahr = 91; fahr <= 100; fahr++) { celsius = 5.0 * (fahr - 32) / 9.0; body++; } printf("body=%d fahr_after=%d celsius=%.1f\n", body, fahr, celsius); return 0; }逻辑说明:循环变量fahr从 91 起,每次加 1,条件是不超过 100,所以取值序列是 91 到 100,共 10 个值,循环体执行 10 次。循环退出发生在fahr自增到 101、条件判断为假的那一刻,所以退出后fahr停留的值是 101,不是 100。参数说明:celsius用了5.0而不是5,是为了把整个表达式提升成 double 运算,否则5/9先按整数算会得到 0,这是同一份题库里另一道题的错误来源。
接着把 printf 挪进循环体,看看第二种答案是怎么来的:
/* case_4_2_variant.c —— printf 写进循环体后的执行次数变化 */ #include <stdio.h> int main(void) { int fahr; double celsius = 0.0; int print_count = 0; for (fahr = 91; fahr <= 100; fahr++) { celsius = 5.0 * (fahr - 32) / 9.0; printf("%4d%6.1f\n", fahr, celsius); print_count++; } printf("print_count=%d fahr_after=%d\n", print_count, fahr); return 0; }逻辑说明:加了这对大括号之后,printf成了复合语句的一条,跟着循环体一起跑 10 次,print_count输出 10。原题里没有这对大括号,所以答案应该是语句①10 次、语句②1 次、循环体 10 次、结束时 fahr=101。参数说明:%4d是宽度 4 的整型、%6.1f是宽度 6 保留一位小数,这两个格式符只影响对齐,不影响执行次数的计算,别被它们带偏。
顺带说一道相关题:for (i = 1; i <= 10; i++) { sum = sum + i; } 执行后 sum 的值是 55,答案 F。不是因为加法算错,而是sum从来没有初始化,它里面是栈上的残留值,累加十次的结果不确定。这道题和上一道是一对:一个考执行次数,一个考变量初值,都属「跑一遍就知道」的类型。
4.2 if-else 的测试用例数:等价类怎么划
题库里连出好几道「为了检查分支是否正确,至少需要设计几组测试用例」的题,很多人是靠数分支硬猜。正确的做法是划等价类:每一个能让程序走出不同路径的输入区间算一类,边界值单独考虑。
看 1-9 那道三分支题:
if (x < 0) { y = 0; } else if (x <= 15) { y = 4 * x / 3; } else { y = 2.5 * x - 10.5; }三个分支对应三个区间:小于 0、介于 0 和 15 之间(含 0 和 15)、大于 15。题干说「至少五组:小于 0 的数、0、大于 0 且小于 15 的数、15、大于 15 的数」,这个划分把两个边界值 0 和 15 单独拎了出来,因为边界是最容易写错<=和<的地方。判据是:分支数不等于用例数,边界值必须单独成组。
再看 1-26 那道字符分类题,三个分支分别是字母、数字、其他,题干的结论是至少 63 组。63 = 52 个大小写字母 + 10 个数字字符 + 1 类其他字符,这个划法是把「等价类」细化到了字符集层面。这里有个容易忽略的点:ch >= 'a' && ch <= 'z'和ch >= 'A' && ch <= 'Z'是两个不连续的区间,中间隔着 6 个 ASCII 字符,如果你按「字母就是连续的」去想,就会漏掉边界。
2-1 那道运算符分支题争议最大,四个分支是+、-、*、/,/里再判value2 != 0,最后还有 else 兜底。按输出路径数,最少是 6 组:加、减、乘、除且除数非零、除且除数为零、未知运算符。教材给的判据通常是 7 组,多出来那一组是把除法分支里value2的正负当成两个等价类——因为value1 / value2的结果符号依赖除数的符号,只测正数发现不了符号错。这类题的稳妥做法是先数输出路径,再看有没有隐藏的符号或边界维度,两边对不上就把维度补上。
4.3 switch 的穿透与等价改写
switch 是这份题库里出题密度最高的语法点,围绕它至少有十道判断题加三道选择题。核心判据只有三条:case 后面必须是常量表达式、case 的值不能重复、没有 break 就往下穿透。
case choice == 1:这道是典型的 F。choice == 1是一个关系表达式,值在运行期才知道,而 case 标签要求编译期常量。真正合法的写法是case 1:。每一个 case 常量表达式的值可以相同也是 F,重复的 case 值编译器会直接报 duplicate case value。在 switch 语句中必须使用 default是 F,default 是可选的,没有匹配项且没有 default 时整个 switch 什么也不做。可以根据需要使用或不使用 break是 T,取决于你要不要穿透。
等价改写那几道选择题,本质是问「换一种写法,行为变没变」。判断方法很简单:把两个版本都编译出来,用同一组输入跑,比对输出。以 2-2 为例,四个 switch 版本的差别只在于 case 的排列顺序和 default 的位置。case 的书写顺序不影响匹配结果,只影响源码可读性;default 放在最前面也不会改变「无匹配时才执行」的语义,因为 switch 是先算表达式再跳转,不是从上往下顺序判断。唯一会改变行为的是把某个分支的 break 删掉,那就会穿透到下一个分支。
我把这类题的验证脚本固定成一个模板:
/* case_switch_cmp.c —— 同一个输入分别喂给两种写法,比对输出 */ #include <stdio.h> static double price_a(int choice) { double price = 0.0; switch (choice) { case 1: price = 3.0; break; case 2: price = 2.5; break; case 3: price = 4.0; break; case 4: price = 3.5; break; default: price = 0.0; break; } return price; } static double price_b(int choice) { double price = 0.0; switch (choice) { default: price = 0.0; break; /* default 提前,语义不变 */ case 4: price = 3.5; break; case 3: price = 4.0; break; case 2: price = 2.5; break; case 1: price = 3.0; break; } return price; } int main(void) { for (int c = 0; c <= 5; c++) { printf("choice=%d a=%.1f b=%.1f %s\n", c, price_a(c), price_b(c), price_a(c) == price_b(c) ? "SAME" : "DIFF"); } return 0; }逻辑说明:把两个版本封成函数,用 0 到 5 六组输入覆盖所有 case 加一个无匹配项,逐组比对返回值。参数说明:choice取 0 和 5 是为了覆盖 default 分支,取 1 到 4 覆盖四个 case;比较时用==判断 double,这里所有值都是 .0 或 .5,二进制能精确表示,换成 0.1 这种就得改成差值比较。这类脚本跑一次就能把整道选择题的四个选项判完,比在纸上推快得多。
5. 避坑与排查:五条从错题本里捞出来的记录
题库刷到第三遍的时候,错题会收敛到几个固定形状。下面这五条是我自己整理错题本时反复出现的,每条都按「现象、原因、解决」写清楚,遇到同款直接对号入座。
5.1 语法判断里的三条高频翻车
第一条:判断题打 T,编译报错。现象是觉得自己理解了概念,一编译就过不去,比如#include <stdio.h>;那道。原因是把「预处理指令」和「普通语句」混成一类,习惯性地给每行补分号。解决方式很机械:凡是#开头的行,行尾不许有分号,编译一次确认。这条判据在写头文件保护、宏定义时同样适用,宏定义末尾加分号会在展开处制造一堆莫名其妙的语法错。
第二条:变量先用后声明,误以为 C99 允许。现象是k = 1; int k;这种写法被判成 T。原因是把「C99 允许块内任意位置声明」记成了「可以先使用后声明」,这两件事方向相反:前者放宽的是声明的位置,后者要求的是声明的时机。解决办法是把声明全部提到函数开头,或者在写代码时养成「先声明、再赋值、后使用」的固定顺序。判据一句话:声明必须早于第一次使用,跟标准版本无关。
第三条:把缩进当语法,循环边界判错。现象是 fahr 那道题把 printf 算进循环体,得出「语句②执行 10 次」。原因是读题干时依赖视觉缩进,忽略了 for 后面没有大括号这个事实。C 语言里缩进只是给人看的,语法边界由分号和大括号决定。解决办法是读题时先给每条语句标出它属于哪个结构,或者干脆把题干敲进编辑器让 IDE 自动缩进——格式化之后谁的循环体是谁的一目了然。
5.2 输入输出与数值计算的两条隐蔽坑
第四条:scanf 格式串末尾多一个空格,程序卡住不动。现象是本地跑着没问题,一提交就超时,或者交互式运行时光标一直闪、不往下走。原因是 scanf 格式串里的空白字符(空格、制表、换行)表示「匹配任意数量的空白」,放在末尾会让函数持续读取输入流直到遇见一个非空白字符,输入流没结束时它就一直等。解决办法是把格式串末尾的空格删掉,需要跳过空白就在读取前显式处理。判据是:scanf 的格式串除了必要的分隔,不要留尾部空白。
第五条:整数除法吃掉小数,累加结果恒为 0。现象是sum = sum + 1/i跑出来是 0,或者阶乘题里product = 0导致结果永远是 0。前者的原因是1和i都是 int,1/i在 |i|>1 时按整数除法截断成 0;后者的原因是累乘的初始值写成了 0,0 乘任何数都是 0。解决办法是把分子写成1.0强制提升为浮点运算,累乘的初值写 1。这类错误编译器不会报,-Wall也未必提示,只能靠跑一遍看结果。我现在的习惯是,凡是出现除法或累乘,先手算前两项,跟程序输出对一遍再往下写。
5.3 教材答案和编译器结论打架时怎么处理
偶尔会碰到一种情况:按编译器跑出来的结论跟题库答案对不上。多数时候不是答案错了,而是题干隐含了课程语境的约定,比如「程序必须有 main 函数」在课程模型里成立、在库文件场景里不成立;再比如int的宽度、char是否有符号,这些在标准里是实现定义,题库按常见实现给答案。
处理顺序建议这样:先确认题干有没有「假定」「假设」「在课程模型中」这类限定词;再把代码在两个标准(C89 / C99)下各编一遍,看结论是否随标准变化;最后才怀疑答案。如果确认是答案与标准冲突,按题库答案记,同时在旁边标注冲突点,考试时按出题方的语境答。这种题不多,但一旦遇上,光靠背答案会反复错。
6. 把 docx 变成错题索引:一套能重复用的自测节奏
文档本身是给人读的,但刷题到后期,你需要的是「按知识点检索」而不是「从头翻到尾」。这一章讲怎么把 docx 抽成结构化索引,以及怎么用它安排自测节奏。
6.1 抽取题干,建立可检索的编号表
文档里的题号形态不统一,有1-3、HW021-3、2-9、4-2几种,正好可以用正则统一识别。装好依赖后跑一段脚本:
pip install python-docx"""extract_pta.py —— 把 docx 里的题干抽成可检索的 TSV""" import re from docx import Document DOC = "PTA理论考部分.docx" # 题号形态:1-3 / HW021-3 / 2-9 / 4-2 NUM = re.compile(r"^(HW\d{3}-)?\d+-\d+$") doc = Document(DOC) rows, buf = [], [] for p in doc.paragraphs: text = p.text.strip() if not text: continue if NUM.match(text): # 命中题号,先把上一条落盘 if buf: rows.append(buf) buf = [text] elif buf: buf.append(text) # 其余段落都算题干 if buf: rows.append(buf) with open("pta_index.tsv", "w", encoding="utf-8") as f: for r in rows: stem = " ".join(r[1:]).replace("\t", " ") f.write(f"{r[0]}\t{stem}\n") print(f"共提取 {len(rows)} 条")逻辑说明:NUM负责识别题号行,buf累积当前题的题干,遇到下一个题号就落盘。之所以按段落而不是按表格抽,是因为这份文档的题目是普通段落排版,表格里只有零星的选项。参数说明:p.text.strip()去掉段首尾空白,避免题号行因为多一个空格匹配不上;replace("\t", " ")保证 TSV 不会被题干里的制表符撑破列。跑完之后用grep按关键词检索就行:
# 按知识点检索,比如所有涉及 scanf 的题 grep -n "scanf" pta_index.tsv # 只看 switch 相关,带行号方便回原文定位 grep -n "switch" pta_index.tsv这一步做完,文档就从「一叠纸」变成了「一张表」。同一个知识点分散在 HW02 和 HW04 里的题,现在能一次全部拉出来对照。
6.2 三轮自测的间隔与判据
索引建好之后,刷题节奏比刷题数量重要。我一般分三轮,每轮之间隔开,判据各不相同。
| 轮次 | 间隔 | 做法 | 过关判据 |
|---|---|---|---|
| 第一轮 | 当天 | 按题号顺序过一遍,错题标红 | 知道每道题考哪个判据 |
| 第二轮 | 隔 2 天 | 只做标红题,写出错误原因 | 能不看答案说出为什么错 |
| 第三轮 | 隔 1 周 | 随机抽题,当场敲代码验证 | 编译通过且输出符合预期 |
第一轮的目标不是做对,是建立「题号—判据」的映射,错了不要紧,标出来就行。第二轮只碰错题,重点写错误原因,写不出来说明还是没懂。第三轮改成随机抽,抽到哪道就把题干敲进编辑器跑一遍,跑通了才算过。三轮下来还错的题,说明背后有个你没搞清的概念,得回去补,而不是继续刷。
从那次把scanf末尾空格写成习惯、本地跑得好好的提交上去直接超时之后,我每次拿到一份题库都先跑一遍编译器再抄答案,哪怕是判断题也敲一遍。判据记在脑子里容易糊,编译器的输出不会骗人。希望帮到你。
本文还有配套的精品资源,点击获取