Programmieren in Fortran 90/95


  1. Ein- und Ausgabe auf Dateien (File - I/O)
  2. Die Ausgabe auf den Bildschirm reicht nicht mehr aus, sobald der Umfang der von einem Programm ermittelten Informationen größer wird. Schnell taucht der Wunsch auf, den Programmoutput dauerhaft zu sichern, um ihn später jederzeit wieder darauf zugreifen und die Ausgabedaten gegebenfalls mit weiteren Programmen weiterverarbeiten zu können.

    Was wir somit benögen, ist ein Verfahren, um von einem Programm heraus Informationen auf dauerhafte Speichermedien, wie z.B. die Festplatte zu sichern.

    Wünschenswert ist es ebenfalls, dass bei Bedarf Informationen direkt von einem Speichermedium eingelesen und von unserem Programm weiterverarbeitet werden können, ohne das jedesmal riesige Datensätze per Hand eingegeben werden müssen.

    Was wir also benötigen, ist das Einlesen von und das Ausgeben auf Dateien. Im Englischen spricht man von File - Input/Output oder kurz von File - I/O.

    Während im Hauptspeicher des Rechners (dem RAM oder Memory des Computers) auf jeden einzelnen Speicherplatz direkt zugegriffen werden kann (direct access), erfolgt der Zugriff auf die externen Speichermedien sequentiell (sequential access). D.h. die einzelnen Informationsbits müssen von einem Startpunkt aus der Reihe nach (sequential) eingelesen werden.

    Bei der Besprechung der write(*,*)-Anweisung wurde bereits erwähnt, dass das erste * für die Standardausgabe steht. Dies ist in der Regel der Bildschirm, das zweite Sternchen steht bisher für das listengesteuerte (oder Default-)-Ausgabeformat.

    An Stelle des ersten Sternchens kann nun eine sogenannte i/o unit number treten. Diese kann entweder einem Ausgabegerät z.B. einem Drucker oder einer Datei mit einem vorher zugewiesenen Namen zugeordnet sein.

    Das Verfahren, Ausgabegeräte über Ziffern kleiner 10 (z.B. den Drucker) anzusteuern, sollte nicht mehr verwendet werden. Erschwerend kommt hinzu, dass je nach Rechnerhersteller ist den Ziffern unter 10 jeweils ein anderes Peripheriegerät zugeordnet ist.

    Die unit numbers werden allerdings weiterhin benötigt und genutzt, und zwar als sogenannte logical units zur Datei-Ein- und Ausgabe auf die Festplatte.

    Durch eine open-Anweisung wird eine Zuordnung zwischen einer unit number und einer Datei eines bestimmten Namens auf der Festplatte geschaffen. Durch die Angabe dieser unit number in einer write oder read-Anweisung kann auf diese Datei sequentiell geschrieben oder gelesen werden.

    Beispiele zum File-I/0: file_io.f90

    Die open-Anweisung

    Durch eine open-Anweisung wird eine feste Zuordnung zwischen einer Datei und einer logischen (von Ihnen zu wählenden) i/o unit number hergestellt.
    Als Angaben innerhalb der open-Anweisung stehen der Reihe nach Angaben zu

    Wurde mit open erfolgreich eine unit number, z.B. 20 eine Datei auf der Festplatte zum Schreiben geöffnet , so kann mit

          write(20,*) <Variablenliste>  
    
    die in der Variablenliste gespeicherten Werte der Variablen nacheinander in eine Zeile geschrieben werden.

    Ein Beispiel zum Erstellen einer Datei auf der Festplatte

    Mit schreiben.f90

program schreiben
   implicit none
   integer :: io_error
   integer :: n

   open(unit=20,file='wertetabelle.txt',status='new',action='write', &
        iostat=io_error)

      if ( io_error == 0) then

         do n = 1, 5
         write(20,*) n, real(n)
         end do
     
       else
           write(*,*) 'Beim OEffenen der Datei ist ein Fehler Nr.', &
                       io_error,' aufgetreten'
       end if
   close(unit=20)
                   
end program schreiben

lässt sich die Datei wertetabelle.txt

           1   1.000000    
           2   2.000000    
           3   3.000000    
           4   4.000000    
           5   5.000000    
in das aktuelle Verzeichnis auf der Festplatte schreiben.

