局部变量的内存可以在其作用域之外访问吗?

发布于 2024-11-16 10:23:55 字数 305 浏览 11 评论 0原文

我有以下代码。

#include <iostream>

int * foo()
{
    int a = 5;
    return &a;
}

int main()
{
    int* p = foo();
    std::cout << *p;
    *p = 8;
    std::cout << *p;
}

并且代码只是运行,没有运行时异常!

输出是58

这是怎么回事?局部变量的内存在其函数之外是不可访问的吗?

I have the following code.

#include <iostream>

int * foo()
{
    int a = 5;
    return &a;
}

int main()
{
    int* p = foo();
    std::cout << *p;
    *p = 8;
    std::cout << *p;
}

And the code is just running with no runtime exceptions!

The output was 58

How can it be? Isn't the memory of a local variable inaccessible outside its function?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(21

溺ぐ爱和你が 2024-11-23 10:23:55

怎么可能呢?局部变量的内存在其函数之外不是不可访问的吗?

你租了一个酒店房间。你把一本书放在床头柜最上面的抽屉里,然后去睡觉。您第二天早上退房,但“忘记”归还钥匙。你偷了钥匙!

一周后,您回到酒店,没有办理入住,而是用偷来的钥匙潜入您的旧房间,然后查看抽屉。你的书还在那里。惊人!

怎么可能呢?如果您没有租用房间,那么酒店房间抽屉里的东西不是就无法访问吗?

嗯,显然这种情况在现实世界中发生是没有问题的。当您不再被授权进入房间时,不会有任何神秘的力量导致您的书消失。也没有什么神秘的力量可以阻止你用偷来的钥匙进入房间。

酒店管理人员不需要删除您的预订。你没有与他们签订合同,规定如果你留下东西,他们会帮你把它撕碎。如果您使用偷来的钥匙非法重新进入房间以取回钥匙,酒店保安人员不需要抓住您偷偷溜进去。您没有与他们签订合同,规定“如果我待会儿想偷偷溜回我的房间,你得阻止我。”相反,你与他们签订了一份合同,上面写着“我保证以后不再溜回我的房间”,但你违反了这份合同。

在这种情况下任何事情都可能发生。这本书可以在那里——你很幸运。别人的书可能在那里,而你的书可能在酒店的熔炉里。当你进来时,可能有人就在那里,把你的书撕成碎片。酒店本可以把桌子和书本全部拆除,换上一个衣柜。整个酒店可能即将被拆除,取而代之的是一个足球场,而你在偷偷摸摸的时候就会死于爆炸。

你不知道会发生什么;当您退房并偷了一把钥匙以便稍后非法使用时,您就放弃了生活在可预测的安全世界中的权利,因为选择违反系统规则。

C++ 不是一种安全语言。它会很高兴地让你打破系统的规则。如果你试图做一些非法和愚蠢的事情,比如回到一个你无权进入的房间,翻阅一张可能已经不存在的桌子,C++ 不会阻止你。比 C++ 更安全的语言通过限制您的权力来解决这个问题,例如对密钥进行更严格的控制。


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

第一个是拥有某种“长期存在”的存储区域,其中存储中每个字节的“生命周期”(即与某个程序变量有效关联的时间段)无法轻松提前预测。编译器生成对“堆管理器”的调用,该管理器知道如何在需要时动态分配存储空间并在不再需要时回收它。

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

局部变量遵循后一种模式;当进入一个方法时,它的局部变量就会活跃起来。当该方法调用另一个方法时,新方法的局部变量就会激活。在第一个方法的局部变量失效之前它们就会失效。与局部变量相关的存储生命周期的开始和结束的相对顺序可以提前算出。

因此,局部变量通常生成为“堆栈”数据结构上的存储,因为堆栈具有这样的属性:第一个压入其中的东西将是最后一个弹出的东西。

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

那么让我们考虑一下堆栈。在许多操作系统中,每个线程都有一个堆栈,并且堆栈被分配为特定的固定大小。当你调用一个方法时,东西就会被压入堆栈。如果您随后将指向堆栈的指针从方法中传回,就像原始发布者在这里所做的那样,那么这只是指向某个完全有效的百万字节内存块中间的指针。在我们的比喻中,你从酒店退房;当您这样做时,您刚刚从编号最大的占用房间中签出。如果没有其他人在您之后办理入住,并且您非法返回自己的房间,那么您的所有物品都保证仍然在这家特定的酒店

