【问题标题】:Question about compilers and how they work关于编译器及其工作方式的问题
【发布时间】:2010-04-14 10:25:27
【问题描述】:

这是释放单链表内存的 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->next 分配给 current。但显然,如果我用其他一些编译器(例如,本书作者使用的编译器)编译这段代码,程序就会工作。所以问题是,为什么用特定编译器编译的这段代码可以工作?是不是因为那个编译器把指令放在二进制文件中,记住 current->next 的地址,尽管我释放了 current 而我的 VC++ 没有。我只是想了解编译器是如何工作的。

【问题讨论】:

  • 请告诉我们这本书,以便我们避免它并推荐反对它。
  • C 入门(所有版本第 5、第 4.. 没有此错误的勘误表)查看此主题:bytes.com/topic/c/answers/212665-freeing-simple-linked-list
  • C 运行时会用某种模式填充已释放块的字节(MSVC 为 0xfeeefeee)。不过,这只发生在调试版本中,如果您在发布版本中尝试它,它很可能会起作用。这仍然是一种未定义的行为,请废弃任何推荐此行为的书。

标签: c++ c compiler-construction


【解决方案1】:

第二个程序正在调用未定义的行为。不是编译器的区别,而是C标准库和函数free()实现的区别。编译器会将指针current 存储为局部变量,但不会存储它所引用的内存的副本。

当您调用 free() 时,您放弃了传递给 free() 函数的指针所指向的内存的所有权。有可能在您放弃所有权后,指向的内存内容仍然是合理的,并且仍然是进程地址空间中的有效内存位置。因此,访问它们可能会起作用(请注意,您可以通过这种方式静默破坏内存)。非空指针指向已放弃的内存,称为dangling pointer,非常危险。仅仅因为它看起来有效并不意味着它是正确的。

我还应该指出,可以以捕获这些错误的方式实现 free(),例如每次分配使用单独的页面,并在调用 free() 时取消映射页面(以便内存地址不再是该进程的有效地址)。这样的实现效率非常低,但有时某些编译器在调试模式下使用它来捕获悬空指针错误。

【讨论】:

  • 将最后 4 个空闲的地址放在 DR0-DR3 寄存器中并在所有这些寄存器上放置一个读断点可能会更容易。
【解决方案2】:

在你做free(current)之后,current指向的内存(current->next所在的地方)已经返回到C库,所以你不应该再访问它了。

C 库可以随时更改该内存的内容——这将导致current->next 被破坏——但它也可能不会更改部分或全部内容,尤其是这么快。这就是为什么它在某些环境中有效,而在其他环境中无效。

这有点像开车闯红灯。有时你会侥幸逃脱,但有时你会被卡车碾过。

【讨论】:

    【解决方案3】:

    了解编译器如何工作的最佳方法不是询问它们如何处理无效代码。你需要阅读一本关于编译的书(实际上不止一本)。一个不错的起点是查看Learning to write a compiler 的资源。

    【讨论】:

      【解决方案4】:

      第二个例子是错误的代码——它不应该在被释放后引用current。这似乎在许多情况下都有效,但它是未定义的行为。使用valgrind 之类的工具将清除此类错误。

      请引用您在其中看到过此示例的任何书籍,因为它需要更正。

      【讨论】:

      【解决方案5】:

      实际上那是 C 运行时,而不是编译器。无论如何,后一个代码包含未定义的行为 - 不要这样做。它在某些实现上对某人有用,但是正如您所见,它在您的实现上严重崩溃。它也可以无声地损坏某些东西。

      为什么后者可能起作用的可能解释是,在某些实现中free() 不会修改块内容并且不会立即将内存块返回给操作系统,因此取消引用指向该块的指针仍然是“合法”,并且块中的数据仍然完好无损。

      【讨论】:

        【解决方案6】:

        是不是因为编译器把指令放在二进制文件中,记住 current->next 的地址,尽管我释放了 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'
        

        如果它是一个类并且有一个析构函数,那么它应该在我们使用删除操作符释放对象在内存中占用的空间之前调用它。

        在销毁对象并释放其内存空间后,您不能再安全地引用 current->next。

        【讨论】:

          猜你喜欢
          • 2019-05-19
          • 2014-03-18
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-10-26
          • 2010-10-05
          • 2012-01-16
          相关资源
          最近更新 更多