Einleitung:
|
Die bisher beschriebenen Arten der Pointerhandhabung sind sozusagen alles "nur lesen" Operationen gewesen. Jetzt kommen wir zu der Art von Pointerhandhabung, bei der man den Pointer selber manipulieren kann. Dies ist eigentlich nicht schwerer, als die in Kapitel 2 beschriebenen Methoden, es gilt nur auch hier, dass man das Prinzip verstanden haben muss, um unnötige Fehler und Programmabstürze zu vermeiden.
|
Grundprinzip:
|
Den Gedanken dahinter möchte ich einmal Anhand von Strukturen und Pointern erklären.
Man stellt sich vor, man hat eine Struktur, die sich irgendwo im Speicher befindet. Man kennt auch die Adresse, an der diese Struktur sich befindet. (So läuft das zum Beispiel, wenn WinAPI eine Struktur als Ergebnis zurückgibt.)
Wie würde man nun die Daten in dieser Struktur lesen?
Man kann ja dies ja nicht mit einer normalen Variable, die eine Struktur enthält lösen, denn diese ist ja dann auf ihren eigenen Speicherbereich begrenzt.
Hier kommen Pointer ins Spiel. Man kann eine sozusagen "virtuelle" Strukturierte Variable erstellen, bei der man dann festlegen kann, auf welchen Bereich des Arbeitsspeichers sie verweist. Also ein selbst definierter Pointer. Man kann dann mit Hilfe dieses Pointers den Inhalt der Struktur im Speicher lesen.
Dies ist zum Beispiel auch nützlich, wenn man einen Speicherbereich mit mehreren hintereinanderliegenden gleichen Strukturen hat. Man kann den Pointer dann sozusagen durch den gesamten Speicherbereich verschieben, und all diese Strukturen nacheinander auslesen, bzw. modifizieren.
|
Notation:
|
Notiert man einen Stern (*) vor einer Variable, oder einer Variable mit einer Struktur, so erstellt man damit anstatt einer Variable einen Pointer.
Wichtig: Man erstellt damit nur "virtuell" etwas. Man kann also in dem Pointer selbst nicht direkt etwas speichern, sondern man greift immer auf den Speicherbereich zu, auf den der Pointer gerade zeigt.
Folgendes Beispiel soll das Problem verdeutlichen:
Structure TestStruktur
Wert1.l
Wert2.l
EndStructure
Variable.TestStruktur
Variable\Wert1 = 5
*Pointer.TestStruktur
*Pointer\Wert1 = 5
Führt man diesen Code mit dem Debugger aus erhält man die Meldung: "Pointer is Null".
Zuerst wird eine Variable definiert. Es ist klar, dass man auf diese auch gleich zugreifen kann.
Der erstellte Pointer reagiert aber nicht, wie eine Variable! Er wurde zwar erstellt, zeigt aber noch auf keinen Bereich im Speicher, somit darf man da auch noch keine Werte hinschreiben. Man muss eben erst definieren, wohin der Pointer zeigen soll.
Um dies zu definieren macht man folgendes:
Wenn man den Pointer erstellt hat, notiert man den Namen des Pointers mit einem Stern davor, und weist ihm mit einem Gleichheitszeichen die Speicheradresse zu, auf die er verweisen soll.
Notiert man noch den Namen eines Elementes dahinter (mit einem Backslash), so greift man damit auf den Wert, der an dieser Stelle im Speicher gespeichert ist zu.
Man kann die Adresse, auf die der Pointer zeigt jederzeit ändern. Alle nachfolgenden Zugriffe auf die Elemente des Pointers beziehen sich dann auf die aktuell gültige Adresse.
Folgendes Beispiel zeigt, wie dies in der Praxis funktioniert:
Structure TestStruktur
Wert1.l
Wert2.l
EndStructure
Variable.TestStruktur
Variable\Wert1 = 5
*Pointer.TestStruktur
*Pointer = @Variable
Debug *Pointer\Wert1
PointerWert.l = *Pointer
Debug PointerWert
Debug @Variable
Ich setze hier den Pointer auf die Adresse der Variable. Somit kann ich über den Pointer auf die Elemente von Variable zugreifen. Was ich mit der Debug-Anweisung auch tue.
Wie am Ende dieses Beispieles gezeigt, kann man die Adresse, auf die ein Pointer verweist auch wieder auslesen. Dazu notiert man einfach wieder nur den Namen des Pointers mit einem Stern davor (keine Elemente). Wie man hier sieht ist die Adresse von Pointer und der Variable gleich. (Das war ja auch beabsichtigt.)
Man kann Pointer wie Variablen auch als Global, Shared oder Protected definieren. Dies hat wie bei Variablen auch die Auswirkung, dass sie nur für eine bestimmte, oder eben für alle Prozeduren gelten. Die Definition funktioniert wie bei Variablen auch:
Global *Pointer.Struktur
Shared *Pointer.Struktur
Protected *Pointer.Struktur
Es gilt bei der Arbeit mit diesen Pointern zu beachten, dass man sie nur auf Adressen setzen darf, für die das Programm auch Lese- oder Schreibzugriff hat. Ansonsten wird das Programm mit dem Hinweis auf eine Zugriffsverletzung abstürzen.
|
Pointer zu Strukturen:
|
Eigentlich bin ich beim Punkt "Notation" schon praktisch auf alles eingegangen, was bei Pointern zu Strukturen wichtig ist:
- Notiert man nur den Namen des Pointers mit einem Stern (ohne Elemente), so erhält/setzt man die Adresse, auf die der Pointer zeigt.
- Notiert man das ganze mit einem Element dahinter, so kann man dieses manipulieren/auslesen.
Ein Beispiel zur Verwendung befindet sich beim Punkt "Strukturen als Parameter bei Prozeduren"
|
Pointer zu einzelnen Variablen:
|
Hier ergibt sich ein Problem (wobei die Frage ist, ob das als Problem anzusehen ist).
Wie bereits beschrieben erhält man bei einer Struktur den Zugriff auf den Speicherbereich, auf den der Pointer zeigt indem man das gewünschte Element dazu notiert. Wie aber erhält man den Inhalt des Speichers eines Pointers, der auf eine einzelne Variable zeigt?
Dazu gibt es in PureBasic keine direkte Möglichkeit.
Es stellt sich auch die Frage, ob man dies überhaupt braucht. Schließlich kann man die Adresse ja in jeder LONG Variable speichern, und wenn man Zugriff auf den Wert im Speicher will, kann man PeekL()/PokeL(), PeekW()/PokeW(), PeekB()/PokeB, PeekF()/PokeF() oder PeekS()/PokeS() verwenden.
Will man es aber gerne mit Pointern direkt lösen empfehle ich folgende Lösung:
Structure Byte
Wert.b
EndStructure
*Pointer.Byte
Variable.b = 5
*Pointer = @Variable
Debug *Pointer\Wert
Man definiert praktisch eine Struktur, die nur 1 Byte groß ist. Man kann dann wie bei den Strukturen die Adresse manipulieren, und über "*Pointer\Wert" den Wert im Speicher lesen.
Dies funktioniert natürlich auch mit WORD und LONG.
Strings sind hier ein Sonderfall, auf den ich noch kurz eingehen möchte:
Speichert man einen String in einer Struktur, so speichert man immer nur den Pointer zum wirklichen String in der Struktur. Das kommt daher, dass Strings ja eine variable Länge haben, eine Struktur muss aber eine vordefinierte, feste Größe haben. Schreibt man also ein einer Struktur zum Beispiel "String.s", so wird dies intern durch einen 4Byte Pointer zum String ersetzt. (Normale String-Variablen sind intern übrigens auch Pointer.) Aus diesem Grund kann man mit dieser Struktur-Technik Strings nicht einfach einlesen. Hier würde ich zu PeekS() und PokeS() greifen.
Ihr werdet bestimmt schon ein mal in irgendeinem Codebeispiel, oder in der PB Hilfe die Verwendung solcher Pointer ohne eine Struktur gesehen haben:
*Fenster = OpenWindow(0, 100, 100, 200, 200, #PB_Window_SystemMenu, "Fenster")
CreateGadgetList(*Fenster)
Dies ist ohne Weiteres möglich. Allerdings manipuliert man hier immer nur eine Adresse, kann aber nie Lesen, was dort wirklich gespeichert ist. Da ein Pointer nichts anderes ist, als eine 4Byte Adresse, hat man hier also praktisch eine LONG Variable mit der Erinnerung, dass es sich hierbei um eine gespeicherte Adresse handelt, und mehr nicht.
Ich habe mir deshalb angewöhnt, den Stern für Pointer nur da zu verwenden, wo er auch Sinn macht, also eigentlich nur im Zusammenhang mit Strukturen. In Fällen wie diesen kann man nämlich genauso gut eine LONG Variable verwenden.
Aus den oben genannten Gründen funktioniert etwas wie *Pointer.s = "Hallo" überhaupt nicht. Man kann auf diese Weise nicht auf Strings im Speicher zugreifen.
|
Pointer zu Speicherbereichen:
|
In vielen Beispielen sieht man, dass Leute, wenn sie mit Speicherbereichen arbeiten, Pointer verwenden.
Beispiel:
*Speicher = AllocateMemory(0, 1024)
Hierzu möchte ich sagen, dass ich das nicht für besonders hilfreich halte. Hier ist nirgendwo eine Struktur im Spiel, und wie ich bereits gesagt habe, kann *Speicher also nichts anders, als die Adresse speichern.
Für solche Dinge empfehle ich, normale LONG Variablen zu verwenden. Ich würde Pointer wirklich nur da verwenden, wo es notwendig und Sinnvoll ist, verwendet man sie zu oft, hat man es um so schwerer Fehler zu finden.
Da kann natürlich jeder anderer Meinung sein, als ich. Ich habe nur schon öfters gesehen, das diese Art der Programmierung bei den Leuten zu einem Durcheinander, und damit zu Fehlern geführt hat.
|
Strukturen als Parameter bei Prozeduren:
|
Die folgenden zwei Punkte sollen ein praktischen Anwendungsbeispiel für die Verwendung von Pointern sein, dass zusätzlich auch ein anderes Problem der Pb Programmierung beschreibt und löst, nämlich die Übergabe von Strukturen an Prozeduren. Dieses Beispiel befindet sich auch in der Hilfe von PB, allerdings ist es dort so knapp beschrieben, dass man es (ohne Vorkenntnisse) kaum versteht.
Problem: Man kann keine Strukturen als Parameter an eine Prozedur übergeben. Nur die Standart Typen von PureBasic.
Lösung: Ein Pointer ist ja auch praktisch ein Standart Typ, also kann man den Pointer zu einer Struktur an eine Prozedur übergeben.
Hierzu ermittelt man beim Aufrufen der Prozedur die Adresse mit Hilfe des in Kapitel 2 beschriebenen @ Symbols, und nimmt in der Prozedur diese Adresse über einen mit einem Stern definierten Pointer entgegen.
Beispiel:
Structure TestStruktur
Wert1.l
Wert2.w
Wert3.b
EndStructure
Procedure TestProzedur(*Pointer.TestStruktur)
Debug *Pointer\Wert2
EndProcedure
Variable.TestStruktur
Variable\Wert2 = 521
TestProzedur(@Variable)
Der Pointer in der Prozedur bekommt beim Aufruf die Adresse der übergebenen Struktur zugewiesen. Man kann nun also über den Pointer auf die Struktur zugreifen.
Wichtig: Modifiziert man Werte der Struktur mit dem Pointer, so wird die Ursprungsstruktur des Hauptprogramms verändert, da der Pointer ja auf deren Speicherbereich verweist. Dies ist beim übergeben der Standart Typen ja anders. Die als Parameter erhaltenen Variablen bekommen dort einen anderen Speicherbereich zugewiesen als die, die als Parameter im Hauptprogramm übergeben wurden.
Man kann dich diesen Effekt auch zu nutze machen, um somit einfach mit Hilfe einer Prozedur den Inhalt einer Struktur zu verändern. Dies ist auch ein guter Weg, um die Rückgabe von Strukturen bei zu realisieren.
Beispiel:
Structure TestStruktur
Wert1.l
Wert2.l
Wert3.l
EndStructure
Procedure TestProzedur(*Pointer.TestStruktur)
*Pointer\Wert3 = *Pointer\Wert1 + *Pointer\Wert2
EndProcedure
Variable.TestStruktur
Variable\Wert1 = 5
Variable\Wert2 = 10
TestProzedur(@Variable)
Debug Variable\Wert3
|
Strukturen als Rückgabewerte von Prozeduren:
|
Man kann das nach genau dem selben Prinzip machen, wie im vorigen Abschnitt.
Man erstellt in der Prozedur eine Variable mit einer Struktur, und übergibt dann den Pointer (mit @) als Rückgabewert. Im Hauptprogramm erstellt man einen Pointer (mit *), der dann auf die Adresse des Rückgabewertes zeigt.
Beispiel:
Structure TestStruktur
Wert1.l
Wert2.l
Wert3.l
EndStructure
Procedure TestProzedur()
Variable.TestStruktur
Variable\Wert2 = 521
ProcedureReturn @Variable
EndProcedure
*Pointer.TestStruktur
*Pointer = TestProzedur()
;Delay(10)
Debug *Pointer\Wert2
Auch falls das Beispiel so bei euch funktioniert, spätestens, wenn man den Kommentar vor dem Delay() wegmacht, kommt es nicht mehr zum gewünschten Ergebnis. Warum?
Hierzu muss man wissen, dass am Ende einer Prozedur alle Variablen freigegeben werden. Das heißt, nachdem man mit ProcedureReturn die Prozedur verlassen hat, hat man nur noch den Pointer zu einer nicht mehr existierenden Variable. Der Wert, den man dann lesen kann ist also nutzlos.
Um dies zu verhindern, muss man dafür sorgen, dass die Variable beim Verlassen der Prozedur nicht zerstört wird. Dies kann man erreichen, in dem man sie als Shared oder Global definiert.
Beispiel:
Structure TestStruktur
Wert1.l
Wert2.l
Wert3.l
EndStructure
Procedure TestProzedur()
Shared Variable.TestStruktur
Variable\Wert2 = 521
ProcedureReturn @Variable
EndProcedure
*Pointer.TestStruktur
*Pointer = TestProzedur()
Delay(10)
Debug *Pointer\Wert2
Diese Methode hat einige entscheidende Nachteile:
- Man kann den Namen der Variable nicht mehr im Hauptprogramm verwenden, wie diese ja Global ist.
- Ruft man mehrmals die Prozedur auf, so wird immer die gleiche Variable mit ihrer Struktur manipuliert. Das heißt, die Werte, die vorher darin gespeichert waren, sind dann verloren, bzw. werden verändert.
Die Bessere Methode ist hier in meinen Augen die, dass das Hauptprogramm die Variable erstellt, sie als Parameter an die Funktion übergeben wird, und die Funktion schreibt dann die passenden Werte hinein. Man macht sich damit den oben beschriebenen Effekt zu nutze.
Dies hat klare Vorteile gegenüber der anderen Möglichkeit:
- Es werden keine Globale Variablen gebraucht.
- Man kann die Funktion aufrufen sooft man will, weil man ja immer eine andere Variable übergeben kann.
|
Zusammenfassung:
|
- Mit einem Stern vor der Variable definiert man einen veränderbaren Pointer.
- Diese sind nur "virtuell", man muss ihnen zuerst eine Adresse zuweisen.
- Man kann sie als Global, Shared oder Protected definieren.
- Sie sind eigentlich nur im Zusammenhang mit Strukturen wirklich sinnvoll.
|
|