Programmieren in Fortran 90/95


  1. Unterprogramme
  2. Steht man vor einer komplexeren Programmieraufgabe, so geht man am besten nach dem logischen Prinzip des Top-Down-Entwurfs vor. Ein wesentlicher Schritt besteht darin, das generalisierte Problem in einzelne Teilschritte zu zerlegen. Diese Teilaufgaben können wiederum sukzessive in einzelne weitere Unteraufgaben verfeinert werden.

    Fortran besitzt eine spezielle Struktur, um diese logische Untergliederung als Programmcode realisieren zu können. So kann jeder einzelne, in sich logisch geschlossene Teilschritt als eigenes Unterprogramm entwickelt und unabhängig vom restlichen Programmcode getestet werden.

    Die einzelnen Unterprogramme bilden in sich geschlossene separate Programmeinheiten. Der Informationsaustausch zwischen den einzelnen Programmteilen eines Fortran-Programms, zu denen auch das Hauptprogramm gehört und z.B. einem Unterprogramm ist über Schnittstellen geregelt.

Vorteile von Unterprogrammen

  1. Unabhängiges Entwicklen und Testen der Teilaufgaben
      Jede einzelne Teilaufgabe kann in Code umgesetzt und als unabhängiger Teil compiliert werden. Die Tests der einzelnen Programmeinheiten können unabhängig von anderen Programmeinheiten stattfinden. Dadurch wird die Wahrscheinlichkeit für das Auftreten unerwünschter Interferenzen zwischen einzelnen Programmteilen reduziert.
      Die Programmentwicklung - und das Programm - werden übersichtlicher, klarer strukturiert und weniger fehleranfällig.

  2. Wiederverwendbarer Code
      Die in sich logisch abgeschlossenen Unterprogrammeinheiten lassen sich in der Regel im aktuellen Programm oder in anderen Programmen orginalgetreu oder leicht modifiziert wieder einsetzen.
      Die Entwicklungs- und Testzeiten sowie die Programme werden kürzer.

  3. Klare Struktur
      Zwischen den einzelnen Programmeinheiten ist die Werteübergabe klar geregelt. Lokale Variablen sind nur in der Programmeinheit gültig, in der sie deklariert wurden (sogenanntes data hiding). Die lokalen Variablen können auch nur innerhalb ihrer Programmeinheit verwendet und modifiziert werden. Die Wahrscheinlichkeit, dass in Folge von Programmänderungen an einer Stelle des Codes unbeabsichtigte Folgen an anderen Stellen des Programms auftreten, wird geringer.
      Die Gefahr von "Programmierunfällen" wird vermindert.
      Die Fehlersuche wird erleichtert.

Die Übersichtlichkeit modular erstellter Programme wissen all diejenigen zu schätzen, die fremdentwickelte Programme betreuen und erweitern müssen.

An der Uni kommt es z.B. recht häufig vor, dass bereits vorhandene Programme von Doktoranden und Diplomanden verwendet, ausgebaut und umgebaut werden sollen. Falls Sie hier ein schlecht dokumentiertes Spaghetti-Code-Programm erben sollten, werden Sie mit Sicherheit wenig Freude haben und einen Großteil Ihrer Zeit darauf verwenden müssen, herauszufinden, was das ererbte Programm eigentlich tut. Der nächste Schritt, Änderungen einzubauen und zu prüfen, ob die Algorithmen wirklich korrekt arbeiten, ist nahezu immer ein schier hoffnungsloses Unterfangen.

Ein persönlicher Tip: Falls nach Ansicht Ihres Betreuers die Ihnen angebotene Diplomarbeit oder Dissertation zu einem grossen Teil daraus bestehen sollte, ein "bewährtes" Spaghetti-Code-Programm eines ihrer Vorgänger umzubauen - gehen Sie lieber - solange noch Zeit ist - an einen anderen Lehrstuhl, der Ihnen die Möglichkeit zu einer echten Forschungsarbeit bietet.

Wenn Sie Programme erstellen, können Sie sich Ihre Arbeit durch den Einsatz von Unterprogrammen erheblich erleichtern!

Prinzipiell kennt Fortran zwei Arten von Unterprogrammen:

subroutine - Unterprogramme

Diese werden von einer anderen Programmeinheit über

call <Name der Subroutine >

bzw. über

call <Name der Subroutine > (Liste der aktuellen Parameter)

aufgerufen. Die zweite Version wird verwendet, wenn zwischen einzelnen Programmeinheiten (z.B. zwischen dem Hauptprogramm und dem Unterprogramm) Informationen ausgetauscht werden sollen.

Die subroutine bildet eine eigene, in sich geschlossene Programmeinheit mit folgender Struktur:

      subroutine <Name der Subroutine> (Liste der formalen Parameter)
            implicit none 
          ! Datentypdeklaration der formalen Parameter
             ...
          ! Datentypdeklaration der lokalen Variablen
             ...
          ! Anweisungsteil der Subroutine
             ...
             ...
      return 
      end subroutine <Name der Subroutine>

In der Liste der formalen Parameter werden als Platzhalter Namen von Variablen aufgeführt. Unmittelbar nach der Kopfzeile des Unterprogramms müssen die formalen Parameter in gewohnter Weise mit Namen und zugehörigem Datentyp deklariert werden.

Beim Aufruf der Subroutine aus einer anderen Programmeinheit heraus muss die Reihenfolge der an das Unterprogramm übergebenen Werte bzw. Variablen (d.h. die Liste der aktuellen Parameter) vom Datentyp und der Zuweisungsreihenfolge her genau mit der Liste der formalen Parameter übereinstimmen.

Ein einfaches Anwendungsbeispiel mit einer subroutine

Download: unterprogramm_demo1.f90, Bildschirmausgabe: unterprogramm_demo1.erg

