Hallo Leute,
in diesem Thread möchte ich euch die Programmiersprache FreePascal näher bringen, von Anfang an. Dieses Tutorial richtet sich vor allem an Einsteiger in die Programmierung, eignet sich allerdings auch für Umsteiger die sich FreePascal aneignen wollen.
Im verlauf der nächsten Wochen/Monate werde ich den Thread mit neuen Teilen immer aktualisieren.
Aufgrund des Epvp Post Größen Limits werde ich die Einzelnen Kapitel auf meine Website auslagern. Neue Kapitel poste ich dennoch zu erst hier
in diesem Thread möchte ich euch die Programmiersprache FreePascal näher bringen, von Anfang an. Dieses Tutorial richtet sich vor allem an Einsteiger in die Programmierung, eignet sich allerdings auch für Umsteiger die sich FreePascal aneignen wollen.
Im verlauf der nächsten Wochen/Monate werde ich den Thread mit neuen Teilen immer aktualisieren.
Aufgrund des Epvp Post Größen Limits werde ich die Einzelnen Kapitel auf meine Website auslagern. Neue Kapitel poste ich dennoch zu erst hier
Inhalt
0. Voraussetzungen
1. FreePascal
2. Erste Schritte & Variablen
3. Kontrollstrukturen
4. Arrays
5. Zeiger
6 Typen
7. Funktionen
8. Rekursive Funktionen und Datentypen
9. Bibliotheken
10. Dateien Lesen&Schreiben
11. Einführung in die OOP
12. Lazarus & LCL
13. Klassen und Instanzen
...
Work in Progress Schon gemacht
0. Voraussetzungen
1. FreePascal
2. Erste Schritte & Variablen
3. Kontrollstrukturen
4. Arrays
5. Zeiger
6 Typen
7. Funktionen
8. Rekursive Funktionen und Datentypen
9. Bibliotheken
10. Dateien Lesen&Schreiben
11. Einführung in die OOP
12. Lazarus & LCL
13. Klassen und Instanzen
...
Work in Progress Schon gemacht
Rekursive Funktionen und Typen
Bisher haben wir nur das Iterative Programmieren kennen gelernt, wobei der Code einfach linear Ausgeführt wird und durch Kontrollstrukturen und Schleifen beinflusst wird. Nun möchte ich auf eine andere Technik eingehen, das rekursive Programmieren.
Bei rekursiven Funktionen definieren wir die Funktion über sich selbst. Das klingt jetzt vielleicht ein wenig Abenteurlich, aber ich erkläre das mal an dem wohl bekanntesten Beispiel.
Fibonacci Zahlen
Die Fibonacci Zahlen ist eine Folge aus der Mathematik. Definiert ist das Element n der Folge als die Summe der Fibonacci Zahlen von n-1 und n-2. Für 0 ist die Fibonacci Folge als 0 definiert und für 1 als 1.
Damit wäre die Fibonacci Zahl von 2: 0 + 1 = 1, die von 3: 1 + 1 = 2, 4: 2 + 1 = 3 und so weiter.
Die formale Schreibweise wäre:
Ein Paradebeispiel für eine Rekursive Funktion.
Wenn wir so etwas Programmieren wollen können wir direkt auf die Rekursive Definition zurückgreifen:
Diese Funktion ruft zur Lösung sich selbst auf, so lange bis eine Abbruchbedingung erfüllt ist. Im Beispiel Fibonacci wären die Abbruchbedingungen n = 0 und n = 1.
Jedes Problem welches sich Rekursiv lösen lässt lässt sich auch Iterativ lösen und vice versa. Manchmal sind Rekursive funtkionen einfacher zu lesen, kürzer und intuitiver. In einigen Fällen ist die Intuitive Kurze Lösung allerdings auch nicht unbedingt die Beste. Das Beispiel Fibonacci ist in dieser Form extrem ineffizient, da so gut wie alle Zahlen mehrfach ausgerechnet werden, was eine Kaskade an Berechnungen zur folge hat. Probiert es gerne mal mit großen Zahlen aus, ihr werdet schnell merken das die Berechnungen sehr schnell sehr lange dauern.
Eine spontane iterative Lösung wäre:
Dieser Ansatz ist schon deutlich besser als der vorherige Rekursive, aber definitiv nicht so schön zu lesen.
Auch dieser Lösungsweg lässt sich Rekursiv umsetzen, das wäre aber ein deutlich größerer Aufwand und lohnen tut es sich in diesem Fall nicht.
Allerdings kann es auch Anders rum sein, das der spontane iterative Lösungsweg nicht so effizient ist wie der spontane Rekursive.
Bemerkung: Ich schreibe bewusst spontan, da man mit etwas Nachdenken auch auf effiziente Rekursive oder effiziente Iterative wege kommen kann, diese sind allerdings nicht direkt erkennbar.
Wenn man viel Programmiert entwickelt man ein Gefühl dafür wann eine rekursive Lösung und wann eine Iterative besser ist.
Allgemeine Rekursive Funktionen
An dem obigen Beispiel habe ich eine typische rekursive Fuktion gezeigt. Nun möchte ich allgemein auf rekursive Funktionen und ihre Eigenschaften eingehen.
Mit rekursiven Funktionen reduzieren wir Probleme durch erneutes Aufrufen mit veränderten Datensatz auf Basisfälle, die Abbruchbedingungen.
Die Anzahl der Aufrufe nennt man die Rekursionstiefe. Die Rekursionstiefe ist allerdings beschränkt. Da jeder Funktionsaufruf seinen eigenen Stack hat auf den mindestens die Rücksprungaddresse geschrieben wird, wird mit jedem Rekursionsaufruf Speicher belegt. Somit ist unsere Rekursionstiefe durch den Speicher begrenzt. Endlosschleifen mit Rekursion sind daher nicht möglich.
Das bedeutet natürlich auch das wir immer eine Abbruchbedingung benötigen. Diese kann wie in dem Beispiel Fibonacci explizit angegeben werden, oder auch Implizit erreicht werden, zum Beispiel bis wir keine Daten zum verarbeiten mehr haben. Ein Beispiel dazu folgt etwas später.
Oben habe ich geschrieben, jedes Problem welches sich iterativ lösen lässt lässt sich auch rekursiv lösen. Das stimmt zwar allerdings lässt sich nicht zu jedem rekursiven Algorithmus auch ein Äquivalenter iterativer Algorithmus finden. Oben der Iterative Fibonacci Algorithmus mag zwar die Fibonacci Zahlen ausrechnen, aber auf eine Komplett andere Art und Weise. Die Rekursiven Funktionen für die es eine Äquivalente iterative Lösung gibt nennt man Primitive Rekursionen.
Ein Beispiel für eine Solche wäre:
Bei diesen Rekursionen kann man einfach einen Aktuellen wert Zwischenspeichern und durch Schleifen die Rekursion simmulieren.
Da man nur Primitve Rekursionen einfach zu Iterativen Funktionen umformen kann gibt es auch keine allgemeingültige Methode um Rekursiv zu Iterativ und vice versa umzuformen.
Nun aber wieder zu etwas praktischeren Beispielen.
Dateien Auflisten
Nun möchte ich mein Lieblingsbeispiel für rekursive Funktionen anführen, das Suchen von Dateien in Unterordnern. Dies ist ein rekursiver Algorithmus auf den ich immer wieder zurückgreife.
Die Idee dabei ist Rekursiv so Vorzugehen:
Die Abbruchbedingung ist hier Impliziet das es irgendwann keine Unterordner mehr gibt, da ein Dateisystem nie Unendlich ist.
Um das nun umzusetzen muss ich zunächst einmal die Funktion FindFirst, FindNext und FindClose.
Mit diesen Funktionen können wir Dateien und Ordner in einem Verzeichnis suchen.
Mit FindFirst beginnen wir die Suche, übergeben das so genannte Searchpattern, die Art der Dateien die wir suchen (z.B. Ordner, versteckte Dateien, etc) und ein SearchRecord, eine Variable in die die alle nötigen Informationen für die Suche und gefundenen Dateien geschrieben wird. Die Rückgabe von FindFirst ist ein Integer. Wurden Dateien gefunden gibt die Funktion 0 zurück, sonst eine negative Zahl.
FindNext bekommt den SearchRecord übergeben und Sucht nach der nächsten Datei. FindNext gibt 0 zurück falls weitere Datei gefunden wurde, also die Suche noch nicht beendet ist.
FindClose gibt die Resourcen wieder frei. Das Dateisystem wird vom Betriebsystem gehandhabt, und ein Aufruf von FindFirst sagt dem Betriebsystem es soll die Dateisuche beginnen. Mit FindClose sagen wir dem Betriebsystem das die Suche beendet ist, und es den Suchprozess aus dem Speicher werfen kann. Das lässt sich mit dem Dispose bei Zeigern vergleichen. Übergeben wird wieder das SearchRecord. Gibt FindFirst nicht 0 zurück so ist der Aufruf von FindClose nicht nötig.
Also beginnen wir:
Das ist schon mal der Anfang. Directory+'*' als SearchPattern bedeutet jede Datei im Verzeichnis, ohne Einschränkungen. faAnyFile ist die Suche nach allen Dateien.
IncludeTrailingPathDelimiter ist eine Funktion die einen Pfad annimmt und diesen zurückgibt mit einem / bzw \ (Windows) am Ende falls dieses nicht vorhanden sein sollte. So wird aus /Pfad dann /Pfad/.
In der Repeat-Until Schleife iterieren wir nun durch alle Dateien die gefunden werden. Sr enthält bei jedem Durchlauf der Schleife die Informationen über die gefundene Datei, das ist zum einen der Name oder auch um was für eine Datei es sich handelt. Die Abfrage ((sr.attr and faDirectory) <> faDirectory) And (ExtractFileExt(sr.Name) = ext) ist die Überprüfung ob es sich um ein Verzeichnis handelt, welche wir später behandeln wollen, und ob die Datei die gewünschte Dateiendung hat. sr.attr enthält die Dateiattribute als Flags (siehe Einschub: Variante Records, With, Flags).
Ist die Bedingung erfüllt geben wir den Dateinamen aus. Wir könnten ihn auch in einem Array Speichern wenn wir die Dateien zwischenspeichern wollen.
Wichtig ist das Try-Finnaly, damit auf jeden Fall das FindClose aufgerufen wird
Nun kommen wir aber zu der Rekursion:
Bei der zweiten Datei Suche wird nur nach Verzeichnissen gesucht und für diese dann eine separate Suche ausgeführt. Die Abfrage (sr.Name <> '..') And (sr.Name <> '.') ist notwendig, da jedes Verzeichnis eine Referenz auf das vorhergehende Verzeichnis ('..') und auf das eigene Verzeichnis ('.') hat, was zu einer Endlosschleife führen würde.
Damit wäre das Programm komplett:
Das würde alle Dateien in allen Unterordnern mit der Dateiendung .pas in Pfad auflisten.
Wöllte man nun die Dateien in einen Array speichern würde die Anpassung recht simpel gehen:
Bemerkung: Diese Möglichkeit mit dem String Array ist nicht sehr gut, der häufige Aufruf von SetLength ist sehr langsam. Später nachdem wir Klassen kennengelernt haben lernen wir eine Klasse, TStringList kennen, welche das besser Implementiert.
Rekursive Datentypen
Wir hatten im 6. Kapitel gelernt wie wir eigene Datentypen schreiben. Wir können Datentypen Rekursiv verwenden. Das bedeutet im Sinne von Datentypen, dass es sich dabei um Zusammengesetzte Datentypen (Records oder Arrays) bei denen 1 oder mehr Elemente ein Zeiger auf den selben Datentyp ist.
Auf diesen Datentypen empfiehlt sich die Nutzung von Rekursiven Funktionen.
Diese Typen verwendet man für so genannte Verkettungen, wobei eine Variable auf andere Variablen des selben Typs verweisen.
Für die Definition solcher Typen gibt es in Pascal eine Syntaktische besonderheit. Zeiger auf einen Typen lassen sich über dem Typen selbst im Quellcode Definieren:
Da Pascal von oben nach Unten den Code durchgeht können wir für Variablen oder Elemente nur Typen verwenden die im Source über unserer Deklaration stehen, außer bei Zeigern. Das erlaubt uns nun eine solche Definition
Somit könne wir einfach einem Record einen Verweis auf einen anderen Record vom selben Typ hinzufügen.
Eine Einsatzmöglichkeit solcher Typen sind z.B. Verkettete Listen (engl. linked list). Diese geben uns die Möglichkeit, ähnlich zu dynamischen Arrays beliebig viele Elemente zu Speichern. Das Prinzip ist recht simpel, jedes element enthält einen Zeiger auf das Nächste. Das letzte Element hat einen Zeiger auf nil.
Um eine verkettete Integer Liste zu Implementieren wäre der Rekursive Typ:
Jetzt können wir über new und dispose immer neue Elemente erstellen oder Löschen und diese dann an ein vorhandenes Element anhängen.
Schreiben wir uns dafür mal ein paar Funktionen die das Realisieren.
Fangen wir an mit einer Funktion zur Berechnung der Anzahl der Elemente:
Diese geht Rekursiv durch die Liste und Addiert für jedes Item 1 hinzu.
Als nächstes eine Funktion zum Hinzufügen am Anfang mit dem wert x
Hier erstelle ich zunächst ein neues Element mit dem Wert x, dann gebe ich List, welches das erste Element unserer verketteten Liste ist, als NextItem, und überschreibe List mit dem neuen ersten Item. Die überschreibung ist möglich dank der Parameterübergabe als Referenz.
Jetzt können wir noch eine Funktion schreiben zum Hinzufügen eines Elements am Ende
Diese Funktion geht Rekursiv durch alle Listenelemente bis wir beim Ende Angekommen sind, und erstellt dort das neue Element.
Nun kommen wir zu der nächsten Funktion, zum löschen eines Elements gegeben mit dem Index i:
Das ist jetzt schon etwas mehr.
Wir gehen Rekursiv durch die gesammte Liste bis wir entweder nil bekommen (nach dem letzten Item) oder wir 1 Element vor dem zu löschenden Element sind.
Dann überprüfen wir ob das zu löschende Element überhaupt exsistiert, wenn es exsistiert speichern wir dessen Nachfolger, löschen das Element und setzen den gespeicherten Nachfolger als das nachvolge Element des Elements vor dem gelöschten.
Natürlich was auch noch Fehlt ist das Auslesen des Wertes eines bestimmten Elements mit Index i
Diese Funktion geht Rekursiv durch die Liste reduziert bei jedem Aufruf i um 1. Wenn i = 0 erreicht, und die Liste noch nicht am Ende ist, so ist das Aktuell mit List übergebene Element unser Element und wir geben den Wert zurück. Falls das Element nicht exsistiert wird Standardmäßig 0 zurückgegeben.
Natürlich benötigen wir auch noch eine Funktion um den Wert eines Elements mit Index i zu setzen
Diese ist relativ ähnlich zum Auslesen, ich denke hier muss ich nicht weiter erklären.
Als letztes fehlt uns nur noch eine Funktion um die gesammte Liste zu löschen und den Speicher freizugeben
Hier wird zunächst Rekursiv das nächste Element gelöscht, danach wird das Aktuelle gelöscht. Damit wird die Liste von Hinten nach Vorne gelöscht und auf nil gesetzt.
Natürlich könnte man noch einen Haufen weiterer Funktionen definieren wie FindElementWithValue um ein bestimmtes Element zu finden oder InsertAt um ein Element an einer bestimmten Position einzufügen, aber ich begnüge mich mit diesem Funktionsumfang.
Also bauen wir jetzt mal ein Projekt was diese Liste verwendet:
Dieses Programm füllt zu erst die Liste mit 8, 3, 10, 5, 15. Erhöt danach jedes Element um 2 und gibt diese dann in umgekehrter Reihenfolge wieder aus. Am ende wird durch ClearList noch der Speicher bereinigt.
Bemerkung: Es gibt in der RTL vernünftige Implementierungen solcher Datenstrukturen wie verkettete Listen, welche wir auch noch kennen lernen werden. Ich führe das hier nur als Beispiel zum lernen an, bei der Anwendung sollte man sich die Arbeit sparen und auf diese Implementierungen zurückgreifen.
Weitere typische Datenstrukturen für die rekursive Typen verwendet werden sind:
Bäume, z.B. ein Binärbaum, der aus verschiedenen Elementen besteht die jeweils genau 2 Nachfolger haben.
Doppelt verkettete Listen, Listen bei denen jedes Element einen Zeiger auf das nächste und das vorhergehende Element haben.
Ringlisten, bei denen das letzte Element wieder auf das erste verweist, etc.
Solche verketteten Listen kann man statt dynamischer Arrays (so genannte Array Listen) benutzen, Arrays bieten einen deutlich schnelleren Zugriff auf einzelne Elemente, und brauchen auch weniger Speicher, allerdings ist das Hinzufügen oder Einfügen von Elementen deutlich langsamer und aufwändiger.
Bisher haben wir nur das Iterative Programmieren kennen gelernt, wobei der Code einfach linear Ausgeführt wird und durch Kontrollstrukturen und Schleifen beinflusst wird. Nun möchte ich auf eine andere Technik eingehen, das rekursive Programmieren.
Bei rekursiven Funktionen definieren wir die Funktion über sich selbst. Das klingt jetzt vielleicht ein wenig Abenteurlich, aber ich erkläre das mal an dem wohl bekanntesten Beispiel.
Fibonacci Zahlen
Die Fibonacci Zahlen ist eine Folge aus der Mathematik. Definiert ist das Element n der Folge als die Summe der Fibonacci Zahlen von n-1 und n-2. Für 0 ist die Fibonacci Folge als 0 definiert und für 1 als 1.
Damit wäre die Fibonacci Zahl von 2: 0 + 1 = 1, die von 3: 1 + 1 = 2, 4: 2 + 1 = 3 und so weiter.
Die formale Schreibweise wäre:
Code:
Fib(n) = 0 | Für n=0 = 1 | Für n=1 = Fib(n-1) + Fib(n-2) | Sonst
Wenn wir so etwas Programmieren wollen können wir direkt auf die Rekursive Definition zurückgreifen:
Code:
function FibRec(n: Integer): Integer; begin if n = 0 then Result := 0 else if n = 1 then Result := 1 else Result := FibRec(n-1) + FibRec(n-2); end;
Jedes Problem welches sich Rekursiv lösen lässt lässt sich auch Iterativ lösen und vice versa. Manchmal sind Rekursive funtkionen einfacher zu lesen, kürzer und intuitiver. In einigen Fällen ist die Intuitive Kurze Lösung allerdings auch nicht unbedingt die Beste. Das Beispiel Fibonacci ist in dieser Form extrem ineffizient, da so gut wie alle Zahlen mehrfach ausgerechnet werden, was eine Kaskade an Berechnungen zur folge hat. Probiert es gerne mal mit großen Zahlen aus, ihr werdet schnell merken das die Berechnungen sehr schnell sehr lange dauern.
Eine spontane iterative Lösung wäre:
Code:
function FibIt(n: Integer): Integer; var Fib: Array of Integer; i: Integer; begin if n=0 then begin Result := 0; Exit; end; SetLength(Fib, n+1); Fib[0]:=0; Fib[1]:=1; for i := 2 to n do Fib[i] := Fib[i-1] + Fib[i-2]; Result := Fib[n] end;
Auch dieser Lösungsweg lässt sich Rekursiv umsetzen, das wäre aber ein deutlich größerer Aufwand und lohnen tut es sich in diesem Fall nicht.
Allerdings kann es auch Anders rum sein, das der spontane iterative Lösungsweg nicht so effizient ist wie der spontane Rekursive.
Bemerkung: Ich schreibe bewusst spontan, da man mit etwas Nachdenken auch auf effiziente Rekursive oder effiziente Iterative wege kommen kann, diese sind allerdings nicht direkt erkennbar.
Wenn man viel Programmiert entwickelt man ein Gefühl dafür wann eine rekursive Lösung und wann eine Iterative besser ist.
Allgemeine Rekursive Funktionen
An dem obigen Beispiel habe ich eine typische rekursive Fuktion gezeigt. Nun möchte ich allgemein auf rekursive Funktionen und ihre Eigenschaften eingehen.
Mit rekursiven Funktionen reduzieren wir Probleme durch erneutes Aufrufen mit veränderten Datensatz auf Basisfälle, die Abbruchbedingungen.
Die Anzahl der Aufrufe nennt man die Rekursionstiefe. Die Rekursionstiefe ist allerdings beschränkt. Da jeder Funktionsaufruf seinen eigenen Stack hat auf den mindestens die Rücksprungaddresse geschrieben wird, wird mit jedem Rekursionsaufruf Speicher belegt. Somit ist unsere Rekursionstiefe durch den Speicher begrenzt. Endlosschleifen mit Rekursion sind daher nicht möglich.
Das bedeutet natürlich auch das wir immer eine Abbruchbedingung benötigen. Diese kann wie in dem Beispiel Fibonacci explizit angegeben werden, oder auch Implizit erreicht werden, zum Beispiel bis wir keine Daten zum verarbeiten mehr haben. Ein Beispiel dazu folgt etwas später.
Oben habe ich geschrieben, jedes Problem welches sich iterativ lösen lässt lässt sich auch rekursiv lösen. Das stimmt zwar allerdings lässt sich nicht zu jedem rekursiven Algorithmus auch ein Äquivalenter iterativer Algorithmus finden. Oben der Iterative Fibonacci Algorithmus mag zwar die Fibonacci Zahlen ausrechnen, aber auf eine Komplett andere Art und Weise. Die Rekursiven Funktionen für die es eine Äquivalente iterative Lösung gibt nennt man Primitive Rekursionen.
Ein Beispiel für eine Solche wäre:
Code:
function SumRec(n: Integer): Integer; begin if n=0 then Result := 0 else Result := SumRec(n-1); end; function SumIt(n: Integer): Integer; var i: Integer begin Result := 0; for i := n downto 0 do Result := Result + i; end;
Da man nur Primitve Rekursionen einfach zu Iterativen Funktionen umformen kann gibt es auch keine allgemeingültige Methode um Rekursiv zu Iterativ und vice versa umzuformen.
Nun aber wieder zu etwas praktischeren Beispielen.
Dateien Auflisten
Nun möchte ich mein Lieblingsbeispiel für rekursive Funktionen anführen, das Suchen von Dateien in Unterordnern. Dies ist ein rekursiver Algorithmus auf den ich immer wieder zurückgreife.
Die Idee dabei ist Rekursiv so Vorzugehen:
Code:
1. Iteriere durch alle Dateien in einem Verzeichnis 2. Wenn die Datei gesucht wird (z.B. die korrekte Dateiendung hat) liste sie auf 3. Iteriere durch alle Unterordner im Verzeichnis 4. Rufe für jeden Rekursiv diese Funktion erneut auf
Um das nun umzusetzen muss ich zunächst einmal die Funktion FindFirst, FindNext und FindClose.
Mit diesen Funktionen können wir Dateien und Ordner in einem Verzeichnis suchen.
Mit FindFirst beginnen wir die Suche, übergeben das so genannte Searchpattern, die Art der Dateien die wir suchen (z.B. Ordner, versteckte Dateien, etc) und ein SearchRecord, eine Variable in die die alle nötigen Informationen für die Suche und gefundenen Dateien geschrieben wird. Die Rückgabe von FindFirst ist ein Integer. Wurden Dateien gefunden gibt die Funktion 0 zurück, sonst eine negative Zahl.
FindNext bekommt den SearchRecord übergeben und Sucht nach der nächsten Datei. FindNext gibt 0 zurück falls weitere Datei gefunden wurde, also die Suche noch nicht beendet ist.
FindClose gibt die Resourcen wieder frei. Das Dateisystem wird vom Betriebsystem gehandhabt, und ein Aufruf von FindFirst sagt dem Betriebsystem es soll die Dateisuche beginnen. Mit FindClose sagen wir dem Betriebsystem das die Suche beendet ist, und es den Suchprozess aus dem Speicher werfen kann. Das lässt sich mit dem Dispose bei Zeigern vergleichen. Übergeben wird wieder das SearchRecord. Gibt FindFirst nicht 0 zurück so ist der Aufruf von FindClose nicht nötig.
Also beginnen wir:
Code:
program ListFiles; {$MODE ObjFPC}{$H+} uses SysUtils; // FindFirst, FindNext, FindClose // Directory: Das Verzeichnes zum Suchen // ext: Die zu suchende Dateiendung procedure ListFiles(Directory, ext: String); var sr: TSearchRec; //Searchrecord begin if FindFirst(IncludeTrailingPathDelimiter(Directory)+'*', faAnyFile, sr) = 0 then try repeat if ((sr.attr and faDirectory) <> faDirectory) And (ExtractFileExt(sr.Name) = ext) then WriteLn(IncludeTrailingPathDelimiter(Directory) + sr.Name); until FindNext(sr) <> 0 finally FindClose(sr); end; end; end.
IncludeTrailingPathDelimiter ist eine Funktion die einen Pfad annimmt und diesen zurückgibt mit einem / bzw \ (Windows) am Ende falls dieses nicht vorhanden sein sollte. So wird aus /Pfad dann /Pfad/.
In der Repeat-Until Schleife iterieren wir nun durch alle Dateien die gefunden werden. Sr enthält bei jedem Durchlauf der Schleife die Informationen über die gefundene Datei, das ist zum einen der Name oder auch um was für eine Datei es sich handelt. Die Abfrage ((sr.attr and faDirectory) <> faDirectory) And (ExtractFileExt(sr.Name) = ext) ist die Überprüfung ob es sich um ein Verzeichnis handelt, welche wir später behandeln wollen, und ob die Datei die gewünschte Dateiendung hat. sr.attr enthält die Dateiattribute als Flags (siehe Einschub: Variante Records, With, Flags).
Ist die Bedingung erfüllt geben wir den Dateinamen aus. Wir könnten ihn auch in einem Array Speichern wenn wir die Dateien zwischenspeichern wollen.
Wichtig ist das Try-Finnaly, damit auf jeden Fall das FindClose aufgerufen wird
Nun kommen wir aber zu der Rekursion:
Code:
program ListFiles; {$MODE ObjFPC}{$H+} uses SysUtils; // FindFirst, FindNext, FindClose // Directory: Das Verzeichnes zum Suchen // ext: Die zu suchende Dateiendung procedure ListFiles(Directory, ext: String); var sr: TSearchRec; //Searchrecord begin if FindFirst(IncludeTrailingPathDelimiter(Directory)+'*', faAnyFile, sr) = 0 then try repeat if ((sr.attr and faDirectory) <> faDirectory) And (ExtractFileExt(sr.Name) = ext) then WriteLn(IncludeTrailingPathDelimiter(Directory) + sr.Name); until FindNext(sr) <> 0 finally FindClose(sr); end; if FindFirst(IncludeTrailingPathDelimiter(Directory)+'*', faDirectory, sr) = 0 then try repeat if ((sr.attr and faDirectory) = faDirectory) And (sr.Name <> '..') And (sr.Name <>'.') then ListFiles(IncludeTrailingPathDelimiter(Directory)+sr.Name, ext); until FindNext(sr) <> 0 finally FindClose(sr); end; end; end.
Damit wäre das Programm komplett:
Code:
program ListFiles; {$MODE ObjFPC}{$H+} uses SysUtils; // FindFirst, FindNext, FindClose // Directory: Das Verzeichnes zum Suchen // ext: Die zu suchende Dateiendung procedure ListFiles(Directory, ext: String); var sr: TSearchRec; //Searchrecord begin if FindFirst(IncludeTrailingPathDelimiter(Directory)+'*', faAnyFile, sr) = 0 then try repeat if ((sr.attr and faDirectory) <> faDirectory) And (ExtractFileExt(sr.Name) = ext) then WriteLn(IncludeTrailingPathDelimiter(Directory) + sr.Name); until FindNext(sr) <> 0 finally FindClose(sr); end; if FindFirst(IncludeTrailingPathDelimiter(Directory)+'*', faDirectory, sr) = 0 then try repeat if ((sr.attr and faDirectory) = faDirectory) And (sr.Name <> '..') And (sr.Name <>'.') then ListFiles(IncludeTrailingPathDelimiter(Directory)+sr.Name, ext); until FindNext(sr) <> 0 finally FindClose(sr); end; end; begin ListFiles('Pfad', '.pas'); end.
Wöllte man nun die Dateien in einen Array speichern würde die Anpassung recht simpel gehen:
Code:
program ListFiles; {$MODE ObjFPC}{$H+} uses SysUtils; // FindFirst, FindNext, FindClose Type TStringArr = array of String; // Directory: Das Verzeichnes zum Suchen // ext: Die zu suchende Dateiendung procedure ListFiles(Directory, ext: String; var arr: TStringArr); var sr: TSearchRec; //Searchrecord begin if FindFirst(IncludeTrailingPathDelimiter(Directory)+'*', faAnyFile, sr) = 0 then try repeat if ((sr.attr and faDirectory) <> faDirectory) And (ExtractFileExt(sr.Name) = ext) then begin SetLength(arr, Length(arr)+1); arr[High(arr)] := IncludeTrailingPathDelimiter(Directory)+sr.Name; end; until FindNext(sr) <> 0 finally FindClose(sr); end; if FindFirst(IncludeTrailingPathDelimiter(Directory)+'*', faDirectory, sr) = 0 then try repeat if ((sr.attr and faDirectory) = faDirectory) And (sr.Name <> '..') And (sr.Name <>'.') then ListFiles(IncludeTrailingPathDelimiter(Directory)+sr.Name, ext, arr); until FindNext(sr) <> 0 finally FindClose(sr); end; end; var a: TStringArr; s: String; begin ListFiles('Pfad', '.pas', a); for s in a do WriteLn(s); end.
Rekursive Datentypen
Wir hatten im 6. Kapitel gelernt wie wir eigene Datentypen schreiben. Wir können Datentypen Rekursiv verwenden. Das bedeutet im Sinne von Datentypen, dass es sich dabei um Zusammengesetzte Datentypen (Records oder Arrays) bei denen 1 oder mehr Elemente ein Zeiger auf den selben Datentyp ist.
Auf diesen Datentypen empfiehlt sich die Nutzung von Rekursiven Funktionen.
Diese Typen verwendet man für so genannte Verkettungen, wobei eine Variable auf andere Variablen des selben Typs verweisen.
Für die Definition solcher Typen gibt es in Pascal eine Syntaktische besonderheit. Zeiger auf einen Typen lassen sich über dem Typen selbst im Quellcode Definieren:
Code:
type PMyRec = ^TMyRec; TMyRec = record //Elemente end;
Code:
type PMyRecursiveType = ^TMyRecursiveType; TMyRecursiveType = record Next: PMyRecursiveType; end;
Eine Einsatzmöglichkeit solcher Typen sind z.B. Verkettete Listen (engl. linked list). Diese geben uns die Möglichkeit, ähnlich zu dynamischen Arrays beliebig viele Elemente zu Speichern. Das Prinzip ist recht simpel, jedes element enthält einen Zeiger auf das Nächste. Das letzte Element hat einen Zeiger auf nil.
Um eine verkettete Integer Liste zu Implementieren wäre der Rekursive Typ:
Code:
type PListItem = ^TListItem; TListItem = record Value: Integer; NextItem: PListItem; end;
Schreiben wir uns dafür mal ein paar Funktionen die das Realisieren.
Fangen wir an mit einer Funktion zur Berechnung der Anzahl der Elemente:
Code:
function GetListLength(List: PListItem): Integer; begin if Assigned(List) then Result := 1 + GetListLength(List^.NextItem) else Result := 0; end;
Als nächstes eine Funktion zum Hinzufügen am Anfang mit dem wert x
Code:
procedure AddElementAtStart(var List: PListItem; x: Integer); var tmp: PListItem; begin new(tmp); tmp^.Value := x; tmp^.NextItem := List; List := tmp; end;
Jetzt können wir noch eine Funktion schreiben zum Hinzufügen eines Elements am Ende
Code:
procedure AddElementAtEnd(var List: PListItem; x: Integer); begin if Assigned(List) then AddElementAtEnd(List^.NextItem, x) else begin new(List); List^.Value := x; List^.NextItem := nil; end; end;
Nun kommen wir zu der nächsten Funktion, zum löschen eines Elements gegeben mit dem Index i:
Code:
procedure DeleteListItem(List: PListItem; i: Integer); var tmp: PListItem; begin if not Assigned(List) then exit; // List Item nicht vorhanden if i>1 then DeleteListItem(List^.NextItem, i-1) else if Assigned(List^.NextItem) then // Wenn das zu löschende element exsistiert begin tmp:=List^.NextItem^.NextItem; //Übernächstes Item dispose(List^.NextItem); List^.NextItem := tmp; // Übernächstes ist jetzt nächstes Item end; end;
Wir gehen Rekursiv durch die gesammte Liste bis wir entweder nil bekommen (nach dem letzten Item) oder wir 1 Element vor dem zu löschenden Element sind.
Dann überprüfen wir ob das zu löschende Element überhaupt exsistiert, wenn es exsistiert speichern wir dessen Nachfolger, löschen das Element und setzen den gespeicherten Nachfolger als das nachvolge Element des Elements vor dem gelöschten.
Natürlich was auch noch Fehlt ist das Auslesen des Wertes eines bestimmten Elements mit Index i
Code:
function GetItemValue(List: PListItem; i: Integer): Integer; begin Result := 0; if Assigned(List) then if i>0 then Result:=GetItemValue(List^.NextItem, i-1) else Result:=List^.Value; end;
Natürlich benötigen wir auch noch eine Funktion um den Wert eines Elements mit Index i zu setzen
Code:
procedure SetItemValue(List: PListItem; i, x: Integer); begin if Assigned(List) then if i>0 then SetItemValue(List^.NextItem, i-1, x) else List^.Value := x; end;
Als letztes fehlt uns nur noch eine Funktion um die gesammte Liste zu löschen und den Speicher freizugeben
Code:
procedure ClearList(var List: PListItem); begin if Assigned(List) then begin ClearList(List^.NextItem); dispose(List); List := nil; end; end;
Natürlich könnte man noch einen Haufen weiterer Funktionen definieren wie FindElementWithValue um ein bestimmtes Element zu finden oder InsertAt um ein Element an einer bestimmten Position einzufügen, aber ich begnüge mich mit diesem Funktionsumfang.
Also bauen wir jetzt mal ein Projekt was diese Liste verwendet:
Code:
program Liste; {$mode objfpc}{$H+} type PListItem = ^TListItem; TListItem = record Value: integer; NextItem: PListItem; end; function GetListLength(List: PListItem): integer; begin if Assigned(List) then Result := 1 + GetListLength(List^.NextItem) else Result := 0; end; procedure AddElementAtStart(var List: PListItem; x: integer); var tmp: PListItem; begin new(tmp); tmp^.Value := x; tmp^.NextItem := List; List := tmp; end; procedure AddElementAtEnd(var List: PListItem; x: integer); begin if Assigned(List) then AddElementAtEnd(List^.NextItem, x) else begin new(List); List^.Value := x; List^.NextItem := nil; end; end; procedure DeleteListItem(List: PListItem; i: integer); var tmp: PListItem; begin if not Assigned(List) then exit; // List Item nicht vorhanden if i > 1 then DeleteListItem(List^.NextItem, i - 1) else if Assigned(List^.NextItem) then // Wenn das zu löschende element exsistiert begin tmp := List^.NextItem^.NextItem; //Übernächstes Item dispose(List^.NextItem); List^.NextItem := tmp; // Übernächstes ist jetzt nächstes Item end; end; function GetItemValue(List: PListItem; i: integer): integer; begin Result := 0; if Assigned(List) then if i > 0 then Result := GetItemValue(List^.NextItem, i - 1) else Result := List^.Value; end; procedure SetItemValue(List: PListItem; i, x: integer); begin if Assigned(List) then if i > 0 then SetItemValue(List^.NextItem, i - 1, x) else List^.Value := x; end; procedure ClearList(var List: PListItem); begin if Assigned(List) then begin ClearList(List^.NextItem); dispose(List); List := nil; end; end; var root: PListItem; i: Integer; begin root:=nil; // Initialisierung leere liste AddElementAtStart(root, 10); AddElementAtEnd(root, 5); AddElementAtStart(root, 3); AddElementAtStart(root, 8); AddElementAtEnd(root, 15); for i:=0 to GetListLength(root) -1 do SetItemValue(root, i, GetItemValue(root, i)+2); for i:=GetListLength(root)-1 downto 0 do WriteLn(GetItemValue(root, i)); ClearList(root); end.
Bemerkung: Es gibt in der RTL vernünftige Implementierungen solcher Datenstrukturen wie verkettete Listen, welche wir auch noch kennen lernen werden. Ich führe das hier nur als Beispiel zum lernen an, bei der Anwendung sollte man sich die Arbeit sparen und auf diese Implementierungen zurückgreifen.
Weitere typische Datenstrukturen für die rekursive Typen verwendet werden sind:
Bäume, z.B. ein Binärbaum, der aus verschiedenen Elementen besteht die jeweils genau 2 Nachfolger haben.
Doppelt verkettete Listen, Listen bei denen jedes Element einen Zeiger auf das nächste und das vorhergehende Element haben.
Ringlisten, bei denen das letzte Element wieder auf das erste verweist, etc.
Solche verketteten Listen kann man statt dynamischer Arrays (so genannte Array Listen) benutzen, Arrays bieten einen deutlich schnelleren Zugriff auf einzelne Elemente, und brauchen auch weniger Speicher, allerdings ist das Hinzufügen oder Einfügen von Elementen deutlich langsamer und aufwändiger.
Bibliotheken
Bibliotheken bezeichnen Sammlungen von Funktionen, Variablen, Typen, Konstanten oder Komponenten. Heutzutage sind Bibliotheken unerlässlig, vor allem in form von Frameworks werden große Bibliotheken von Unternehmen und Gruppen entwickelt, so dass der nutzer sich nicht um alle Implementierungen kümmern muss, sondern sich auf die Programme selbst konzentrieren kann. Im letzten Kapitel hatten wir die Datenstruktur der verketteten Liste kennengelernt, dank Bibliotheken müssen wir so etwas nie mehr selbst programmieren, da wir immer auf Implementierungenen in Bibliotheken zurückgreifen können.
Die Hauptbibliotheken mit denen wir in FreePascal arbeiten, bzw. arbeiten werden sind die RTL und FCL, welche Teil des FPC sind, und die LCL welche Teil der Lazarus Entwicklungsumgebung ist.
Units
Essentiell für Bibliotheken sind die Unit Dateien. Unit Dateien bieten uns die Möglichkeit Pascal Code, egal ob Typen, Konstanten, Variablen oder Funktionen, in eine extra Datei auszulagern, und diese dann in Programmen oder anderen Units zu verwenden. Eine Unit die wir bereits schon recht häufig verwendet haben ist die Unit SysUtils aus der RTL. Wir haben vor allem Funktionen wie StrToInt verwendet, aber auch schon Typen wie die Exception EConvertError (siehe Einschub Kontrollstrukturen 2).
Die Verwendung von Units geschieht über die Uses Klausel, wie wir es bereits mit SysUtils gemacht haben. Zu den Besonderheiten der Uses Klausel komme ich später noch zurück, doch jetzt erst einmal zum aufbau einer Unit Datei.
Ein Unit Datei ist eine Pascal Datei, normalerweise mit Dateiendung .pas oder .pp. Der Aufbau ist wie folgt:
Eine Unit Programmdatei beginnt mit dem Schlüsselwort unit gefolgt von dem Unit Name. Dieser Name ist der Bezeichner der in der Uses Klausel steht. Der Unit Name sollte mit dem Dateinamen übereinstimmen. Die Unit MyUnit sollte also entsprechend in einer Datei MyUnit.pas oder MyUnit.pp stehen. Diese Kopfzeile wird, wie bei Programm Dateien mit einem Semikolon abgeschlossen.
Abgeschlossen wird die Datei mit einem end. welches, wie bei Programmen auch, das Dateiende Signalisiert.
Direkt unter dem Kopf kommen dann globale Compileranweisungen wie unser altbekanntes $Mode.
Darunter wird die Unit in 2 Bereiche eingeteilt, dem Interface und der Implementation.
Der Interface Teil enthält die allgemeine Uses Klausel der Datei sowie alle Informationen die nach außen Sichtbar sein sollen, also die von anderen Units und Programmen verwendet werden können. Darunter fallen Typen, Funktionen, globale Variablen und Konstanten. Im Interface kann aber kein ausführbarer Block stehen. Um Funktionen öffentlich zu machen werden diese im Implementation Teil geschrieben, und im Interface steht ein Verweis darauf, der Funktionskopf.
Der Implementation Teil kann auch eine Uses Klausel enthalten, diese ist allerdings nur im Implementation Teil nutzbar. Dies hat den Grund, da man in Pascal keine Zirkulären Unit Referenzen schreiben kann, das heißt wenn Unit1 im Interface über Uses Unit2 einbindet darf Unit2 im Interface Teil nicht Unit1 einbinden. Werden allerdings dennoch Elemente, z.B. eine Globale Variable aus Unit1 benötigt so kann man Unit1 im Implementation Teil über uses Einbinden. Allerdings sollte man auch möglichst versuchen das zu vermeiden, und für die geteilten Informationen einfach eine extra Unit erstellen welche von Unit1 und Unit2 verwendet wird.
Darauf folgen im Implementation Teil Typen, Variablen und Konstanten die nur innerhalb der Unit verwendet werden. Hierbei gilt die Regel, es ist alles benutzbar was im Quelltext drüber steht.
Außerdem lassen sich im Implementation Teil Funktionen schreiben. Dies ist eigentlich genau so wie das Funktionsschreiben in einem Programm. Möchte man nun diese Funktionen für anderen Dateien sichtbar machen, so muss man den Funktionskopf in den Implementation Teil kopieren.
Unter dem Implementation Teil kann noch ein initialization und ein finalization Block stehen. Der Initialization Block wird beim start der Anwendung ausgeführt und kann genutzt werden um z.B. Globale Variablen für die Unit zu setzen. Der Finalization Block wird bei Programmende Ausgeführt und kann z.B. zum Aufräumen wie dem Freigeben von Zeigern verwendet werden.
Eine Beispiel Unit könnte wie folgt aussehen:
Diese Unit würde nun einem Programm oder anderen Units eine Globale Variable, eine Funktion, zwei Typen und eine Konstante bereitstellen. Gleichzeitig übernimmt diese Unit noch das Erstellen und bereinigen eines Zeigers und verwendet intern noch eine Variable und eine Funktion welche von anderen Dateien nicht verwendet werden können.
Nun aber zu einem etwas praktischeren Beispiel, nehmen wir die Verkettete Liste aus dem Letzen Kapitel, diese eignet sich sehr gut zum Auslagern.
Also erstellen wir im ordner unseres Programms Liste aus dem letzen Kapitel eine neue Datei, linkedlist.pas und schreiben dort schonmal die Allgemeine Unit struktur hin:
Bemerkung: In Lazarus gibt es die Schaltfläche "Neue Unit" oder den Menüeintrag Datei->Neue Unit um eine solche Unit zu erstellen.
Nun gehen wir in unsere Programmdatei, alle Funktionen die wir für die Liste erstellt haben können wir nun in den Implementation Teil der neuen Unit kopieren, und dann aus der Programmdatei löschen
Um diese nun öffentlich nutzbar zu machen müssen wir noch die Funktionsköpfe jeder Funktion in den Interface Teil der Unit schreiben:
Nun müssen wir nur noch den Typ vom Programm in die Unit kopieren, und wir haben unsere Verkettete Liste komplett in eine Unit ausgelagert:
In unserem Programm fügen wir dann noch ein uses LinkedList hinzu:
Und schon haben wir eine eigene kleine Bibliotheken für Verkettete Listen erstellt, die wir jetzt in anderen Units oder Programmen verwenden können.
Units geben uns die Möglichkeit unser Programm in kleine Programmdateien zu unterteilen, was zum einen eine bessere Struktur und Lesbarkeit gibt, wodurch es einfacher ist bestimmte dinge zu finden, und zu bearbeiten, zum anderen erlauben sie uns aber auch Programmcode ohne viel aufwand jederzeit wieder zu verwenden.
Nun möchte ich nochmal auf Uses eingehen. Im Uses wird, über Komma getrennt, der name der Units angegeben die man in dieser Datei verwenden möchte, dabei sucht der FPC zunächst einmal in den Suchpfaden nach der Unit Datei, heißt die Unit z.B. Unit1 so sucht der Compiler nach Unit1.pas oder Unit1.pp.
Die Suchpfade des FPC sind:
1. Das Programmverzeichnis
2. Das Compilerverzeichnis
3. Die Suchpfade aus der fpc.cfg
4. Die Suchpfade die über -Fu beim Compilieren angegeben werden.
Bei der Verwendung von Lazarus kümmert sich die IDE um das verwalten der Suchpfade.
Liegt eine Datei nicht in den Suchpfaden, oder ist anders als der Unitname benannt, so kann man mit dem Schlüsselwort in auch den Pfad angeben:
Bemerkung: Unter Windows sollte man natürlich \ statt / verwenden.
Die Reihenfolge in der man die Units einbindet spielt auch eine Rolle, hat man mehrere Dateien die gleichnamige Funktionen, Konstanten, etc. hat, so wird der FPC immer entweder aus der selben Datei nehemen oder aus der, die aus der Unit nehmen die als erste in der Uses Klausel steht. Möchte man dennoch z.B. eine Funktion aus einer anderen Unit verwenden kann man diese speziell angeben:
Außerdem werden die Initialization und Finalization Teile in der Reihenfolge ausgeführt wie sie in der Uses Klausel stehen. Hat man also eine Unit, welche eine z.B. eine Globale Variable im Initialization Teil setzt, welche von anderen Units im Initialization Teil verwendet wird, so muss diese vor den anderen Units im Uses stehen. Ein Beispiel dazu folgt später in diesem Kapitel.
Programmbibliotheken
Mit Units können wir Pascal Quelltext Bibliotheken erstellen. Eine weitere Form der Bibliotheken sind die Programmbibliotheken (engl. shared library). Programmbibliotheken sind Sammlungen von Funktionen in kompilierter Form. Programmbibliotheken ermöglichen das bereitstellen von Programmcode Programmiersprachenunabhängig. Das heißt einfach, da diese Bibliotheken kompiliert sind, sind sie wie Programme von Betriebsystem verwaltet, und andere Programme könne diese einfach Aufrufen, dann wird der Programmcode der Bibliothek ausgeführt. Im gegensatz zu Programmen sind Programmbibliotheken keine eigenständigen Programme und benötigen ein aufrufendes Programm welche sie aufruft.
Außerdem da Programmbibliotheken Kompiliert sind bieten sie die Möglichkeit Funktionen bereitzustellen, ohne dass der nutzer einblick in diesen erhält, oder darin was verändern kann.
Typische Programmbibliotheken sind System API's, wie die Windows API. Diese API's (Application Programming Interface) bieten über Bibliotheken die möglichkeit Systemfunktionen aufzurufen.
Programmbibliotheken sind je nach Betriebsystem unterschiedlich. Außerdem gibt es Statische Bibliothek und Dynamische Bibliotheken.
Statische und Dynamische Bibliotheken werden durch die Dateiendung unterschieden:
Windows: Statisch .lib Dynamisch .dll
Linux: Statisch .a Dynamisch .so
Mac OS X: Statisch .a Dynamisch .dylib
Dynamische Bibliotheken werden nicht in die Executeable kompiliert, und müssen separat mitgeliefert werden als so genannte Abhängigkeiten (engl. dependency). Bei Statischen Bibliotheken fällt dies weg. Solche Dateien sind euch daher vielleicht schon mal bei anderen Programmen aufgefallen.
Funktionen aus statische Bibliotheken werden von dem Linker während des Kompilierens im Programm verwiesen, sodass das Programm diese beim Ausführen über diese verweise aufgerufen.
Dynamische Bibliotheken können das selbe aber bieten auch die Möglichkeit während der Laufzeit Funktionen aus diesem Bibliotheken mit dem Funktionsnamen über Funktionszeiger aufzurufen. Damit lässt sich zum Beispiel ein Plugin System schreiben, welches alle Bibliotheken in einem Ordner läd und eine Funktion LoadPlugin aufruft.
Bei der Nutzung von Bibliotheken kümmert sich das System um das laden der Bibliotheken. Dabei suchen die Systeme in verschiedenen Ordnern nach den gesuchten Bibliotheken. Zunächst wird von den Systemen immer im Aktuellen Programmverzeichnis nach der Bibliothek gesucht. Wird sie dort nicht gefunden wird in den System Bibliotheksverzeichnis gesucht (Windows: Windows\System32, Linux/Unix: /lib/ und /usr/lib/). Wenn dort die Bibliothek nicht gefunden wird kennt das System noch Pfadvariablen (Windows PATH, Unix: /etc/ld.so.conf) welche weitere Suchpfade angeben können.
Außerdem kann auch der absolute Pfad zu der Bibliotheksdatei angegeben werden.
Kommen wir zur Nutzung in Pascal.
Um Bibliotheken zu verwenden benötigen wir Schnittstellen für diese Bibliothek in einer Unit. Bei größeren Bibliothek mit vielen Funktionen empfiehlt es sich eine Unit pro Bibliothek zu schreiben, welche dann eine Schnittstelle zu allen Funktionen dieser Bibliothek darstellt. Diese Units nennt man Wrapper.
Befassen wir uns zunächst mal mit dem Statischen Linken.
Um dem Linker zu sagen er soll eine bestimmte Bibliothek gegenlinken kann man die Compileranweisungen {$LinkLib Lib} global in einer Datei verwenden, wobei der Linker dann nach der Bibliothek Lib sucht und wenn Funktionen aus dieser angefordert werden diese gegenlinkt. Als Beispiel nehmen wir die Funktion strlen aus der Programmiersprache C:
Der Linklib Compilerschalter sagt dem Linker er soll die C Bibliothek gegenlinken. Um Pascal nun zu sagen das die Funktion strlen exsistiert müssen wir dafür einen Funktionskopf angeben, welcher dem Funktionskopf der Bibliothek entspricht. Durch die cdecl Direktive sagen bestimmen wir für die Funktion die C Aufrufkonvention, da diese Funktion aus der C Lib stammt, und daher diese Aufrufkonvention verwendet. Das Schlüsselwort external sagt dem Linker er soll in den Bibliotheksdateien nach der Funktion strlen suchen und diese mit diesem Kopf verknüpfen. Da dieser Funktion im Interface steht ist sie damit auch für alle anderen Dateien zugänglich.
Damit lassen sich sowohl Dynamische als auch Statische Bibliotheken nutzen.
Funktionen aus dynamische Bibliotheken können mit einer Angabe bei der Funktionsdefinition verknüpft werden.
Nach dem external über eine String Konstante, in diesem Fall 'c', kann angegeben werden in welcher Bibliothek die Funktion gesucht werden soll. Wird noch ein "name Stringkonstante" angegeben, so wird festgelegt wie die Funktion in der Bibliothek heißt, das ist zum Beispiel möglich um die Funktion in Pascal anders zu benennen als in der Bibliothek. Würde man diese Unit so in einem Programm einbinden, so würde sowohl strlen als auch GetStrLen die selbe Funktion aus der C Bibliothek aufrufen.
Bemerkung: Name kann man auch mit der Linklib Methode verwenden.
Zuletzt gibt es noch die Möglichkeit Bibliotheken dynamisch in der Laufzeit zu laden, dafür stellt die RTL die Unit dynlibs zur Verfügung.
Mit der Funktion LoadLibrary können wir eine Bibliothek laden, diese Funktion gibt uns dann ein Handle zurück, eine art Zeiger für das Betriebsystem, damit das System weis um welche Bibliothek es sich handelt. SharedSuffix ist eine Konstante die je nach System die Dateiendung von Programmbibliotheken zurückgibt (Windows: 'dll'). Wird NilHandle (0) zurückgegeben so ist ein Fehler aufgetreten und die Bibliothek konnte nicht geladen werden. Danach öffnen wir einen Try-Finally Block, um sicher zu gehen, dass die Bibliothek auch Freigegeben wird.
Über GetProcedureAddress bekommen wir dann einen Funktionszeiger als untypisierten Zeiger übergeben. Um diesen ausführen zu können müssen wir ihn in einen Funktionszeiger der Bibliotheksfunktion Casten. In diesem Fall wäre das eine Funktion ohne Rückgabewert die einen Integer als Parameter an nimmt.
Das Freigeben der Bibliothek erfolgt am Ende über UnloadLibrary.
Unter Unix/Linux: Benutzt man Programmbibliotheken die nicht in Pascal geschrieben wurden welche Dynamischen Speicher (Zeiger, Strings, Dyn Arrays) übergeben der in der Bibliothek als auch im Programm verwaltet (Freigeben oder Alloziieren) wird, so muss als erste Unit in der Uses Klausel der Program Datei die Unit cmem eingebunden werden, damit diese im Initialization und Finalization Teil den Memory Manager umstellen kann.
Bibliotheken schreiben
Nun nachdem wir gelernt haben wie Programmbibliotheken verwendet werden befassen wir uns mit dem schreiben solcher. FreePascal unterstützt nur das schreiben Dynamischer Bibliotheken, bei Statischen kann man die Object Files welche beim Kompilieren erzeugt werden selbst gegenlinken und als Statische Bibliothek verwenden.
Dafür gibt es einen weiteren Typ von Pascal Quelltextdateien, die Library Datei.
Vom Aufbau ist dieser Dateityp sehr ähnlich zu dem Program Datentyp:
Das Schlüsselwort für diese Dateien ist library, sonst unterscheiden sich Program und Library Dateien bei der exports Sektion. In dieser Sektion listet man alle Funktionsnamen mit Komma getrennt auf die durch die Programmbibliothek bereitgestellt werden sollen.
Wie man sieht haben Bibliotheken auch eine ausführbaren Teil wie Programme, einen begin Block. Dieser wird beim Laden der Bibliothek ausgeführt. Sowohl die exports als auch begin Sektion sind optional, das heißt es ist möglich Bibliotheken zu schreiben die keine Funktionen bereitstellen, sondern nur Programmcode ausführen, aber auch möglich Bibliotheken zu schreiben welche keinen Programmcode ausführen. Außerdem ist es möglich Bibliotheken zu schreiben welche weder Programmcode ausführen noch Funktionen bereitstellen, wie sinnreich das ist dürft ihr selbst beurteilen.
Bemerkung: Der Initialization Teil einer der Units die von Bibliotheken verwendet werden wird beim Laden der Bibliothek ausgeführt und der Finalization Teil bei dem Freigeben.
Um nocheinmal unser Beispiel mit der Verketteten Liste zu verwenden schreiben wir nun für diese Liste eine eigene Programmbibliothek in der Datei linkedlist.pas:
Diese kompilieren wir dann ganz normal über
Damit sollte der FPC eine Dynamisch Programmbibliothek erzeugen, nach den Nameskonventionen des betreffenden Betriebsystems (Unter Unix ist wird vor den Dateinamen ein lib gehängt).
Dann benötigen wir eine Wrapper Unit llist.pas für diese Bibliothek:
Über die IFDEF Compileranweisungen befüllen wir je nach Betriebsystem die Konstante lllib mit dem Namen der Bibliothek. Bei Mac OSX (Darwin) muss außerdem noch eine $Linklib Compileranweisung gesetzt werden.
Dann können wir diese Wrapperunit ganz normal verwenden:
Konventionen für Programmbibliotheken
Programmbibliotheken bieten die Möglichkeit Funktionen bereitzustellen. Da sich jede Programmiersprache und auch einige Compiler bei Align von Records oder den Standardaufrufkonventionen von Funktionen unterscheiden muss man sich natürlich beim bereitstellen einer Programmbibliothek entsprechend richten.
FreePascal benutzt z.B. standardmäßig die Pascal Aufrufkonvention und ein eigenes Align für Recordelemente. Um nun die Bibliothek allgemein zugänglich zu machen ist dies nicht die beste Wahl. Den allgemeinen Standard bietet dabei die Programmiersprache C, da diese sehr weit Verbreitet ist, und der Standard durch die ISO wohldefiniert und einfach zugänglich ist.
Konkret bedeutet das:
Verwendung von Packed Records, oder C Record Align
Funktionsaufrufe mit cdecl Aufrufkonvention
Unter Unix/Linux: C Memory Management für Zeiger und Dynamischen Speicher (Strings und Arrays)
Um die Records anzupassen ist die einfachste Möglichkeit einfach Packed Records zu verwenden, diese sind zwar langsamer und umständlicher für das System als Records mit Alignment, aber werden von jeder Sprache gleich unterstützt und genutzt.
Wenn man nun aber nicht mit Packed Records arbeiten möchte tut es auch eine Zeile:
am Anfang jeder Datei in der Records Definiert werden, um das C Alignment zu verwenden.
Die Aufrufkonvention für Funkitonen kann man im Funktionskopf definieren:
würde eine Funktion mit der C Aufrufkonvention definieren.
Unter Windows wird auch sehr oft, z.B. in der Windows System API, die stdcall Aufrufkonvention verwendet:
Unter Unix/Linux:
Um das Memory Management umzustellen gibt es die Unit cmem, welche in ihrem Initialization und Finalization Teil darum kümmert. Diese muss als erste Unit der Bibliothek über Uses eingebunden werden:
Den Memory Manager umzustellen ist unerlässlig wenn man Dynamische Datentypen wie Zeiger, Strings oder Dynamische Arrays sowohl in der Bibliothek und einer Anwendung verwaltet wird. Aber es kommt sehr selten vor das Dynamischer Speicher sowohl von Bibliothek als auch Programm verwaltet werden muss.
Bemerkung: Während das Record Align und das Memory Management wichtig sind, um die Funktionalität bei der Nutzung durch andere Sprachen und Compiler zu gewährleisten, ist die Wahl der Aufrufkonvention (Pascal, C, StdCall) jedem Entwickler selbst überlassen, solange sie sauber dokumentiert ist.
Außerdem gibt es bei Mac OSX (Darwin) noch ein paar kleinere eigenheiten was das Dynamische Laden über LoadLibrary angeht. Über GetProcedureAddress(Lib, Name) können nur Funktionen geladen werden welche mit einem '_' vor dem Namen beginnen, das heißt beim Publizieren einer .dylib für OSX welche Dynamisch geladen werden soll muss in der Bibliothek jede Funktion noch ein _ vor den Namen bekommen:
Bei Statischer Nutzung ist dies nicht notwendig.
Unsere Bibliothek würde mit diesen Konventionen dann so aussehen:
Und der Wrapper:
Unsere Program Datei muss nicht bearbeitet werden.
Bemerkung: Da wir diese Bibliothek nur Statisch verwenden tritt das Problem unter OSX mit _ nicht auf.
Wrapper erstellen
Für viele Programmbibliotheken gibt es Wrapper für viele verschiedene Sprachen, meißt auch Pascal (oder Delphi). Ist dies aber nicht der Fall, so lässt sich zwar aus einer sauberen Dokumentation der Bibliothek ein solcher Konstruieren, allerdings ist dies sehr mühsam und in der Realität wird man schnell merken dass die meißten Bibliotheken recht unsauber dokumentiert sind.
Abhilfe verschafft uns dabei die Wrapper in anderen Programmiersprachen. Für die meißten Bibliotheken gibt es einen Wrapper in C, als .h Datei, welcher alle Funktionen sauber implementiert. Wenn man nun nicht so gut C kann, oder es einem zu aufwändig ist den Wrapper zu übersetzen gibt es ein kleines Programm als Teil der FPC Installation, h2pas, welches C Headerfiles weitestgehend in Pascal Units übersetzt. Die Dokumentation zu diesem Programm findet ihr .
Bibliotheken bezeichnen Sammlungen von Funktionen, Variablen, Typen, Konstanten oder Komponenten. Heutzutage sind Bibliotheken unerlässlig, vor allem in form von Frameworks werden große Bibliotheken von Unternehmen und Gruppen entwickelt, so dass der nutzer sich nicht um alle Implementierungen kümmern muss, sondern sich auf die Programme selbst konzentrieren kann. Im letzten Kapitel hatten wir die Datenstruktur der verketteten Liste kennengelernt, dank Bibliotheken müssen wir so etwas nie mehr selbst programmieren, da wir immer auf Implementierungenen in Bibliotheken zurückgreifen können.
Die Hauptbibliotheken mit denen wir in FreePascal arbeiten, bzw. arbeiten werden sind die RTL und FCL, welche Teil des FPC sind, und die LCL welche Teil der Lazarus Entwicklungsumgebung ist.
Units
Essentiell für Bibliotheken sind die Unit Dateien. Unit Dateien bieten uns die Möglichkeit Pascal Code, egal ob Typen, Konstanten, Variablen oder Funktionen, in eine extra Datei auszulagern, und diese dann in Programmen oder anderen Units zu verwenden. Eine Unit die wir bereits schon recht häufig verwendet haben ist die Unit SysUtils aus der RTL. Wir haben vor allem Funktionen wie StrToInt verwendet, aber auch schon Typen wie die Exception EConvertError (siehe Einschub Kontrollstrukturen 2).
Die Verwendung von Units geschieht über die Uses Klausel, wie wir es bereits mit SysUtils gemacht haben. Zu den Besonderheiten der Uses Klausel komme ich später noch zurück, doch jetzt erst einmal zum aufbau einer Unit Datei.
Ein Unit Datei ist eine Pascal Datei, normalerweise mit Dateiendung .pas oder .pp. Der Aufbau ist wie folgt:
Code:
unit Name; {$MODE ObjFpc}{$H+} // Weitere Compileranweisungen z.B. Packrecords interface // Öffentlicher Bereich, Zugriff aus allen anderen Dateien möglich { Uses Klausel } { Type Klausel } { Var Klausel } { Const Klausel } { Öffentliche Funktionsköpfe } implementation // Privater Bereich, nur lokale Definitionen und Programmcode { Uses Klausel } { Type Klausel } { Var Klausel } { Const Klausel } { Funktionen } initialization // optional // Dieser Bereich wird bei Programmstart ausgeführt finalization // optional // Dieser Bereich wird bei Programmende ausgeführt end.
Abgeschlossen wird die Datei mit einem end. welches, wie bei Programmen auch, das Dateiende Signalisiert.
Direkt unter dem Kopf kommen dann globale Compileranweisungen wie unser altbekanntes $Mode.
Darunter wird die Unit in 2 Bereiche eingeteilt, dem Interface und der Implementation.
Der Interface Teil enthält die allgemeine Uses Klausel der Datei sowie alle Informationen die nach außen Sichtbar sein sollen, also die von anderen Units und Programmen verwendet werden können. Darunter fallen Typen, Funktionen, globale Variablen und Konstanten. Im Interface kann aber kein ausführbarer Block stehen. Um Funktionen öffentlich zu machen werden diese im Implementation Teil geschrieben, und im Interface steht ein Verweis darauf, der Funktionskopf.
Der Implementation Teil kann auch eine Uses Klausel enthalten, diese ist allerdings nur im Implementation Teil nutzbar. Dies hat den Grund, da man in Pascal keine Zirkulären Unit Referenzen schreiben kann, das heißt wenn Unit1 im Interface über Uses Unit2 einbindet darf Unit2 im Interface Teil nicht Unit1 einbinden. Werden allerdings dennoch Elemente, z.B. eine Globale Variable aus Unit1 benötigt so kann man Unit1 im Implementation Teil über uses Einbinden. Allerdings sollte man auch möglichst versuchen das zu vermeiden, und für die geteilten Informationen einfach eine extra Unit erstellen welche von Unit1 und Unit2 verwendet wird.
Darauf folgen im Implementation Teil Typen, Variablen und Konstanten die nur innerhalb der Unit verwendet werden. Hierbei gilt die Regel, es ist alles benutzbar was im Quelltext drüber steht.
Außerdem lassen sich im Implementation Teil Funktionen schreiben. Dies ist eigentlich genau so wie das Funktionsschreiben in einem Programm. Möchte man nun diese Funktionen für anderen Dateien sichtbar machen, so muss man den Funktionskopf in den Implementation Teil kopieren.
Unter dem Implementation Teil kann noch ein initialization und ein finalization Block stehen. Der Initialization Block wird beim start der Anwendung ausgeführt und kann genutzt werden um z.B. Globale Variablen für die Unit zu setzen. Der Finalization Block wird bei Programmende Ausgeführt und kann z.B. zum Aufräumen wie dem Freigeben von Zeigern verwendet werden.
Eine Beispiel Unit könnte wie folgt aussehen:
Code:
unit TestUnit; {$Mode ObjFpc}{$H+} interface uses SysUtils; type PTestType = ^TTestType; TTestType = record i, x: Integer; end; var GlobaleVariable: PTestType; const GlobaleKonstante = 5; function GlobaleFunktion(i: Integer): Integer; implementation var Aufrufe: Integer; procedure LocalCounter; begin inc(Aufrufe); end; function GlobaleFunktion(i: Integer): Integer; begin Result := i * GlobaleKonstante; LocalCounter; end; initialization new(GlobaleVariable); Aufrufe := 0; finalization dispose(GlobaleVariable); end.
Nun aber zu einem etwas praktischeren Beispiel, nehmen wir die Verkettete Liste aus dem Letzen Kapitel, diese eignet sich sehr gut zum Auslagern.
Also erstellen wir im ordner unseres Programms Liste aus dem letzen Kapitel eine neue Datei, linkedlist.pas und schreiben dort schonmal die Allgemeine Unit struktur hin:
Code:
unit LinkedList; {$MODE ObjFpc}{$H+} interface implementation end.
Nun gehen wir in unsere Programmdatei, alle Funktionen die wir für die Liste erstellt haben können wir nun in den Implementation Teil der neuen Unit kopieren, und dann aus der Programmdatei löschen
Code:
function GetListLength(List: PListItem): integer; begin if Assigned(List) then Result := 1 + GetListLength(List^.NextItem) else Result := 0; end; procedure AddElementAtStart(var List: PListItem; x: integer); var tmp: PListItem; begin new(tmp); tmp^.Value := x; tmp^.NextItem := List; List := tmp; end; procedure AddElementAtEnd(var List: PListItem; x: integer); begin if Assigned(List) then AddElementAtEnd(List^.NextItem, x) else begin new(List); List^.Value := x; List^.NextItem := nil; end; end; procedure DeleteListItem(List: PListItem; i: integer); var tmp: PListItem; begin if not Assigned(List) then exit; // List Item nicht vorhanden if i > 1 then DeleteListItem(List^.NextItem, i - 1) else if Assigned(List^.NextItem) then // Wenn das zu löschende element exsistiert begin tmp := List^.NextItem^.NextItem; //Übernächstes Item dispose(List^.NextItem); List^.NextItem := tmp; // Übernächstes ist jetzt nächstes Item end; end; function GetItemValue(List: PListItem; i: integer): integer; begin Result := 0; if Assigned(List) then if i > 0 then Result := GetItemValue(List^.NextItem, i - 1) else Result := List^.Value; end; procedure SetItemValue(List: PListItem; i, x: integer); begin if Assigned(List) then if i > 0 then SetItemValue(List^.NextItem, i - 1, x) else List^.Value := x; end; procedure ClearList(var List: PListItem); begin if Assigned(List) then begin ClearList(List^.NextItem); dispose(List); List := nil; end; end;
Code:
function GetListLength(List: PListItem): integer; procedure AddElementAtStart(var List: PListItem; x: integer); procedure AddElementAtEnd(var List: PListItem; x: integer); procedure DeleteListItem(List: PListItem; i: integer); function GetItemValue(List: PListItem; i: integer): integer; procedure SetItemValue(List: PListItem; i, x: integer); procedure ClearList(var List: PListItem);
Code:
unit LinkedList; {$MODE ObjFpc}{$H+} interface type PListItem = ^TListItem; TListItem = record Value: integer; NextItem: PListItem; end; function GetListLength(List: PListItem): integer; procedure AddElementAtStart(var List: PListItem; x: integer); procedure AddElementAtEnd(var List: PListItem; x: integer); procedure DeleteListItem(List: PListItem; i: integer); function GetItemValue(List: PListItem; i: integer): integer; procedure SetItemValue(List: PListItem; i, x: integer); procedure ClearList(var List: PListItem); implementation function GetListLength(List: PListItem): integer; begin if Assigned(List) then Result := 1 + GetListLength(List^.NextItem) else Result := 0; end; procedure AddElementAtStart(var List: PListItem; x: integer); var tmp: PListItem; begin new(tmp); tmp^.Value := x; tmp^.NextItem := List; List := tmp; end; procedure AddElementAtEnd(var List: PListItem; x: integer); begin if Assigned(List) then AddElementAtEnd(List^.NextItem, x) else begin new(List); List^.Value := x; List^.NextItem := nil; end; end; procedure DeleteListItem(List: PListItem; i: integer); var tmp: PListItem; begin if not Assigned(List) then exit; // List Item nicht vorhanden if i > 1 then DeleteListItem(List^.NextItem, i - 1) else if Assigned(List^.NextItem) then // Wenn das zu löschende element exsistiert begin tmp := List^.NextItem^.NextItem; //Übernächstes Item dispose(List^.NextItem); List^.NextItem := tmp; // Übernächstes ist jetzt nächstes Item end; end; function GetItemValue(List: PListItem; i: integer): integer; begin Result := 0; if Assigned(List) then if i > 0 then Result := GetItemValue(List^.NextItem, i - 1) else Result := List^.Value; end; procedure SetItemValue(List: PListItem; i, x: integer); begin if Assigned(List) then if i > 0 then SetItemValue(List^.NextItem, i - 1, x) else List^.Value := x; end; procedure ClearList(var List: PListItem); begin if Assigned(List) then begin ClearList(List^.NextItem); dispose(List); List := nil; end; end; end.
Code:
program Liste; {$MODE ObjFpc}{$H+} uses LinkedList; var root: PListItem; i: Integer; begin root:=nil; // Initialisierung leere liste AddElementAtStart(root, 10); AddElementAtEnd(root, 5); AddElementAtStart(root, 3); AddElementAtStart(root, 8); AddElementAtEnd(root, 15); for i:=0 to GetListLength(root) -1 do SetItemValue(root, i, GetItemValue(root, i)+2); for i:=GetListLength(root)-1 downto 0 do WriteLn(GetItemValue(root, i)); ClearList(root); end.
Units geben uns die Möglichkeit unser Programm in kleine Programmdateien zu unterteilen, was zum einen eine bessere Struktur und Lesbarkeit gibt, wodurch es einfacher ist bestimmte dinge zu finden, und zu bearbeiten, zum anderen erlauben sie uns aber auch Programmcode ohne viel aufwand jederzeit wieder zu verwenden.
Nun möchte ich nochmal auf Uses eingehen. Im Uses wird, über Komma getrennt, der name der Units angegeben die man in dieser Datei verwenden möchte, dabei sucht der FPC zunächst einmal in den Suchpfaden nach der Unit Datei, heißt die Unit z.B. Unit1 so sucht der Compiler nach Unit1.pas oder Unit1.pp.
Die Suchpfade des FPC sind:
1. Das Programmverzeichnis
2. Das Compilerverzeichnis
3. Die Suchpfade aus der fpc.cfg
4. Die Suchpfade die über -Fu beim Compilieren angegeben werden.
Bei der Verwendung von Lazarus kümmert sich die IDE um das verwalten der Suchpfade.
Liegt eine Datei nicht in den Suchpfaden, oder ist anders als der Unitname benannt, so kann man mit dem Schlüsselwort in auch den Pfad angeben:
Code:
uses Unit1 in 'Unit1.pas', // Im selben Ordner Unit2 in '../Unit2.pas', // Relative Pfadangaben mit ../ und ./ möglich Unit3 in '/Pfad/Zur/Unit.pas'; // absoluze Pfadangaben
Die Reihenfolge in der man die Units einbindet spielt auch eine Rolle, hat man mehrere Dateien die gleichnamige Funktionen, Konstanten, etc. hat, so wird der FPC immer entweder aus der selben Datei nehemen oder aus der, die aus der Unit nehmen die als erste in der Uses Klausel steht. Möchte man dennoch z.B. eine Funktion aus einer anderen Unit verwenden kann man diese speziell angeben:
Code:
uses Unit1, Unit2; ... procedure Foo; begin end; begin Test; // Prozedur Test aus Unit1 Unit2.Test; // Aus Unit2 Foo; // Aus eigener datei; Unit1.Foo; // Aus Unit1 end;
Programmbibliotheken
Mit Units können wir Pascal Quelltext Bibliotheken erstellen. Eine weitere Form der Bibliotheken sind die Programmbibliotheken (engl. shared library). Programmbibliotheken sind Sammlungen von Funktionen in kompilierter Form. Programmbibliotheken ermöglichen das bereitstellen von Programmcode Programmiersprachenunabhängig. Das heißt einfach, da diese Bibliotheken kompiliert sind, sind sie wie Programme von Betriebsystem verwaltet, und andere Programme könne diese einfach Aufrufen, dann wird der Programmcode der Bibliothek ausgeführt. Im gegensatz zu Programmen sind Programmbibliotheken keine eigenständigen Programme und benötigen ein aufrufendes Programm welche sie aufruft.
Außerdem da Programmbibliotheken Kompiliert sind bieten sie die Möglichkeit Funktionen bereitzustellen, ohne dass der nutzer einblick in diesen erhält, oder darin was verändern kann.
Typische Programmbibliotheken sind System API's, wie die Windows API. Diese API's (Application Programming Interface) bieten über Bibliotheken die möglichkeit Systemfunktionen aufzurufen.
Programmbibliotheken sind je nach Betriebsystem unterschiedlich. Außerdem gibt es Statische Bibliothek und Dynamische Bibliotheken.
Statische und Dynamische Bibliotheken werden durch die Dateiendung unterschieden:
Windows: Statisch .lib Dynamisch .dll
Linux: Statisch .a Dynamisch .so
Mac OS X: Statisch .a Dynamisch .dylib
Dynamische Bibliotheken werden nicht in die Executeable kompiliert, und müssen separat mitgeliefert werden als so genannte Abhängigkeiten (engl. dependency). Bei Statischen Bibliotheken fällt dies weg. Solche Dateien sind euch daher vielleicht schon mal bei anderen Programmen aufgefallen.
Funktionen aus statische Bibliotheken werden von dem Linker während des Kompilierens im Programm verwiesen, sodass das Programm diese beim Ausführen über diese verweise aufgerufen.
Dynamische Bibliotheken können das selbe aber bieten auch die Möglichkeit während der Laufzeit Funktionen aus diesem Bibliotheken mit dem Funktionsnamen über Funktionszeiger aufzurufen. Damit lässt sich zum Beispiel ein Plugin System schreiben, welches alle Bibliotheken in einem Ordner läd und eine Funktion LoadPlugin aufruft.
Bei der Nutzung von Bibliotheken kümmert sich das System um das laden der Bibliotheken. Dabei suchen die Systeme in verschiedenen Ordnern nach den gesuchten Bibliotheken. Zunächst wird von den Systemen immer im Aktuellen Programmverzeichnis nach der Bibliothek gesucht. Wird sie dort nicht gefunden wird in den System Bibliotheksverzeichnis gesucht (Windows: Windows\System32, Linux/Unix: /lib/ und /usr/lib/). Wenn dort die Bibliothek nicht gefunden wird kennt das System noch Pfadvariablen (Windows PATH, Unix: /etc/ld.so.conf) welche weitere Suchpfade angeben können.
Außerdem kann auch der absolute Pfad zu der Bibliotheksdatei angegeben werden.
Kommen wir zur Nutzung in Pascal.
Um Bibliotheken zu verwenden benötigen wir Schnittstellen für diese Bibliothek in einer Unit. Bei größeren Bibliothek mit vielen Funktionen empfiehlt es sich eine Unit pro Bibliothek zu schreiben, welche dann eine Schnittstelle zu allen Funktionen dieser Bibliothek darstellt. Diese Units nennt man Wrapper.
Befassen wir uns zunächst mal mit dem Statischen Linken.
Um dem Linker zu sagen er soll eine bestimmte Bibliothek gegenlinken kann man die Compileranweisungen {$LinkLib Lib} global in einer Datei verwenden, wobei der Linker dann nach der Bibliothek Lib sucht und wenn Funktionen aus dieser angefordert werden diese gegenlinkt. Als Beispiel nehmen wir die Funktion strlen aus der Programmiersprache C:
Code:
unit libtest; {$MODE ObjFpc}{$H+} interface {$LINKLIB c} function strlen(P: pchar): longint; cdecl; external; implementation end.
Damit lassen sich sowohl Dynamische als auch Statische Bibliotheken nutzen.
Funktionen aus dynamische Bibliotheken können mit einer Angabe bei der Funktionsdefinition verknüpft werden.
Code:
unit libtest; {$MODE ObjFpc}{$H+} interface const LibName = 'c'; function strlen(P: pchar): longint; cdecl; external 'c'; function GetStrLen(P: pchar): longint; cdecl; external LibName name 'strlen'; implementation end.
Bemerkung: Name kann man auch mit der Linklib Methode verwenden.
Zuletzt gibt es noch die Möglichkeit Bibliotheken dynamisch in der Laufzeit zu laden, dafür stellt die RTL die Unit dynlibs zur Verfügung.
Code:
uses dynlibs; type TMyProc = procedure(Argument: Integer); // Funktionszeiger var Lib: TLibHandle; func: TMyProc; begin Lib := LoadLibrary('MyLib.' + SharedSuffix); // Bibliothek laden if Lib = NilHandle then Exit; // Fehler beim laden -> Ende try func := TMyProc(GetProcedureAddress(Lib, 'Funktionsname')); if Assigned(func) then // Funkiton wurde gefunden func(12); // Funktion aufrufen finally UnloadLibrary(Lib); // Freigeben end; end.
Über GetProcedureAddress bekommen wir dann einen Funktionszeiger als untypisierten Zeiger übergeben. Um diesen ausführen zu können müssen wir ihn in einen Funktionszeiger der Bibliotheksfunktion Casten. In diesem Fall wäre das eine Funktion ohne Rückgabewert die einen Integer als Parameter an nimmt.
Das Freigeben der Bibliothek erfolgt am Ende über UnloadLibrary.
Unter Unix/Linux: Benutzt man Programmbibliotheken die nicht in Pascal geschrieben wurden welche Dynamischen Speicher (Zeiger, Strings, Dyn Arrays) übergeben der in der Bibliothek als auch im Programm verwaltet (Freigeben oder Alloziieren) wird, so muss als erste Unit in der Uses Klausel der Program Datei die Unit cmem eingebunden werden, damit diese im Initialization und Finalization Teil den Memory Manager umstellen kann.
Bibliotheken schreiben
Nun nachdem wir gelernt haben wie Programmbibliotheken verwendet werden befassen wir uns mit dem schreiben solcher. FreePascal unterstützt nur das schreiben Dynamischer Bibliotheken, bei Statischen kann man die Object Files welche beim Kompilieren erzeugt werden selbst gegenlinken und als Statische Bibliothek verwenden.
Dafür gibt es einen weiteren Typ von Pascal Quelltextdateien, die Library Datei.
Vom Aufbau ist dieser Dateityp sehr ähnlich zu dem Program Datentyp:
Code:
library Name; { Compilerschalter } { Uses } { Typen } { Variablen } { Konstanten } { Funktionen } exports // optional { Funktionsnamen } begin // Optional { Quelltext } end.
Wie man sieht haben Bibliotheken auch eine ausführbaren Teil wie Programme, einen begin Block. Dieser wird beim Laden der Bibliothek ausgeführt. Sowohl die exports als auch begin Sektion sind optional, das heißt es ist möglich Bibliotheken zu schreiben die keine Funktionen bereitstellen, sondern nur Programmcode ausführen, aber auch möglich Bibliotheken zu schreiben welche keinen Programmcode ausführen. Außerdem ist es möglich Bibliotheken zu schreiben welche weder Programmcode ausführen noch Funktionen bereitstellen, wie sinnreich das ist dürft ihr selbst beurteilen.
Bemerkung: Der Initialization Teil einer der Units die von Bibliotheken verwendet werden wird beim Laden der Bibliothek ausgeführt und der Finalization Teil bei dem Freigeben.
Um nocheinmal unser Beispiel mit der Verketteten Liste zu verwenden schreiben wir nun für diese Liste eine eigene Programmbibliothek in der Datei linkedlist.pas:
Code:
library LinkedList; {$MODE OBJFPC}{$H+} type PListItem = ^TListItem; TListItem = record Value: integer; NextItem: PListItem; end; function GetListLength(List: PListItem): integer; begin if Assigned(List) then Result := 1 + GetListLength(List^.NextItem) else Result := 0; end; procedure AddElementAtStart(var List: PListItem; x: integer); var tmp: PListItem; begin new(tmp); tmp^.Value := x; tmp^.NextItem := List; List := tmp; end; procedure AddElementAtEnd(var List: PListItem; x: integer); begin if Assigned(List) then AddElementAtEnd(List^.NextItem, x) else begin new(List); List^.Value := x; List^.NextItem := nil; end; end; procedure DeleteListItem(List: PListItem; i: integer); var tmp: PListItem; begin if not Assigned(List) then exit; // List Item nicht vorhanden if i > 1 then DeleteListItem(List^.NextItem, i - 1) else if Assigned(List^.NextItem) then // Wenn das zu löschende element exsistiert begin tmp := List^.NextItem^.NextItem; //Übernächstes Item dispose(List^.NextItem); List^.NextItem := tmp; // Übernächstes ist jetzt nächstes Item end; end; function GetItemValue(List: PListItem; i: integer): integer; begin Result := 0; if Assigned(List) then if i > 0 then Result := GetItemValue(List^.NextItem, i - 1) else Result := List^.Value; end; procedure SetItemValue(List: PListItem; i, x: integer); begin if Assigned(List) then if i > 0 then SetItemValue(List^.NextItem, i - 1, x) else List^.Value := x; end; procedure ClearList(var List: PListItem); begin if Assigned(List) then begin ClearList(List^.NextItem); dispose(List); List := nil; end; end; exports GetListLength, AddElementAtStart, AddElementAtEnd, DeleteListItem, GetItemValue, SetItemValue, ClearList; end.
Code:
fpc linkedlist.pas
Dann benötigen wir eine Wrapper Unit llist.pas für diese Bibliothek:
Code:
unit llist; {$MODE OBJFPC}{$H+} interface type PListItem = ^TListItem; TListItem = record Value: integer; NextItem: PListItem; end; const {$ifdef win32} lllib = 'linkedlist.dll'; {$else} {$ifdef darwin} lllib = 'liblinkedlist.dylib'; {$linklib liblinkedlist.dylib} {$else} lllib = 'liblinkedlist.so'; {$endif} {$endif} function GetListLength(List: PListItem): integer; external lllib; procedure AddElementAtStart(var List: PListItem; x: integer); external lllib; procedure AddElementAtEnd(var List: PListItem; x: integer); external lllib; procedure DeleteListItem(List: PListItem; i: integer); external lllib; function GetItemValue(List: PListItem; i: integer): integer; external lllib; procedure SetItemValue(List: PListItem; i, x: integer); external lllib; procedure ClearList(var List: PListItem); external lllib; implementation end.
Dann können wir diese Wrapperunit ganz normal verwenden:
Code:
program LinkedListTest; {$MODE OBJFPC}{$H+} uses llist; var root: PListItem; i: Integer; begin root:=nil; // Initialisierung leere liste AddElementAtStart(root, 10); AddElementAtEnd(root, 5); AddElementAtStart(root, 3); AddElementAtStart(root, 8); AddElementAtEnd(root, 15); for i:=0 to GetListLength(root) -1 do SetItemValue(root, i, GetItemValue(root, i)+2); for i:=GetListLength(root)-1 downto 0 do WriteLn(GetItemValue(root, i)); ClearList(root); end.
Konventionen für Programmbibliotheken
Programmbibliotheken bieten die Möglichkeit Funktionen bereitzustellen. Da sich jede Programmiersprache und auch einige Compiler bei Align von Records oder den Standardaufrufkonventionen von Funktionen unterscheiden muss man sich natürlich beim bereitstellen einer Programmbibliothek entsprechend richten.
FreePascal benutzt z.B. standardmäßig die Pascal Aufrufkonvention und ein eigenes Align für Recordelemente. Um nun die Bibliothek allgemein zugänglich zu machen ist dies nicht die beste Wahl. Den allgemeinen Standard bietet dabei die Programmiersprache C, da diese sehr weit Verbreitet ist, und der Standard durch die ISO wohldefiniert und einfach zugänglich ist.
Konkret bedeutet das:
Verwendung von Packed Records, oder C Record Align
Funktionsaufrufe mit cdecl Aufrufkonvention
Unter Unix/Linux: C Memory Management für Zeiger und Dynamischen Speicher (Strings und Arrays)
Um die Records anzupassen ist die einfachste Möglichkeit einfach Packed Records zu verwenden, diese sind zwar langsamer und umständlicher für das System als Records mit Alignment, aber werden von jeder Sprache gleich unterstützt und genutzt.
Wenn man nun aber nicht mit Packed Records arbeiten möchte tut es auch eine Zeile:
Code:
{$PackRecords C}
Die Aufrufkonvention für Funkitonen kann man im Funktionskopf definieren:
Code:
procedure Foo(Arg: Typ); cdecl;
Unter Windows wird auch sehr oft, z.B. in der Windows System API, die stdcall Aufrufkonvention verwendet:
Code:
procedure Foo(Arg: Typ); stdcall;
Um das Memory Management umzustellen gibt es die Unit cmem, welche in ihrem Initialization und Finalization Teil darum kümmert. Diese muss als erste Unit der Bibliothek über Uses eingebunden werden:
Code:
library Name; { Compilerschalter } uses cmem, //Weitere Units ...
Bemerkung: Während das Record Align und das Memory Management wichtig sind, um die Funktionalität bei der Nutzung durch andere Sprachen und Compiler zu gewährleisten, ist die Wahl der Aufrufkonvention (Pascal, C, StdCall) jedem Entwickler selbst überlassen, solange sie sauber dokumentiert ist.
Außerdem gibt es bei Mac OSX (Darwin) noch ein paar kleinere eigenheiten was das Dynamische Laden über LoadLibrary angeht. Über GetProcedureAddress(Lib, Name) können nur Funktionen geladen werden welche mit einem '_' vor dem Namen beginnen, das heißt beim Publizieren einer .dylib für OSX welche Dynamisch geladen werden soll muss in der Bibliothek jede Funktion noch ein _ vor den Namen bekommen:
Code:
library Example; {$ifdef darwin} procedure _Foo; {$else} procedure Foo; {$endif} begin ... end; exports {$ifdef darwin} _Foo; {$else} Foo; {$endif} end.
Unsere Bibliothek würde mit diesen Konventionen dann so aussehen:
Code:
library LinkedList; {$MODE OBJFPC}{$H+} {$PackRecords C} type PListItem = ^TListItem; TListItem = record Value: integer; NextItem: PListItem; end; function GetListLength(List: PListItem): integer; cdecl; begin if Assigned(List) then Result := 1 + GetListLength(List^.NextItem) else Result := 0; end; procedure AddElementAtStart(var List: PListItem; x: integer); cdecl; var tmp: PListItem; begin new(tmp); tmp^.Value := x; tmp^.NextItem := List; List := tmp; end; procedure AddElementAtEnd(var List: PListItem; x: integer); cdecl; begin if Assigned(List) then AddElementAtEnd(List^.NextItem, x) else begin new(List); List^.Value := x; List^.NextItem := nil; end; end; procedure DeleteListItem(List: PListItem; i: integer); cdecl; var tmp: PListItem; begin if not Assigned(List) then exit; // List Item nicht vorhanden if i > 1 then DeleteListItem(List^.NextItem, i - 1) else if Assigned(List^.NextItem) then // Wenn das zu löschende element exsistiert begin tmp := List^.NextItem^.NextItem; //Übernächstes Item dispose(List^.NextItem); List^.NextItem := tmp; // Übernächstes ist jetzt nächstes Item end; end; function GetItemValue(List: PListItem; i: integer): integer; cdecl; begin Result := 0; if Assigned(List) then if i > 0 then Result := GetItemValue(List^.NextItem, i - 1) else Result := List^.Value; end; procedure SetItemValue(List: PListItem; i, x: integer); cdecl; begin if Assigned(List) then if i > 0 then SetItemValue(List^.NextItem, i - 1, x) else List^.Value := x; end; procedure ClearList(var List: PListItem); cdecl; begin if Assigned(List) then begin ClearList(List^.NextItem); dispose(List); List := nil; end; end; exports GetListLength, AddElementAtStart, AddElementAtEnd, DeleteListItem, GetItemValue, SetItemValue, ClearList; end.
Code:
unit llist; {$MODE OBJFPC}{$H+} {$PackRecords C} interface type PListItem = ^TListItem; TListItem = record Value: integer; NextItem: PListItem; end; const {$ifdef win32} lllib = 'linkedlist.dll'; {$else} {$ifdef darwin} lllib = 'liblinkedlist.dylib'; {$linklib liblinkedlist.dylib} {$else} lllib = 'liblinkedlist.so'; {$endif} {$endif} function GetListLength(List: PListItem): integer; cdecl; external lllib; procedure AddElementAtStart(var List: PListItem; x: integer); cdecl; external lllib; procedure AddElementAtEnd(var List: PListItem; x: integer); cdecl; external lllib; procedure DeleteListItem(List: PListItem; i: integer); cdecl; external lllib; function GetItemValue(List: PListItem; i: integer): integer; cdecl; external lllib; procedure SetItemValue(List: PListItem; i, x: integer); cdecl; external lllib; procedure ClearList(var List: PListItem); cdecl; external lllib; implementation end.
Bemerkung: Da wir diese Bibliothek nur Statisch verwenden tritt das Problem unter OSX mit _ nicht auf.
Wrapper erstellen
Für viele Programmbibliotheken gibt es Wrapper für viele verschiedene Sprachen, meißt auch Pascal (oder Delphi). Ist dies aber nicht der Fall, so lässt sich zwar aus einer sauberen Dokumentation der Bibliothek ein solcher Konstruieren, allerdings ist dies sehr mühsam und in der Realität wird man schnell merken dass die meißten Bibliotheken recht unsauber dokumentiert sind.
Abhilfe verschafft uns dabei die Wrapper in anderen Programmiersprachen. Für die meißten Bibliotheken gibt es einen Wrapper in C, als .h Datei, welcher alle Funktionen sauber implementiert. Wenn man nun nicht so gut C kann, oder es einem zu aufwändig ist den Wrapper zu übersetzen gibt es ein kleines Programm als Teil der FPC Installation, h2pas, welches C Headerfiles weitestgehend in Pascal Units übersetzt. Die Dokumentation zu diesem Programm findet ihr .
Dateien Lesen & Schreiben
Das letzte Kapitel zum Klassischen Pascal geht um das Thema Dateien. Wie Bekannt sein sollte Speichert der Computer Informationen dauerhaft in Form von Dateien in einem Dateisystem. Dabei hat jede Datei einen Namen, und einen Pfad zu dem Ordner der die Datei enthält. Je nach Betriebsystem unterscheidet sich die Darstellung von Dateiname und Pfad. Windows z.B. stellt jedes Speichermedium als Laufwerk da, welches mit einem Buchstaben und einem Doppelpunkt gekennzeichnet ist (C:, E:, D:), und dann mit Backslashes getrennt die Ordnerstruktur des Pfades, und am Ende der Dateiname, z.B.
Unix Systeme hingegen Haben ein so genanntes Root Directory, der Basispfad des Speichermediums auf dem das System Installiert ist, und weitere Speichermedien werden als Ordner in diese Struktur eingehängt. Die Darstellung beginnt mit einem Slash, und darauf werden alle Ordner mit Slashes getrennt und am Ende folgt der Dateiname. Z.B:
Zur Kategoriesierung der Dateien werden u.a. Dateiendungen verwendet, welche mit einem Punkt getrennt an den Dateinamen angehängt werden (um Beispiel .ext). Es gibt keine wirklichen Vorgaben an die Dateiendungen, und die können auch theoretisch beliebig verwendet werden, allerdings wird meißt eine aussagekräftige Abkürzung für den Dateiinhalt verwendet. So steht z.B. .txt für Textdateien, .exe für Windows Executeables, und .bmp für Bitmap.
Konvention: Bei dem schreiben von eigenen Dateieformaten sollte man eine vernünftige Dateiendung wählen, so behält man selbst, oder andere die sich die Programme ansehen einen Überblick darüber was für ein Inhalt sich hinter einer Datei verbirgt. Meißt werden zur nomenklatur 3-4 Buchstaben lange Abkürzungen englischer Wörter verwendet wie .bin für binäre Dateien, .lst für Listen, .cfg für Konfigurationsdateien. Man sollte auch versuchen keine Etablierten Dateiendungen zu verwenden (wie .jpg für Textdokumente).
Dateitypen und Aufbau
Grundsätzlich unterscheiden wir zwischen zwei Typen von Dateien, Textdateien und Binärdateien, da diese sich im Umgang unterscheiden.
Textdateien sind Dateien mit für Menschen lesbaren Buchstaben. Textdateien werden entweder Zeilenweise, Wort für Wort, oder jeder Charakter einzeln gelesen.
Binärdateien hingegen werden Byteweise beschrieben. In Binärdateien werden die Werte in dem selben Byteformat wie die Pascal Datentypen gespeichert.
Da Binäredatentypen im gegensatz zu Textdateien nicht über einen einfachen Syntaxcheck verifiziert werden können, und die Bytes in verschiedenen Datentypen komplett andere Werte hat, verwendet man einen Header, um Informationen über den Inhalt zu stellen. Als Teil dieses Headers verwendet man oftmals eine so genannte Magic Number, eine Zahl, die den Datentypen Beschreibt. Wenn die Magic Number nicht der erwartete Wert ist weiß man dass es nicht der richtige Datentyp ist. Im Umkehrschluss kann man, bei mehreren Dateien mit gleicher Dateiendung über die Magic Number feststellen was der Inhalt dieser Dateien ist.
Pascal kennt außerdem noch Typisierte Binärdateien, das sind Binärdateien die nur einen Datentypen enthalten können, z.B. nur Integer Werte.
Dateien in Pascal
Um Dateien zu verwenden nutzt man in Pascal so genannte File Handles, das sind Betriebsystem interne Kennnummern für die geöffneten Dateien. Der Variablentyp für ein solches Dateihandle ist:
TextFile für Textdateien
File of Typ für typisierte Binärdateien
File für untypisierte Binärdateien.
Egal welcher Art, die Dateihandles bekommt man via AssignFile, muss sie mit CloseFile wieder Freigeben.
Genau wie bei Zeigern empfiehlt sich hier die Nutzung eines Try-Finally Blocks.
Mit dem AssignFile wird das Handle zugewiesen, danach muss die Datei allerdings noch geöffnet werden. Das öffnen geschieht über Reset, Rewrite oder Append.
Append ist zum Schreiben am Ende von Textdateien, also zum Anhängen von Texten. Rewrite erstellt eine neue Datei, exsistiert die Datei bereits wird sie überschrieben. Reset öffnet Textdateien zum lesen, oder Binärdateien zum Lesen und/oder Schreiben.
Nun gehe ich näher auf die einzelnen Typen ein.
Textdateien
Textdateien ließt und schreibt man genau wie mit der Konsole. Man verwendet Read und ReadLn bzw Write und WriteLn:
Am besten erklärt das ein Beispiel Code:
Dieses Programm sichert einen String, einen Integer und einen Double Zeilenweise in einer Textdatei und ließt diese anschließend wieder aus. Der Dateiinhalt ist mit 123 als Integer, String als String und 3.5 als Double dann:
Viel zu beachten gibt es an dieser Stelle eher nicht, da wir diese Form des Lesen und Schreiben schon von der Konsole kennen.
Typisierte Binärdateien
Bei Typisierten Binärdateien gibt man bei der Definition der Handle Variable einen Datentypen an, je nach dem was der Inhalt der Datei sein soll.
Binäre Dateien kann man mittels Reset in verschiedenen Modi öffnen. Dafür stellt Pascal die Globale Variable FileMode bereit. Diese kann man vor dem verwenden von Reset auf 3 verschiedene Werte setzen:
Um die Datei entsprechend zum Lesen, Schreiben oder beidem zu öffnen.
Wenn man eine Datei zum Schreiben öffnet, ohne die entsprechende Berechtigung im System zu haben wirft dies einen Fehler.
Hinweis: Wenn man die Variable FileMode nicht setzt so wird die Datei mit dem Default Modus fmOpenReadWrite geöffnet.
Bei typisierten binären Dateien werden Informationen via Read und Write geschrieben.
In dem folgenden Beispiel wird ein Record in eine Binärdatei geschrieben:
Bemerkung: In diesem Beispiel verwende ich ShortString, da ShortStrings als statisch zusammengesetze Datenstruktur sich einfach Byteweise sichern lassen. Bei Dynamischen Strings würde lediglich der Zeiger auf den String gesichert werden.
Als Typen für diese Dateien kann man letztlich alles nehmen, außer Zeiger, Dynamische Arrays oder Dynamische Strings, da man damit nur den Zeiger Speichern würde, welcher beim nächsten Durchlauf natürlich nicht unbedingt der Selbe sein muss. Statische Arrays, ShortStrings, Records oder andere Ordinärypen lassen sich aber ohne Probleme speichern.
Untypisierte Binärdateien
Schlussendlich gibt es noch untypisierte Binärdateien. In diese kann man beliebige binäre Daten sichern.
Dafür übergibt man der Funktion Reset bzw. Rewrite neben dem FileHandle noch einen weiteren Parameter, RecordSize. Diese gibt an in wie großen Blöcken geschrieben bzw gelesen werden soll. Schreibt man z.B. nur Integer (4 Byte) und Double (8 Byte) in eine Datei kann man eine RecordSize von 4 Byte wählen. Will man z.B. Bytes, oder genaue Größen wie von Packed Records sichern sollte man eine RecordSize von 1 Byte wählen.
Gelesen und Geschrieben wird dann mit den Funktionen:
Wobei WriteCount bzw. ReadCount Optionale Var Parameter sind. Buffer is auch ein Var Parameter, über welchen die Variable übergeben wird deren Inhalt geschrieben werden soll, oder in die gelesen werden soll. Count gibt an wie viele Einheiten der über Reset bzw. Rewrite RecordSize größe gelesen werden soll. Wenn eine Optionale Variable ReadCount bzw. WriteCount übergeben wird, wird dort gespeichert wie viele Einheiten tatsächlich gelesen wurden (im Fall dass man versucht mehr Bytes zu lesen als die Datei enthält).
Bemerkung: Will man größere Datenmengen in einen Array oder String laden, so empfiehlt sich die Elemente in größeren Blöcken (z.B. 1024 Bytes) zu lesen, und über ReadCount kann man dann überprüfen ob man am Ende der Datei angekommen ist.
Ein kleines Beispiel:
Dabei wird ein Integer, ein Double und ein ShortString in die Binärdatei geschrieben.
Generell lässt sich über die Verschiedenen Dateien sagen, Binärdateien sind deutlich schneller zu lesen, dafür aber nicht mit normalen Editoren veränderbar. Textdateien sind für jeden Nutzer gut Lesbar und Editierbar. Typisierte Binärdateien sind sehr einfach zu verwenden, und solange man nur einzelne Datentypen sichert eine sehr schnelle einfache Möglichkeit, die vor allem kürzer ist als die untypisierte Lösung.
Weitere Dateioperationen
Zum arbeiten mit Dateien gibt es noch mehr Funktionen:
function Eof(FileHandle): Boolean;
Diese Funktion gibt True zurück wenn das FileHandle auf das Ende der Datei zeigt.
procedure Seek(FileHandle; Pos);
Diese Funktion setzt die Position des FileHandle auf Pos. Pos gibt die Position als Einheiten in RecordSize an oder in Größe des Typs bei typisierten Dateien. Seek funktioniert nicht auf Textdateien, sondern nur auf Binären Dateien.
function FilePos(FileHandle): Integer;
Diese Funktion gibt die aktuelle Position des FileHandles zurück. Wie bei Seek auch wird die Position als Einheiten in Record bzw. Typgrößen angegeben.
Arrays, Strings und Zeiger schreiben
Zuletzt möchte ich noch kurz darauf eingehen wie man Arrays und Strings in Binärdateien schreibt. Bei statischen Arrays und ShortStrings ist das absolut kein Problem, und die können genau so wie andere Variablen auch geschrieben werden. Dynamische Strings und Arrays allerdings sind letztlich Zeiger, und würde man diese einfach so schreiben würde das nur den Zeiger in die Datei schreiben, welcher beim nächsten lesen wahrscheinlich nicht mehr gültig ist, oder vielleicht auf ein Komplett anderes Element zeigt. Dafür sehen wir uns zunächst einmal an wie wir Zeiger schreiben würden:
Durch den Dereferenzierungsoperator ^ geben wir an dass wir den Inhalt auf den der Zeiger zeigt schreiben wollen, bzw. das Gelesene das Element hinter dem Zeiger schreiben wollen.
Bei Arrays oder Strings können wir es genauso machen, wir können diese einfach in einen Pointer Casten und dann über ^ Schreiben lassen. Da die Länge allerdings Dynamisch ist müssen wir noch dazu speichern wie viele Elemente wir schreiben werden. Außerdem müssen wir bei dem Count Parameter noch die Anzahl an Elementen und ihre Größe berücksichtigen:
vor dem Lesen müssen wir dann die Länge auslesen und den Array/String mit SetLength auf diese Länge setzen:
Eine andere Möglichkeit ist es statt Zeiger zu nehmen Arrayelemente zu Adressieren:
Damit können auch sehr gut nur Teile eines Arrays oder Strings gelesen bzw. geschrieben werden. Allerdings hat das den Nachteil, wenn man mit einem Debugger arbeitet (z.B. dem GDB) welcher mit Array RangeChecks arbeitet kann es vorkommen, dass dieser Dabei Fehler wirft, weil man bei einem Leeren Array kein Element Adressieren sollte (auch wenn in diesem Fall eh kein Zugriff geschehen würde da Len = 0 wäre). Also ein Fehlarlam, welcher dennoch sehr nervig sein kann.
Hinweis: Dieses Beispiel bezieht sich auf AnsiStrings, seit FPC 3.0 werden UTF-8 Strings verwendet, aber mit zu den Unterschieden werde ich ein Späteres Kapitel noch Füllen.
Damit wäre auch das Kapitel Dateien abgeschlossen.
Das letzte Kapitel zum Klassischen Pascal geht um das Thema Dateien. Wie Bekannt sein sollte Speichert der Computer Informationen dauerhaft in Form von Dateien in einem Dateisystem. Dabei hat jede Datei einen Namen, und einen Pfad zu dem Ordner der die Datei enthält. Je nach Betriebsystem unterscheidet sich die Darstellung von Dateiname und Pfad. Windows z.B. stellt jedes Speichermedium als Laufwerk da, welches mit einem Buchstaben und einem Doppelpunkt gekennzeichnet ist (C:, E:, D:), und dann mit Backslashes getrennt die Ordnerstruktur des Pfades, und am Ende der Dateiname, z.B.
Code:
C:\Pfad\Zur\Datei.ext
Code:
/Pfad/Zur/Datei.ext
Konvention: Bei dem schreiben von eigenen Dateieformaten sollte man eine vernünftige Dateiendung wählen, so behält man selbst, oder andere die sich die Programme ansehen einen Überblick darüber was für ein Inhalt sich hinter einer Datei verbirgt. Meißt werden zur nomenklatur 3-4 Buchstaben lange Abkürzungen englischer Wörter verwendet wie .bin für binäre Dateien, .lst für Listen, .cfg für Konfigurationsdateien. Man sollte auch versuchen keine Etablierten Dateiendungen zu verwenden (wie .jpg für Textdokumente).
Dateitypen und Aufbau
Grundsätzlich unterscheiden wir zwischen zwei Typen von Dateien, Textdateien und Binärdateien, da diese sich im Umgang unterscheiden.
Textdateien sind Dateien mit für Menschen lesbaren Buchstaben. Textdateien werden entweder Zeilenweise, Wort für Wort, oder jeder Charakter einzeln gelesen.
Binärdateien hingegen werden Byteweise beschrieben. In Binärdateien werden die Werte in dem selben Byteformat wie die Pascal Datentypen gespeichert.
Da Binäredatentypen im gegensatz zu Textdateien nicht über einen einfachen Syntaxcheck verifiziert werden können, und die Bytes in verschiedenen Datentypen komplett andere Werte hat, verwendet man einen Header, um Informationen über den Inhalt zu stellen. Als Teil dieses Headers verwendet man oftmals eine so genannte Magic Number, eine Zahl, die den Datentypen Beschreibt. Wenn die Magic Number nicht der erwartete Wert ist weiß man dass es nicht der richtige Datentyp ist. Im Umkehrschluss kann man, bei mehreren Dateien mit gleicher Dateiendung über die Magic Number feststellen was der Inhalt dieser Dateien ist.
Pascal kennt außerdem noch Typisierte Binärdateien, das sind Binärdateien die nur einen Datentypen enthalten können, z.B. nur Integer Werte.
Dateien in Pascal
Um Dateien zu verwenden nutzt man in Pascal so genannte File Handles, das sind Betriebsystem interne Kennnummern für die geöffneten Dateien. Der Variablentyp für ein solches Dateihandle ist:
TextFile für Textdateien
File of Typ für typisierte Binärdateien
File für untypisierte Binärdateien.
Egal welcher Art, die Dateihandles bekommt man via AssignFile, muss sie mit CloseFile wieder Freigeben.
Genau wie bei Zeigern empfiehlt sich hier die Nutzung eines Try-Finally Blocks.
Code:
var F: TextFile; begin AssignFile(F, 'Pfad/Zur/Datei.ext'); try finally CloseFile(F); end; end;
Append ist zum Schreiben am Ende von Textdateien, also zum Anhängen von Texten. Rewrite erstellt eine neue Datei, exsistiert die Datei bereits wird sie überschrieben. Reset öffnet Textdateien zum lesen, oder Binärdateien zum Lesen und/oder Schreiben.
Nun gehe ich näher auf die einzelnen Typen ein.
Textdateien
Textdateien ließt und schreibt man genau wie mit der Konsole. Man verwendet Read und ReadLn bzw Write und WriteLn:
Code:
Read(FileHandle, Variable); // Lese Inhalt von FileHandle in Variable ReadLn(FileHandle, Variable); // Lese eine Zeile in Variable Write(FileHandle, Wert); // Schreibt Wert in die Textdatei WriteLn(FileHandle, Wert); // Schreibt Wert+Zeilenumbruch in Textdatei
Code:
program ReadText; {$Mode ObjFPC}{$H+} var F: TextFile; i: Integer; s: String; d: Double; begin // Variablen über Konsole einlesen ReadLn(i); ReadLn(s); ReadLn(d); AssignFile(F, 'datei.txt'); try Rewrite(F); // Zeilenweise Informationen Schreiben WriteLn(F, i); WriteLn(F, s); WriteLn(F, d); finally CloseFile(F); end; // Daten Lesen AssignFile(F, 'datei.txt'); try Reset(F); // Zeilenweise auslesen ReadLn(F, i); ReadLn(F, s); ReadLn(F, d); finally CloseFile(F); end; // Daten Ausgeben WriteLn(i); WriteLn(s); WriteLn(d); end.
Code:
123 String 3.5000000000000000E+000
Typisierte Binärdateien
Bei Typisierten Binärdateien gibt man bei der Definition der Handle Variable einen Datentypen an, je nach dem was der Inhalt der Datei sein soll.
Binäre Dateien kann man mittels Reset in verschiedenen Modi öffnen. Dafür stellt Pascal die Globale Variable FileMode bereit. Diese kann man vor dem verwenden von Reset auf 3 verschiedene Werte setzen:
Code:
fmOpenRead = 0 fmOpenWrite = 1 fmOpenReadWrite = 2
Wenn man eine Datei zum Schreiben öffnet, ohne die entsprechende Berechtigung im System zu haben wirft dies einen Fehler.
Hinweis: Wenn man die Variable FileMode nicht setzt so wird die Datei mit dem Default Modus fmOpenReadWrite geöffnet.
Bei typisierten binären Dateien werden Informationen via Read und Write geschrieben.
In dem folgenden Beispiel wird ein Record in eine Binärdatei geschrieben:
Code:
program BinFileTest; {$MODE ObjFPC}{$H+} uses SysUtils; type // Record der gesichert werden soll TMyRec = record MyInt1, MyInt2: Integer; MyStr: ShortString; MyDouble: Double; end; var // FileHandle F: File of TMyRec; // Record Variable Rec: TMyRec; begin // Record befüllen Rec.MyInt1 := 5; Rec.MyInt2 := 10; Rec.MyStr := 'Hallo Welt'; // ShortString statische zusammengesetze Struktur Rec.MyDouble := 3.5; // Datei öffnen AssignFile(F, 'Test.bin'); try Rewrite(F); // Record schreiben Write(F, Rec); finally CloseFile(F); end; // Record Löschen FillChar(Rec, SizeOf(Rec), #00); // Datei wieder öffnen AssignFile(F, 'Test.bin'); try // FileMode setzen FileMode := fmOpenRead; Reset(F); // Daten Lesen Read(F, Rec); finally CloseFile(F); end; // Daten Ausgeben WriteLn(Rec.MyInt1); WriteLn(Rec.MyInt2); WriteLn(Rec.MyStr); WriteLn(Rec.MyDouble); end.
Als Typen für diese Dateien kann man letztlich alles nehmen, außer Zeiger, Dynamische Arrays oder Dynamische Strings, da man damit nur den Zeiger Speichern würde, welcher beim nächsten Durchlauf natürlich nicht unbedingt der Selbe sein muss. Statische Arrays, ShortStrings, Records oder andere Ordinärypen lassen sich aber ohne Probleme speichern.
Untypisierte Binärdateien
Schlussendlich gibt es noch untypisierte Binärdateien. In diese kann man beliebige binäre Daten sichern.
Dafür übergibt man der Funktion Reset bzw. Rewrite neben dem FileHandle noch einen weiteren Parameter, RecordSize. Diese gibt an in wie großen Blöcken geschrieben bzw gelesen werden soll. Schreibt man z.B. nur Integer (4 Byte) und Double (8 Byte) in eine Datei kann man eine RecordSize von 4 Byte wählen. Will man z.B. Bytes, oder genaue Größen wie von Packed Records sichern sollte man eine RecordSize von 1 Byte wählen.
Gelesen und Geschrieben wird dann mit den Funktionen:
Code:
BlockRead(FileHandle, Buffer, Count, ReadCount); BlockWrite(FileHandle, Buffer, Count, WriteCount);
Bemerkung: Will man größere Datenmengen in einen Array oder String laden, so empfiehlt sich die Elemente in größeren Blöcken (z.B. 1024 Bytes) zu lesen, und über ReadCount kann man dann überprüfen ob man am Ende der Datei angekommen ist.
Ein kleines Beispiel:
Code:
program BinFileTest; {$MODE ObjFPC}{$H+} uses SysUtils; var F: File; i: Integer; s: ShortString; d: Double; begin i := 4; s := 'Hallo Welt'; d := 3.5; AssignFile(F, 'Test.bin'); try Rewrite(F, 1); // 1 Byte RecordSize BlockWrite(F, i, SizeOf(Integer)); // Integer Schreiben BlockWrite(F, s, SizeOf(s)); // ShortString schreiben BlockWrite(F, d, SizeOf(d)); // Double schreiben finally CloseFile(F); end; AssignFile(F, 'Test.bin'); try FileMode := fmOpenRead; // Lesen Reset(F, 1); // 1 Byte RecordSize BlockRead(F, i, SizeOf(Integer)); // Integer Schreiben BlockRead(F, s, SizeOf(s)); // ShortString schreiben BlockRead(F, d, SizeOf(d)); // Double schreiben finally CloseFile(F); end; WriteLn(i); WriteLn(s); WriteLn(d); end.
Generell lässt sich über die Verschiedenen Dateien sagen, Binärdateien sind deutlich schneller zu lesen, dafür aber nicht mit normalen Editoren veränderbar. Textdateien sind für jeden Nutzer gut Lesbar und Editierbar. Typisierte Binärdateien sind sehr einfach zu verwenden, und solange man nur einzelne Datentypen sichert eine sehr schnelle einfache Möglichkeit, die vor allem kürzer ist als die untypisierte Lösung.
Weitere Dateioperationen
Zum arbeiten mit Dateien gibt es noch mehr Funktionen:
function Eof(FileHandle): Boolean;
Diese Funktion gibt True zurück wenn das FileHandle auf das Ende der Datei zeigt.
Code:
While not Eof(F) do begin // Zeilenweise Auslesen und ausgeben ReadLn(F, s); WriteLn(s); end;
Diese Funktion setzt die Position des FileHandle auf Pos. Pos gibt die Position als Einheiten in RecordSize an oder in Größe des Typs bei typisierten Dateien. Seek funktioniert nicht auf Textdateien, sondern nur auf Binären Dateien.
Code:
Seek(F, 0); Read(F, i); // Erstes Element auslesen Seek(F, 2); Read(F, i); // Drittes Element auslesen
Diese Funktion gibt die aktuelle Position des FileHandles zurück. Wie bei Seek auch wird die Position als Einheiten in Record bzw. Typgrößen angegeben.
Code:
Seek(F, FilePos(F) - 1); Read(F, i); // Vorheriges Element auslesen
Arrays, Strings und Zeiger schreiben
Zuletzt möchte ich noch kurz darauf eingehen wie man Arrays und Strings in Binärdateien schreibt. Bei statischen Arrays und ShortStrings ist das absolut kein Problem, und die können genau so wie andere Variablen auch geschrieben werden. Dynamische Strings und Arrays allerdings sind letztlich Zeiger, und würde man diese einfach so schreiben würde das nur den Zeiger in die Datei schreiben, welcher beim nächsten lesen wahrscheinlich nicht mehr gültig ist, oder vielleicht auf ein Komplett anderes Element zeigt. Dafür sehen wir uns zunächst einmal an wie wir Zeiger schreiben würden:
Code:
var PInt1: PInteger; F: File; begin // Initialisierung von F und PInt1 BlockWrite(F, PInt1^, SizeOf(Integer)); //... BlockRead(F, PInt1^, SizeOf(Integer)); end;
Bei Arrays oder Strings können wir es genauso machen, wir können diese einfach in einen Pointer Casten und dann über ^ Schreiben lassen. Da die Länge allerdings Dynamisch ist müssen wir noch dazu speichern wie viele Elemente wir schreiben werden. Außerdem müssen wir bei dem Count Parameter noch die Anzahl an Elementen und ihre Größe berücksichtigen:
Code:
var arr: Array of Integer; Str: AnsiString; Len: Integer; F: File; begin // Initialisierung // Array schreiben Len := Length(arr); BlockWrite(F, Len, SizeOf(Len)); BlockWrite(F, PInteger(arr)^, Len * SizeOf(Integer)); // String Schreiben Len := Length(Str); BlockWrite(F, Len, SizeOf(Len)); BlockWrite(F, PChar(Str)^, Len * SizeOf(Char)); end;
Code:
var Arr: Array of Integer; Str: AnsiString; Len: Integer; begin // Initialisierung // Array Lesen BlockRead(F, Len, SizeOf(Len)); SetLength(arr, Len); BlockRead(F, PInteger(arr)^, Len * SizeOf(Integer)); // String Lesen BlockRead(F, Len, SizeOf(Len)); SetLength(Str, Len); BlockRead(F, PChar(Str)^, Len * SizeOf(Char)); end;
Code:
var arr: Array of Integer; Str: AnsiString; Len: Integer; F: File; begin // Initialisierung // Array schreiben Len := Length(arr); BlockWrite(F, Len, SizeOf(Len)); BlockWrite(F, arr[0], Len * SizeOf(Integer)); // String Schreiben Len := Length(Str); BlockWrite(F, Len, SizeOf(Len)); BlockWrite(F, Str[1], Len * SizeOf(Char)); // Array Lesen BlockRead(F, Len, SizeOf(Len)); SetLength(arr, Len); BlockRead(F, arr[0], Len * SizeOf(Integer)); // String Lesen BlockRead(F, Len, SizeOf(Len)); SetLength(Str, Len); BlockRead(F, Str[1], Len * SizeOf(Char));
Hinweis: Dieses Beispiel bezieht sich auf AnsiStrings, seit FPC 3.0 werden UTF-8 Strings verwendet, aber mit zu den Unterschieden werde ich ein Späteres Kapitel noch Füllen.
Damit wäre auch das Kapitel Dateien abgeschlossen.
Nachwort
Solltet ihr fragen zu diesem Tutorial oder zu FreePascal oder Lazarus im allgemeinen haben, könnt ihr mich auch gerne hier im Thread Fragen.
Wie bereits erwähnt werde ich immer neue Kapitel diesem Thread hinzufügen, allerdings dauert das schreiben eines Kapitels immer seine Zeit, und deshalb verzeiht mir bitte wenn es ein wenig dauern kann bis ich mit den geplanten 12 Kapiteln durch bin.
Außer den bereits angekündigten 12 Kapiteln kann es sein dass ich noch welche Hinzufüge, zwischenschiebe oder die Reihenfolge der Kapitel im Nachhinein nochmal ändern werde.