关于c ++:可以在其范围之外访问局部变量的内存吗?

Can a local variable's memory be accessed outside its scope?

我有以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

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

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

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

输出为58

怎么可能呢?局部变量的内存在其函数之外是否不可访问?


How can it be? Isn't the memory of a local variable inaccessible outside its function?

Ok.

你租了一间酒店房间。你把一本书放在床头柜最上面的抽屉里,然后去睡觉。你第二天早上退房,但"忘了"把钥匙还给我。你偷了钥匙!好的。

一周后,你回到酒店,不办理入住手续,带着偷来的钥匙潜入你的旧房间,然后看看抽屉。你的书还在那里。令人吃惊的!好的。

怎么会这样?如果你还没有租过房间,酒店房间抽屉里的东西难道就不可进入吗?好的。

很明显,这种情况在现实世界中是可以发生的,没问题。当你不再被授权在房间里的时候,没有神秘的力量能让你的书消失。也没有神秘的力量阻止你带着偷来的钥匙进入房间。好的。

酒店管理层不需要删除您的预订。你没有和他们签订合同说如果你把东西留下,他们会帮你把它撕碎。如果你非法带着偷来的钥匙重新进入你的房间,酒店的保安人员不需要抓住你的潜入。你没有和他们签订合同,合同上说"如果我以后再溜回我的房间,你就必须阻止我。"相反,你和他们签订了合同,合同上说"我保证以后不会溜回我的房间",这是你违反的合同。好的。

在这种情况下,任何事情都可能发生。这本书可以在那里——你很幸运。别人的书可以在那里,你的书可以在酒店的火炉里。你进来的时候可能有人在那儿,把你的书撕成碎片。旅馆本可以把桌子和书全部搬走,换上衣柜。整个酒店可能即将被拆毁,取而代之的是一个足球场,当你偷偷摸摸的时候,你会死于爆炸。好的。

你不知道会发生什么;当你退房以后偷了一把钥匙非法使用时,你就放弃了在一个可预测的安全世界里生活的权利,因为你选择了打破这个系统的规则。好的。

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

天啊,这个答案引起了很多人的注意。(我不知道为什么)我认为这只是一个"有趣"的小比喻,但不管怎样。好的。

我认为用更多的技术思想来更新这一点可能是正确的。好的。

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

第一种是拥有某种"长期"存储区域,在这种存储区域中,每个字节在存储器中的"生存期"(即与某个程序变量有效关联的时间段)不容易提前预测。编译器生成对"堆管理器"的调用,它知道如何在需要时动态分配存储,在不再需要时回收存储。好的。

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

局部变量遵循后一种模式;当一个方法被输入时,它的局部变量就会活跃起来。当该方法调用另一个方法时,新方法的局部变量将激活。在第一个方法的局部变量死之前,它们就死了。与局部变量相关的存储器的生存期的开始和结束的相对顺序可以提前计算出来。好的。

由于这个原因,局部变量通常被生成为"stack"数据结构上的存储,因为一个堆栈有一个属性,第一个推到它上面的东西将是最后一个弹出的东西。好的。

就像酒店决定只按顺序出租房间一样,除非每个房间的房间号都高于您的退房号,否则您无法退房。好的。

那么让我们考虑一下这个堆栈。在许多操作系统中,每个线程有一个栈,栈被分配为一定的固定大小。当你调用一个方法时,东西被推到堆栈上。然后,如果您将指向堆栈的指针从方法中返回,就像原始海报在这里所做的那样,这只是指向某个完全有效的百万字节内存块中间的指针。在我们的类比中,您是从酒店退房的;当您退房时,您只是从编号最高的入住房间退房。如果没有其他人在你之后登记,而你非法回到你的房间,你所有的东西都保证仍然在这个特定的酒店。好的。

我们使用临时商店的堆栈,因为它们非常便宜和容易。不需要使用C++来实现本地存储的堆栈;它可以使用堆。不会,因为那样会使程序变慢。好的。

C++的实现不需要将未被保留的堆栈中的垃圾留下,以便以后可以非法返回;对于编译器来说,生成代码是完全合法的,在您刚刚腾出的"房间"中,所有的代码都会变回零。这并不是因为,那会很贵。好的。

不需要实现C++,以确保栈在逻辑上收缩时,仍然有效的地址仍然映射到内存中。允许实现告诉操作系统"我们已经使用堆栈的这个页面完成了"。除非我另有说明,否则如果有人触摸以前有效的堆栈页,则会发出一个异常来破坏进程。同样,实现实际上并不是这样做的,因为它是缓慢的和不必要的。好的。

相反,实现可以让你犯错误,并摆脱它。大部分时间。直到有一天真正糟糕的事情发生了,过程就爆炸了。好的。

这是有问题的。有很多规则,很容易被意外打破。我当然有很多次。更糟糕的是,只有在检测到内存在崩溃发生数十亿纳秒后被破坏时,这个问题才会出现,因为很难找出是谁把它弄糟了。好的。

更多的记忆安全语言通过限制你的能力来解决这个问题。在"普通"C中,根本没有办法获取本地地址并将其返回或存储以备以后使用。你可以取一个本地人的地址,但是语言设计得很巧妙,这样在本地人的生命周期结束后就不可能使用它了。为了获取本地地址并将其传递回去,您必须将编译器置于特殊的"不安全"模式,并在程序中使用"不安全"一词,以引起注意,您可能正在做一些可能违反规则的危险事情。好的。