program unterprogramm_demo1
  implicit none
  real :: a, b, c
  
  write(*,*) 'Berechnet die Hypotenusenlaenge eines rechtwinkligen Dreiecks' 
  write(*,*) 'Bitte die Laenge der Seiten eingeben:'
  read(*,*)  a, b

  call hypotenuse(a,b,c)  ! a, b wurden interaktiv eingelesen
                          ! c, die Hypotenusenlaenge wird berechnet
  write(*,*) 'Die Laenge der Hypotenuse betraegt: ', c

  call hypotenuse(3.0,4.0,c)  ! Statt Variablen, zu denen 
                              ! Werte einlesen wurden, 
                              ! koennen auch fuer die beiden
                              ! ersten formalen Parameter in der
                              ! Subroutine direkt Werte uebergeben 
                              ! werden
  write(*,*) 'Die Laenge der Hypotenuse bei a=3.0, b= 4.0 betraegt: ', c
end program unterprogramm_demo1




subroutine hypotenuse(x, y, z) ! Ein Unterprogramm des Typs subroutine
  ! Deklaration der formalen Parameter
  implicit none 
  real, intent(in)  :: x, y    
  real, intent(out) :: z
 
  ! Deklaration der lokalen Variablen
  real :: wert
  
  wert = x*x + y*y
  z = sqrt(wert)
  return   
end subroutine hypotenuse

Die Subroutine hypotenuse wird im Hauptprogramm erstmals über die Anweisung

        call hypotenuse(a,b,c) 
aufgerufen. Zuvor wurden den Variablen a und b durch die Anweisung
        read(*,*)  a, b
interaktiv durch den Anwender Werte zugewiesen. Beim Aufruf der Subroutine werden diese Werte an die formalen Parameter x und y übergeben. In Fortran 90 und 95 wird bei der Deklaration der formalen Parameter im Unterprogramm durch das Attribut intent(in) die Richtung der Informationsübergabe gekennzeichnet
        real, intent(in)  :: x, y 
Das Attribut intent(in) kennzeichnet, dass die Information von der aufrufenden Programmeinheit an das Unterprogramm übergeben wird und damit die Richtung des Informationsflusses. Demententsprechend kennzeichnet das Attribut
        real, intent(out) :: z
dass die Information im Unterprogramm berechnet und an die aufrufende Programmeinheit (in diesem Fall das Hauptprogramm) zurücküberreicht wird. Der Rücksprung zur aufrufenden Programmeinheit wird eingeleitet, sobald der Programmablaufzeiger ein return ( oder die end-Anweisung eines Unterprogramms erreicht). In einem Unterprogramm können mehr als ein return stehen. Prinzipiell sollte jedoch zur Sicherheit vor der end-Anweisung eines Unterprogramms ein return eingefügt werden, weil es ab und zu Compiler gab (gibt), die unter Umständen ohne return falsche Rückgabewerte abgeliefert haben (abliefern würden?).

Das "pass by reference scheme"

Oder: was passiert beim Aufruf von Unterprogrammen?

Beim Aufruf von Unterprogrammen werden zur Informationsübermittlung Zeiger (engl. pointer) auf die Speicherplätze der Variablen im Hauptspeicher übergeben, an denen die in der Liste der aktuellen Parameter angelegten Speicherplätze der Variablen zu finden sind.

Zum Beispiel sind im obigen Programm unterprogramm_demo1.f90 in der Anweisung

     call hypotenuse(a,b,c)
als aktuelle Parameter a, b und c angegeben. Diese Variablen wurden im Deklarationsteil des Hauptprogramms als dem Datentyp real zugehörig vereinbart.

Die Kopfzeile des Unterprogramms beginnt mit

     subroutine hypotenuse(x, y, z) 
      ! Deklaration der formalen Parameter
      implicit none 
      real, intent(in)  :: x, y    
      real, intent(out) :: z
Im Unterprogramm erscheinen die formalen Parameter als x, y, z. Sowohl von der Reihenfolge als auch von den Datentypen her muss die Liste der aktuellen Parameter mit der Liste der formalen Parameter übereinstimmen.

Der Mechanismus des Informationsaustauschs zwischen aufrufender Programmeinheit und Unterprogramm wird als "pass by reference scheme" bezeichnet. Das "pass by reference scheme" stellt sich graphisch wie folgt dar:

pass by reference scheme

Beim Aufruf eines Unterprogramms werden also nicht die aktuellen Werte, sondern nur Zeiger (Pointer) auf die Speicherplätze übergeben, in denen die jeweiligen Werte abgelegt sind. Da Werte vom Datentyp real im allgemeinen 4 Bytes im Memory belegen, wird für den formalen Parameter x der Subroutine beispielsweise ein Zeiger auf den Speicherplatz 0001 übergeben. Die Speicherplätze 0001, 0002, 0003 und 0004 werden benötigt, um den real-Wert für die Variable a abzulegen, demzufolge wird an den formalen Parameter y beim Aufruf der Subroutine ein Zeiger überreicht, der dem Speicherplatz 0005 zugeordnet ist und an dem der Speicherbereich für den aktuellen Wert b beginnt. Analog erfolgt die Zuordung zwischen dem formalen Parameter z und dem aktuellen Parameter c.

Im Diagramm bezeichnet die Richtung des Pfeils nach rechts, dass vom dem Unterprogramm Information vom einem Speicherplatz gelesen werden sollen (intent(in)). Wird ein formaler Parameter mit dem Attribut (intent(out)) deklariert, wird von der Subroutine Information an dem Speicherplätzen abgelegt, auf den der entsprechende Pointer zeigt.

Soll durch den Aufruf eines Unterprogramms der Wert einer übergebenen Variablen durch den Unterprogrammaufruf verändert werden, so muss dementsprechend als Attribut intent(inout) gesetzt werden.

