C++ 局部变量的内存是否可以在其作用域之外访问?

C++ 局部变量的内存是否可以在其作用域之外访问?,c++,memory-management,local-variables,dangling-pointer,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 怎么可能呢?局部变量的内存不是在其功能之外无法访问吗?因为存储空间还没有被占用。不

我有以下代码

#include <iostream>

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

int main()
{
    int* p = foo();
    std::cout << *p;
    *p = 8;
    std::cout << *p;
}
代码只是在运行,没有运行时异常

产量为58


怎么可能呢?局部变量的内存不是在其功能之外无法访问吗?

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

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

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
}
是的,如果您尝试取消引用该内存地址,您将有未定义的行为

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
}

您是否在启用Optimizer的情况下编译程序?foo函数非常简单,可能已在生成的代码中内联或替换


<>但是我同意Mark B的结果,行为是不明确的。

C++中,你可以访问任何地址,但并不意味着你应该。您正在访问的地址不再有效。它之所以能工作,是因为在foo返回后,没有其他东西会扰乱内存,但在许多情况下,它可能会崩溃。试着分析你的程序,或者只是编译它,然后…………< P/> < P>你从不通过访问无效的内存抛出C++异常。您只是给出了一个引用任意内存位置的一般思想示例。我也可以这样做:

unsigned int q = 123456;

*(double*)(q) = 1.2;
在这里,我只是把123456当作一个double的地址,然后写信给它。任何事情都可能发生:

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


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

在典型的编译器实现中,您可以将代码视为打印出内存块的值,该内存块的地址过去由。此外,如果向包含本地int的函数添加新函数调用,则a的值或a用于指向的内存地址很有可能发生更改。这是因为堆栈将被包含不同数据的新帧覆盖

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

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

你租了一间旅馆房间。你把一本书放在床头柜最上面的抽屉里,然后睡觉。你第二天早上就退房了,但忘了归还钥匙。你偷了钥匙

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

这怎么可能?如果你还没有租过房间,难道酒店房间抽屉里的东西就无法进入吗

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

酒店管理层无需删除您的预订。你没有和他们签订合同说如果你留下东西,他们会帮你把它撕碎。如果你用偷来的钥匙非法重新进入房间取回钥匙,酒店安全人员不需要抓到你偷偷进入。你没有和他们签过合同,如果我晚些时候试图溜回我的房间,你必须阻止我。相反,你和他们签了一份合同,上面说我保证以后不会偷偷溜进我的房间,这是你违反的合同

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

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

C++不是一种安全的语言。它将愉快地允许你 打破制度规则。如果你试图做一些非法的、愚蠢的事情,比如回到房间里,你就没有被授权进入并翻找一张甚至不在那里的桌子,C++不会阻止你。比C++更安全的语言通过限制你的力量来解决这个问题——例如,对键有更严格的控制。 使现代化 天哪,这个答案得到了很多关注。我不知道为什么——我认为这只是一个有趣的小类比,但不管怎样

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

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

首先是要有某种长寿命的存储区,在这种存储区中,每个字节的生存期——也就是它与某个程序变量有效关联的时间段——无法提前轻松预测。编译器生成对堆管理器的调用,堆管理器知道如何在需要时动态分配存储,在不再需要时回收存储

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

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

由于这个原因,局部变量通常被生成为堆栈数据结构上的存储,因为堆栈具有这样一个属性,即推到堆栈上的第一个对象将是最后弹出的对象

就像酒店决定只按顺序出租房间一样,在所有房间号高于你的人都退房之前,你不能退房

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

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

< >不需要将C++的实现留下给堆栈上未被保留的垃圾,以便以后非法返回。编译器生成的代码将您刚刚腾出的房间中的所有内容都归零是完全合法的。这并不是因为再次强调,这将是昂贵的

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

相反,实现可以让您犯错误,并且可以逍遥法外。大多数时候。直到有一天,真正可怕的事情发生了,整个过程爆发了

这是有问题的。有很多规则,很容易意外地打破它们。我当然有很多次。更糟糕的是,当内存在崩溃发生几十亿纳秒后被检测到被破坏时,问题往往才会浮出水面,当时很难找出是谁把它搞砸了

更多的内存安全语言通过限制您的权限来解决此问题。在普通的C语言中,根本没有办法获取本地地址并返回它或存储它以备将来使用。你可以用当地人的地址,但是语言设计得很巧妙,因此是不可能的 可以在本地生命周期结束后使用它。为了获取本地地址并将其传回,您必须将编译器置于一种特殊的不安全模式,并在程序中使用“不安全”一词,以提醒人们注意,您可能正在做一些危险的事情,可能会违反规则

进一步阅读:

如果C允许返回引用呢?巧合的是,这正是今天博文的主题:

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


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


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

