C语言面试题详解(第4节),define 宏相关的面试题

研究各大公司的笔试、面试题目,好像很多人都比较反感,觉得它们大都属于偏题、怪题,没有实际的应用价值。但是,所谓的“偏”和“怪”,换个角度来说,也许就只是“比较注重基础”而已。

也有朋友认为,现在的计算机性能已经非常棒了,没有必要再“使用各种古怪的操作去追求效率和节省空间”,认为研究基础底层是过时的、无用的,只有“新技术”才是值得琢磨的。至少在嵌入式领域,这种观点其实很不好,基础知识在任何时候都不会过时,倒是哪些时髦的“新技术”有可能很快就过时了。

事实上,如果基础不牢,有时都不知道写出的代码为什么会出错,这是很危险的。本系列文章也并不只是为了做题而做题,而是通过一些比较有代表性的面试题,检查自己的技术欠缺点,再针对此,巩固自己的技术基础。

本系列文章一般不会出现像 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语言中的宏定义的参数没有类型,预处理器只负责做形式上的替换,而不做参数类型检查,所有它和函数定义还是有不少不同点的,使用起来要非常小心。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif