Strukturierte Programmierung II: Zeiger


Inhalt dieses Kapitels

Was sind Zeiger?

Wir haben das Konzept der Zeiger schon an verschiedenen Punkten kennengelernt, zuletzt flüchtig bei IMAGECREATE. Hier noch einmal eine etwas genauere Erklärung von Hauptspeicherzeigern, im folgenden verkürzt "Zeiger" gennant, und dann eine Darstellung ihrer Verwendung.

Zeiger sind "Adressen". Adressen, die angeben, an welchem Ort eine Variable gespeichert wird. Das funktioniert bei Dateien, da diese byteweise Adressen besitzen. Das funktioniert genauso mit Variablen, da diese im Hauptspeicher gespeichert sind und der Hauptspeicher wie eine Datei byteweise durchnumeriert ist. Es gibt eine Adresse 0, 1, 2 bis 4294967295, wenn Sie 4G Hauptspeicher haben sollten. Und bei 3123457 sitzt unsere Variable. Also ist 3123457 die Adresse. Und die können wir wieder in einer Variablen speichern: p=3123457. Und das ist dann unser Zeiger.

Naturgemäss muss man ziemlich aufpassen, dass an dem Ort, an den eine Zeigervariable zeigt, etwas Sinnvolles steht. Daher ist das Arbeiten mit Zeigern immer mit einem gewissen Risiko für die Stabilität des Programms verbunden. Das Risiko hält sich in Grenzen, wenn man die Zeiger nicht missbraucht und sie für das verwendet, für was sie gedacht sind.

Wozu braucht man das? Darauf werden wir gleich ausführlich zu sprechen kommen. Hier sei nur kurz zusammengefasst: Für zwei Dinge. Einmal: Als Verweis oder Referenz. Man will einem Objekt Zugang zu den Informationen eines anderen Objekts verschaffen, ohne dass man das letztere global deklariert. Zum anderen: Mit Hilfe von Zeigern kann man Arrays so benutzen, dass ihre Grösse zur Laufzeit erst bestimmt wird. Das nennt man dann "dynamische Reservierung" oder "dynamische Allokation".

Verwendung von Zeigern

Wir deklarieren einen Zeiger auf einen Variablentyp durch Anhängen des Schlüsselworts "PTR":

DIM AS INTEGER i        'integer-Variable
DIM AS INTEGER PTR pi   'Zeiger auf integer-Variable

TYPE tobject
  DATA(100) AS INTEGER
  NAME AS STRING
  id AS INTEGER
END TYPE

DIM AS tobject o1        'Objekt vom Typ tobject
DIM AS tobject PTR po1   'Zeiger auf ein Objekt vom Typ tobject

Wir sehen gleich, dass es ratsam ist, in der Namensgebung zu kennzeichnen, ob es sich bei der Variable um einen Zeiger handelt oder nicht. Ob Sie das mit dem Voranstellen eines p's im Namen machen wie hier oder auf andere Art, das bleibt Ihnen überlassen. Aber es verhindert Irritationen.

Gut. Jetzt haben wir eine Variable, in der wir eine Adresse speichern können. Aber wie kriege ich nun konkret eine Adresse da rein? Dazu gibt es zwei Möglichkeiten:

  1. Man lässt eine neue Variable generieren. D.h., das Betriebssystem reserviert den entsprechenden Speicherplatz neu und gibt mir die Adresse zurück, die ich dann in meiner Zeigervariablen speichere. In diesem Fall existiert die Variable selbst nicht als Name. Ich kann sie nur über den Zeiger ansprechen.
  2. Man holt die Adresse einer schon deklarierten Variablen und speichert sie.

Die Einsatzzwecke beider Aktionen sind sehr unterschiedlich: Im ersten Fall benutze ich Zeiger, um Variablen zu erzeugen, man sagt dazu auch "alloziieren" oder einfach "reservieren". Der Witz daran ist, dass wir erst zur Laufzeit entscheiden können, wieviel und welche Variablen erzeugt werden sollen - ein grosser Unterschied zu bisher. Mit der Ausnahme der via IMAGECREATE erzeugten Bildspeicher, die wir schon kennengelernt haben: Auch hier konnten wir zur Laufzeit entscheiden, wieviel wir haben wollen und wie gross sie sind. Kein Wunder: Auch hier haben wir das über Hauptspeicherzeiger gemacht. Man nennt das im allgemeinen "dynamische" Reservierung. Gegenüber der "statischen" Reservierung via DIM.

Referenzierung und Dereferenzierung

Die zweite Aktion dient hingegen der oben erwähnten Verweise. Das Objekt, das den Zeiger besitzt, kann auch mit der Variablen arbeiten, auf die der Zeiger zeigt. Der Zeiger ist ein Zugang, ein Tor zu mehr Information für das Objekt.

Wie weisen wir die Adresse einer schon existierenden Variablen zu? Dazu gibt es den @-Operator: pi=@i speichert die Adresse von i in pi. Dazu sagt man: pi "referenziert" i. Nun nehmen wir an, ich habe pi zur Verfügung, aber nicht i. Toll. Nun habe ich eine Adresse. Ich will aber den Wert von i haben, nicht die Adresse. Wie komme ich ran? Dazu gibt es den Dereferenzierungsoperator *. *pi gibt mir den Wert der Variablen zurück, auf die pi zeigt. So, und nun ein Übungsbeispiel:

SUB test()
  
  DIM AS INTEGER i,j,k
  DIM AS INTEGER PTR pi
  
  i=23
  j=16
  
  FOR k=0 TO 9
    IF (k MOD 2 = 0) THEN pi=@i ELSE pi=@j
    ? k,*pi
  NEXT k
  
END SUB

Ausgabe:
0     23
1     16
2     23
3     16
4     23
5     16
...

Der Zeiger pi zeigt abwechselnd auf i und j und der Wert der Variablen, auf den pi gerade zeigt, wird ausgegeben.

Dereferenzierung bei zusammengesetzten Typen

Betrachten wir nun das folgende (inhaltlich sinnlose) Beispiel:

TYPE ta  
  a AS INTEGER
  b AS DOUBLE
END TYPE

TYPE tb
  a(2) AS ta PTR
END TYPE

SUB test5
  
  DIM AS ta a,b
  DIM AS tb x
  DIM AS tb PTR px
  
  a.a=1
  a.b=1.5
  b.a=2
  b.b=2.5
  
  x.a(0)=@a
  x.a(1)=@b
  px=@x
    
END SUB

Hier werden Zeiger und Variablen nur deklariert und initialisiert. So. Und nun haben wir nur noch px. Und wollen von diesem Zeiger aus auf die Elemente von a und b zugreifen. Wie machen wir das?

Versuchen Sie es nicht, es geht (mit den bisherigen Kenntnissen und in den bisherigen FB-Versionen) nicht. Obwohl es theoretisch natürlich möglich sein müsste. Der Reihe nach müsste es so gehen: *px ist x. Dann ist *px.a(0) @a. Und - ja, dann müssen wir *px.a(0) dereferenzieren. Aber wie? **px.a(0) wäre nicht richtig. *px.*a(0)? Aber derefenziert das dann das ganze Array oder nur sein nulltes Element? Fragen über Fragen... Man könnte versuchen, Klammern einzusetzen: *((*px).a(0)) könnte dann z.B. heissen: Dereferenziere px, nimm dann das Ergebnisobjekt, gehe ins Element a und dort zum nullten Eintrag. Dann nimm dies wieder her - äussere Klammer - und dereferenziere es. Übersichtlich ist das auch nicht gerade...

In der Programmiersprache C, die ebenfalls * als Dereferenzierungsoperator hat, waren in den 80er-Jahren solche Dereferenzierungsorgien an der Tagesordnung. Man wurde quasi zum Künstler von solchen *().-Ausdrücken. Die Einarbeitungszeit war gewaltig. Vor allem die Unterscheidung zwischen "Zeiger auf Array" und "Array von Zeigern" machte immer wieder Probleme. Aber auch bei TYPES sind die *-Operatoren völlig ungeeignet, da man bei mehrfachem Abstieg in Elemente rechts die "."-Operatoren setzt, also x.a.b.c usw., aber links die Sterne schreiben muss, die die jeweilige Ebene dereferenzieren: (*(*(*x).a).b).c. Das ist bescheuert.

Freebasic's *-Operator verweigert schlicht die Arbeit, wenn er auf einen TYPE-Zeiger trifft.

SUB test
  
  DIM AS ta a,b
  DIM AS ta PTR px
  
  a.a=1
  a.b=1.5
  
  px=@a  
  ? (*px).a
  
  SLEEP
    
END SUB

ergibt die Fehlermeldung "Invalid data types found: '.a'. Stattdessen benutzt man den Pfeiloperator ->, um Dereferenzierung und Elementabstieg zusammenzufassen:

SUB test
  
  DIM AS ta a,b
  DIM AS ta PTR px
  
  a.a=1
  a.b=1.5
  
  px=@a  
  ? px->a
  
  SLEEP
    
END SUB

Das klappt. Und, wie sieht nun die Lösung des obigen px-Zugriff-Problems aus? Richtg: px->a(0)->a. Das sieht intiuitiv aus.

Anwendung von Zeigern als Verweise

Ein ausführliches Beispiel zu Zeigern stellen die Speicherklassen dar, die wir im nächsten Kapitel besprechen werden. Abgesehen von diesen ist die typische Situation, wo Zeiger als Verweise zum Einsatz kommen, die folgende: Nehmen wir an, ich schreibe gerade eine Funktion twindow_hide(), die ein Fenster einfach vom Bildschirm verschwinden lassen soll. Von der Logik her sollte die Funktion nur ein Argument brauchen: Das Fenster, das verschwinden soll. Ich denke nun weiter drüber nach und stelle fest, dass das Verschwinden am Besten dadurch geschieht, dass der ganze Bildschirm neu aufgebaut wird. Dazu gibt es schon eine Funktion screen_nowindow(). Perfekt. Weniger perfekt ist, dass diese Funktion eine ganze Reihe von Informationen braucht: Eine Liste der aktiven Sprites und der Spriteclocks. Und woher bekomme ich die? Mir bleibt nichts anderes übrig, als zu verlangen, dass beim Aufruf von twindow_hide() diese ganzen Informationen mit übergeben werden: SUB twindow_hide(win AS twindow, spritecl() AS tspriteclock, sprites() AS tsprite, nsprite AS INTEGER). Ih gitt. Was zum Teufel hat twindow_hide mit den Sprites und Spriteclocks zu tun? Für den blossen Nutzer der Funktion schwer einzusehen. Und sehr wahrscheinlich, dass er diese Informationen nicht parat hat.

Der elegante Ausweg sind Verweise in twindow, das die Daten zum Fenster hält. Damit das klappt, müssen wir allerdings die Arrays spritecl() und sprites() jeweils noch in TYPE packen. So in der Form:

TYPE tspritelist
  sprites(10) as tsprite
END TYPE

Dann können wir in twindow einen Verweis auf eine solche tspritelist vorsehen:

TYPE twindow
  spritelist AS tspritelist PTR
  spriteclocklist AS tspriteclocklist PTR
  (...) Rest des twindow-Inhalts
END TYPE

Für spriteclocklist wurde dasselbe vorgesehen. Und schon können wir überall da, wo wir ein twindow-Objekt haben, auch auf die Spritelisten und Spriteclocklisten zugreifen, von denen wir Informationen brauchen.

Unverschämte Frage: Warum haben wir die Spritelisten und Spriteclocklisten nicht gleich global definiert. Dann wäre das doch auch möglich? Jaaaaa... Das wäre es schon. Aber nehmen wir an, das Programmprojekt wäre zu einem umfangreichen Spiel gewachsen. Das vorsieht, dass es mehrere Hauptfenster gibt, innerhalb derer sich jeweils Sprites bewegen. Vielleicht sogar soviele, wie der User wünscht? Dann haben wir eine beliebige Anzahl von Spritelisten und Spriteclocklisten. Das wird dann global schon etwas unübersichtlich. Hingegen ist bei der Verweistechnik alles klar: Sind bei der Initialisierung die Verweise richtig gesetzt, dann bin ich mir in jeder Zeile des Programms sicher, jeweils auf die richtige Spriteliste zuzugreifen, wenn ich dem Zeiger in twindow folge. Das ist ein ganz, ganz wesentlicher und machtvoller Aspekt der strukturierten Programmierung: Man kann durch Zeiger die Zugehörigkeiten auch bei sehr komplexen Strukturen richtig festlegen.

Dies sei, weil es wichtig ist, an einem anderen Beispiel verdeutlicht: Wir wollen eine Netzwerksimulation schreiben. Es geht um Akteure, (z.B. politische oder soziale), die jeweils mit Nachbarn kommunizieren und interagieren. Jeder Akteur ist mit bestimmten anderen Akteuren verbunden:

