一个本地变量的内存是否可以在其作用域外被访问?

1160

我有以下的代码。

#include <iostream>

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

int main()
{
    int* p = foo();
    std::cout << *p;
    *p = 8;
    std::cout << *p;
}

而且代码只是运行而没有运行时异常!

输出为58

怎么可能?一个局部变量的内存不是在其函数外部无法访问吗?


15
如果不进行修改,这段代码甚至无法编译;即使你修复了格式错误,gcc仍然会警告“返回本地变量'a'的地址”;而valgrind则显示“大小为4的非法写入[...]地址0xbefd7114刚好在堆栈指针下方”。请注意,我的翻译尽可能准确地呈现原始文本的含义,同时也更易于理解。 - sehe
89
@Serge: 在我年轻时,我曾经在Netware操作系统上工作过一些有点棘手的零环代码,涉及到巧妙地移动堆栈指针,这种方式并不被操作系统完全批准。当我犯了错误时,通常堆栈会重叠在屏幕内存上,我可以直接观察到字节被写入显示器上。这种做法现在行不通了。 - Eric Lippert
23
在理解问题所在之前,我需要阅读问题和一些答案。这实际上是关于变量访问范围的问题吗?你甚至没有在函数外使用“a”。这就是问题的全部。从内存引用方面展开讨论是完全不同的话题,与变量范围无关。 - erikbstack
12
重复的答案并不意味着重复的问题。很多人在这里提出的重复问题实际上是完全不同的问题,只是碰巧涉及到相同的潜在症状...但是提问者没有办法知道这一点,所以这些问题应该保持开放状态。我关闭了一个旧的重复问题,并将其合并到这个问题中,因为它有一个非常好的答案,所以应该保持开放状态。 - Joel Spolsky
16
@Joel:如果这里的答案不错,应该将其___合并到更早的问题___中,而不是相反。而这个___问题___确实是其他在这里提出的问题的副本,甚至更多(尽管其中一些比其他问题更适合)。请注意,我认为Eric的回答很好。(事实上,我标记了这个问题以便将答案合并到更早的问题中,以挽救早期的问题。) - sbi
显示剩余13条评论
21个回答

18

正如Alex指出的,这种行为是未定义的。实际上,大多数编译器都会警告不要这样做,因为这是导致程序崩溃的简单方法。

作为您可能会遇到的奇怪行为的一个例子,可以尝试以下示例:

int *a()
{
   int x = 5;
   return &x;
}

void b( int *c )
{
   int y = 29;
   *c = 123;
   cout << "y=" << y << endl;
}

int main()
{
   b( a() );
   return 0;
}

这会输出“y=123”,但你的结果可能因机器不同而异(真的!)。您的指针正在破坏其他不相关的局部变量。


18

你只是返回了一个内存地址。虽然这是允许的,但很可能是一种错误。

如果您尝试解引用该内存地址,则会导致未定义的行为

int * ref () {

    int tmp = 100;
    return &tmp;
}

int main () {

    int * a = ref();
    // Up until this point there is defined results
    // You can even print the address returned
    // but yes probably a bug

    cout << *a << endl;//Undefined results
}

我不同意:在 cout 之前存在一个问题。 *a 指向未分配(释放)的内存。即使您不对其进行解引用,它仍然是危险的(并且很可能是虚假的)。 - ereOn
@ereOn:我更清楚地解释了我所说的问题,但它并不危险,至少对于有效的C++代码而言是如此。但是,从用户角度来看,这很危险,因为用户可能会犯错误并做出一些不好的事情。例如,也许你正在尝试查看堆栈增长情况,你只关心地址值,永远不会对其进行引用。 - Brian R. Bondy

17

这是典型的未定义行为,在这里不到两天前就有讨论了——在网站上搜索一下。简而言之,你很幸运,但任何事情都可能发生,你的代码正在对内存进行无效访问。


16

你实际上触发了未定义行为。

返回临时变量的地址是可行的,但由于临时变量在函数结束时被销毁,访问这些变量的结果将是未定义的。

因此,你并没有修改 a 变量,而是修改了 a 所在内存位置。这种差异与崩溃和不崩溃之间的差异非常相似。


14

这是可能的,因为a是一个在其作用域内(foo函数)分配了临时内存的变量。当你从foo返回后,该内存就可以被释放并被覆盖。

你正在进行的操作被描述为未定义行为,结果无法预测。


14

在典型的编译器实现中,您可以认为代码是“打印出地址为a的内存块的值,该地址曾经被占用”。此外,如果将一个新的函数调用添加到包含本地int的函数中,a的值(或a曾经指向的内存地址)很可能会发生改变。这是因为堆栈将被覆盖以包含不同的数据帧。

然而,这是未定义的行为,您不应该依赖它来工作!


