可以在其范围之外访问局部变量的内存吗?

时间:2011-06-22 14:05:48

标签: c++ memory-management local-variables dangling-pointer

我有以下代码。

#include <iostream>

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

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

代码运行时没有运行时异常!

输出为58

怎么可能?本地变量的记忆不是在其功能之外无法访问的吗?

21 个答案:

答案 0 :(得分:4697)

  

怎么可能?是不是在其功能之外无法访问的局部变量的记忆?

您租用酒店房间。你把一本书放在床头柜的顶部抽屉里去睡觉。你第二天早上退房,但是忘了&#34;把钥匙还给你。你偷了钥匙!

一周后,您返回酒店,不要办理登机手续,用偷来的钥匙偷偷溜进您的旧房间,然后看看抽屉里。你的书还在那里。惊人!

怎么会这样?如果您没有租房,那么酒店房间抽屉的内容无法进入吗?

嗯,显然这种情况可能发生在现实世界中没问题。当您不再被授权进入房间时,没有神秘的力量会导致您的图书消失。也没有一种神秘的力量阻止你进入一个被盗钥匙的房间。

酒店管理层不 删除您的图书。你没有与他们签订合同,说如果你留下东西,他们就会为你撕碎它。如果您使用被盗钥匙非法重新进入您的房间以便将其取回,酒店保安人员不会要求让您偷偷溜进去。您没有与他们签订合同说&#34;如果我以后试图偷偷回到我的房间,你必须阻止我。&#34;相反,你和他们签了一份合同,说过&#34;我保证不会在以后偷偷回到我的房间&#34;,一个你破坏了的合同。

在这种情况下任何事情都可能发生。这本书可以在那里 - 你很幸运。别人的书可以在那里,而你的书可以在酒店的炉子里。当你进来时,有人可能会在你身边,将你的书撕成碎片。酒店可以完全拆除桌子和书本,并用衣柜取代。整个酒店可能即将被拆除,取而代之的是一个足球场,当你潜行时,你会在爆炸中死去。

你不知道会发生什么;当您退房并偷走了以后非法使用的钥匙时,您放弃了生活在可预测,安全的世界中的权利,因为选择违反了系统规则。

C ++不是一种安全的语言。它会愉快地让你打破系统的规则。如果你试图做一些非法和愚蠢的事情,比如回到一个房间,你没有被授权进入并且通过一个甚至可能不在那里的桌子翻找,C ++也不会阻止你。比C ++更安全的语言通过限制你的能力来解决这个问题 - 例如,通过对键进行更严格的控制。

更新

