首页 文章

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

提问于
浏览
909

我有以下代码 .

#include <iostream>

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

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

并且代码正在运行,没有运行时异常!

输出是 58

怎么会这样?本地变量的内存不能在其功能之外无法访问吗?

19 回答

  • 20

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

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

    Let me take a real world example:

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

  • 14

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

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

  • 13

    怎么会这样?本地变量的内存不能在其功能之外无法访问吗?

    你租了一个酒店房间 . 你把一本书放在床头柜的顶部抽屉里睡觉 . 你第二天早上退房,但“忘记”给你钥匙 . 你偷了钥匙!

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

    How can that be? Aren't the contents of a hotel room drawer inaccessible if you haven't rented the room?

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

    酒店管理层无需删除您的图书 . 你没有为你撕碎它 . 如果您用偷来的钥匙非法重新进入您的房间以便将其取回,酒店保安人员无需让您偷偷溜进去 . 您没有与他们签订 Contract "if I try to sneak back into my room later, you are required to stop me."而是您与他们签订了 Contract 那说"I promise not to sneak back into my room later",你打破的 Contract .

    在这种情况下 anything can happen . 这本书可以在那里 - 你很幸运 . 别人's book can be there and yours could be in the hotel'的炉子 . 当你进来时,有人可能就在那里,将你的书撕成碎片 . 酒店可以完全拆除 table 和书本,并用衣柜取代 . 整个酒店可能即将被拆除,取而代之的是一个足球场,当你潜行时,你会在爆炸中死去 .

    你不知道会发生什么;当你离开酒店并偷了钥匙以后非法使用时,你放弃了生活在一个可预测,安全的世界的权利,因为你选择违反了系统的规则 .

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

    更新

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

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

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

    第一种是拥有某种“长寿命”存储区域,其中存储中每个字节的“生命周期” - 即与某个程序变量有效关联的时间段 - 无法在前面轻松预测时间编译器生成对“堆管理器”的调用,该管理器知道如何在需要时动态分配存储,并在不再需要时回收存储 .

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

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

    出于这个原因,局部变量通常作为“堆栈”数据结构上的存储生成,因为堆栈具有推送它的第一个东西将是弹出的最后一个东西的属性 .

    这就像酒店决定只按顺序出租房间,在房间号码高于您的所有人都检查出来之前,您不能退房 .

    所以让's think about the stack. In many operating systems you get one stack per thread and the stack is allocated to be a certain fixed size. When you call a method, stuff is pushed onto the stack. If you then pass a pointer to the stack back out of your method, as the original poster does here, that'只是一个指向一些完全有效的百万字节内存块中间的指针 . 在我们的比喻中,您退房酒店;当你这样做时,你刚刚检查出编号最高的房间 . 如果没有其他人在您之后办理登机手续,并且您非法返回您的房间,那么您所有的东西都将保证在这个特定的酒店仍然存在 .

    我们使用堆栈临时商店,因为它们真的很便宜 . 使用堆栈来存储本地时不需要C的实现;它可以使用堆 . 它没有,因为这会使程序变慢 .

    不需要实现C来保持你留在堆栈上的垃圾不受影响,这样你就可以非法地回来了;编译器生成的代码在您刚刚腾出的“房间”中变回零是完全合法的 . 它不是因为那将是昂贵的 .

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

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

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

    更多内存安全语言通过限制功率来解决此问题 . 在"normal" C#中,根本无法获取本地的地址并将其返回或存储以供日后使用 . 您可以获取本地的地址,但语言设计巧妙,因此在本地生命周期结束后无法使用它 . 为了获取本地的地址并将其传回,你必须将编译器置于一个特殊的"unsafe"模式,并在你的程序中加入"unsafe"这个词,以引起注意你可能正在做一些危险的事情 . 打破规则 .

    进一步阅读:

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

    http://blogs.msdn.com/b/ericlippert/archive/2011/06/23/ref-returns-and-ref-locals.aspx

    • 为什么我们使用堆栈来管理内存? C#中的值类型是否始终存储在堆栈中?虚拟内存如何工作?关于C#内存管理器如何工作的更多主题 . 其中许多文章也与C程序员密切相关:

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

  • 11

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

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

  • 62

    如果你使用:: 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); 
    }
    
  • 0

    你在这里做的只是读取和写入曾经是 a 的地址的内存 . 既然你已经超出了 foo ,它就会通过继续使用它来破坏任何东西,而其他任何东西都没有覆盖它 . 因此, 5 仍然存在 . 在一个真实的程序中,这个内存几乎可以立即重复使用,你可以通过这样做来破坏某些东西(尽管症状可能要到很晚才出现!)

    当你从 foo 返回时,你告诉操作系统你很幸运,它永远不会被重新分配,操作系统也不会躲开谎言 . 尽管如此,你最终还是会写出最终以该地址结束的其他内容 .

    现在,如果你抱怨,可能是因为优化消除了 foo . 它通常会警告你这类事情 . C假设你知道你在这里违反了范围(在 foo 之外没有引用 a 本身),只有内存访问规则,它只触发警告而不是错误 .

    简而言之:这通常不会起作用,但有时会偶然发生 .

  • 2

    因为存储空间还没有被踩到 . 不要指望那种行为 .

  • 9

    所有答案的一点点补充:

    如果你做那样的事情:

    #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()重用 . 如果您拆卸可执行文件,您将清楚地看到它 .

  • 73

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

  • 4622

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

    unsigned int q = 123456;
    
    *(double*)(q) = 1.2;
    

    在这里,我只是将123456视为double的地址并写入它 . 可能发生任何事情:

    • q 实际上可能确实是双重的有效地址,例如 double p; q = &p; .

    • q 可能指向已分配内存中的某处,我只是在那里覆盖了8个字节 .

    • q 指向分配的内存之外,操作系统的内存管理器向我的程序发送分段错误信号,导致运行时终止它 .

    • 你赢了彩票 .

    你设置它的方式是更合理的,返回的地址指向一个有效的内存区域,因为它可能只是在堆栈的下方,但它仍然是一个无法访问的无效位置确定性的时尚 .

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

  • 12

    您是否在启用优化器的情况下编译程序?

    foo()函数非常简单,可能在结果代码中内联/替换 .

    但是我和马克B一致认为结果行为是未定义的 .

  • 263

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

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

  • 13

    你只是返回一个内存地址,它是允许的,但可能是一个错误 .

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

    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
    }
    
  • 138

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

  • 59

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

  • 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”,但结果可能会有所不同(真的!) . 你的指针正在破坏其他不相关的局部变量 .

  • 27

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

    返回临时工作的地址,但由于临时工作在函数末尾被销毁,访问它们的结果将是不确定的 .

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

  • 14

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

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

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

  • 14

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

相关问题