这是释放单链表的内存的C代码。它是使用Visual C ++ 2008编译的,代码可以正常工作。
/* Program done, so free allocated memory */
current = head;
struct film * temp;
temp = current;
while (current != NULL)
{
temp = current->next;
free(current);
current = temp;
}
但我也遇到过(甚至在书中)同样的代码:
/* Program done, so free allocated memory */
current = head;
while (current != NULL)
{
free(current);
current = current->next;
}
如果我使用VC ++ 2008编译该代码,程序崩溃是因为我首先释放电流,然后在current旁边分配current->。但显然如果我用其他编译器(例如,书籍作者使用的编译器)编译此代码,程序将起作用。所以问题是,为什么用特定编译器编译的代码工作?是因为编译器将指令放在记住current->的地址的二进制文件中,然后我释放了当前而我的VC ++没有。我只想了解编译器的工作原理。
答案 0 :(得分:18)
第二个程序正在调用未定义的行为。它与编译器没有区别,而是C标准库和函数free()的实现差异。编译器将指针current
存储为局部变量,但它不会存储它引用的内存副本。
当你调用free()时,你放弃了传递给free()函数的指针所指向的内存的所有权。在放弃所有权后,指向的内存内容仍然是合理的,并且仍然是进程地址空间中的有效内存位置。因此,访问它们可能会起作用(请注意,您可以通过这种方式静默地破坏内存)。指向非空并且指向已经放弃的内存的指针称为dangling pointer并且非常危险。仅仅因为它似乎有效并不意味着它是正确的。
我还应该指出,可以以捕获这些错误的方式实现free(),例如每次分配使用单独的页面,并在调用free()时取消映射页面(以便内存地址不再是该进程的有效地址)。这样的实现非常低效,但有时在某些编译器处于调试模式时会用来捕获悬空指针错误。
答案 1 :(得分:11)
执行free(current)
后,current
指向的内存(其中存储了current->next
)已返回到C库,因此您不应再访问它。 / p>
C库可以随时更改该内存的内容 - 这将导致current->next
被破坏 - 但它也可能不会改变部分或全部内容,特别是这么快。这就是为什么它适用于某些环境,而不是其他环境。
这有点像驾驶红色交通灯。有时你会逃脱它,但有时候你会被卡车碾过。
答案 2 :(得分:4)
了解编译器如何工作的最佳方法不是询问它们如何处理无效代码。您需要在编译时阅读一本书(实际上不止一本)。入门的好地方是查看Learning to write a compiler的资源。
答案 3 :(得分:3)
第二个例子是错误的代码 - 它在释放后不应引用current
。这似乎在许多情况下都有效,但它是未定义的行为。使用像valgrind
这样的工具可以清除这样的错误。
请引用您在此示例中看到的任何书籍,因为需要更正。
答案 4 :(得分:1)
实际上这是C运行时,而不是编译器。无论如何,后一个代码包含未定义的行为 - 不要这样做。它适用于某些实现的人,但是当你看到它在你的身上崩溃时。它也可以默默地破坏某些东西。
对于后者可能起作用的可能解释是,在某些实现上free()
不会修改块内容并且不会立即将内存块返回给操作系统,因此取消引用指向块的指针仍然是“合法的”,块中的数据仍然完好无损。
答案 5 :(得分:0)
是因为编译器将指令放在记住current->的地址的二进制文件中,然后我释放了当前的,而我的VC ++没有。
我不这么认为。
我只想了解编译器的工作原理。
这是GCC编译器的一个例子(我没有VC ++)
struct film { film* next; };
int main() {
film* current = new film();
delete current;
return 0;
}
;Creation
movl $4, (%esp) ;the sizeof(film) into the stack (4 bytes)
call _Znwj ;this line calls the 'new operator'
;the register %eax now has the pointer
;to the newly created object
movl $0, (%eax) ;initializes the only variable in film
;Destruction
movl %eax, (%esp) ;push the 'current' point to the stack
call _ZdlPv ;calls the 'delete operator' on 'current'
如果它是一个类并且有一个析构函数,那么应该在我们使用delete运算符释放对象在内存中占用的空间之前调用它。
在摧毁物体并释放其存储空间后,您无法再安全地参考当前的>