10

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

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() 之后,我将不再能够访问该结构的成员。我期待某种“不再可用”的警告。

我很想知道更多关于底层过程是如何工作的。

4

6 回答 6

9

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

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

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

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

于 2013-05-09T17:51:11.690 回答
5

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

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

于 2013-05-09T17:28:40.597 回答
5

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

于 2013-05-09T17:30:14.523 回答
2

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

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

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

于 2013-05-09T17:28:56.470 回答
2

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

 int x = 42;

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

同样,当您编写时:

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

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

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

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

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

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

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

于 2013-05-09T18:09:07.473 回答
1

您需要将 NULL 分配给 mynode->next_node:

mynode->next_node = NULL;

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

在不分配 NULL 值的情况下,它仍然指向先前释放的内存位置。

于 2013-05-10T00:44:23.443 回答