我有下面的代码。
#include <iostream>
int * foo()
{
int a = 5;
return &a;
}
int main()
{
int* p = foo();
std::cout << *p;
*p = 8;
std::cout << *p;
}
并且代码正在运行,没有运行时异常!
输出是58
怎么可能呢?局部变量的内存在其函数之外不是不可访问的吗?
我有下面的代码。
#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
}
在c++中,你可以访问任何地址,但这并不意味着你应该这样做。您正在访问的地址不再有效。它之所以能工作,是因为在foo返回后没有其他东西扰乱内存,但在许多情况下它可能崩溃。试着用Valgrind分析你的程序,甚至只是优化编译它,然后看看…
永远不要通过访问无效内存来抛出c++异常。您只是给出了一个关于引用任意内存位置的一般概念的示例。我也可以这样做:
unsigned int q = 123456;
*(double*)(q) = 1.2;
在这里,我简单地将123456作为double类型的地址,并对其进行写入。任何事情都可能发生:
Q实际上可能是double的有效地址,例如double p;Q = &p; q可能指向已分配内存中的某个地方,我只是在那里覆盖了8个字节。 Q指向分配的内存之外,操作系统的内存管理器向我的程序发送了一个分割错误信号,导致运行时终止它。 你中了彩票。
你设置它的方式是更合理的,返回的地址指向内存的有效区域,因为它可能只是在堆栈的下一点,但它仍然是一个无效的位置,你不能以确定的方式访问。
在正常的程序执行期间,没有人会自动检查内存地址的语义有效性。但是,像valgrind这样的内存调试器很乐意这样做,所以您应该通过它运行程序并观察错误。
在典型的编译器实现中,您可以将代码视为“打印出内存块的值,该值带有曾经被a占用的地址”。此外,如果您将一个新的函数调用添加到一个约束局部整型的函数中,则a的值(或a所指向的内存地址)很有可能发生变化。这是因为堆栈将被包含不同数据的新帧覆盖。
然而,这是未定义的行为,你不应该依赖它来工作!
怎么可能呢?局部变量的内存在其函数之外不是不可访问的吗?
你租了一个酒店房间。你把一本书放在床头柜最上面的抽屉里,然后去睡觉。第二天早上你退房了,但“忘记”把钥匙还给你。你偷了钥匙!
一周后,你回到酒店,没有入住,用偷来的钥匙偷偷溜进你的旧房间,看了看抽屉。你的书还在那儿。惊人的!
这怎么可能呢?如果你还没有租到酒店房间,难道你就找不到房间抽屉里的东西吗?
显然,这种情况在现实世界中是可以发生的。当你被禁止进入房间时,没有什么神秘的力量会让你的书消失。也没有一种神秘的力量阻止你用偷来的钥匙进入一个房间。
The hotel management is not required to remove your book. You didn't make a contract with them that said that if you leave stuff behind, they'll shred it for you. If you illegally re-enter your room with a stolen key to get it back, the hotel security staff is not required to catch you sneaking in. You didn't make a contract with them that said "if I try to sneak back into my room later, you are required to stop me." Rather, you signed a contract with them that said "I promise not to sneak back into my room later", a contract which you broke.
在这种情况下,任何事情都可能发生。书可以在那里,你很幸运。别人的书可能在那儿,而你的可能在旅馆的火炉里。你进来的时候可能就有人在那里,把你的书撕成碎片。酒店本可以把桌子和书全部移走,用一个衣柜取而代之。整个酒店可能会被拆除,取而代之的是一个足球场,而你会在你偷偷摸摸的时候死于爆炸。
你不知道会发生什么;当你从酒店退房并偷了一把钥匙供日后非法使用时,你就放弃了生活在一个可预测的、安全的世界里的权利,因为你选择了打破系统的规则。
c++不是一种安全的语言。它会高兴地允许你打破系统的规则。如果你试图做一些非法和愚蠢的事情,比如回到一个你没有被授权进入的房间,翻找一张可能已经不在那里的桌子,c++不会阻止你。比c++更安全的语言通过限制您的能力来解决这个问题——例如,通过对键有更严格的控制。
更新
天哪,这个答案引起了很多关注。(我不知道为什么——我认为这只是一个“有趣”的小类比,但不管怎样。)
我认为这可能是密切相关的更新这一点与一些更多的技术思想。
编译器的业务是生成代码,这些代码管理由该程序操作的数据的存储。生成代码来管理内存有很多不同的方法,但随着时间的推移,有两种基本技术已经根深蒂固。
第一种是要有某种“长寿命”的存储区域,其中存储中每个字节的“寿命”——也就是说,它与某个程序变量有效关联的时间——不能轻易提前预测。编译器生成对“堆管理器”的调用,该管理器知道如何在需要时动态分配存储空间,并在不再需要时回收存储空间。
第二种方法是使用“短寿命”存储区域,其中每个字节的生命周期都是众所周知的。在这里,生命周期遵循“嵌套”模式。这些短期变量中寿命最长的变量将在任何其他短期变量之前分配,并将最后释放。寿命较短的变量将在寿命最长的变量之后分配,并在它们之前释放。这些短寿命变量的生命周期“嵌套”在长寿命变量的生命周期中。
局部变量遵循后一种模式;当输入一个方法时,它的局部变量就激活了。当该方法调用另一个方法时,新方法的局部变量就会激活。它们将在第一个方法的局部变量失效之前失效。与局部变量相关的存储生命周期的开始和结束的相对顺序可以提前计算出来。
由于这个原因,局部变量通常被生成为“堆栈”数据结构上的存储,因为堆栈具有这样的属性,即第一个压入它的东西将是最后一个弹出的东西。
这就像酒店决定只按顺序出租房间,在房间号比你高的人都退房之前,你不能退房。
So let's think about the stack. In many operating systems you get one stack per thread and the stack is allocated to be a certain fixed size. When you call a method, stuff is pushed onto the stack. If you then pass a pointer to the stack back out of your method, as the original poster does here, that's just a pointer to the middle of some entirely valid million-byte memory block. In our analogy, you check out of the hotel; when you do, you just checked out of the highest-numbered occupied room. If no one else checks in after you, and you go back to your room illegally, all your stuff is guaranteed to still be there in this particular hotel.
我们使用堆栈作为临时商店,因为它们真的很便宜和简单。c++的实现不需要使用堆栈来存储局部变量;它可以使用堆。它不会,因为那样会使程序变慢。
c++的实现不需要保持你留在堆栈上的垃圾不动,以便你以后可以非法地回来取它;编译器生成的代码将您刚刚腾出的“房间”中的所有东西归零是完全合法的。不会,因为那样会很贵。
不需要c++的实现来确保当堆栈逻辑上收缩时,以前有效的地址仍然映射到内存中。实现被允许告诉操作系统“我们现在已经使用完了这一页堆栈。除非我另有说明,否则如果有人触及之前有效的堆栈页面,就会发出一个异常,破坏该进程”。同样,实现实际上并没有这样做,因为它很慢而且没有必要。
相反,实现允许您犯错误并逃脱惩罚。大多数时候是这样。直到有一天,真正可怕的事情发生了,这个过程爆炸了。
这是有问题的。有很多规则,很容易不小心打破它们。我当然有很多次。更糟糕的是,当内存在发生损坏后数十亿纳秒被检测到损坏时,问题往往才会浮出水面,这时很难找出是谁弄乱了内存。
More memory-safe languages solve this problem by restricting your power. In "normal" C# there simply is no way to take the address of a local and return it or store it for later. You can take the address of a local, but the language is cleverly designed so that it is impossible to use it after the lifetime of the local ends. In order to take the address of a local and pass it back, you have to put the compiler in a special "unsafe" mode, and put the word "unsafe" in your program, to call attention to the fact that you are probably doing something dangerous that could be breaking the rules.
欲进一步阅读:
如果c#允许返回引用呢?巧合的是,这也是今天这篇博文的主题: https://ericlippert.com/2011/06/23/ref-returns-and-ref-locals/ 为什么我们要使用堆栈来管理内存?c#中的值类型总是存储在堆栈中吗?虚拟内存是如何工作的?还有更多关于c#内存管理器如何工作的主题。其中许多文章也与c++程序员密切相关: https://ericlippert.com/tag/memory-management/
你的问题与范围无关。在你所展示的代码中,函数main看不到函数foo中的名称,所以你不能在foo之外直接使用这个名称访问foo中的a。
您遇到的问题是为什么程序在引用非法内存时不发出错误信号。这是因为c++标准在非法内存和合法内存之间没有明确的界限。引用弹出堆栈中的内容有时会导致错误,有时不会。视情况而定。不要指望这种行为。假设在编程时它总是会导致错误,但在调试时它永远不会发出错误信号。
What you're doing here is simply reading and writing to memory that used to be the address of a. Now that you're outside of foo, it's just a pointer to some random memory area. It just so happens that in your example, that memory area does exist and nothing else is using it at the moment. You don't break anything by continuing to use it, and nothing else has overwritten it yet. Therefore, the 5 is still there. In a real program, that memory would be re-used almost immediately and you'd break something by doing this (though the symptoms may not appear until much later!)
当您从foo返回时,您告诉操作系统您不再使用该内存,并且可以将其重新分配给其他内存。如果你很幸运,它从来没有被重新分配,而且操作系统也没有发现你再次使用它,那么你就可以摆脱这个谎言。很有可能你最终会以那个地址结尾。
现在,如果你想知道为什么编译器没有报错,这可能是因为foo被优化淘汰了。它通常会警告你这类事情。C假设您知道自己在做什么,而且从技术上讲,这里没有违反scope(在foo之外没有引用a本身),只有内存访问规则,这只会触发警告而不是错误。
简而言之:这通常不会起作用,但有时会偶然起作用。
它之所以能工作,是因为自从a被放入堆栈以来,堆栈(还没有)被改变过。 在再次访问a之前调用一些其他函数(它们也调用其他函数),你可能不会再那么幸运了……: -)
如果使用::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);
}
您实际上调用了未定义的行为。
返回临时作品的地址,但由于临时作品在函数的末尾被销毁,访问它们的结果将是未定义的。
因此,您没有修改a,而是修改了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”,但您的结果可能会有所不同(真的!)你的指针正在敲打其他不相关的局部变量。
给所有的答案补充一点:
如果你这样做:
#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()重用。 如果你分解可执行文件,你会清楚地看到它。
注意所有警告。不要只解决错误。 GCC显示此警告
警告:返回局部变量'a'的地址
这就是c++的强大之处。你应该关心记忆。有了-Werror标志,这个警告就变成了一个错误,现在您必须调试它。
It's 'Dirty' way of using memory addresses. When you return an address (pointer) you don't know whether it belongs to local scope of a function. It's just an address. Now that you invoked the 'foo' function, that address (memory location) of 'a' was already allocated there in the (safely, for now at least) addressable memory of your application (process). After the 'foo' function returned, the address of 'a' can be considered 'dirty' but it's there, not cleaned up, nor disturbed/modified by expressions in other part of program (in this specific case at least). A C/C++ compiler doesn't stop you from such 'dirty' access (might warn you though, if you care). You can safely use (update) any memory location that is in the data segment of your program instance (process) unless you protect the address by some means.
从函数返回后,所有标识符都将被销毁,而不是将值保存在内存位置,如果没有标识符,则无法定位值。但该位置仍然包含前一个函数存储的值。
因此,这里函数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 new-line in the output, IMO
*p = 8;
std::cout << *p;
delete p;
return 0;
}
不像你的例子,在这个例子中你是:
将int的内存分配到本地函数中 当函数过期时,该内存地址仍然有效(它不会被任何人删除)。 内存地址是可信任的(该内存块不被认为是空闲的,因此在删除它之前不会被覆盖) 内存地址不使用时应删除。(见程序末尾的删除)