是否可以在其范围之外访问局部变量的内存?

Can a local variable's memory be accessed outside its scope?

本文关键字:局部变量 访问 内存 范围 是否      更新时间:2023-10-16

我有以下代码。

#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++更安全的语言通过限制你的能力来解决这个问题——例如,通过对密钥进行更严格的控制。

更新

天哪,这个答案受到了很多关注。(我不知道为什么——我认为这只是一个"有趣"的小比喻,但不管怎样。)

我认为用一些更技术的想法来更新这一点可能是密切相关的。

编译器的业务是生成代码,管理该程序操作的数据的存储。有很多不同的方法可以生成代码来管理内存,但随着时间的推移,两种基本技术已经根深蒂固。

第一种是要有某种";长寿;存储区域;寿命;存储中每个字节的长度——也就是说,它与某个程序变量有效关联的时间段——不能很容易地提前预测。编译器生成对";堆管理器";它知道如何在需要时动态分配存储,并在不再需要时回收存储。

第二种方法是有一个"短命"存储区域,其中每个字节的寿命都是众所周知的。在这里,生命周期遵循"嵌套"模式。这些短命变量中寿命最长的一个将在任何其他短命变量之前分配,并将最后释放。寿命较短的变量将在寿命最长的变量之后分配,并在它们之前释放。这些寿命较短的变量的寿命"嵌套"在寿命较长的变量的生命周期内。

局部变量遵循后一种模式;当一个方法被输入时,它的局部变量变为活动的。当该方法调用另一个方法时,新方法的局部变量将变为活动的。在第一个方法的局部变量失效之前,它们就会失效。与局部变量相关的存储器的寿命开始和结束的相对顺序可以提前计算出来。

由于这个原因,局部变量通常被生成为"0"上的存储;堆叠";数据结构,因为堆栈具有这样的属性,即推到它上面的第一个东西将是弹出的最后一个东西

这就像酒店决定只按顺序出租房间,直到所有房间号比你高的人都退房后,你才能退房。

所以让我们考虑一下堆栈。在许多操作系统中,每个线程有一个堆栈,堆栈被分配为某个固定大小。当您调用一个方法时,内容会被推送到堆栈中。如果你把一个指向堆栈的指针从你的方法中传回来,就像最初的海报在这里所做的那样,那只是一个指向某个完全有效的百万字节内存块中间的指针。在我们的类比中,你从酒店退房;当你这样做的时候,你只是从人数最多的房间退房。如果没有其他人在你之后办理入住手续,而你又非法返回房间,那么你所有的东西都保证会留在这家酒店。

我们使用堆叠作为临时商店,因为它们非常便宜和容易。C++的实现不需要使用堆栈来存储本地;它可以使用堆。没有,因为那样会使程序变慢。

C++的实现不需要保留您留在堆栈上的垃圾,这样您以后就可以非法返回;编译器生成将"0"中的所有内容都归零的代码是完全合法的;房间";你刚刚腾空的。没有,因为再说一遍,那会很贵。

不需要C++的实现来确保当堆栈逻辑收缩时,过去有效的地址仍然映射到内存中。允许该实现告诉操作系统";我们现在已经用完了这一页的堆栈。除非我另有说明,否则,如果有人接触到以前有效的堆栈页,就会发出一个破坏进程的异常;。同样,实现实际上并没有做到这一点,因为它很慢而且没有必要。

相反,在大多数情况下,实现会让你犯错误并逍遥法外。直到有一天真正可怕的事情出了问题,整个过程爆炸了。

这是有问题的。有很多规则,很容易不小心打破它们。我当然有很多次。更糟糕的是,当内存在损坏发生数十亿纳秒后被检测到损坏时,问题往往才会浮出水面,而此时很难弄清楚是谁把它搞砸了。

更多的内存安全语言通过限制你的能力来解决这个问题。在";正常的";C#根本没有办法获取本地地址并将其返回或存储以备以后使用。你可以取当地人的地址,但语言设计得很巧妙,在当地人的生命结束后就不可能使用它了。为了获取本地的地址并将其传回,您必须将编译器放在一个特殊的"中;"不安全";模式,将单词";"不安全";在你的程序中,提醒人们注意你可能正在做一些可能违反规则的危险事情。

进一步阅读:

  • 如果C#允许返回引用怎么办?无独有偶,这就是今天博客文章的主题:

    引用返回和引用本地

  • 为什么我们使用堆栈来管理内存?C#中的值类型总是存储在堆栈中吗?虚拟内存是如何工作的?以及关于C#内存管理器如何工作的更多主题。其中许多文章也与C++程序员密切相关:

    内存管理

您只是在读写曾经是a地址的内存。现在您在foo之外,它只是指向某个随机内存区域的指针。碰巧在您的示例中,该内存区域确实存在,而此时没有其他人在使用它。

继续使用它不会破坏任何东西,也没有其他东西覆盖它。因此,5仍然存在。在一个真实的程序中,内存几乎会立即被重用,这样做会破坏一些东西(尽管症状可能要很久以后才会出现!)。