您在这里所做的只是读取和写入到内存中,该内存以前是a的地址。现在您已经在foo之外了,它只是指向某个随机内存区域的指针。恰好在您的示例中,内存区域确实存在,而此时没有其他任何东西在使用它。你不会因为继续使用它而破坏任何东西,其他任何东西都不会覆盖它。因此,5仍然存在。在真实的程序中,该内存几乎会立即被重新使用,这样做会破坏某些东西,尽管症状可能要过很久才会出现

当您从foo返回时,您告诉操作系统您不再使用该内存,并且可以将其重新分配给其他内存。如果你很幸运,而且它从来没有被重新分配过,操作系统也没有发现你再次使用它,那么你就不会说谎了。很有可能你最终会写下其他任何以那个地址结尾的东西

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


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

之所以起作用,是因为自从一个堆栈被放在那里之后,堆栈还没有被修改过。
在再次访问之前调用一些其他函数,这些函数也在调用其他函数,您可能不再那么幸运了-

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

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

返回临时工程的地址,但由于临时工程在功能结束时被销毁,访问临时工程的结果将不确定

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

它可以,因为a是为其scope foo函数的生存期临时分配的变量。从foo返回后,内存是空闲的,可以被覆盖


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

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

正如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,但您的结果可能会有很大差异!。您的指针正在撞击其他不相关的局部变量。

除了所有答案之外,还有一点补充:

如果您这样做:

#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重用。
如果您取消组装可执行文件,您将清楚地看到它。

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

警告:返回了局部变量“a”的地址

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

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

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

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

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


假设一个人把钱藏在某个地方,然后告诉你这个地方。过了一段时间,告诉你钱所在地的人死了。但是你仍然可以使用那些隐藏的钱。

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

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

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

相反,请考虑此示例,并对其进行测试:

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的内存分配到本地函数中 该内存地址在函数过期时仍然有效,不会被任何人删除 内存地址是可信任的,因为内存块不被认为是空闲的,所以在删除之前它不会被覆盖 不使用时应删除内存地址。请参见程序末尾的删除
这取决于语言。在C&C++/Cpp中,是的,从技术上讲,您可以这样做,因为它对任何给定指针是否实际指向某个有效位置的检查非常弱。如果您试图在变量超出范围时访问变量本身,编译器将报告一个错误,但如果您有意将指向该变量位置的指针复制到以后仍在范围内的其他变量,编译器可能不会足够聪明地知道

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


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