我们使用堆栈作为临时存储,因为它们非常便宜且简单。 C++ 的实现不需要使用堆栈来存储局部变量;它可以使用堆。事实并非如此,因为这会使程序变慢。

C++ 的实现不需要将您留在堆栈上的垃圾原封不动地保留下来,以便您以后可以非法地返回;编译器生成将您刚刚腾出的“房间”中的所有内容归零的代码是完全合法的。并不是因为那样会很贵。

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

相反,实施会让你犯错误并侥幸逃脱惩罚。大多数时候。直到有一天,出现了真正可怕的问题,整个过程崩溃了。

这是有问题的。规则有很多,很容易不小心违反。我当然有很多次了。更糟糕的是,通常只有在损坏发生数十亿纳秒后检测到内存损坏时,问题才会浮出水面,而此时很难找出是谁搞砸了。

更多内存安全语言通过限制你的能力来解决这个问题。在“普通”C# 中,根本无法获取本地地址并将其返回或存储以供以后使用。您可以获取本地地址,但该语言经过巧妙设计,使得在本地生命周期结束后无法使用它。为了获取本地地址并将其传回,您必须将编译器置于特殊的“不安全”模式,在程序中添加“不安全”一词,以引起注意事实上,您可能正在做一些可能违反规则的危险事情。

进一步阅读:

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

    引用返回值和引用局部变量

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

    内存管理

How can it be? Isn't the memory of a local variable inaccessible outside its function?

You rent a hotel room. You put a book in the top drawer of the bedside table and go to sleep. You check out the next morning, but "forget" to give back your key. You steal the key!

A week later, you return to the hotel, do not check in, sneak into your old room with your stolen key, and look in the drawer. Your book is still there. Astonishing!

How can that be? Aren't the contents of a hotel room drawer inaccessible if you haven't rented the room?

Well, obviously that scenario can happen in the real world no problem. There is no mysterious force that causes your book to disappear when you are no longer authorized to be in the room. Nor is there a mysterious force that prevents you from entering a room with a stolen key.

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.

In this situation anything can happen. The book can be there—you got lucky. Someone else's book can be there and yours could be in the hotel's furnace. Someone could be there right when you come in, tearing your book to pieces. The hotel could have removed the table and book entirely and replaced it with a wardrobe. The entire hotel could be just about to be torn down and replaced with a football stadium, and you are going to die in an explosion while you are sneaking around.

You don't know what is going to happen; when you checked out of the hotel and stole a key to illegally use later, you gave up the right to live in a predictable, safe world because you chose to break the rules of the system.

C++ is not a safe language. It will cheerfully allow you to break the rules of the system. If you try to do something illegal and foolish like going back into a room you're not authorized to be in and rummaging through a desk that might not even be there anymore, C++ is not going to stop you. Safer languages than C++ solve this problem by restricting your power—by having much stricter control over keys, for example.


Compilers are in the business of generating code which manages the storage of the data manipulated by that program. There are lots of different ways of generating code to manage memory, but over time two basic techniques have become entrenched.

The first is to have some sort of "long lived" storage area where the "lifetime" of each byte in the storage—that is, the period of time when it is validly associated with some program variable—cannot be easily predicted ahead of time. The compiler generates calls into a "heap manager" that knows how to dynamically allocate storage when it is needed and reclaim it when it is no longer needed.

The second method is to have a “short-lived” storage area where the lifetime of each byte is well known. Here, the lifetimes follow a “nesting” pattern. The longest-lived of these short-lived variables will be allocated before any other short-lived variables, and will be freed last. Shorter-lived variables will be allocated after the longest-lived ones, and will be freed before them. The lifetime of these shorter-lived variables is “nested” within the lifetime of longer-lived ones.

Local variables follow the latter pattern; when a method is entered, its local variables come alive. When that method calls another method, the new method's local variables come alive. They'll be dead before the first method's local variables are dead. The relative order of the beginnings and endings of lifetimes of storages associated with local variables can be worked out ahead of time.

For this reason, local variables are usually generated as storage on a "stack" data structure, because a stack has the property that the first thing pushed on it is going to be the last thing popped off.