当您从foo返回时,您会告诉操作系统您不再使用该内存,并且可以将其重新分配给其他内存。如果你很幸运,它从未被重新分配,操作系统也没有发现你再次使用它,那么你就可以逃脱谎言。不过,很有可能你最终会写下这个地址的其他内容。

现在,如果您想知道编译器为什么不抱怨,那可能是因为优化消除了foo。它通常会警告你这类事情。C假设您知道自己在做什么,并且从技术上讲,您没有违反这里的范围(在foo之外没有引用a本身),只有内存访问规则,这只会触发警告而不是错误。

简而言之:这通常不会奏效,但有时会偶然发生。

因为存储空间还没有被占用。不要指望那种行为。

所有答案的一个小补充:

如果你这样做:

#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("%dn", *p);
}

输出可能是:7

这是因为从foo()返回后,堆栈被释放,然后被boo()重用。

如果你拆开可执行文件,你会看得很清楚。

在C++中,可以访问任何地址,但这并不意味着应该。您正在访问的地址不再有效。它工作,因为在foo返回后没有其他东西扰乱内存,但在许多情况下它可能会崩溃。尝试使用Valgrind分析您的程序,甚至只是对其进行优化编译,然后查看。。。

您永远不会通过访问无效内存来引发C++异常。您只是举了一个引用任意内存位置的一般概念的例子。我也可以这样做:

unsigned int q = 123456;
*(double*)(q) = 1.2;

在这里,我只是简单地将123456作为一个double的地址并写入它

  1. q实际上可能是双精度的有效地址,例如double p; q = &p;
  2. q可能指向已分配内存中的某个位置,而我只覆盖其中的8个字节
  3. q指向分配的内存之外,操作系统的内存管理器向我的程序发送分段故障信号,导致运行时终止它
  4. 你中了彩票

您设置它的方式更合理的是,返回的地址指向内存的有效区域,因为它可能只在堆栈的下面一点,但它仍然是一个无效的位置,您无法以确定性的方式访问它。

在正常程序执行期间,没有人会自动为您检查类似内存地址的语义有效性。然而,像Valgrind这样的内存调试器会很乐意这样做,所以您应该通过它运行程序并见证错误。

您是否在启用优化器的情况下编译程序?foo()函数非常简单,可能已在生成的代码中内联或替换。

但我同意Mark B的观点,即由此产生的行为是未定义的。

您的问题与作用域无关。在您显示的代码中,函数main看不到函数foo中的名称,因此您不能使用foo之外的this名称直接访问foo中的a

您遇到的问题是,为什么程序在引用非法内存时不会发出错误信号。这是因为C++标准没有在非法内存和合法内存之间指定一个非常明确的边界。引用弹出堆栈中的某些内容有时会导致错误,有时则不会。这取决于情况。不要指望这种行为。假设它在编程时总是会导致错误,但假设它在调试时永远不会发出错误信号。

注意所有警告。不要只解决错误。

GCC显示此警告

警告:本地变量"a"的地址返回

这就是C++的威力。你应该关心记忆力。有了-Werror标志,这个警告变成了一个错误,现在你必须调试它

它之所以有效,是因为自从a被放在那里以来,堆栈还没有被更改。在再次访问a之前,调用一些其他函数(这些函数也在调用其他函数),你可能就不会那么幸运了…;-)

您只是返回一个内存地址。这是允许的,但可能是一个错误。

是的,如果你试图取消引用该内存地址,你将有未定义的行为。

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
}
正如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〃;,但你的结果可能会有所不同(真的!)。您的指针正在撞击其他不相关的局部变量。

这是两天前在这里讨论的经典的未定义行为——在网站周围搜索一下。简而言之,你很幸运,但任何事情都可能发生,你的代码正在对内存进行无效访问。

您实际上调用了未定义的行为。

返回临时地址有效,但由于临时在函数结束时被销毁,访问它们的结果将是未定义的。

因此,您没有修改a,而是修改了a曾经所在的内存位置。这种差异与崩溃和不崩溃之间的差异非常相似。

在典型的编译器实现中,您可以将代码想象为"打印出带有地址的内存块的值,该地址曾被占用"。此外,如果向一个保持本地int的函数添加一个新的函数调用,则很有可能a的值(或a用来指向的内存地址)发生更改。发生这种情况是因为堆栈将被包含不同数据的新帧覆盖。

然而,这是未定义的行为,您不应该依赖它来工作!

它可以,因为a是为其作用域(foo函数)的生存期临时分配的变量。从foo返回后,内存是空闲的,可以被覆盖。

您所做的操作被描述为未定义的行为。结果无法预测。

如果使用::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("%sn", s);
}

这是使用内存地址的"肮脏"方式。当您返回一个地址(指针)时,您不知道它是否属于函数的本地范围。这只是一个地址。

既然您调用了"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的内存分配到本地函数中
  • 当函数过期时,该内存地址仍然有效(任何人都不会删除)
  • 内存地址是可信任的(该内存块不被认为是空闲的,因此在删除之前不会被覆盖)
  • 存储器地址在不使用时应该被删除。(请参阅程序末尾的删除)