进一步阅读:好的。

  • 如果C允许返回引用怎么办?巧合的是,今天的博客主题是:好的。

    http://blogs.msdn.com/b/ericlippet/archive/2011/06/23/ref-returns-and-ref-locals.aspx好的。

  • 为什么我们要使用堆栈来管理内存?C中的值类型是否总是存储在堆栈中?虚拟内存是如何工作的?以及更多关于C内存管理器如何工作的主题。这些文章中的许多也与C++程序员有密切关系:好的。

    https://blogs.msdn.microsoft.com/ericlippert/tag/memory-management/好的。

好啊。


你在这里所做的只是简单地读写记忆,它曾经是a的地址。既然您不在foo中,它只是指向某个随机内存区域的指针。在您的示例中,内存区域确实存在,而目前没有其他任何东西在使用它。你不会因为继续使用而破坏任何东西,而且还没有其他东西覆盖它。因此,5仍然存在。在一个真正的程序中,该内存几乎会立即被重新使用,这样做会破坏某些东西(尽管这些症状直到很晚才会出现!)

当您从foo返回时,您告诉操作系统您不再使用该内存,它可以重新分配给其他对象。如果你很幸运,它永远不会被重新分配,操作系统也不会再发现你在使用它,那么你就可以摆脱谎言了。很有可能你最终会把地址写下来。

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

简而言之:这通常不起作用,但有时是偶然的。


因为存储空间还没有被踩上去。别指望那种行为。


在所有答案中添加一点:

如果你这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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
"
,*p);
}

输出可能是:7

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


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


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

1
2
3
unsigned int q = 123456;

*(double*)(q) = 1.2;

在这里,我只是把123456作为一个双精度数的地址,并写信给它。任何事情都可能发生:

  • 实际上,q可能是双精度的有效地址,例如double p; q = &p;
  • q可能指向已分配内存中的某个位置,而我只覆盖其中的8个字节。
  • q指向分配的内存之外,操作系统的内存管理器向我的程序发送一个分段故障信号,导致运行时终止它。
  • 你中了彩票。
  • 您设置它的方式更合理一点,返回的地址指向有效的内存区域,因为它可能只是在堆栈的下面稍微远一点,但它仍然是一个无效的位置,您不能以确定的方式访问它。

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


    是否使用启用优化器编译程序?EDCOX1的0个函数非常简单,可能在结果代码中被内联或替换。

    但我同意Mark B,结果行为是未定义的。


    你的问题与范围无关。在您所显示的代码中,函数main没有看到函数foo中的名称,因此您不能在foo之外直接访问foo中的a

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


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

    是的,如果您试图取消引用该内存地址,您将具有未定义的行为。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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
    }


    它工作是因为自A被放到那里以来堆栈还没有被改变。在再次访问a之前,调用一些其他函数(也调用其他函数),您可能不再那么幸运了…;-)


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


    正如亚历克斯指出的那样,这种行为是未定义的——事实上,大多数编译器会警告不要这样做,因为这是一个简单的方法来获得崩溃。

    对于您可能会得到的这种令人毛骨悚然的行为的示例,请尝试以下示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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",但您的结果可能会有所不同(真的!)。您的指针正在删除其他不相关的局部变量。


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

    warning: address of local variable 'a' returned

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


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

    返回临时工程的地址,但由于临时工程在函数结束时被破坏,访问临时工程的结果将不确定。

    所以您没有修改a,而是修改了a曾经所在的内存位置。这一区别与碰撞和不碰撞的区别非常相似。


    它可以,因为a是在其作用域(foo函数)的生命周期内临时分配的变量。从foo返回后,内存是空闲的,可以被覆盖。

    你所做的被描述为未定义的行为。结果是无法预测的。


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

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


    正确的东西(?)如果使用::printf但不能使用,则控制台输出可能会发生显著变化。您可以在以下代码中使用调试器(在x86、32位、MSVisual Studio上测试):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    char* foo()
    {
      char buf[10];
      ::strcpy(buf,"TEST");
      return buf;
    }

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

    从函数返回后,所有标识符将被销毁,而不是保存在内存位置中的值,如果没有标识符,则无法定位值。但该位置仍包含由前一个函数存储的值。

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

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

    假设一个人把钱藏在一个地方,然后告诉你地点。过了一段时间,那个告诉你钱的人死了。但你仍然可以得到那笔隐藏的钱。


    这是使用内存地址的"肮脏"方式。当您返回一个地址(指针)时,您不知道它是否属于函数的本地范围。只是一个地址。既然调用了"foo"函数,"a"的地址(内存位置)已经分配到应用程序(进程)的可寻址内存中(至少目前是安全的)。"foo"函数返回后,"a"的地址可以被认为是"脏"的,但它在那里,没有被清除,也没有被程序其他部分的表达式干扰/修改(至少在这种特定情况下)。一个C/C++编译器不会阻止你这样的"脏"访问(如果你在意的话,也许会警告你)。您可以安全地使用(更新)程序实例(进程)数据段中的任何内存位置,除非您通过某种方式保护地址。


    你的代码非常危险。您正在创建一个局部变量(函数结束后wich被视为已销毁),并在该变量被销毁后返回该变量的内存地址。

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

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

    考虑这个例子,然后测试它:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    int * foo()
    {
       int *x = new int;
       *x = 5;
       return x;
    }

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

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

    • 将int的内存分配给本地函数
    • 当函数过期时,该内存地址仍然有效(不会被任何人删除)
    • 内存地址是可信任的(该内存块不被视为可用的,因此在删除之前不会被覆盖)
    • 不使用时应删除内存地址。(见程序末尾的删除)