It's like the hotel decides to only rent out rooms sequentially, and you can't check out until everyone with a room number higher than you has checked out.

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.

We use stacks for temporary stores because they are really cheap and easy. An implementation of C++ is not required to use a stack for storage of locals; it could use the heap. It doesn't, because that would make the program slower.

An implementation of C++ is not required to leave the garbage you left on the stack untouched so that you can come back for it later illegally; it is perfectly legal for the compiler to generate code that turns back to zero everything in the "room" that you just vacated. It doesn't because again, that would be expensive.

An implementation of C++ is not required to ensure that when the stack logically shrinks, the addresses that used to be valid are still mapped into memory. The implementation is allowed to tell the operating system "we're done using this page of stack now. Until I say otherwise, issue an exception that destroys the process if anyone touches the previously-valid stack page". Again, implementations do not actually do that because it is slow and unnecessary.

Instead, implementations let you make mistakes and get away with it. Most of the time. Until one day something truly awful goes wrong and the process explodes.

This is problematic. There are a lot of rules and it is very easy to break them accidentally. I certainly have many times. And worse, the problem often only surfaces when memory is detected to be corrupt billions of nanoseconds after the corruption happened, when it is very hard to figure out who messed it up.

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.

For further reading:

  • What if C# did allow returning references? Coincidentally that is the subject of today's blog post:

    Ref returns and ref locals

  • Why do we use stacks to manage memory? Are value types in C# always stored on the stack? How does virtual memory work? And many more topics in how the C# memory manager works. Many of these articles are also germane to C++ programmers:

    Memory management

独享拥抱 2024-11-23 10:23:55

您只是在读取和写入曾经a地址的内存。现在您已经在 foo 之外了,它只是一个指向某个随机内存区域的指针。碰巧在您的示例中,该内存区域确实存在,并且目前没有其他东西在使用它。

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

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

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

简而言之:这通常不起作用,但有时会偶然发生。

You're are 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 reused almost immediately and you'd break something by doing this (though the symptoms may not appear until much later!).

When you return from foo, you tell the OS that you're no longer using that memory and it can be reassigned to something else. If you're lucky and it never does get reassigned, and the OS doesn't catch you using it again, then you'll get away with the lie. Chances are though you'll end up writing over whatever else ends up with that address.

Now if you're wondering why the compiler doesn't complain, it's probably because foo got eliminated by optimization. It usually will warn you about this sort of thing. C assumes you know what you're doing though, and technically you haven't violated scope here (there's no reference to a itself outside of foo), only memory access rules, which only triggers a warning rather than an error.

In short: this won't usually work, but sometimes will by chance.

静若繁花 2024-11-23 10:23:55

因为存储空间还没有被踩踏。不要指望这种行为。

Because the storage space wasn't stomped on just yet. Don't count on that behavior.

若水般的淡然安静女子 2024-11-23 10:23:55

对所有答案的一点补充:

如果你这样做:

#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() 重用。

如果你反汇编可执行文件,你就会清楚地看到它。

A little addition to all the answers:

If you do something like this:

#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);
}

The output probably will be: 7

That is because after returning from foo() the stack is freed and then reused by boo().

If you disassemble the executable, you will see it clearly.

感悟人生的甜 2024-11-23 10:23:55

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

In C++, you can access any address, but it doesn't mean you should. The address you are accessing is no longer valid. It works because nothing else scrambled the memory after foo returned, but it could crash under many circumstances. Try analyzing your program with Valgrind, or even just compiling it optimized, and see...

可爱咩 2024-11-23 10:23:55

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

unsigned int q = 123456;

*(double*)(q) = 1.2;

这里我只是将 123456 视为 double 的地址并写入它。任何事情都可能发生:

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

设置它的方式更合理的是,返回的地址指向有效的内存区域,因为它可能只是在堆栈中稍远一些,但它仍然是一个无效的位置,您无法在确定性的时尚。

在正常程序执行期间,没有人会像您那样自动检查内存地址的语义有效性。然而,诸如 Valgrind 之类的内存调试器会很乐意执行此操作,因此您应该通过以下方式运行您的程序并见证错误。