Innerhalb dieser Simulation hat jeder Akteur eine gewisse Intelligenz. Jeder Akteur überlegt nämlich, was sein Nachbar machen wird, wenn er auf eine bestimmte Weise handeln wird. Er spielt also sozusagen das Szenario durch, was beinhaltet, dass der Nachbar wiederum mit seinen Nachbarn interagieren wird. Die informatische Anforderung ist also, mit den Nachbarn von den Nachbarn zu hantieren. So. Und nun machen Sie das mal mit einem globalen Array actor() as tactor! Bei der sie in so einem Fall erstmal rausfieseln müssen, wer der Nachbar ist und wer die Nachbarn vom Nachbarn sind - keine gute Idee! Hier spielen die Zeiger voll ihren Vorteil aus. Jedes tactor hat ein Array mit Zeigern auf die jeweiligen Nachbarn. Fertig ist die Laube. Wenn ich dann auf die Nachbarn der Nachbarn zugreifen will, dann lautet das einfach actor.neighbour(i)->neighbour(j). Wenn am Anfang die Zeiger korrekt gesetzt wurden, dann bin ich mir hunderprozentig sicher, dass ich jetzt die richtigen Daten habe. Und das ist sehr viel wert!

"Zeigerlose Programmierung"

Es wird manchmal betont, dass eine moderne Sprache deshalb so toll sei, weil sie keine Zeiger hätte. Und nun wurde gerade erklärt, dass Zeiger so ne tolle Sache seien. Wie kann man das zusammenbringen? Das ist ganz einfach. Wir haben gerade Zeiger als "Verweise" kennengelernt. Ein anderes Wort dafür sind "Referenzen". In modernen Programmiersprachen wimmelt es nur so von Referenzen. Gerade in den objektorientierten Programmiersprachen wie Java, Smalltalk, Ruby usw. ist fast alles, mit dem man hantiert, eine Referenz. Eine Referenz? Ich dachte, das seien dort Objekte, mit denen man hantiert? Falsch gedacht. Objekte werden dort immer dynamisch reserviert (siehe oben), mit dem Ergebnis, dass das, mit dem hantiert, immer Referenzen sind - Zeiger auf Objekte. Das bemerkt man zunächst bei der OOP-Programmierung nicht, aber spätestens, wenn man ein Objekt, das andere Objekte enthält, kopieren will, merkt man, dass man nur die Referenzen kopiert hat und nicht die Objekte selbst, (was z.B. bei zweidimensionalen Arrays fatal ist).

Der Begriff "zeigerlos" kommt daher, dass in älteren Programmiersprachen die Adressen in den Zeigervariablen direkt manipuliert werden konnten. Das geht übrigens auch bei Freebasic. Sie können den Inhalt von @i auf den Bildschirm bringen und bekommen einen sieben- oder achtstelligen Integer - die Adresse. Und sie können da natürlich irgendwas draufaddieren oder abziehen. Das ist in den meisten Fällen böse Hackerei. Früher nannte man das "Zeigerarithmetik". Vergessen Sie es. Sie brauchen das nur in einem einzigen - für Grafikprogrammierer allerdings sehr wichtigen - Spezialfall. Oder wenn Sie einen Virus schreiben wollen. Und "zeigerlos" heisst lediglich, dass man genau diese Hackerei mit den Referenzzeigern von Java usw. nicht machen kann. Das ist aber dann auch schon alles.

Der Spezialfall: Bitmap-Landkarten

Bei den meisten 2D-Spielen ist es so, dass der Bildschirm lediglich die Funktion eines Fensters hat, mit dem man auf eine Karte schauen kann. Die Ausmasse der Karte übersteigen die des Bildschirms oder Fensters bei weitem. Es gibt dabei zwei Techniken, wie man solche Karten (oder Maps) realisieren kann. Die erste und sehr viel einfachere besteht darin, dass der Inhalt der Karte nicht als Bitmap gespeichert ist, sondern inhaltlich, z.B. über die Angabe der Koordinaten einer überschaubaren Anzahl von Objekten. Dies ist meist bei alten Jump-and-Run-Spielen der Fall oder auch bei Rennspielen o.ä.

Die zweite, schwierigere Technik besteht darin, die ganze Karte als Bitmap zu realisieren. Die Bitmap zu reservieren, ist in Freebasic mit IMAGECREATE gar kein Problem. Aber wie beschreibt man eine Bitmap, die man gar nicht auf den Screen bringt? Falls die gfx-Lib in ihrer weiteren Entwicklung (ab 2006) nicht hier noch spezielle Routinen hervorzaubert, dann geht es nur dadurch, dass man den jeweils sichtbaren Ausschnitt aus der Karte in den Bildspeicher kopiert - und umgekehrt. Dazu braucht man dreierlei: Einen hinreichend schnellen Kopierbefehl, eine Bildspeicheradresse und eine Adresse mitten im Kartenspeicher. Die Bildspeicheradresse wird sich nie ändern, aber die Adresse in der Karte, die dem gerade angezeigten Ausschnitt entspricht, ändert sich laufend. Und es muss jeweils errechnet werden, wo sich diese Adresse befindet. In diesem Fall müssen Zeiger direkt manipuliert werden. Das ist übrigens auch ein Fall, in dem der Einsatz von Assembler (den Freebasic beherrscht), sehr ratsam ist.