Ein Beispiel:
In einer Firma soll bei der Rechnungserstellung für treue Kunden noch 3 Prozent Rabatt von der Rechnungsendsumme abgezogen werden. Nach der Prüfung der Voraussetzungen, ob ein zusätzlicher Rabatt gewährt werden soll, erfolgt von einer Programmeinheit aus der Aufruf des Unterprogramms

       ...
       pruefe_rabattvorausssetzung: if (...) then 
          call gewaehre_3_prozent_rabatt(betrag)
          write(*,'(1X,A)') 'Als treuen Kunden gewaehren wir Ihnen &
                            &3 Prozent Rabatt.' 
          else 
             write(*,*)
       end if pruefe_rabattvoraussetzung
      
       write(*,'(1X,A)') 'Vielen Dank fuer Ihren Einkauf!'
       write(*,'(//1X,A,F12.2,1X,A5)') 'Bitte ueberweisen Sie ', &  
                                        betrag, 'Euro.'
        ... 
Dementsprechend könnte die Subroutine lauten:
        subroutine gewaehre_3_prozent_rabatt(summe)
           implicit none
           real, intent(inout) :: summe 
           summe = summe*0.97 
           return  
        subroutine gewaehre_3_prozent_rabatt(summe)
Hier ist es notwendig, dass als Attribut intent(inout) bei der Deklaration des formalen Paramters summe steht.

Beim Aufruf des Unterprogramms

          call gewaehre_3_prozent_rabatt(betrag)
wird ein Zeiger auf den Speicherplatz von betrag übergeben ("pass by reference scheme"). Von dort liest das Unterprogramm den Wert aus und verarbeitet diesen als Wert der Variablen summe weiter. Erreicht der Programmablaufzeiger im Unterprogramm die return-Anweisung, wird der aktuelle Wert der Variablen summe in den Speicherplatz von betrag geschrieben. Daraufhin wird das Programms mit dem sich an die call-Anweisung anschließenden Befehl fortgesetzt.

Steht als Attribut eines formalen Parameters intent(in), ist es dem Unterprogramm untersagt, den dort gespeicherten Wert zu verändern. Von diesem Speicherplatz dürfen vom Unterprogramm ausschließlich Information gelesen werden.

Wird ein formaler Parameter mit dem Attribut intent(out) deklariert, darf dementsprechend das Unterprogramm am Speicherplatz dieser Variablen nur Information ablegen und dadurch Informationen an die aufrufende Programmeinheit weitergeben. Das Unterprogramm darf keinesfalls versuchen von einem als intent(out) deklarierten formalen Parameter Information zu erhalten.

Bei Widersprüchen zwischen der Deklaration der formalen Parameter und dem Unterprogrammcode geben Fortran 90/95-Compiler oft nur bei Aktivierung bestimmter Compileroptionen eine Fehlermeldung aus und der Compiliervorgang wird abgebrochen. Ohne diese strengen Compileroptionen zum Datentypabgleich kann es passieren, dass ein Programm scheinbar fehlerfrei compiliert wird. Startet man das Executable, treten in diesem Fall jedoch Fehler im Programmablauf auf, deren Ursache in Typ-Mismatches zwischen den aktuellen Parametern beim Unterprogrammaufruf und der Datentypdeklaration bei den formalen Parametern des Unterprogramms liegt.

Fazit: Durch die Implementierung definierter Schnittstellen zum Informationsaustausch zwischen Hauptprogramm und Unterprogramm und durch die Verwendung nur lokal im Unterprogramm gültiger Variablen lassen sich unerwünschte Seiteneffekte durch ein versehentliches Verändern von Variablenwerten vermeiden. Ein Unterprogramm stellt eine Art in sich gekapselte Untereinheit dar ("black box" oder "information hiding" ), deren Interaktion mit dem Hauptprogramm bzw. der aufrufenden Programmeinheit allein über die definierte Schnittstelle im Unterprogrammkopf (die Liste der formalen Parameter) geregelt ist.

Die FORTRAN 77 - Syntax

Fortran 90/95 stellt eine Fortentwicklung von FORTRAN 77 dar. Das Konzept mit dem intent-Attribut bei der Deklaration der formalen Parameter eines Unterprogramms war in FORTRAN 77 noch nicht vorhanden. Die erweiterte Fortran 90/95 - Syntax steigert die Strukturqualität und die Übersichtlichkeit der Programme und reduziert die Fehleranfälligkeit.

Ein Vorab-Hinweis: Eine weitere Verbesserung der Qualität beim Datenaustausch stellen in Fortran 90/95 die Module als Weiterentwicklung der veralteten COMMON-Blöcke dar.

function - Unterprogramme

Anders als eine subroutine besitzt eine function genau einen Rückgabewert. Man kann sich dies ähnlich einer mathematischen Funktion vorstellen, bei der genau ein Funktionswert aus dem/den Variablenwert/en berechnet wird. Vor einer function muss der Datentyp des Rückgabewertes deklariert werden.

 
  <Datentyp d. Rueckgabewerts> function <Name der Function> (Liste 
                                  der formalen Parameter)
         implicit none 
        ! Datentypdeklaration der formalen Parameter
        ..., intent(in) :: ...     ! ausschliesslich intent(in) 
        ..., intent(in) :: ...     ! fuer die formalen Parameter 
        ...
        ! Datentypdeklaration der lokalen Variablen
        ...
        ! Anweisungsteil der Function
        ...
        ...
        <Name der Function> = ... ! abschliessende Wertzuweisung 
        ! des Rueckgabewertes
        return 
  end function <Name der Function>

