为啥在释放指向它的指针后仍然可以访问结构的成员?

Posted

技术标签:

【中文标题】为啥在释放指向它的指针后仍然可以访问结构的成员?【英文标题】:Why can I still access a member of a struct after the pointer to it is freed?为什么在释放指向它的指针后仍然可以访问结构的成员? 【发布时间】:2013-05-04 06:28:07 【问题描述】:

如果我定义一个结构...

struct LinkNode

  int node_val;
  struct LinkNode *next_node;
;

然后创建一个指向它的指针...

struct LinkNode *mynode = malloc(sizeof(struct LinkNode));

...然后最后 free() 它...

free(mynode);

...我仍然可以访问结构的“下一个节点”成员。

mynode->next_node

我的问题是:哪一部分底层机制会跟踪这个内存块应该代表结构 LinkNode 的事实?我是 C 的新手,我希望在我对指向我的 LinkNode 的指针使用 free() 之后,我将不再能够访问该结构的成员。我预计会出现某种“不再可用”警告。

我很想进一步了解底层流程的工作原理。

【问题讨论】:

你会期望内存在调用 free 时蒸发吗?是否仍然存在(或不存在),或者它可能具有不同的含义。 指针仍然具有相同的值。在调用 free() 之后你放弃了内存:你告诉 malloc/free 你不想再使用它了。将其与电话号码进行比较:在我退出电话附件后,我的号码不再有效。但是您仍然可以尝试拨打它。甚至可能是我在接电话。或噪音。或者完全不同的人。数字(=地址)仍然存在,但不再使用它。它可能指向核电站的控制... Eric Lippert 的analogy 在这里很合适。 WRT 处理结构成员:检查(汇编器)输出 gcc -S 以了解它们是如何工作的。 p->next 本质上翻译为p + some_offset。在p 被释放之后,该代码(和偏移量)当然是相同的。但它是无效的,因为释放后,p 不再引用有效对象。 还有一个额外的细节:在 c89/ANSI 之前(在一些/大多数 unix 平台上)它曾经是一个 要求 指针(或它曾经用来指向) 仍然可以在 free() 之后使用,因为没有干预 malloc/free 调用。 【参考方案1】:

没有,你也不能。这是未定义行为的经典案例。

当您有未定义的行为时,任何事情都可能发生。它甚至可能看起来有效,只是在一年后随机崩溃。

【讨论】:

这并没有真正回答所提出的问题,特别是为什么结构成员的相对定位仍然有效,即使访问它们的结果是未定义的。 @ChrisStratton:如果您从公共储物柜中取出锁但将您的东西留在里面,然后过一段时间您回来查看里面,您的东西可能仍然在那里。或者它可能是一些看起来像你的东西,但实际上属于其他有相似品味的人。如果你碰巧在储物柜里找到了你上次安排的东西,那么唯一的“原因”就是没有人碰巧对这个空间做任何其他事情。 这里忽略的一点是,你右边第三个储物柜仍然是你拥有的那个右边第三个储物柜,即使它不再是你的了。此外,在 cmets 中,它指出行为实际上 并不总是未定义。 @Chris 抱歉,我不明白提问者想要什么。大概他来自像 Python 这样的语言,其中字段(通常)没有固定的偏移量。 @Ducain,.net 实际上也有固定偏移字段的东西,它只是对你隐藏了它。您甚至可以在 C# 中声明原始结构。【参考方案2】:

底层内存的任何部分都不会跟踪它。这只是编程语言赋予内存块的语义。你可以例如将其转换为完全不同的东西,并且仍然可以访问相同的内存区域。然而这里的问题是,这更有可能导致错误。特别是类型安全将消失。在您的情况下,仅仅因为您调用了free 并不意味着底层内存会发生变化。您的操作系统中只有一个标志再次将该区域标记为空闲。

这样想:free-函数类似于“最小”的内存管理系统。如果您的调用需要的不仅仅是设置一个标志,它会引入不必要的开销。此外,当您访问成员时(即您的操作系统)可以检查此内存区域的标志是否设置为“空闲”或“使用中”。但这又是开销。

当然,这并不意味着做这些事情没有意义。它将避免很多安全漏洞,例如在 .Net 和 Java 中完成。但是这些运行时比 C 更年轻,而且我们现在拥有更多的资源。

【讨论】:

【参考方案3】:

它完全靠运气,因为释放的内存还没有被其他东西覆盖。释放内存后,您有责任避免再次使用它。

【讨论】:

【参考方案4】:

编译后的程序不再有任何关于struct LinkedNode 或名为next_node 的字段或类似的信息。任何名称都完全从编译的程序中消失了。编译后的程序以数值的形式运行,可以起到内存地址、偏移量、索引等作用。

在您的示例中,当您在程序的源代码中读取 mynode->next_node 时,它会被编译成机器代码,该机器代码只是从某个保留的内存位置(在您的程序中称为变量 mynode源代码),将其加 4(这是next_node 字段的偏移量)并读取结果地址(即mynode->next_node)处的 4 字节值。如您所见,此代码根据整数值(地址、大小和偏移量)进行操作。它不关心任何名称,例如LinkedNodenext_node。它不关心内存是否被分配和/或释放。它不关心这些访问是否合法。

