3

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

 3 years ago
source link: https://blog.popkx.com/c-language-interview-question-explanation-section-4-define-macro-related-interview-questions/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

发表于 2019-02-14 22:02:55   |   已被 访问: 640 次   |   分类于:   C语言   |   暂无评论

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

3dcdb3f8f5f0b900ac7be4991d65fe19.png

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

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

本系列文章一般不会出现像 a+=a+++++b 这样没什么难度,纯粹考眼力的应试题目,不过,如果基础足够扎实,即使出现了这样的题目,要解决之也是手到擒来的。

721f8a1e9987ec6ea71e0948293ca151.png

来看看这个问题

今天来看看这个问题,这是美国某著名搜索引擎公司(你知道的)的招聘题目。你看,即使是注重实践的美帝,也一样重视基础。

#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;
}
ee1a51d452fb8470d7e4d0320d01e9b5.png

以上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语言程序,发现编译失败了:

72d4378448df0e3db3b316d2329260e7.png

果然没那么简单,那么到底怎么回事呢,为什么会编译失败呢?

进一步分析

相信大家应该都清楚,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;
}
128d03e09a6e18ebb37da17f2b400808.png

到这里就非常清楚了,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)

输入预处理命令,得到如下结果:

3aabb3ca69a9f2e8ab47427c6ddd3d67.png

这时再编译该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))

这样就比较合适了。

a79906c11a96b41c22030b5d24735cf8.png

C语言中的宏定义的参数没有类型,预处理器只负责做形式上的替换,而不做参数类型检查,所有它和函数定义还是有不少不同点的,使用起来要非常小心。

阅读更多:   C语言


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK