商汤C/算法校招笔试题复盘:指针、位运算与内存对齐考点拆解

发布时间:2026/10/12 20:17:25
商汤C/算法校招笔试题复盘:指针、位运算与内存对齐考点拆解 1. 从一道校招题看C/算法岗的筛选逻辑2018年前后那几年计算机视觉和深度学习方向正处于大规模扩张期各家做AI的公司都在抢人校招笔试自然成了第一道筛子。商汤那年的C/算法笔试题在圈子里流传挺广我后来陆续跟几个当年参加过这场笔试的人聊过也自己拿这套题做过复盘。说实话这套题放到今天看依然有参考价值因为它考的不是死记硬背而是你对C语言底层细节和基础算法的手感。很多人一听到算法岗笔试就默认全是LeetCode那套动态规划、图论其实那年的卷子结构挺有意思C语言基础题占了相当比例指针、内存布局、位运算这些老派内容反复出现算法部分反而更偏向数组操作、字符串处理和简单的递归/迭代转换。这个配比背后是有逻辑的——做底层算子优化、模型部署、推理引擎的人C功底不过关根本没法干活。你写得出快速排序但搞不清指针和数组的区别在实际工程里是要出事的。这篇文章我打算把这套题的几个典型考点拆开讲透不是给你一份标准答案就完事而是把每道题背后面试官到底想筛掉什么人讲清楚。适合准备C/C方向校招的应届生也适合工作几年但想回头补一补底层基础的开发者。我会补充一些当年题目里没写、但实际面试中经常追问的延伸点以及我自己踩过的坑。2. 指针与数组的边界题最容易翻车的第一道坎2.1 为什么这类题几乎年年出现C语言笔试卷子里指针和数组的关系是永恒的开场题。当年那套题里有一道很典型的给一段涉及数组名、指针运算、sizeof和strlen混用的代码让你判断输出。这类题看着简单但正确率一直不高原因在于大部分人学C的时候是背结论而不是理解内存。我先把核心结论摆出来但更重要的是理解它为什么成立。数组名在大多数表达式里会退化成指向首元素的指针但它本身不是指针——它是一个数组类型的对象。这个区别在sizeof和取地址两种场景下会暴露出来。sizeof(数组名)得到的是整个数组占用的字节数而sizeof(指针)得到的是指针本身的大小32位系统4字节64位系统8字节。很多人栽就栽在这里。int arr[10]; int *p arr; printf(%zu\n, sizeof(arr)); // 40整个数组 printf(%zu\n, sizeof(p)); // 8指针本身64位 printf(%zu\n, sizeof(arr) / sizeof(arr[0])); // 10元素个数注意sizeof是编译期运算符不是函数。它不会对表达式求值所以sizeof(arr)里的arr根本不会执行。这个点在笔试题里被反复利用来设陷阱。2.2 指针运算的步长问题指针加减整数时移动的步长是所指类型的大小不是字节数。int *p做p1地址实际增加4字节char *p做p1地址增加1字节。当年有道题给了一个int数组和一个指向它的指针让你算p1和(char*)p1分别指向哪里本质就是考这个步长概念。我见过太多人在这里出错因为他们脑子里把指针当成地址数字在算而忽略了类型信息。正确的思维模型是指针 地址 类型。类型决定了它一次能跨多远也决定了解引用时读几个字节。再延伸一个高频追问a和a的区别。假设int a[5]a退化后是int*a是int(*)[5]两者数值相同但类型不同a1跨4字节a1跨20字节。这个点在实际写代码时很少用到但面试官特别爱问因为它能瞬间区分出真懂和背过的人。2.3 二维数组传参的经典陷阱二维数组作为函数参数时第二维的长度不能省略。void f(int a[][5])合法void f(int a[][])非法。原因是编译器需要知道每一行有多长才能正确计算a[i][j]的地址。这个知识点在当年那套题里以找错的形式出现让你指出函数声明哪里有问题。我在实际项目里也踩过这个坑。早期写图像处理代码时想把一个二维像素数组传给处理函数图省事写成了int **结果运行直接崩。因为二维数组在内存里是连续的一块而int **是指针的指针两者内存布局完全不同。正确做法要么用int (*p)[WIDTH]要么干脆退化成一维数组加手动索引data[i * WIDTH j]。后者在性能敏感的场合反而更常见因为编译器优化起来更友好。3. 位运算与内存对齐区分会用C和懂C的分水岭3.1 位运算题的套路与实战价值那套题里位运算部分有几道很经典的判断一个数是不是2的幂、统计二进制中1的个数、不用临时变量交换两个数。这些题现在网上烂大街了但当年做出来的人不多因为很多人对位运算不熟。判断2的幂标准解法是n 0 (n (n - 1)) 0。原理是2的幂的二进制只有一个1减1之后那个1变成0、后面全变1两者按位与必然为0。这个技巧在实际工程里用得很多比如哈希表扩容时判断容量、内存池按2的幂对齐分配。统计1的个数n (n - 1)能消掉最低位的1循环几次就有几个1。比逐位检查快因为循环次数等于1的个数而不是总位数。还有个更骚的解法是查表法把8位的结果预先算好存数组然后按字节查适合对性能极致要求的场景。int count_ones(unsigned int n) { int count 0; while (n) { n (n - 1); count; } return count; }提示写位运算时一定要用无符号类型。有符号数的右移行为是实现定义的算术右移还是逻辑右移取决于编译器而且负数做位运算容易出意外。这是当年题目里一个隐藏的坑。3.2 内存对齐笔试爱考、工程更要命内存对齐是C语言里课本讲得少、实际用得多的典型。当年有道题给一个结构体让你算sizeof里面故意混了char、int、double考的就是对齐规则。规则本身不复杂每个成员的偏移量必须是它自身大小的整数倍在默认对齐下结构体总大小必须是最大成员大小的整数倍。但实际算起来容易乱。我一般用画格子的方法从偏移0开始每个成员先对齐再占位最后整体补齐。struct S { char a; // 偏移0占1字节 // 填充3字节 int b; // 偏移4占4字节 char c; // 偏移8占1字节 // 填充7字节 double d; // 偏移16占8字节 }; // 总大小24为什么要有对齐因为CPU访问内存是按字word来的不对齐的话一个变量可能横跨两个内存块需要两次访问再拼接性能差。有些架构比如某些ARM甚至直接不允许非对齐访问会触发硬件异常。实际工程里的经验结构体成员按大小从大到小排列能减少填充字节。上面那个结构体如果改成double d; int b; char a; char c;大小能压到16字节。这个技巧在写网络协议包、文件格式解析时特别有用因为你要精确控制每个字节的位置。我做过一个二进制协议解析的项目结构体没排好序多出来的填充字节导致解析错位排查了大半天才定位到。3.3 大小端判断与联合体的妙用判断机器是大端还是小端经典写法是用联合体int is_little_endian() { union { int i; char c; } u; u.i 1; return u.c 1; }联合体的所有成员共享同一块内存给i赋值1后如果最低地址的字节是1说明是小端。这个技巧当年也考过。大小端在网络编程里是绕不开的跨平台传输数据必须统一字节序通常约定用大端网络字节序发送前用htonl、htons转换。4. 算法题拆解数组、字符串与递归的实战变形4.1 数组类题目的原地操作思路算法部分那套题偏向基础没有特别刁钻的。有一道是原地删除数组中指定元素要求空间复杂度O(1)。标准解法是双指针一个快指针遍历一个慢指针记录有效位置遇到不等于目标值的就写到慢指针位置。int remove_element(int *nums, int n, int val) { int slow 0; for (int fast 0; fast n; fast) { if (nums[fast] ! val) { nums[slow] nums[fast]; } } return slow; }这类原地题的核心思想是不要新建数组用读写指针在原数组上覆盖。这个模式在图像处理里很常见比如原地做像素值的阈值过滤避免额外分配内存。我做过一个实时视频处理的项目每帧都新建缓冲区的话内存分配开销扛不住必须原地操作。4.2 字符串处理的边界条件字符串题最容易在边界上翻车空串、只有空格、全是同一个字符、超长串。当年有道题是反转字符串里的单词比如hello world变成world hello。思路是先整体反转再逐个单词反转。void reverse(char *s, int l, int r) { while (l r) { char t s[l]; s[l] s[r]; s[r] t; l; r--; } } void reverse_words(char *s) { int n strlen(s); reverse(s, 0, n - 1); int start 0; for (int i 0; i n; i) { if (s[i] || s[i] \0) { reverse(s, start, i - 1); start i 1; } } }注意处理字符串时永远要问自己如果输入是空串会怎样如果末尾没有空格会怎样。我见过太多代码在i n和i n之间搞错导致最后一个单词没被处理或者越界访问。4.3 递归转迭代面试官最爱追问的延伸那套题里有一道递归题具体记不太清了但面试环节经常追问能不能改成迭代。递归转迭代是C/算法岗的高频考点因为递归有栈溢出风险工程代码里能迭代就迭代。以斐波那契为例朴素递归是指数级复杂度改成迭代就是O(n)long fib(int n) { if (n 2) return n; long a 0, b 1; for (int i 2; i n; i) { long c a b; a b; b c; } return b; }更一般的情况递归转迭代需要自己用栈模拟调用过程。比如二叉树的中序遍历递归写法三行搞定迭代写法要手动维护一个栈。这个转换能力能体现你对函数调用栈的理解深度面试官很看重。5. 复盘这套题当年考生最容易丢分的几个点5.1 眼高手低算法会写C基础题反而错我聊过的几个人里有个共同现象刷了几百道LeetCode结果C基础题错一片。原因很简单LeetCode用的是高级语言的抽象你不需要关心内存、指针、对齐这些。但C/算法岗的笔试恰恰要考这些底层细节因为岗位本身就是要跟底层打交道。我的建议是准备这类岗位时专门花时间过一遍《C专家编程》和《深入理解计算机系统》里关于指针、内存、链接的章节。不用全看挑指针、数组、结构体对齐、位运算这几块精读。这些内容在笔试里性价比极高投入产出比远超再刷50道动态规划。5.2 忽略编译期与运行期的区别sizeof、宏展开、const、static这些概念很多人在编译期还是运行期上犯迷糊。比如sizeof是编译期求值宏是预处理期文本替换const变量在C里其实还是运行期的C里才更多体现编译期常量。这些区别在笔试题里经常被拿来设陷阱。举个典型例子#define SQUARE(x) x*x调用SQUARE(12)得到的是12*12 5而不是9。因为宏是文本替换没有括号保护。正确写法是#define SQUARE(x) ((x)*(x))。这个坑我当年也踩过后来写宏一律给参数和整体都加括号。5.3 时间分配失衡那套题题量不小很多人前面C基础题磨太久后面算法题没时间写。合理的策略是先扫一遍全卷把有把握的快速拿下卡壳的先跳过。C基础题如果30秒内没思路大概率是想偏了先标记跳过。算法题哪怕写不出最优解也要把暴力解写出来拿部分分。我个人的经验是笔试和实际写代码是两回事。笔试考的是在压力下快速调用知识储备所以平时练习一定要计时。我当年准备的时候每周掐时间做两套完整卷子模拟真实考场节奏效果比零散刷题好得多。6. 从这套题延伸到今天的准备策略6.1 底层功底依然是硬通货虽然现在AI岗很多转向了Python和框架层面但做推理优化、算子开发、嵌入式部署的方向C功底依然是硬门槛。那套2018年的题放到今天考点几乎没变因为底层的东西变化慢。指针、内存、位运算、对齐这些十年后还是这些。如果你目标是这类岗位我的建议是别只盯着刷题网站。找一本讲C底层的书配合实际写点小项目——比如手写一个内存池、实现一个简单的哈希表、做个位图。写的过程中你会自然遇到对齐、指针运算、内存管理这些问题比单纯做题理解深得多。6.2 算法部分要抓变形能力基础算法就那些但笔试和面试考的是变形。同一道两数之和可以变成三数之和最接近的两数之和有序数组的两数之和。核心是理解双指针、哈希、排序这些工具的适用场景而不是背题。我习惯的做法是每做完一道题问自己三个问题这题的暴力解是什么优化点在哪如果条件变一下比如数组变成链表、有序变成无序该怎么改这三个问题想清楚一道题顶十道。6.3 笔试之外的准备笔试只是第一关后面还有面试。那套题对应的面试环节据我了解会追问项目细节、手写代码、现场分析问题。所以准备笔试的同时别忽略项目梳理和表达能力。我见过笔试满分但面试讲不清自己项目的人一样被刷。具体来说把你做过的项目按背景-难点-方案-结果四段式整理好每个项目准备一个能讲三分钟的故事。手写代码部分重点练数组、字符串、链表、二叉树这几类因为面试现场写代码时间紧这些是最常考的。最后分享一个我自己的小习惯每次做完一套笔试卷不管对错都把错题和卡壳的题整理到一个文档里标注错因和正确思路。考前一周只看这个文档比重新刷题效率高得多。这个习惯我从准备校招一直保持到现在工作后排查线上问题也靠类似的复盘文档积累经验。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询