为什么在释放指向它的指针后仍然可以访问结构的成员?
如果我定义一个结构……
struct LinkNode { int node_val; struct LinkNode *next_node; };
然后创建一个指向它的指针……
struct LinkNode *mynode = malloc(sizeof(struct LinkNode));
…然后终于免费()它……
free(mynode);
…我仍然可以访问结构的’next_node’成员。
mynode->next_node
我的问题是: 底层机制的哪一部分跟踪这个内存块应该代表一个结构LinkNode的事实? 我是C的新手,我期望在我使用指向我的LinkNode的指针上的free()后,我将无法再访问该结构的成员。 我期待某种“不再可用”的警告。
我想更多地了解基础流程的工作原理。
已编译的程序不再具有关于struct LinkedNode
或名为next_node
字段或类似内容的任何知识。 任何名称都已从编译的程序中完全消失。 编译后的程序以数值运算,可以起到内存地址,偏移量,索引等作用。
在您的示例中,当您在程序的源代码中读取mynode->next_node
时,它被编译为机器代码,它只是从一些保留的内存位置读取4字节数值(在源代码中称为变量mynode
),向它添加4(它是next_node
字段的偏移量)并在结果地址(即mynode->next_node
)处读取4字节值。 如您所见,此代码按整数值(地址,大小和偏移量)运行。 它不关心任何名称,如LinkedNode
或next_node
。 它不关心是否分配和/或释放内存。 它并不关心这些访问是否合法。
(我在上面的示例中重复使用的常量4特定于32位平台。在64位平台上,它将在大多数(或所有)实例中被8替换。)
如果尝试读取已释放的内存,则这些访问可能会使程序崩溃。 或者他们可能不会。 这是一个纯粹的运气问题。 就语言而言,行为是不确定的。
没有,你不能。 这是未定义行为的经典案例。
当您有未定义的行为时,任何事情都可能发生。 它甚至可能看起来有效,但一年后随机崩溃。
它运作纯粹,因为释放的内存尚未被其他东西覆盖。 一旦释放内存,您有责任避免再次使用它。
底层内存的任何部分都不会跟踪它。 它只是编程语言给大块内存的语义。 你可以将它转换为完全不同的东西,并且仍然可以访问相同的内存区域。 然而,这里的问题是,这更容易导致错误。 特别是类型安全将会消失。 在你的情况下,仅仅因为你被称为free
并不意味着底层记忆根本就没有。 您的操作系统中只有一个标记,可以将该区域标记为空闲。
以这种方式思考: free
function就像一个“最小”的内存管理系统。 如果您的呼叫需要的不仅仅是设置一个标志,那么它将引入不必要的开销。 此外,当您访问该成员时,您(即您的操作系统)可以检查该内存区域的标志是否设置为“空闲”或“正在使用”。 但那又是开销。
当然,这并不意味着做那些事情是没有意义的。 它可以避免很多安全漏洞,例如在.Net和Java中完成。 但是那些运行时比C小很多,而且这些天我们有更多的资源。
当您的编译器将C代码转换为可执行的机器代码时,会丢弃大量信息,包括类型信息。 你写的地方:
int x = 42;
生成的代码只是将某个位模式复制到某个特定的内存块(一个块通常可能是4个字节)。 你无法通过检查机器代码来判断内存块是int
类型的对象。
同样,当你写:
if (mynode->next_node == NULL) { /* ... */ }
生成的代码将通过解除引用另一个指针大小的内存块来获取指针大小的内存块,并将结果与系统的空指针表示进行比较(通常是所有位为零)。 生成的代码不直接反映了next_node
是结构的成员的事实,或者关于结构如何分配或者是否仍然存在的任何事实。
编译器可以在编译时检查很多东西,但它不一定会生成代码以在执行时执行检查。 作为程序员,您可以避免首先出错。
在这种特定情况下,在调用free
, mynode
具有不确定的值。 它没有指向任何有效的对象,但是没有要求实现对该知识做任何事情。 调用free
不会破坏分配的内存,它只是通过将来调用malloc
来分配它。
实现可以通过多种方式执行此类检查,如果在free
指针后取消引用指针,则会触发运行时错误。 但是C语言不需要这样的检查,并且它们通常没有实现,因为(a)它们会非常昂贵,使得程序运行得更慢,并且(b)检查无论如何都无法捕获所有错误。
定义了C,以便在程序完成所有操作时,内存分配和指针操作将正常工作。 如果您在编译时发现可以检测到的某些错误,编译器可以对其进行诊断。 例如,将指针值分配给整数对象至少需要编译时警告。 但是其他错误(例如解除引用free
d指针)会导致程序出现未定义的行为 。 作为一名程序员,你应该首先避免犯这些错误。 如果你失败了,你就是靠自己。
当然有一些工具可以提供帮助。 Valgrind就是其中之一; 聪明的优化编译器是另一个。 (启用优化会使编译器对代码执行更多分析,这通常可以使其诊断出更多错误。)但最终C不是一种掌握您的手的语言。 它是一个很好的工具 – 可用于构建更安全的工具,例如执行更多运行时检查的解释语言。
您需要为mynode-> next_node分配NULL:
mynode-> next_node = NULL;
释放内存后,它将指示您不再使用已分配的内存。
如果不指定NULL值,它仍然指向先前释放的内存位置。