圣洁的善良,这个答案得到了很多关注。 (我不确定为什么 - 我认为它只是一个有趣的&#34;有点类比,但无论如何。)

我认为用一些技术性的想法来更新这一点可能是密切相关的。

编译器处于生成代码的业务中,该代码管理由该程序操纵的数据的存储。有许多不同的方法来生成代码来管理内存,但随着时间的推移,两种基本技术已经变得根深蒂固。

首先是要有某种长寿&#34;存储区域&#34;生命周期&#34;存储器中每个字节的长度 - 也就是说,它与某个程序变量有效关联的时间段 - 不能提前预测。编译器生成对#34;堆管理器的调用&#34;知道如何在需要时动态分配存储,并在不再需要时回收它。

第二种方法是拥有一个“短期”存储区域,其中每个字节的生命周期是众所周知的。在这里,生命周期遵循“嵌套”模式。这些短期变量中寿命最长的变量将在任何其他短期变量之前分配,并将在最后被释放。较短寿命的变量将在最长寿命的变量之后分配,并将在它们之前被释放。这些寿命较短的变量的生命周期在长寿命变量的生命周期内“嵌套”。

局部变量遵循后一种模式;输入方法时,其局部变量变为活动状态。当该方法调用另一个方法时,新方法的局部变量就会生效。在第一种方法的局部变量死亡之前,他们已经死了。与局部变量相关的存储寿命的开始和结束的相对顺序可以提前计算出来。

出于这个原因,局部变量通常作为存储在&#34;堆栈上生成。数据结构,因为堆栈具有推送它的第一件事就是弹出的最后一件事。

这就像酒店决定只按顺序出租房间一样,你不能退房,直到房间号码高于你的每个人都检查出来。

所以让我们考虑堆栈。在许多操作系统中,每个线程获得一个堆栈,并且堆栈被分配为特定的固定大小。当你调用一个方法时,东西被推入堆栈。如果你然后从你的方法中传回一个指向堆栈的指针,正如原始海报在这里做的那样,那只是指向一些完全有效的百万字节内存块中间的指针。在我们的比喻中,您可以退房;当你这样做时,你刚刚检查出编号最高的房间。如果没有其他人在您之后办理登机手续,并且您非法回到您的房间,那么您所有的东西都将保证在这个特定的酒店中

我们将堆栈用于临时商店,因为它们非常便宜且容易。使用堆栈存储本地文件不需要C ++的实现;它可以使用堆。它没有,因为这会使程序变慢。

不需要使用C ++实现让你留在堆栈上的垃圾不受影响,以便你可以在以后非法回来;编译器生成的代码在&#34;房间内变回零是完全合法的。你刚刚腾空了。它并不是因为那将是昂贵的。

不需要实现C ++来确保当堆栈在逻辑上收缩时,以前有效的地址仍然会映射到内存中。允许实现告诉操作系统&#34;我们现在使用此页面堆栈完成。除非我另有说法,否则发出一个异常,如果有人触及之前有效的堆栈页面,则会破坏该过程。同样,实现实际上并不这样做,因为它很慢且没必要。

相反,实现会让你犯错并逃脱它。大多数时候。直到有一天,真正可怕的事情出现了问题并且这个过程爆炸了。

这是有问题的。有很多规则,很容易意外地打破它们。我当然有很多次。更糟糕的是,这个问题通常只会在腐败发生后检测到内存损坏数十亿纳秒时出现,而很难弄清楚是谁弄乱了它。

更多内存安全语言通过限制电源来解决此问题。在&#34;正常&#34; C#根本没有办法获取本地的地址并将其返回或存储以供日后使用。您可以获取本地的地址,但语言设计巧妙,因此在本地生命周期结束后无法使用它。为了获取本地的地址并将其传回,您必须将编译器置于一个特殊的&#34;不安全的#34;模式,将单词&#34;不安全&#34;在你的程序中,要注意你可能正在做一些可能违反规则的危险事实。

进一步阅读:

答案 1 :(得分:271)

您在这里所做的只是读取和写入过去作为a地址的内存。现在你在foo之外,它只是指向一些随机存储区的指针。事实上,在您的示例中,该内存区域确实存在,此刻没有其他任何内容正在使用它。你不会因为继续使用它而破坏任何东西,而其他任何东西都没有覆盖它。因此,5仍然存在。在一个真实的程序中,该内存几乎可以立即重复使用,你可以通过这样做来破坏某些东西(虽然症状可能要到很晚才出现!)

foo返回时,告诉操作系统您不再使用该内存,可以将其重新分配给其他内容。如果你很幸运,它永远不会被重新分配,并且操作系统不会让你再次使用它,那么你就可以逃脱谎言。有可能你最终会写到最后用该地址写的任何东西。

现在,如果您想知道为什么编译器不会抱怨,可能是因为foo被优化消除了。它通常会警告你这类事情。 C假设您知道自己在做什么,从技术上讲,您没有违反范围(a之外没有foo本身的引用),只有内存访问规则,只触发警告而不是错误。

简而言之:这通常不会奏效,但有时会偶然。

答案 2 :(得分:148)

因为存储空间还没有被踩踏。不要指望这种行为。

答案 3 :(得分:79)

所有答案的一点点补充:

如果您这样做:

#include<stdio.h>
#include <stdlib.h>
int * foo(){
    int a = 5;
    return &a;
}
void boo(){
    int a = 7;

}
int main(){
    int * p = foo();
    boo();
    printf("%d\n",*p);
}

输出可能是:7

这是因为从foo()返回后,堆栈被释放,然后由boo()重用。 如果您拆卸可执行文件,您将清楚地看到它。

答案 4 :(得分:68)

在C ++中,可以访问任何地址,但这并不意味着应该。您访问的地址不再有效。 工作因为在foo返回后没有其他东西扰乱内存,但在许多情况下它可能会崩溃。尝试使用Valgrind分析您的程序,或者甚至只编译优化的程序,然后看看......

答案 5 :(得分:65)

您永远不会通过访问无效内存来抛出C ++异常。您只是举例说明引用任意内存位置的一般概念。我也可以这样做:

unsigned int q = 123456;

*(double*)(q) = 1.2;

这里我只是将123456视为双精度的地址并写入。可能会发生任何事情:

  1. q实际上可能确实是双重的有效地址,例如double p; q = &p;
  2. q可能指向已分配内存中的某处,我只是在那里覆盖8个字节。
  3. q指向分配的内存之外,操作系统的内存管理器向我的程序发送分段错误信号,导致运行时终止它。
  4. 你赢了彩票。
  5. 你设置它的方式更合理的是返回的地址指向一个有效的内存区域,因为它可能只是在堆栈的下方,但它仍然是一个无法无效的位置,你不能以确定的方式访问。

    在正常程序执行期间,没有人会自动检查内存地址的语义有效性。但是,像valgrind这样的内存调试器会很乐意这样做,因此您应该通过它运行程序并查看错误。

答案 6 :(得分:28)

您是否在启用优化程序的情况下编译程序? foo()函数非常简单,可能已在结果代码中内联或替换。

但我同意Mark B所说的结果是未定义的。

答案 7 :(得分:22)

您的问题与范围无关。在您显示的代码中,函数main未在函数foo中看到名称,因此您无法使用 this 直接访问foo中的a名称在foo之外。

您遇到的问题是程序在引用非法内存时没有发出错误信号的原因。这是因为C ++标准没有在非法内存和合法内存之间指定非常清晰的边界。在弹出堆栈中引用某些内容有时会导致错误,有时则不会。这取决于。不要指望这种行为。假设它在编程时总是会导致错误,但是假设它在调试时永远不会发出错误信号。

答案 8 :(得分:17)

您只是返回一个内存地址,它被允许但可能是一个错误。

如果您尝试取消引用该内存地址,则会有未定义的行为。

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
}

