简介:C语言笔试大全是一份面向程序员求职者与在校学生的C语言核心考点速查整理,内容覆盖static关键字用途、引用与指针区别、全局变量与局部变量内存存储位置、平衡二叉树定义、堆栈溢出原因、不能声明为虚函数的限制、冒泡排序时间复杂度、TCP/IP协议层次结构、IP地址编码组成等经典高频笔试题目。资源以1个doc文档形式打包,整体仅225KB,轻量便捷,便于离线查阅、打印复习或考前突击。目前已有257人浏览学习,适合正在备战C语言笔试、校招机试或需要快速回顾基础概念的读者。文档以问答方式呈现20余组高频知识点,从语法细节到数据结构与网络基础均有涉及,并附带部分程序分析题、输出结果讲解以及sizeof、typedef等易错点解析,可帮助读者系统梳理易混淆概念,强化记忆,提升笔试答题准确率。
1. C语言笔试到底在考什么:先搞懂它再刷题
C语言笔试大概是技术类笔试里最讲“体检”的一种。它不问你写过多少项目,也不看你的作品集,翻来覆去就是指针、内存、关键字、数据结构那几个固定考点,可偏偏无数人在这里翻车——见过背了三百道题的同学,遇到“a 和 &a 有什么区别”依然卡壳。
一句话定义:C语言笔试,是把 C 语言的基本功浓缩成一张考卷,用来验证你脑子里有没有建立“内存模型”。为什么要搞懂它?因为这类笔试题目高度重复,考的不是智商而是基础扎实程度。只要把核心概念真正吃透,就能以不变应万变,而不是靠题海战术硬扛。这篇就从最常考的 static、指针、内存、数据结构讲起,再帮你把最容易混的概念一一拆开。
2. 五个核心概念必须吃透:为什么面试官总盯着static和指针
2.1 static:一个关键字、三种身份
“static 有什么用途?(至少说明两种)”这是材料里出现频率最高的一道题。标准答法就两条:限制变量的作用域、设置变量的存储域。听起来绕,拆开说非常直白。
static 修饰局部变量时,改变的是生存期。普通局部变量在函数调用结束时就被销毁,static 局部变量只初始化一次,程序真正结束才销毁,作用域没变,还是只在当前函数里能用,但“活得更久”了。写一个计数器就能立刻感觉到:
void counter() { static int count = 0; // 只初始化一次,下次进来保留上次的值 count++; printf("%d\n", count); }去掉 static,每次调用 count 都会重新归零;加上 static,连续三次调用会输出 1、2、3。这就是“设置存储域”的具体表现——变量从栈上的临时数据,变成了静态存储区里的长效数据。
static 修饰全局变量时,改变的是作用域。普通全局变量整个程序可见,加 static 后圈定在当前源文件内,别的文件想碰都碰不到。static 修饰函数同理,函数变成“内部函数”,只在本文件可见。这类题真正想考的,是你有没有分清“生存期”和“作用域”是两回事。
2.2 指针和引用:一个便签、一个外号
“引用与指针有什么区别?”几乎每次笔试都会出现。注意引用是 C++ 的概念,但 C 语言笔试常把它们放一起问,因为它考的是对“间接访问”的理解深度。
三个核心区别,材料里写得清清楚楚:引用必须被初始化,指针不必;引用初始化以后不能被改变,指针可以改指别的对象;不存在指向空值的引用,但存在指向空值的指针。
打个比方:指针像写在便签上的门牌号,你可以把便签撕掉换成另一个地址;引用像你的外号,起好了就一直这么叫,而且外号必然对应一个真实存在的人,不可能给“空气”起外号。所以 int &ref; 单独声明是编译错误,而 int *ptr; 完全合法——哪怕它暂时还没指向谁。
材料里还有一道延伸题:int (*s[10])(int) 是什么?答案是“函数指针数组”:一个数组,里面有 10 个指针,每个指针指向一个形如 int func(int) 的函数。这类题表面考语法,实际考你有没有把“指针也是变量,能装进数组”这件事想明白。
2.3 全局变量与局部变量:内存区域从开始就不一样
材料里有一道直接问内存区别的题:全局变量储存在静态存储区,局部变量储存在堆栈。一个是“公共宿舍”,整个程序共用;一个是“临时工位”,函数一结束就拆掉。
由此引出一串经典考点。局部变量可以和全局变量重名,在函数内引用时用的是局部变量,这叫“局部屏蔽全局”;想强行访问全局版本,C++ 里用 :: 作用域运算符。如果面试官再往下追,就会问 extern 的两种引用方式:用头文件声明,编译期就能发现名字写错;用 extern 关键字,编译期不报错,链接期才暴露问题——因为编译器当时还不知道这个符号在哪,链接器去找才发现找不到。
还有一个高频判断题:全局变量能不能定义在多个 .C 文件都包含的头文件里?答案是能,但通常用 static 在每个文件里声明同名的全局变量,或者保证只有其中一个文件赋初值,其余文件用 extern 声明,否则链接阶段会报重复定义。
2.4 堆与栈:谁管分配、谁管回收
heap 是堆,stack 是栈,中文读音都像“堆”,所以特别容易混。栈由系统自动分配和释放,空间有限,默认通常只有几兆;堆是手动分配的大片自由存储区,C 里用 malloc 申请、free 释放,C++ 里对应 new 和 delete。
栈的“自动”体现在函数调用上:调用函数时参数压栈、局部变量入栈,函数返回时统一弹出,不需要你操心。堆的“手动”体现在生命周期上:malloc 出来的内存会一直存在,直到你主动 free,或者程序结束。
材料里那道“堆栈溢出一般是由什么原因导致的”,流传答案是“没有回收垃圾资源”。这个说法其实不准,要小心:不停 new/malloc 而不释放,造成的是内存泄漏,不是栈溢出;真正的栈溢出,多半是递归层数太深、局部数组开太大,把有限的栈空间撑爆了。笔试时把这点说清楚,反而比背流传答案更能让面试官记住你。
2.5 const和volatile:写给编译器的两种承诺
const 和 volatile 是笔试里最爱结对出现的关键字,因为它们的含义正好相反。
const 是向编译器承诺“我不会改它”。三种写法材料里给了:char * const p 是常量指针,p 本身不能改;char const * p 和 const char * p 是指向常量的指针,p 指向的那个 char 值不能通过 p 改。怎么记?看 const 挨着谁,它限制的就是谁。
volatile 是反向承诺:“这个变量的值可能在编译器没察觉的时候被外部改变,你别给我优化掉。”典型场景有三个:硬件寄存器、多线程共享变量、中断服务程序里修改的变量。笔试问“volatile 有什么用”,只要说出“提醒编译器不要优化、每次从内存重新读取”就过关。
到这里还常附带一问:构造函数为什么不能是虚函数?因为虚函数机制要等对象构造完、虚表建立好才可靠,而构造函数执行的时候对象还没真正成形。这一题放在系统知识章节再展开。
3. 内存与指针是翻车高发区:易错题背后的三条原理
3.1 sizeof的坑:数组参数为什么“退化”成指针
材料里有一道很经典的找错题,字符串转大写:
void UpperCase(char str[]) { for (size_t i = 0; i < sizeof(str) / sizeof(str[0]); ++i) if ('a' <= str[i] && str[i] <= 'z') str[i] -= ('a' - 'A'); }问题出在函数内的 sizeof。函数外的 char str[] = "aBcDe" 是静态定义的数组,sizeof 能算出 6(5 个字符加结尾的 \0);但一旦作为参数传进函数,str 就“退化”成指针,函数里的 sizeof(str) 只会得到指针的大小——32 位环境下是 4,64 位环境下是 8。
这题的底层原理是:数组名作为实参传递时,C 语言只传了首地址,没带长度信息。所以 sizeof 在“数组定义处”和“函数参数处”给出的答案完全不同。笔试里凡是看到 sizeof 和数组参数在一起,第一反应就该是查“这到底是数组还是指针”。
3.2 自增的副作用:b=a++; c=++a 等于多少
材料里那道输出题特别适合做自增运算的入门:
int a = 10, b, c, d; b = a++; // 先用 a 的旧值 10 给 b,然后 a 变成 11 c = ++a; // a 先变成 12,再把 12 给 c d = 10 * a++; // 用旧值 12 参与乘法得 120,然后 a 变成 13 printf("%d, %d, %d\n", b, c, d); // 输出 10, 12, 120核心就一句话:++a 是先加后用,a++ 是先用后加。这类题真正想考的是“副作用”——表达式在求值之外改变了变量状态,而多个副作用凑在一起时,人脑很容易算错。
答题时别只报答案,最好补一句“这种写法在实际项目里不推荐,因为可读性差,容易踩坑”。面试官听到这句,比听到正确答案更满意,因为说明你有工程意识。
3.3 字符串与字符数组:能改的和不能改的
“char *str = 'AAA'; str[0]='B'; 有什么错?”材料里的答案是:str 指向字符串常量,常量区的内容不可写,修改它会触发运行期错误。正确写法是 cosnt char *str = "AAA",或者用 char str[] = "AAA" 定义成可修改的数组。
同类的还有 strcpy 越界题:
char a; char *str = &a; strcpy(str, "hello"); // 一个字符的空间,硬塞 6 个字节虽然有时能“碰巧”输出正确结果,但本质是越界读写,随时可能崩溃。这是笔试找错题最爱的埋点:不考虑目标空间够不够。
字符串倒序的找错题,材料里给了一组很完整的修正,把要点串起来看:
char *src = "hello,world"; int len = strlen(src); char *dest = (char*)malloc(len + 1); // 必须为结尾的 '\0' 多留一个字节 char *d = dest; char *s = &src[len - 1]; // 指向最后一个字符,不是 src[len] while (len-- != 0) *d++ = *s--; *d = 0; // 补上字符串结束符 printf("%s\n", dest); free(dest); // 用完释放,避免内存泄漏原始代码三个错:malloc(len) 少一个字节、src[len] 指向的是 '\0' 而不是末字符、漏了末尾补 \0 和 free。这三条错误代表了字符串题的核心:长度、边界、收尾。
3.4 动态内存的配对:malloc与free为什么总被一起考
材料里那道 getmemory 的找错题是动态内存的经典翻车现场:
void getmemory(char *p) { p = (char *)malloc(100); // 改的是形参拷贝,不是实参 strcpy(p, "hello world"); } int main() { char *str = NULL; getmemory(str); // str 还是 NULL printf("%s\n", str); // 打印空指针,崩溃 free(str); // 对 NULL 执行 free 是危险的 return 0; }问题本质:参数传递是值传递,getmemory 里的 p 是 str 的拷贝,函数里 malloc 的地址赋值给拷贝,函数结束拷贝销毁,str 依然 NULL。要改主调函数的指针,必须传指针的指针,或者让函数返回新指针:
char *getmemory() { char *p = (char *)malloc(100); strcpy(p, "hello world"); return p; }这里可以记一条口诀:malloc 和 free 必须配对。不 free 是泄漏,free 两次是双重释放,free 之后再用是野指针。笔试的所有动态内存题,本质都在考“这块内存的生命周期到底由谁负责”。
4. 数据结构、算法和系统常识:笔试并不只考语法
4.1 栈、队列与链表:三种容器、三种性格
“队列和栈有什么区别?”材料里的答案是八个字:队列先进先出,栈后进先出。可以类比成排队买奶茶 vs 一摞盘子:队列先来的先服务,栈则是后放上去的盘子先被拿走。
链表和数组的区别也要能脱口而出:数组是连续存储、大小固定、按下标随机访问;链表是分散存储、大小动态可变、只能顺着指针遍历。笔试里有一道约瑟夫环题——用户输入 M、N,从 1 到 N 循环数数,每数到 M 就输出该数,直到全部输出。材料提示用循环链表,也可以直接用取余操作模拟下标位置,因为循环数数本质就是“下标对当前长度取余”。
用两个栈实现队列是另一道常考题,思路很巧妙:入队时把元素 push 进栈 A;出队时先看栈 B 是不是空,如果空就把 A 里的元素全部弹出再压进 B,最后从 B 栈顶弹出。这样入队和出队的平摊复杂度都是 O(1),比每次出队都搬运一遍高效得多。
4.2 二叉树、排序与数值判断:经典题背后的判定标准
“什么是平衡二叉树?”材料里的定义很标准:左右子树都是平衡二叉树,且左右子树深度差的绝对值不大于 1。通俗讲,就是整棵树不能一边特别深一边特别浅,否则查找效率会退化。至于“冒泡排序的时间复杂度是 O(n^2)”,记住结论之外,最好能说出原因:两层嵌套循环,每轮把一个最大值“冒”到末尾,比较次数是 n(n-1)/2 这个量级。
数值判断类也有固定套路。比如“写出 float x 与零值比较的 if 语句”——注意很多流传版本会把不等号方向写反,正解是判断绝对值是否小于某个精度:
float x = 0.0000001f; if (fabs(x) < 0.000001f) // 浮点数不能直接用 == 比较 printf("x 近似为 0\n");浮点数的二进制表示有精度误差,直接 x == 0 大概率不成立,所以笔试标准答法永远是“和一个小阈值比较”。
这类题里还有两个一语句的经典:判断 x 是不是 2 的若干次幂,用 (x & (x - 1)) == 0,前提是 x 为正整数;交换两个变量不用第三变量,可以用算数法 a=a+b; b=a-b; a=a-b,也可以用异或 a ^= b; b ^= a; a ^= b。这些题考的是位运算的直觉,不会影响大局,但会做能加分。
4.3 宏与内联函数:预处理阶段和编译阶段的分工
“频繁使用的短小函数,C 语言里怎么实现?C++ 里怎么实现?”材料里的答案是:C 用宏定义,C++ 用 inline。原因在于宏在预处理阶段直接做文本替换,没有函数调用的压栈开销;inline 是编译期的“建议内联”,同样省去调用开销,但保留了类型检查。
宏的经典陷阱是副作用。材料里的 SQUARE 宏:
#define SQUARE(a) ((a) * (a)) int b = SQUARE(a++);展开后变成 ((a++)*(a++)),a 被自增两次,行为完全不可预期。这类题的考点不是宏本身,而是“文本替换不做求值保护”。所以写宏时每个参数都要加括号,这已经成了行业共识。宏的其他变形还有取最小值、交换两个变量:
#define Min(X, Y) ((X) > (Y) ? (Y) : (X)) #define SWAP(x, y) do { x = x + y; y = x - y; x = x - y; } while (0)注意宏定义结尾不要加分号,否则在 if-else 里会翻车。另外笔试常考“C 语言怎么写死循环”——while(1) 或 for(;;),这两个写法等价,for(;;) 还省一个常量比较,但现代编译器两者生成代码几乎一样。
材料里还有一个隐蔽的宏关联题:#define Max_CB 500,循环变量是 unsigned char ucCmdNum,for(ucCmdNum=0; ucCmdNum<Max_CB; ucCmdNum++) 形成死循环。因为 unsigned char 最大只能表示 255,永远到不了 500。这类题提醒你:宏展开后,变量类型的宽度也要一起考虑。
宏不只是 C 语言的考点,还牵扯 C 和 C++ 的 struct 区别。材料里提到:C 的 struct 不能含成员函数,C++ 可以;C++ 里 struct 和 class 的区别是默认访问权限不同——struct 默认 public,class 默认 private。顺带一提,C++ 的 struct 依然可以用在 C 风格写法里,所以它是 C 到 C++ 的平滑过渡点。
4.4 操作系统与网络常识:笔试的“跨界”考点
C 语言笔试考到一半,往往会突然蹦出几道“不是 C 语言”的题,材料里就混着实时系统、进程线程、死锁、网络协议。这些题其实不是跑题,而是想看你有没有系统的计算机基础。
实时系统的基本特性,材料里两句话:特定时间内完成特定任务,核心是实时性与可靠性。进程和线程的区别,记住这个版本:线程是进程内的执行单元和调度基本单位;进程是资源拥有的基本单位。创建和撤销线程的开销远小于进程,因为线程不单独分配系统资源,而是共享所属进程的资源。
死锁的四个必要条件要能完整背出:互斥、请求保持、不可剥夺、环路等待。TCP 和 UDP 的区别也常考:TCP 提供可靠传输、流量控制,但包头大、冗余高;UDP 不保证可靠、无状态,包头小、开销低。材料里还提到互联网通常按五层理解:应用层、传输层、网络层、数据链路层、物理层;TCP/UDP 属于传输层,IP 属于网络层,ARP 负责把 IP 地址解析成网卡的物理地址(MAC 地址)。IP 地址分网络号和主机号,但要用子网掩码按位与之后,才能区分哪些位是网络号、哪些位是主机号。
这些内容看似零散,但都有一个共同点:考的是“概念是否清晰”而非“会不会背定义”。哪怕只能讲个大概,也比答错或者空白强很多。
5. 易混考点辨析:丢分多半不是不会,是混了
笔试丢分最多的场景,往往不是知识点完全空白,而是几个概念长得太像,一紧张就混。这一章专门把高频易混点逐一拆开,每条按“哪里混、为什么混、怎么区分”讲清楚。
5.1 static全局变量 vs 普通全局变量
易混点:名字都带“全局变量”,都存放在静态存储区,连存储方式都一样,看起来没区别。
为什么混:因为两者的存储位置完全相同,很多人误以为加了 static 的全局变量只是“写法不同”,功能没变。
怎么区分:作用域。普通全局变量的作用域是整个源程序,只要 extern 声明,别的文件也能用;static 全局变量把作用域限制在定义它的源文件内,其他文件无法访问,相当于给全局变量加了一道“文件级防火墙”。核心记忆点:static 修饰全局变量,改的不是存哪,而是谁能看见。
5.2 数组变量 vs 指针变量:地址相同不代表一回事
材料里有这样一道题:
char str1[] = "abc"; char str2[] = "abc"; const char *str5 = "abc"; const char *str6 = "abc"; cout << (str1 == str2) << endl; // 0 cout << (str5 == str6) << endl; // 1易混点:str1 和 str5 都是“abc 字符串的开头地址”,为什么 str1==str2 是假,str5==str6 却是真?
为什么混:因为数组名和指针在代码里都表示“一个地址”,用起来经常互相替换,就以为它们是同一回事。
怎么区分:str1 和 str2 是两个数组变量,各自拥有独立的内存空间,哪怕内容相同,首地址也不相同,所以比较结果是假。str5 和 str6 是指针,它们指向的都是同一个字符串常量——“abc”在常量区只有一份,两个指针都指向那一份,所以比较结果是真。一句话:数组比的是“各自的房子”,指针比的是“共同的门牌”。
5.3 a+1 vs &a+1:同一个地址,两种加法
材料里那道题是经典中的经典:
int a[5] = {1, 2, 3, 4, 5}; int *ptr = (int *)(&a + 1); printf("%d, %d", *(a + 1), *(ptr - 1)); // 输出 2, 5易混点:a 和 &a 打印出来的地址值完全一样,为什么 a+1 是 a[1],&a+1 却跳到了 a[5]?
为什么混:很多人把“地址值相同”等同于“类型相同”,忽略了指针加法的单位取决于指针的类型。
怎么区分:a 是数组首元素的地址,类型是 int*,加 1 跳过一个 int,所以指向 a[1];&a 是整个数组的地址,类型是 int(*)[5],加 1 跳过整个数组的大小(5 个 int),指向数组末尾之后。ptr 指向 a[5],ptr-1 退回一个 int,指向 a[4],值是 5。这类题的通用解法:先看指针类型,再加偏移,而不是先盯地址值。
给指针加数字的规则同样适用于其他类型。材料里有一道:unsigned char* p1 加 5,实际偏移 5 个字节;unsigned long* p2 加 5,32 位环境下偏移 5×4=20 个字节。地址值相同,指针类型不同,加减结果天差地别。
5.4 while vs do-while:先判和后判的区别
易混点:两种循环都能重复执行一段代码,看起来只是写法不同。
为什么混:很多人在实际编码中只习惯用 while,遇到 do-while 不知道它存在的意义。
怎么区分:while 是先判断后执行,一次都不满足则一次都不执行;do-while 是先执行一遍,再判断条件,所以循环体至少会执行一次。经典区分题是“输入一个正整数再继续处理”的场景:do-while 会先让你输入一次,再判断是否合法;while 则要先设置一个初始值。材料里原话是“前一个循环一遍再判断,后一个判断以后再循环”,记住这个顺序就够了。
5.5 指针常量 vs 常量指针:const站在谁旁边
三种声明材料里一次性给齐了:
char * const p; // 指针常量:p 不能改,p 指向的值可以改 char const * p; // 常量指针:p 可以改,*p 不能通过 p 改 const char *p; // 和 char const *p 完全等价易混点:const 和 * 的位置稍一变化,含义就完全不同,而且三种写法长得几乎一样。
为什么混:因为中文翻译五花八门,“指针常量”“常量指针”在不同资料里还经常被互换使用。
怎么区分:看 const 修饰谁。const 在 * 的左边(const char *p / char const *p),修饰的是“指针指向的内容”,内容不可变但其本身可改;const 在 * 的右边(char * const p),修饰的是“指针变量本身”,p 的指向固定,指向的内容可改。记住口诀:“左定值,右定向”——const 在 * 左边是定值,在 * 右边是定向。
5.6 struct vs union:各算各的空间 vs 共用一块地
材料里那道 sizeof 计算题很能说明问题:
typedef union { long i; int k[5]; char c; } DATE; struct data { int cat; DATE cow; double dog; } too; printf("%d", sizeof(struct data) + sizeof(DATE));易混点:struct 和 union 都能组合多个成员,很多人以为 sizeof 计算方式一样。
为什么混:因为两者定义成员的语法几乎一样,而且都能嵌套使用。
怎么区分:struct 的成员各自占独立空间,大小是所有成员大小之和(还要考虑对齐);union 的成员共用同一块内存,大小是最大成员的大小。上面 DATE 里最大成员是 int[5],占 20 字节;struct data 是 int 4 + DATE 20 + double 8 = 32;结果是 20 + 32 = 52。记忆点:struct 是每个人一间房,union 是一群人合租一个房间,房间大小取决于最胖的那个人。
6. 什么时候背、什么时候理解:给复习划一条边界
准备 C 语言笔试,最容易走两个极端。一个极端是把材料从头背到尾,背到“引用必须被初始化”这种句子脱口而出,但问他为什么,答不上来;另一个极端是只追求“懂原理”,连 static 的两种用途都要现场推导,结果选择题答得又慢又慌。正确的边界其实很清楚:固定答案靠背,可变场景靠理解。
| 适合直接背 | 适合动手推一遍 |
|---|---|
| static 的三种用途 | 指针加减的偏移量 |
| 引用与指针的三条区别 | 全局变量与局部变量的存储位置 |
| 栈和队列的进出顺序 | 自增运算符的求值顺序 |
| 死锁的四个必要条件 | malloc/free 与内存生命周期 |
| TCP 与 UDP 的特点对比 | const 在不同位置的限定对象 |
| 平衡二叉树定义、冒泡排序复杂度 | 数组名与 &数组名加 1 的差别 |
怎么判断一道题该背还是该理解?问自己一个问题:如果把题里换个数字、换个变量名、换个问法,我还能答对吗?能,说明理解了;不能,说明你背的只是那道题的“外观”。材料里那句“static 全局变量只初始化一次,防止其他文件单元引用”就是典型的理解型答案——你只有知道 static 改变的是作用域,才能解释为什么其他文件不能引用。
什么时候别用这套材料?一种是笔试前夜才开始翻,企图靠它临时抱佛脚;另一种是只刷题不敲代码。C 语言笔试的内容,本质上都来自真实的程序行为——你把倒序代码真正跑一遍,看到崩溃和正常输出之间的差别,比默写十遍答案都记得牢。很多易混概念,比如 do-while 和 while,写两行代码就再也不会混了。
从那以后,我每次准备 C 语言笔试,都强制自己把题目先分到“背”和“理解”两堆,错题本上只留下后一类。因为需要理解的题才是真正能拉开差距的题,而那些靠背诵就能拿下的基础分,只要临考前再过一遍材料里列出的定义,通常就够了。希望这份梳理能帮你把 C 语言笔试的考点看清楚,也祝你在真正的考场上少一点“见过但选错”的遗憾。
本文还有配套的精品资源,点击获取