【问题标题】:Simple custom linked list in Fortran - Unexpected behaviorFortran 中的简单自定义链表 - 意外行为
【发布时间】:2020-02-03 03:16:05
【问题描述】:

我正在尝试在 Fortran 中创建一个非常简单的链表“类”。我的 Fortran 代码基于以下 C 示例:

https://www.tutorialspoint.com/learn_c_by_examples/simple_linked_list_program_in_c.htm

有时我编译并运行代码,我会得到预期的输出,一个“列表”,其中包含链接到其他节点的节点,以及有序值:

5, 4, 3, 2, 1

但是,在其他时候,使用相同的确切代码,我会得到类似:

5, 4, 3, 2, 4, 3, 2, 4, 3, 2, 4, 3, 2, ... ! or
5, 4, 3, 3, 3, 3, 3, 3, ...

我猜是内存分配问题?我是 Fortran 新手,也不知道如何使用指针,我根本不知道从哪里开始调试这些类型的问题......

class_list.f90

MODULE CLASS_LIST

    PRIVATE

    TYPE :: NODE
        REAL :: VALUE
        TYPE(NODE), POINTER :: NEXT
    END TYPE

    TYPE, PUBLIC :: LIST
        TYPE(NODE), POINTER :: HEAD
        CONTAINS
        PROCEDURE :: APPEND
    END TYPE

    CONTAINS

    SUBROUTINE APPEND(THIS, VALUE)
        CLASS(LIST), INTENT(INOUT) :: THIS
        REAL, INTENT(IN) :: VALUE
        TYPE(NODE), ALLOCATABLE, TARGET :: LINK
        ALLOCATE(LINK)
        LINK.VALUE = VALUE
        LINK.NEXT => THIS.HEAD
        THIS.HEAD => LINK
    END SUBROUTINE

END MODULE

ma​​in.f90

PROGRAM MAIN

    USE CLASS_LIST

    TYPE(LIST) :: A
    INTEGER :: I

    DO I = 1, 5, 1
        CALL A.APPEND(REAL(I))
    END DO

END PROGRAM

预期行为(Visual Studio 中的主要局部变量):

意外行为示例(Visual Studio 中的主要局部变量):

【问题讨论】:

  • 首先注意这不是 Fortran 不是 C,所以 % 用于选择标准 Fortran 中的结构组件。其次,我认为这是因为您的可分配数组属于子例程的本地范围,当您返回时会自动解除分配。但是,在我正确回答之前,让我检查一下 Target 是否改变了,我很少使用指针。
  • 不,目标不会改变任何东西。永远不要返回指向局部变量的指针,在 C 中也是经典的初学者错误。应该只分配 LINK 作为指针。
  • 至少,你应该将指针组件初始化为HEAD => null(),否则你有未定义的内存。然后,子例程将节点声明为allocatable 的事实意味着变量link 在子例程退出时自动释放。你应该把它设为pointer
  • 这是一个更好的例子,你可以从中学习,它是用 Fortran 编写的,有很好的解释。 剧透警告:页面以代码开头,您需要跳过该代码并向下滚动大约三分之一的页面,以查看带有示例的主要文章,以免破坏您的学习(如果您的目标是学习,当然)。这是链接gist.github.com/n-s-k/de4af7ce6cc8f2c85e4b33cedb51fd88

标签: list linked-list fortran


【解决方案1】:

你有三个问题,正如 cmets 中所建议的那样

  1. 在 Fortran 中,结构组件选择器是 %NOT .。因此您的程序包含语法错误
  2. 在 Fortran 中,当 allocatable 数组超出范围时,它会自动释放,除非它具有 save 属性。这是一件好事,因为这意味着使用可分配数组的内存泄漏是不可能的,但它会伤害你,因为当你退出附加例程时,数组LINK 被释放,所以你丢失了你的数据。这不是您想要的,您最终会得到一个悬空指针 - 因此任何行为都是可能的,甚至包括看起来有效。您可以通过使用指针而不是可分配数组来避免这种情况。这在这里有效,因为指针在超出范围时不会自动释放,但这确实意味着内存泄漏和其他奇怪行为的可能性更大,所以通常你应该尝试在任何地方使用可分配数组而不是指针可能。
  3. 在 Fortran 中,指针的初始关联状态是未定义的,除非您对其进行初始化。由于未定义的指针可能导致奇怪的行为,最好使用=> null() 显式初始化

事实上,一旦您修复了第 1 项,gfortran 至少可以告诉您,如果您打开所有警告标志,就会出现问题。下面包含此修复程序,以及您省略的打印例程。查看编译器产生的警告:

ijb@ianbushdesktop ~/work/stack $ cat link_alloc.f90
Module CLASS_LIST

  Private

  Type :: NODE
     Real :: Value
     Type(NODE), Pointer :: NEXT
  End Type NODE

  Type, Public :: LIST
     Type(NODE), Pointer :: HEAD
   Contains
     Procedure :: APPEND
     Procedure :: Print
  End Type LIST

Contains

  Subroutine APPEND(THIS, Value)
    Class(LIST), Intent(INOUT) :: THIS
    Real, Intent(IN) :: Value
    Type(NODE), Allocatable, Target :: LINK
    Allocate(LINK)
    LINK%Value = Value
    LINK%NEXT => THIS%HEAD
    THIS%HEAD => LINK
  End Subroutine APPEND

  Subroutine Print( this )

    Class( list ), Intent( In ) :: this

    Call descend( this%head )

  Contains

    Recursive Subroutine descend( head )

      Type( node ), Intent( In ) :: head

      Write( *, '( f5.0, 1x )' ) head%value

      If( Associated( head%next ) ) Then
         Call descend( head%next )
      End If

    End Subroutine descend

  End Subroutine Print

