研究各大公司的笔试、面试题目,好像很多人都比较反感,觉得它们大都属于偏题、怪题,没有实际的应用价值。但是,所谓的“偏”和“怪”,换个角度来说,也许就只是“比较注重基础”而已。
也有朋友认为,现在的计算机性能已经非常棒了,没有必要再“使用各种古怪的操作去追求效率和节省空间”,认为研究基础底层是过时的、无用的,只有“新技术”才是值得琢磨的。至少在嵌入式领域,这种观点其实很不好,基础知识在任何时候都不会过时,倒是哪些时髦的“新技术”有可能很快就过时了。
事实上,如果基础不牢,有时都不知道写出的代码为什么会出错,这是很危险的。本系列文章也并不只是为了做题而做题,而是通过一些比较有代表性的面试题,检查自己的技术欠缺点,再针对此,巩固自己的技术基础。
本系列文章一般不会出现像 a+=a+++++b 这样没什么难度,纯粹考眼力的应试题目,不过,如果基础足够扎实,即使出现了这样的题目,要解决之也是手到擒来的。
来看看这个问题
今天来看看这个问题,这是美国某著名搜索引擎公司(你知道的)的招聘题目。你看,即使是注重实践的美帝,也一样重视基础。
#include <stdio.h>
#define SUB(x,y) x-y
#define ACCESS_BEFORE(elem, ofst, val) *SUB(&elem, ofst) = val
int main()
{
int i;
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
ACCESS_BEFORE(arr[5], 4, 6);
for(i=0; i<10; ++i){
printf("%d ", arr[i]);
}
return 0;
}
以上C语言程序会:
A. 输出 1 6 3 4 5 6 7 8 9 10
B. 输出 6 2 3 4 5 6 7 8 9 10
C. 程序可以正确编译,但是运行时会崩溃
D. 程序语法错误,编译失败
先简要分析一下
与读小说从上而下的顺序不同,分析这里的C语言代码,更方便是从 main 开始。容易看出,传递给 ACCESS_BEFORE 宏的第一个参数是一个数组元素,这时 ACCESS_BEFORE 宏的初衷就好理解了:无非就是取接收到的数组元素的地址,然后减去偏移 ofst,再将 val 传递到该地址。
请注意“初衷”这个词。
也就是说,写出该代码的程序员很可能是希望能够将 arr[5-4] 赋值为 6,也就是说应该选 A 了?实践是检验真理的唯一标准,我们尝试编译和执行该C语言程序,发现编译失败了:
果然没那么简单,那么到底怎么回事呢,为什么会编译失败呢?
进一步分析
相信大家应该都清楚,C 语言中的 define 宏定义在编译之前的预处理阶段,预处理器只做简单的替换而已,在 Linux 中输入如下命令:
# gcc -E t.c
即可查看C语言程序经过预处理展开后的代码:
...
int main()
{
int i;
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
*&arr[5]-4 = 6;
for(i=0; i<10; ++i){
printf("%d ", arr[i]);
}
return 0;
}
到这里就非常清楚了,ACCESS_BEFORE(arr[5], 4, 6); 被预处理器替换为 * &arr[5]-4 = 6; 了, * &arr[5]-4 做左值当然会引发语法错误。如果想实现该程序员的“初衷”,只需要对 SUB 宏做适当的修改就可以了:
//#define SUB(x,y) x-y
//修改为
#define SUB(x,y) (x-y)
输入预处理命令,得到如下结果:
这时再编译该C语言程序,执行结果就和 “初衷”A 一致了:
# gcc t.c
# ./a.out
1 6 3 4 5 6 7 8 9 10
再看一个类似的问题
下面这个题目来自美国某著名计算机嵌入式公司的面试题,请看:
写一个标准宏 MIN,这个宏输入两个参数,并返回较小的一个。
稍微有些经验的朋友都会想起 “:?”三目运算符,并且有了上一题的经验,还会在宏定义中加上括号:
#define MIN(A, B) (A<B?A:B)
这样就可以了吗?暂且不回答这个问题,先来看看假如某次调用宏 MIN 时,传递给它的参数是一个表达式的情况:
m = k * MIN(i&0xff, j&0xff);
这时把该 C 语言代码做预处理替换,会得到下面的语句:
m = k * (i&0xff<j&0xff?i&0xff:j&0xff);
显然此时括号内部的运算符优先级不符合我们的初衷,极有可能得到意想不到的结果。所以,上面那种宏定义方式是不合适的,应该将 A 和 B 都加上括号:
#define MIN(A, B) ((A)<(B)?(A):(B))
这样就比较合适了。
总结
C语言中的宏定义的参数没有类型,预处理器只负责做形式上的替换,而不做参数类型检查,所有它和函数定义还是有不少不同点的,使用起来要非常小心。