You never throw a C++ exception by accessing invalid memory. You are just giving an example of the general idea of referencing an arbitrary memory location. I could do the same like this:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Here I am simply treating 123456 as the address of a double and write to it. Any number of things could happen:

  1. q might in fact genuinely be a valid address of a double, e.g. double p; q = &p;.
  2. q might point somewhere inside allocated memory and I just overwrite 8 bytes in there.
  3. q points outside allocated memory and the operating system's memory manager sends a segmentation fault signal to my program, causing the runtime to terminate it.
  4. You win the lottery.

The way you set it up it is a bit more reasonable that the returned address points into a valid area of memory, as it will probably just be a little further down the stack, but it is still an invalid location that you cannot access in a deterministic fashion.

Nobody will automatically check the semantic validity of memory addresses like that for you during normal program execution. However, a memory debugger such as Valgrind will happily do this, so you should run your program through it and witness the errors.

自此以后,行同陌路 2024-11-23 10:23:55

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

但我同意Mark B 结果行为是未定义的。

Did you compile your program with the optimiser enabled? The foo() function is quite simple and might have been inlined or replaced in the resulting code.

But I agree with Mark B that the resulting behavior is undefined.

一萌ing 2024-11-23 10:23:55

您的问题与范围无关。在您显示的代码中,函数 main 看不到函数 foo 中的名称,因此您无法直接访问 foo 中的 a this 名称位于 foo 之外。

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

Your problem has nothing to do with scope. In the code you show, the function main does not see the names in the function foo, so you can't access a in foo directly with this name outside foo.

The problem you are having is why the program doesn't signal an error when referencing illegal memory. This is because C++ standards does not specify a very clear boundary between illegal memory and legal memory. Referencing something in popped out stack sometimes causes error and sometimes not. It depends. Don't count on this behavior. Assume it will always result in error when you program, but assume it will never signal error when you debug.

跨年 2024-11-23 10:23:55

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

GCC 显示此警告

警告:返回局部变量“a”的地址

这是 C++ 的强大功能。你应该关心记忆。使用 -Werror 标志,此警告变成了错误,现在你必须调试它。

Pay attention to all warnings. Do not only solve errors.

GCC shows this warning:

warning: address of local variable 'a' returned

This is the power of C++. You should care about memory. With the -Werror flag, this warning became an error and now you have to debug it.

听不够的曲调 2024-11-23 10:23:55

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

It works because the stack has not been altered (yet) since a was put there.
Call a few other functions (which are also calling other functions) before accessing a again and you will probably not be so lucky anymore... ;-)

起风了 2024-11-23 10:23:55

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

是的,如果您尝试取消引用该内存地址,您将会遇到未定义的行为

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
}

You are just returning a memory address. It's allowed, but it's probably an error.

Yes, if you try to dereference that memory address you will have undefined behavior.

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
}
厌倦 2024-11-23 10:23:55

此行为未定义,正如 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”,但您的结果可能会有所不同(真的!)。您的指针正在破坏其他不相关的局部变量。

This behavior is undefined, as Alex pointed out. In fact, most compilers will warn against doing this, because it's an easy way to get crashes.

For an example of the kind of spooky behavior you are likely to get, try this sample:

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;
}

This prints out "y=123", but your results may vary (really!). Your pointer is clobbering other, unrelated local variables.

青瓷清茶倾城歌 2024-11-23 10:23:55

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

That's classic undefined behaviour that's been discussed here not two days ago -- search around the site for a bit. In a nutshell, you were lucky, but anything could have happened and your code is making invalid access to memory.

梦旅人picnic 2024-11-23 10:23:55

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

返回临时对象的地址是可行的,但由于临时对象在函数结束时被销毁,因此访问它们的结果将是未定义的。

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

You actually invoked undefined behaviour.

Returning the address of a temporary works, but as temporaries are destroyed at the end of a function the results of accessing them will be undefined.

So you did not modify a but rather the memory location where a once was. This difference is very similar to the difference between crashing and not crashing.

凶凌 2024-11-23 10:23:55

在典型的编译器实现中,您可以将代码视为“打印出地址曾经被a占用的内存块的值”。另外,如果您向包含本地 int 的函数添加新的函数调用,则 a 的值(或 a 的内存地址)很可能会被调用。 用于指向)更改。发生这种情况是因为堆栈将被包含不同数据的新帧覆盖。

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

In typical compiler implementations, you can think of the code as "print out the value of the memory block with adress that used to be occupied by a". Also, if you add a new function invocation to a function that constains a local int it's a good chance that the value of a (or the memory address that a used to point to) changes. This happens because the stack will be overwritten with a new frame containing different data.