答案 9 :(得分:16)

它起作用,因为堆栈没有被改变(但是)因为放在那里。 在再次访问a之前调用其他一些函数(也调用其他函数),你可能不再那么幸运了......; - )

答案 10 :(得分:16)

这是经典的未定义的行为,这是在两天前讨论过的 - 在网站上搜索了一下。简而言之,你很幸运,但任何事情都可能发生,你的代码无法访问内存。

答案 11 :(得分:16)

这种行为是未定义的,正如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”,但结果可能会有所不同(真的!)。你的指针正在破坏其他无关的局部变量。

答案 12 :(得分:16)

注意所有警告。不仅要解决错误 GCC显示此警告

  

警告:返回的本地变量'a'的地址

这是C ++的强大功能。你应该关心记忆。使用-Werror标志,此警告会出现错误,现在您必须对其进行调试。

答案 13 :(得分:15)

您实际上调用了未定义的行为。

返回临时作品的地址,但由于临时作品在函数末尾被销毁,因此访问它们的结果将是未定义的。

所以你没有修改a,而是修改a曾经的内存位置。这种差异非常类似于崩溃和不崩溃之间的区别。

答案 14 :(得分:13)

在典型的编译器实现中,您可以将代码视为“使用以前占用的地址打印出内存块的值”。此外,如果向一个维护本地int的函数添加一个新的函数调用,那么a的值(或a用来指向的内存地址)很有可能变化。发生这种情况是因为堆栈将被包含不同数据的新帧覆盖。

但是,这是未定义的行为,您不应该依赖它来运作!

答案 15 :(得分:13)

可以,因为a是在其范围(foo函数)的生命周期内临时分配的变量。从foo返回后,内存是免费的,可以被覆盖。

您正在做的事情被描述为未定义的行为。结果无法预测。

答案 16 :(得分:11)

如果使用:: printf而不是cout,那么具有正确(?)控制台输出的内容可能会发生显着变化。 您可以在下面的代码中使用调试器(在x86,32位,MSVisual Studio上测试):

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

int main() 
{
  char* s = foo();    //place breakpoint & check 's' varialbe here
  ::printf("%s\n", s); 
}

答案 17 :(得分:4)

从函数返回后,所有标识符都被销毁,而不是保留在内存位置的值,我们无法在没有标识符的情况下找到值。但该位置仍包含上一个函数存储的值。

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

让我举一个现实世界的例子:

假设一个人在一个地方隐藏钱并告诉你该位置。过了一段时间,那个告诉你钱位置的男人死了。但是你仍然可以获得隐藏的钱。

答案 18 :(得分:2)

它&#39; Dirty&#39;使用内存地址的方法。当您返回地址(指针)时,您不知道它是否属于函数的本地范围。它只是一个地址。现在你调用了“foo”&#39;功能,即&#39; a&#39;的地址(存储位置)。已经在(安全地,至少现在至少)可寻址的应用程序(进程)内存中分配了。在'foo&#39;之后函数返回,地址为&#39; a&#39;可以认为是“肮脏的”#39;但它在那里,没有清理,也没有受到程序其他部分表达的干扰/修改(至少在这个特定情况下)。 C / C ++编译器并没有阻止你从这样的“肮脏”中解脱出来。访问(如果你关心,可能会警告你)。您可以安全地使用(更新)程序实例(进程)数据段中的任何内存位置,除非您通过某种方式保护地址。

答案 19 :(得分:0)

您的代码非常危险。您正在创建一个局部变量(在函数结束后将其视为破坏),并在对该变量进行存储之后返回该变量的内存地址。

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

这意味着您正在做一件非常糟糕的事情,因为您根本无法信任将内存地址传递给一个指针。

请考虑以下示例,并对其进行测试:

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

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

与您的示例不同,在本示例中,您是:

  • 为int分配内存到本地函数
  • 该功能到期时该内存地址仍然有效(任何人都不会删除它)
  • 内存地址是可信任的(该内存块不被认为是空闲的,因此在删除之前不会覆盖它)
  • 不使用时应删除内存地址。 (请参阅程序末尾的删除)

答案 20 :(得分:0)

这取决于语言。在C & C++/Cpp,技术上可以,因为它对任何给定的指针是否实际指向某个有效或无效的地方的检查非常弱。如果您在变量超出范围时尝试访问该变量本身,编译器将报告错误,但它可能不够聪明,无法知道您是否有意将指向该变量位置的指针复制到某些稍后仍将在范围内的其他变量。

但是,一旦变量超出范围,修改该内存将产生完全未定义的效果。您可能会破坏堆栈,这可能会为新变量重用该空间。

更现代的语言,例如 Java 或 C# 经常竭尽全力避免程序员首先需要访问变量的实际地址,以及边界检查数组访问,保持指向堆中对象的变量的引用计数,这样它们就不会过早地被释放,等等。所有这些都是为了帮助程序员避免无意中做一些不安全和/或超出范围内变量范围的事情。