(我在上面的例子中重复使用的常量 4 是特定于 32 位平台的。在 64 位平台上,在大多数(或所有)实例中它会被 8 替换。)

如果尝试读取已释放的内存,这些访问可能会使您的程序崩溃。或者他们可能不会。这是纯粹的运气问题。就语言而言,行为是未定义的。

【讨论】:

摇滚。正是我所追求的信息类型。也谢谢你。【参考方案5】:

当您的编译器将您的 C 代码翻译成可执行的机器代码时,会丢弃大量信息,包括类型信息。你写的地方:

 int x = 42;

生成的代码只是将某个位模式复制到某个内存块(通常可能是 4 个字节的块)。您无法通过检查机器代码来判断该内存块是 int 类型的对象。

同样,当你写作时:

if (mynode->next_node == NULL)  /* ... */ 

生成的代码将通过取消引用另一个指针大小的内存块来获取一个指针大小的内存块,并将结果与​​系统表示的空指针(通常全位为零)进行比较。生成的代码不直接反映 next_node 是结构的成员这一事实,也不反映结构是如何分配的或它是否仍然存在的任何信息。

编译器可以在编译时检查很多东西,但它不一定会在执行时生成代码来执行检查。作为程序员,首先要避免出错,这取决于您。

在这种特定情况下,在调用free 之后,mynode 具有不确定的值。它不指向任何有效的对象,但没有要求实现使用该知识做任何事情。调用free 不会破坏分配的内存,它只是使其可用于将来调用malloc 进行分配。

实现可以通过多种方式执行这样的检查,如果您在freeing 之后取消引用指针,则会触发运行时错误。但是 C 语言不需要这样的检查,并且它们通常不会被实现,因为 (a) 它们会非常昂贵,使您的程序运行得更慢,并且 (b) 检查无论如何都无法捕获所有错误。

C 的定义是,如果您的程序一切正常,内存分配和指针操作将正常工作。如果您犯了可以在编译时检测到的某些错误,编译器可以诊断它们。例如,将指针值分配给整数对象至少需要一个编译时警告。但是其他错误,例如取消引用 freed 指针,会导致您的程序具有未定义的行为。作为程序员,首先要避免犯这些错误取决于您。如果你失败了,你就靠自己了。

当然有一些工具可以提供帮助。 Valgrind 就是其中之一。聪明的优化编译器是另一个。 (启用优化会导致编译器对您的代码执行更多分析,这通常可以使其诊断出更多错误。)但最终,C 并不是一种掌握您的语言的语言。它是一种锋利的工具,可用于构建更安全的工具,例如执行更多运行时检查的解释语言。

【讨论】:

优化器还可以帮助发现错误,因为它可能会破坏有错误的代码。看起来没有优化就可以工作的代码在经过优化编译时可能会更明显地被破坏。 @Antimony:是的,如果代码有未定义的行为,编译器可以生成它喜欢的任何代码。优化可以影响它做出的决策,改变实际行为并揭示错误。 实际上,在 C 中,在取消引用指针时通常不会检查 null。相反,它是盲目的尝试——尽管在许多现代系统上会导致内存保护单元出现故障。不,mynode 的值是 not 不确定的。它仍然指出它使用它的地方,可能只是那里的内存现在可能被用于其他用途。 @ChrisStratton:从形式上讲,指针值确实变得不确定。 ISO C11 标准,第 6.2.4p2 节说:“当指针指向(或刚刚过去)的对象达到其生命周期结束时,指针的值变得不确定。”在机器级别上,它可能会保留相同的位模式并指向相同的(虚拟或物理)内存位置,但语言标准并没有承诺。 @KeithThompson:我怀疑这是为了允许实现可能具有由标识内存块的指针以及该块的偏移量组成的指针。这种设计是标准允许的,并且允许捕获许多类型的错误指针访问。如果不介意将指针类型膨胀到 128 位,它可能会一直陷入使用范围外指针的陷阱(假设 64 位计数器永远不会溢出)。【参考方案6】:

你需要给mynode赋值NULL->next_node:

mynode->next_node = NULL;

释放内存后,它将表明您不再使用分配的内存。

没有分配NULL值,它仍然指向之前释放的内存位置。

【讨论】:

以上是关于为啥在释放指向它的指针后仍然可以访问结构的成员?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我可以通过指向派生对象的基类指针访问派生私有成员函数?

结构体指针在使用完free后,该指针所指向的内存区域是啥,这个指针是变成了NULL,还是野指针。

为啥我不能访问指向数组中成员函数的指针?

C语言free释放内存后为啥指针里的值不变?竟然还可以输出

为啥我的指针在删除后仍然指向一个地址?

C语言中 内存消亡 指向她的指针就一定消亡或成了空指针为啥是错的啊