今天面试的时候面试官花了不少时间琢磨一段 C 程序代码,因为之前自己学的时候喜欢看很多相关东西,而且确实看过有关编译器实现的部分细节所以基本也都答出来了,但确实被问到的时候而且在面试后自己试了一下才惊叹于这样居然也可以编译通过...

先说结论:

  1. C 函数签名只有函数名(我只记得 C++ 是函数名+参数类型了2333)
  2. 调用函数时参数进栈顺序是逆序(原因待考究
    相关知识点:函数调用原理,编译器对函数的签名,编译大体流程

涉及这个问题面试问答大体流程

考虑如下 C 代码:

#include <string.h>
#include <stdio.h>
int main()
{
  printf("%d", strlen("hello"));
  return 0;
}

面试的时候首先让介绍了 #include 是什么(是预处理指令,在编译阶段进行处理,功能是引入相关文件代码,但并不是简单直接插入,因为要处理一些变量作用域等其他问题),下面大概是问答的过程

Q:如果将 #include <string.h> 注释掉能否编译通过?
A:不能,因为一个函数要调用,必须要有相应的声明以及实现。

Q:如果将 string.h 文件里的内容写到 main 函数前能否编译通过?具体来说,插入一行 int strlen(char *);
A:可以编译,因为编译器已经找到了 strlen 的声明知道该如何调用这个函数。

Q:但是这里只有声明没有具体代码,能正常执行吗?
A:可以,因为编译时是分单元编译的,main.o 的中间文件生成只需要这个文件中所有被调用的函数有声明即可;在连接阶段的时候编译器才会去寻找其具体的实现代码,而 strlen 是 C 标准库提供的,连接阶段都会进行连接,所以相应的代码能够被正确连接并生成可执行文件。

Q:如果我把 strlen 的声明改为 int strlen(char *, int),并且在调用的时候给他传递第二个参数呢?能否编译通过?
A:应该不能,因为在连接阶段的时候找不到对应的实现,我对 C++ 比较熟,C++ 编译器在生成函数签名的时候包括了返回值以及参数,连接的时候会一起检查,而 C 编译器我知道返回值是一定不包含在函数签名中的,至于参数有没有我不太确定。

上面的回答潜台词就是:如果编译器认为函数签名一致则可以连接成功并生成可执行代码,在 C++ 中一定不能编译成功,而在 C 中取决于函数签名是否包含了参数。

上面的回答其实是有错误的点的,现在回忆一下 C++ 的函数签名也是没有返回值的,其只包括函数名及参数类型;而 C 语言则只有函数名,这也是 C 不支持函数重载的原因。也就是说上面问题的答案是可以编译通过。

应该是基于我上面的回答问了下面的问题:
Q:函数调用的过程是怎么样的?
A:首先 CPU 会保存当前上下文,然后将函数的参数入栈,然后 jmp 到函数的代码去执行,函数内部的局部变量的清理我记得似乎是可以分为两种,一种是调用者清理,一种是被调用者清理,似乎是看具体实现的标准来定的。在函数代码执行完毕之后会 ret 到原本的位置。

Q:参数进栈的顺序是怎么样的?
A:我感觉似乎顺序进栈和逆序进栈都可以,只要保证调用和被调用统一即可;但具体好像大部分都是实现的逆序进栈。

Q:如果上述的代码(传递两个参数的版本)确实能够调用到实际标准库中的代码,能够正确输出结果吗?
A:可以,因为进栈顺序是逆序的话,调用时进栈 2 次,第一个参数 char * 最后进栈,而 strlen 里面只 pop 一次取参数,取到的是正确的参数,因此可以正确执行代码。

结论

因为我自认为了解得相对比较透彻,前面的问题我都比较有把握,需要测试的时与标准库不同参数的声明能否正确编译运行;
面试完成之后我立刻新建了一个文件进行测试,测试结果是在 C 编译器下可以,而 C++ 不行,后者报错找不到对应的实现,连接失败,因此可以得出以下结论:

  1. C 的函数签名只有函数名,不包括返回值也不包括参数
  2. 进栈顺序是逆序是为了让这种情况下也可以正确执行程序(保证参数组合的前缀相同即可?

而关于进栈顺序是逆序的进一步原因还有待查阅相关资料,私以为这类不匹配的情况就不应该让其能够编译运行,因为既然参数不匹配就说明程序员对参数记忆有误,而且有额外的参数是没有被使用到的;那么与其在测试的时候正确执行了,我认为还是直接在测试时崩溃查错更好,毕竟既然参数不匹配,就说明理解与编码存在偏差,而这个偏差就有可能代入产品中产生隐藏的 bug。

以上,欢迎交流,请多指教。

标签: cpp, c, 编译原理

添加新评论