4
“print out the value of the memory block with address that used to be occupied by a"并不太准确。这使得它听起来像他的代码有一些明确定义的含义,但事实并非如此。尽管如此,你是正确的,这可能是大多数编译器实现的方式。 - Brennan Vincent
@BrennanVincent:当存储器被a占用时,指针保存了a的地址。虽然标准不要求实现定义目标生命周期结束后地址的行为,但它也认识到在某些平台上,UB以环境特征的记录方式进行处理。虽然局部变量的地址在超出其作用域后通常没有多大用处,但其他类型的地址在其各自目标的生命周期结束后仍可能具有意义。 - supercat
例如,虽然标准可能不要求实现允许将传递给 realloc 的指针与返回值进行比较,也不允许调整指向旧块内地址的指针以指向新块,但某些实现确实这样做,利用此功能的代码可能比必须避免涉及分配给 realloc 的指针的任何操作 - 甚至是比较 - 更有效率。 - supercat

12

如果你使用::printf而不是cout,那么正确的控制台输出会发生很大变化。

你可以在下面的代码中尝试使用调试器进行测试(已在x86、32位、Visual Studio上测试通过):

char* foo()
{
  char buf[10];
  ::strcpy(buf, "TEST");
  return buf;
}

int main()
{
  char* s = foo();    // Place breakpoint and the check 's' variable here
  ::printf("%s\n", s);
}

无法编译(也由语法高亮指示)。存在一个非ASCII双引号: - Peter Mortensen

6
这是一种“脏”使用内存地址的方法。当您返回一个地址(指针)时,您不知道它是否属于函数的本地作用域。它只是一个地址。
现在您已调用“foo”函数,“a”的内存位置已经分配到了应用程序(进程)的可寻址内存中(至少目前是安全的)。
在“foo”函数返回后,“a”的地址可以被认为是“脏”的,但它仍然存在,没有被清除或被程序其他部分的表达式干扰/修改(至少在这种情况下如此)。
C/C++编译器不会阻止您进行这种“脏”访问(如果您关心的话,它可能会发出警告)。除非您通过某种手段保护地址,否则您可以安全地使用(更新)程序实例(进程)中数据段中的任何内存位置。

5

函数返回后,所有标识符都被销毁,而不是保留在内存中的值。如果没有标识符,我们无法定位这些值。但该位置仍然包含之前函数存储的值。

因此,在此函数中,foo() 返回 a 的地址,而 a 在返回其地址后被销毁。您可以通过返回的地址访问修改后的值。

让我举个现实生活中的例子:

假设一个人将钱藏在某个地方,并告诉您这个地方。一段时间后,告诉您钱的位置的人去世了。但您仍然可以访问那笔隐藏的钱。


0

你的代码非常危险。你正在创建一个本地变量(在函数结束后被认为已经销毁),并且在它被销毁后返回该变量的内存地址。

这意味着内存地址可能有效也可能无效,你的代码将容易受到可能的内存地址问题(例如分段错误)的影响。

这意味着你正在做一件非常糟糕的事情,因为你正在传递一个不可信的指针给一个内存地址。

相反,考虑以下示例并进行测试:

int * foo()
{
    int *x = new int;
    *x = 5;
    return x;
}

int main()
{
    int* p = foo();
    std::cout << *p << "\n"; // Better to put a newline in the output, IMO
    *p = 8;
    std::cout << *p;
    delete p;
    return 0;
}

与您的示例不同,使用此示例:
  • 在本地函数中为int分配内存
  • 该内存地址在函数过期时仍然有效(没有被任何人删除)
  • 该内存地址是可靠的(该内存块不被视为自由,因此在删除之前不会被覆盖)
  • 该内存地址在不使用时应删除。(请参见程序结尾处的delete)

1
提问者使用了原始指针。我做了一个例子,完全反映了他所做的例子,以便让他看到不可靠指针和可靠指针之间的区别。实际上,还有另一个与我的答案类似的答案,但它使用strcpy,我认为这可能对初学者编码人员来说不太清晰,而我的例子使用new则更加清晰明了。 - Nobun
他们没有使用new。你教他们使用new。但是你不应该使用new - Lightness Races in Orbit
那么在您的意见中,将地址传递给在函数中被销毁的本地变量比实际分配内存更好吗?这没有意义。理解分配和释放内存的概念非常重要,尤其是如果您正在询问指针(提问者没有使用new,但使用了指针)。 - Nobun
智能指针肯定比new更好,我同意你的看法。但是对于初学者来说,使用和理解new要比智能指针简单。我认为应该逐步增加复杂度。首先,必须了解什么是分配,然后才能达到下一步(可能是...如何更好地使用分配? -> 智能指针和std中的其他工具)。然而,我承认我是一个非常老派的C++自学者:D 所以 -> 我的错 :P - Nobun
智能指针的对象管理应该是教授的内容。newdelete 是一种高级话题,可以稍后教授。 :) - Lightness Races in Orbit
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接