我不同意:在cout之前有一个问题*a指向未分配的已释放内存。即使你不去破坏它,它仍然是危险的,而且可能是假的。”艾伦:我澄清了更多的问题的意思,但是没有什么是有效的C++代码。但这是危险的,因为用户很可能犯了错误,并且会做坏事。例如,您可能正在尝试查看堆栈是如何增长的,而您只关心地址值,永远不会取消对它的引用;如果您修复了非格式化业务,gcc仍然会警告返回的局部变量“a”的地址;valgrind显示大小为4[…]的无效写入地址0xbefd7114正好位于堆栈下方ptr@Serge:在我年轻的时候,我曾经在Netware操作系统上运行过一种棘手的零环代码,它涉及到以操作系统不完全认可的方式巧妙地绕过堆栈指针。我知道我什么时候犯了错误,因为堆栈通常会与屏幕内存重叠,我只能看着字节直接写入显示器。这几天你做那种事是逃不掉的。哈哈。在我理解问题所在之前,我需要阅读问题和一些答案。这实际上是关于变量访问范围的问题吗?你甚至不在你的功能之外使用“a”。这就是全部。抛出一些内存引用完全是一个错误
不同于可变范围的主题。重复回答并不意味着重复问题。人们在这里提出的许多重复问题都是完全不同的问题,恰好涉及到相同的潜在症状。。。但发问者知道如何知道,所以他们应该保持开放。我关闭了一个旧的重复,并将其合并到这个问题中,这个问题应该保持开放状态,因为它有一个非常好的答案。@Joel:如果这里的答案是好的,它应该合并到旧的问题中,这是一个重复,而不是相反。这个问题实际上是这里提出的其他问题的翻版,还有一些问题,尽管其中一些问题比其他问题更适合。请注意,我认为Eric的答案是好的。事实上,我把这个问题标记为将答案合并到一个旧问题中,以挽救旧问题。这是我的赌注。优化器转储了函数调用。这不是必需的。因为在foo之后没有调用新函数,所以函数本地堆栈框架还没有被覆盖。在foo之后添加另一个函数调用,5将被更改…我用GCC4.8运行程序,用printf替换cout,包括stdio。正确警告:局部变量“a”的地址返回[-Wreturn local addr]。输出58无优化,输出08有-O3。奇怪的是,P确实有一个地址,即使它的值是0。我希望将NULL 0作为地址。打印出地址为的内存块的值,该地址过去被a占用,但不太正确。这使得他的代码听起来有一些定义明确的含义,但事实并非如此。您是对的,这可能是大多数编译器实现它的方式。@BrennanVincent:当存储被a占用时,指针持有a的地址。尽管该标准不要求实现在其目标生命周期结束后定义地址的行为,但它也承认在某些平台上,UB是以环境特有的文档化方式处理的。虽然局部变量的地址在超出作用域后通常不会有多大用处,但其他一些类型的地址在其各自目标的生存期之后可能仍然有意义。@BrennanVincent:例如,虽然标准可能不要求实现允许将传递给realloc的指针与返回值进行比较,也不允许将指向旧块中地址的指针调整为指向新块,但有些实现会这样做,利用这种特性的代码可能比避免任何操作的代码更有效,甚至包括指向分配给realloc的指针的比较。@muntoo:不幸的是,操作系统在取消启用或释放一页虚拟内存之前并没有发出警告警报。如果你在不再拥有内存的情况下乱搞内存,那么当你触摸一个被释放的页面时,操作系统完全有权关闭整个过程。轰@凯尔:只有安全的酒店才会这么做。不安全的酒店从不必浪费时间在编程键上获得可观的利润。@ CycGuijARRO:C++不是安全的记忆,这是一个简单的事实。这不是在抨击什么。如果我说,C++是一个可怕的杂乱无章的,在一个脆弱的、危险的内存模型之上堆叠的、不太复杂的特征,我感谢每天我不再为它自己的心智工作,那会抨击C++。指出这不是记忆安全就是解释为什么原始海报看到了这个问题;这是在回答问题,而不是发表社论。严格地说,这个类比应该提到,酒店的接待员很高兴你带着钥匙。哦,你介意我带上这把钥匙吗?着手我为什么会在乎?我只在这里工作。在你尝试使用它之前,它并不是非法的。请至少考虑一下一天写一本书。我会买它,即使它只是一个修改和扩大博客帖子的集合,我相信很多人也会买。但是,如果你有一本关于各种编程相关问题的独到见解的书,那将是一本不错的读物。我知道,很难找到时间,但请考虑写一个。我记得从IBM的Turbo C编程的老副本,我曾经玩过一些方式,当如何直接处理图形存储器,以及IBM的文本模式视频存储器的布局,详细描述。当然,代码运行的系统清楚地定义了写入这些地址的含义,因此只要您不担心到其他系统的可移植性,一切都很好。IIRC,指向虚空的指针是那本书的一个共同主题。@Michael Kjörling:当然!人们喜欢偶尔干些脏活;简单但很好的例子来理解底层堆栈理论;在foo中,静态int a=5;
可以用来理解静态变量的范围和生存时间。编译器可能在boo中注册a。它可能会删除它,因为它是不必要的。很有可能*p不是5,但这并不意味着有任何特别好的理由说明它可能是7。这被称为未定义行为!boo为什么以及如何重用foo堆栈?函数堆栈不是彼此分离的吗?我在VisualStudio上运行这段代码时也会收到垃圾2015@ampawd它已经有将近一年的历史了,但是没有,函数堆栈并没有相互分离。上下文有一个堆栈。该上下文使用其堆栈进入main,然后下降到foo,exists,然后下降到boo。Foo和Boo都使用位于同一位置的堆栈指针输入。然而,这并不是应该依赖的行为。其他“东西”,比如中断,或者操作系统可以使用boo和foo调用之间的堆栈,修改它的内容……我现在要写一个程序,继续运行这个程序,这样我就可以中彩票了。你可能是说你可以尝试访问任何地址。因为现在大多数操作系统都不允许任何程序访问任何地址;有大量的安全措施来保护地址空间。这就是为什么不会有另一个LOADLIN.EXE出现。伙计,这是自那以来等待评论时间最长的一次,什么是真相?开玩笑的彼拉多说。也许是酒店抽屉里的吉迪恩的圣经。他们到底怎么了?请注意,他们已经不在了,至少在伦敦。我想根据平等法,你需要一个宗教书籍图书馆。我可以发誓我很久以前就写过,但最近它突然出现,发现我的反应不在那里。现在我必须去弄清楚你上面的暗示,因为我希望我这样做的时候会很开心。弗朗西斯·培根是英国最伟大的散文家之一,一些人怀疑他写过莎士比亚的戏剧,因为他们不能接受一个来自英国的文法学校的孩子,一个格洛弗的儿子,可能是天才。这就是英语课堂体系。耶稣说:“我是真理。”。您是否添加了现有答案尚未涵盖的内容?请不要使用原始指针/new。询问者使用了原始指针。我做了一个例子,它正好反映了他所做的例子,以便让他看到不可信指针和可信指针之间的区别。事实上,还有一个答案和我的类似,但它使用strcpy-wich,IMHO,对于一个新手来说,可能没有我的例子使用new那么清楚。他们没有使用new。你在教他们使用新的。但是你不应该使用new,所以在你看来,把一个地址传递给一个在函数中被破坏的局部变量比实际分配内存要好?这毫无意义。理解分配内存的概念是很重要的,imho,主要是如果你问的是指针,asker没有使用新的,而是使用过的指针。我什么时候说的?不,最好使用智能指针来正确指示被引用资源的所有权。不要在2019年使用new,除非您正在编写库代码,也不要教新手这样做!干杯