Will man eine function von einer anderen Programmeinheit aus aufrufen, so gibt man nur den Namen der Function an. Jedoch muss bei der Variablen-Deklaration in der aufrufenden Programmeinheit der Datentyp der Funktion mit aufgelistet werden, damit der Datenaustausch zwischen zwischen aufrufender Programmeinheit und function -Unterprogramm reibungslos vonstatten gehen kann.

In der aufrufenden Programmeinheit muss expliziert im Deklarationsteil (am besten an dessen Ende) die Funktion zusammen mit dem dazugehörigen Datentyp deklariert werden.

     ! In der aufrufenden Programmeinheit muss 
     ! (am besten am Ende des Deklarationsteils) stehen: 
       
       <Datentyp der function> ::  <Name der function>
       
       ....
     ! innerhalb der aufrufenden Programmeinheit kann nun die 
     ! function ueber Ihrem Namen bei entsprechender Angabe der
     ! aktuellen Parameter aufgerufen werden 

Um diesen noch recht abstrakten Sachverhalt zu veranschaulichen, soll die Berechnung der Länge der Hypotenuse eines rechtwinkligen Dreieck diesmal nicht mit einer subroutine, sondern mit einer function realisiert werden.

Da bei der Berechnung der Hypotenusenlänge der Wert der den rechten Winkel einschließenden Dreiecksseiten nicht verändert wird und sich die Länge der Hypotenuse aus der Wurzel der Summe der Quadrate der beiden Seitenlägen ergibt (mathematisch gesehen eine eindeutige Zuordnung), ist es sinnvoll mit einer function zu arbeiten.


Das obigen Programm wurde einfach umgeschrieben: (
unterprogramm_demo2.f90)

program unterprogram_demo2
   implicit none
   
   real :: hypotenusen_laenge   ! der Datentyp des Rueckgabewertes 
                                ! einer Function muss in der aufrufenden
                                ! Programmeinheit deklariert werden
   
   real :: a, b, c

   write(*,*) 'Berechnet die Hypotenusenlaenge eines rechtwinkligen Dreiecks'
   write(*,*) 'Bitte die Laenge der Seiten eingeben:'
   read(*,*)  a, b              ! a, b wurden interaktiv eingelesen

   ! c, die Hypotenusenlaenge wird berechnet
   c = hypotenusen_laenge(a,b) 

   write(*,*) 'Die Laenge der Hypotenuse betraegt: ', c

   c = hypotenusen_laenge(3.0,4.0)  ! Statt Variablen, zu denen Werte einlesen
                            ! wurden, koennen auch fuer die beiden
                            ! ersten formalen Parameter der
                            ! Funktion direkt Werte uebergeben werden
   write(*,*) 'Die Laenge der Hypotenuse bei a=3.0, b= 4.0 betraegt: ', c
end program unterprogram_demo2


real function hypotenusen_laenge(x, y) 
   ! Deklaration der formalen Parameter
   implicit none
   real, intent(in)  :: x, y
   ! Deklaration der lokalen Variablen
   real :: wert

   wert = x*x + y*y
   hypotenusen_laenge = sqrt(wert)
   return
end function hypotenusen_laenge 

Wie wir sehen, stellt die letzte Zeile der function vor dem return (der Rücksprunganweisung) zur aufrufenden Programmeinheit eine Wertzuweisung dar. Hier wird der Wert von sqrt(wert) an den Speicherplatz der real-Variablen hypotenusen_laenge geschrieben und beim Rücksprung zur aufrufenden Programmeinheit der Zeiger (engl. pointer) auf diesen Speicherplatz übermittelt. Diese Art der Informationsübertragung zwischen Unterprogramm und aufrufender Programmeinheit wird wiederum "pass by reference scheme" bezeichnet.