Ein einfaches Beispiel zum Lesen von Informationen aus einer Datei

Dieses Programm prüft nur auf Fehler, die in Zusammenhang mit open auftreten können: lesen.f90


program lesen
   implicit none
   integer :: io_error
   integer :: n
   integer :: i
   real    :: y 
    
   open(unit=20,file='wertetabelle.txt',status='old',action='read', &
        iostat=io_error) 

      if ( io_error == 0) then

         do n = 1, 5 
         read(20,*) i, y 
         write(*,*) n,'-te Zeile:', i, y 
         end do
     
       else
           write(*,*) 'Beim OEffenen der Datei ist ein Fehler Nr.', & 
                       io_error,' aufgetreten' 
       end if
   close(unit=20) 
                   
end program lesen

Bildschirmausgabe: lesen.erg

           1 -te Zeile:           1   1.000000    
           2 -te Zeile:           2   2.000000    
           3 -te Zeile:           3   3.000000    
           4 -te Zeile:           4   4.000000    
           5 -te Zeile:           5   5.000000    
Empfehlenswert ist es, zusätzlich auf Fehler zu prüfen, die während des Lesens eines Datensatzes auftreten können:

Fehlererkennung und Behandlung über iostat in der read-Anweisung

Zunächst soll anhand eines Beispiels gezeigt werden, wie mit iostat in einer read-Anweisung Fehlersituationen, die bei der Ausführung des Programms auftreten können, abfangen kann.

Typischerweise würde ein Laufzeitfehler (run time error) auftreten, wenn mit read ein real-Wert eingelesen werden soll, aber bei der Ausführung der read-Anweisung ein character-Zeichen an die real-Variable übergeben wird.

Anwendungsbeispiel:

Mit dem einfachen Programm real_wert_einlesen.f90 wird die obige Situation nachgebildet:

program real_wert_einlesen
   implicit none
   real :: wert

   write(*,*) 'Geben Sie einen Wert vom Datentyp real ein: '
   read(*,*) wert
   write(*,*) 'Ihre Eingabe : ', wert
   
end program real_wert_einlesen

Im Regelfall zeigt dieses Programm das gewünschte Verhalten:

 Geben Sie einen Wert vom Datentyp real ein: 
2.0
 Ihre Eingabe :    2.000000    

Gibt ein Anwender versehentlich statt einer Zahl einen Buchstaben ein, kommt es - wie beschrieben - während der Programmausführung zu einem Laufzeitfehler

 Geben Sie einen Wert vom Datentyp real ein: 
a
forrtl: severe (59): list-directed I/O syntax error, unit -4, file /dev/pts/0
   0: __FINI_00_remove_gp_range [0x3ff81a6c374]
   1: __FINI_00_remove_gp_range [0x3ff81a6c8f4]
   2: __FINI_00_remove_gp_range [0x3ff81a94e68]
   3: __FINI_00_remove_gp_range [0x3ff81a94538]
   4: werteinlesen_ [real_wert_einlesen.f90: 6, 0x120001ca4]
   5: main [for_main.c: 203, 0x120001bcc]
   6: __start [0x120001b48]

Durch eine Erweiterung der read-Anweisung mit Fehlerabfang-Routinen über iostat lässt sich der Quellcode erheblich verbessern ( real_wert_einlesen_mod.f90):

program real_wert_einlesen_mod
   implicit none
   real    :: wert
   integer :: io_err

   write(*,*) 'Geben Sie einen Wert vom Datentyp real ein: '
   read(*,*,iostat=io_err) wert

   write(*,*) 'Rueckgabewert von iostat:  io_err = ', io_err
   if ( io_err == 0) then
      write(*,*) 'Ihre Eingabe : ', wert
      else
         stop '=> Fehler in der Eingabe! Programmabbruch.'
    end if
   
end program real_wert_einlesen_mod

In diesem Fall erhält man bei einem "Data type mismatch" statt des Laufzeitfehlers folgende Bildschirmausgabe:

 Geben Sie einen Wert vom Datentyp real ein: 
a
 Rueckgabewert von iostat:  io_err =           59
=> Fehler in der Eingabe! Programmabbruch.

Im Fehlerfall wird eine positive ganze Zahl zurückgegeben. Der genaue Zahlenwert ist betriebssystem- und compilerabhängig und lässt sich in der Dokumentation zum Compiler nachlesen. Nach Fortran-Standard ist nur festgelegt, dass die Zahl grösser als 0 sein muss. Wenn Sie also Programme nach Fortran-Standard schreiben möchten (was sich zur besseren Portierbarkeit der Programme sehr empfiehlt), reicht zum "error handling" die Abfrage, ob iostat gröszlig;er als 0 sei, vollkommen aus.

Will man bei einer fehlerhaften Eingabe den Anwender erneut einen Wert eingeben lassen, so kann man dies mit einer zusätzlichen do-Schleife realisieren (real_wert_einlesen_cycle.f90).

program real_wert_einlesen_cycle
   implicit none
   real    :: wert
   integer :: io_err 

   do 
      write(*,*) 'Geben Sie einen Wert vom Datentyp real ein: '
      read(*,*,iostat=io_err) wert

      if ( io_err == 0) then
         write(*,*) 'Ihre Eingabe : ', wert
         exit 
      else
         cycle
      end if
   end do
end program real_wert_einlesen_cycle

In diesem Fall erhält man als exemplarische Bildschirmausgabe

 Geben Sie einen Wert vom Datentyp real ein: 
a
 Geben Sie einen Wert vom Datentyp real ein: 
2.0
 Ihre Eingabe :    2.000000    

iostat in Zusammenhang mit dem Einlesen von Werten aus Dateien

Fehlerabfangroutinen lassen sich über iostat sowohl beim Öffnen einer Datei (open) als auch beim Einlesen von Werten aus Dateien (read) einsetzen.

Insbesondere lässt sich über iostat bei read feststellen, wann das Dateiende erreicht wird: einlesen.f90

program einlesen

! Beispiel zum Einlesen von Datensaetzen aus Dateien
! mit Fehlererkennung

implicit none


character(len=20) :: filename     ! Name des Files
integer :: anzahl = 0             ! Anzahl der eingelesenen Werte, 
                                  ! hier gleichzeitig Nummer des Datenstatzes
integer :: status_open            ! Rueckgabewert aus iostat bei open
integer :: status_read            ! Rueckgabewert aus iostat beim 
                                  ! Einlesen der Daten mit read

real :: wert                      ! eingeleser Wert
                                   


! Interaktive Eingabe des Filenamens

write(*,*) ' Bitte geben Sie den Namen der zu lesenden Datei'
write(*,'(A$)') ' (nicht in Anfuehrungszeichen eingeschlossen) an: '
read(*,'(A)') filename
write(*,*) ' Als Name der Datei wurde eingelesen: ', filename
write(*,*)
 

! OEffnen der Datei mit Abfangen von I/O-Fehlern
open(unit=25, file=filename, status='old', action='read', iostat=status_open)

oeffnen: if ( status_open == 0 ) then
         ! Beim OEffnen der Datei sind keine Fehler aufgetreten
         ! es geht weiter mit dem Einlesen der Werte


          einlese_schleife: do
          read (25,*,iostat=status_read) wert   ! Einlesen des Wertes 
          if ( status_read /= 0 ) exit          ! Programm wird am Ende 
                                            ! der einlese_schleife fortgesetzt,
                                            ! wenn beim Einlesen des Wertes
                                            ! ein Fehler auftritt oder das
                                            ! Dateiende erreicht wurde 

           anzahl = anzahl + 1              ! Anzahl der eingelesenen Werte 
                                            ! hier gleich Zeilennummer

           !Bildschirmausgabe
           write(*,*) ' Zeilennummer = ', anzahl, ' Wert =', wert 
           end do einlese_schleife       

           ! hier geht's weiter, wenn status_read /= 0 war, deshalb:
           ! Behandlung der 2 moeglichen Faelle mit status_read <> 0 
           readif: if ( status_read > 0 ) then 
                      write(*,*) 'Beim Lesen von Zeile ', anzahl+1, &
                      ' ist ein Fehler aufgetreten'
                                   
                      else ! status_read < 0 
                           ! das Dateiende (end of file = EOF) wurde erreicht 
                           ! der Benutzer wird darueber explizit infomiert und
                           ! die Gesamtanzahl der Datensaetze 
                           ! wird nochmals ausgegeben

                           write(*,*)
                           write(*,*) ' Hinweis: das Dateiende wurde erreicht'
                           write(*,*) ' => In der Datei sind insgesamt ', & 
                                      anzahl, 'Werte'
           end if readif



           ! die noch ausstehende Behandlung des Fehlerfalls 
           ! beim OEffnen der Datei
           ! status_open <> 0 wird hier behandelt

           else oeffnen
             write(*,*) 'Beim OEffnen der Datei trat &
                         &  Systemfehler Nr. ', status_open,' auf'

