简介:C语言笔试大全是一份面向求职者与在校学生的C语言笔试复习资料,聚焦笔试高频考点与易错概念,帮助读者在有限时间内梳理知识脉络、查漏补缺。资源包内含1个doc文档,压缩包约225KB,以文字问答形式组织内容,便于打印或对照复习。资料围绕static关键字用途、引用与指针区别、全局变量与局部变量的内存分布、平衡二叉树定义、堆栈溢出成因、冒泡排序时间复杂度、float与零值比较写法、TCP/IP协议层次、ARP地址转换、IP地址编码划分等常见题目展开,并附有循环链表报数、字符串倒序等程序设计题的思路与代码分析,还涉及switch参数类型、局部变量与全局变量重名、extern引用全局变量等细节辨析。已有258人学习,适合准备C语言笔试的读者按知识点逐项过筛,快速定位薄弱环节。
1. C语言笔试的底层逻辑:从“语法记忆”到“内存模型”的跨越
很多人复习 C 语言笔试,第一反应是背题:static 有几个用途、冒泡排序复杂度是多少、sizeof和strlen有什么区别。背完一轮去模考,遇到“char *str = &a; strcpy(str, "hello")哪里错了”这种题还是发懵。原因不在记性,在于没有把知识点挂到一根主轴上。C 语言笔试真正考的不是语法条目,而是你对内存模型的理解:变量存在哪、生命周期多长、指针指向谁、数组名和指针差在哪、函数调用时栈上发生了什么。把这条线拉直,散落的选择题、改错题、程序输出题会自动归位。这份梳理按“存储与作用域 → 指针与数组 → 函数与参数传递 → 结构体与内存对齐 → 经典算法与操作系统交叉点”的顺序展开,适合已经写过 C 代码、正在系统复习或准备笔试的人拿来当查漏补缺的地图。
2. 存储与作用域:static、extern 与变量的一生
2.1 四个存储区与变量的归属
C 程序运行时的内存大致分成代码区、静态存储区(全局区)、栈区和堆区。笔试里反复出现的“全局变量和局部变量在内存中是否有区别”,答案就落在这张分区表上。全局变量和 static 变量存放在静态存储区,程序启动时分配、结束时释放;普通局部变量存放在栈上,函数返回即销毁;malloc申请的空间在堆上,需要手动free。把变量按“存储位置 + 生命周期 + 作用域”三个维度列表,比死记结论稳得多。
| 变量类型 | 存储位置 | 生命周期 | 作用域 | 初始化 |
|---|---|---|---|---|
| 普通全局变量 | 静态存储区 | 整个程序 | 整个源程序(多文件可见) | 默认 0 |
| static 全局变量 | 静态存储区 | 整个程序 | 仅本源文件 | 默认 0 |
| 普通局部变量 | 栈 | 函数调用期间 | 函数或语句块内 | 不确定 |
| static 局部变量 | 静态存储区 | 整个程序 | 函数或语句块内 | 只初始化一次,默认 0 |
| malloc 申请 | 堆 | 从申请到 free | 由指针控制 | 不确定 |
这张表能直接回答一串高频题:static 局部变量“只初始化一次,下一次依据上一次结果值”;static 全局变量“防止在其他文件单元中被引用”;static 函数“仅在本文件可见”。注意一个容易混的点:全局变量本身就是静态存储方式,加 static 改变的不是存储方式,而是链接属性,从外部链接变成内部链接。
2.2 static 的三副面孔与 extern 的引用方式
static 在笔试里几乎必考,标准答法是三条:修饰局部变量,改变生命周期不改变作用域;修饰全局变量,改变作用域(限制在本文件)不改变生命周期;修饰函数,把函数变成内部函数,只在本文件可见。三条背后是同一个机制——把名字的可见范围收窄,同时把存储期拉长到整个程序。
extern 则相反,用来引用已经定义过的全局变量。这里有个血泪经验级的细节:用“引用头文件”的方式引用全局变量,变量名写错在编译期就报错;用 extern 方式引用,写错在编译期不报错,链接期才报错。原因很直白,extern 只是声明“有这么个东西”,编译器不核对定义,链接器合并符号时才发现找不到。复习时把“编译期 vs 链接期”这组对比记住,能顺带解决一批“以下说法正确的是”型选择题。
// file1.c int g_count = 0; // 定义,分配空间 static int s_count = 0; // 仅本文件可见 // file2.c extern int g_count; // 声明,链接期找定义 // extern int s_count; // 链接期报错:s_count 是内部链接上面代码的关键在于:extern声明不分配空间,只告诉编译器符号存在;static定义把符号的链接属性改成内部,别的翻译单元看不到。参数层面没有可调项,但要注意声明与定义的类型必须完全一致,否则链接期或运行期出问题。
2.3 局部与全局重名、头文件里的全局变量
“局部变量能否和全局变量重名”答案是能,函数内引用时局部屏蔽全局。原文提到用::访问全局变量,这其实是 C++ 的作用域解析运算符,纯 C 里没有这个语法,复习时别把 C 和 C++ 的规则混着答。C 里要访问被屏蔽的全局变量,只能靠改名或通过指针间接访问。
“全局变量可不可以定义在可被多个 .c 文件包含的头文件中”这题容易翻车。直接答“可以”是不严谨的:头文件被多个源文件包含,等于同一个定义重复出现,链接期报重定义。正确做法是头文件里只放extern声明,定义放在某一个 .c 文件里;或者用 static 让每个翻译单元各有一份独立副本,但那样就不是共享同一个变量了。这个点笔试常以“以下做法正确的是”出现,判断标准就一条:定义只能有一处,声明可以有多处。
3. 指针与数组:笔试改错题的重灾区
3.1 指针、数组名与 &a 的偏移语义
指针和数组的题之所以难,是因为同一个符号在不同上下文里含义不同。a是数组首元素地址,&a是数组对象的地址,两者数值相同但类型不同:前者是int *,后者是int (*)[5]。所以a + 1偏移一个 int,&a + 1偏移整个数组。原文那道int *ptr = (int *)(&a + 1); printf("%d,%d", *(a+1), *(ptr-1));输出2,5,就是这条规则的直接应用。
int a[5] = {1, 2, 3, 4, 5}; int *p1 = a; // 指向 a[0],p1+1 偏移 4 字节 int (*p2)[5] = &a; // 指向整个数组,p2+1 偏移 20 字节 printf("%d\n", *(p1 + 1)); // 2 printf("%d\n", *(*(p2 + 1) - 1)); // 越界读,仅用于说明偏移量p1的类型决定步长是sizeof(int),p2的类型决定步长是sizeof(int[5])。复习时养成习惯:看到指针加法先问“指针的基类型是什么”,答案自然出来。另一个高频点是sizeof用在函数参数上——数组形参退化为指针,函数内sizeof(str)得到的是指针大小(32 位机 4 字节),不是数组长度。原文UpperCase那道题错就错在这,函数外sizeof(str)/sizeof(str[0])是 6,函数内变成 4。
3.2 const 与指针的四种组合
const修饰指针的位置不同,语义完全不同,这是选择题的常客。判断口诀:从变量名往外读,const 挨着谁就锁谁。
| 声明 | 含义 | 能否改指针 | 能否改所指内容 |
|---|---|---|---|
const char *p | 指向常量的指针 | 能 | 不能 |
char const *p | 同上 | 能 | 不能 |
char * const p | 常量指针 | 不能 | 能 |
const char * const p | 两者都锁 | 不能 | 不能 |
原文里int const *f const = &a;这种写法本身就有语法问题,两个 const 中间缺了*,复习时看到类似表达式先检查语法合法性,再判断语义。还有char *s = "AAA"; s[0] = 'B';这类题,字符串字面量在只读区,通过指针改它是未定义行为,正确写法是const char *s = "AAA";。这类题不考你背没背过,考的是“字面量存在哪”这个内存模型问题。
3.3 字符串倒序:一道题串起五个易错点
原文那道字符串倒序改错题信息量极大,几乎把指针操作的坑踩全了:malloc(len)少分配\0的空间、char *s = src[len]把字符赋给指针、循环里d++ = s--先自增再赋值导致越界、结尾没补\0、用完没free。把它当模板题吃透,比刷十道同类题管用。
#include <stdio.h> #include <string.h> #include <stdlib.h> int main(void) { const char *src = "hello,world"; int len = strlen(src); char *dest = (char *)malloc(len + 1); // +1 给 '\0' if (dest == NULL) return -1; char *d = dest; const char *s = &src[len - 1]; // 指向最后一个有效字符 while (len-- != 0) *d++ = *s--; // 先取值再移动 *d = '\0'; // 手动补结束符 printf("%s\n", dest); free(dest); // 释放,避免内存泄漏 return 0; }关键参数说明:malloc(len + 1)的+1是给字符串结束符留位;&src[len - 1]取最后一个字符的地址而不是字符值;*d++ = *s--是先解引用再移动指针,顺序反了就越界。这段代码建议手写三遍,直到不看原文也能默出来,因为它是“指针 + 内存 + 字符串”三合一的典型。
4. 函数、参数传递与程序输出:值传递的照妖镜
4.1 值传递、地址传递与“改不了实参”的根因
C 只有值传递。传指针看似能改外部变量,本质是把地址这个值复制进形参,通过地址间接改目标内存。原文getmemory(char *p)那道题崩溃的根因就在这:p是形参,函数内p = malloc(100)只改了形参副本,main里的str仍是 NULL,后面strcpy和free全部作用在空指针上。要修就得传二级指针char **p,或者让函数返回申请到的地址。
void getmemory(char **p) { *p = (char *)malloc(100); if (*p != NULL) strcpy(*p, "hello world"); } // 调用:getmemory(&str);对比原版,差别只在“改的是 p 还是 *p”。这类题在笔试里常以“程序输出什么/哪里错”出现,判断方法固定:看函数内修改的是形参本身,还是形参指向的内容。改形参本身,外部无感;改指向内容,外部可见。
4.2 自增自减与函数调用的输出推演
a=10; b=a++; c=++a; d=10*a++;输出10,12,120,推演时把“取值”和“自增”拆成两步就不容易错:b=a++先把 10 赋给 b,a 变 11;c=++a先 a 变 12 再赋给 c;d=10*a++先算10*12=120赋给 d,a 再变 13。注意printf参数求值顺序在标准里未定义,不同编译器结果可能不同,笔试若考多个自增表达式挤在同一个printf里,属于“玄学题”,答题时按从左到右的常见实现推,但要心里有数这不是可移植代码。
函数指针那题show(multi, 10, &a)输出 110,考的是 typedef 函数指针、回调调用和指针传参三件事叠在一起。拆开看:FUNC2是“返回 int、收三个 int* 的函数指针”类型,show里先通过inc把 10 变成 11,再调multi算11 * 10 = 110写进arg2。这类题不要求你背答案,要求你能顺着类型一层层剥。
4.3 宏的陷阱:SQUARE(a++) 为什么错
#define SQUARE(a) ((a)*(a))看似加了括号很安全,但SQUARE(a++)展开成((a++)*(a++)),a 被自增两次,结果依赖求值顺序,属于未定义行为。宏是文本替换,不是函数调用,参数带副作用就会翻车。安全写法是用inline函数,或者至少保证宏参数不被重复求值。同类题还有#define Min(X,Y) ((X)>(Y)?(Y):(X)),参数带++一样出问题。复习时记住一句话:宏的参数出现几次就求值几次。
5. 结构体、内存对齐与经典算法交叉点
5.1 union 与 struct 的 sizeof 计算
原文那道typedef union {long i; int k[5]; char c;} DATE;求sizeof(struct date) + sizeof(max)得 52,考的是两条规则:union 的大小等于最大成员的大小(还要考虑对齐),struct 的大小是各成员大小之和再按最大对齐数补齐。DATE里int k[5]占 20 字节最大,所以 union 是 20;struct data里int4 +DATE20 +double8 = 32;两者相加 52。注意原文提到 16 位编译器下 int 可能是 2 字节,结果变 20,这说明 sizeof 的结果依赖平台,答题时先确认字长假设。
| 构造 | 大小规则 | 本例结果(32 位) |
|---|---|---|
| union | 最大成员大小,按最大对齐补齐 | 20 |
| struct | 成员之和 + 对齐填充 | 32 |
| 指针 | 由地址总线决定,32 位机 4 字节 | 4 |
5.2 两个栈实现队列、循环链表与位运算技巧
“两个栈实现队列”是数据结构与 C 语言交叉的经典题:入队压栈 A,出队时若 B 空则把 A 全部倒入 B,再从 B 弹出。平摊复杂度 O(1)。原文的约瑟夫环用循环链表加取余,思路是把“数到 M 输出”转成链表节点删除。位运算那边,“判断 x 是否为 2 的幂”用(x & (x - 1)) == 0,“交换两数不借第三变量”用异或三次,都是笔试小题但很能区分基本功。
// 判断是否为 2 的幂:2 的幂二进制只有一个 1 int is_pow2(unsigned int x) { return x != 0 && (x & (x - 1)) == 0; } // 异或交换,注意 a、b 不能是同一块内存 a ^= b; b ^= a; a ^= b;异或交换有个隐藏坑:如果a和b指向同一地址,三次异或会把值清零。笔试若问“这段交换代码有什么问题”,答案往往就是这个别名问题。
5.3 操作系统与网络的交叉考点
C 语言笔试常夹带操作系统和网络题:进程与线程的差别(调度单位 vs 资源单位、并发性、开销)、死锁四个必要条件(互斥、请求保持、不可剥夺、环路)、进程调度策略(FCFS、优先级、时间片轮转、多级反馈)、TCP/IP 四层与 OSI 七层的对应、ARP 做 IP 到物理地址的转换、TCP 三次握手确定端口。这些点不需要深挖,但要能一句话说准。复习时把它们单独列一张“交叉考点卡”,考前扫一遍即可,别和 C 语法混在一起背。
6. 易漏点清单:五类最容易翻车的地方
6.1 易漏点一:把 C 和 C++ 的规则混答
为什么漏:复习资料常把 C 和 C++ 题混排,::访问全局变量、引用必须初始化、虚函数、new/delete这些是 C++ 的规则,答到 C 语言题里就错。怎么补:每道题先确认语言前提,C 没有引用、没有类、没有虚函数表;struct在 C 里不能有成员函数,在 C++ 里可以且默认 public。
6.2 易漏点二:sizeof 与 strlen、数组与指针的边界
为什么漏:两者长得像,实际一个是编译期算大小、一个是运行期数到\0;数组名在多数表达式里退化为指针,但在sizeof和&面前不退。怎么补:遇到sizeof先问“操作数是数组还是指针”,遇到strlen先问“有没有\0”,两个问题答完基本不会错。
6.3 易漏点三:malloc 的 +1 与 free 的配对
为什么漏:字符串题里malloc(len)少一个字节,程序可能“看起来能跑”,换个环境就崩,属于典型的隐性错误。怎么补:凡是给字符串申请空间,一律写len + 1;凡是malloc,检查是否配对free;凡是函数内申请内存要传出,检查是否用了二级指针或返回值。
6.4 易漏点四:宏参数的副作用与重复求值
为什么漏:宏加了括号就以为安全,忽略了参数被展开多次。怎么补:宏参数里不写++、--、函数调用等有副作用的表达式;能写inline就不写宏;必须用宏时,参数在宏体里只出现一次。
6.5 易漏点五:未定义行为当成“结果不确定但能跑”
为什么漏:printf里多个自增、i = i++、越界读写,这些在某个编译器上可能输出“正常”结果,让人误以为规则如此。怎么补:把“标准未定义”和“实现相关”分开记,未定义行为不推结果,实现相关的(如 int 字长、字节序)答题时注明平台假设。
7. 考前一周怎么过:一份可执行的查漏补缺流程
复习到最后一阶段,最怕的是“题都见过,一考就错”。我的做法是把这份笔记当检查表用,而不是当题库刷。具体分三步走。第一步,合上资料,在白纸上默画内存分区图和变量归属表,画完对照第 2 章的表找缺口,缺哪格补哪格。第二步,挑五道模板题手写代码:字符串倒序、两个栈实现队列、二级指针修 getmemory、union/struct 的 sizeof 计算、宏交换两数,写完自己讲一遍每行为什么这么写,讲不顺的地方就是没真懂的地方。第三步,把第 6 章五条易漏点逐条自问“我能不能举一个反例”,举不出来说明还停留在背结论。
| 复习动作 | 检验标准 | 对应章节 |
|---|---|---|
| 默画内存分区与变量表 | 五个存储位置全对 | 第 2 章 |
| 手写字符串倒序 | 不看原文一次通过 | 第 3 章 |
| 讲清值传递与二级指针 | 能说出改形参和改指向的区别 | 第 4 章 |
| 算对 union/struct 大小 | 能说明对齐规则 | 第 5 章 |
| 五条易漏点各举反例 | 每条都能落到具体代码 | 第 6 章 |
时间分配上,语法记忆类(static 三用途、const 四组合、调度策略)用零碎时间过,每天十分钟;代码推演类(指针偏移、函数调用输出、宏展开)必须动笔,光看会骗自己。最容易忽略的是“平台假设”这一层,同一道 sizeof 题在 16 位和 32 位下答案不同,答题时先写假设再给结果,这个习惯能救回不少分。
从那以后我每次复习这类笔试资料,都强制自己先画一遍内存图再开始背题,因为背下来的结论会忘,画出来的模型不会。希望这份梳理帮到你,把散落的题串成一张能自己生长的网。
本文还有配套的精品资源,点击获取