Avi*_*ron 990 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;
}
Run Code Online (Sandbox Code Playgroud)
而代码只是运行而没有运行时异常!
输出是 58
怎么会这样?本地变量的内存不能在其功能之外无法访问吗?
Eri*_*ert 4751
怎么会这样?本地变量的内存不能在其功能之外无法访问吗?
你租了一个酒店房间.你把一本书放在床头柜的顶部抽屉里去睡觉.你第二天早上退房,但"忘了"把你的钥匙还给我.你偷了钥匙!
一个星期后,你回到酒店,不要办理登机手续,用偷来的钥匙潜入你的旧房间,然后看看抽屉里.你的书还在那里.惊人!
怎么可能?如果您没有租用房间,是不是酒店房间抽屉的内容无法进入?
好吧,显然这种情况可能发生在现实世界中没问题.当您不再被授权进入房间时,没有神秘的力量会导致您的图书消失.也没有一种神秘的力量阻止你进入一个被盗钥匙的房间.
酒店管理层无需删除您的图书.你没有与他们签订合同,如果你留下东西,他们会为你粉碎它.如果您用偷来的钥匙非法重新进入您的房间以便将其取回,酒店保安人员无需让您偷偷溜进去.您没有与他们签订合同,说"如果我试图潜入我的房间以后,你需要阻止我." 相反,你和他们签了一份合同,上面写着"我保证不会再偷回我的房间",这是你破坏的合同.
在这种情况下,一切都会发生.这本书可以在那里 - 你很幸运.别人的书可以在那里,你的可以在酒店的炉子里.当你进来时,有人可能会在你身边,将你的书撕成碎片.酒店可以完全拆除桌子和书本,并用衣柜取代.整个酒店可能即将被拆除,取而代之的是一个足球场,当你潜行时,你会在爆炸中死去.
你不知道会发生什么; 当你退房并偷了钥匙以后非法使用时,你放弃了生活在一个可预测,安全的世界的权利,因为你选择违反了系统的规则.
C++不是一种安全的语言.它会愉快地让你打破系统的规则.如果你试图做一些非法和愚蠢的事情,比如回到房间,你就没有被授权进入并且通过一张甚至可能不在那里的桌子翻找,C++也不会阻止你.比C++更安全的语言通过限制你的能力来解决这个问题 - 例如,通过对键进行更严格的控制.
圣洁的善良,这个答案得到了很多关注.(我不确定为什么 - 我认为它只是一个"有趣"的小类比,但无论如何.)
我认为通过一些技术性的想法更新这一点可能是密切相关的.
编译器处于生成代码的业务中,该代码管理由该程序操纵的数据的存储.有许多不同的方法来生成代码来管理内存,但随着时间的推移,两种基本技术已经变得根深蒂固.
第一种是拥有某种"长寿命"的存储区域,其中存储中每个字节的"生命周期" - 即与某个程序变量有效关联的时间段 - 无法在前面轻松预测时间 编译器生成对"堆管理器"的调用,该管理器知道如何在需要时动态分配存储,并在不再需要时回收存储.
第二种方法是具有"短期"存储区域,其中每个字节的寿命是众所周知的.在这里,生命周期遵循"嵌套"模式.这些短期变量中寿命最长的变量将在任何其他短期变量之前分配,并将在最后被释放.较短寿命的变量将在最长寿命的变量之后分配,并将在它们之前被释放.这些寿命较短的变量的生命周期是在长寿命变量的生命周期内"嵌套"的.
局部变量遵循后一种模式; 输入方法时,其局部变量变为活动状态.当该方法调用另一个方法时,新方法的局部变量就会生效.在第一个方法的局部变量死亡之前,它们将会死亡.可以提前计算与局部变量相关的存储寿命的开始和结束的相对顺序.
出于这个原因,局部变量通常作为"堆栈"数据结构上的存储生成,因为堆栈具有推送它的第一个东西将是弹出的最后一个东西的属性.
这就像酒店决定只按顺序出租房间,在房间号码高于您的所有人都检查出来之前,您不能退房.
所以让我们考虑一下堆栈.在许多操作系统中,每个线程获得一个堆栈,并且堆栈被分配为特定的固定大小.当你调用一个方法时,东西被推入堆栈.如果你然后从你的方法中传回一个指向堆栈的指针,就像原始海报在这里做的那样,那只是一个指向一些完全有效的百万字节内存块中间的指针.在我们的比喻中,您可以退房; 当你这样做时,你刚刚检查出编号最高的房间.如果没有其他人在您之后办理登机手续,并且您非法回到您的房间,那么您所有的东西都保证在这个特定的酒店仍然存在.
我们将堆栈用于临时商店,因为它们非常便宜且容易.使用堆栈存储本地文件不需要C++的实现; 它可以使用堆.它没有,因为这会使程序变慢.
不需要实现C++就可以保持你在堆栈中留下的垃圾不受影响,这样你就可以非法地回来了.编译器生成的代码在您刚刚腾出的"房间"中变回零是完全合法的.它不是因为那将是昂贵的.
不需要C++的实现来确保当堆栈在逻辑上收缩时,过去有效的地址仍然映射到内存中.允许实现告诉操作系统"我们现在已经完成了使用此页面的堆栈.除非我另有说明,否则发出一个异常,如果有人触及先前有效的堆栈页面,则会破坏该进程".同样,实现实际上并不这样做,因为它很慢且不必要.
相反,实现会让你犯错并逃脱它.大多数时候.直到有一天,真正可怕的事情出现了问题并且这个过程爆炸了.
这是有问题的.有很多规则,很容易意外地打破它们.我当然有很多次.更糟糕的是,这个问题通常只会在腐败发生后检测到内存损坏数十亿纳秒后才会出现,而很难弄清楚是谁弄乱了它.
更多内存安全语言通过限制您的电源来解决此问题.在"普通"C#中,根本无法获取本地的地址并将其返回或存储以供日后使用.您可以获取本地的地址,但语言设计巧妙,因此在本地生命周期结束后无法使用它.为了获取本地的地址并将其传回,您必须将编译器置于特殊的"不安全"模式,并在程序中添加"不安全"一词,以引起注意您可能正在做的事实危险的东西,可能违反规则.
进一步阅读:
如果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/
Ren*_*ena 271
你在这里做的只是读取和写入曾经是地址的内存a.既然你在外面foo,它只是指向一些随机存储区的指针.事实上,在您的示例中,该内存区域确实存在,此刻没有其他任何内容正在使用它.你不会因为继续使用它而破坏任何东西,而其他任何东西都没有覆盖它.因此,5仍然存在.在一个真实的程序中,该内存几乎可以立即重用,你可以通过这样做来破坏某些东西(尽管这些症状可能要到很晚才出现!)
当您返回时foo,您告诉操作系统您不再使用该内存,并且可以将其重新分配给其他内容.如果你很幸运并且它永远不会被重新分配,并且操作系统不会让你再次使用它,那么你就会逃脱谎言.尽管如此,你最终还是会写完最后的那个地址.
现在,如果你想知道编译器为什么不抱怨,那可能是因为foo优化消除了.它通常会警告你这类事情.C假设你知道你正在做什么,从技术上来说你没有违反范围(除了a它之外没有引用foo),只有内存访问规则,它只触发警告而不是错误.
简而言之:这通常不会起作用,但有时会偶然发生.
msw*_*msw 148
因为存储空间还没有被踩到.不要指望那种行为.
Mic*_*ael 80
所有答案的一点点补充:
如果你做那样的事情:
#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);
}
Run Code Online (Sandbox Code Playgroud)
输出可能是:7
这是因为从foo()返回后,堆栈被释放,然后由boo()重用.如果您拆卸可执行文件,您将清楚地看到它.
Ker*_* SB 66
您永远不会通过访问无效内存来抛出C++异常.您只是举例说明引用任意内存位置的一般概念.我可以像这样做:
unsigned int q = 123456;
*(double*)(q) = 1.2;
Run Code Online (Sandbox Code Playgroud)
在这里,我只是将123456视为double的地址并写入它.可能发生任何事情:
q实际上可能真的是双重的有效地址,例如double p; q = &p;.q 可能指向已分配内存中的某处,我只是在那里覆盖8个字节. q 分配内存之外的点,操作系统的内存管理器向我的程序发送分段错误信号,导致运行时终止它. 你设置它的方式是更合理的,返回的地址指向一个有效的内存区域,因为它可能只是在堆栈的下方,但它仍然是一个无法访问的无效位置确定性的时尚.
在正常的程序执行过程中,没有人会自动检查内存地址的语义有效性.但是,像这样的内存调试器valgrind会很乐意这样做,因此您应该通过它运行程序并见证错误.
gas*_*ush 28
您是否在启用优化器的情况下编译程序?
foo()函数非常简单,可能已在结果代码中内联/替换.
但我与马克B签约,结果是未定义的行为.
Cha*_*eng 22
您的问题与范围无关.在您显示的代码中,该函数main没有在函数中看到名称foo,因此您无法a在外部使用此名称直接访问foo foo.
您遇到的问题是程序在引用非法内存时没有发出错误信号的原因.这是因为C++标准没有在非法内存和合法内存之间指定非常清晰的边界.在弹出堆栈中引用某些内容有时会导致错误,有时则不会.这取决于.不要指望这种行为.假设它在编程时总是会导致错误,但是假设它在调试时永远不会发出错误信号.
Bri*_*ndy 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
}
Run Code Online (Sandbox Code Playgroud)
sam*_*sam 17
注意所有警告.不仅要解决错误.
GCC显示此警告
警告:返回的局部变量'a'的地址
这是C++的强大功能.你应该关心记忆.使用该-Werror标志,此警告就会出错,现在您必须对其进行调试.
AHe*_*lps 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;
}
Run Code Online (Sandbox Code Playgroud)
打印出"y = 123",但结果可能会有所不同(真的!).你的指针正在破坏其他不相关的局部变量.
Ale*_*ler 15
您实际上调用了未定义的行为.
返回临时作品的地址,但随着临时工作在函数末尾被销毁,访问它们的结果将是不确定的.
所以你没有修改a,而是a曾经的内存位置.这种差异非常类似于崩溃和不崩溃之间的区别.
lar*_*moa 13
在典型的编译器实现中,您可以将代码视为" 使用以前占用的地址打印出内存块的值".此外,如果向一个维护本地的函数添加一个新的函数调用int,那么a(或者a用于指向的内存地址)的值很可能会发生变化.发生这种情况是因为堆栈将被包含不同数据的新帧覆盖.
但是,这是未定义的行为,你不应该依赖它来工作!
小智 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);
}
Run Code Online (Sandbox Code Playgroud)
这是使用内存地址的“肮脏”方式。当您返回一个地址(指针)时,您不知道它是否属于函数的本地范围。这只是一个地址。
现在您调用了“foo”函数,“a”的地址(内存位置)已经分配在应用程序(进程)的(至少目前是安全的)可寻址内存中。
在“foo”函数返回之后,“a”的地址可以被认为是“脏”的,但它就在那里,没有被清理,也没有被程序其他部分中的表达式干扰/修改(至少在这个特定情况下)。
AC/C++ 编译器不会阻止您进行此类“脏”访问(不过,如果您关心的话,它可能会警告您)。您可以安全地使用(更新)程序实例(进程)数据段中的任何内存位置,除非您通过某种方式保护该地址。
小智 5
从函数返回后,所有标识符都被销毁,而不是将值保存在内存位置,如果没有标识符,我们就无法定位这些值。但该位置仍然包含前一个函数存储的值。
所以,这里的函数foo()是返回的地址,a并a在返回其地址后被销毁。您可以通过返回的地址访问修改后的值。
让我举一个现实世界的例子:
假设一个人把钱藏在一个地方,然后告诉你位置。过了一段时间,告诉你钱位置的人死了。但是你仍然可以使用那些隐藏的钱。