printf 函数的一些细节

一道容易犯思维惯性错误的 C 语言题目...

大概一个月之前,看见一道有趣的 C 语言题目,题目的形式很简单,代码部分也比较容易理解,大致如下:

test1
1
2
3
4
5
6
7
#include <stdio.h>

int main() {
unsigned int a = 16;
printf("%d %d %d\n", a >> 2, a = a >> 2, a);
return 0;
}

现在,问输出的值各是多少。当时,简单思考了一下,就给出了4 1 1的答案。后来,自己模拟了一下,得到的结果竟然是1 4 4。脑子中立马反应过来,这是 C 语言中的未定义行为(Undefined behavior,简称 UB 行为😂),但是一是半会搞不明白为啥会得到这样的结果。查阅了一些资料后,发现printf函数的执行过程有一个入栈出栈的过程,具体而言:

  1. printf函数的参数是从后往前依次入栈的,这样出栈时就是从前往后依次输出的了
  2. 在入栈的过程中,表达式已经执行了,最后栈内存储的“东西”大概率是某个变量的地址注意,这里没有深究

按照上面的规则,可以分析出为何上面的代码会得出1 4 4的结果:

1
2
3
4
5
6
1. 压 a 的地址入栈
2. a = a >> 2,表达式的值为 a 此时的值,即 4,入栈
3. 表达式的值为 a >> 2 的值 1,入栈
4. 出栈,解析为 1
5. 出栈,解析为 4
6. 出栈,解析为 4

有了上面的分析,对结果的由来就不再陌生了。
但这里需要注意的是,最后输出结果中第二个参数的值4并不能佐证栈内存储的“东西”大概率是某个变量的地址这个观点。实际上,可以通过另外一个简单的例子来验证一下猜想:

test2
1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
int b;
printf("%d %d %d\n", b = 3, b = 2, b = 1);
printf("%d\n", b);
return 0;
}

如果栈内存储的是表达式的值,那么最后输出的结果应该是:

1
2
3 2 1
3

但实际上却是:

1
2
3 3 3
3

按照从后往前入栈并执行的规则,变量b一共被赋值了 3 次,但结果以最后一次为准,也即b = 3。但此时又会有一个新的问题产生,那就是究竟是printf函数的参数列表入栈后保存的是变量的地址,还是赋值表达式本身的返回结果是一个地址呢?

哈哈,想要解决这个问题,应该有三个方法:

  1. 仔细看看printf函数的源码,研究一下具体是如何实现的
  2. 直接编译成汇编,看看汇编代码是怎么写的(这需要一定的汇编基础)
  3. 借助一些调试工具,看看栈上的地址是如何变化的

在这里就不再深究了,为啥?因为深究这些 UB 行为没有太大的意义😄,之所以这么说,是因为不同编译器的具体实现不同,这就导致相同形式的未定义行为在不同编译器下编译后产生的结果可能也不相同😂。
当然了,阅读一下源码,学习一下汇编或者研究一下如何使用调试工具都是很好的,这些就留到下次再说吧...
PS:主要是下次又可以水文了,嘻嘻~😁


Buy me a coffee ? :)
0%