end if oeffnen

close( unit=25 )  !Datei schliessen

end program einlesen

Beispieldateien zum Ausprobieren:

Positionierung innerhalb einer geöffneten Datei

Falls mit open eine Verknüpfung zwischen einer unit number und einer Datei hergestellt wurde, kann man mit
    backspace(unit=< unit number >)
einen Datensatz (d.h. eine Zeile) "zurückgespult" werden.

An den Dateianfang gelangt man mit

    rewind(unit=< unit number >)
Die beiden obigen Kommandos werden in der Regel nur in Zusammenhang mit scratch-Dateien benötigt.

Öffnen einer Datei zum Anhängen von weiteren Werten

Ergänzt man die open-Anweisung durch den Zusatz position = 'append' kann an eine bereits bestehende Datei weitere Daten angehängt werden, z.B. könnte eine open-Anweisung zum Änhängen an die bereits bestehende Datei 'wertetabelle.txt' lauten
   open(unit=< unit number >, file='wertetabelle.txt', status='old', action='write',position='append',iostat=io_err) 

Die close-Anweisung

Die close-Anweisung wurde bereits in jedem der obigen Programmbeispiele verwendet.
    close(unit=< unit number >)
oder kompakter
    close(< unit number >)
dient dazu, um die vorher durch die entsprechende open-Anweisung zwischen einer unit number und einer Datei auf der Festplatte geschaffene Zuordnung wieder aufzuheben. Bei Erreichen des Programmendes (end program ...) würde ohnehin die Zuordnungen aus open wieder aufgehoben. Jedoch sollte man aus mindestens 3 Gründen explizit jede Zuordnung zwischen unit number und Datei wieder aufheben, sobald diese nicht mehr benötigt wird:

Eine Modifikation der close-Anweisung erlaubt, Dateien mit dem "Schließen" gleichzeitig zu löschen. Dazu wird wird status='delete' eingefügt.

    close(unit=< unit number >,status='delete')

Um mögliche Fehler bei close-Anweisungen abfangen zu können, gibt es analog zu open bei close die Methodik mit iostat.

Wird status='delete' nicht angegeben, geht der Compiler davon aus, dass status='keep' gilt und die Datei bleibt natürlich erhalten. Achtung: Erst wenn eine mit open geöffnete Datei mit close oder durch Erreichen des Programmendes wieder geschlossen wurde, sollte von anderen Prozessen des Betriebssystems aus (z.B. mit einem Editor) auf diese Datei zugegriffen werden.

Die inquire-Anweisung

Mit inquire lassen sich z.B. Informationen über den Zustand einer Datei gewinnen, bevor diese geöffnet wird. Will man z.B. feststellen, ob eine Datei des Namens wertetabelle.txt vorhanden ist, so geht dies z.B. mit
   inquire(file='wertetabelle.txt',exist=vorhanden)  
Die Variable vorhanden muss dazu vorher als vom Datentyp logical deklariert worden sein.
Existiert die Datei im aktuellen Verzeichnis, so ist wird der Wert von vorhanden .true., falls die Datei nicht vorhanden wäre, so würde vorhanden der Wert .false. zugewiesen.

Anhand des Wertes der Variable vorhanden lässt sich bei Bedarf das Programm sinnvoll verzweigen. Soll z.B. verhindert werden, dass eine bereits vorhandene Datei überschrieben wird, so kann der Anwender gefragt werden, ob

Zurück zur Vorlesungsseite


Heidrun.Kolinsky@uni-bayreuth.de
(Dr. Heidrun Kolinsky, Rechenzentrum der Universität Bayreuth, Gebäude NW2, Raum 159, Universitätsstraße 30, D-95440 Bayreuth, Tel. 0921/55-2687)