However, this is undefined behaviour and you should not rely on it to work!

深居我梦 2024-11-23 10:23:55

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

您正在做的事情被描述为未定义行为。结果无法预测。

It can, because a is a variable allocated temporarily for the lifetime of its scope (foo function). After you return from foo the memory is free and can be overwritten.

What you're doing is described as undefined behavior. The result cannot be predicted.

挽清梦 2024-11-23 10:23:55

如果您使用 ::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);
}

The things with correct (?) console output can change dramatically if you use ::printf but not cout.

You can play around with debugger within below code (tested on x86, 32-bit, 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);
}
幸福%小乖 2024-11-23 10:23:55

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

现在您调用了“foo”函数,“a”的地址(内存位置)已经分配在应用程序(进程)的(至少目前是安全的)可寻址内存中。

在“foo”函数返回之后,“a”的地址可以被认为是“脏”的,但它就在那里,没有被清理,也没有被程序其他部分中的表达式干扰/修改(至少在这个特定情况下)。

AC/C++ 编译器不会阻止您进行此类“脏”访问(不过,如果您关心的话,它可能会警告您)。您可以安全地使用(更新)程序实例(进程)数据段中的任何内存位置,除非您通过某种方式保护该地址。

It's the '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 (it 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.

寻找我们的幸福 2024-11-23 10:23:55

从函数返回后,所有标识符都被销毁,而不是将值保留在内存位置中,并且如果没有标识符,我们无法找到这些值。但该位置仍然包含先前函数存储的值。

因此,这里函数 foo() 返回 a 的地址,并且 a 在返回其地址后被销毁。您可以通过返回的地址访问修改后的值。

让我举一个现实世界的例子:

假设一个人将钱藏在某个位置并告诉您该位置。一段时间后,告诉你钱所在位置的人死了。但你仍然可以使用那些隐藏的钱。

After returning from a function, all identifiers are destroyed instead of kept values in a memory location and we can not locate the values without having an identifier. But that location still contains the value stored by previous function.

So, here function foo() is returning the address of a and a is destroyed after returning its address. And you can access the modified value through that returned address.

Let me take a real world example:

Suppose a man hides money at a location and tells you the location. After some time, the man who had told you the money location dies. But still you have the access of that hidden money.

給妳壹絲溫柔 2024-11-23 10:23:55

你的代码风险很大。您正在创建一个局部变量(在函数结束后被视为已销毁),并在该变量被销毁后返回该变量的内存地址。

这意味着内存地址可能有效也可能无效,并且您的代码将容易受到可能的内存地址问题的影响(例如, 分段错误)。

这意味着您正在做一件非常糟糕的事情,因为您将内存地址传递给根本不可信的指针。

相反,考虑这个例子,并测试它:

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

Your code is very risky. You are creating a local variable (which is considered destroyed after function ends) and you return the address of memory of that variable after it is destroyed.

That means the memory address could be valid or not, and your code will be vulnerable to possible memory address issues (for example, a segmentation fault).

This means that you are doing a very bad thing, because you are passing a memory address to a pointer which is not trustable at all.

Consider this example, instead, and test it:

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;
}

Unlike your example, with this example you are:

  • allocating memory for an int into a local function
  • that memory address is still valid also when function expires (it is not deleted by anyone)
  • the memory address is trustable (that memory block is not considered free, so it will be not overridden until it is deleted)
  • the memory address should be deleted when not used. (see the delete at the end of the program)
追风人 2024-11-23 10:23:55

快速回答:您在这里所做的称为悬空指针。当您退出函数的作用域时,内部的所有内容都会被破坏,因此从技术上讲,您的指针只是指向任何地方。当您访问它时,它会导致未定义的行为。

在这种情况下,你很幸运,程序按照你想象的方式运行。但很多时候,由于未定义的行为,它不会发生。所以一般来说避免做你所做的事情。

Quick answer: What you did here is called dangling pointer. When you exit the scope of function everything inside is destroyed, so your pointer is technically just pointing nowhere. When you access it it causes undefined behavior.

In this case you got lucky, the program acted in a way that you thought it would. But often times it won't with undefined behavior. So in general avoid doing what you did.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文