Übung 1:

Ich gehe davon aus, dass Sie die Spriteuhren im letzten Kapitel programmiert haben. Wenn nicht, sollten Sie es jetzt tun. Wenn ja: Verbessern Sie nun das Programm mittels Scopes, Byval-Übergaben und vor allem mittels Zeiger. Halten Sie die Listen mit den Sprites nicht global, sondern gehen Sie davon aus, dass diese Listen irgendwo im Programm generiert wurden. Die Sprite-Routinen dürfen nicht abhängig davon sein, wo das geschah. Gleichzeitig sollten keine Ballast-Argumente in den Routinen auftauchen. Jeder Routine sollte möglichst genau nur die Steuerungswünsche des Nutzers und das Bezugsobjekt übergeben werden. Also: movesprite(sprite as tsprite, newx as integer, newy as integer). Und nicht: movesprite(sprite as tsprite, newx as integer, newy as integer, sprites() as tsprite, otherinformations() as tinformation).

Dynamische Arrays

In Teil 2 war einmal eine Übung, einen Texteditor zu programmieren. Wir nehmen an, wir wollen nun diese Aufgabe im Hauptspeicher erledigen, d.h. der ganze Text soll zunächst im Hauptspeicher niedergelegt werden. Ist ja nun mit Freenbasic kein Problem mehr; wir können lässig auf 100 MB und mehr zugreifen, je nach Ihrer RAM-Ausstattung...

Natürlich benutzen wir als Speichertyp ein String-Array. Ein String ist eine Zeile. Aber wieviel Zeilen wird der Text haben? Wieviel Zeilen müssen reserviert werden? Nach der bisherigen Technik sitzen wir zwischen zwei Stühlen: Wählen wir das Array sehr gross, belegt unser Editor gleich 100 MB Hauptspeicher, obwohl nur ein paar Wörter eingetippt wurden. Wählen wir es kleiner, laufen wir Gefahr, dass das Array zu klein wird, wenn der Schreiber sehr fleissig ist.

Der falsche Weg: $DYNAMIC und REDIM

Für solche Situationen bieten sich dynamische Arrays an, deren Grösse zur Laufzeit festgelegt werden können. Schon QBASIC sah solche dynamischen Arrays vor. Es gab schon bei QBASIC s.g. "Metabefehle" - wir werden sie bei Freebasic auch noch näher kennenlernen. Das sind Anweisungen an den Compiler, auf eine bestimmte Art und Weise zu kompilieren. (Für Interpreter eigentlich nicht notwendig). Sie schreibt man in Freebasic (wie auch in Pascal) in einen Kommentar hinein. Bei Freebasic beginnen die Befehle mit einem Dollarzeichen. Das Ganze sieht in unserem Fall dann so aus: '$DYNAMIC. Wenn wir dieses $DYNAMIC einem DIM voranstellen, dann wird das dort deklarierte Array dynamisch reserviert - man kann die Grösse des Arrays noch zu Laufzeit ändern. Das geschieht mit dem Befehl REDIM(). Wie das Ganze funktionieren könnte, zeigt das folgende Beispiel.

'Sehr einfacher Editor, der NICHT funktioniert.

'$DYNAMIC
DIM buf(1) AS STRING*80
DIM bufsize AS INTEGER
DIM foundend AS INTEGER
DIM inputs AS STRING*80
DIM i AS INTEGER

foundend=0
bufsize=1

WHILE NOT (foundend=1)
  INPUT inputs
  foundend=0
  IF (inputs="quit") THEN
    foundend=1
  ELSE
    buf(bufsize-1)=inputs
    bufsize=bufsize+1
    REDIM buf(bufsize) AS STRING*80
  END IF
WEND

FOR i=0 TO bufsize-1
  ? buf(i)
NEXT i
SLEEP

Das könnte der einfachste Editor der Welt sein. Ohne eigene Speicherplatzbeschränkung. Können Sie die Bibel reintippen. Aber es geht nicht. Warum, werden Sie schnell festgestellt haben: Beim REDIM() wird das ganze Array gelöscht!

