简介:面向高校学生与备考C语言期末考试的学习者,这份 PDF 题库汇集 305 道题目,围绕函数与 main 入口、标识符合法性、基本数据类型、算术与条件运算符、顺序/选择/循环结构、数组及类型转换等高频考点组织,适合章节复习、考前自测与错题查漏。包内为 1 个 PDF 文件,约 1.56 MB,按选择题、填空题、判断题、读程序写结果和压轴编码等题型编排,每道题附有解析,便于理解命题角度与易错点。已有 428 人学习,说明其题量与解析形式对期末备考具有参考价值。读者可借助详解题库反复练习,掌握语法细节、程序执行流程和读程序推结果的方法,并通过编码大题训练综合运用能力,形成较完整的复习闭环。
1. 305 道题全部做完后我才意识到:C语言考试拉开差距的不是语法,而是对默认规则的预判
把这份 305 道题的题库从头到尾过一遍,会发现一个反直觉的现象:错误选项往往看起来更“合理”。11/3你会以为是 3.67,b=(float)(1/a)你会以为是 0.5,char a[]="student"你会数成 7 个字节。这些题真正在考的,是 C 语言在每个表达式背后悄悄执行的一套默认规则:整型提升、截断、数组名退化、\0收尾、求值顺序。语法记不住可以查,但这套默认规则一旦猜错,写出来的代码和心里的模型就会完全是两回事。C语言期末备考时刷详解题库的价值,恰恰不在于背答案,而在于把每一次“我以为”纠正成“机器实际怎么算”。这份材料把选择题、填空题、判断题、读程序写结果到压轴编码串成一条线,正好能把这条推理链走完。
2. 从 .c 文件到 main 执行:编译期和运行期到底谁在管事
初学者最容易把“编译”和“执行”混成一件事,题库里那道“程序有死循环时上机编译不能通过”的判断题,就是在考这个分界线。编译器只负责把文本翻译成机器能认的指令,它不负责判断你的循环会不会停。理解这条边界,后面所有关于“什么时候报错、什么时候行为未定义”的问题才有落脚点。
2.1 预处理、编译、汇编、链接:四个阶段各自拦截什么
一个.c文件变成可执行文件要经过四步:预处理展开#include和#define、编译生成汇编、汇编生成目标文件、链接把多个目标文件和库函数拼到一起。题库里填空题问“C语言源程序文件的后缀是”,答案是.c;而“预处理命令是以#开头”,说的是第一阶段的语法特征。#define N 10这种宏定义在预处理阶段就被替换成字面量 10,所以int a[N];能通过编译,因为展开后[]里是常量。
#include <stdio.h> #define N 10 /* 预处理阶段直接文本替换 */ int main(void) { int a[N]; /* 展开后等价于 int a[10]; */ printf("%zu\n", sizeof(a)); /* 40,10 个 int */ return 0; }这段代码的关键点在N。它不是变量,没有存储单元,也不参与运算,编译器看到的是替换后的10。这解释了题库第 22 题的判定:int n=10, a[n];在考试环境里被判错,因为n是变量,而数组定义要求[]内是常量表达式。至于变长数组,属于后续标准引入的特性,不同编译器行为不一致,考试按常量规则走。
链接阶段的坑更隐蔽。声明了函数却没定义,编译能过、链接报错;main缺失,链接器会明确提示找不到入口符号。这解释了为什么“程序必须有 main”是一条链接期约束,而不是语法约束。
2.2 main 的契约:入口地址为什么不是文件第一行
题库反复考“C 程序从 main 函数开始执行”,三个选项分别说从第一条可执行语句、从第一个定义的函数、从包含文件里的第一个函数开始,全是干扰项。真正的原因在于链接器:它把main的地址写进可执行文件的入口字段,操作系统加载时从这个地址开始取指令。写在main之前的函数定义,只是被放在代码段里待调用,不会被执行。
#include <stdio.h> void helper(void) { /* 定义在 main 之前,但不会自动执行 */ printf("helper\n"); } int main(void) { printf("main\n"); /* 入口在这里,先打印 main */ helper(); /* 显式调用才执行 */ return 0; }helper存在与否不影响入口位置。这也解释了题库判断题“C 程序从源文件第一条语句开始执行”为什么是错的:源文件的物理顺序和运行顺序没有必然关系。同理,main不要求写在文件开头,return 0的值会作为进程退出码交给运行时环境。
注意:
main的签名有int main(void)和int main(int argc, char *argv[])两种常见形式,题库里只考“必须叫 main、必须存在”,但实际工程中返回值类型写成void会让退出码变成不确定值。
2.3 存储类别与生命周期:auto、static、extern、register 的分配差异
填空题里“未指定存储类别的变量,其隐含的存储类别”答案是auto,而“表示静态存储类别的关键字”是static。这四个关键字管的不是类型,是变量存在多久、放在哪块内存。auto是栈上的局部变量,函数返回即失效;static修饰局部变量时把它挪到静态存储区,函数退出后值仍在;extern是声明而非定义,告诉编译器“这个符号在别处”;register只是建议放进寄存器,现代编译器基本忽略。
#include <stdio.h> void counter(void) { int a = 0; /* 每次调用重新初始化为 0 */ static int b = 0; /* 只初始化一次,之后保留上次的值 */ a++; b++; printf("a=%d b=%d\n", a, b); } int main(void) { counter(); /* a=1 b=1 */ counter(); /* a=1 b=2 */ counter(); /* a=1 b=3 */ return 0; }static局部变量的初始化发生在程序加载时而非函数调用时,这是它和auto最本质的区别。题库里“函数间的数据传递不可以使全局变量”被判定为错误,原因也在这里:全局变量本身就是静态存储期,作用域覆盖整个文件。
2.4 函数调用的值传递:形参改了实参为什么不变
填空题明确写“实参向形参的数据传递是单向的,只能由实参到形参”。这句话的机制基础是:调用发生时,实参的值被复制到形参对应的栈空间,函数体内操作的是副本。题库第 35 题问“实参和形参可以同名吗”,答案是可以,因为它们位于不同的作用域,同名不会冲突。
#include <stdio.h> void swap_wrong(int x, int y) { /* 交换的是副本 */ int t = x; x = y; y = t; } void swap_right(int *x, int *y) { /* 传地址,解引用后改的是原变量 */ int t = *x; *x = *y; *y = t; } int main(void) { int a = 1, b = 2; swap_wrong(a, b); /* a=1 b=2,没变 */ swap_right(&a, &b); /* a=2 b=1,变了 */ return 0; }swap_wrong里x和y是a、b的拷贝,函数返回后副本销毁。swap_right传的是地址,*x定位回原变量所在的内存单元,写入才生效。指针之所以能“突破”单向传递,是因为它传的仍然是值,只不过这个值恰好是一个地址。
3. 表达式求值:1/2 为什么永远等于 0
题库里错得最集中的一类题,全是表达式结果判断。11/3、11.0/3、(float)11/3、(int)(11.0/3+0.5)四个式子放在一起,能同时区分整数除法、隐式提升、强制转换、截断四种行为。搞懂这一组,C 的算术规则基本就通了。
3.1 标识符规则背后是 ASCII 表
题库第 7、8、9、10 题连考四道标识符合法性,答案都指向同一条规则:由字母、数字、下划线组成,首字符不能是数字,不能是关键字。-a1错在减号,a[i]错在方括号,3ax错在首字符,while、case、union错在关键字。第 5 题“C 语言不区分大小写”被判错,解析给的依据是a的 ASCII 值是 97、A是 65。
#include <stdio.h> int main(void) { char lower = 'a', upper = 'A'; printf("%d %d\n", lower, upper); /* 97 65 */ printf("%d\n", 'a' - 'A'); /* 32,大小写字母的固定差值 */ return 0; }大小写敏感不是风格约定,而是编码层面的硬事实:两个字符在内存里存的是不同的整数值。标识符Main和main是两个完全不同的符号,链接器不会把它们当成同一个。
3.2 整型提升与算术转换:三条通往 4 的路径
题库第 17 题问哪个表达式值为 4,四个选项分别是11/3、11.0/3、(float)11/3、(int)(11.0/3+0.5)。正确答案是最后一个,但前三个的结果同样值得记:11/3是 3,因为两个操作数都是int,除法执行整数除法,小数部分直接丢弃;11.0/3是 3.666667,因为11.0是double,3被隐式提升为double再除。
#include <stdio.h> int main(void) { printf("%d\n", 11 / 3); /* 3,整数除法截断 */ printf("%f\n", 11.0 / 3); /* 3.666667,double 运算 */ printf("%f\n", (float)11 / 3); /* 3.666667,先转 float 再除 */ printf("%d\n", (int)(11.0 / 3 + 0.5)); /* 4,加 0.5 后截断等于四舍五入 */ return 0; }第四个式子是手工实现四舍五入的经典写法:先加 0.5 把 3.6667 抬到 4.1667,再强转int砍掉小数。这也说明(int)的行为是向零截断,不是四舍五入。题库第 19 题n=(int)f%3(f=13.8)同理:先转成 13,再对 3 取余得 1,优先级决定了(int)只作用于f,不会把%3一起吞进去。
3.3 强制类型转换是值的拷贝,不是变量的改造
题库判断题“强制类型转换不会改变原变量的原有数据类型”答案是正确。(float)a产生一个临时的float值参与运算,a本身仍是int。这一点在第 18 题里露出真面目:b=(float)(1/a),a=2,括号先算1/2得 0,再转float得 0.0,不是 0.5。
#include <stdio.h> int main(void) { int a = 2; float b1 = 1.0 / a; /* 0.5,先提升再除 */ float b2 = (float)(1 / a); /* 0.0,先整除再转 */ float b3 = 1 / (float)a; /* 0.5,分母先转 */ printf("%f %f %f\n", b1, b2, b3); return 0; }三个式子的差别全在括号位置。(float)加在除法外面,整数除法已经执行完,损失的精度捡不回来;加在分母上,提升发生在除法之前,结果才保留小数。这是题库里最容易连环错的一个点,也是实际工程里血泪经验最多的地方。
3.4 优先级、结合性与短路:&& 右边的副作用可能不执行
填空题第 5 题问“C 语言不仅规定了运算符的优先级,还规定了运算符的结合性”,第 26 题问优先级相同时按什么顺序执行,答案是“自左向右”。优先级决定谁先结合,结合性决定同级怎么分组,两者合起来才是完整的求值顺序。题库判断题x=10; t=(x && x>10);后t为 0,靠的就是&&的短路:左侧x非零为真,继续算右侧x>10得假,整体为假即 0。
#include <stdio.h> int main(void) { int i = 0; /* 左侧为假,右侧的 i++ 根本不会执行 */ if (0 && i++) { } printf("%d\n", i); /* 0 */ /* 左侧为真,右侧才执行 */ if (1 && i++) { } printf("%d\n", i); /* 1 */ return 0; }短路不只是优化,它是语言层面的语义保证,很多代码依赖它做空指针保护。但反过来,把有副作用的表达式写在&&或||右侧,执行与否取决于左侧,这类代码一旦被后人改动左侧条件就会翻车。优先级表里还有一处常考:?:的结合方向是自右向左,a?b:c?d:e等价于a?b:(c?d:e)。
4. 数组与指针:下标运算其实是地址算术的语法糖
题库从第 20 题到第 50 题几乎被数组和指针包场,其中第 48 题最关键:int a[5], *p=a;问哪个是对数组元素的正确引用,答案是*(p+2)。这道题点破了 C 语言里最核心的一层抽象——a[i]和*(a+i)是同一件事。
4.1 下标从 0 开始:a[i] 的等价展开
数组名在多数表达式里会退化成指向首元素的指针,a等价于&a[0]。于是a[i]被编译器翻译成*(a + i):先取首地址,加上i个元素的偏移,再解引用。偏移量按元素大小缩放,int数组加 1 走 4 个字节,这就是指针算术和普通整数加法的区别。
#include <stdio.h> int main(void) { int a[5] = {10, 20, 30, 40, 50}; int *p = a; /* 等价于 p = &a[0] */ printf("%d\n", a[2]); /* 30 */ printf("%d\n", *(a + 2)); /* 30,和上面完全等价 */ printf("%d\n", *(p + 2)); /* 30,指针算术 */ printf("%d\n", p[2]); /* 30,指针也能用下标 */ return 0; }p[2]合法这件事说明下标运算符不专属于数组,它只是*(p+2)的另一种写法。题库第 50 题问怎么让指针指向数组第三个元素,答案是point = &array[2],而point = array指向首元素、&array[3]越界、$array[2]语法错误。第 23 题同理:int a[10]的合法下标是 0 到 9,a[10]已经越界,但编译器不会拦,这是 C 把检查责任交给程序员的具体体现。
4.2 二维数组:为什么第二维不能省
题库第 25 题int a[][3]={{1,2,3},{4,5},{6,7}}问第一维大小,答案是 3。推理过程是:初始化列表里有三个内层花括号,即三行;每行声明了 3 列,缺的元素补 0。第 26 题问哪个二维数组定义正确,答案是int a[][3]={1,2,3,4,5,6},因为第一维可以由初始化元素个数除以列数推出来,第二维必须显式写出。
#include <stdio.h> int main(void) { /* 三行三列,第二行和第三行不足的部分自动补 0 */ int a[][3] = {{1, 2, 3}, {4, 5}, {6, 7}}; int rows = sizeof(a) / sizeof(a[0]); /* 36 / 12 = 3 */ printf("%d %d\n", rows, a[2][2]); /* 3 0 */ return 0; }第二维不能省的原因藏在内存布局里:二维数组在内存中是一维连续排列,编译器要知道每行多长才能算出a[i][j]的偏移量i * 列数 + j。列数未知,偏移就算不出来。第 27 题a[2][4]越界、a[1,3]是逗号表达式、a(2)(1)语法错误,全是在考这条偏移规则。
4.3 字符数组的 \0:strlen 与 sizeof 差的那一个字节
题库第 29 题char x[]="hello"和char y[5]={'h','e','l','l','o'}比长度,答案是x大于y。原因是字符串字面量赋值给字符数组时,编译器会自动在末尾补一个'\0',所以x占 6 个字节,y只占 5 个。第 43 题char a[]="student"长度是 8 不是 7,同一个机制。
#include <stdio.h> #include <string.h> int main(void) { char x[] = "hello"; /* 6 字节,含结尾 '\0' */ char y[5] = {'h','e','l','l','o'}; /* 5 字节,没有 '\0' */ printf("%zu %zu\n", sizeof(x), sizeof(y)); /* 6 5 */ printf("%zu\n", strlen(x)); /* 5,不数 '\0' */ /* strlen(y) 会越界扫描,直到偶然遇到 0,结果不确定 */ return 0; }strlen从头开始数到第一个'\0'为止,sizeof数的是数组占的总字节数。第 42 题“strlen 求出的长度包含'\0'”被判错,正是这条规则的直接推论。第 28 题char x[][10]={"Linux","windows","Mac"}求x[1][2],答案是'n':第二行是windows,第三个字符是n,行优先存储让二维字符数组的每一行都像一个独立字符串。
4.4 指针、结构体成员与 typedef:类型名的两种身份
结构体那组题反复在区分三个名字:struct student是类型名,stu是变量名,num是成员名。题库第 41 题问哪个是非法引用,答案是student.age,因为student是类型名,类型本身不占存储单元,没法取成员。正确写法是stu1.age、p->age、(*p).age三种。
#include <stdio.h> struct student { int age; int num; }; typedef struct student Stu; /* 给类型起别名,不是定义变量 */ int main(void) { Stu s1 = {20, 1}; Stu *p = &s1; printf("%d %d %d\n", s1.age, p->age, (*p).age); /* 20 20 20 */ return 0; }第 43 题typedef struct stu { int a; float b; } stutype;问哪个叙述错误,答案是“stutype 是用户定义的结构体变量名”——它其实是类型别名,等价于把struct stu重命名。第 42 题p=&data.a考的是成员地址:data.a是int变量,&取它的地址赋给int *p才类型匹配。指针变量本身包含两部分信息:地址值和所指类型,后者决定解引用时读几个字节、按什么格式解释。
5. 避坑与排查:题库里五个连环错的现场复盘
这五个坑的共同点是:单看每个知识点都懂,串起来就错。下面按“现象 → 原因 → 解决”拆开,都是我在实际调试和带人复习时反复见到的。
5.1 现象:(float)(1/a)算出 0.0,以为是精度问题
原因:括号内的1/a先按整数除法执行,a=2时结果已经是 0,类型转换发生在精度丢失之后,转什么都救不回来。解决:把转换放到除法之前,写成1.0/a或1/(float)a;写完后盯着括号位置复查一遍,凡是(类型)(表达式)的形式,先问一句括号里是不是已经做过整数运算。
5.2 现象:int n=10; int a[n];在本地能编译,考试判错
原因:[]内的长度在标准 C 里要求是常量表达式,n是变量,编译器要么报错要么按扩展特性处理,不同环境行为不一致。解决:用#define N 10或enum { N = 10 }定义常量,再写int a[N];。凡是数组长度来自运行期变量,就要意识到这已经越过了考试范围。
5.3 现象:scanf("%s%s", &a, &b)编译通过但输出乱码
原因:数组名本身就代表首元素地址,a的类型在传参时退化为char *,再加&变成char (*)[N],类型不匹配,scanf按%s解析时拿到的地址偏移错了。解决:去掉&,写scanf("%s%s", a, b);反过来,读单个int变量时必须写scanf("%d", &n),因为n不是数组,没有退化行为。
5.4 现象:char a[]="student"长度算成 7,strlen和sizeof结果对不上
原因:字符串字面量末尾隐藏一个'\0',sizeof把它算进去得 8,strlen数到它就停得 7。解决:记死两条——sizeof数字节、strlen数字符;字符数组用{}逐个初始化且个数正好等于长度时,不会补'\0',此时对它调strlen属于越界读,行为不确定。
5.5 现象:for(i=0;i<3;i++);之后认为i等于 2,或者认为死循环编译不过
原因:循环结束时i的值是第一次让条件为假的值,即 3;编译期只做语法和类型检查,不模拟执行路径,无限循环完全合法。解决:判断循环变量的终值,看的是退出条件而不是最后一次进入循环的值;判断“能不能编译过”,看的是语法、类型、符号是否齐全,和运行时行为无关。
提示:这五条里有三条属于“编译器不报错但结果错”,靠肉眼审查很难发现。养成写完表达式后手工代入一组具体数字跑一遍的习惯,比反复读代码有效得多。
6. 设计取舍:C 把检查推到运行期,红利和代价分别落在谁身上
C 语言里有一批让初学者抓狂的设计:数组越界不检查、scanf不校验类型、指针可以指向任何地址、除零行为未定义。这些不是遗漏,而是明确的取舍——语言本身保持一层薄薄的、可预测的抽象,把运行时开销压到接近零。代价是安全网被撤掉,责任转移到程序员身上。理解了这一点,题库里那些“为什么这么判”的题就不再是死记硬背。
这个取舍带来的红利很实在。a[i]编译成一条带缩放的地址加法,没有边界判断分支;结构体成员的偏移在编译期就固定,访问就是基址加常量;函数调用只做值拷贝,不需要运行时类型信息。这些特性让 C 至今仍是操作系统内核、嵌入式固件、数据库底层这些场景的首选。反过来,代价同样具体:一个越界写可能破坏相邻变量,直到程序在完全无关的地方崩溃才暴露;一个未初始化的指针可能指向合法但错误的内存,症状和原因隔了几百行代码。
验证这套模型最有效的方式是让机器把中间结果打出来,而不是靠脑补。我通常会在关键位置插三类检查:用sizeof验证类型占位和数组长度,用%p打印地址确认指针偏移,用-Wall -Wextra打开编译器警告把隐式转换和可疑比较提前暴露出来。
#include <stdio.h> int main(void) { int a[5] = {0}; printf("sizeof(a) = %zu\n", sizeof(a)); /* 20,整个数组 */ printf("sizeof(a[0])= %zu\n", sizeof(a[0])); /* 4,单个元素 */ printf("元素个数 = %zu\n", sizeof(a) / sizeof(a[0])); /* 5 */ printf("&a[0] = %p, &a[3] = %p\n", (void*)&a[0], (void*)&a[3]); /* 两个地址相差 12 字节,即 3 个 int,验证指针算术的缩放 */ return 0; }这段代码的价值在于把“数组名退化”“指针步长”“元素个数推导”三件事同时变成可读的数字。凡是怀疑自己算错了,就让它打印出来。带参数编译时加上-Wall -Wextra -Wconversion,编译器会把(float)(1/a)这类可疑写法、scanf参数类型不匹配、有符号无符号比较一一标出来,相当于一个不需要运行时的静态检查层。
有一年帮人复盘一份模拟项目的代码,一个数组下标写成了<=,越界写坏了紧邻的循环变量,程序在另一个函数里死循环。从现象追到根因花了两个多小时,最后改动只有一个字符。从那以后我每次写带下标的循环,都强制把边界条件单独抄出来念一遍:长度是 N,合法下标是 0 到 N-1,循环条件写< N。题库里那 305 道题的价值也在这里,它把这类错误提前压缩成几十秒一道的判断,让你在真正动手写项目之前就把模型校准好。希望帮到你。
本文还有配套的精品资源,点击获取