我有以下代码。
<code>#include <iostream> int * foo() { int a = 5; return &a; } int main() { int* p = foo(); std::cout << *p; *p = 8; std::cout << *p; }
而且代码没有任何运行时异常就可以运行!
输出为58
怎么可能? 局部变量的存储不是在其功能之外不可访问的吗?
#1楼
因为存储空间尚未增加。 不要指望这种行为。
#2楼
您只是返回了一个内存地址,这是允许的,但可能是错误。
是的,如果您尝试取消引用该内存地址,则将具有未定义的行为。
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
}
#3楼
注意所有警告。 不仅解决错误。
GCC显示此警告
警告:返回了本地变量“ a”的地址
这是C ++的强大功能。 您应该关心内存。 使用-Werror
标志,此警告-Werror
错误,现在您必须对其进行调试。
#4楼
这是使用内存地址的“肮脏”方式。 返回地址(指针)时,您不知道该地址是否属于函数的本地范围。 这只是一个地址。 既然您已经调用了'foo'函数,那么'a'的地址(内存位置)已经被分配到了应用程序(进程)的(至少现在是安全的)可寻址内存中。 返回'foo'函数后,'a'的地址可以被认为是'dirty',但是它在那里,没有被清理,也没有被程序其他部分的表达式所干扰/修改(至少在这种情况下)。 AC / C ++编译器不会阻止您进行此类“肮脏”访问(但是,如果您愿意的话,可能会警告您)。 除非通过某种方式保护地址,否则您可以安全地使用(更新)程序实例(进程)的数据段中的任何内存位置。
#5楼
从函数返回后,所有标识符都会被破坏,而不是将值保留在内存位置中,并且如果没有标识符,我们将无法定位这些值。但是该位置仍然包含先前函数存储的值。
所以,在这里发挥作用foo()
的返回地址a
和a
返回它的地址后,被破坏。 您可以通过该返回地址访问修改后的值。
让我以现实世界为例:
假设某人将钱藏在某个地点,然后告诉您该地点。 一段时间后,告诉您钱款所在地的那个人去世了。 但是您仍然可以使用这些隐藏的资金。
#6楼
您的代码很有风险。 您正在创建一个局部变量(在函数结束后将其视为破坏),并且在对该变量进行存储之后返回该变量的内存地址。
这意味着内存地址可能有效或无效,并且您的代码将很容易受到可能的内存地址问题(例如分段错误)的影响。
这意味着您正在做一件非常糟糕的事情,因为您根本无法信任将内存地址传递给指针的指针。
请考虑以下示例,并对其进行测试:
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分配内存到局部函数
- 当功能到期时,该内存地址也仍然有效(任何人都不会删除它)
- 内存地址是可信任的(该内存块不被认为是空闲的,因此在删除之前不会覆盖它)
- 不使用时,应删除内存地址。 (请参阅程序末尾的删除)
#7楼
您是否在启用优化程序的情况下编译了程序? foo()
函数非常简单,可能已内联或替换为结果代码。
但是我同意Mark B的观点,即行为是不确定的。
#8楼
在C ++中,您可以访问任何地址,但这并不意味着您应该 。 您正在访问的地址不再有效。 之所以起作用,是因为在foo返回之后,没有其他事情扰乱了内存,但是在许多情况下它可能崩溃。 尝试使用Valgrind分析您的程序,甚至只是对其进行优化编译,然后查看...
#9楼
您永远不会通过访问无效的内存而引发C ++异常。 您只是在举例说明引用任意内存位置的一般思路。 我可以这样做:
unsigned int q = 123456;
*(double*)(q) = 1.2;
在这里,我只是将123456当作double的地址并写入它。 可能发生任何事情:
-
q
实际上可能确实是双精度数(例如,double p; q = &p;
的有效地址double p; q = &p;
double p; q = &p;
。 -
q
可能指向分配的内存中的某个地方,而我只覆盖了其中的8个字节。 -
q
指向分配的内存之外,操作系统的内存管理器将分段错误信号发送到我的程序,从而导致运行时终止该程序。 - 您中奖了。
设置方式将返回的地址指向内存的有效区域更为合理,因为它可能位于堆栈的更下方,但它仍然是无效的位置,您无法在其中访问确定性时尚。
在正常程序执行期间,没有人会像您一样自动检查内存地址的语义有效性。 但是,内存调试器(例如valgrind
会很乐意这样做,因此您应该通过它运行程序并见证错误。
#10楼
在典型的编译器实现中,您可以将代码视为“ 用以前被a占用的地址打印出内存块的值”。 另外,如果你添加一个新的函数调用的函数,constains本地int
这是一个很好的机会,价值a
(或存储器地址a
用来点)的变化。 发生这种情况是因为堆栈将被包含不同数据的新帧覆盖。
但是,这是未定义的行为,您不应依靠它来工作!
#11楼
怎么可能? 局部变量的存储不是在其功能之外不可访问的吗?
您租了旅馆房间。 您将一本书放在床头柜的顶部抽屉中,然后入睡。 您第二天早上退房,但是“忘记了”退还您的钥匙。 您偷了钥匙!
一周后,您返回酒店,不办理入住手续,用偷来的钥匙偷偷进入旧房间,然后看向抽屉。 你的书还在那里。 惊人!
怎么可能? 如果您没有租房,不是不是无法进入酒店房间抽屉的内容吗?
好吧,显然,这种情况可以在现实世界中发生,没有问题。 当您不再被授权进入房间时,没有任何神秘的力量会使您的书消失。 也没有一种神秘的力量阻止您使用失窃的钥匙进入房间。
不需要酒店管理人员删除您的书。 您没有与他们签订合同,说如果您留下东西,他们会为您切碎。 如果您使用偷来的钥匙非法重新进入房间以将其取回,则无需酒店安全人员抓到您潜行。您没有与他们订立合同,说“如果我尝试潜入我的房间,房间过后,您必须阻止我。” 相反,您与他们签订了一份合同,上面写着“我保证以后不会再潜入我的房间”,这是您违反的合同。
在这种情况下, 任何事情都可能发生 。 这本书可以在那里-您很幸运。 可能有人的书在那里,而您的书可能在酒店的炉子里。 当您进来时,有人可能会在那里,将您的书撕成碎片。 该酒店本可以删除桌子并完全预订,然后用衣柜代替。 整个酒店可能会被拆毁,取而代之的是一个足球场,而当您潜行时,您将在爆炸中丧生。
您不知道会发生什么; 当您退房并偷走了以后非法使用的钥匙时,您放弃了生活在可预测的安全世界中的权利,因为您选择了违反系统规则。
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/
#12楼
您的问题与范围无关。 在你展示的代码,该功能main
不看在功能名称foo
,所以你不能访问a
foo中使用该名称外直接foo
。
您遇到的问题是,为什么程序在引用非法内存时没有发出错误信号。 这是因为C ++标准没有在非法内存和合法内存之间指定非常明确的界限。 引用弹出堆栈中的某些内容有时会导致错误,有时则不会。 这取决于。 不要指望这种行为。 假定在编程时它总是会导致错误,但是假定在调试时它永远不会提示错误。
#13楼
be the address of a
. 您在这里所做的只是读取和写入是a
地址的内存。 现在您不在foo
之外,它只是指向某个随机内存区域的指针。 碰巧的是,在您的示例中,该内存区域确实存在,并且目前没有其他人在使用它。 您不会通过继续使用而破坏任何东西,并且还没有其他东西覆盖它。 因此, 5
仍然存在。 在实际的程序中,该内存将几乎立即被重新使用,并且这样做会破坏某些功能(尽管症状可能要等到很久以后才会出现!)
从foo
返回时,您告诉操作系统您不再使用该内存,可以将其重新分配给其他内存。 如果您很幸运,但是它从未被重新分配,并且操作系统没有抓住您再次使用它的机会,那么您将可以摆脱谎言。 尽管您最终可能会写满以该地址结尾的其他内容,但很有可能。
现在,如果您想知道为什么编译器没有抱怨,那可能是因为foo
被优化消除了。 通常会警告您这种事情。 ç假设你知道你在做什么,虽然,在技术上还没有在这里侵犯范围(有没有引用a
本身之外的foo
),只读存储器访问规则,仅触发警告而不是错误。
简而言之:这通常不起作用,但有时会偶然。
#14楼
之所以起作用,是因为自将a放入堆栈以来,堆栈尚未更改(但尚未更改)。 访问之前,调用一些其他的功能(这还呼吁其他功能) a
又和你可能不会这么幸运了...... ;-)
#15楼
如果使用:: 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);
}
#16楼
您实际上调用了未定义的行为。
返回临时作品的地址,但是由于临时元素在函数末尾被销毁,因此访问它们的结果将是不确定的。
因此,您没有修改a
而是修改a
一次的存储位置。 这种差异与崩溃与不崩溃之间的差异非常相似。
#17楼
可以这样,因为a
是在其作用域的生命周期内临时分配的变量( foo
函数)。 从foo
返回后,内存是可用的,可以被覆盖。
您所做的事情被描述为未定义的行为 。 结果无法预测。
#18楼
这是经典的未定义行为 ,前两天在这里进行了讨论-在站点周围进行搜索。 简而言之,您很幸运,但是任何事情都可能发生,并且您的代码使对内存的访问无效。
#19楼
正如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”,但是您的结果可能会有所不同(真的!)。 您的指针正在破坏其他不相关的局部变量。
#20楼
对所有答案的补充:
如果您这样做:
#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()重用。 如果您对可执行文件进行反汇编,则会清楚看到它。