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