提问人: 提问时间:6/22/2011 最后编辑:24 revs, 14 users 26%unknown 更新时间:9/16/2023 访问量:302105
是否可以在其范围之外访问局部变量的内存?
Can a local variable's memory be accessed outside its scope?
问:
我有以下代码。
#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
}
评论
cout
*a
您是否在启用优化器的情况下编译程序?该函数非常简单,可能已在生成的代码中内联或替换。foo()
但我同意 Mark B 的观点,即由此产生的行为是未定义的。
评论
5
在 C++ 中,您可以访问任何地址,但这并不意味着您应该访问。您正在访问的地址不再有效。它之所以有效,是因为在 foo 返回后没有其他东西扰乱内存,但它在许多情况下可能会崩溃。尝试使用 Valgrind 分析您的程序,甚至只是对其进行优化编译,然后查看...
评论
您永远不会通过访问无效内存来引发 C++ 异常。您只是举了一个示例来说明引用任意内存位置的一般思路。我可以像这样做同样的事情:
unsigned int q = 123456;
*(double*)(q) = 1.2;
在这里,我只是将123456视为双精度的地址并写信给它。可能发生许多事情:
q
实际上可能真的是双精度的有效地址,例如 .double p; q = &p;
q
可能指向分配内存中的某个地方,我只是覆盖其中的 8 个字节。q
点在分配的内存之外,操作系统的内存管理器会向我的程序发送分段故障信号,导致运行时终止它。- 你中了彩票。
设置它的方式更合理一些,返回的地址指向一个有效的内存区域,因为它可能只是在堆栈的更下方,但它仍然是一个无效的位置,你不能以确定性的方式访问。
在正常程序执行期间,没有人会像这样自动检查内存地址的语义有效性。但是,像 Valgrind 这样的内存调试器会很乐意这样做,因此您应该通过它运行程序并见证错误。
评论
4) I win the lottery
在典型的编译器实现中,您可以将代码视为“打印出内存块的值,其中包含过去被 a 占用的地址”。此外,如果将新的函数调用添加到包含本地的函数,则 的值(或过去指向的内存地址)很有可能发生变化。发生这种情况是因为堆栈将被包含不同数据的新帧覆盖。int
a
a
但是,这是未定义的行为,您不应依赖它来工作!
评论
a
a
realloc
realloc
怎么可能?局部变量的内存不是在其函数之外无法访问的吗?
你租一个酒店房间。你把一本书放在床头柜的顶部抽屉里,然后去睡觉。你第二天早上退房,但“忘记”归还你的钥匙。你偷了钥匙!
一周后,你回到酒店,不办理入住手续,拿着偷来的钥匙偷偷溜进你的旧房间,在抽屉里看看。你的书还在那里。惊人!
这怎么可能?如果您没有租过房间,酒店房间抽屉里的东西不是无法取用吗?
嗯,显然这种情况可以在现实世界中发生,没有问题。当你不再被授权进入房间时,没有任何神秘的力量会导致你的书消失。也没有一种神秘的力量阻止你带着被盗的钥匙进入房间。
酒店管理层无需删除您的预订。你没有和他们签订合同,说如果你留下东西,他们会为你撕碎。如果您使用偷来的钥匙非法重新进入您的房间以取回它,酒店保安人员不需要抓住您偷偷溜进来。你没有和他们签订合同,上面写着“如果我以后试图偷偷溜回我的房间,你必须阻止我。相反,你和他们签订了一份合同,上面写着“我保证以后不会偷偷溜回我的房间”,但你打破了这份合同。
在这种情况下,任何事情都可能发生。这本书可以在那里——你很幸运。别人的书可能在那里,而你的书可能在酒店的炉子里。当你进来时,有人可能就在那里,把你的书撕成碎片。酒店本可以完全拆除桌子和预订,并用衣柜代替它。整个酒店可能即将被拆除,取而代之的是足球场,而你偷偷摸摸的时候会在爆炸中丧生。
你不知道会发生什么;当你退房并偷走了一把钥匙以供以后非法使用时,你放弃了生活在一个可预测的、安全的世界中的权利,因为你选择打破了系统的规则。
C++ 不是一种安全的语言。它会愉快地让你打破系统的规则。如果你试图做一些非法和愚蠢的事情,比如回到一个你没有被授权进入的房间,在一张甚至可能不再存在的桌子上翻找,C++ 不会阻止你。比 C++ 更安全的语言通过限制您的权力(例如,通过对密钥进行更严格的控制)来解决这个问题。
更新
天哪,这个答案引起了很多关注。(我不知道为什么——我认为这只是一个“有趣”的小类比,但无论如何。
我认为用更多的技术思想来更新一下可能会很有意义。
编译器的业务是生成代码,这些代码管理该程序操作的数据的存储。有许多不同的方法可以生成代码来管理内存,但随着时间的推移,两种基本技术已经根深蒂固。
第一种是建立某种“长期”存储区域,其中存储中每个字节的“生存期”(即它与某些程序变量有效关联的时间段)不能轻易提前预测。编译器生成对“堆管理器”的调用,该管理器知道如何在需要时动态分配存储,并在不再需要时回收存储。
第二种方法是有一个“短期”存储区域,其中每个字节的生存期是众所周知的。在这里,生存期遵循“嵌套”模式。这些短期变量中生存期最长的变量将在任何其他短期变量之前分配,并将最后释放。生存期较短的变量将在生存期最长的变量之后分配,并在它们之前释放。这些生存期较短的变量的生存期“嵌套”在生存期较长的变量的生存期内。
局部变量遵循后一种模式;当输入方法时,其局部变量将激活。当该方法调用另一个方法时,新方法的局部变量将激活。在第一种方法的局部变量失效之前,它们就已经死了。可以提前计算出与局部变量关联的存储生命周期的开始和结束的相对顺序。
出于这个原因,局部变量通常作为“堆栈”数据结构上的存储生成,因为堆栈具有这样的属性,即推送到它上面的第一件事将是最后弹出的东西。
这就像酒店决定只按顺序出租房间,直到房间号高于您的每个人都退房后,您才能退房。
因此,让我们考虑一下堆栈。在许多操作系统中,每个线程都有一个堆栈,并且堆栈被分配为一定的固定大小。当你调用一个方法时,内容会被推送到堆栈上。如果随后将指向堆栈的指针从方法中传回,就像此处的原始海报一样,则这只是指向某个完全有效的百万字节内存块中间的指针。在我们的类比中,您从酒店退房;当您这样做时,您刚刚从入住人数最多的房间退房。如果没有其他人在你身后办理入住手续,而你非法回到你的房间,你所有的东西都保证仍然在这家特定的酒店里。
我们使用堆栈作为临时商店,因为它们非常便宜且简单。使用堆栈存储局部变量不需要 C++ 的实现;它可以使用堆。它不会,因为这会使程序变慢。
不需要 C++ 的实现来保持您留在堆栈上的垃圾不变,以便您以后可以非法返回它;编译器生成的代码将您刚刚腾出的“房间”中的所有内容都归零是完全合法的。它没有,因为同样,那会很昂贵。
不需要 C++ 的实现来确保当堆栈在逻辑上收缩时,以前有效的地址仍映射到内存中。允许实现告诉操作系统“我们现在已经完成了堆栈的这一页。在我另有说明之前,请发出一个例外,如果有人触摸以前有效的堆栈页面,则会破坏该过程”。同样,实现实际上并没有这样做,因为它很慢且没有必要。
相反,实现会让你犯错误并逃脱惩罚。大多数时候。直到有一天,一些真正可怕的事情出了问题,这个过程爆炸了。
这是有问题的。有很多规则,很容易不小心打破它们。我当然有很多次。更糟糕的是,问题通常只有在损坏发生数十亿纳秒后检测到内存损坏时才会浮出水面,这时很难弄清楚是谁搞砸了它。
更多内存安全语言通过限制您的能力来解决这个问题。在“普通”C# 中,根本没有办法获取本地地址并返回或存储它以备后用。您可以采用本地地址,但该语言设计巧妙,因此在本地结束的生命周期之后无法使用它。为了获取本地地址并将其传回,您必须将编译器置于特殊的“不安全”模式,并在程序中加入“不安全”一词,以引起人们的注意,即您可能正在做一些可能违反规则的危险事情。
进一步阅读:
如果 C# 允许返回引用呢?巧合的是,这就是今天博客文章的主题:
为什么我们使用堆栈来管理内存?C# 中的值类型是否始终存储在堆栈中?虚拟内存如何工作?以及有关 C# 内存管理器工作原理的更多主题。其中许多文章也与 C++ 程序员密切相关:
评论
您的问题与范围无关。在你展示的代码中,函数看不到函数中的名称,所以你不能直接在foo中用这个名字在外面访问。main
foo
a
foo
您遇到的问题是为什么程序在引用非法内存时没有发出错误信号。这是因为 C++ 标准没有指定非法内存和合法内存之间的非常明确的界限。在弹出的堆栈中引用某些内容有时会导致错误,有时则不会导致错误。这要视情况而定。不要指望这种行为。假设它在编程时总是会导致错误,但假设它在调试时永远不会发出错误信号。
评论
你只是在读取和写入曾经是 的地址的内存。现在你不在了,它只是指向某个随机内存区域的指针。碰巧的是,在您的示例中,该内存区域确实存在,目前没有其他区域在使用它。a
foo
继续使用它不会破坏任何东西,而且还没有其他任何东西覆盖它。因此,仍然存在。在真正的程序中,该内存几乎会立即被重用,并且这样做会破坏某些东西(尽管症状可能要到很久以后才会出现!5
当您从 返回时,您告诉操作系统您不再使用该内存,并且可以将其重新分配给其他内存。如果你很幸运,它永远不会被重新分配,并且操作系统没有发现你再次使用它,那么你就会逃脱谎言。很有可能,你最终会写下任何其他以该地址结尾的东西。foo
现在,如果你想知道为什么编译器不抱怨,那可能是因为被优化淘汰了。它通常会警告你这种事情。C 假设你知道你在做什么,从技术上讲,你没有违反这里的范围(除了 之外没有引用它自己),只有内存访问规则,它只会触发警告而不是错误。foo
a
foo
简而言之:这通常行不通,但有时是偶然的。
它之所以有效,是因为自从 a 放在那里以来,堆栈还没有被更改。
在再次访问之前调用一些其他函数(也在调用其他函数),您可能不再那么幸运了...... ;-)a
如果您使用但不使用,具有正确 (?) 控制台输出的内容可能会发生巨大变化。::printf
cout
可以在以下代码中使用调试器(在 x86、32 位、Visual Studio 上测试):
char* foo()
{
char buf[10];
::strcpy(buf, "TEST");
return buf;
}
int main()
{
char* s = foo(); // Place breakpoint and the check 's' variable here
::printf("%s\n", s);
}
评论
”
您实际上调用了未定义的行为。
返回临时地址是有效的,但是由于临时地址在函数结束时被销毁,因此访问它们的结果将是不确定的。
所以你没有修改,而是修改了曾经的内存位置。这种差异与崩溃和不崩溃之间的区别非常相似。a
a
它可以,因为是在其作用域(函数)的生存期内临时分配的变量。从内存返回后,它是空闲的,可以被覆盖。a
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() 重用。
如果拆解可执行文件,您将清楚地看到它。
评论
boo
foo
foo()
boo()
Foo()
Boo()
boo()
foo()
注意所有警告。不仅要解决错误。
GCC 显示以下警告:
警告:返回局部变量“a”的地址
这就是 C++ 的强大功能。你应该关心记忆。使用 -Werror 标志时,此警告成为错误,现在您必须对其进行调试。
评论
-Werror -Wall -Wextra
-fsanitize=address,undefined
这是使用内存地址的“肮脏”方式。当你返回一个地址(指针)时,你不知道它是否属于函数的本地范围。它只是一个地址。
现在您调用了“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 newline in the output, IMO
*p = 8;
std::cout << *p;
delete p;
return 0;
}
与您的示例不同,在此示例中,您将:
- 将 int 的内存分配给本地函数
- 当函数到期时,该内存地址仍然有效(任何人都不会删除它)
- 内存地址是可信的(该内存块不被视为空闲,因此在删除之前不会被覆盖)
- 不使用时应删除内存地址。(请参阅程序末尾的删除)
评论
new
new
new
new
new
快速回答:你在这里所做的被称为悬空指针。当你退出功能范围时,里面的一切都被破坏了,所以从技术上讲,你的指针只是无处可指。当您访问它时,它会导致未定义的行为。
在这种情况下,你很幸运,程序以你想象的方式运行。但很多时候,它不会带有未定义的行为。所以一般来说,避免做你做过的事情。
评论
address of local variable ‘a’ returned
Invalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr