简介:这份文档面向准备PTA平台C语言理论考试的学生与自学者,围绕判断题、选择题及参考答案进行整理,可用于考前自测、答案核对与薄弱知识点复盘。压缩包内仅含1个docx文件,大小约345KB,内容集中、查阅方便,适合电脑端阅读或打印后批注。文档覆盖编译预处理命令、main函数、变量与存储单元、运算符优先级、输入输出格式、条件判断、for循环及复合语句等基础考点,并配有知识点详解,帮助读者理解题目背后的语法规则与常见易错点,兼顾概念理解与题目训练,减少同类题目的重复失分。目前已有1897人学习下载,可作为课堂练习、单元复习与刷题巩固的配套材料,尤其适合需要短期梳理C语言基础概念、查漏补缺的初学者使用。
1. PTA 理论考里的那张 C 语言地图
PTA 理论考部分的 C 语言题,拆开看只有五个板块:程序骨架(预处理命令、main 入口、语句分号)、表达式与运算符、输入输出格式、循环结构、分支与字符处理。判断题和选择题共用同一套知识点,只是把「能不能编译」换成「输出是什么」来问。适合两类人:语法刚过完一遍、准备第一次理论考的人;做过一轮题、错题散在各处、需要一张地图把碎片收拢起来的人。下面按板块摊开考点,每块给要点、给容易判断反的地方,最后落到一张能逐条打勾的自检表。
2. 程序骨架与表达式:预处理命令、main 入口、变量声明与运算符优先级
C 语言判断题里有一大块考的不是知识点深度,而是语法边界:代码写出来能不能编译、执行从哪开始、哪个符号该出现哪个不该出现。这类题没有推理空间,只有规则记没记住。换个角度理解会更清楚:把一个.c文件当成一条流水线,预处理器先处理#开头的行,编译器再管语句,链接器最后拼出可执行文件——所以「#include要不要分号」和「int x;要不要分号」根本不是一个层级的问题,混在一起记必然翻车。
2.1 预处理命令与程序执行入口
#include <stdio.h>属于编译预处理命令,特征是行首写#、整行独占一行、结尾不加分号。预处理器在编译之前把stdio.h的内容原样展开进来,程序才能用printf、scanf这些函数。分号是 C 语句的结束标记,预处理命令不是语句,所以多写一个分号在严格模式下会直接报错。这一条很多人是靠语感记的,一遇到判断题就含糊。
#include <stdio.h> /* 预处理命令:行首写 #,整行结束不加分号 */ int main(void) /* 唯一的入口函数,返回类型为 int */ { int fahr = 100; /* 变量先声明,后使用 */ double celsius = 5.0 * (fahr - 32) / 9.0; /* 5.0 和 9.0 保证浮点运算 */ printf("%.1f\n", celsius); /* 输出保留一位小数 */ return 0; /* 返回 0 表示正常结束 */ }第一行告诉编译器把标准输入输出库并进来;main是程序入口,返回值交给操作系统;fahr声明为int,celsius声明为double,5.0与9.0是浮点常量,保证(fahr - 32)先做浮点乘法而不是整数除法截断。如果写成5 * (fahr - 32) / 9,虽然语法合法,但整数除法会把小数部分全部丢掉,这是「语法对、结果错」的典型。
执行入口有两条铁律:一是可执行程序必须有且只有一个main函数,多个源文件拼成的程序也只能有一个;二是程序从main的第一条可执行语句开始执行,跟main写在文件的第几行毫无关系,哪怕它排在所有函数定义的最后。「从源文件第一条语句开始执行」这种说法,是把源文件的物理顺序和运行时的执行顺序混成了一件事。
2.2 变量声明顺序、存储单元与分号规则
变量的本质是内存里的一个存储单元,声明的作用是告诉编译器「这个名字对应多大的空间、按什么类型解释」。因此变量必须先声明后使用,k = 1; int k;这种先赋值再声明的写法在语法上通不过:编译器执行到第一句时,符号表里根本没有k。同理,变量名代表存储单元,对变量赋值就是往那块单元里写数据,这一点在选择题里常以「变量就是存储单元」的表述出现。
分号规则可以整理成一张对照表,考前扫一眼就能定位错误:
| 写法 | 结尾是否加分号 | 说明 |
|---|---|---|
int x | 加 | 变量声明也是语句 |
printf("hi\n") | 加 | 表达式语句 |
return 0 | 加 | 跳转语句 |
break/continue | 加 | 跳转语句 |
#include <stdio.h> | 不加 | 预处理命令,不是 C 语句 |
int main(void)后紧跟{ | 不加 | 函数定义头,不是语句 |
{ ... }复合语句 | 不加 | 右大括号本身就是结束边界 |
单独一个; | 本身就是分号 | 空语句,什么也不做 |
do { ... } while (x < 0) | 加 | do-while 的 while 后必须加分号 |
表里最容易被忽略的是复合语句和空语句两条。用一对大括号括起来的多条语句构成复合语句,语法上按「一条语句」对待,所以它的右大括号后面不再加分号;一个孤零零的分号构成空语句,不执行任何操作,但它确实是一条合法语句,if (x >= 0);后面多一个分号,整个分支体就变成了空语句,后面的语句跟 if 再无关系——这类多写一个符号的题,是血泪经验密度最高的地方。
2.3 运算符优先级与表达式合法性判定
判断一个表达式合法不合法,可以固定走三步:数操作数个数、看运算符对类型的要求、看赋值号左边是不是可修改的左值。单目运算符只需要一个操作数,++、--、!、-(取负)都属于这一类,说「单目运算符需要 2 个操作数」直接违反定义。赋值运算符的优先级低于算术运算符,所以n = 10 + 2先算加法再赋值,n得 12;如果反过来假设赋值优先级更高,表达式会变成(n = 10) + 2,n的结果完全不同,这类题本质上考的是优先级表。
| 表达式 | 是否合法 | 原因 |
|---|---|---|
5 * (fahr - 32) / 9 | 合法 | 乘号显式写出,括号改变结合顺序 |
5(fahr - 32) / 9 | 不合法 | 缺乘号,5(...)被当作函数调用写法 |
x % 2(x为double) | 不合法 | %只接受整型操作数 |
x + y = 22 | 不合法 | 赋值左边必须是可修改的左值,x + y是表达式 |
1 / n(n为int,绝对值大于 1) | 合法但恒为 0 | 整数除法向零截断 |
fahr++ | 合法 | 自增,等价于fahr = fahr + 1 |
表里最后一行要做区分:fahr++与fahr + 1在数值结果上看似相同,但前者改变fahr自身的值,后者只是算出一个临时值。作为表达式的结果使用时,后缀++是「先取值、后自增」,前缀++fahr是「先自增、后取值」,两者在赋值语句里会得到不同的结果。
还有一类合法但危险的表达式:赋值出现在条件里。if (x = 20)用的是赋值号不是比较号,表达式的值是 20,条件恒为真,同时x被改写成 20。它与if (x == 20)在语法上都合法,编译器通常只给个警告,考场上却常常拿它设陷阱。
3. 输入输出与字符处理:scanf/printf 匹配、分隔符与 ASCII 值
输入输出这一块看起来只是记格式符,真正拉开分差的是「类型是否严格匹配」。printf拿到的是值,scanf拿到的是地址,两者对同一个变量的格式符要求并不完全一样:printf输出double用%f就够,scanf读double必须写%lf。把这条弄反,代码可能编译通过、运行也能跑,结果却完全不是预期的数。
3.1 格式符与变量类型的对应表
| 数据类型 | printf格式符 | scanf格式符 | 备注 |
|---|---|---|---|
int | %d | %d | 最常用 |
float | %f | %f | 读写都用%f |
double | %f(%lf也行) | %lf | 读double必须%lf |
char | %c | %c | 读字符不跳过空白 |
| 字符数组 | %s | %s | 不带&,数组名即地址 |
double x; scanf("%lf", &x); /* double 必须用 %lf,用 %f 会按 float 的字节数写入 */ printf("%.2f\n", x); /* printf 里 %f 输出 double,.2 表示保留两位小数 */scanf需要变量的地址,所以double前面要加&;printf直接传值,不加&。如果double变量配了%f,scanf仍会按float的大小写内存,而double占的字节更多,写入的那部分和变量本身对不上,输出就成了垃圾值——这类错误不会报编译错,属于最阴的一类问题。
3.2 scanf 的普通字符与分隔符规则
格式串里除了格式符,还可以写普通字符,这些字符必须在输入中原样出现。scanf("x=%lf", &x);要求用户输入x=30这种形式,只敲30会匹配失败。格式串里的空白字符(空格、制表、换行)表示「跳过任意多个空白」,所以scanf("%d %d", &a, &b)用空格、回车分隔输入都能成功读取。
int money, year; double rate; /* %d 对 int,%d 对 int,%lf 对 double,顺序与变量一一对应 */ if (scanf("%d %d %lf", &money, &year, &rate) != 3) { printf("input error\n"); return 1; } printf("%d#%d#%.3f\n", money, year, rate);这段代码里%d %d %lf与int、int、double严格对应,scanf的返回值是成功读入的项数,等于 3 才说明三项都拿到了。如果格式串写成%d %lf %d而变量仍是int、int、double,读取时就会把浮点格式按到整型变量上,后面的项全部错位,输出自然对不上。
输出侧的分隔符是「写什么出什么」:printf("%d#%d#%.3f")里的#是普通字符,会原样输出,三个值之间就用#隔开。宽度和小数位数写在格式符里,.3表示三位小数并四舍五入,%4d表示占四个字符宽度、默认右对齐。
3.3 精度控制、putchar/getchar 与 ASCII 铁律
字符常量用一对单引号括起单个字符,'A'、'0'、'$'都是合法的字符常量;"A"是字符串,占两个字节(字符加结束符),两者不能混。字符型数据的值就是它在 ASCII 表里的次序值,所以'A'参与运算时按整数处理,'A' + 1得到'B'。
| 字符 | 十进制值 | 记忆点 |
|---|---|---|
'0' | 48 | 数字字符起点 |
'9' | 57 | 与'0'之间升序连续 |
'A' | 65 | 大写字母起点 |
'Z' | 90 | 大写字母终点 |
'a' | 97 | 小写字母起点 |
'z' | 122 | 小写字母终点 |
| 空格 | 32 | 最常用的空白字符 |
几条必须记牢的推论:数字字符'0'到'9'的值升序连续,但'1'的值是 49 而不是 1,要把字符转成数字必须写'1' - '0';大写字母'A'到'Z'、小写字母'a'到'z'各自连续,但两段之间隔着 6 个字符,所以'a'不等于'Z' + 1;'a' - 'A'恰好是 32,这条差值在做大小写转换时非常好用;由于大写在小写之前,'M'的值比'm'小,说「'M'的值比'm'大」就是把两段的位置记反了。'D' - 'A' == 'd' - 'a'成立,因为两段内部的间隔完全一致。
char ch = getchar(); /* 读入一个字符,返回类型其实是 int */ putchar(ch); /* 输出该字符本身,不带引号 */ putchar('\n'); /* 换行 */putchar('S')在屏幕上输出的是S,不是带引号的'S'——引号只是代码里标记字符常量的符号,不参与输出。getchar每次读一个字符,scanf("%c%c%c", &ch1, &ch2, &ch3)则连续读三个字符,输入三个字符时printf("%c", ch2)输出的是第二个,不是第一个。这些点单独看都很简单,一放进判断题里,就靠「引号到底算不算内容」来定对错。
4. 控制结构:for 三表达式、循环次数、if-else 配对与 switch 穿透
控制结构是理论考里分值最集中的一块。循环考的是执行时序和边界值,分支考的是配对规则和作用范围,switch考的是穿透和等价改写。这三类题的共同点是:光看代码「像是对的」远远不够,必须能在纸上把每一步的执行过程走一遍。
4.1 for 三表达式的执行时序
for (表达式1; 表达式2; 表达式3) 循环体的执行顺序固定:表达式1 在进入循环前执行一次,之后不再执行;表达式2 在每轮循环体之前判断,值为真才进入循环体,为假就结束循环;表达式3 在每轮循环体执行完之后执行。三个表达式都可以省略,但两个分号一个都不能少,for (;;)是标准写法死循环。
int i, sum = 0; for (i = 1; i <= 10; i++) { /* 1 只跑一次;2 每轮判断;3 在循环体之后 */ sum = sum + i; } /* 循环结束时 i = 11,sum = 55 */这里表达式1i = 1只执行一次;表达式2i <= 10共判断 11 次(最后一次为假);表达式3i++执行 10 次;循环体执行 10 次。循环结束后i的值是 11 而不是 10,因为让条件变成假的那次自增已经发生了。
i = 1; sum = 0; while (i <= 10) { sum = sum + i; i++; }上面这段与for版本完全等价:初始化提到循环外,条件放在while里,自增放在循环体末尾。做等价改写的选择题时,重点核对三件事——初始化是否在循环之前只做一次、条件是否在每轮之前判断、自增是否在每轮体之后执行,缺一项就不等价。
4.2 循环次数与循环结束后的变量值
int fahr; double celsius; for (fahr = 91; fahr <= 100; fahr++) celsius = 5.0 * (fahr - 32) / 9.0; /* 语句①:紧跟在 for 后面,就是循环体 */ printf("%4d%6.1f\n", fahr, celsius); /* 语句②:循环结束之后才执行一次 */这段代码里for后面没有大括号,所以只有紧挨着的那一条语句是循环体,printf已经跑到循环外面了。由此可以确定四个数:循环体执行 10 次(fahr从 91 到 100);语句① 就是循环体,执行 10 次;语句② 在循环外,执行 1 次;循环结束时fahr为 101。如果把printf也包进大括号,语句② 就变成循环体的一部分,执行次数同步变成 10 次——加大括号与不加大括号,答案差一个数量级。
%4d和%6.1f只影响输出对齐:前者占 4 个字符宽度,后者占 6 个字符宽度并保留一位小数,数值本身不受影响。这类题问的是「执行了多少次」「循环结束后变量的值是多少」,而不是输出长什么样,别被格式符带偏。
4.3 累加器初始化与整数除法陷阱
循环求和求积有三处固定雷区:累加器没有初值、累乘初值写成 0、整数除法把小数据抹平。
/* 雷区一:sum 没有初值,累加结果不可预测 */ int i, sum; for (i = 1; i <= 10; i++) { sum = sum + i; } /* 雷区二:累乘初值必须是 1,写 0 会一路乘成 0 */ double product = 0; for (i = 1; i <= 5; i++) { product = product * i; } /* 正确写法:double 累加,1.0 / i 强制浮点除法 */ int n = 4; double s = 0.0; for (i = 1; i <= n; i++) { s = s + 1.0 / i; }第一段里sum是局部变量且未初始化,初值不确定,哪怕循环体逻辑完全正确,结果也不是 55;要让它等于 55,必须写成sum = 0再进循环。第二段里product初值写 0,第一轮0 * 1还是 0,之后永远是 0,求 n 的阶乘必须初始化为 1。第三段用1.0 / i,因为1.0是浮点常量,除法按浮点进行,i = 4时得到 0.25。
反过来的错误同样常见:把结果变量声明成int。
int i, sum = 0; for (i = 1; i <= 2; i++) { sum = sum + 1.0 / i; /* 1.0 + 0.5 = 1.5,赋给 int 被截断成 1 */ }1.0 / i本身算得没错,但接收结果的sum是int,1.5 赋进去只剩 1。还有一类更隐蔽:步长不是 1 且除法用的是整数。
int i; double sum = 0; for (i = 2; i <= 4; i = i + 2) { sum = sum + 1 / i; /* 1 / 2 是整数除法,结果为 0,sum 始终是 0 */ }1 / 2两边都是整数,结果向零截断为 0,sum累加两轮还是 0。要得到 0.75 这种值,必须写成1.0 / i或(double)1 / i。判断这类题时先看两个操作数的类型,再看接收变量的类型,两关都过才算对。
4.4 if-else 配对与常见分支错误
if语句的每个分支只能跟一条语句,需要执行多条时必须用大括号组成复合语句。这条规则本身不难,难的是「哪条语句属于哪个分支」。C 语言规定,else与它前面最近的、尚未配对的if配对,跟缩进无关。
int a = 2, b = -1, c = 2; if (a < b) if (b < 0) c = 0; else /* 配给内层 if,不是外层 if */ c++; printf("%d\n", c); /* 输出 2:外层条件不成立,内层整个被跳过 */外层a < b是 2 < -1,为假,内层if和它的else整体被跳过,c保持 2。如果误以为else配给外层,会算出先执行c++得 3,这就是缩进带来的错觉。
另外两类分支错误也高频:一是if (x = 0)用了赋值号,表达式的值为 0,条件恒假,并且x被改写成 0;if (x = 20)则恒真,x被改成 20。二是分支末尾多写分号,if (x >= 0);后面那个分号让分支体变成空语句,后面的语句无条件执行。看到if或else后面紧跟分号,先把这条标出来再看别的。
4.5 switch 的执行流程与 break 穿透
switch的语法约束比if严格得多:括号里的表达式必须是整型或字符型;case后面必须是常量表达式,不能出现变量,也不能写choice == 1这种关系表达式;同一个switch里case的值不能重复;default可以有也可以没有,位置随意,只有所有case都不匹配时才执行它。break不是必须的,用不用取决于你想不想让它穿透。
int x = 1, a = 0, b = 0; switch (x) { case 0: b++; /* 不匹配,跳过 */ case 1: a++; /* 匹配,从这里开始执行,没有 break,继续往下 */ case 2: a++; b++; /* 穿透进来继续执行 */ } printf("a=%d,b=%d\n", a, b); /* a=2, b=1 */x等于 1,从case 1进入,执行a++后因为没有break而继续执行case 2的语句,所以a加了两次,b只加一次。如果没有穿透,a只会是 1。带break的版本执行流程是:求表达式值、匹配到某个case、执行该段语句、遇到break跳出整个switch;不匹配任何case就走default。不带break的版本则会从匹配点一路执行到switch结束。
等价改写的题,判断标准只有一条:所有输入取值下结果是否完全一致。default放在最前面与放在最后面等价,因为它的触发条件始终是「没匹配上任何case」;把default去掉、在switch外面先写一句price = 0.0;也可以等价,前提是每个case分支都有break。少一个break,穿透路径就变了,等价性立刻不成立。
5. 易漏点复盘:五处最容易判断反的 C 语言细节
5.1 预处理命令后面那个分号
易漏点:把#include <stdio.h>;当成「加了分号更规范」的正确写法。
为什么漏:平时写的几乎每行代码都以分号收尾,眼睛扫过去很容易把#开头的行也归进「语句」里。而预处理命令在语法层级上根本不参与编译,分号不属于它。
怎么补:看到#开头的行,先在心里把它划出 C 语句的范畴,整行读到换行结束即可。判断时问自己一句:这一行是给预处理器看的,还是给编译器看的?给预处理器看的,不加分号。
5.2 输入输出格式符与变量类型不匹配
易漏点:double变量用%f读入,或者int、double的顺序与格式串错位。
为什么漏:printf里double用%f完全没问题,于是顺手把同一套写法搬到scanf上;另一种是三个输入项写得整齐,却没核对格式符和变量声明的先后顺序。
怎么补:读入时逐项对照,scanf的%lf只配double,%f只配float,%d配int。养成看返回值的习惯,scanf返回成功项数,不等于预期项数就说明格式串和输入对不上。
5.3 没有大括号的循环体与循环结束后的变量值
易漏点:for后面没加大括号时,误以为紧跟的多条语句都是循环体;或者把循环结束后变量的值当成最后一次进入循环时的值。
为什么漏:缩进会骗人,视觉上几行代码是「一组」,语法上只有第一条属于循环体。结束值的问题则来自少算一次自增。
怎么补:看到for先找循环体边界——有大括号就是大括号内,没有就是紧挨着的那一条。算次数时用「边界值代入法」:把起始值、终止值各代一次,再用「结束时条件第一次为假的取值」确定变量的终值。
5.4 悬垂 else 与条件里的等号
易漏点:把else配给缩进更靠外的if;把if (x = 0)当成比较。
为什么漏:else的配对规则看的是「最近的、尚未配对的if」,跟缩进无关,而代码排版往往按人的直觉缩进。条件里的=和==只差一个字符,扫读时几乎看不出来。
怎么补:遇到嵌套if,从最内层往外画配对线,配完再看缩进对不对。遇到条件里出现单个=,先按赋值理解:表达式的值就是被赋的那个值,非零即真,同时变量被改写。
5.5 switch 的 case 常量、default 与 break 穿透
易漏点:认为case后面可以写变量或关系表达式;认为default必须写;认为每个case都必须有break。
为什么漏:switch的结构看起来像一串并列分支,容易套用else if的写法,于是写出case choice == 1:这种表达式;而「必须有 default」「必须有 break」是从别的语言或习惯里
本文还有配套的精品资源,点击获取