Ein weiteres einfaches Beispiel für den Einsatz einer function (unterprogramm_demo3.f90, (Bildschirmausgabe: unterprogramm_demo3.erg)

program parabel
  implicit none
  integer :: i
  real    :: a, b, c
  real    :: x
  real    ::  f 

  write(*,*) 'Das Programm berechnet Werte zu'
  write(*,*) ' f(x) = a*x**2 + b*x +c'
  write(*,*) 'Bitte geben Sie die Werte fuer a, b und c ein:'
  read(*,*) a, b, c
        
  write(*,*) 'Wertetabelle fuer f(x) fuer x von 1 .. 10:'
  write(*,*) '   x          f(x)'
  write(*,*) '------------------'
        
  do i = 0, 10
    x = real(i)
    write(*,'(1X,F5.2,3X,F6.2)') x, f(x,a,b,c)
  end do

  write(*,*)
  write(*,*) 'Aufruf der Funktion f(x,a,b,c) mit direkter Werteuebergabe'
  write(*,*) 'z.B.  f(2.0,1.0,-1.0,-1.0) = ', f(2.0,1.0,-1.0,-1.0)

end program parabel


real function f (x,a,b,c)
  implicit none
  real, intent(in) :: x, a, b, c
  f = a * x * x + b * x + c
  return
end function f

In diesem Beispiel treten sowohl im Hauptprogramm (beim Funktionsaufruf) als auch in der function die Variablen (x,a,b,c) sowohl als aktuelle (im Hauptprogramm) als auch als formale Parameter (im Unterprogramm-Kopf) auf. Dies ist durchaus erlaubt und sinnvoll, wenn dadurch die Arbeitsweise des Gesamtprogramms transparenter wird. Genausogut wäre es möglich, den formalen Parametern im Unterprogramm ganz andere Namen als den Variablen im Hauptprogramm zu geben.

Entscheidend für eine fehlerfreie Compilierung ist allein, dass sowohl Reihenfolge als auch Datentypen beim Unterprogrammaufruf mit den formalen Parametern im Unterprogrammkopf 1:1 übereinstimmen. Dann können Sie sicher sein, dass Ihr Programm fehlerfrei compiliert wird.

Achtung:
Natürlich gilt auch im Zusammenhang mit Unterprogrammen, dass ein Compiler nur Fehler auf Syntax-Ebene und nicht auf der logischen Algorithmus-Ebene finden kann. Es bleibt wie immer Ihre Aufgabe sicherzustellen, dass Ihr Programm keine logischen Fehler enthält.

Beispiel für einen logischen Fehler in einem Unterprogramm, den den nur Sie als Mensch finden und eliminieren können:

Hätten Sie z.B. wie oben den Wert einer quadratischen Parabel mit

 a = 1.0, b = -1.0, c = -3.0
also den Wert von x**2 - x - 3 an der Stelle x = 2.0 berechnen wollen und versehentlich in einer falschen Reihenfolge die Liste der aktuellen Parameter statt mit f(x,a,b,c) - wie es aufgrund der Deklaration des Unterprogramms richtig gewesen wäre - fälschlicherweise f(a,b,c,x) in den Programmcode geschrieben, so stimmen zwar die Anzahl, Reihenfolge und Datentypen der formalen und aktuellen Parameter exakt überein, jedoch liegt ein schwerwiegender logischer Fehler vor, denn aufgrund des pass by reference schemes wird im Zahlenbeispiel statt (wie es mathematisch korrekt wäre)
   f(2.0,1.0,-1.0,-3.0) = 
   1.0 * 2.0 * 2.0 + (-1.0) * 2.0 + (-3.0) 
   = -1.0
nun aufgrund der falsch gewählten Reihenfolge in der Liste der aktuellen Parameter
   f(1.0,-1.0,-3.0,2.0)
   (-1.0) * 1.0 * 1.0 + (-3.0) * 1.0 + 2.0
   = -2.0
und damit ein falscher Zahlenwert berechnet.

Auf Compilerebene liegt kein syntaktischer Fehler vor, dennoch ist das Programm solange unbrauchbar, bis der logische Fehler in der Reihenfolge der aktuellen Parameter gefunden und beseitigt worden ist. Hier handelt es sich um einen typischen logischen Fehler, der nur von einem Menschen durch den Vergleich der Programmresultate mit unabhängig ermittelten Ergebnissen (je nach Anforderungsgrad: z.B. durch Kopfrechnen, Papier und Bleistift, Computeralgebrasystem, wissenschaftliche Veröffentlichungen der Konkurrenz) gefunden und eliminiert werden kann.

Merke: Das Beispiel zeigt, dass beim Aufruf von Unterprogrammen mit mehr als einem formalen Parameter eine sorgfätige Prüfung der Liste der aktuellen Parameter auf die logisch richtige Reihenfolge angeraten ist, um evtl. Rechenfehler aufgrund einer logisch falschen Zuordnung in der Reihung von formalen und aktuellen Parametern auszuschließen.

Übergabe von Datenfeldern (engl. Arrays) an Unterprogramme

Datenfelder (auch engl. arrays oder indizierte Variablen genannt) werden eingesetzt, wenn viele gleichartig strukturierte Daten mit einem Programm verarbeitet werden sollen. Zur Deklaration von Datenfeldern siehe das vorausgegangene Kapitel.

Wie wir gesehen haben, wird beim Unterprogrammaufruf ein Zeiger (engl. Pointer) auf den Beginn des Speicherplatzes übergeben, an denen der erste Wert des Feldes beginnt. Da in Fortran die Werte eines Arrays sequentiell (der Reihe nach) und nach der im Standard vereinbarten Indexreihenfolge (z.B. 2-dimensionale Datenfelder spaltenweise nacheinander) abgespeichert werden, muss an das Unterprogramm neben dem Namen des Feldes (dem aktuellen Parameter) zusätzlich die Information übermittelt werden, welche Gestalt das Datenfeld aufweist.

Zum Beispiel braucht bei einem eindimensionalem Array (einem Vektor) das Unterprogramm eine Mitteilung darüber, bis zu welchem maximalen Komponentenindex das Unterprogramm gehen darf. Dies ist notwendig, um zu vermeiden, dass aus dem Speicher Werte ausgelesen werden, die nicht mehr zu dem eindimensionalen Datenfeld gehören.

In der Regel wird an des Unterprogramm neben dem Namen des eindimensionalen Feldes zusätzlich die Anzahl der relevanten Komponenten eines Vektors übergeben.

Ein einfaches Beispiel, das noch das Risiko einer Bereichsüberschreitung bei der Ausgabe des Datenfeldes in sich birgt, ist z.B. vektor_uebergabe.f90

program vektor_uebergabe
   integer, parameter    :: nmax = 3
   real, dimension(nmax) :: vektor 
   integer               :: i 
   
   do i = 1, nmax                
      vektor(i) = real(i)     ! den Vektorkomponenten werden Werte zugewiesen
   end do 

   call ausgabe(nmax,vektor)  ! Ausgabe aller Komponenten des Vektors  
   call ausgabe(2,vektor)     ! Ausgabe der beiden ersten Komponenten
   call ausgabe(4,vektor)     ! Achtung: Fehler
                              ! Bereichsueberschreitung: 4 > nmax 
      
end program vektor_uebergabe


subroutine ausgabe(n,v)

! Achtung: Subroutine noch unzulaenglich programmiert
! Bereichsueberschreitung des Datenfeldes bei der Ausgabe moeglich

   implicit none
   ! Deklaration der formalen Parameter 
   integer, intent(in)               :: n 
   real, dimension(n), intent(in)    :: v
   ! Deklaration der lokalen Variablen
   integer :: i 

   write(*,*)
   write(*,*) 'Es werden die',n,'ersten Komponenten des Vektors ausgegeben.'
   write(*,*)
   do i = 1, n
      write(*,*) v(i) 
   end do
   write(*,*) 
return
end subroutine ausgabe

Im obigen Programm wurde die subroutine ausgabe dreimal nacheinander aufgerufen:

   call ausgabe(nmax,vektor)
   call ausgabe(2,vektor)
   call ausgabe(4,vektor)
Beim ersten Aufruf wird die Anzahl der auszugebenden Komponenten nmax (die Gesamtanzahl der Feldkomponenten) übergeben.

Beim folgenden Unterprogrammaufruf werden nur 2 der nmax(=3)-Komponenten angefordert und als Felddimension in der Unterroutine vereinbart. Dies ist durchaus zulässig, weil beim Unterprogrammaufruf nur ein Zeiger (pointer) auf den Beginn des Datenfeldes im Hauptspeicher übergeben wird und weil gleichzeitig der gewählte Wert 2 leiner als die Zahl der Datenfeldelemente bleibt.

Problematisch ist der 3. Aufruf.

   call ausgabe(4,vektor)
Hier werden als aktuelle Parameter (4,vektor) den formalen Parametern (n,v) der subroutine ausgabe(n,v) zugeordnet. Und damit wird das Unterprogramm aufgefordert, 4 Komponenten eines Datenfeldes auszugeben, welches nur 3 Komponenten umfasst.

Beim Aufruf des Unterprogramms wird der Zeiger auf den Beginn des Datenfeldes übergeben, im dem der Vektor v gespeichert ist. Was beim Unterprogrammaufruf nicht übergeben wird, ist die Information, wieviele Komponenten das Datenfeld umfasst. Dies führt dazu, dass ein falscher, nicht mehr zum Datenfeld gehöhrender, vierter Wert ausgeben wird.

Bildschirmausgabe zu vektor_uebergabe.f90

 
 Es werden die           3 ersten Komponenten des Vektors ausgegeben.
 
   1.000000    
   2.000000    
   3.000000    
 
 
 Es werden die           2 ersten Komponenten des Vektors ausgegeben.
 
   1.000000    
   2.000000    
 
 
 Es werden die           4 ersten Komponenten des Vektors ausgegeben.
 
   1.000000    
   2.000000    
   3.000000    
  1.3900881E-42

Der Compiler selbst bietet aufgrund des pass by reference-Mechanismus, (nämlich dass beim Unterprogrammaufruf nur ein Zeiger auf den Beginn eines Datenfeldes übergeben und nur noch auf die Gleichheit der Datentypen von aktuellem und formalen Parameter geprüft wird), im Unterprogramm evtl. Bereichsüberschreitungen festzustellen. Dies bleibt Aufgabe des Programmierers.

Deshalb könnte eine korrigierte Version des obigen Programms wie folgt aussehen (vektor_uebergabe_korr.f90):

program vektor_uebergabe
   integer, parameter    :: nmax = 3
   real, dimension(nmax) :: vektor 
   integer               :: i 
   
   do i = 1, nmax
      vektor(i) = real(i)
   end do 

   call ausgabe(nmax,vektor,nmax)  ! Ausgabe der Komponenten des Vektors  
   call ausgabe(nmax,vektor,2)     ! Ausgabe der beiden ersten Komponenten     
   call ausgabe(nmax,vektor,4)     ! Achtung: Fehler
                                   ! Bereichsueberschreitung: 4 > nmax 
      
end program vektor_uebergabe


subroutine ausgabe(n,v,m)
! Bereichsueberschreitung des Datenfeldes bei der Ausgabe 
! wird vermieden durch eine zusaeztliche Pruefung

   implicit none
   ! Deklaration der formalen Parameter 
   integer, intent(in)               :: n   ! Dimension des Datenfeldes
   real, dimension(n), intent(in)    :: v   ! Name des Datenfeldes
   integer, intent(in)               :: m   ! Anzahl der auszugebenden 
                                            ! Komponenten
   ! Deklaration der lokalen Variablen
   integer ::  m_local, i              

   m_local = m 

   if ( m_local > n ) then   
      write(*,*) '------------------------------------------------------&
                 &-----------------'
      write(*,*) 'Achtung:'
      write(*,*) 'Anzahl der Komponenten im Datenfeld: ', n
      write(*,*) 'gewuenschte Anzahl:                  ', m_local
      write(*,*)
      write(*,*) 'Die automatische Begrenzung der Anzahl der auszugebenden &
                  &Komponenten'
      write(*,*) 'tritt in Kraft'
      write(*,*) '------------------------------------------------------&
                 &-----------------'
      write(*,*)
      m_local = n
   end if

      write(*,*)
      write(*,*) 'Es werden die',m_local, &
                 'ersten Komponenten des Vektors ausgegeben.'
      write(*,*)
      do i = 1, m_local
        write(*,*) v(i) 
      end do
      write(*,*)
return
end subroutine ausgabe

Bildschirmausgabe zu vektor_uebergabe_korr.f90

 
 Es werden die           3 ersten Komponenten des Vektors ausgegeben.
 
   1.000000    
   2.000000    
   3.000000    
 
 
 Es werden die           2 ersten Komponenten des Vektors ausgegeben.
 
   1.000000    
   2.000000    
 
 --------------------------------------------------------------------
 Achtung:
 Anzahl der Komponenten im Datenfeld:            3
 gewuenschte Anzahl:                             4
 
 Die automatische Begrenzung der Anzahl der auszugebenden Komponenten
 tritt in Kraft
 --------------------------------------------------------------------
 
 
 Es werden die           3 ersten Komponenten des Vektors ausgegeben.
 
   1.000000    
   2.000000    
   3.000000    

Übergabe mehrdimensionaler Arrays an Unterprogramme

Sollen mehrdimensionale Arrays zwischen einzelnen Programmeinheiten übergeben werden, muss man zunächst rekapitulieren, wie in Fortran 90/95 im Speicher Datenfelder ablegt werden. Mehrdimensionale Datenfelder werden intern dergestalt abgespeichert, dass zuerst der erste Index hochgezählt wird, dann erst der zweite und dann der dritte und so fort ... Bei einem zweidimensionalen Datenfeld (z.B. einer Matrix) entspricht dies einer spaltenweisen sequentiellen Sicherung der einzelnen Komponenten.

Liegt zum Beispiel eine n x m Matrix a mit n Zeilen und m Spalten vor, deren Komponenten mathematisch als

     a(1,1) a(1,2) a(1,3) ... a(1,m) 

     a(2,1) a(2,2) a(2,3) ... a(2,m) 

     a(3,1) a(3,2) ...
     
     ...

     a(n,1) a(n,2) ...        a(n,m)
bezeichnet werden, so legt Fortran die Komponenten spaltenweise als
     a(1,1) a(2,1) a(3,1) ... a(n,1) a(1,2) a(2,2) .... a(1,m) ... a(n,m)
im Speicher ab. Durch den Namen eines Datenfelds wird ein Zeiger (Pointer) auf den Speicherplatz definiert, an dem die sequentielle Speicherung der Komponenten beginnt. Bei der statischen Deklaration einer Matrix wird aufgrund des Datentyps der pro Komponente benötigten Bytes berechnet. Durch das dimension-Attribut wird die Dimension und die Anzahl der Kompontenten des Datenfeldes festgelegt.

Bei der statischen Deklaration eines Datenfeldes müssen im Vereinbarungsteil feste Zahlenwerte eingetragen werden, damit der Compiler die Anzahl der insgesamt benötigten Speicherplätze festlegen kann, z.B.

    real,dimension(2,3) :: a 
Hiermit werden 2 x 3 x 4 Byte = 24 Byte Speicherplatz für die Matrix a reserviert.

Wichtig: ist zu Beginn die genaue Anzahl an Zeilen und Spalten noch nicht bekannt, muss man beim statischen Deklarationsverfahren über die Festlegung eines Maximalwertes für n und m (z.B. nmax=10 und mmax=10) einen geschätzten maximalen Speicherbedarf reservieren.

    integer, parameter        :: nmax = 10, mmax = 10
    real,dimension(nmax,mmax) :: a = 0.0
Für die Matrix a sind nun im Speicher 10x10x4 Byte = 400 Byte fest reserviert worden. Werden erst später im Programmcode die Werte von n und m eingelesen und zugewiesen, so bleiben im statischen Fall ungenutzte Lücken im reservierten Speicherplatz frei.

Angenommen, es stellt sich im Laufe des Programms heraus, dass die Anzahl der Zeilen mit 3 (n=3) und die der Spalten mit 2 (m=2) festgelegt wird. Dann wird zum einen aufgrund der Schätzung eigentlich unnötig viel Speicherplatz für die aktuellen Komponenten der Matrix a reserviert. Dies ist der grundlegende Nachteil der statischen Speicherallokierung, dass wegen der universelleren Einsatzbarkeit der Programme im einzelnen Anwendungsfall mehr Speicher reserviert wurde, als im konkreten Fall gerade notwendig ist. Ab Fortran 90 bietet die dynamische Speicherallokierung die Möglichkeit, universelle Programme für den Umgang mit Datenfeldern zu entwickeln, die genau soviel Speicher reservieren und verwenden, wie dies im konkreten Einsatz gerade notwendig ist.

Den Fall der statischen Speicherallokierung in Zusammenhang mit der Übergabe von Datenfeldern an Unterprogramme ist jedoch von prinzipiellem Interesse (evtl. noch in Bibliotheksfunktionen verwendet bzw. in FORTRAN 77) und soll deshalb weiter genauer betrachtet werden.

Zurück zu dem konkreten Zahlenbeispiel:
Da die Komponenten einer Matrix spaltenorientiert abgespeichert werden, sieht die interne Speicherbelegung für die Matrix a wie folgt aus:

     a(1,1) a(2,1) a(3,1) [7x4 Byte frei] a(2,1) a(2,2) a(2,3) [7x4 Byte frei] [80x4 Byte]
Das Unterprogramm braucht, um mit der Matrix richtig umgehen zu können, also mehr Informationen als nur den Datentyp der Komponenten und den Zeiger auf den Beginn des Datenfeldes:

Merke:
Wenn Unterprogramme statisch deklarierte, mehrdimensionale Datenfelder als Ganzes verarbeiten sollen, muss an Information an das Unterprogramm mindestens übermittelt werden:
  • Name des Datenfeldes (dadurch wird ein Zeiger auf den Beginn des Speicherbereiches übergeben.
  • den Datentyp der Komponenten
  • die bei der statischen Deklaration im Deklarationsteil vereinbarte Anzahl der Komponenten in mindestens allen Dimensionen bis einschließlich der vorletzten Dimension
  • die tatsächlich im Programmcode verwendete Anzahl der Komponenten in jeder Dimension bis einschlie▀lich in der vorletzten Dimension
  • Wurde nicht die Anzahl der vereinbarten Komponenten in der letzten Dimension angegeben und stattdessen * verwendet, so ist es notwendig, stattdessen die exakte Anzahl der vereinbarten Komponenten in der vorletzten Dimension anzugeben.
  • Um eine Überschreitung des Indexbereichs in der letzten Komponente zu vermeiden, ist es besser, auch für die letzte Dimension die in der statischen Deklaration festgelegte Anzahl an Komponenten an das Unterprogramm zu übergeben und analog zu dem Beispiel vektor_uebergabe_korr.f90 explizit auf eine mögliche Indexbereichsüberschreitung zu testen.

Die obigen Forderungen resultieren daher, dass innerhalb des Unterprogramms die Zugriffe auf ein Datenfeld sich ausschließlich innerhalb der gültigen Indexgrenzen bewegen darf. Dazu muss dem Unterprogramm mitgeteilt werden, wie das übermittelte Datenfeld strukturiert ist.

Im obigen Beispiel muss an das Unterprogramm dementsprechend mindestens die maximale Zeilenanzahl nmax und die tatsächliche Zeilenanzahl n sowie in der letzten Dimension mindestens die tatsächliche Anzahl an Spalten m angegeben werden, um die durch die Matrixkomponenten belegten Speicherbereiche exakt angeben zu können und jede belegte Matrixkomponente im Hauptspeicher aufsuchen zu können.

Die relative Position zu Beginn des Datenfeldes z.B. von a(i,j) berechnet sich aus ((j-1)*nmax + (i-1)) * 4 Byte, konkret findet sich im obigen Beispiel der Beginn des Speicherbereichs für a(3,2) (2-1)*10 + (3-1) = 12 Speicherplätze oder 12*4 = 48 Byte vom Beginn des Datenfeldes entfernt.

Konkrete Anwendungsbeispiele:
array_uebergabe.f90
array_uebergabe_2.f90

Ergänzende Bemerkungen

Bei der dynamischen Allokierung von Datenfeldern fallen natürlich die bei der statischen Allokierung benötigten Maximalwerte für die im Speicher zu reservierende jeweilige maximale Anzahl an Komponenten weg. An das Unterprogramm brauchen deshalb nur die jeweilige tatsächliche Anzahl an Komponenten übergeben zu werden.

Das folgende gilt sowohl für dynamisch als auch für statisch allokierte Datenfelder:
Für Arrays, die nicht wie üblich mit dem Index 1 beginnend deklariert werden, müssen bei Bedarf evtl. zusätzlich an das Unterprogramm die Informationen über die unteren Indexgrenzen übermittelt werden. Ob dies tatsächlich notwendig sein wird, hängt im Einzelfall von der Art der mathematischen Algorithmen ab, die Sie im Unterprogramm implementieren möchten. Manchmal reicht vielleicht im Programm evt. der relative Bezug des bzgl. des ersten Indices aus, manchmal vielleicht nicht.

Das save-Attribut in Unterprogrammen

Normalerweise sind die lokalen Variablen eines Unterprogramms nur solange gültig, bis das Unterprogramm wieder verlassen wurde. Nach Verlassen eines Unterprogramms sind die Werte der dort eingesetzten Variablen undefiniert.

Will man nun sicher erreichen, dass in einem Unterprogramm ermittelte Informationen beim nächsten Aufruf des Unterprogramms wieder zur Verfügung stehen, kann man die entsprechenden Variablen mit dem save-Attribut versehen, so dass diese Informationen gesichert werden (Fortran 90/95-Standard).

Das save-Attribut wird z.B. gebraucht, wenn man beim 2. Aufruf eines Unterprogramms Werte lokaler Variablen wieder verwenden möchte, die beim ersten Aufruf berechnet wurden. Dementsprechend falls man beim (n+1)-ten Aufruf eines Unterprogramms die beim n-ten Aufruf im Unterprogramm ermittelten Werte weiterverarbeiten möchte.

Hierzu müssen die lokalen Variablen, deren Wert beim nächsten Unterprogrammaufruf wieder zur Verfügung stehen soll, bei der Deklaration mit dem Attribut save versehen werden.

  <Datentyp>, save :: <Variablenname>

Beispiel: save_demo.f90, Bildschirmausgabe: save_demo.erg.

Rekursive Unterprogramm-Aufrufe

Seit Fortran 90 ist es möglich, dass ein Unterprogramm sich wieder selbst aufruft. Man spricht dann von einem rekursiven Unterprogrammaufruf. Die rekursiven Unterprogrammaufrufe kann z.B. bestimmte Sortieralgorithmen realisieren oder die Fakultät einer Zahl berechnen.

So ist z.B. 5! = 5·4·3·2·1 und 0! = 1. Damit lässt sich die Berechnung von z.B. 5! zerlegen in

   5! = 5 · 4!  
            4! = 4 · 3!  
                     3! = 3 · 2!  
                              2! = 1 · 1!  
                                       1! = 1 · 0!
                                                0! = 1   
Der Berechnungs-Algorithmus für die Fakultät lässt sich sukzessive beschreiben als
   Zerlege n! sukzessive in n! = n · (n-1)!
   solange bis als letzte Zerlegung 0!=1 erscheint 
   dann werte den resultierenden Gesamtausdruck aus 
Beispiel einer recursive subroutine zur Berechnung der Fakultät einer Zahl:

recursive subroutine factorial (n, ergebnis)
   implicit none
   integer, intent(in)  :: n
   integer, intent(out) :: ergebnis

   integer :: z  ! lokale Variable

   if ( n >= 1) then
        call factorial(n-1,z)
        ergebnis = n * z
      else
        ergebnis = 1
   end if

end subroutine factorial

Beispiel einer recursive function zur Berechnung der Fakultät einer Zahl:

recursive function fakultaet(n) result(produkt)
   implicit none
   
   integer,intent(in) :: n
   integer            :: produkt
   
   if ( n >= 1) then 
      produkt = n * fakultaet(n-1)
      else 
         produkt = 1
   end if
end function fakultaet

Beispielprogramm: recursive_demo.f90, Bildschirmausgabe: recursive_demo.erg.

Ein wichtiges Einsatzgebiet von sich selbst aufrufenden Unterprogrammen sind z.B. bestimmte Sortier-Algorithmen.

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)