为什么这个悬垂指针在 C 中?

why is this dangling pointer in C?

为什么ptr是悬挂指针。我知道“ch”超出范围,但 ch 的地址在内部块之外仍然有效。当我打印 *ptr 时,我得到正确的值,即 5.

void main()
{
   int *ptr;
   
   {
       int ch = 5;
       ptr = &ch;
   } 
  
  
  printf("%d", *ptr);
}

ptr 是指向不再属于您的内存的指针,它可以包含任何内容,包括您期望的值。你看到的5只是剩下的,随时都可能被覆盖。您在这里看到的是未定义的行为

在这个简单的例子中,编译器生成的代码很可能与编译器为该程序生成的代码相同(完全合法),这可能是您得到 5:

的原因
void main()
{
   int *ptr;
   
   int ch = 5;
   ptr = &ch;
   
   printf("%d", *ptr);
}

考虑这个稍微复杂一点的案例:

int *foo()
{
    int ch = 5;
    return &ch;
}

void main()
{
  int* ptr = foo();

  printf("%d ", *ptr);
  printf("%d ", *ptr);
}

这里的输出可能是这样的:

5 45643

第一次你可能会得到5,因为内存还没有被覆盖,第二次你会得到别的东西,因为同时内存已经被覆盖。

请注意,输出可能是其他任何内容,甚至可能会崩溃,因为这是未定义的行为。

另请阅读:Can a local variable's memory be accessed outside its scope?,本文适用于 C++,但也适用于 C 语言。

您遇到这种情况是因为您很可能没有尝试在启用优化的情况下编译代码。当您这样做时,您的应用程序输出将出现无法预料的行为,因为这违反了 CC++.

范围的语义

如果你不使用编译时优化,即使你违反了语义规则,你仍然可以有某种可预测性。这是因为编译器限制自己按照编写的顺序和逻辑生成代码。

一旦开始优化,只有编程语言的语义规则会继续为您提供对生成的机器代码的控制和可预测性。这就是为什么在生产代码中(你几乎总是希望在发布二进制文件中打开优化),你永远不会尝试这些学术黑客

更长的解释

编译器管理堆栈的方式遵循两种类型的契约

  1. a strong contract - 就像不同二进制文件(如共享库)之间的函数调用的情况,它被命名为 caling convention(看here). Roughly speaking, this calling convention defines how the stack frame is managed when a function is called. This is a strong contract, because it will not change based on optimization settings, or other compiler settings, or even different versions of the compiler. Otherwise, the ABI会坏掉。

  2. a weak contract - 就像函数、语句或复合语句中的局部变量或调用仅可见的函数在某个编译单元内。这里没有关于编译器如何管理堆栈的标准。它可以做任何它想做的事,只要它遵循该编程语言的语义,并且它将成为编译时优化算法的目标。

在你的例子或我的例子中(见下文),语义被破坏:我们定义了一个复合语句,退出它的范围但仍然保留(或使用)一些对该范围内使用的内存的引用。

例如

让我们用这个扩展您的示例并将其保存到 local.c 文件:

int main(int argc, char * argv[]) {
    int *ptr1, *ptr2;

    {
        int ch = 5;
        ptr1 = &ch;
    } 
    {
        int ch = 10;
        ptr2 = &ch;
    }

    printf(
        "pointer1: %d\n"
        "pointer2: %d\n",
        *ptr1, *ptr2
    );
    
    return 0;
}

现在,让我们使用 gcc 并以两种不同的方式编译它,看看会发生什么:

  1. 禁用优化
  2. 启用优化
1。禁用优化
# gcc local.c -O0 -o local; ./local
pointer1: 10
pointer2: 10

嗯,我们看到 ptr1ptr2 都指向确切的位置。这在某种程度上是有道理的,因为在第一个 复合语句 关闭后,编译器将其保留的 space 用于第二个语句。这是我们预期的行为,一旦我们使用 {} 括号定义这些复合语句的范围。

这也是您在示例中遇到的情况。您正在保存一个指向堆栈位置的地址,编译器知道它可以在到达右括号 } 后立即免费使用。但是,您的示例没有即将到来的声明来查看实际效果。

2。启用优化
# gcc local.c -O1 -o local; ./local
pointer1: 0
pointer2: 0
等等,什么?

是的,相同的代码产生两个不同的输出。启用优化后,行为会发生变化,现在编译器决定用更快或更小的代码替换您的代码。

试验函数栈帧

为了好玩,让我们尝试同样的函数:

void fn_set() { char a = 5; printf("fn_set: a=%d\n", a); }
void fn_get() { char a    ; printf("fn_get: a=%d\n", a); }

int main(int argc, char * argv[]) {
    fn_set();
    fn_get();  
    return 0;
}

我们希望 fn_get 打印 5,就像我们之前的例子一样。

让我们再次测试一下:

# gcc local.c -O0 -o local; ./local # without optimizations
fn_set: a=5
fn_get: a=5

# gcc local.c -O1 -o local; ./local # with optimizatins enabled
fn_set: a=5
fn_get: a=0

结果是一样的。理论上,函数fn_getfn_set具有相同的栈指纹。它们应该很好地重叠。在实践中,没有语义或规则与之绑定,因此编译器优化删除了不必要的代码(如 fn_get 中未使用的变量 a)并使用它们的 simplest/fastest 版本。