我有以下代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <iostream>
int * foo()
{
int a = 5;
return &a;
}
int main()
{
int* p = foo();
std::cout << *p;
*p = 8;
std::cout << *p;
} |
而代码只是在运行,没有运行时异常!
输出为58。
怎么可能呢?局部变量的内存在其函数之外是否不可访问?
- 这甚至不会按原样编译;如果您修复非成形业务,GCC仍会警告address of local variable ‘a’ returned;Valgrind显示Invalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr。
- 在某些平台/编译器(尤其是DOS的旧编译器)中,您甚至可以通过空指针进行写入,在覆盖某些重要内容(如正在执行的代码)之前,一切都正常。:)
- @这是因为现在大多数操作系统都有写保护的零页,但不是所有操作系统都有写保护的零页!
- @谢尔盖:在我年轻的时候,我曾经研究过一些复杂的零环代码,这些代码运行在Netware操作系统上,涉及到巧妙地围绕堆栈指针移动,而这种方式并不完全受操作系统的限制。我知道我什么时候犯了错误,因为通常堆栈最终会与屏幕内存重叠,我只能看着字节被直接写到显示器上。你这些天没法摆脱这种事。
- 啊,这让我怀念我的C++/DCOM/VB天。我们有一个自产的红黑树,有无效的指针访问问题。我非常乐意调试它。
- @JasperBekkers:"这是因为现在大多数操作系统都有写保护的零页,但并非所有操作系统都有写保护!"赞成。我知道。
- @ Xeo——并不是每个人都是C++中的超级巫师。我们中的一些人仍然有一些需要学习的东西,特别是当从C语言和Java等"更安全"的语言中加入C++时。所以,除非你真的打算在这里教我一些东西,请把你的评论留给我。
- @埃里克:提醒我:j00ru.vexillium.org/?P=781
- @我想你误解了我…我知道这是不安全的,这是肯定的!我以为这是不可能的。我想我应该习惯C++给开发者带来的自由。
- 哈哈。我需要先读一下这个问题和一些答案,然后才知道问题在哪里。这实际上是一个关于变量访问范围的问题吗?你甚至不会在你的功能之外使用"A"。这就是一切。抛出一些内存引用与变量范围完全不同。
- @托马拉克请提供一个dupe链接,我很高兴投票支持close。我们可以请一位主持人把这个问题和这个问题合并起来。
- 重复答案并不意味着重复问题。人们在这里提出的许多重复的问题都是完全不同的问题,恰好指的是相同的潜在症状…但发问者知道如何知道,所以他们应该保持开放。我结束了一个旧的重复,把它合并到这个问题中,这个问题应该保持开放,因为它有一个非常好的答案。
- @乔尔:如果这里的答案是好的,它应该被合并成旧的问题,其中这是一个重复,而不是相反的方式。这个问题确实是这里提出的其他问题的翻版,然后是一些(尽管有些提出的问题比其他问题更适合)。请注意,我认为埃里克的回答很好。(事实上,我把这个问题标记为将答案合并成一个旧问题,以便挽救旧问题。)
- @Joel Dupe的意思是(引述)"这个问题和之前关于这个主题的问题包含完全相同的基础;",而不是"这个问题和这个主题的新问题包含完全相同的基础;"。无论是合并还是"关闭"弹出窗口都是向后的。
- 但这样人们就不必手动点击前进链接了…所以这可能是个好主意。但合并仍然是倒退的。试图通过说这是正确的方法来证明是不可行的。
- 这个奇怪的问题有这么多的爱,我曾经认为C开发人员必须理解硬件是如何工作的,堆栈分配永远是一样的。
- @maxpm,8086上的零页(也有0000:0000)有它的用途——中断向量等,所以寻址是很正常的。回到过去,病毒(和反病毒)经常覆盖相当多的病毒。
- @阿夫拉罕姆舒克,没有什么不可能的,除了比光速更快的旅行(而且这不是绝对确定的):)
- 所以内存被覆盖。否则你会得到"55"
- 我的意思是在我退出函数foo之后它不会被覆盖。即使局部变量被破坏,我也可以输出它。
- 这行是加在后面的,我只是忘记写"编辑",有一个问题的结果-2。当我开始这个问题的时候,我不知道这个问题已经被问到了。但当我找到它时,我用它编辑了一个问题,我发现了一个可疑的问题,并立即重新记录了我的问题。所以我不承担,为什么你会减去我。
- 它会导致分割错误吗?
- @埃里克利珀特,"你这些天不能摆脱这种事情",为什么不呢?
- @边界分解,所以伟大的答案应该是不带疑问的?或者你是说它应该写在另一个域名上?
How can it be? Isn't the memory of a local variable inaccessible outside its function?
Ok.
你租了一间酒店房间。你把一本书放在床头柜最上面的抽屉里,然后去睡觉。你第二天早上退房,但"忘了"把钥匙还给我。你偷了钥匙!好的。
一周后,你回到酒店,不办理入住手续,带着偷来的钥匙潜入你的旧房间,然后看看抽屉。你的书还在那里。令人吃惊的!好的。
怎么会这样?如果你还没有租过房间,酒店房间抽屉里的东西难道就不可进入吗?好的。
很明显,这种情况在现实世界中是可以发生的,没问题。当你不再被授权在房间里的时候,没有神秘的力量能让你的书消失。也没有神秘的力量阻止你带着偷来的钥匙进入房间。好的。
酒店管理层不需要删除您的预订。你没有和他们签订合同说如果你把东西留下,他们会帮你把它撕碎。如果你非法带着偷来的钥匙重新进入你的房间,酒店的保安人员不需要抓住你的潜入。你没有和他们签订合同,合同上说"如果我以后再溜回我的房间,你就必须阻止我。"相反,你和他们签订了合同,合同上说"我保证以后不会溜回我的房间",这是你违反的合同。好的。
在这种情况下,任何事情都可能发生。这本书可以在那里——你很幸运。别人的书可以在那里,你的书可以在酒店的火炉里。你进来的时候可能有人在那儿,把你的书撕成碎片。旅馆本可以把桌子和书全部搬走,换上衣柜。整个酒店可能即将被拆毁,取而代之的是一个足球场,当你偷偷摸摸的时候,你会死于爆炸。好的。
你不知道会发生什么;当你退房以后偷了一把钥匙非法使用时,你就放弃了在一个可预测的安全世界里生活的权利,因为你选择了打破这个系统的规则。好的。
C++不是一种安全的语言。它会让你愉快地打破系统的规则。如果你试图做一些非法的、愚蠢的事情,比如回到房间里,你没有被授权进入并翻找一张甚至不在那里的桌子,C++就不会阻止你。比C++更安全的语言通过限制你的力量来解决这个问题,例如,对键有更严格的控制。好的。更新
天啊,这个答案引起了很多人的注意。(我不知道为什么)我认为这只是一个"有趣"的小比喻,但不管怎样。好的。
我认为用更多的技术思想来更新这一点可能是正确的。好的。
编译器的工作是生成代码,该代码管理由该程序操作的数据的存储。有很多不同的生成代码来管理内存的方法,但随着时间的推移,两种基本技术已经变得根深蒂固。好的。
第一种是拥有某种"长期"存储区域,在这种存储区域中,每个字节在存储器中的"生存期"(即与某个程序变量有效关联的时间段)不容易提前预测。编译器生成对"堆管理器"的调用,它知道如何在需要时动态分配存储,在不再需要时回收存储。好的。
第二种方法是有一个"短命"的存储区域,每个字节的寿命是众所周知的。在这里,生活遵循一个"嵌套"模式。这些短寿命变量的最长寿命将在任何其他短期变量之前分配,最后将被释放。寿命较短的变量将在最长的生命周期之后分配,并将在它们之前释放。这些寿命较短的变量的寿命在较长寿命的生命周期内是"嵌套的"。好的。
局部变量遵循后一种模式;当一个方法被输入时,它的局部变量就会活跃起来。当该方法调用另一个方法时,新方法的局部变量将激活。在第一个方法的局部变量死之前,它们就死了。与局部变量相关的存储器的生存期的开始和结束的相对顺序可以提前计算出来。好的。
由于这个原因,局部变量通常被生成为"stack"数据结构上的存储,因为一个堆栈有一个属性,第一个推到它上面的东西将是最后一个弹出的东西。好的。
就像酒店决定只按顺序出租房间一样,除非每个房间的房间号都高于您的退房号,否则您无法退房。好的。
那么让我们考虑一下这个堆栈。在许多操作系统中,每个线程有一个栈,栈被分配为一定的固定大小。当你调用一个方法时,东西被推到堆栈上。然后,如果您将指向堆栈的指针从方法中返回,就像原始海报在这里所做的那样,这只是指向某个完全有效的百万字节内存块中间的指针。在我们的类比中,您是从酒店退房的;当您退房时,您只是从编号最高的入住房间退房。如果没有其他人在你之后登记,而你非法回到你的房间,你所有的东西都保证仍然在这个特定的酒店。好的。
我们使用临时商店的堆栈,因为它们非常便宜和容易。不需要使用C++来实现本地存储的堆栈;它可以使用堆。不会,因为那样会使程序变慢。好的。
C++的实现不需要将未被保留的堆栈中的垃圾留下,以便以后可以非法返回;对于编译器来说,生成代码是完全合法的,在您刚刚腾出的"房间"中,所有的代码都会变回零。这并不是因为,那会很贵。好的。
不需要实现C++,以确保栈在逻辑上收缩时,仍然有效的地址仍然映射到内存中。允许实现告诉操作系统"我们已经使用堆栈的这个页面完成了"。除非我另有说明,否则如果有人触摸以前有效的堆栈页,则会发出一个异常来破坏进程。同样,实现实际上并不是这样做的,因为它是缓慢的和不必要的。好的。
相反,实现可以让你犯错误,并摆脱它。大部分时间。直到有一天真正糟糕的事情发生了,过程就爆炸了。好的。
这是有问题的。有很多规则,很容易被意外打破。我当然有很多次。更糟糕的是,只有在检测到内存在崩溃发生数十亿纳秒后被破坏时,这个问题才会出现,因为很难找出是谁把它弄糟了。好的。
更多的记忆安全语言通过限制你的能力来解决这个问题。在"普通"C中,根本没有办法获取本地地址并将其返回或存储以备以后使用。你可以取一个本地人的地址,但是语言设计得很巧妙,这样在本地人的生命周期结束后就不可能使用它了。为了获取本地地址并将其传递回去,您必须将编译器置于特殊的"不安全"模式,并在程序中使用"不安全"一词,以引起注意,您可能正在做一些可能违反规则的危险事情。好的。
进一步阅读:好的。
如果C允许返回引用怎么办?巧合的是,今天的博客主题是:好的。
http://blogs.msdn.com/b/ericlippet/archive/2011/06/23/ref-returns-and-ref-locals.aspx好的。
为什么我们要使用堆栈来管理内存?C中的值类型是否总是存储在堆栈中?虚拟内存是如何工作的?以及更多关于C内存管理器如何工作的主题。这些文章中的许多也与C++程序员有密切关系:好的。
https://blogs.msdn.microsoft.com/ericlippert/tag/memory-management/好的。
好啊。
- 如果旅馆即将被一个足球场所取代,你难道没有注意到缺少人吗?还是外面那群巨大的推土机?
- @蒙托奥:不幸的是,操作系统在关闭或解除分配一页虚拟内存之前,不会发出警报。如果你在不拥有内存的情况下到处乱搞,那么操作系统完全有权在触摸解除分配的页面时关闭整个进程。繁荣!
- 我喜欢这个比喻,但几乎所有的酒店都使用可编程的钥匙卡,在指定的时间被锁在外面,或者在为那个房间发出新的钥匙时,以先到者为准。我想,很少有不使用这种系统的酒店会坚持要求您在结账时归还钥匙。
- 这是一个很好的类比,但是在最后抨击C++是不行的。C++没有施加太多的限制,但是缺乏限制通常会在可测量的性能收益中得到回报。
- @凯尔:只有安全的酒店才会这么做。不安全的酒店不必浪费时间在编程键上就能获得可观的利润。
- 我不认为他是在抨击C++。C++是不安全的,正如你所说的,在很多情况下,这是一件好事。同样,更安全的语言也不那么强大,但更容易使用。它们只是不同而已。
- @ CycGuijARRO:C++不是内存安全,这是一个简单的事实。这不是"抨击"任何东西。例如,我说过,"C++是一个可怕的杂乱无章的、过于复杂的特征,堆在一个易碎、危险的内存模型之上,我感谢每天我不再为它自己的心智而工作",这将抨击C++。指出这是不安全的记忆解释了为什么原来的海报看到这个问题;它回答的是问题,而不是社论。
- @埃里克:C(真的,.net)在这方面也不"安全"。我可以将Math.Random、IntPtr和Marshal.Copy组合起来,造成完全混乱(不需要unsafe关键字或/unsafe编译器开关)。安全源于遵守合同,而不是语言设计(尽管语言可以而且应该以尽可能容易遵守合同的方式进行编码,并在违反合同时提供警告。)
- 很好的解释,埃里克。快速提问!你认为哪种语言更安全?!
- @位图:徽标很安全。
- @Ben Well-duh,显然有一些方法会变得不安全,其中包括标记为不安全的库函数(因此,如果需要的话,权限会生效)。如果有人用一个允许intptr道德等价物的库函数执行了一个徽标,那么你的度量标准也会使它不再安全。
- 严格地说,这个比喻应该提到,酒店的接待员很乐意让你随身携带钥匙。"哦,你介意我带着这把钥匙吗?"前进。为什么我会在意?我只在这里工作。除非你尝试使用它,否则它不会成为非法的。
- @菲尔纳什:那里有点坏,因为钥匙通常是酒店的财产。
- @本:@shuggycouk是对的;如果你误用了一些库函数,它们会做一些可怕的事情,这是这些库函数的属性,而不是C语言。C如果您没有"不安全"的代码块,那么该语言是内存安全的,也是类型安全的。如果你这么做了,那么它就和C++一样都是不安全的。重点是将不安全的内存区域隔离到易于识别和彻底检查的区域。
- 这意味着圣经是一个静止的物体?我是说,它从不离开房间。
- @凯尔·克罗宁,你的观点只是进一步的类比。当C++被发明时,酒店的可编程卡钥匙不太常见,甚至不存在。较新的酒店和较新的语言一样,自然也采用了更安全的做法。甚至更老的酒店都改装了新的锁,如C++(智能指针有人吗?)
- C++不是内存安全,它是实用的。可以使用一些技巧,如果C++过于安全,那些黑客就不会存在。
- @泰迪:首先,有很多语用语言是安全的。然而,C++的问题并不在于它不安全。问题是,很容易不小心做了一些严重不安全的事情,直到你把最终用户的机器撞坏后才意识到你这样做了。内存不安全的语言通常是非常有用的,我同意,但是应该有一种方法来隔离这种不安全性,特别是那些"棘手的,黑客的"代码位,真正需要它。
- 埃里克:这个问题可能是因为它是黑客新闻的头条:news.ycombinator.com/item?ID=2686580。不管怎样,在24小时内有1100票上涨?!到目前为止,那一定是一个记录。
- 拜托,请至少有一天考虑写一本书。我会买它,即使它只是一个修订和扩展的博客文章的集合,我相信很多人也会。但是,一本关于各种编程相关问题的原创性思想的书将会是一本伟大的书。我知道很难找到时间,但请考虑写一篇。
- @谢谢你的好话。我已经写了几本书,我很清楚这本书的工作量有多大!我考虑过把博客变成一本书,如果我能同时找到时间和一个愿意的出版商,我可能在某个时候找到。
- 实际上,C和C++中有三种基本的内存管理技术。您提到的两个变量加上static内存,其中变量具有进程生存期。如果您不介意获得非常技术性的文件存储,也有注册文件存储,但这显然被当前的编译器忽略了。
- 所以,每次我潜入同一个房间,找到那本书的频率到底是多少?另外,这个频率依赖于什么因素?
- @选择用科学回答你的问题。找几百个C编译器,尝试每种编译器的几百种不同配置,很快你就会有很好的经验数据。还有什么是猜测。
- 我用很多不同的语言编写了很多代码。我最不喜欢的语言是C++。我对不安全的语言没有问题——我遇到的问题是,语言设计得太差(然后被黑客攻击以掩盖这些设计缺陷),以至于很容易创建代码来区分错误。我现在正在处理一个应该用来修复内存泄漏的问题。现在它变成了断层。乐趣。-
- 同样的方法是否也适用于同一个函数中声明在不同范围内的局部变量?我在问,因为根据我的经验,gcc和msvc都不警告(即使使用-wextra)在不同的范围内使用指向变量的指针,以及b)创建程序集,建议他们通过指针跟踪每个变量的使用情况,甚至超出变量的范围。示例:void foo(void)int i,*px;for(i=0;i<10;i+=*px)int x=i+1;px=&;x;printf("*px=%i",*px);
- @蒂莫,你必须永远不要使用一个地址到一个本地人的生命已经结束。如果您这样做了,并且它碰巧工作,那么,再次声明,当您违反规则时,运行时不需要失败。由于某种原因,不安全代码被标记为不安全。
- @埃里克利珀特,我认为这是我见过的最好的类比,关于这个话题,但我有一个困惑,你写道,"你的可能在酒店的熔炉里",这意味着我的价值可能在系统中的其他地方,或者你试图解释的其他地方?
- @Vikasverma:有些内存管理器故意在内存不再可用时将其切碎。例如,Microsoft C Runtime的调试版本将未使用的内存设置为0xCC,因为(1)很容易在调试器内存窗口中看到特定的内存块现在不再有效,(2)这是"闯入调试器"指令代码;如果曾经执行过拆分的内存,则调试器将激活。
- 事实上,我不同意这一说法,因为在发送链接到它之后,它没有回答海报混乱的明确原因。他清楚地认为,与对象类似,函数包含自己的本地存储,因此在函数被"销毁"后不存在。只是它没有被真正摧毁。与类不同,它们不是用于变量的容器,而是存储在堆栈或寄存器中的项。然而,这个答案是一个很好的类比,说明如何操作堆栈的访问可以(也不能)工作。不过,技术上的答案更为重要。
- @德吉:你的精神力量比我强得多,我不知道原来海报在想什么。
- @erriclippert这不是精神力量,而是更熟悉困惑,他使用的例子和他问的实际问题。他问记忆是否无法接近,这意味着他可能认为记忆已经不存在了。两者都是不真实的,内存是可访问的,并且确实存在。原因是它们的存储方式不同,这就是为什么我认为"答案"应该把重点放在技术层面上。
- @这意味着你的抽屉正在重建。
- @埃里克:谢谢你的回答。你对C++ 11和C++ 14使用智能指针的免费商店管理方式有什么看法?现在我可以说,现代C++是安全的语言,因为不需要使用删除操作符。
- Access:我并不是一个专家,对C++和11中添加了什么,尽管在与专家的交谈中,我觉得里面有很多好东西。更一般地说,我很高兴看到C++委员会愿意大胆而主动地把语言推向更现代、更不容易出错的地方。
- @艾瑞克:好的。但问题是,如果我做了愚蠢的事情,为什么C++不会阻止我?当我试图获取局部变量的地址时,如果编译器给了我错误,这不是很好吗?为什么C++给程序员提供了这么多的自由?或者这些是C++继承的问题?感谢您的帮助。
- MET:你应该问一些C++设计人员的问题,我不想猜测C++语言设计者的动机。我会注意到,"阻止用户做一些愚蠢的事情"似乎并没有在C的设计者认为值得钦佩的特性列表中占据太高的位置。
- 记住,C++是一种通用的语言。对于广泛的应用程序(破解工具),需要完全的内存控制。
- @ EricLippert:C++没有定义这种行为,是真的。但我认为,如果您在x86上运行,那么该体系结构保证您可以安全地在堆栈指针(esp上)上写入和读取多达128个字节,而不会冒内存更改的风险。现在,如果编译器不编译任何主动修改该内存的指令(可能是这样的,因为它只会增加esp并在离开函数时返回),我认为从技术上讲,在x86上,这是定义的行为。这是真的吗?有什么想法吗?
- @martijncodeaux:谁说编译器需要使用esp来确定局部变量的位置?如果特定的编译器供应商为特定的实现定义了行为,那么该行为就是实现定义的。
- @埃里克:我不知道你要去哪里。即使局部变量现在位于stackpointer(&var < esp上),在特定的体系结构/编译器组合上,这是否会导致一致的行为?例如:我用gcc-o0尝试了x86_64,它生成的代码我认为会产生一致的结果。
- @玛蒂:未定义的行为可以做任何事情。一致的行为是任何事物的子集,所以是的,这是可能的。我要说的是:您要问的是,由特定实现定义的行为是否是一种实现定义的行为。是的,是的。
- 在更新之前的最后一段中,你说了一些"C++不是一种安全的语言…………更安全的语言,比如C++ +……",你的意思是说C语言更安全吗?
- 如果答案至少提到过"未定义行为"这个词,那就太好了。
- @Giorgimoniava:评论指出。考虑写一个你更喜欢的答案,这样整个网站都会得到改进。
- @埃里克利珀特,你的回答已经很好了,我不想批评它,只是写了我的意见。谢谢。在这里写一些比当前答案更好的东西并不容易/现实。
- 我必须同意@dyppl,我想读一本你写的书。除了Jooq工作人员或Josh Bloch/Goetz撰写的一些博客文章/答案之外,您的答案还提供了一个非常详细且易于理解的关于幕后/幕后编程语言细节的材料。
你在这里所做的只是简单地读写记忆,它曾经是a的地址。既然您不在foo中,它只是指向某个随机内存区域的指针。在您的示例中,内存区域确实存在,而目前没有其他任何东西在使用它。你不会因为继续使用而破坏任何东西,而且还没有其他东西覆盖它。因此,5仍然存在。在一个真正的程序中,该内存几乎会立即被重新使用,这样做会破坏某些东西(尽管这些症状直到很晚才会出现!)
当您从foo返回时,您告诉操作系统您不再使用该内存,它可以重新分配给其他对象。如果你很幸运,它永远不会被重新分配,操作系统也不会再发现你在使用它,那么你就可以摆脱谎言了。很有可能你最终会把地址写下来。
现在,如果您想知道编译器为什么不抱怨,可能是因为优化消除了foo。它通常会警告你这类事情。C假设你知道你在做什么,技术上你没有违反这里的范围(在foo之外没有提到a本身),只有内存访问规则,它只触发一个警告而不是一个错误。
简而言之:这通常不起作用,但有时是偶然的。
因为存储空间还没有被踩上去。别指望那种行为。
- 很明显,我很喜欢埃里克的回答,我不仅仅是因为这个原因而显得过分,而且这个答案有简洁和正确的优点。
- 伙计,这是自"真相是什么"以来最长的等待评论的时间了。彼拉多开玩笑说:"也许这是吉迪恩在酒店抽屉里的圣经。不管怎样,他们怎么了?注意他们已经不在了,至少在伦敦。我想根据平等法,你需要一个宗教教义图书馆。
- 我本可以发誓很久以前就写了这篇文章,但它最近突然出现,发现我的回答不在那里。现在我得去想一下你上面的暗示了,我想当我这样做的时候我会很开心的。<
- 哈哈。弗朗西斯·培根,英国最伟大的散文家之一,一些人怀疑他写过莎士比亚的戏剧,因为他们不能接受一个来自英国的文法学校的孩子,格洛弗的儿子,可能是个天才。这就是英语课堂体系。耶稣说,我是真理。俄勒冈州立大学.edu/instruct/phl302/texts/bacon/bacon ou随笔.htm&zwnj;&8203;l
在所有答案中添加一点:
如果你这样做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #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
",*p);
} |
输出可能是:7
这是因为从foo()返回后,堆栈被释放,然后由boo()重用。如果您重新组装可执行文件,您将清楚地看到它。
- 理解底层堆栈理论的简单但很好的例子。只需添加一个测试,声明"int a=5";在foo()中声明"static int a=5";就可以用来理解静态变量的作用域和生命周期。
- -1"可能是7"。编译器可能会在boo中注册a。它可能会删除它,因为它是不必要的。很有可能*P不会是5,但这并不意味着有任何特别好的理由可以解释为什么它可能是7。
- 这叫做未定义行为!
- 为什么以及如何使用boo堆栈?函数堆栈不是彼此分开的吗?在Visual Studio 2015上运行此代码也会产生垃圾
- @AMPAWD已经有将近一年的历史了,但是没有,"函数栈"是不相互分离的。上下文有一个堆栈。该上下文使用其堆栈进入main,然后下降到foo(),exists,然后下降到boo()。foo()和boo()都输入,堆栈指针位于同一位置。然而,这不是应该依赖的行为。其他"东西"(如中断或操作系统)可以使用boo()和foo()调用之间的堆栈,修改其内容…
在C++中,你可以访问任何地址,但并不意味着你应该访问。您正在访问的地址不再有效。它的工作是因为在foo返回后没有其他东西扰乱内存,但在许多情况下它可能崩溃。试着用valgrind分析你的程序,或者只是优化编译它,然后看…
- 你可能是说你可以尝试访问任何地址。因为现在大多数操作系统都不允许任何程序访问任何地址;有大量的保护措施来保护地址空间。这就是为什么外面不会有另一个loadlin.exe。
您从不通过访问无效内存抛出C++异常。您只是举一个引用任意内存位置的一般概念的例子。我也可以这样做:
1 2 3
| unsigned int q = 123456;
*(double*)(q) = 1.2; |
在这里,我只是把123456作为一个双精度数的地址,并写信给它。任何事情都可能发生:
实际上,q可能是双精度的有效地址,例如double p; q = &p;。
q可能指向已分配内存中的某个位置,而我只覆盖其中的8个字节。
q指向分配的内存之外,操作系统的内存管理器向我的程序发送一个分段故障信号,导致运行时终止它。
你中了彩票。
您设置它的方式更合理一点,返回的地址指向有效的内存区域,因为它可能只是在堆栈的下面稍微远一点,但它仍然是一个无效的位置,您不能以确定的方式访问它。
在正常程序执行期间,没有人会自动为您检查像这样的内存地址的语义有效性。但是,像valgrind这样的内存调试程序会很乐意这样做,因此您应该运行您的程序并看到错误。
- 我现在要写一个程序,继续运行这个程序,这样4) I win the lottery就可以
是否使用启用优化器编译程序?EDCOX1的0个函数非常简单,可能在结果代码中被内联或替换。
但我同意Mark B,结果行为是未定义的。
- 这是我的赌注。优化器转储了函数调用。
- 这是不必要的。因为在foo()之后没有调用任何新函数,所以函数的本地堆栈帧还没有被覆盖。在foo()后添加另一个函数调用,5将被更改…
- 我用GCC4.8运行程序,用printf(包括stdio)替换cout。正确地警告"警告:返回的本地变量"a"的地址是[-wreturn local addr]"。输出58没有优化,08有-O3。奇怪的是,p确实有一个地址,即使它的值是0。我需要空(0)作为地址。
你的问题与范围无关。在您所显示的代码中,函数main没有看到函数foo中的名称,因此您不能在foo之外直接访问foo中的a。
您遇到的问题是,程序在引用非法内存时不会发出错误信号的原因。这是因为C++标准没有规定非法内存和合法内存之间非常明确的界限。在弹出堆栈中引用某些内容有时会导致错误,有时不会。这要看情况而定。别指望这种行为。假设它在编程时总是会导致错误,但假设它在调试时永远不会发出错误信号。
- 我记得从IBM的一个旧的turbo C编程副本中,我曾经在前面的某个地方玩过这个游戏,详细描述了如何直接操作图形内存以及IBM的文本模式视频内存的布局。当然,运行代码的系统清楚地定义了写入这些地址意味着什么,只要您不担心可移植到其他系统,一切都很好。在那本书中,指向虚空的指针是一个共同的主题。
- @迈克尔·凯吉&246;林:当然!人们喜欢偶尔做些脏活;)
您只是返回一个内存地址,这是允许的,但可能是一个错误。
是的,如果您试图取消引用该内存地址,您将具有未定义的行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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
} |
- 我不同意:cout之前有个问题。*a指向未分配(释放)的内存。即使你不抛弃它,它仍然是危险的(很可能是伪造的)。
- eReon:我更清楚地说明了问题的含义,但从有效C++代码来看,它并不危险。但就用户可能犯了错误并会做坏事而言,这是危险的。例如,您可能试图查看堆栈是如何增长的,您只关心地址值,永远不会取消对它的引用。
它工作是因为自A被放到那里以来堆栈还没有被改变。在再次访问a之前,调用一些其他函数(也调用其他函数),您可能不再那么幸运了…;-)
这是两天前没有讨论过的经典的未定义行为——在网站上搜索一下。简而言之,你是幸运的,但是任何事情都可能发生,而且你的代码对内存的访问是无效的。
正如亚历克斯指出的那样,这种行为是未定义的——事实上,大多数编译器会警告不要这样做,因为这是一个简单的方法来获得崩溃。
对于您可能会得到的这种令人毛骨悚然的行为的示例,请尝试以下示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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显示此警告
warning: address of local variable 'a' returned
这是C++的力量。你应该关心记忆。使用-Werror标志,此警告会导致错误,现在您必须对其进行调试。
实际上调用了未定义的行为。
返回临时工程的地址,但由于临时工程在函数结束时被破坏,访问临时工程的结果将不确定。
所以您没有修改a,而是修改了a曾经所在的内存位置。这一区别与碰撞和不碰撞的区别非常相似。
它可以,因为a是在其作用域(foo函数)的生命周期内临时分配的变量。从foo返回后,内存是空闲的,可以被覆盖。
你所做的被描述为未定义的行为。结果是无法预测的。
在典型的编译器实现中,您可以将代码视为"用以前被占用的地址打印出内存块的值"。另外,如果向一个包含本地int的函数添加一个新的函数调用,那么a的值(或a用来指向的内存地址)很有可能发生更改。这是因为堆栈将被包含不同数据的新帧覆盖。
但是,这是未定义的行为,您不应该依赖它来工作!
- "用以前被占用的地址打印出内存块的值"不太正确。这使得他的代码听起来有一些定义明确的含义,但事实并非如此。不过,您是对的,这可能是大多数编译器实现它的方式。
- @布伦南文森特:当存储被a占用时,指针保持a的地址。虽然该标准不要求实现在目标的生命周期结束后定义地址的行为,但它也认识到,在某些平台上,UB是以记录在案的环境特征处理的。虽然局部变量的地址在超出作用域后通常不会有太大的用处,但在它们各自目标的生存期之后,其他一些类型的地址可能仍然有意义。
- @布伦南文森特:例如,虽然标准可能不要求实现允许将传递给realloc的指针与返回值进行比较,也不允许将旧块中的地址指针调整为指向新块,但有些实现会这样做,并且利用这种特性的代码可能更有效。比必须避免任何操作(甚至是比较)的代码更重要的是,这些操作包括指向给定给realloc的分配的指针。
正确的东西(?)如果使用::printf但不能使用,则控制台输出可能会发生显著变化。您可以在以下代码中使用调试器(在x86、32位、MSVisual Studio上测试):
1 2 3 4 5 6 7 8 9 10 11 12 13
| char* foo()
{
char buf[10];
::strcpy(buf,"TEST");
return buf;
}
int main()
{
char* s = foo(); //place breakpoint & check 's' varialbe here
::printf("%s
", s);
} |
从函数返回后,所有标识符将被销毁,而不是保存在内存位置中的值,如果没有标识符,则无法定位值。但该位置仍包含由前一个函数存储的值。
因此,这里函数foo()返回a的地址,a返回地址后被销毁。您可以通过返回的地址访问修改后的值。
让我举一个现实世界的例子:
假设一个人把钱藏在一个地方,然后告诉你地点。过了一段时间,那个告诉你钱的人死了。但你仍然可以得到那笔隐藏的钱。
这是使用内存地址的"肮脏"方式。当您返回一个地址(指针)时,您不知道它是否属于函数的本地范围。只是一个地址。既然调用了"foo"函数,"a"的地址(内存位置)已经分配到应用程序(进程)的可寻址内存中(至少目前是安全的)。"foo"函数返回后,"a"的地址可以被认为是"脏"的,但它在那里,没有被清除,也没有被程序其他部分的表达式干扰/修改(至少在这种特定情况下)。一个C/C++编译器不会阻止你这样的"脏"访问(如果你在意的话,也许会警告你)。您可以安全地使用(更新)程序实例(进程)数据段中的任何内存位置,除非您通过某种方式保护地址。
你的代码非常危险。您正在创建一个局部变量(函数结束后wich被视为已销毁),并在该变量被销毁后返回该变量的内存地址。
这意味着内存地址可能有效或无效,并且您的代码将容易受到可能的内存地址问题(例如分段错误)的影响。
这意味着你做了一件非常糟糕的事情,因为你将一个内存地址传递给一个根本不可信的指针。
考虑这个例子,然后测试它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int * foo()
{
int *x = new int;
*x = 5;
return x;
}
int main()
{
int* p = foo();
std::cout << *p <<"
"; //better to put a new-line in the output, IMO
*p = 8;
std::cout << *p;
delete p;
return 0;
} |
与您的示例不同,在这个示例中,您是:
- 将int的内存分配给本地函数
- 当函数过期时,该内存地址仍然有效(不会被任何人删除)
- 内存地址是可信任的(该内存块不被视为可用的,因此在删除之前不会被覆盖)
- 不使用时应删除内存地址。(见程序末尾的删除)
- 您是否添加了现有答案中未包含的内容?请不要使用原始指针/new。
- 询问者使用了原始指针。我做了一个例子,它准确地反映了他所做的例子,以便让他看到不可信的指针和可信的指针之间的区别。实际上还有另一个类似于我的答案,但是它使用的是strcpy wich,imho,对于新手程序员来说,可能比我使用new的例子更不清楚。
- 他们没有使用new。你在教他们使用new。但你不应该使用new。
- 所以在你看来,把地址传递给局部变量比实际分配内存要好,因为它在函数中被破坏了?这没有道理。理解分配e解除分配内存的概念很重要,imho,主要是当您询问指针(asker没有使用新的,但使用了指针)时。
- 我什么时候说的?不,最好使用智能指针来正确指示引用资源的所有权。不要在2019年使用new(除非你在写图书馆代码),也不要教新来者这样做!干杯。
- 我同意你的看法,聪明的指点肯定比江户强。但我使用new,因为对于新手来说,使用和理解EDOCX1比智能指针简单。我认为复杂性应该被扩展。第一件事是在进入下一步之前理解分配意味着什么(wich可以是…如何更好地使用分配?"智能指针,以及STD中的其他工具::"我承认,我是一个非常古老的学校C++学习者:D SO——我的错误:P
- 使用智能指针的对象管理是应该教授的。new和delete是一个高级课题,可以稍后讲授。:)