3

当我尝试理解函数调用时,我编写了一个简单的代码。但我无法理解它的输出。

#include <stdio.h>

int* foo(int n)
{
    int *p = &n;
    return p;
}

int f(int m)
{
    int n = 1;
    return 999;
}

int main(int argc, char *argv[])
{
    int num = 1;
    int *p = foo(num);
    int q = f(999);
    printf("[%d]\n[%d]\n", *p, q);
    /* printf("[%d]\n", *q); */
}

输出:

[999]
[999]

为什么*p是999?

然后我修改了我的代码如下:

#include <stdio.h>

int* foo(int n)
{
    int *p = &n;
    return p;
}

int f()
{
    int n = 1;
    return 999;
}

int main(int argc, char *argv[])
{
    int num = 1;
    int *p = foo(num);
    int q = f();
    printf("[%d]\n[%d]\n", *p, q);
    /* printf("[%d]\n", *q); */
}

输出:

[1]
[999]

为什么*p1在这里?我在 Linux 中,使用 gcc 但 Clang 得到了相同的输出。

4

6 回答 6

4

除了您的代码会因为您返回一个指向堆栈变量的指针而引发未定义行为这一事实之外,您还在询问为什么行为会随着更改 f() 的签名而改变。

之所以

原因在于编译器为函数构建堆栈帧的方式。假设编译器正在为 foo() 构建堆栈帧,如下所示:

Address Contents  
0x199   local variable p
0x200   Saved register A that gets overwritten in this function
0x201   parameter n
0x202   return value
0x203   return address

对于 f(int m),堆栈看起来很相似:

Address Contents  
0x199   local variable n
0x200   Saved register A that gets overwritten in this function
0x201   parameter m
0x202   return value
0x203   return address

现在,如果你在 foo 中返回一个指向 'n' 的指针会发生什么?结果指针将为 0x201。返回 foo 后,堆栈的顶部位于 0x204。内存保持不变,您仍然可以读取值“1”。这一直有效,直到调用另一个函数(在您的情况下为“f”)。调用 f 后,位置 0x201 被参数 m 的值覆盖。

如果您访问此位置(并且您使用 printf 语句),它会显示“999”。如果您在调用 f() 之前复制了此位置的值,您将找到值“1”。

继续我们的示例,f() 的堆栈帧看起来像这样,因为没有指定参数:

Address Contents  
0x200   local variable n
0x201   Saved register A that gets overwritten in this function
0x202   return value
0x203   return address

当您使用“1”初始化局部变量时,您可以在调用 f() 后在位置 0x200 处读取“1”。如果您现在从位置 0x201 读取值,您将获得已保存寄存器的内容。

一些进一步的陈述

  • 重要的是要理解上述解释是向您展示观察所见所闻的方法论。
  • 真正的行为取决于您使用的工具链和所谓的调用 convections。
  • 人们很容易想象,有时很难预测会发生什么。这是一个与释放内存后访问内存类似的安静情况。这就是为什么通常无法预测会发生什么的原因。
  • 这种行为甚至可以随着优化级别的改变而改变。例如,我可以想象,如果您打开 -O3 例如,观察结果会有所不同,因为未使用的变量 n 将不再出现在二进制文件中。
  • 了解了背后的机制之后,应该可以理解为什么对从 foo 检索到的地址进行写访问会导致严重问题。

对于试图通过实验证明这一解释的勇敢者

首先,重要的是要看到上面的解释不依赖于真正的堆栈框架布局。我只是介绍了布局,以便有一个易于理解的插图。

如果您想在自己的机器上测试行为,我建议您使用您最喜欢的调试器并查看放置局部变量和参数的地址,以了解实际情况。请记住:更改 f 的签名会更改放置在堆栈上的信息。因此,唯一真正的“便携式”测试是更改 f() 的参数并观察 p 指向的值的输出。

在调用 f(void) 的情况下,放入堆栈的信息有很大不同,写入 p 指向的位置的值不再一定取决于参数或局部变量。它还可以依赖于主函数中的堆栈变量。

例如,在我的机器上,复制显示您在第二个变体中读取的“1”来自将用于存储“1”的寄存器保存到“num”,因为它似乎用于加载 n。

我希望这能给你一些见解。如果您还有其他问题,请发表评论。(我知道这理解起来有点奇怪)

于 2013-01-25T14:59:42.307 回答
2

您正在调用未定义的行为。您不能返回局部变量的地址(在本例中为参数int n)并期望它以后有用。

于 2013-01-25T14:20:53.227 回答
2

一个局部变量,就像n你的代码一样:

int* foo(int n)
{
    int *p = &n;
    return p;
}

foo功能完成后立即“消失” 。

您不能使用它,因为访问该变量可能会给您带来不可预知的结果。不过,你可以写这样的东西:

int* foo(int* n)
{
    *n = 999;
    return p;
}

int main(int argc, char *argv[])
{
    int num = 1;
    int *p = foo(&num);
    printf("[%d]\n", *p);
}

因为您的变量num在打印时仍然存在。

于 2013-01-25T14:24:19.597 回答
0

没有汇编器输出并不容易,但这是我的猜测:

局部变量和参数存储在堆栈中。所以调用foo的时候会返回第一个参数的地址,这个地址在栈上。

在第一个示例中,您将一个参数传递给您的第二个函数,该函数也将被推送到堆栈中,确切地说是p指向的位置。因此它会覆盖 的值*p

在第二个示例中,在第二次调用中未触及堆栈。旧值 (of num) 仍然存在。

于 2013-01-25T14:53:28.877 回答
0

在你的第一个样本中,当你这样做时

int num = 1;
int *p = foo(num);

foo()在哪里

int* foo(int n)
{
    int *p = &n;
    return p;
}

当变量numfrommain()被传递时,它按值传递给foo。换句话说,在堆栈上创建了一个num名为的变量的副本。n两者numn具有相同的值,但它们是不同的变量,因此将具有不同的地址。

当您从 中返回pfoo(),将获取与delared inmain()的地址不同的地址的值nummain()

相同的解释适用于您修改后的程序。

让我们看另一个例子来澄清:

int i = 2;

int * foo()
{
return &i;
}

int main() {

i = 1;
int *p = foo();
return 0;

}

在这种情况下,i在堆上声明,并且在和i中都引用了相同的内容。相同的地址和相同的值。main()foo()

让我们看第三个例子:

int i = 2;

int * foo(int i)
{
return &i;
}

int main() {

int i = 1;
int *p = foo(i);
return 0;

}

在这里,即使有一个 global i,它也被局部变量iin隐藏了main(),这就是传递给foo(). 因此,&i从 的返回foo,即pin的值main(),将与我在 main() 中声明的变量的地址不同。

希望这可以澄清变量范围和按值传递,

于 2013-01-25T14:32:36.483 回答
0

这种未定义的行为是由于堆栈的参与

int *p = foo(num);
int q = f(999);

在第一种情况下,当您说 时&num,它实际上将地址存储在num存储的堆栈中。然后 foo(num) 完成其执行,f(999) 使用参数 999 开始动作。由于使用了相同的堆栈,因此堆栈中存储 num 的相同位置现在具有参数 999。我们知道堆栈是连续的.

这就是两者印刷的原因999。实际上两者都试图打印堆栈中相同位置的内容。

而在第二种情况下, num 不会被覆盖,因为没有参数传递给 f() 所以,这会按预期打印。

于 2013-01-25T16:34:21.400 回答