End Module CLASS_LIST

Program MAIN

  Use CLASS_LIST

  Type(LIST) :: A
  Integer :: I

  Do I = 1, 5, 1
     Call A%APPEND(Real(I))
  End Do

  Call a%print

End Program MAIN
ijb@ianbushdesktop ~/work/stack $ gfortran -std=f2008 -Wall -Wextra -fcheck=all -O -g link_alloc.f90 -o link_alloc
link_alloc.f90:26:4:

     THIS%HEAD => LINK
    1
Warning: Pointer at (1) in pointer assignment might outlive the pointer target [-Wtarget-lifetime]
ijb@ianbushdesktop ~/work/stack $ 

英语有点神秘,但它真正告诉你的是上面的第 2 点——因为数组即将被释放,指针的寿命比它指向的要长。编译器警告非常有用,学习如何使用它们!同样,运行时检查 (-fcheck=all) 表明一切都被破坏了:

ijb@ianbushdesktop ~/work/stack $ ./link_alloc 
   0.

Program received signal SIGSEGV: Segmentation fault - invalid memory reference.

Backtrace for this error:
#0  0x7f470401f4af in ???
#1  0x7f4704ca3c49 in get_float_string
    at ../../../gcc-7.4.0/libgfortran/io/write_float.def:1065
#2  0x7f4704ca4fe7 in write_float_0
    at ../../../gcc-7.4.0/libgfortran/io/write.c:1597
#3  0x7f4704c9c9b4 in formatted_transfer_scalar_write
    at ../../../gcc-7.4.0/libgfortran/io/transfer.c:2041
#4  0x7f4704c9cf4c in formatted_transfer
    at ../../../gcc-7.4.0/libgfortran/io/transfer.c:2279
#5  0x40098a in descend
    at /home/ijb/work/stack/link_alloc.f90:41
#6  0x4009a9 in descend
    at /home/ijb/work/stack/link_alloc.f90:43
#7  0x4009d2 in __class_list_MOD_print
    at /home/ijb/work/stack/link_alloc.f90:33
#8  0x400b1c in MAIN__
    at /home/ijb/work/stack/link_alloc.f90:64
#9  0x400b1c in main
    at /home/ijb/work/stack/link_alloc.f90:55
Segmentation fault
ijb@ianbushdesktop ~/work/stack $ 

通过使用新节点的指针来固定上面的第 2 点和第 3 点,并显式初始化指针,导致

ijb@ianbushdesktop ~/work/stack $ cat link_pointer.f90
Module CLASS_LIST

  Private

  Type :: NODE
     Real :: Value
     Type(NODE), Pointer :: NEXT => Null()
  End Type NODE

  Type, Public :: LIST
     Type(NODE), Pointer :: HEAD => Null()
   Contains
     Procedure :: APPEND
     Procedure :: Print
  End Type LIST

Contains

  Subroutine APPEND(THIS, Value)
    Class(LIST), Intent(INOUT) :: THIS
    Real, Intent(IN) :: Value
    Type(NODE), Pointer :: LINK
    Allocate(LINK)
    LINK%Value = Value
    LINK%NEXT => THIS%HEAD
    THIS%HEAD => LINK
  End Subroutine APPEND

  Subroutine Print( this )

    Class( list ), Intent( In ) :: this

    Call descend( this%head )

  Contains

    Recursive Subroutine descend( head )

      Type( node ), Intent( In ) :: head

      Write( *, '( f5.0, 1x )' ) head%value

      If( Associated( head%next ) ) Then
         Call descend( head%next )
      End If

    End Subroutine descend

  End Subroutine Print

End Module CLASS_LIST

Program MAIN

  Use CLASS_LIST

  Type(LIST) :: A
  Integer :: I

  Do I = 1, 5, 1
     Call A%APPEND(Real(I))
  End Do

  Call a%print

End Program MAIN

这编译没有警告并且重复正确运行:

ijb@ianbushdesktop ~/work/stack $ gfortran -std=f2008 -Wall -Wextra -fcheck=all -O -g link_pointer.f90 -o link_pointer
ijb@ianbushdesktop ~/work/stack $ ./link_pointer 
   5.
   4.
   3.
   2.
   1.
ijb@ianbushdesktop ~/work/stack $ ./link_pointer 
   5.
   4.
   3.
   2.
   1.
ijb@ianbushdesktop ~/work/stack $ ./link_pointer 
   5.
   4.
   3.
   2.
   1.
ijb@ianbushdesktop ~/work/stack $ ./link_pointer 
   5.
   4.
   3.
   2.
   1.
ijb@ianbushdesktop ~/work/stack $ ./link_pointer 
   5.
   4.
   3.
   2.
   1.
ijb@ianbushdesktop ~/work/stack $ ./link_pointer 
   5.
   4.
   3.
   2.
   1.

【讨论】:

  • 感谢您的详细回答,它帮助我对 Fortran 有了更多的了解。所以在我的原始代码中,局部变量LINK 在超出范围时会被自动释放。解决方案是让它成为一个指针,它不会自动释放,对吗?只有一件事,在您更新的代码中,Type(NODE), Pointer :: LINK 也应该用=> null() 初始化,对吗?
  • 小心,初始化局部变量会使其保存。
  • 是的,指针不会自动被释放。但是正如 VladimirF 指出的那样,初始化局部变量会导致一个问题,因为初始化会导致变量获得 save 属性,这不是你想要的——你每次都需要一个不同的指针,它在子例程结束后持续存在
猜你喜欢
  • 2014-09-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-04
  • 1970-01-01
  • 1970-01-01
  • 2014-12-03
  • 1970-01-01
相关资源
最近更新 更多