REDIM ist also für die Fälle schön, in denen der Speicherplatz von Run zu Run unterschiedlich sein kann, aber zu Beginn des Runs feststeht, z.B. durch eine Konfigurationsdatei. Ändern sich während der Laufzeit die Anforderungen - Pech gehabt. Natürlich kann man den Inhalt von buf() immer erst in einem anderen Array sichern, dann buf() vergrössern und alles wieder zurückspeichern. Ich kann auch mit einer Pinzette ein Haus abreissen. Aber in Freebasic und anderen strukturierten Programmiersprachen gibt's bessere Wege - Dank Zeiger.

Dynamische Allokation - Grundprinzip

Das Prinzip haben wir eigentlich schon kennengelernt, nämlich bei IMAGECREATE und IMAGEDESTROY: "Gib mir bitte x Bytes Speicherplatz und sag mir, wo er ist." So funktioniert das ganz allgemein mit Zeigern. Dafür gibt's den Befehl ALLOCATE: a = ALLOCATE(1000) reserviert z.B. 1000 Bytes und speichert die Adresse in Zeiger a.

DIM AS INTEGER PTR pa

pa=ALLOCATE(1000)
IF (pa=NULL) THEN 
  PRINT "Captain, wir haben ein Problem!"
  END
END IF

PRINT "Wir haben 1000 Bytes!"

DEALLOCATE pa

In der ersten Zeile werden 1000 Bytes reserviert und die Adresse des freien Platzes wird in pa gespeichert. Dann wird getestet, ob ein NULL-Zeiger zurückgegeben wurde. NULL ist eine System-Konstante von Freebasic und enthält einfach die Zahl 0. Man hätte also auch if (pa=0) then... schreiben können. Aber dann sieht man nicht mehr, dass es sich bei pa um einen Zeiger handelt. Deshalb ist es immer schöner, NULL zu verwenden und damit gleichzeitig den Typ anzuzeigen, mit dem man hier hantiert.

Ein NULL-Zeiger wird vom System dann zurückgegeben, wenn der angeforderte Speicher nicht reserviert werden konnte.

Hat die Reservierung geklappt, macht das Programm gar nichts, ausser eine Meldung auszugeben und dann den freigewordenen Speicher wieder ans System zurückzugeben.

So einfach diese Zeilen sein mögen, sie tragen Millionen von Programmiererstunden in sich, die schon an ihren beiden Problemen verloren gegangen sind.

Oh jeh. Das macht nicht gerade Appetit auf dynamische Allolation! Nun, es gibt im Rahmen von Programmiersprachen ohne Memory Management - und dazu zählen sowohl C/C++ wie Delphi wie Freebasic - durchaus einige Grundregeln, deren Beachtung diese Probleme stark eindämmen:

Die Grösse des benötigten Speicherplatzes bestimmen

Die Allokation oben beinhaltet noch eine komische Sache: Die Anzahl Bytes. Warum 1000? Was machen wir eigentlich mit dem Speicherplatz, wenn wir ihn haben? Wie sprechen wir ihn an?

Das Grundprinzip ist, den Speicher für ein Array zu benutzen. Daher wollen wir eine Anzahl Variablen des Typs speichern, dem der Zeiger entspricht. Wir haben einen integer ptr - also wollen wir eine Anzahl integer speichern. Doch wieviel Speicherplatz braucht ein integer? Und wieviel brauchen 1000 Integer? Jedenfalls mehr als 1000 Bytes. Wie rechnen wir das um?

Dazu können wir die Funktion LEN() benutzen, die bisher nur für Strings da war. Sie akzeptiert nun auch TYPEN als Argument. Z.B. LEN(INTEGER). Probieren Sie das mal aus:

PRINT LEN(INTEGER)

Ergebnis: 4 Bytes. Auf diese Weise können wir nun ganz einfach die Anzahl der benötigten Elemente auf den benötigten Speicherplatz umrechnen, z.B.

TYPE tb
  a AS STRING*20
  b AS INTEGER
  c(10) AS DOUBLE
END TYPE

TYPE ta
   a(3) AS tb
   b(10) AS INTEGER
END TYPE

DIM AS ta PTR px

px = ALLOCATE(LEN(ta)*1000)
IF (px=NULL) THEN 
  PRINT "Problem"
  END
END IF

PRINT "1000 Elemente vom Typ ta alloziiert."

DEALLOCATE px

Zugriff auf dynamische Arrays