Modellierung/Programmierung Vorlesungsskript Dipl.-Math. Erik Wallacher FR 6.1 - Mathematik Universit¨at des Saarlandes Wintersemester 2005/2006 Inhaltsverzeichnis 1 Das erste Programm 1.1 1.2 1.3 1.4 1.5 1.6 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Variablen, Datentypen und Operationen 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 3 1 Allgemeine Grundlagen und Organisation Programmieren in C . . . . . . . . . . . . Das Betriebssystem LINUX . . . . . . . Arbeiten unter Windows . . . . . . . . . Ein erstes C-Programm . . . . . . . . . . Interne Details beim Compilieren . . . . . Deklaration, Initialisierung, Definition . . . . . . . . . Elementare Datentypen . . . . . . . . . . . . . . . . . Felder und Strings . . . . . . . . . . . . . . . . . . . . 2.3.1 Felder . . . . . . . . . . . . . . . . . . . . . . 2.3.2 Mehrdimensionale Felder . . . . . . . . . . . 2.3.3 Zeichenketten (Strings) . . . . . . . . . . . . . Ausdr¨ucke, Operatoren und mathematische Funktionen 2.4.1 Arithmetische Operatoren . . . . . . . . . . . 2.4.2 Vergleichsoperatoren . . . . . . . . . . . . . . 2.4.3 Logische Operatoren . . . . . . . . . . . . . . 2.4.4 Bitorientierte Operatoren . . . . . . . . . . . . 2.4.5 Inkrement- und Dekrementoperatoren . . . . . 2.4.6 Adressoperator . . . . . . . . . . . . . . . . . 2.4.7 Priorit¨aten von Operatoren . . . . . . . . . . . Operationen mit vordefinierten Funktionen . . . . . . . 2.5.1 Mathematische Funktionen . . . . . . . . . . . 2.5.2 Funktionen f¨ur Zeichenketten (Strings) . . . . Zusammengesetzte Anweisungen . . . . . . . . . . . . N¨utzliche Konstanten . . . . . . . . . . . . . . . . . . Typkonversion (cast) . . . . . . . . . . . . . . . . . . Standardein- und -ausgabe . . . . . . . . . . . . . . . 2.9.1 Ausgabe . . . . . . . . . . . . . . . . . . . . . 2.9.2 Eingabe . . . . . . . . . . . . . . . . . . . . . 1 1 2 5 5 6 8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 9 11 11 11 12 13 14 15 16 16 18 19 19 21 21 22 23 24 25 26 26 27 Programmflusskontrolle 30 3.1 30 30 Bedingte Ausf¨uhrung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.1 Die if()-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . I 3.2 3.3 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2 6.3 6.4 . . . . . . . . . . . . . . . 39 40 41 42 43 47 Deklaration, Definition und R¨uckgabewerte Lokale und globale Variablen . . . . . . . . Call by value . . . . . . . . . . . . . . . . Call by reference . . . . . . . . . . . . . . Rekursive Programmierung . . . . . . . . . Kommandozeilen-Parameter . . . . . . . . Wie werden Deklarationen gelesen . . . . . Zeiger auf Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 49 51 53 54 55 57 57 60 Strukturen . . . . . . . . . . . . . . . . . 6.1.1 Deklaration von Strukturen . . . . 6.1.2 Definition von Strukturvariablen . 6.1.3 Felder von Strukturen . . . . . . 6.1.4 Zugriff auf Strukturen . . . . . . 6.1.5 Zugriff auf Strukturen mit Zeigern 6.1.6 Geschachtelte Strukturen . . . . . 6.1.7 Listen . . . . . . . . . . . . . . Unions . . . . . . . . . . . . . . . . . . . Aufz¨ahlungstyp . . . . . . . . . . . . . . Allgemeine Typendefinition . . . . . . . . . . . . . . . . . . Arbeiten mit Dateien 7.1 7.2 7.3 7.4 7.5 7.6 31 33 33 36 37 38 39 Adressen . . . . . . . . . . . . . . . . . . . . Pointervariablen . . . . . . . . . . . . . . . . . Adressoperator und Zugriffsoperator . . . . . . Zusammenhang zwischen Zeigern und Feldern Dynamische Felder mittels Zeiger . . . . . . . Strukturierte Datentypen 6.1 7 . . . . . . Funktionen 5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 6 . . . . . . Zeiger (Pointer) 4.1 4.2 4.3 4.4 4.5 5 3.1.2 Die switch()-Anweisung . . . . . . . . . . . Schleifen . . . . . . . . . . . . . . . . . . . . . . . 3.2.1 Der Z¨ahlzyklus (for-Schleife) . . . . . . . . 3.2.2 Abweisender Zyklus (while-Schleife) . . . . 3.2.3 Nichtabweisender Zyklus (do-while-Schleife) Anweisungen zur unbedingten Steuerungs¨ubergabe . 60 60 61 61 61 62 63 64 66 67 68 69 Dateien o¨ ffnen und schließen . . . . Existenz einer Datei pr¨ufen . . . . . Zeichenorientierte Ein- und Ausgabe Bin¨are Ein- und Ausgabe . . . . . . Dateien l¨oschen/umbenennen . . . . Positionierung in einem Datenstrom . . . . . . II . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 71 71 77 79 81 7.7 8 Dateiausgabe umlenken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Mehrdateiprojekte 8.1 8.2 8.3 8.4 82 84 Verteilung des Codes auf Dateien . . . . . . . . . Manuelles Kompilieren eines Mehrdateiprojektes Automatisiertes Kompilieren mit make . . . . . Eigene Bibliotheken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A 86 89 90 93 95 A.1 Zahlendarstellung im Rechner und Computerarithmetik . . . . . . . . . . . . A.2 IEEE Gleitkommazahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.3 Computerarithmetik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Anhang 95 97 99 95 A.4 Die O-Notation . . . . . . . . . . . . . A.5 Der Pr¨aprozessor . . . . . . . . . . . . A.5.1 Dateien einf¨ugen (# include) . . A.5.2 Konstanten definieren (#define) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 104 104 106 Literaturverzeichnis 107 Index 108 III 1 Das erste Programm 1.1 Allgemeine Grundlagen und Organisation Die Programmentwicklung erfolgt grunds¨atzlich in vier Schritten. 1.) Problemstellung : Welche Aufgabe soll der Computer bew¨altigen. 2.) Erarbeiten eines Algorithmus zur L¨osung der Aufgabe. 3.) Erstellen (editieren) eines Quelltextes zur Umsetzung des Algorithmus. ¨ 4.) Ubersetzen (compilieren bzw. interpretieren) des Quelltextes in einen Code, der vom Computer verstanden werden kann. Ein Computerprogramm ist also die Umsetzung eines Algorithmus in eine Form, die von einem Computer verarbeitet werden kann. Der Begriff Programm wird sowohl f¨ur den in einer Programmiersprache verfassten Quelltext, als auch f¨ur den von einem Computer ausf¨uhrbaren Maschinencode verwendet. Um aus dem Quelltext den Maschinencode zu generieren, wird ein Compiler oder Interpreter ben¨otigt. Diese u¨ bersetzen die Befehle der Programmiersprache, die f¨ur menschliche Benutzer verst¨andlich und bearbeitbar sein sollen, in die semantisch entsprechenden Befehle der Maschinensprache des verwendeten Computers. 1.2 Programmieren in C Wie aus Kapitel 1.1 schon hervorgeht wird zum Programmieren (neben einem Computer) ein ¨ Editor und ein Ubersetzer ben¨otigt. Editor Ein Editor ist ein Programm zum Erstellen und Ver¨andern von Textdateien. Speziell wird es bei der Programmierung zum Erstellen des Quellcodes verwandt. 1 ¨ Ubersetzer (Compiler, Linker) ¨ Wie bei allen Compiler-Sprachen besteht der Ubersetzer aus zwei Hauptkomponenten: dem eigentlichen Compiler und dem Linker. Der Compiler erzeugt aus dem Quelltext einen f¨ur den Rechner lesbaren Objektcode. Der Linker erstellt das ausf¨uhrbare Programm, indem es in die vom Compiler erzeugte Objektdatei Funktionen (siehe auch Kapitel 5) aus Bibliotheken (Libraries) einbindet. Der Begriff Compiler wird h¨aufig auch als Synonym f¨ur das gesamte Entwicklungssystem (Compiler, Linker, Bibliotheken) verwendet. Bemerkung: ¨ F¨ur die Bearbeitung der Ubungsaufgaben werden Editor und C-Compiler bereitgestellt. Es handelt sich dabei um frei erh¨altliche (kostenlose) Programme, die auf LINUXBetriebssystemen (siehe Kapitel 1.3) arbeiten. Grundlegende Kenntnisse u¨ ber LINUX ¨ werden in der ersten Ubung vermittelt. Die Aufgaben k¨onnen an den an der Universit¨at verf¨ugbaren Rechnern oder in Heimarbeit erledigt werden. F¨ur die Bearbeitung unter Windows siehe Kapitel 1.4. Warnung: Es handelt sich bei C um eine weitgehend standardisierte Programmiersprache. Dennoch ist C-Compiler nicht gleich C-Compiler. Die Vergangenheit hat gezeigt, dass nicht alle Programme unter verschiedenen Compilern lauff¨ahig sind. Wer mit einem anderen als den von uns bereitgestellten Compilern arbeitet, hat unter Umst¨anden mit Schwierigkeiten bei der ¨ Kompatibilit¨at zu rechnen. In den Ubungen kann hierauf keine R¨ucksicht genommen werden. 1.3 Das Betriebssystem LINUX LINUX wurde von dem finnischen Informatikstudenten Linus Torvals entwickelt, um PCs kompatibel zu den u.a. im universit¨aren Umfeld weit verbreiteten UNIX-Systemen zu betreiben. Die rasch wachsende Beliebheit f¨uhrte dazu, dass heute eine großen Programmierergemeinde zum gr¨oßten Teil unentgeltlich die Entwicklung weiterf¨uhrt. LINUX ist frei erh¨altlich und kann kostenlos weitergeben werden. Das System wird in unterschiedlichen Varianten (Distributionen) angeboten. Dateien und Verzeichnisse Daten werden auf dem Medium in Dateien zusammengefasst abgespeichert. Die Datei wird mit einem bestimmten Dateinamen bezeichnet. Folgendes ist f¨ur Dateinamen zu beachten: • LINUX unterscheidet zwischen Groß- und Kleinschreibung. 2 Wurzelverzeichnis/ ` P ``` @P PP``` PP ``` @ ``` PP ``` @ P P ` @ P bin/ etc/ home/ P ... usr/ var/ @PPP PP @ PP @ PP @ P modprog000/ X ... uebung1/ ` ... modprog999/ HXX HHXXX HH XXX XXX HH XX uebung12/ PP ``` @ P ``` PP @ PP````` ``` PP @ `` P @ P aufg1.c aufg1.out aufg2.c aufg2.out Abbildung 1.1: Der Verzeichnisbaum am Beispiel von LINUX • Der Schr¨agstrich / darf nicht verwendet werden, da er zur Trennung von Verzeichnisnamen verwendet wird. • Sonderzeichen sollte man nicht verwenden, da einige eine spezielle Bedeutung haben. Mehrere Dateien k¨onnen in einem Verzeichnis gesammelt werden. LINUX besitzt ein hierarchisches, baumstrukturiertes Dateisystem: Ausgehend von dem Wurzelverzeichnis / besitzt der Verzeichnisbaum die Struktur in Abbildung 1.1. Die Position eines beliebigen (Unter-)Verzeichnisses innerhalb des Dateisystems ist durch den Pfad gegeben. Der Pfad gibt an, wie man vom Wurzelverzeichnis zu der gew¨unschten Datei bzw. dem gew¨unschten Verzeichnis gelangt. Die Unterverzeichnisnamen im Pfad werden durch den Schr¨agstrich getrennt. Pfade, die mit / beginnen, heißen absolute Pfade. Daneben gibt es die so genannten relativen Pfade, die relativ zur aktuellen Position im Verzeichnisbaum aufzufassen sind. Die folgenden relativen Pfadbezeichner haben eine besondere Bedeutung: . .. aktuelle Position im Verzeichnisbaum (Arbeitsverzeichnis) das u¨ bergeordnee Verzeichnis Der absolute Pfad der Datei aufg1.c lautet beispielsweise /home/modprog000/uebung1/aufg1.c. 3 Es ist m¨oglich, mehrere Dateien oder Verzeichnisse gleichzeitig anzusprechen. Dazu dienen die so genannten Wildcards: * ? [zeichen1 - zeichen2] wird durch beliebig viele Zeichen (auch keines) ersetzt wird durch ein Einzelzeichen ersetzt Auswahlsequenz; wird ersetzt durch alle Zeichen zwischen zeichen1 und zeichen2 Die Shell Ein elementares Programm ist die Shell. Sie ist ein Kommandointerpreter, der von der Kommandozeile Anweisungen einliest, auf ihre Korrektheit u¨ berpr¨uft und ausf¨uhrt. Hier ist eine ¨ kleine Ubersicht u¨ ber die allerwichtigsten Befehle (Kommandos). cd Verzeichnis pwd cp Datei Pfad cp -R Verzeichnis1 Verzeichnis2 mv Pfad1 Pfad2 ls ls Verzeichnis ls -l Pfad rm Datei rm -i Datei rm -f Datei rm -Rf Verzeichnis mkdir Verzeichnis rmdir Verzeichnis cat Datei more Datei wechsele in das angegebene Verzeichnis zeige die aktuelle Position im Verzeichnisbaum an kopiere Datei an die angegebene Position kopiere rekursiv, d.h. mit allen Unterverzeichnissen verschiebe Pfad1 nach Pfad2 zeige Inhalt des aktuellen Verzeichnisses an zeige Inhalt von Verzeichnis an ausf¨uhrliche Anzeige von Informationen l¨osche Datei l¨osche Datei mit Best¨atigung l¨osche Datei ohne Best¨atigung l¨osche Verzeichnis rekursiv ohne Best¨atigung erzeuge neues Verzeichnis l¨osche (leeres) Verzeichnis zeige Inhalt von Datei zeige Inhalt von Datei seitenweise Hilfe zu Kommandos Die man-Anweisung zeigt eine Dokumentation (Manpage) zu einem Befehl an. Man verwendet sie einfach durch man Befehlsname Sucht man Befehle, die mit einem bestimmten Schl¨usselbegriff zusammenh¨angen, so leistet der Aufruf man -k Schl¨usselbegriff das Gew¨unschte. 4 1.4 Arbeiten unter Windows Wie schon in Kapitel 1.2 angemerkt, kann es bei Verwendung von unterschiedlichen C-Compilern zu Schwierigkeiten kommen. In dieser Vorlesung beziehen wir uns stets auf den unter LINUX verwendeten GNU-C-Compiler. F¨ur die Nutzung dieses Programms unter Windows muss eine LINUX a¨ hnliche Plattform unter Windows simuliert werden. Diese Aufgabe u¨ bernimmt das sogenannte Programmpaket Cygwin. Wir stellen hierf¨ur eine Basisausstattung von Cygwin bereit. Auf Anfrage erh¨alt jeder Teilnehmer leihweise eine CD mit Installationsanleitung. 1.5 Ein erstes C-Programm Aufgabe: Der Computer soll nur eine Meldung auf dem Bildschirm schreiben. Quelltext: (HalloWelt.c): /* HalloWelt.c */ #include <stdio.h> int main() { printf("Hallo Welt \n"); return 0; } /* "\n new line */ Folgende Strukturen finden wir in diesem ersten einfachen Programm vor: • Kommentare werden mit /* eingeleitet und mit */ beendet. Sie k¨onnen sich u¨ ber mehrere Zeilen erstrecken und werden vom Compiler (genauer vom Pr¨aprozessor ) entfernt. • Pr¨aprozessordirektiven werden mit # eingeleitet. Sie werden vom Pr¨aprozessor ausgewertet. Die Direktive #include bedeutet, dass die nachfolgende Headerdatei einzuf¨ugen ist. Headerdateien haben die Dateinamenendung (Suffix) .h. Die hier einzuf¨ugende Datei stdio.h enth¨alt die ben¨otigten Informationen zur standardm¨aßigen Ein- und Ausgabe von Daten (standard input/output). • Das Schl¨usselwort main markiert den Beginn des Hauptprogramms, d.h. den Punkt, an dem die Ausf¨uhrung der Anweisungen beginnt. Auf die Bedeutung des Schl¨usselwortes void und der Klammer () wird sp¨ater detaillierter eingegangen. • Syntaktisch (und inhaltlich) zusammengeh¨orende Anweisungen werden in Bl¨ocken zusammengefasst. Dies geschieht durch die Einschließung eines Blocks in geschweifte Klammern: 5 { ... Erste Anweisung ... Letzte Anweisung } • Die erste Anweisung, die wir hier kennenlernen, ist printf(). Sie ist eine in stdio.h deklarierte Funktion, die Zeichenketten (Strings) auf dem Standardausgabeger¨at (Bildschirm) ausgibt. Die auszugebende Zeichenkette wird in Anf¨uhrungsstriche gesetzt. Zus¨atzlich wird eine Escapesequenz angef¨ugt: \n bedeutet, dass nach der Ausgabe des Textes Hallo Welt eine neue Zeile begonnen wird. Anweisungen innerhalb eines Blocks werden mit Semikolon ; abgeschlossen. Der u¨ bliche Suffix f¨ur C-Quelldateien ist .c und wir nehmen an, dass der obige Code in der Datei hallo.c abgespeichert ist. ¨ Die einfachste Form des Ubersetzungsvorgangs ist die Verwendung des folgenden Befehls in der Kommandozeile: gcc hallo.c gcc ist der Programmname des GNU-C-Compilers. Der Aufruf des Befehls erzeugt die ausf¨uhrbare Datei a.out (a.exe unter Windows). Nach Eingabe von ./a.out (bzw. ./a.exe) wird das Programm gestartet. Auf dem Bildschirm erscheint die Ausgabe ”Hallo Welt”. (Das Voranstellen von ./ kann weggelassen werden, falls sich das Arbeitsverzeichnis ./ im Suchpfad befindet. Durch Eingabe von export PATH=$PATH:. wird das Arbeitsverzeichnis in den Suchpfad aufgenommen). 1.6 Interne Details beim Compilieren Der leicht ge¨anderte Aufruf zum Compilieren gcc -v hallo.c erzeugt eine l¨angere Bilschirmausgabe, welche mehrere Phasen des Compilierens anzeigt. Im folgenden einige Tipps, wie man sich diese einzelnen Phasen anschauen kann, um den Ablauf besser zu verstehen: 6 a) Pr¨aprozessing: Headerfiles (*.h) werden zur Quelldatei hinzugef¨ugt (+ Makrodefinitionen, bedingte Compilierung) gcc -E hallo.c > hallo.E Der Zusatz > hallo.E lenkt die Bildschirmausgabe in die Datei hallo.E. Diese Datei hallo.E kann mit einem Editor angesehen werden und ist eine lange C-Quelltextdatei. ¨ b) Ubersetzen in Assemblercode: Hier wird eine Quelltextdatei in der (prozessorspezifischen) Programmiersprache Assembler erzeugt. gcc -S hallo.c Die entstandene Datei hallo.s kann mit dem Editor angesehen werden. c) Objektcode erzeugen: Nunmehr wird eine Datei erzeugt, welche die direkten Steuerbefehle, d.h. Zahlen, f¨ur den Prozessor beinhaltet. gcc -c hallo.c Die Ansicht dieser Datei mit einem normalen Texteditor liefert eine unverst¨andliche Zeichenfolge. Einblicke in die Struktur vom Objektcodedateien k¨onnen mit Hilfe eines Monitors (auch ein Editor Programm) erfolgen. hexedit hallo.o (Nur falls das Programm hexedit oder ein anderer Monitor installiert ist.) d) Linken: Verbinden aller Objektdateien und notwendigen Bibliotheken zum ausf¨uhrbaren Programm Dateiname.out (Dateiname.exe unter Windows). gcc -o Dateiname hallo.c 7 2 Variablen, Datentypen und Operationen 2.1 Deklaration, Initialisierung, Definition F¨ur die Speicherung und Manipulation von Ein- und Ausgabedaten sowie der Hilfsgr¨oßen eines Algorithmus werden bei der Programmierung Variablen eingesetzt. Je nach Art der Daten w¨ahlt man einen von der jeweiligen Programmiersprache vorgegebenen geeigneten Datentyp aus. Vor ihrer ersten Verwendung m¨ussen die Variablen durch Angabe ihres Typs und ihres Namens deklariert werden. In C hat die Deklaration die folgende Form. Datentyp Variablenname; Man kann auch mehrere Variablen desselben Typs auf einmal deklarieren, indem man die entsprechenden Variablennamen mit Komma auflistet: Datentyp Variablenname1, Variablenname2, ...,VariablennameN; Bei der Deklaration k¨onnen einer Variablen auch schon Werte zugewiesen werden, d.h. eine Initialisierung der Variablen ist bereits m¨oglich. Zusammen mit der Deklaration gilt die Variable dann als definiert. ¨ Die Deklaration von Variablen findet vor der ersten Ausfuhrungsanweisung statt. ¨ Dies ist bei den allermeisten Compilern nicht zwingend notwendig, dient aber der Ubersicht des Quelltextes. Variablennamen Bei der Vergabe von Variablennamen ist folgendes zu beachten: • Variablennamen d¨urfen keine Umlaute enthalten. Als einzigstes Sonderzeichen ist der Unterstrich (engl. underscore) erlaubt. • Variablennamen d¨urfen Zahlen enthalten, aber nicht mit ihnen beginnen. • Groß- und Kleinschreibung von Buchstaben wird unterschieden. 8 ¨ Schlusselwort char int float double void Datentyp Zeichen ganze Zahl Gleitkommazahl mit einfacher Genauigkeit Gleitkommazahl mit doppelter Genauigkeit leerer Datentyp Anzahl Bytes 1 4 4 8 Abbildung 2.1: Elementare Datentypen. Die Bytel¨ange ist von Architektur zu Architektur unterschiedlich (hier: GNU-C-Compiler unter LINUX f¨ur die x86-Architektur. Siehe auch sizeof()). 2.2 Elementare Datentypen ¨ Die folgende Tabelle 2.1 gibt die Ubersicht u¨ ber die wichtigsten Datentypen in C: Anhang A widmet sich speziell der Zahlendarstellung im Rechner. Insbesondere werden dort die Begriffe Gleitkommazahl und deren Genauigkeit er¨ortert. Beispiel 2.1 (Deklaration, Initialisierung, Definition) #include <stdio.h> int main() { int a=4; /* Deklaration von a als ganze Zahl */ /* + Initialisierung von a, d.h. a wird der Wert 4 zugewiesen */ printf("Die int-Variable a wurde initialisiert mit %i\n" ,a ); /* Die Formatangabe %i zeigt an, dass eine int-Variable ausgegeben wird */ return 0; } Der Datentyp char wird intern als ganzzahliger Datentyp behandelt. Er kann daher mit allen Operatoren behandelt werden, die auch f¨ur int verwendet werden. Erst durch die Abbildung der Zahlen von 0 bis 255 auf entsprechende Zeichen (ASCII-Tabelle) entsteht die Verkn¨upfung zu den Zeichen. Einige dieser Datentypen k¨onnen durch Voranstellen von weiteren Schl¨usselw¨ortern modifiziert werden. Modifizierer sind: • signed/unsigned: Gibt f¨ur die Typen int und char an, ob sie mit/ohne Vorzeichen behandelt werden (nur int und char). • short/long: Reduziert/erh¨oht die Bytel¨ange des betreffenden Datentyps. Dabei wirkt sich short nur auf int und long nur auf double aus. 9 • const: Eine so modifizierte Variable kann initialisiert, aber danach nicht mehr mit einem anderen Wert belegt werden. Die Variable ist schreibgesch¨utzt“. ” Bei den zul¨assigen Kombinationen ist die Reihenfolge const - signed/unsigned - long/short Datentyp Variablenname Beispiel 2.2 (Deklaration / Definition von Variablen) Zul¨assig: int a; signed char zeichen1; unsigned short int b; oder a¨ quivalent unsigned short b; long double eps; const int c=12; Im letzten Beispiel wurde der Zuweisungsoperator = (s. Abschnitt 2.4) verwendet, um die schreibgesch¨utzte Variable c zu initialisieren. Variablen vom Typ char werden durch sogenannte Zeichenkonstanten initialisiert. Zeichenkonstanten gibt man an, indem man ein Zeichen in Hochkommata setzt, z.B. char zeichen1=’A’; Nicht zul¨assig: unsigned double d; long char zeichen1; char 1zeichen; (unzul¨assiger Variablenname) Die Funktion sizeof() liefert die Anzahl der Bytes zur¨uck, die f¨ur einen bestimmten Datentyp ben¨otigt werden. Die Funktion sizeof() hat als R¨uckgabewert den Typ int. Beispiel 2.3 (C-Anweisung : sizeof()) /* Beispiel: sizeof() */ # include <stdio.h> int main() { printf("Eine int-Variable benoetigt %i Bytes", sizeof(int) ); return 0; } 10 2.3 Felder und Strings 2.3.1 Felder Eine M¨oglichkeit, aus elementaren Datentypen weitere Typen abzuleiten, ist das Feld (Array). Ein Feld besteht aus n Objekten des gleichen Datentyps. Die Deklaration eines Feldes ist von der Form Datentyp Feldname[n]; Weitere Merkmale: • Die Nummerierung der Feldkomponenten beginnt bei 0 und endet mit n-1. • Die i-te Komponente des Feldes wird mit Feldname[i] angesprochen. • Felder k¨onnen bei der Deklaration initialisiert werden. Dies geschieht unter Verwendung des Zuweisungsoperators und der geschweiften Klammer. Beispiel 2.4 (Felder) #include<stdio.h> int main() { float a[3]={3.2, 5, 6}; /* Deklaration und Initialisierung eines (1 x 3) float-Feldes */ printf("Die 0.-te Komponente von a hat den Wert %f" ,a[0] ); /* Die Formatangabe %f zeigt an, dass eine float bzw. double-Variable ausgegeben wird */ return 0; } 2.3.2 Mehrdimensionale Felder Es ist m¨oglich, die Eintr¨age eines Feldes mehrfach zu indizieren und so h¨oherdimensionale Objekte zu erzeugen; f¨ur d Dimensionen lautet die Deklaration dann: Datentyp Feldname[n1 ][n2 ]...[nd ]; 11 Beispiel 2.5 (Deklaration und Initialisierung einer ganzzahligen 2 x 3-Matrix) #include <stdio.h> int main() { int a[2][3]={{1, 2, 3}, {4, 5, 6}}; printf("Die [0,1].-te Komponente von a hat den Wert %i",a[0][1]); return 0; } 2.3.3 Zeichenketten (Strings) Eine Sonderstellung unter den Feldern nehmen die Zeichenketten (Strings) ein. Es handelt sich dabei um Felder aus Zeichen: char Stringname [L¨ange] Eine Besonderheit stellt dar, dass das Stringende durch die Zeichenkonstante ’\0’ markiert wird. Der String Hallo wird also durch char text[]={’H’,’a’,’l’,’l’,’o’,’\0’}; initialisiert. Ein String kann auch durch char text[]=“Hallo“; initialisiert werden. Dieser String hat auch die L¨ange 6, obwohl nur 5 Zeichen zur Initialisierung benutzt wurden. Das Ende eines Strings markiert immer die Zeichenkonstante ’\0’. Beispiel 2.6 (Deklaration und Initialisierung eines Strings) #include <stdio.h> int main() { char text[]="Hallo"; printf("%s" ,text); /* Die Formatangabe %s zeigt an, dass ein String ausgegeben wird. */ return 0; } 12 ¨ 2.4 Ausdrucke, Operatoren und mathematische Funktionen Der Zuweisungsoperator operand1 = operand2 weist dem linken Operanden den Wert des rechten Operanden zu. Zum Beispiel ist im Ergebnis der Anweisungsfolge Beispiel 2.7 (Zuweisungsoperator) #include <stdio.h> int main() { int x,y; x=2; y=x+4; printf("x=%i und y=%i",x,y); /* Formatangabe %i gibt dem printf-Befehl an, * dass an dieser Stelle eine Integervariable * ausgeben werden soll. */ return 0; } der Wert von x gleich 2 und der Wert von y gleich 6. Hierbei sind x, y, 0, x+4 Operanden, wobei letzterer gleichzeitig ein Ausdruck, bestehend aus den Operanden x, 4 und dem Operator + ist. Sowohl x=2 als auch y=x+4 sind Ausdr¨ucke. Erst das abschließende Semikolon ; wandelt diese Ausdr¨ucke in auszuf¨uhrende Anweisungen. Es k¨onnen auch Mehrfachzuweisungen auftreten. Die folgenden drei Zuweisungen sind a¨ quivalent. Beispiel 2.8 (Mehrfachzuweisung) #include <stdio.h> int main() { int a,b,c; /* 1. Moeglichkeit */ a = b = c = 123; /* 2. Moeglichkeit */ 13 a = (b = (c = 123)); /* 3. Moeglichkeit (Standard) */ c = 123; b=c; a=b; printf("a=%i, b=%i, c=%i",a,b,c); return 0; } 2.4.1 Arithmetische Operatoren Un¨are Operatoren Bei un¨aren Operatoren tritt nur ein Operand auf. Operator - Beschreibung Negation Beispiel -a Bin¨are Operatoren Bei bin¨aren Operatoren treten zwei Operanden auf. Der Ergebnistyp der Operation h¨angt vom Operator ab. Operator + * / % Beschreibung Addition Subtraktion Multiplikation Division (Achtung bei Integerwerten !!!) Rest bei ganzzahliger Division (Modulooperation) Beispiel a+b a-b a*b a/b a%b Achtung!!! Die Division von Integerzahlen berechnet den ganzzahligen Anteil der Division, z.B. liefert 8/3 das Ergebnis 2. Wird jedoch einer der beiden Operanden in eine Gleitkommazahl umgewandelt, so erh¨alt man das exakte Ergebnis. z.B. 8.0/3 liefert 2.66666 als Ergebnis (siehe auch Kapitel 2.8). Analog zur Mathematik gilt ”Punktrechnung geht vor Strichrechnung ”. Desweiteren werden Ausdr¨ucke in runden Klammern zuerst berechnet. Beispiel 2.9 (Arithmetische Operatoren) % ermoeglicht das Setzen von mathematischen Ausdruecken % wird hier fuer die Referenz benutzt #include <stdio.h> int main() 14 { int a,b,c; double x; a=1; a=9/8; a=3.12; a=-3.12; /* /* /* /* a=1 */ a=1, Integerdivision */ a=3, abrunden wegen int-Variable */ a=-3 oder -4, Compiler abhaengig */ b=6; /* b=6 */ c=10; /* c=10 */ x=b/c; /* x=0 */ x=(double) b/c; /* x=0.6 siehe Kapitel 2.8 */ x=(1+1)/2; /* x=1 */ x=0.5+1.0/2; /* x=1 */ x=0.5+1/2; /* x=0.5 */ x=4.2e12; /* x=4.2*10ˆ{12} wissenschaftl. Notation */ return 0; } 2.4.2 Vergleichsoperatoren Vergleichsoperatoren sind bin¨are Operatoren. Der Ergebniswert ist immer ein Integerwert. Sie liefern den Wert 0, falls die Aussage falsch, und den Wert 1, falls die Aussage richtig ist. Operator > >= < <= == != Beschreibung gr¨oßer gr¨oßer oder gleich kleiner kleiner oder gleich gleich (Achtung bei Gleitkommazahlen !!!) ungleich (Achtung bei Gleitkommazahlen !!!) Beispiel a>b a>=b a<b/3 a*b<=c a==b a!=3.14 Achtung !!! Ein typischer Fehler tritt beim Test auf Gleichheit auf, indem statt des Vergleichsoperators == der Zuweisungsoperator = geschrieben wird. Das Pr¨ufen von Gleitkommazahlen auf (Un-)gleichheit kann nur bis auf den Bereich der Maschinengenauigkeit erfolgen und sollte daher vermieden werden. Beispiel 2.10 (Vergleichsoperatoren) #include <stdio.h> int main() 15 { int a,b; int aussage; float x,y; a=3; b=2; aussage = a>b; aussage = a==b; /* /* /* /* a=3 */ b=2 */ aussage=1 ; entspricht wahr */ aussage=0 ; entspricht falsch */ x=1.0+1.0e-8; y=1.0+2.0e-8; aussage = (x==y); /* x=1 + 1.0 *10ˆ{-8} */ /* y=1 + 2.0 *10ˆ{-8} */ /* aussage=0 oder 1 ; entspricht wahr, falls eps > 10ˆ{-8}, obwohl x ungleich y */ return 0; } 2.4.3 Logische Operatoren Es gibt nur einen un¨aren logischen Operator Operator ! Beschreibung logische Negation Beispiel ! (3>4) /* Ergebnis= 1; entspricht wahr */ und zwei bin¨are logische Operatoren Operator && || Beschreibung logisches UND logisches ODER Beispiel (3>4) && (3<=4) /* Ergebnis = 0; entspricht falsch */ (3>4) | | (3<=4) /* Ergebnis = 1; entspricht wahr */ Die Wahrheitstafeln f¨ur das logische UND und das logische ODER sind aus der Algebra bekannt. 2.4.4 Bitorientierte Operatoren Bitorientierte Operatoren sind nur auf int-Variablen (bzw. char-Variablen) anwendbar. Um die Funktionsweise zu verstehen, muss man zun¨achst die Darstellung von Ganzzahlen innerhalb des Rechners verstehen. Ein Bit ist die kleinste Informationseinheit mit genau zwei m¨oglichen Zust¨anden: ( ( ( bit ungesetzt 0 falsch ≡ ≡ bit gesetzt 1 wahr Ein Byte besteht aus 8 Bit. Eine short int-Variable besteht aus 2 Byte. Damit kann also eine short int-Variable 216 Werte annehme. Das erste Bit bestimmt das Vorzeichen der Zahl. Gesetzt bedeutet - (negativ); nicht gesetzt entspricht + (positiv). 16 Beispiel 2.11 ((Short)-Integerdarstellung im Rechner) Darstellung im Rechner (bin¨ar) 0 0000000 00001010 |{z} | {z } | + {z 1. Byte Dezimal 23 + 21 = 10 2. Byte } −(25 + 22 ) − 1 = −37 1 1111111 11011011 |{z} | {z } − | {z } 2. Byte 1. Byte ¨ bitorientierte Operatoren Unare Operator ∼ Beschreibung Bin¨arkomplement Beispiel ∼a ¨ bitorientierte Operatoren Binare Operator & | ∧ << >> Beschreibung bitweises UND bitweises ODER bitweises exklusives ODER Linksshift der Bits von op1 um op2 Stellen Rechtsshift der Bits von op1 um op2 Stellen Beispiel a&1 a |1 a∧1 a << 1 a >> 2 Wahrheitstafel x 0 0 1 1 y 0 1 0 1 x&y 0 0 0 1 x |y 0 1 1 1 x∧y 0 1 1 0 Beispiel 2.12 (Bitorientierte Operatoren) #include <stdio.h> int main() { short int a,b,c; a=5; b=6; /* 00000000 00000101 = 5 */ /* 00000000 00000110 = 6 */ c= ˜ b; c=a & b; /* Komplement 11111111 11111001 =-(2ˆ2+2ˆ1)-1=-7 */ /* 00000000 00000101 = 5 */ 17 /* /* /* /* bit-UND & 00000000 00000110 = 6 gleich 00000000 00000100 = 4 */ */ */ */ c=a | b; /* bit-ODER 00000000 00000111 = 7 */ c=aˆb; /* bit-ODER exklusiv 00000000 00000011 = 3 c=a << 2; /* 2 x Linksshift 00000000 00010100 = 20 c=a >> 1; /* 1 x Rechtsshift & 00000000 00000010 = 2 */ */ */ return 0; } ¨ Anwendungen zu bitorientierten Operatoren werden in den Ubungen besprochen. 2.4.5 Inkrement- und Dekrementoperatoren Pr¨afixnotation Notation ++ operand −− operand Beschreibung operand=operand+1 operand=operand-1 Beispiel 2.13 (Pr¨afixnotation) #include <stdio.h> int main() { int i=3,j; ++i; /* i=4 */ j=++i; /* i=5, j=5 */ /* oben angegebene Notation ist aequivalent zu */ i=i+1; j=i; return 0; } 18 Postfixnotation Notation operand++ operand−− Beschreibung operand=operand+1 operand=operand-1 Beispiel 2.14 (Postfixnotation) #include <stdio.h> int main() { int i=3,j; i++; /* i=4 */ j=i++; /* j=4 ,i=5 */ /* oben angegebene Notatation ist aequivalent zu */ j=i; i=i+1; return 0; } 2.4.6 Adressoperator Der Vollst¨andigkeit halber wird der Adressoperator “ & “ schon in diesem Kapitel eingef¨uhrt, obwohl die Bedeutung erst in Kapitel 4 klar wird. & datenobjekt ¨ 2.4.7 Prioritaten von Operatoren Es k¨onnen beliebig viele Aussagen durch Operatoren verkn¨upft werden. Die Reihenfolge der Ausf¨uhrung h¨angt von der Priorit¨at der jeweiligen Operatoren ab. Operatoren mit h¨oherer Priorit¨at werden vor Operatoren niedriger Priorit¨at ausgef¨uhrt. Haben Operatoren die gleiche Priorit¨at so werden sie gem¨aß ihrer sogenannten Assoziativit¨at von links nach rechts oder umgekehrt abgearbeitet. Priorit¨aten von Operatoren beginnend mit der H¨ochsten 19 Priorit¨at 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 Operator () [] −> . + ! ˜ ++ −− ++ −− & ∗ (Typ) sizeof() ∗ / % + << >> < <= > >= == != & ∧ | && || :? = ∗ =, / =, + = − =, & =, ∧ = | =, <<= >>= , Beschreibung Funktionsaufruf Indizierung Elementzugriff Elementzugriff Vorzeichen Vorzeichen Negation Bitkomplement Pr¨afix-Inkrement Pr¨afix-Dekrement Postfix-Inkrement Postfix-Dekrement Adresse Zeigerdereferenzierung Cast Gr¨oße Multiplikation Division Modulo Addition Subtraktion Links-Shift Rechts-Shift kleiner kleiner gleich gr¨oßer gr¨oßer gleich gleich ungleich bitweises UND bitweises exklusivers ODER bitweises ODER logisches UND logisches ODER Bedingung Zuweisung Zusammengesetzte Zuweisung Zusammengesetzte Zuweisung Zusammengesetzte Zuweisung Komma-Operator Im Zweifelsfall kann die Priorit¨at durch Klammerung erzwungen werden. 20 Assoz. → → → → ← ← ← ← ← ← ← ← ← ← ← ← → → → → → → → → → → → → → → → → → → ← ← ← ← ← → Beispiel 2.15 (Priorit¨aten von Operatoren) #include <stdio.h> int main() { int a=-4, b=-3, c; c=a<b<-1; /* c=0 ; falsch */ c=a<(b<-1); /* c=1 ; wahr */ c=a ==-4 && b == -2; /* c=0 ; falsch */ return 0; } 2.5 Operationen mit vordefinierten Funktionen 2.5.1 Mathematische Funktionen Im Headerfile math.h werden u.a. Deklarationen der in Tabelle 2.1 zusammengefassten mathematischen Funktionen und Konstanten bereitgestellt: Funktion/Konstante sqrt(x) exp(x) log(x) pow(x,y) fabs(x) fmod(x,y) ceil(x) floor(x) sin(x), cos(x), tan(x) asin(x), acos(x), atan(x) ME M PI Beschreibung Wurzel von x ex nat¨urlicher Logarithmus von x xy Absolutbetrag von x : |x| realzahliger Rest von x/y n¨achste ganze Zahl ≥ x n¨achste ganze Zahl ≤ x trigonometrische Funktionen trig. Umkehrfunktionen Eulersche Zahl e π Tabelle 2.1: Mathematische Funktionen F¨ur die Zul¨assigkeit der Operation, d.h. den Definitionsbereich der Argumente, ist der Programmierer verantwortlich. Ansonsten werden Programmabbr¨uche oder unsinnige Ergebnisse produziert. 21 Beispiel 2.16 (Mathematische Funktionen und Konstanten) #include <stdio.h> #include <math.h> int main() { float x,y,z; x=4.5; /* x=4.5 */ y=sqrt(x); /* y=2.121320, was ungefaehr = sqrt(4.5) */ z=M_PI; /* z=3.141593, was ungefaehr = pi */ return 0; } ¨ Zeichenketten (Strings) 2.5.2 Funktionen fur Im Headerfile string.h werden u.a. die Deklarationen der folgenden Funktionen f¨ur Strings bereitgestellt: Funktion strcat(s1,s2) strcmp(s1,s2) strcpy(s1,s2) strlen(s) strchr(s,c) Beschreibung Anh¨angen von s2 an s1 Lexikographischer Vergleich der Strings s1 und s2 Kopiert s2 auf s1 Anzahl der Zeichen in String s ( = sizeof(s)-1) Sucht Zeichenkonstante (Character) c in String s Tabelle 2.2: Funktionen f¨ur Strings ¨ Zeichenketten (Strings)) Beispiel 2.17 (Funktionen fur #include <string.h> #include <stdio.h> int main() { int i; char s1[]="Hallo"; /* reserviert 5+1 Byte im Speicher fuer s1 und belegt sie mit H,a,l,l,o,\0 */ char s2[]="Welt"; /* reserviert 4+1 Byte im Speicher f¨ ur s2 */ char s3[100]="Hallo"; /* reserviert 100 Byte im Speicher f¨ ur s3 * und belegt die ersten 6 mit H,a,l,l,o,\0 */ 22 /* !!!NICHT ZULAESSIG!!! (Kann zu Programmabsturz fuehren) *** */ strcat(s1,s2); /* Im reservierten Speicherbereich von s1 * steht nun H,a,l,l,o,W * Der Rest von s2 wird irgendwo in den * Speicher geschrieben */ /* ZULAESSIG */ strcat(s3,s2); /* Die ersten 10 Bytes von s3 sind nun * belegt mit H,a,l,l,o,W,e,l,t,\0 * Der Rest ist zufaellig beschrieben */ strcpy(s1,s2); /* Die ersten 5 Bytes von s1 sind nun * belegt mit W,e,l,t,\0 */ i=strlen(s2); /* i=4 */ i=strcmp(s2,s3); /* i=15, Unterschied zwischen ’W’ und ’H’ in * ASCII*/ return 0; } Achtung Der Umgang mit Strings ist problematisch, z.B. wird bei dem Befehl strcat(s1,s2) der String s2 an s1 angeh¨angt. Dadurch wird der Speicherbedarf f¨ur String s1 vergr¨oßert. Wurde bei der Deklaration von s1 zu wenig Speicherplatz reserviert (allokiert) schreibt der Computer die u¨ bersch¨ussigen Zeichen in einen nicht vorher bestimmten Speicherbereich. Dies kann unter Umst¨anden sogar zum Absturz des Programms f¨uhren – die Alternative sind seltsame und schwer zu findende Fehler des Programms (siehe auch Beipiel 2.17). 2.6 Zusammengesetzte Anweisungen Wertzuweisungen der Form op1=op1 operator op2 k¨onnen zu op1 operator = op2 verk¨urzt werden. Hierbei ist operator ∈ {+, −, ∗, /, %, |, ∧ , <<, >>}. Beispiel 2.18 (Zusammengesetzte Anweisungen) #include <stdio.h> int main() { int i=7,j=3; 23 i += j; /* i=i+j; */ i >>= 1; /* i=i >> 1 (i=i/2) */ j *= i; /* j=j*i */ return 0; } ¨ 2.7 Nutzliche Konstanten F¨ur systemabh¨angige Zahlenbereiche, Genauigkeiten usw. ist die Auswahl der Konstanten aus Tabelle 2.3 und Tabelle 2.4 recht hilfreich. Sie stehen dem Programmierer durch Einbinden der Headerdateien float.h bzw. limits.h zur Verf¨ugung. Tabelle 2.3: Konstanten aus float.h Konstante FLT DIG FLT MIN FLT MAX FLT EPSILON DBL LDBL Beschreibung Anzahl g¨ultiger Dezimalstellen f¨ur float Kleinste, darstellbare echt positive float Zahl Gr¨oßte, darstellbare positive float Zahl Kleinste positive Zahl mit 1.0 + eps , 1.0 wie oben f¨ur double wie oben f¨ur long double Tabelle 2.4: Konstanten aus limits.h Konstante INT MIN INT MAX SHRT Beschreibung Kleinste, darstellbare int Zahl Gr¨oßte, darstellbare int Zahl wie oben f¨ur INT Weitere Konstanten k¨onnen in der Datei float.h nachgeschaut werden. Der genaue Speicherort dieser Datei ist abh¨angig von der gerade verwendeten Version des gcc und der verwendeten Distribution. Die entsprechenden Headerfiles k¨onnen auch mit dem Befehl find /usr -name float.h -print gesucht werden. Dieser Befehl durchsucht den entsprechenden Teil des Verzeichnisbaums (/usr) nach der Datei namens float.h 24 2.8 Typkonversion (cast) Beispiel 2.19 (Abgeschnittene Division) Nach der Zuweisung hat die Variable quotient den Wert 3.0, obwohl sie als Gleitkommazahl deklariert wurde! Ursache: Resultat der Division zweier int-Variablen ist standardm¨asig wieder ein int-Datenobjekt. #include <stdio.h> int main() { int a=10, b=3; float quotient; quotient = a/b; /* quotient = 3 */ quotient = (double) a/b; /* quotient = 3.3333 */ return 0; } Abhilfe schaffen hier Typumwandlungen (engl.: Casts). Dazu setzt man den gew¨unschten Datentyp in Klammern vor das umzuwandelnde Objekt, im obigen Beispiel: quotient = (float) a/b; Hierdurch wird das Ergebnis mit den Nachkommastellen u¨ bergeben. Vorsicht bei Klammerung von Ausdr¨ucken! Die Anweisung quotient = (float) (a/b); f¨uhrt wegen der Klammern die Division komplett im int-Kontext durch und der Cast bleibt wirkungslos. Bemerkung 2.20 Die im ersten Beispiel gezeigte abgeschnittene Division erlaubt in Verbindung mit dem Modulooperator % eine einfache Programmierung der Division mit Rest. Ist einer der Operanden eine Konstante, so kann man auch auf Casts verzichten: Statt quotient = (float) 10/b; kann man die Anweisung quotient = 10.0/b; verwenden. 25 2.9 Standardein- und -ausgabe Eingabe: Das Programm fordert ben¨otigte Informationen/Daten vom Benutzer an. Ausgabe: Das Programm teilt die Forderung nach Eingabedaten dem Benutzer mit und gibt (Zwischen-) Ergebnisse aus. 2.9.1 Ausgabe Die Ausgabe auf das Standardausgabeger¨at (Terminal, Bildschirm) erfolgt mit der printf()Bibliotheksfunktion. Die Anweisung ist von der Form printf(Formatstringkonstante, Argumentliste); Die Argumentliste ist eine Liste von auszugebenden Objekten, jeweils durch ein Komma getrennt (Variablennamen, arithmetische Ausdr¨ucke etc.). Die Formatstringkonstante enth¨alt zus¨atzliche spezielle Zeichen: spezielle Zeichenkonstanten (Escapesequenzen) und Formatangaben. Zeichenkonstante \n \t \v \b \\ \? \’ \” erzeugt neue Zeile Tabulator vertikaler Tabulator Backspace Backslash \ Fragezeichen ? Hochkomma Anf¨uhrungsstriche Die Formatangaben spezifizieren, welcher Datentyp auszugeben ist und wie er auszugeben ist. Sie beginnen mit %. ¨ Die folgende Tabelle gibt einen Uberblick u¨ ber die wichtigsten Formatangaben: Formatangabe %f %i, %d %u %o %x %c %s %li, %ld %Lf %e Datentyp float, double int, short unsigned int int, short oktal int, short hexadezimal char Zeichenkette (String) long long double float, double wissenschaftl. Notation 26 • Durch Einf¨ugen eines Leerzeichens nach % wird Platz f¨ur das Vorzeichen ausgespart. Nur negative Vorzeichen werden angezeigt. F¨ugt man stattdessen ein + ein, so wird das Vorzeichen immer angezeigt. • Weitere Optionen kann man aus Beispiel 2.21 entnehmen. Beispiel 2.21 (Ausgabe von Gleitkommazahlen) #include <stdio.h> int main() { const double pi=3.14159265; printf("Pi printf("Pi printf("Pi printf("Pi printf("Pi = = = = = \%f\n",pi); \% f\n",pi); \%+f\n",pi); \%.3f\n",pi); \%.7e\n",pi); return 0; } erzeugen die Bildschirmausgabe Pi = 3.141593 Pi = 3.141593 Pi = +3.141593 Pi = 3.142 Pi = 3.1415927e+00 2.9.2 Eingabe F¨ur das Einlesen von Tastatureingaben des Benutzers steht u.a. die Bibliotheksfunktion scanf() zur Verf¨ugung. Ihre Verwendung ist auf den ersten Blick identisch mit der von printf(). scanf(Formatstringkonstante, Argumentliste); Die Argumentliste bezieht sich auf die Variablen, in denen die eingegebenen Werte abgelegt werden sollen, wobei zu beachten ist, dass in der Argumentliste nicht die Variablen selbst, sondern ihre Adressen anzugeben sind. Dazu verwendet man den Adressoperator &. 27 Beispiel 2.22 (Einlesen einer ganzen Zahl) #include <stdio.h> int main() { int a; printf("Geben Sie eine ganze Zahl ein: "); scanf("%i",&a); printf("a hat nun den Wert : %i",a); return 0; } Die eingegebene Zahl wird als int interpretiert und an der Adresse der Variablen a abgelegt. Die anderen Formatangaben sind im Wesentlichen analog zu printf(). Eine Ausnahme ist das Einlesen von double- und long double-Variablen. Statt %f sollte man hier • %lf f¨ur double • %Lf f¨ur long double verwenden. Das Verhalten variiert je nach verwendetem C-Compiler. Achtung ! Handelt es sich bei der einzulesenden Variable um ein Feld (insbesondere String) oder eine Zeiger Variable (siehe Kapitel 4), so entf¨allt der Adressoperator & im scanf()-Befehl. Bsp.: char text[100]; scanf(”%s”,text); Die Funktion scanf ist immer wieder eine Quelle f¨ur Fehler. int zahl; char buchstabe; scanf("%i", &zahl); scanf("%c", &buchstabe); Wenn man einen solchen Code laufen l¨asst, wird man sehen, dass das Programm den zweiten scanf -Befehl scheinbar einfach u¨ berspringt. Der Grund ist die Art, wie scanf arbeitet. Die Eingabe des Benutzers beim ersten scanf besteht aus zwei Teilen: einer Zahl (sagen wir 23) und der Eingabetaste (die wir mit ’\n’ bezeichnen). Die Zahl 23 wird in die Variable zahl kopiert, das ’\n’ steht aber immer noch im sog. Tastaturpuffer. Beim zweiten scanf liest der Rechner dann sofort das ’\n’ aus und geht davon aus, dass der Benutzer dieses ’\n’ als Wert f¨ur die Variable buchstabe wollte. Vermeiden kann man dies mit einem auf den ersten Blick komplizierten Konstrukt, das daf¨ur deutlich flexibler ist. 28 int zahl; char buchstabe; char tempstring[80]; fgets(tempstring, sizeof(tempstring), stdin); /* wir lesen eine ganze Zeile in den String tempstring von stdin -* das ist die Standardeingabe */ /* Wir haben jetzt einen ganzen String, wie teilen wir ihn auf? * => mit der Funktion sscanf */ sscanf(tempstring, "%d", &zahl); /* und nun nochmal fuer den Buchstaben */ fgets(tempstring, sizeof(tempstring), stdin); sscanf(tempstring, "%c", &buchstabe); Der R¨uckgabewert von fgets ist ein Zeiger; der obige Code u¨ berpr¨uft nicht, ob dies ein NULL ¨ Zeiger ist – diese Uberpr¨ ufung ist in einem Programm nat¨urlich Pflicht! Die Funktionen fgets und sscanf sind in stdio.h deklariert. 29 3 Programmflusskontrolle ¨ 3.1 Bedingte Ausfuhrung Bei der Bedingten Ausf¨uhrung werden Ausdr¨ucke auf ihren Wahrheitswert hin u¨ berpr¨uft und der weitere Ablauf des Programms davon abh¨angig gemacht. C sieht hierf¨ur die Anweisungen if und switch vor: 3.1.1 Die if()-Anweisung Die allgemeine Form der Verzweigung (Alternative) ist if (logischer Ausdruck) { Anweisungen A } else { Anweisungen B } und z¨ahlt ihrerseits wiederum als Anweisung. Der else-Zweig kann weggelassen werden (einfache Alternative). Folgt nach dem if- bzw. else-Befehl nur eine Anweisung, so muss diese nicht in einen Block (geschweifte Klammern) geschrieben werden. Dies sollte dann aber durch Schreiben der einzigen Anweisung hinter den if -Befehl deutlich gemacht werden! Beispiel 3.1 (Signum-Funktion) Die Signum-Funktion gibt das Vorzeichen an: 1 y(x) = 0 −1 30 x>0 x=0 x<0 int main() /* Signum Funktion */ { float x,y; if (x>0.0) { y=1.0; } else { if (x == 0.0) { y=0.0; } else { y=-1.0; } } return 0; } 3.1.2 Die switch()-Anweisung Zur Unterscheidung von mehreren F¨allen ist die Verwendung von switch-case-Kombinationen bequemer. Mit dem Schl¨usselwort switch wird ein zu u¨ berpr¨ufender Ausdruck benannt. Es folgt ein Block mit case-Anweisungen, die f¨ur die einzelnen m¨oglichen F¨alle Anweisungsbl¨ocke vorsehen. Mit dem Schl¨usselwort default wird ein Anweisungsblock eingeleitet, der dann auszuf¨uhren ist, wenn keiner der anderen F¨alle eingetreten ist (optional). switch (Ausdruck) { case Fall 1: { Anweisungen f¨ ur Fall 1 break; } ... case Fall n: { Anweisungen f¨ ur Fall n break; } default: { 31 Anweisungen f¨ ur alle anderen F¨ alle break; } } Beispiel 3.2 (switch-Anweisung) #include <stdio.h> int main() { int nummer; printf("Geben sie eine ganze Zahl an: "); scanf("%i",&nummer); printf("Namen der Zahlen aus {1,2,3} switch (nummer) { case 1: { printf("Eins = %i \n", break; } case 2: { printf("Zwei = %i \n", break; } case 3: { printf("Drei = %i \n", break; } default: { printf("Die Zahl liegt break; } } return 0; } 32 \n"); nummer); nummer); nummer); nicht in der Menge {1,2,3} \n"); 3.2 Schleifen Schleifen dienen dazu, die Ausf¨uhrung von Anweisungsbl¨ocken zu wiederholen. Die Anzahl der Wiederholungen ist dabei an eine Bedingung gekn¨upft. Zur Untersuchung, ob eine Bedingung erf¨ullt ist, werden Vergleichs- und Logikoperatoren aus Kapitel 2.4 benutzt. ¨ 3.2.1 Der Zahlzyklus (for-Schleife) Beim Z¨ahlzyklus steht die Anzahl der Zyklendurchl¨aufe a-priori fest, der Abbruchtest erfolgt vor dem Durchlauf eines Zyklus. Die allgemeine Form ist for (ausdruck1; ausdruck2; ausdruck3) { Anweisungen } Am besten versteht man den Z¨ahlzyklus an einem Beispiel. ¨ Beispiel 3.3 (Summe der naturlichen Zahlen von 1 bis n) #include <stdio.h> int main() { int i,summe,n; char tempstring[80]; /* Einlesen der oberen Schranke n * von der Tastatur */ printf("Obere Schranke der Summe : "); fgets(tempstring, sizeof(tempstring), stdin); sscanf(tempstring, "%i", &n); summe=0; /* Setze summe auf 0 */ for (i=1; i<=n; i=i+1) { summe=summe+i; } printf("Summe der Zahlen von 1 bis %i ist %i \n",n,summe); return 0; } Im obigen Programmbeispiel ist i die Laufvariable des Z¨ahlzyklus, welche mit i=1 (ausdruck1) initialisiert wird, mit i=i+1 (ausdruck3) weitergez¨ahlt und in i <= n (ausdruck2) bzgl. der oberen Grenze der Schleifendurchl¨aufe getestet wird. Im Schleifeninneren summe=summe+i; (anweisung) erfolgen die eigentlichen Berechnungsschritte des Zyklus. Die Summationsvariable uss vor dem Eintritt in den Zyklus initialisiert werden. 33 Eine kompakte Version dieser Summationsschleife (korrekt, aber sehr schlecht lesbar) w¨are: for (summe=0, i=1; i <=n; summe+=i, i++) Man unterscheidet dabei zwischen den Abschluss einer Anweisung “;“ und dem Trennzeichen “,“ in einer Liste von Ausdr¨ucken. Diese Listen werden von links nach rechts abgearbeitet. Der ausdruck2 ist stets ein logischer Ausdruck und ausdruck3 ist ein arithmetischer Ausdruck zur Manipulation der Laufvariablen. Die Laufvariable kann eine einfache Variable vom Typ int, float oder double sein. Achtung!!! Vorsicht bei der Verwendung von Gleitkommazahlen (float,double) als Laufvariable. Dort ist der korrekte Abbruchtest wegen der internen Zahlendarstellung unter Umst¨anden nicht einfach zu realisieren. Die folgenden Beipiele 3.4, 3.5 verdeutlichen die Problematik der begrenzten Genauigkeit von Gleitkommazahlen in Verbindung mit Zyklen und einige Tipps zu deren Umgehung. Beispiel 3.4 Ausgabe der diskreten Knoten xi des Intervalls [a,b], welches in n gleichgroße Teilintervalle zerlegt wird, d.h. xi = a + i ∗ h, i = 0, . . . , n mit h = b−a n #include <stdio.h> int main() { float a,b,xi,h; int n; char tempstring[80]; a=0.0; b=1.0; /* Intervall [a,b] wird initialisiert */ /* mit [0,1] */ printf("Geben Sie die Anzahl der Teilintervalle an: "); fgets(tempstring, sizeof(tempstring), stdin); sscanf(tempstring, "%i", &n); h=(b-a)/n; n=1; /* n wird nun als Hilfsvariable verwendet */ for (xi=a; xi<=b; xi=xi+h) { printf("%i.te Knoten : %f \n",n,xi); n=n+1; } return 0; } 34 Da Gleitkommazahlen nur eine limitierte Anzahl g¨ultiger Ziffern besitzt, kann es (meistens) passieren, dass der letzte Knoten xn = b nicht ausgegeben wird. Auswege sind: ¨ 1.) Anderung des Abbruchtests in xi <= b + h/2.0 (jedoch ist xn immer noch fehlerbehaftet). 2.) Zyklus mit int-Variable for (i=0; i<=n; i++) { xi=a+i*h; printf(“%i.te Knoten : %f \n“,n,xi); } Die gemeinsame Summation kleinerer und gr¨oßerer Zahlen kann ebenfalls zu UngenauigkeiP ten f¨uhren. Im Beispiel 3.5 wird die Summe ni=1 1/i2 auf zwei verschiedene Arten berechnet. Beispiel 3.5 #include <stdio.h> #include <math.h> #include <limits.h> /* enth¨ alt die Konstante INT_MAX */ int main() { float summe1 = 0.0,summe2 = 0.0; int i,n; char tempstring[80]; printf("Der erste Algorithmus wird ungenau f¨ ur n bei ca. %f \n", ceil(sqrt(1.0/1.0e-6)) ); /* siehe Kommentar 1.Schranke */ printf("Weitere Fehler ergeben sich f¨ ur n >= %f, \n", ceil(sqrt(INT_MAX)) ); /* siehe Kommentar 2.Schranke */ printf("Geben Sie die obere Summationsschranke n an : "); fgets(tempstring, sizeof(tempstring), stdin); sscanf(tempstring, "%i", &n); for (i=1; { /* /* /* i<=n; i++) 1. Schranke f¨ ur i */ Der Summand 1.0/(i*i) wird bei der Addition */ nich mehr ber¨ ucksichtigt, falls 1.0/(i*i) < 1.0e-6 */ /* 2. Schranke f¨ ur i */ /* Das Produkt i*i ist als int-Variable nicht */ 35 /* mehr darstellbar, falls i*i > INT_MAX */ summe1=summe1+1.0/(i*i); } for (i=n; i>=1; i--) { summe2=summe2+1.0/i/i; } printf("Der erste Algorithmus liefert das Ergebnis : %f \n",summe1); printf("Der zweite Algorithmus liefert das Ergebnis : %f \n",summe2); return 0; } Das numerische Resultat in summe2 ist genauer, da dort zuerst alle kleinen Zahlen addiert werden, welche bei summe1 wegen der beschr¨ankten Anzahl g¨ultiger Ziffern keinen Beitrag zur Summation mehr liefern k¨onnen. Gleichzeitig ist zu beachten, dass die Berechnung von i*i nicht mehr in int-Zahlen darstellbar ist f¨ur i*i > INT MAX. Dagegen erfolgt die Berechnung 1.0/i/i vollst¨andig im Bereich von Gleitkommazahlen. 3.2.2 Abweisender Zyklus (while-Schleife) Beim abweisenden Zyklus steht die Anzahl der Durchl¨aufe nicht a-priori fest. Der Abbruchtest erfolgt vor dem Durchlauf eines Zyklus. Die allgemeine Form ist while(logischer Ausdruck) { Anweisungen } Beispiel 3.6 (while-Schleife) F¨ur eine beliebige Anzahl von Zahlen soll das Quadrat berechnet werden. Die Eingabeserie wird durch die Eingabe von 0 beendet. #include <stdio.h> int main() { float zahl; char tempstring[80]; printf("Geben Sie eine Zahl ein (’0’ f¨ ur Ende) : "); fgets(tempstring, sizeof(tempstring), stdin); sscanf(tempstring, "%f", &zahl); 36 while (zahl != 0.0) { printf("%f hoch 2 = %f \n",zahl,zahl*zahl); printf("Geben Sie eine Zahl ein (’0’ f¨ ur Ende) : "); fgets(tempstring, sizeof(tempstring), stdin); sscanf(tempstring, "%f", &zahl); } return 0; } 3.2.3 Nichtabweisender Zyklus (do-while-Schleife) Beim nichtabweisenden Zyklus steht die Anzahl der Durchl¨aufe nicht a-priori fest. Der Abbruchtest erfolgt nach dem Durchlauf eines Zyklus. Somit durchl¨auft der nichtabweisende Zyklus mindestens einmal die Anweisungen im Zykleninneren. Die allgemeine Form ist do { Anweisungen } while(logischer Ausdruck); Beispiel 3.7 (do-while-Schleife) Es wird solange eine Zeichenkette von der Tastatur eingelesen, bis die Eingabe eine ganze Zahl ist. #include <stdio.h> #include <math.h> /* F¨ ur pow */ int main() { int i, n, zahl=0; char text[100]; do { printf("Geben Sie eine ganze Zahl ein : "); fgets(text, sizeof(text), stdin); sscanf(text, "%s",text); /* Es wird nacheinander gepr¨ uft ob text[i] */ /* eine Ziffer ist. Besteht die Eingabe */ /* nur aus Ziffern, dann wird abgebrochen */ 37 i=0; while (’0’ <= text[i] && text[i] <= ’9’) { /* ASCII */ i=i+1; } if (text[i] != ’\0’) { printf("%c ist keine Ziffer \n",text[i]); } } while (text[i] != ’\0’); /* Umwandlung von String zu Integer */ n=i; /* Die L¨ ange des Strings == i */ for (i=0;i<=n-1;i++) { zahl=zahl+ (text[i]-’0’)*pow(10,n-1-i); } printf("Die Eingabe %s entspricht der ganzen Zahl %i\n",text,zahl); return 0; } Intern behandelt der Computer Zeichenkonstanten wie int-Variablen. Die Zuweisung erfolgt u¨ ber die ASCII-Tabelle. So entsprechen z.B. die Zeichenkonstanten ’0’,...,’9’ den Werten 48,..,57. ¨ 3.3 Anweisungen zur unbedingten Steuerungsubergabe • break Es erfolgt der sofortige Abbruch der n¨achst¨außeren switch-, while-, do-whileoder for-Anweisung. • continue Abbruch des aktuellen und Start des n¨achsten Zyklus einer while-, do-whileoder for-Schleife. • goto marke Fortsetzung des Programms an der mit marke markierten Anweisung Achtung!!! Die goto-Anweisung sollte sparsam (besser gar nicht) verwendet werden, da sie dem strukturierten Programmieren zuwider l¨auft und den gef¨urchteten Spaghetticode erzeugt. In den ¨ Ubungen (und in der Klausur) ist die goto-Anweisung zur L¨osung der Aufgaben nicht erlaubt. 38 4 Zeiger (Pointer) Bislang war beim Zugriff auf eine Variable nur ihr Inhalt von Interesse. Dabei war es unwichtig, wo (an welcher Speicheradresse) der Inhalt abgelegt wurde. Ein neuer Variablentyp, der Pointer (Zeiger), speichert Adressen unter Ber¨ucksichtigung des dort abgelegten Datentyps. 4.1 Adressen Das folgende Programm demonstriert, wie man Speicheradressen von Variablen ermittelt. Beispiel 4.1 (Adressen von Variablen) #include <stdio.h> int main() { int a=16; int b=4; double f=1.23; float g=5.23; /* Formatangabe %u steht /* &a = Adresse von a */ printf("Wert von a = %i, printf("Wert von b = %i, printf("Wert von f = %f, printf("Wert von g = %f, return 0; f¨ ur unsigned int */ \t \t \t \t Adresse Adresse Adresse Adresse von von von von a b f g = = = = %u %u %u %u \n",a,(unsigned \n",b,(unsigned \n",f,(unsigned \n",g,(unsigned int)&a); int)&b); int)&f); int)&g); } Nach dem Start des Programms erscheint folgende Ausgabe: Wert Wert Wert Wert von von von von a b f g = = = = 16, Adresse von a = 2289604 4, Adresse von b = 2289600 1.230000, Adresse von f = 2289592 5.230000, Adresse von g = 2289588 Bemerkung 4.2 ( Bemerkung zu Beispiel 4.1) 1.) Dieses Programm zeigt die Werte und die Adressen der Variablen a, b, f, g an. (Die Adressangaben sind abh¨angig vom System und Compiler und variieren dementsprechend). 39 2.) Der Adressoperator im printf()-Befehl sorgt daf¨ur, dass nicht der Inhalt der jeweiligen Variable ausgegeben wird, sondern die Adresse der Variable im Speicher. Die Formatangabe %u dient zur Ausgabe von vorzeichenlosen Integerzahlen (unsigned int). Dieser Platzhalter ist hier n¨otig, da der gesamte Wertebereich der Ganzzahl ausgesch¨opft werden soll und negative Adressen nicht sinnvoll sind. 3.) In der letzten Zeile wird angegeben, dass die Variable g auf der Speicheradresse 2289588 liegt und die Variable f auf der Adresse 2289592. Die Differenz beruht auf der Tatsache, dass die Variable g vom Typ float zur Speicherung sizeof(float)=4 Bytes ben¨otigt. Auch bei den anderen Variablen kann man erkennen, wieviel Speicherplatz sie aufgrund ihres Datentyps ben¨otigen. 4.2 Pointervariablen Eine Pointervariable (Zeigervariable) ist eine Variable, deren Wert (Inhalt) eine Adresse ist. Die Deklaration erfolgt durch: Datentyp *Variablenname; Das n¨achste Programm veranschaulicht diese Schreibweise. Beispiel 4.3 #include <stdio.h> int main() { int a=16; int *pa; /* Deklaration von int Zeiger pa - pa ist ein Zeiger auf * eine Integer*/ double f=1.23; double *pf; /* Deklaration von double Zeiger pf */ pa=&a; /* Zeiger pa wird die Adresse von a zugewiesen */ pf=&f; /* Zeiger pf wird die Adresse von f zugewiesen */ printf("Variable a : Inhalt = %i\t Adresse = %u\t ,a,(unsigned int)&a,sizeof(a)); printf("Variable pa : Inhalt = %u\t Adresse = %u\t ,(unsigned int)pa,(unsigned int)&pa,sizeof(pa)); printf("Variable f : Inhalt = %f\t Adresse = %u\t ,f,(unsigned int)&f,sizeof(f)); printf("Variable pf : Inhalt = %u\t Adresse = %u\t ,(unsigned int)pf,(unsigned int)&pf,sizeof(pf)); return 0; } 40 Gr¨ oße %i \n" Gr¨ oße %i \n" Gr¨ oße %i \n" Gr¨ oße %i \n" Das Programm erzeugt folgende Ausgabe: Variable Variable Variable Variable a : Inhalt = 16 Adresse = 2289604 pa : Inhalt = 2289604 Adresse = 2289600 f : Inhalt = 1.230000 Adresse = 2289592 pf : Inhalt = 2289592 Adresse = 2289588 Gr¨ oße Gr¨ oße Gr¨ oße Gr¨ oße 4 4 8 4 Bemerkung 4.4 ( Bemerkung zu Beispiel 4.3) 1.) Da Pointervariablen wieder eine Speicheradresse besitzen, ist die Definition eines Pointers auf einen Pointer nicht nur sinnvoll, sondern auch n¨utzlich (siehe Beispiel 4.9). 2.) Die Gr¨oße des ben¨otigten Speicherplatzes f¨ur einen Pointer ist unabh¨angig vom Typ der ihm zu Grunde liegt, da der Inhalt stets eine Adresse ist. Der hier verwendete Rechner (32-Bit-System) hat einen Speicherplatzbedarf von 4 Byte (= 32 Bit). 4.3 Adressoperator und Zugriffsoperator Der un¨are Adressoperator & (Referenzoperator) & Variablenname bestimmt die Adresse der Variable. Der un¨are Zugriffsoperator * (Dereferenzoperator) * pointer erlaubt den (indirekten) Zugriff auf den Inhalt, auf den der Pointer zeigt. Die Daten k¨onnen wie Variablen manipuliert werden. Beispiel 4.5 #include <stdio.h> int main() { int a=16; int b; int *p; /* Deklaration von int Zeiger p */ p=&a; /* Zeiger p wird die Adresse von a zugewiesen */ b=*p; /* b = Wert unter Adresse p = a = 16 */ printf("Wert von b = %i = %i = Wert von printf("Wert von a = %i = %i = Wert von printf("Adresse von a = %u = %u = Wert ,(unsigned int)&a,(unsigned int)p); printf("Adresse von b = %u != %u = Wert 41 *p \n",b,*p); *p \n",a,*p); von p\n" von p\n\n" ,(unsigned int)&b,(unsigned int)p); *p=*p+2; /* Wert unter Adresse p wird um 2 erh¨ oht */ /* d.h. a=a+2 */ printf("Wert von b = %i != %i = Wert von *p printf("Wert von a = %i = %i = Wert von *p printf("Adresse von a = %u = %u = Wert von ,(unsigned int)&a,(unsigned int)p); printf("Adresse von b = %u != %u = Wert von ,(unsigned int)&b,(unsigned int)p); return 0; \n",b,*p); \n",a,*p); p\n" p\n\n" } Das Programm erzeugt folgende Ausgabe: Wert von b = 16 Wert von a = 16 Adresse von a = Adresse von b = = 16 = Wert von *p = 16 = Wert von *p 2289604 = 2289604 = Wert von p 2289600 != 2289604 = Wert von p Wert von b = 16 Wert von a = 18 Adresse von a = Adresse von b = != 18 = Wert von *p = 18 = Wert von *p 2289604 = 2289604 = Wert von p 2289600 != 2289604 = Wert von p 4.4 Zusammenhang zwischen Zeigern und Feldern Felder nutzen das Modell des linearen Speichers, d.h. ein im Index nachfolgendes Element ist auch physisch im unmittelbar nachfolgenden Speicherbereich abgelegt. Dieser Fakt erlaubt die Interpretation von Zeigervariablen als Feldbezeichner und umgekehrt. Beispiel 4.6 (Zeiger und Felder) #include <stdio.h> int main() { float ausgabe; float f[4]={1,2,3,4}; float *pf; pf=f; /* A¨quivalent w¨ are die Zuweisung pf=&f[0] */ 42 /* Nicht Zul¨ assige Operationen mit Feldern */ /* f=g; * f=f+1; */ /* A¨quivalente Zugriffe auf Feldelemente */ ausgabe=f[3]; ausgabe=*(f+3); ausgabe=pf[3]; ausgabe=*(pf+3); return 0; } Bemerkung 4.7 ( zu Beispiel 4.6) 1.) Das Beispiel zeigt, dass der Zugriff auf einzelne Feldelemente f¨ur Zeiger und Felder identisch ist, obwohl es sich um unterschiedliche Datentypen handelt. 2.) Die Arithmetischen Operatoren + und - haben bei Zeigern und Feldern auch den gleichen Effekt. Der Ausdruck pf + 3 liefert als Wert (Adresse in pf) + 3 x sizeof(Typ) (und nicht (Adresse in pf) + 3 !!!) 3.) Der Zuweisungsoperator = ist f¨ur Felder nicht anwendbar, d.h. f=ausdruck ist nicht zul¨assig. Einzelne Feldelemente k¨onnen jedoch wie gewohnt manipuliert werden (z.B. f[2]=g[3] ist zul¨assig). Die Zuweisung pf=pf+1 hingegen bewirkt, dass pf nun auf f[1] zeigt. 4.) Ein weiterer Unterschied zwischen Feldvariablen und Pointervariablen ist der ben¨otigte Speicherplatz. Im Beipiel liefert sizeof(pf) den Wert 4 und sizeof(f) den Wert 16 (=4 x sizeof(float)). Die folgenden Operatoren sind auf Zeiger anwendbar : • Vergleichsoperatoren: ==, !=, <, >, <=, >= • Addition + und Subtraktion • Inkrement ++, Dekrement -- und zusammengesetzte Operatoren +=, -= 4.5 Dynamische Felder mittels Zeiger Bisher wurde die L¨ange von Feldern bereits bei der Deklaration bzw. Definition angegeben. Da viele Aufgaben und Probleme stets nach dem selben Prinzip ausgef¨uhrt werden k¨onnen, m¨ochte man die Feldl¨ange gerne als Parameter und nicht als feste Gr¨oße in die Programmierung einbeziehen. Die ben¨otigten Datenobjekte werden dann in der entsprechenden Gr¨oße und 43 damit mit entsprechend optimalem Speicherbedarf erzeugt. F¨ur Probleme dieser Art bietet C mehrere Funktionen (in der Headerdatei malloc.h), die den notwendigen Speicherplatz zur Laufzeit verwalten. Dazu z¨ahlen: malloc() calloc() realloc() free() reserviert Speicher einer bestimmten Gr¨oße reserviert Speicher einer bestimmten Gr¨oße und initialisiert die Feldelemente mit 0 erweitert einen reservierten Speicherbereich gibt den Speicherbereich wieder frei Die Funktionen malloc(), calloc() und realloc() versuchen, den angeforderten Speicher bereitzustellen (allokieren), und liefern einen Pointer auf diesen Bereich zur¨uck. Konnte die Speicheranforderung nicht erf¨ullt werden, wird ein Null-Pointer (NULL in C, d.h. Pointer zeigt auf die 0 Adresse im Speicher) zur¨uckliefert. Die Funktion free() enth¨alt als Argument einen so definierten Pointer und gibt den zugeh¨origen Speicherbereich wieder frei. Das folgende Programm demonstriert die Reservierung von Speicher durch die Funktion malloc() und die Freigabe durch free(). Beispiel 4.8 ( Norm des Vektors (1,...,n)) #include <stdio.h> #include <math.h> #include <malloc.h> int main() { int n, i; float *vektor, norm=0.0; printf("Geben Sie die Dimension n des Vektorraums an: "); scanf("%i",&n); /* Dynamische Speicher Reservierung */ vektor = (float *) malloc(n*sizeof(float)); if (vektor == NULL) /* Gen¨ ugend Speicher vorhanden? */ { printf("Nicht gen¨ ugend Speicher vorhanden \n"); return 1; /* Programm beendet sich und gibt einen Fehlerwert zurueck */ } else { /* Initialisierung des Vektors */ /* Norm des Vektors */ for (i=0;i<n;i=i+1) { 44 vektor[i]=i+1; norm=norm+vektor[i]*vektor[i]; } norm=sqrt(norm); /* Freigabe des Speichers */ free(vektor); printf("Die Norm des eingegebenen Vektors (1,...,%i) ist : %f \n" ,n,norm); } return 0; } Ein zweidimensionales dynamisches Feld l¨aßt sich einerseits durch ein eindimensionales dynamisches Feld darstellen, als auch durch einen Zeiger auf ein Feld von Zeigern. Dies sieht f¨ur eine Matrix von m Zeilen und n Spalten wie folgt aus: Beispiel 4.9 ( Dynamisches 2D Feld) #include <stdio.h> #include <malloc.h> int main() { int n,m,i,j; double **p; /* Zeiger auf Zeiger vom Typ double */ printf("Geben Sie die Anzahl der Zeilen der Matrix an: "); scanf("%i",&m); printf("Geben Sie die Anzahl der Spalten der Matrix an: "); scanf("%i",&n); /* Allokalisiert Speicher f¨ ur Zeiger auf die Zeilen der Matrix */ p=(double **) malloc(m*sizeof(double*)); for (i=0;i<m;i++) { /* Allokalisiert Speicher f¨ ur die Spalten der Matrix */ p[i]= (double *) malloc(n*sizeof(double)); } for (i=0;i<m;i++) /* Initialisierung von Matrix p */ { for (j=0;j<n;j++) { p[i][j]=(i+1)*(j+1); printf("%f ",p[i][j]); 45 } printf("\n"); } for (i=0;i<m;i++) { free(p[i]); /* Freigabe der Spalten */ } free(p); /* Freigabe der Zeilenzeiger */ return 0; } Zuerst muss der Speicher auf die Zeilenpointer allokiert werden, erst danach kann der Speicher f¨ur die einzelnen Zeilen angefordert werden. Beim Deallokieren des Speichers m¨ussen ebenfalls alle Spalten und danach alle Zeilen wieder freigegeben werden. F¨ur den Fall m=3 und n=4 veranschaulicht das Bild die Ablage der Daten im Speicher. Zeiger auf Zeiger p Zeiger auf Feld - Feld p0 - p00 p01 p02 p03 p1 - p10 p11 p12 p13 p2 - p20 p21 p22 p23 Abbildung 4.1: Dynamisches 2D Feld mit Zeiger auf Zeiger Achtung ! Es gibt keine Garantie, dass die einzelnen Zeilen der Matrix hintereinander im Speicher angeordnet sind. Somit unterscheidet sich die Speicherung des dynamischen 2D-Feldes von der Speicherung des statischen 2D-Feldes (siehe Kapitel 2.3.2), obwohl die Syntax des Elementzugriffes p[i][j] identisch ist. Daf¨ur ist diese Matrixspeicherung flexibler, da die Zeilen auch unterschiedliche L¨angen haben d¨urfen. Insbesondere findet das dynamische 2D-Feld Anwendung zur Speicherreservierung bei der Bearbeitung von d¨unnbesetzten Matrizen. 46 5 Funktionen Ein C-Programm gliedert sich ausschließlich in Funktionen. Beispiele f¨ur Funktionen wurden bereits vorgestellt: • Die Funktion main(), die den Ausf¨uhrungsbeginn des Programms markiert und somit das Hauptprogramm darstellt. • Bibliotheksfunktionen, die h¨aufiger ben¨otigte h¨ohere Funktionalit¨at bereitstellen (z.B. printf(), scanf(), sqrt(), strcpy() etc). C verf¨ugt nur u¨ ber einen sehr kleinen Sprachumfang, stellt jedoch eine Vielzahl an Funktionen in Bibliotheken f¨ur fast jeden Bedarf bereit. Was aber, wenn man eine Funktion f¨ur eine ganz spezielle Aufgabe ben¨otigt und nichts Brauchbares in den Bibliotheken vorhanden ist? Ganz einfach: Man schreibt sich diese Funktionen selbst. ¨ 5.1 Deklaration, Definition und Ruckgabewerte Die Deklaration einer Funktion hat die Gestalt Datentyp Funktionsname(Datentyp1,...,DatentypN); Die Deklaration beginnt mit dem Datentyp des R¨uckgabewertes, gefolgt vom Funktionsnamen. Es folgt eine Liste der Datentypen der Funktionsparameter . Bei der Deklaration k¨onnen auch die Namen f¨ur die Funktionsparameter vergeben werden: Datentyp Funktionsname(Datentyp1 Variable1,...,DatentypN VariableN); Die Deklaration legt jedoch nur das Allernotwendigste fest, so ist z.B. noch nichts dar¨uber gesagt, was mit den Funktionsparametern im Einzelnen geschieht und wie der R¨uckgabewert gebildet wird. Diese wichtigen Aspekte werden in der Definition der Funktion behandelt: Datentyp Funktionsname(Datentyp1 Variable1,...,DatentypN VariableN) { Deklaration der Funktionsvariablen Anweisungen } 47 Der Funktionsrumpf besteht ggf. aus Deklarationen von weiteren Variablen (z.B. f¨ur Hilfsgr¨oßen oder den R¨uckgabewert) sowie Anweisungen. Folgendes ist bei Funktionen zu beachten: • Die Deklaration bzw. Definition von Funktionen wird außerhalb jeder anderen Funktion - speziell main() - vorgenommen. ¨ • Funktionen mussen vor ihrer Verwendung zumindestens deklariert sein. Unterl¨asst man dies, so nimmt der Compiler eine implizite Deklaration mit Standardr¨uckgabewert int vor, was eine h¨aufige Ursache f¨ur Laufzeitfehler darstellt. • Deklaration/Definition k¨onnen in beliebiger Reihenfolge erfolgen. • Deklaration und Definition m¨ussen konsistent sein, d.h. die Datentypen f¨ur R¨uckgabewert und Funktionsparameter m¨ussen u¨ bereinstimmen. Beispiel 5.1 ( Funktion) #include <stdio.h> /* Deklaration der Maximum-Funktion */ /************************************/ float maximum (float, float); /************************************/ /* Deklaration globaler Variablen (nur f¨ ur den Debugger) */ /*********************************************************/ float *pa=(float *)&pa; float *pb=(float *)&pb; float *px=(float *)&px; float *py=(float *)&py; /*********************************************************/ /* Hauptprogramm */ /*****************************************************************/ int main() { float a=3.0,b=2.0; pa=&a;pb=&b; /* pa, pb Zeiger auf a bzw. b */ printf("Das Maximumvon %f und %f ist %f",a,b,maximum(a,b)); return 0; } /*****************************************************************/ /* Definition der Maximum-Funktion */ 48 /*************************************************************/ float maximum (float x, float y) { /* Die Funktion maximum() erstellt Kopien * der Funktionswerte (reserviert neuen Speicher) * und speichert sie in x bzw. y */ float maxi; px=&x;py=&y; /* px, py Zeiger auf x bzw. y */ if (x>y) maxi=x; else maxi=y; return maxi; /* R¨ uckgabewert der Funktion */ } /*************************************************************/ 5.2 Lokale und globale Variablen Variablen lassen sich nach ihrem G¨ultigkeitsbereich unterteilen: lokale Variablen: Sie gelten in dem Anweisungsblock (z.B. Funktionenrumpf oder Schlei- fenrumpf), in dem sie deklariert wurden. F¨ur diesen Block gelten sie als lokal. Bei der Ausf¨uhrung des Programms existieren die Variablen bis zum Verlassen des Anweisungsblocks. globale Variablen: Sie werden außerhalb aller Funktionen deklariert/definiert (z.B. direkt nach den Pr¨aprozessordirektiven) und sind zun¨achst im gesamten Programm einschließlich aller Subroutinen g¨ultig. Dies bedeutet speziell, dass jede Funktion sie ver¨andern kann. (Achtung: unvorhergesehener Programmablauf m¨oglich.) Variablen die auf einer h¨oheren Ebene (global oder im Rumpf einer aufrufenden Funktion) deklariert/definiert wurden, k¨onnen durch Deklaration gleichnamiger lokaler Variablen im Rumpf einer aufgerufenen Funktion verdeckt“ werden. In diesem Zusammenhang spricht ” man von G¨ultigkeit bzw. Sichtbarkeit von Variablen. ¨ Beispiel 5.2 ( Gultigkeitsbereich von Variablen) /***********************************************/ /* Im Beispiel wird 4 x die Variable a deklariert * Mit Hilfe eines Debuggers und der Zeiger * pa_global, pa_main , pa_sub_main und pa_summe * kann jeweils die Adresse und deren zugeh¨ origer * Inhalt angezeigt werden */ /***********************************************/ 49 /* Deklaration der Funktion summe() */ /************************************/ int summe (int, int); /************************************/ /* Deklaration globaler Variablen */ /*************************************************/ int a=1; /* globales a =1 */ int *pa_global=(int *) &pa_global; int *pa_main=(int *) &pa_main; int *pa_sub_main=(int *) &pa_sub_main; int *pa_summe=(int *) &pa_summe; /*************************************************/ int main() { pa_global=&a; /* Zeiger auf globales a */ int a=2; /* a in main() = 2 und * ¨ uberdeckt globales a*/ pa_main=&a; /* Zeiger auf a in main() */ { int a=2; /* lokales a in main() = 2 und * ¨ uberdeckt a in main() */ pa_sub_main=&a; /* Zeiger auf lokales a in main() */ a=a+1; /* lokales a in main() wird um 1 erh¨ oht */ a=summe(a,a); /* lokales a in main() = 2 x lokales a in main() * Gleichzeitig wird das globale a in * der Funktion summe um 1 erh¨ oht */ /* lokales a in main() wird gel¨ oscht */ } a=summe(a,a); /* a in main = 2 x lokales a in main() * Gleichzeitig wird das globale a in * der Funktion summe um 1 erh¨ oht */ return 0; } /* Definition Summen-Funktion */ /****************************************************************/ int summe (int x, int y) { 50 /* Die Funktion summe() erstellt Kopien * der Funktionswerte (reserviert neuen Speicher) * und speichert sie in x bzw. y */ a=a+1; /* globales a wird um 1 erh¨ oht */ int a=0; /* a in der Funktion summe() */ pa_summe=&a; /* Zeiger auf a in der Funkton summe */ a=x+y; /* a in der Funktion summe = x+y */ return a; /* a in der Funktion wird zur¨ uckgegeben */ /* a,x,y in der Funktion summe werden gel¨ oscht */ } /****************************************************************/ 5.3 Call by value Die Standard¨ubergabe von Funktionsparametern geschieht folgendermaßen: An die Funktion werden Kopien der Variablen als Parameter u¨ bergeben und von dieser zur Verarbeitung genutzt. Die urspr¨ungliche Variable bleibt von den in der Funktion vorgenommenen Manipulationen unber¨uhrt (es sei denn, sie wird durch den R¨uckgabewert der Funktion u¨ berschrieben). Beispiel 5.3 ( Call by value I) #include <stdio.h> /* Deklaration der Funktion setze() */ /************************************/ void setze (int); /************************************/ /* Deklaration globaler Variablen (nur f¨ ur den Debugger) */ /*********************************************************/ int *pb=(int *)&pb; int *pb_setze=(int *)&pb_setze; /*********************************************************/ /* Hauptprogramm */ /*****************************/ int main() { 51 int b=0;pb=&b; setze(b); printf("b=%i\n",b); return 0; } /*****************************/ /* Definition der Funktion setze () */ /************************************/ void setze (int b) { b=3;pb_setze=&b; } /************************************/ Die Ausgabe lautet: b=0 Die Funktion setze() hat nur eine Kopie der Variablen b als Parameter erhalten und auf einen neuen Wert gesetzt. Die eigentliche Variable behielt ihren Wert. Beispiel 5.4 ( Call by value II) #include <stdio.h> /* Deklaration der Funktion setze() */ /************************************/ int setze (int); /************************************/ /* Deklaration globaler Variablen (nur f¨ ur den Debugger) */ /*********************************************************/ int *pb=(int *)&pb; int *pb_setze=(int *)&pb_setze; /*********************************************************/ /* Hauptprogramm */ /*****************************/ int main() { int b=0;pb=&b; b=setze(b); printf("b=%i\n",b); return 0; } 52 /*****************************/ /* Definition der Funktion setze () */ /************************************/ int setze (int b) { b=3;pb_setze=&b; return b; } /************************************/ Die Ausgabe lautet: b=3 Die Funktion setze() hat wieder nur eine Kopie der Variablen b als Parameter erhalten und auf einen neuen Wert gesetzt. Durch das Zur¨uckliefern und die Zuweisung an die eigentliche ¨ Variable b wurde die Anderung wirksam. 5.4 Call by reference Bei call by reference wird der Funktion nicht eine Kopie der Variablen selbst, sondern in Form eines Pointers auf die Variable eine Kopie der Adresse der Variablen u¨ bergeben. ¨ Uber die Kenntnis der Variablenadresse kann die Funktion den Variableninhalt manipulieren. Hierzu kommen beim Aufruf der Funktion der Adressoperator und im Funktionsrumpf der Inhaltsoperator in geeigneter Weise zum Einsatz. Beispiel 5.5 ( Call by reference) #include <stdio.h> /* Deklaration der Funktion setze() */ /************************************/ void setze (int *); /************************************/ /* Deklaration globaler Variablen (nur f¨ ur den Debugger) */ /*********************************************************/ int *pb=(int *)&pb; /*********************************************************/ /* Hauptprogramm */ /*****************************/ int main() 53 { int b=0;pb=&b; setze(&b); printf("b=%i\n",b); return 0; } /*****************************/ /* Definition der Funktion setze () */ /************************************/ void setze (int *b) { *b=3; } /************************************/ Die Ausgabe lautet: b=3 Der Funktion setze() wird ein Zeiger auf eine int-Variable u¨ bergeben und sie verwendet den Inhaltsoperator *, um den Wert der entsprechenden Variablen zu ver¨andern. Im Hauptprogramm wird der Zeiger mit Hilfe des Adressoperators & erzeugt. 5.5 Rekursive Programmierung Bisher haben Funktionen ihre Aufgabe in einem Durchgang komplett erledigt. Eine Funktion kann ihre Aufgabe aber manchmal auch dadurch erledigen, dass sie sich selbst mehrmals aufruft und jedesmal nur eine Teilaufgabe l¨ost. Das f¨uhrt zu rekursiven Aufrufen. Das folgende Programm berechnet xk (x ∈ R, k ∈ N) rekursiv. Beispiel 5.6 ( Rekursive Programmierung) #include <stdio.h> /* Deklaration potenz-Funktion */ double potenz (double, int); /* Hauptprogramm */ int main() { double x; int k; printf("Zahl x : "); scanf("%lf",&x); printf("Potenz k : "); scanf("%i",&k); 54 printf("xˆk = %f \n",potenz(x,k)); } /* Definition potenz-Funktion */ double potenz (double x, int k) { if (k<0) /* Falls k < 0 berechne (1/x)ˆ(-k) */ { return potenz(1.0/x,-k); } else { if (k==0) /* Rekursionsende */ { return 1; } else { return x*potenz(x,k-1); /* Rekursionsaufruf */ } } } Die Funktion potenz ruft sich solange selbst auf, bis der Fall k == 0 eintritt. Das Ergebnis dieses Falls liefert sie an die aufrufende Funktion zur¨uck. Achtung Bei der rekursiven Programmierung ist stets darauf zu achten, dass der Fall des Rekursionsabbruchs (im Beispiel k==0) immer erreicht wird, da sonst die Maschine bis zur Unendlichkeit rechnet. 5.6 Kommandozeilen-Parameter Ausf¨uhrbaren Programmen k¨onnen beim Aufruf Parameter u¨ bergeben werden, indem man nach dem Programmnamen eine Liste der Parameter (Kommandozeilen-Parameter) anf¨ugt. In C-Programmen k¨onnen so der Funktion main Parameter u¨ bergeben werden. Zur Illustration wird folgendes Beispiel betrachtet. Beispiel 5.7 /* /* /* /* /* /* 1 2 3 4 5 6 */ # include <stdio.h> */ */ int main(int argc, char* argv[]) */ { */ int zaehler; */ float zahl,summe=0; 55 /* 7 */ /* 8 */ / *9 */ /* 10 */ /* 11 */ /* 12 */ /* 13 */ /* 14 */ /* 15 */ /* 16 */ /* 17 */ /* 18 */ /* 19 */ /* 20 */ /* 21 */ /* 22 */ } for (zaehler=0;zaehler < argc ; zaehler++) { printf("Parameter %i = %s \n",zaehler,argv[zaehler]); } printf("\n"); for (zaehler=1;zaehler < argc ; zaehler++) { sscanf(argv[zaehler],"%f",&zahl); summe=summe+zahl; } printf("Die Summe der Kommandozeilen-Parameter : %f\n",summe); return 0; Nach dem Start des obigen Programms zum Beispiel durch beispiel5_7.out 1.4 3.2 4.5 erscheint folgende Ausgabe auf dem Bildschirm Parameter Parameter Parameter Parameter 0 1 2 3 = = = = beispiel5_7 1.4 3.2 4.5 Die Summe der Kommandozeilen-Parameter : 9.100000 Die Angaben in der Kommandozeile sind an das Programm u¨ bergeben worden und konnten hier auch verarbeitet werde. Zeile 3: main erh¨alt vom Betriebssystem zwei Parameter. Die Variabel argc enth¨alt die Anzahl der u¨ bergebenen Parameter und argv[] die Parameter selbst. Die Namen der Variablen argc und argv sind nat¨urlich frei w¨ahlbar, es hat sich jedoch eingeb¨urgert, die hier verwendeten Bezeichnungen zu benutzen. Sie leiten sich von argument count und argument values ab. Bei argc ist eine Besonderheit zu beachten. Hat diese Variable zum Beispiel den Wert 1, so bedeutet das, daß kein Kommandozeilen-Parameter eingegeben wurde. Das Betriebssystem u¨ bergibt als ersten Parameter n¨amlich grunds¨atzlich den Namen des Programms selbst. Also erst wenn argc gr¨oßer als 1, wurde wirklich ein Parameter eingegeben. Die Deklaration char *argv[] bedeutet: Zeiger auf Zeiger auf Zeichen. (Man h¨atte auch char **argv schreiben k¨onnen). Mit anderen Worten: argv ist ein Zeiger, der auf ein Feld zeigt, das wiederum Zeiger enth¨alt. Diese Pointer zeigen schließlich auf die einzelnen KommandozeilenParameter. Die leeren eckigen Klammern weisen darauf hin, daß es sich um ein Feld unbestimmter Gr¨oße handelt. Die einzelnen Argumente k¨onnen durch Indizierung von argv ange- 56 sprochen werden. argv[1] zeigt also auf das erste Argument (”1.4”), argv[2] auf ”3.2” usw. Zeile 15: Da es sich bei argv[i] um Strings handelt m¨ussen die Eingabeparameter eventuell (je nach ihrer Bestimmung) in einen anderen Typ umgewandelt werden. Dies geschieht in diesem Beispiel mit Hilfe des sscanf -Befehls. 5.7 Wie werden Deklarationen gelesen Eine Deklaration besteht grunds¨atzlich aus einem Bezeichner (Variablennamen oder Funktionsnamen), der durch einen oder mehrere Zeiger, Feld- oder Funktions-Modifikatoren beschrieben wird. Wenn mehrere solcher Modifikatoren miteinander kombinieren, muß man darauf achten, daß Funktionen keine Funktionen oder Felder zur¨uckgeben k¨onnen und daß Felder auch keine Funktionen als Elemente haben k¨onnen. Ansonsten sind alle Kombinationen erlaubt. Dabei haben Funktions- und Array-Modifikatoren Vorrang vor Zeiger-Modifikatoren. Durch Klammerung kann diese Rangfolge ge¨andert werden. Bei der Interpretation beginnt man am besten beim Bezeichner und liest nach rechts bis zum Ende usw. bis zu einer einzelnen rechten Klammer. Dann f¨ahrt man links vom Bezeichner mit evtl. vorhandenen Zeiger-Modifikatoren fort, bis das Ende oder eine einzelne linke Klammer erreicht wird. Dieses Verfahren wird f¨ur jede geschachtelte Klammer von innen nach außen wiederholt. Zum Schluß wird der Typ-Kennzeichner gelesen. Beispiel 5.8 char |{z} ∗ (|{z} ∗ (|{z} ∗ Bezeichner ( ) ) |{z} [20] |{z} | {z }) |{z} 7 6 4 2 1 3 5 Bezeichner (1) ist hier ein Zeiger (2) auf eine Funktion (3), die einen Zeiger (4) auf ein Feld mit 20 Elementen(5) zur¨uckgibt, die Zeiger (6) auf char-Werte(7) sind! Die folgenden vier Beispiele sollen noch einmal den Einsatz von Klammern verdeutlichen: (i) (ii) (iii) char * a[10] char (* a)[10] char *a(int) (iv) char (*a)(int) a ist ein Feld der Gr¨oße 10 mit Zeigern auf char-Werte a ist Zeiger auf ein Feld der Gr¨oße 10 mit char-Werte a ist Funktion, die als Eingabeparameter einen int-Wert verlangt und einen Zeiger auf char-Wert zur¨uckgibt a ist Zeiger, auf eine Funktion die als Eingabeparameter einen int-Wert verlangt und einen char-Wert zur¨uckgibt 5.8 Zeiger auf Funktionen Manchmal ist es n¨utzlich Funktion an Funktionen zu u¨ bergeben. Dies kann mit Hilfe von Zeigern auf Funktionen realisiert werden. Im folgenden Beispiel (5.9) wird der Funktion trapez regel ein Zeiger auf die zu integrierende Funktion mitgeliefert. Dadurch ist es m¨oglich 57 beliebige Funktionen mit Hilfe der Trapez-Regel numerisch zu integrieren. Die Intervallgrenzen und Anzahl der St¨utzstellen sollen dem Programm durch KommandozeilenParameter u¨ bergeben werden. Beispiel 5.9 # include <stdio.h> # include <math.h> /* Deklaration der Funktion trapez_regel() */ /*************************************************************/ double trapez_regel(double (*f)(double ),double ,double ,int ); /* Eingabeparameter : 1.) Zeiger auf Funktion mit * Eingabeparameter double-Wert * und double-R¨ uckgabewert * 2.) double f¨ ur linke Intervallgrenze * 3.) double f¨ ur rechte Intervallgrenze * 4.) int f¨ ur Anzahl der St¨ utzstellen * R¨ uckgabewert : double f¨ ur das Integral **************************************************************/ /* Hauptprogramm */ /************************************************************************/ int main(int argc,char** argv) { int n; double a,b,integral; /* Zeiger auf eine Funktion die als R¨ uckgabewert * eine double-Variable besitzt und als * Eingabe eine double-Variable verlangt */ double (*fptr)(double); if (argc<4) { printf("Programm ben¨ otigt 3 Kommandozeilenparameter :\n"); printf("1.) Linker Intervallrand (double)\n"); printf("2.) Rechter Intervallrand (double)\n"); printf("3.) Anzahl der Teilintervalle f¨ ur"); printf(" numerische Integration (int)\n"); return 1; } else { sscanf(argv[1],"%lf",&a); 58 sscanf(argv[2],"%lf",&b); sscanf(argv[3],"%i",&n); fptr=(double(*)(double)) cos;/* Zeiger fptr auf cos-Funktion */ integral=trapez_regel(fptr,a,b,n); printf("Das Integral der cos-Funktion ¨ uber das Intervall"); printf(" [%f , %f]\n",a,b); printf("betr¨ agt : \t %f (numerisch mit %i",integral,n+1); printf(" St¨ utzstellen)\n"); printf(" \t %f (exakt)\n\n",sin(b)-sin(a)); fptr=(double (*)(double)) sin;/*Zeiger fptr auf sin-Funktion */ integral=trapez_regel(fptr,a,b,n); printf("Das Integral der sin-Funktion ¨ uber das Intervall"); printf(" [%f , %f]\n",a,b); printf("betr¨ agt : \t %f (numerisch mit %i",integral,n+1); printf(" St¨ utzstellen)\n"); printf(" \t %f (exakt)\n\n",cos(a)-cos(b)); return 0; } } /************************************************************************/ /* Definition der Funktion trapez_regel() */ /*******************************************************************/ double trapez_regel(double (*f)(double ),double a,double b,int n) { n=n+1; int k; double h=(b-a)/n; double integral=0; for (k=0;k<=n-1;k++) integral=integral+h/2*(f(a+k*h)+f(a+(k+1)*h)); return integral; } /*******************************************************************/ 59 6 Strukturierte Datentypen ¨ Uber die normalen Datentypen hinaus gibt es in C weitere, komplexere Typen, die sich aus den einfacheren zusammensetzen. Außerdem besteht die M¨oglichkeit, eigene Synonyme f¨ur h¨aufig verwendete Typen festzulegen. Feld (array) Struktur (struct) Union (union) Aufz¨ahlungstyp (enum) Zusammenfassung von Elementen gleichen Typs Zusammenfassung von Elementen verschiedenen Typs ¨ Uberlagerung mehrerer Komponenten verschiedenen Typs auf dem gleichen Speicherplatz Grunddatentyp mit frei w¨ahlbarem Wertebereich Der Typ Feld wurde bereits in Kapitel 2.3 vorgestellt. 6.1 Strukturen Die Struktur definiert einen neuen Datentyp, welcher Komponenten unterschiedlichen Typs vereint. Die L¨ange einer solchen Struktur ist gleich der Gesamtl¨ange der einzelnen Bestandteile. Angewendet werden Strukturen h¨aufig dann, wenn verschiedene Variablen logisch zusammengeh¨oren, wie zum Beispiel Name, Vorname, Straße etc. bei der Bearbeitung von Adressen. Die Verwendung von Strukturen ist nicht zwingend n¨otig, man kann sie auch durch die Benutzung von mehreren einzelnen Variablen ersetzen. Sie bieten bei der Programmierung jedoch ¨ einige Vorteile, da sie die Ubersichtlichkeit erh¨ohen und die Bearbeitung vereinfachen. 6.1.1 Deklaration von Strukturen Zur Deklaration einer Struktur benutzt man das Schl¨usselwort struct. Ihm folgt der Name der Struktur. In geschweiften Klammern werden dann die einzelnen Variablen aufgef¨uhrt, aus denen die Struktur bestehen soll. Achtung: Die Variablen innerhalb einer Stuktur k¨onnen nicht initialisiert werden. struct Strukturname Datendeklaration ; Im folgenden Beispiel wird eine Stuktur mit Namen Student deklariert, die aus einer intVariablen und einem String besteht. 60 Beispiel 6.1 ( Deklaration einer Struktur) struct Student { int matrikel; char name[16]; }; 6.1.2 Definition von Strukturvariablen Die Strukturschablone selbst hat noch keinen Speicherplatz belegt, sie hat lediglich ein Muster festgelegt, mit dem die eigentlichen Variablen definiert werden. Die Definition erfolgt genau wie bei den Standardvariablen (int a, double summe etc.), indem man den Variablentyp (struct Student) gefolgt von einem oder mehreren Variablennamen angibt, zum Beispiel: struct Student peter, paul; Hierdurch werden zwei Variablen peter, paul deklariert, die beide vom Typ struct Student sind. Neben dieser beschriebenen Methode gibt es noch eine weitere Form der Definition von Strukturvariablen: Beispiel 6.2 ( Deklaration einer Struktur + Definition der Strukturvariablen) struct Student { int matrikel; char name [16]; } peter, paul; In diesem Fall erfolgt die Definition der Variablen zusammen mit der Deklaration. Dazu m¨ussen nur eine oder mehrere Variablen direkt hinter die Deklaration gesetzt werden. 6.1.3 Felder von Strukturen Eine Struktur wird h¨aufig nicht nur zur Aufnahme eines einzelnen Datensatzes, sondern zur Speicherung vieler gleichartiger S¨atze verwendet, beispielsweise mit struct Student Studenten2005[1000]; Der Zugriff auf einzelne Feldelemente erfolgt wie gewohnt mit eckigen Klammern []. 6.1.4 Zugriff auf Strukturen Um Strukturen sinnvoll nutzen zu k¨onnen, muss man ihren Elementen Werte zuweisen und auf diese Werte auch wieder zugreifen k¨onnen. Zu diesem Zweck kennt C den Strukturoperator ’.’, mit dem jeder Bestandteil einer Struktur direkt angesprochen werden kann. 61 Beispiel 6.3 ( Deklaration + Definition + Zuweisung) #include <stdio.h> #include <string.h> /* F¨ ur strcpy */ int main() { /* Deklaration der Struktur Student */ struct Student { int matrikel; char Vorname[16]; }; /* Definition eines Feldes der Struktur Student */ struct Student Mathe[100]; /* Zugriff auf die einzelnen Elemente */ Mathe[0].matrikel=242834; strcpy(Mathe[0].Vorname,"Peter"); Mathe[1].matrikel=343334; strcpy(Mathe[1].Vorname,"Paul"); /* Der Zuweisungsoperator = ist auf Strukturen gleichen Typs anwendbar */ Mathe[2]=Mathe[1]; printf("Vorname von Student 2: %s",Mathe[2].Vorname); } return 0; 6.1.5 Zugriff auf Strukturen mit Zeigern Wurde eine Struktur deklariert, so kann man auch einen Zeiger auf diesen Typ wie gewohnt definieren, wie zum Beispiel struct Student *p; Der Zugriff auf den Inhalt der Adresse, auf die der Zeiger verweist, erfolgt wie u¨ blich durch den Zugriffsoperator *. Wahlweise kann auch der Auswahloperator -> benutzt werden. 62 Beispiel 6.4 ( Zugriff mit Zeigern) #include <stdio.h> #include <string.h> /* F¨ ur strcpy */ int main() { /* Deklaration der Struktur Student */ struct Student { int matrikel; char Strasse[16]; }; /* Definition Strukturvariablen peter, paul */ struct Student peter, paul; struct Student *p; /* Zeiger auf Struktur Student */ p=&peter; /* p zeigt auf peter */ /* Zugriff auf die einzelnen Elemente */ (*p).matrikel=242834; strcpy((*p).Strasse,"Finkenweg 4"); /* Alternativ */ p=&paul; p->matrikel=423323; strcpy(p->Strasse,"Dorfgosse 2"); printf("Paul, Matr.-Nr.: %i , Str.: %s " ,paul.matrikel,paul.Strasse); return 0; } 6.1.6 Geschachtelte Strukturen Bei der Deklaration von Strukturen kann innerhalb einer Struktur eine weitere Struktur eingebettet werden. Dabei kann es sich um eine bereits an einer anderen Stelle deklarierte Struktur handeln, oder man verwendet an der entsprechenden Stelle nochmals das Schl¨usselwort struct und deklariert eine Struktur innerhalb der anderen. Das Programm 6.5 zeigt ein Beispiel f¨ur eine geschachtelte Struktur. 63 Beispiel 6.5 ( Geschachtelte Strukturen) #include <stdio.h> int main() { struct Punkt3D { float x; float y; float z; }; struct Strecke3D { struct Punkt3D anfangspunkt; struct Punkt3D endpunkt; }; struct Strecke3D s; /* Initialisierung einer Strecke */ s.anfangspunkt.x=1.0; s.anfangspunkt.y=-1.0; s.anfangspunkt.z=2.0; s.endpunkt.x=1.1; s.endpunkt.y=1.2; s.endpunkt.z=1.4; printf("%f \n",s.endpunkt.z); return 0; } 6.1.7 Listen Wie in Abschnitt 6.1.6 bereits gezeigt wurde, k¨onnen Strukturen auch andere (zuvor deklarierte) Strukturen als Komponenten enthalten. Eine Struktur darf sich aber nicht selbst als Variable enthalten!. Allerdings darf eine Struktur einen Zeiger auf sich selbst als Komponente beinhalten. Diese Datenstrukturen kommen zum Einsatz, wenn man nicht im voraus wissen kann, wieviel Speicher man f¨ur eine Liste von Datens¨atzen reservieren muss und daher die Verwendung von Feldern unzweckm¨aßig ist. 64 Beispiel 6.6 ( Liste) #include <stdio.h> #include <string.h> int main() { struct Student { char name[16]; char familienstand[16]; char geschlecht[16]; struct Student *next; }; struct Student Mathe[2]; struct Student Bio[1]; struct Student *startzeiger,*eintrag; /* Initialisierung der Mathe-Liste */ strcpy(Mathe[0].name,"Kerstin"); strcpy(Mathe[0].familienstand,"ledig\t"); strcpy(Mathe[0].geschlecht,"weiblich"); Mathe[0].next=&Mathe[1]; /* next zeigt auf den n¨ achsten Eintag */ strcpy(Mathe[1].name,"Claudia"); strcpy(Mathe[1].familienstand,"verheiratet"); strcpy(Mathe[1].geschlecht,"weiblich"); Mathe[1].next=NULL; /* next zeigt auf NULL, d.h Listenende */ /* Initialisierung der Bio-Liste */ strcpy(Bio[0].name,"Peter"); strcpy(Bio[0].familienstand,"geschieden"); annlich"); strcpy(Bio[0].geschlecht,"m¨ Bio[0].next=NULL; /* next zeigt auf NULL, d.h Listenende */ /* Ausgabe der Mathe-Liste */ startzeiger=&Mathe[0]; printf("Name\tFamilienstand\tGeschlecht\n\n"); for (eintrag=startzeiger;eintrag!=NULL;eintrag=eintrag->next) { printf("%s\t%s\t%s\n" ,eintrag->name,eintrag->familienstand,eintrag->geschlecht); } /* Anh¨ angen der Bio-Liste an die Mathe-Liste */ 65 Mathe[1].next=&Bio[0]; /* Ausgabe der Mathe-Bio-Liste */ printf("\n\nName\tFamilienstand\tGeschlecht\n\n"); for (eintrag=startzeiger;eintrag!=NULL;eintrag=eintrag->next) { printf("%s\t%s\t%s\n" ,eintrag->name,eintrag->familienstand,eintrag->geschlecht); } return 0; } 6.2 Unions W¨ahrend die Struktur sich dadurch auszeichnet, dass sie sich aus mehreren verschiedenen Datentypen zusammensetzt, ist das Charakteristische an Unions, dass sie zu verschiedenen Zeitpunkten jeweils einen bestimmten Datentyp aufnehmen k¨onnen. Beispiel 6.7 ( Union) Um die Klausurergebnisse von Studierenden zu speichern, m¨usste normalerweise zwischen Zahlen (Notenwerte im Falle des Bestehens) und Zeichenketten ( nicht bestanden“) unter” schieden werden. Verwendet man eine Unionvariable so kann man die jeweilige Situation flexibel handhaben #include <stdio.h> #include <string.h> int main() { union klausurresultat { float note; char nichtbestanden[16]; }; union klausurresultat ergebnis, *ergebnispointer; /* Zugriff mit pointer */ ergebnispointer=&ergebnis; ergebnispointer->note=1.7; strcpy(ergebnispointer->nichtbestanden,"nicht bestanden"); printf("%s\n",ergebnispointer->nichtbestanden); 66 /* Zugriff ohne pointer */ ergebnis.note=3.3; strcpy(ergebnis.nichtbestanden,"nicht bestanden"); printf("%s\n",ergebnis.nichtbestanden); return 0; } Der Speicherplatzbedarf einer Union richtet sich nach der gr¨oßten Komponente (im Beispiel: 16 x sizeof(char) = 16 Byte). Gr¨unde f¨ur das Benutzen von Unions fallen nicht so stark ins Auge wie bei Strukturen. Im wesentlichen sind es zwei Anwendungsf¨alle, in denen man sie einsetzt: • Man m¨ochte auf einen Speicherbereich auf unterschiedliche Weise zugreifen. Dies k¨onnte bei der obigen Union doppelt der Fall sein. Hier kann man mit Hilfe der Komponente nichtbestanden auf die einzelnen Bytes der Komponente note zugreifen. • Man benutzt in einer Struktur einen Bereich f¨ur verschiedene Aufgaben. Sollen beispielsweise in einer Struktur Mitarbeiterdaten gespeichert werde, so kann es sein, dass f¨ur den einen Angaben zum Stundenlohn in der Struktur vorhanden sein sollen, w¨ahrend der andere Speicherplatz f¨ur ein Monatsgehalt und der Dritte noch zus¨atzlich Angaben u¨ ber Provisionen ben¨otigt. Damit man nun nicht alle Varianten in die Struktur einbauen muss, wobei jeweils zwei unbenutzt blieben, definiert man einen Speicherbereich, in dem die jeweils ben¨otigten Informationen abgelegt werden, als Union. ¨ 6.3 Aufzahlungstyp Der Aufz¨ahlungstyp ist ein Grundtyp mit frei w¨ahlbarem Wertebereich. Veranschaulicht wird dies am Beipiel der Wochtage. Beispiel 6.8 ( Aufz¨ahlungstyp) #include <stdio.h> #include <string.h> int main() { enum tag { montag, dienstag, mittwoch, donnerstag, freitag, samstag, sonntag }; enum tag wochentag; wochentag=montag; 67 if (wochentag==montag) printf("Schlechte Laune"); return 0; } 6.4 Allgemeine Typendefinition Das Schl¨usselwort typedef definiert neue Namen f¨ur bestehende Datentypen. Es erlaubt eine k¨urzere Schreibweise bei aufwendigen Deklarationen und kann Datentypen auf Wunsch aussagekr¨aftigere Namen geben. Die Syntax f¨ur die Definition eines neuen Datentypnamens sieht wie folgt aus: typedef typ Variablenname; Beispiel 6.9 ( Typendefinition) #include <stdio.h> #include <string.h> int main() { struct vektor { float x; float y; float z; }; typedef char text[100]; typedef struct vektor punkt; /* Deklaration der Variablen p und nachricht */ punkt p; text nachricht; /* Initialisierung der Variablen p und nachricht */ p.x=1.0;p.y=0.0;p.z=0.0; strcpy(nachricht,"nachricht ist eine Variable vom Typ text"); printf("%s\n",nachricht); return 0; } Interessanterweise ist eine Variable vom Typ text nunmehr stets eine Zeichenkette der (max.) L¨ange 100. Weiterhin zeigt das Beispiel, dass auch strukturierten Datentypen eigene Typdefinitionen zugewiesen werden k¨onnen. 68 7 Arbeiten mit Dateien Viele der Programme, die bisher vorgestellt wurden, erlaubten es, Daten einzugeben, zu bearbeiten und wieder auszugeben. Nach dem Programmende waren diese Zahlen und Texte dann allerdings wieder verloren. Was also noch fehlt, ist die M¨oglichkeit, die Daten auf Diskette oder Platte dauerhaft zu sichern. ¨ 7.1 Dateien offnen und schließen Der erste Schritt beim Arbeiten mit einer Datei besteht darin, diese Datei zu o¨ ffnen. Diese ¨ Aufgabe wird mit der Bibliotheksfunktion fopen() erledigt. Nach dem Offnen kann der Inhalt bearbeitet werden. Ist dies beendet, wird die Datei mit der Funktion fclose() wieder geschlossen. Die Funktionen fclose() und fopen() werden in der Headerdatei stdio.h deklariert. F¨ur die Arbeit mit Dateien, allgemeiner gesagt mit Datenstr¨omen, gibt es in C den Datentyp FILE. Dieser Typ wird in der Headerdatei stdio.h definiert. fopen() Die Funktionsbibliothek deklariert fopen() folgendermaßen: FILE * fopen (char *Dateiname, char *Modus); Diese Beschreibung wird folgendermaßen interpretiert: • Die Funktion heißt fopen. • Sie hat als R¨uckgabewert einen Pointer vom Typ FILE. • Der erste Eingabeparameter erh¨alt einen Zeiger auf den String des Dateinamens. • Der zweite Eingabeparameter erh¨alt einen Zeiger auf den String des Bearbeitungsmodus. Er legt fest, wie auf die Datei zugegriffen werden kann. Als Bearbeitungsmodi sind g¨ultig: • “r“ o¨ ffnet die Datei zum Lesen. • “w“ o¨ ffnet die Datei zum Schreiben. Wenn die Datei existiert, wird sie u¨ berschrieben, andernfalls neu angelegt. • “a“ o¨ ffnet die Datei zum Schreiben am Ende der Datei (Anh¨angen). Sie wird neu angelegt, falls sie noch nicht existiert. 69 • “rb“, “wb“, “ab“ o¨ ffnet die Datei wie oben f¨ur bin¨are Ein- und Ausgabe im Gegensatz zur zeichenorientierten Ein- und Ausgabe. • Das zus¨atzliche Anh¨angen von +“ gibt die Erlaubnis, eine Datei gleichzeitig zu mani” pulieren und zu lesen. ¨ Schl¨agt das Offnen einer Datei fehl, so liefert die Funktion fopen() den NULL-Pointer als R¨uckgabewert. Die Funktion fclose() liefert den Wert 0 zur¨uck, falls eine Datei erfolgreich geschlossen werden konnte. Um sicher zu sein, dass eine Datei wirklich geschlossen ist, sollte man stets vor Beendigung des Programms alle mit fopen() ge¨offneten Datenstr¨ome auch wieder schließen. Die folgenden speziellen Datenstr¨ome sind vordefiniert und m¨ussen nicht eigens ge¨offnet werden: • stdin: Standardeingabe • stdout: Standardausgabe • stderr: Standardfehlerausgabe Das Beispiel 7.1 o¨ ffnet die Dateien test1.txt und test2.txt und schließt sie anschließend wieder. Falls die Dateien bis dahin noch nicht im Verzeichnis waren, so existieren sie nach dem Programmende. Es sind allerdings Dateien ohne Inhalt. ¨ Beispiel 7.1 ( Offnen und schließen von Dateien) #include <stdio.h> int main() { FILE * datei_ptr; /* Zeiger auf Datei (Datenstrom) */ char dateiname[]="test1.txt"; /* O¨ffnen der Datei "test1.txt" zum schreiben*/ datei_ptr=fopen(dateiname,"w"); fclose(datei_ptr); /* Schließen der Datei "test1.txt" */ dateiname[4]=’2’; /* O¨ffnen der Datei "test2.txt" zum schreiben*/ datei_ptr=fopen(dateiname,"w"); fclose(datei_ptr); /* Schließen der Datei "test2.txt" */ return 0; } 70 ¨ 7.2 Existenz einer Datei prufen Manchmal ist es f¨ur ein Programm nur wichtig, ob eine bestimmte Datei existiert. Da es f¨ur diese Aufgabe in der Standardbibliothek keine Funktion gibt, muss man sich mit einem Trick behelfen, den auch das folgende Programm benutzt. ¨ Beispiel 7.2 ( Existenz von Dateien prufen) #include <stdio.h> int main() { FILE * datei_ptr; /* Zeiger auf Datei (Datenstrom) */ char dateiname[]="test1.txt"; /* O¨ffnen der Datei "test1.txt" zum lesen*/ datei_ptr=fopen(dateiname,"r"); if (datei_ptr==NULL) /* Fehler beim O¨ffnen ? */ { printf("Die Datei %s existiert nicht\n",dateiname); } else { printf("Die Datei %s wurde erfolgreich ge¨ offnet\n",dateiname); fclose(datei_ptr); /* Schließen der Datei "test1.txt" */ } return 0; } Die zu u¨ berpr¨ufende Datei test1.txt“ wird zum Lesen ge¨offnet. Schl¨agt der Versuch fehl, so ” liefert die Funktion fopen() den NULL-Zeiger zur¨uck. 7.3 Zeichenorientierte Ein- und Ausgabe Die folgenden in stdio.h deklarierten Funktionen dienen dem zeichen- bzw. string-orientierten Austausch von Daten eines Programms mit Dateien. fprintf Deklaration: int fprintf (FILE * Dateiname, const char* Formatstring,...); Beschreibung: Verh¨alt sich wie printf(), mit dem Unterschied, dass die einzelnen Zei- chen des Strings nicht auf den Bildschirm, sondern in den Datenstrom (Datei) geschrieben werden. Es gilt : printf(. . . ) = fprintf(stdout,. . . ); fscanf 71 Deklaration: int fscanf (FILE * Dateiname, const char* Formatstring,...); Beschreibung: Verh¨alt sich wie scanf(), mit dem Unterschied, dass die einzelnen Zei- chen des Strings nicht vom Bildschirm, sondern aus dem Datenstrom (Datei) eingelesen werden. Es gilt : scanf(. . . )=fscanf(stdin,. . . ); fputc() Deklaration: int fputc (int zeichen, FILE * Dateiname); Beschreibung: Schreibt ein Zeichen in den Datenstrom (Datei) und liefert bei Erfolg das Zeichen zur¨uck. fgetc() Deklaration: int fgetc (FILE * Dateiname); Beschreibung: Liest ein Zeichen aus dem Datenstrom (Datei) und liefert bei Erfolg das Zeichen zur¨uck. Bei erneutem Aufruf wird das n¨achste Zeichen von dort eingelesen. Beispiel 7.3 (Lesen und Speichern von Matrizen) Programmbeschreibung Das Beispiel zeigt, wie man Matrizen in Dateien schreibt und sie wieder ausliest. Die Dimensionen der Matrizen werden durch Pr¨aprozessorvariablen realisiert. Der Name der zu betrachtenden Datei (hier: matrix.txt) wird ebenfalls durch eine Pr¨aprozessordirektive definiert. (i) Hauptprogramm Das Hauptprogramm bietet dem Benutzer ein Menu mit f¨unf Auswahlm¨oglichkeiten an: 1 - Der Benutzer soll eine Matrix u¨ ber die Tastatur initialisieren. 2 - Die aktuell im Speicher vorhandene Matrix wird auf dem Bildschirm ausgegeben. 3 - Die Datei matrix.txt wird ausgelesen, in den Speicher der aktuellen Matrix geschrieben und auf dem Bildschirm ausgegeben. Existiert die Datei matrix.txt nicht, so erscheint auf dem Bildschirm die Meldung: Datei existiert nicht. 4 - Die aktuell im Speicher vorhandene Matrix wird in die Datei matrix.txt geschrieben. Zeilen werden dabei durch das Zeichen “\n“ markiert. sonst - Abbruch. (ii) newline (int N) Die Funktion newline erzeugt rekursiv Zeilenumbr¨uche. Die Anzahl der Zeilenumbr¨uche wird der Funktion als Parameter u¨ bergeben. (iii) matrix eingeben (float **A, int zeilen, int spalten) Initialisierung der Matrix durch den Benutzer. Anstatt des scanf()-Befehls wird die Kombination fgets() und sscanf() benutzt (siehe Kapitel ??). 72 (iv) matrix ausgeben (float **A, int zeilen, int spalten) Ausgabe der Matrix. (v) matrix speichern (float **A, char *dateiname, int zeilen, int spalten) Funktion o¨ ffnet Datei namens des Strings, auf den der Pointer dateiname zeigt, zum ¨ Schreiben. Schl¨agt der Versuch fehl, so erscheint die Ausgabe: Fehler beim Offnen. Andernfalls werden die einzelnen Matrixeintr¨age in die ge¨offnete Datei geschrieben. Die Eintr¨age werden innerhalb der Zeile durch ein Leerzeichen getrennt. Zeilen werden durch das Zeichen \n“ markiert. ” (vi) matrix laden(float **A, char *dateiname, int zeilen, int spalten) Funktion o¨ ffnet Datei namens des Strings, auf den der Pointer dateiname zeigt, zum Lesen. Schl¨agt der Versuch fehl, so erscheint die Ausgabe: Datei existiert nicht. Die Matrixeintr¨age werden nacheinander aus der Datei gelesen und im reservierten Speicher der Matrix abgelegt. (Auf die Parameter¨ubergabe der Dimension der Matrix k¨onnte in dieser Funktion verzichtet werden, da die Zeilen bzw. Matrixeintr¨age durch \n“ bzw. durch ” ein Leerzeichen bei der Speicherung getrennt wurden. Zur Reduzierung des Quelltextes wurde dies jedoch nicht ber¨ucksichigt.) #include <stdio.h> #include <malloc.h> #define M 3 /* Zeilen der Matrix */ #define N 3 /* Spalten der Matrix */ #define MATRIXDATEI "matrix.txt" /* Dateiname */ /***** Deklaration der Funktionen ************************/ void newline(int); void matrix_eingeben(float **A, int zeilen, int spalten); void matrix_ausgeben(float **A, int zeilen, int spalten); void matrix_speichern(float **A, char *dateiname, int zeilen, int spalten); void matrix_laden(float **A,char *, int, int); /* Variablennamen koennen, muessen aber nicht in der Deklaration * angegeben sein */ /***** Hauptprogramm *************************************/ int main() { float ** A=NULL; /* Doppelpointer f¨ ur Matrix */ int zeilen = M; /* Anzahl Zeilen der Matrix */ int spalten = N; /* Anzahl Spalten der Matrix */ char menu; /* Menuauswahl */ char dateiname[]=MATRIXDATEI; /* Dateiname */ char tempstring[20]; /* Zum Einlesen vom Bildschirm */ int i; 73 /***** Allokalisiert Speicher f¨ ur Matrix *****/ A=(float **) malloc(M*sizeof(float)); for (i=0;i<M;i++) { A[i]=(float *) malloc(N*sizeof(float)); } /***** Eingabe Menu **************************/ do { printf("1 - Matrix ¨ uber Bildschirm eingeben\n"); printf("2 - Matrix auf Bildschirm ausgeben\n"); printf("3 - Matrix aus Datei laden \n"); printf("4 - Matrix in Datei speichern \n"); printf("sonst - Abbruch: "); fgets(tempstring,sizeof(tempstring),stdin); sscanf(tempstring,"%c",&menu); switch (menu) { case ’1’: {matrix_eingeben(A,zeilen,spalten);break;} case ’2’: {matrix_ausgeben(A,zeilen,spalten);break;} case ’3’: {matrix_laden(A,dateiname,zeilen,spalten); break;} case ’4’: {matrix_speichern(A,dateiname,zeilen,spalten); break;} default: {printf("Wiedersehn\n");break;} } } while (menu >= ’1’ && menu <= ’4’); return 0; } /***** Definition der Funktionen *************************/ /***** Rekursive Definition einer neuen Zeile **/ void newline(int anzahl) { if (anzahl > 0) { printf("\n"); newline(anzahl-1); } } /***** Matrix vom Bildschirm einlesen **********/ 74 void matrix_eingeben(float **A, int zeilen, int spalten) { int i=0, j=0; char tempstring[20]=""; newline(3); for (i=0;i<zeilen;i++) { for (j=0;j < spalten;j++) { printf("Wert f¨ ur A[%i,%i] : ",i+1,j+1); fgets(tempstring,sizeof(tempstring),stdin); sscanf(tempstring,"%f",&A[i][j]); } } newline(3); } /***** Matrix auf dem Bildschirm ausgeben ******/ void matrix_ausgeben(float **A, int zeilen, int spalten) { int i=0, j=0; newline(1); for (i=0;i<zeilen;i++) { for (j=0;j < spalten;j++) { printf("%f ",A[i][j]); } newline(1); } newline(1); } /***** Matrix in eine Datei schreiben **********/ void matrix_speichern(float **A, char *dateiname, int zeilen, int spalten) { int i=0, j=0; FILE * datei_ptr; newline(3); datei_ptr=fopen(dateiname,"w"); ¨ffnen */ /* zum Schreiben o if (datei_ptr == NULL) { printf("Fehler beim O¨ffnen\n"); fclose(datei_ptr); 75 } else { for (i=0;i<zeilen;i++) { for (j=0;j < spalten;j++) { fprintf(datei_ptr,"%f ",A[i][j]); } fprintf(datei_ptr,"\n"); } fclose(datei_ptr); /* sind mit Speichern fertig */ printf("Matrix \n"); matrix_ausgeben(A,zeilen,spalten); printf("in Datei %s geschrieben\n", dateiname); newline(2); } } /***** Matrix aus einer Datei lesen ************/ void matrix_laden(float **A ,char *dateiname,int zeilen,int spalten) { int i=0, j=0; FILE * datei_ptr; char tempchar = ’ ’; newline(3); datei_ptr=fopen(dateiname,"r"); ¨ffnen */ /* zum Lesen o if (datei_ptr == NULL) { printf("Datei existiert nicht\n"); newline(2); } else { for (i=0;i<zeilen;i++) { for (j=0;j < spalten;j++) { fscanf(datei_ptr,"%f",&A[i][j]); } fscanf(datei_ptr,"%c",&tempchar); } fclose(datei_ptr); 76 /* sind mit Einlesen fertig */ printf("Matrix \n"); matrix_ausgeben(A,zeilen,spalten); printf("aus Datei %s eingelesen\n", dateiname); newline(2); } } Das Beispiel kann so modifiziert werden, dass die Dimension und der Dateiname durch den Benutzer eingegeben werden kann. Nach Initialisierung und Speichern kann die Datei matrix.txt mit dem Editor betrachtet werden. Ihr Inhalt hat bis auf die Werte die Gestalt: 4.000000 4.000000 3.000000 3.000000 2.000000 2.000000 5.000000 4.000000 1.000000 Die Zeilenumbr¨uche und Leerzeichen sieht man nur indirekt auf dem Bildschirm, sie stehen aber selbstverst¨andlich auch in der Datei. Zur Speicherung eines Zeichens ben¨otigt der Rechner ein Byte auf der Festplatte. F¨ur obiges Beispiel ben¨otigt der Computer 84 Byte (Jede Zahl wird durch 8 Zeichen dargestellt. Nach jeder Zahl folgt ein Leerzeichen. Jede Zeile wird mit \n“ beendet). ” ¨ Ein- und Ausgabe 7.4 Binare Neben der zeichenorientierten Ein- und Ausgabe in Dateien gibt es noch die M¨oglichkeit, die Daten bin¨ar zu verarbeiten. Gr¨unde, sich f¨ur diese Form der Datenausgabe zu entscheiden, sind: • Bei der Ausgabe von Gleitkommazahlen als Zeichen h¨angt der ben¨otigte Speicherplatz von der Anzahl der Stellen ab. Im Bin¨arformat bestimmt der Datentyp die Gr¨oße. • Die bin¨are Ein- und Ausgabe ist schneller. Gr¨unde dagegen sind: • Bin¨arformate unterscheiden sich von System zu System, so dass die entsprechenden Dateien evtl. in das jeweilige, systemabh¨angige Bin¨arformat umzuwandeln sind. Die Daten sind nicht portabel. • Manche System ben¨otigen zur Kennung des Bin¨arformats den Modusbezeichner b, andere Systeme wiederum nicht. Dies hat zur Folge, dass der Quelltext nicht portabel ist. • Man kann Daten, die im Bin¨arcode abgespeichert wurden, nicht mit einem Editor betrachten oder manipulieren. Zur bin¨aren Ein- und Ausgabe verwendet man 77 fwrite() Deklaration: size t fwrite (const void *ptr, size t groesse, size t anzahl, FILE * Datei); Beschreibung: Die Funktion schreibt von der Speicherposition, auf die ptr zeigt, an- zahl Datenobjekte der Gr¨oße groesse in den Datenstrom Datei. Der R¨uckgabewert ist die Anzahl der erfolgreich geschriebenen Datenobjekte. fread() Deklaration: size t fread (const void *ptr, size t groesse, size t anzahl, FILE * Datei); Beschreibung: Die Funktion liest aus dem Datenstrom Datei anzahl Datenobjekte der Gr¨oße groesse und speichert sie (sequenziell) ab der Position, auf die ptr zeigt. Der R¨uckgabewert ist die Anzahl der erfolgreich gelesenen Datenobjekte. Beispiel 7.4 ( Bin¨are Ein- und Ausgabe) Das Programm speichert die Matrix 4.0 4.0 3.0 A = 3.0 2.0 2.0 5.0 4.0 1.0 bin¨ar in die Datei matrix2.txt. Anschließend wird der Inhalt aus dieser Datei in die Matrix B geladen. #include <stdio.h> #define M 3 #define N 3 #define MATRIXDATEI "matrix2.txt" int main() { float A[M][N]={{4,4,3}, {3,2,2}, {5,4,1}}; float B[M][N]; void * ptr; FILE * datei_ptr; /***** Bin¨ are Ausgabe ************************/ /* Pointer auf die Adresse im Speicher setzen, * ab der die Bytes in eine Datei geschrieben werden * sollen */ ptr=&A[0][0]; ¨ffnen */ /* Datenstrom (Datei) o datei_ptr=fopen(MATRIXDATEI,"wb"); 78 /* Schreibt N*M*sizeof(float) Bytes ab * pointer-Adresse in den Datenstrom (Datei) */ fwrite(ptr,sizeof(float),N*M,datei_ptr); /* Alternativ aber umst¨ andlich und langsam * for (i=0; i < M; i++) * { * for (j=0; j < N; j++) * { * fwrite(ptr,sizeof(float),1,datei_ptr); * ptr=ptr+sizeof(float); * } * } */ /* Datenstrom (Datei) schließen */ fclose(datei_ptr); /***** Bin¨ are Eingabe ************************/ ¨ffnen */ /* Datenstrom (Datei) o datei_ptr=fopen(MATRIXDATEI,"rb"); /* Pointer auf die Adresse im Speicher setzen, * ab der die Bytes aus der Datei in den Speicher * geschrieben werden sollen */ ptr=&B[0][0]; /* Liest N*M*sizeof(float) Bytes aus der Datei * und schreibt sie sequenziell an die pointer-Adresse */ fread(ptr,sizeof(float),N*M,datei_ptr); /* Datenstrom (Datei) schließen */ fclose(datei_ptr); return 0; } ¨ 7.5 Dateien loschen/umbenennen Die Anweisung, mit der Dateien gel¨oscht werden k¨onnen, lautet remove(pfadname). Diese Funktion liefert den Wert -1, wenn nicht gel¨oscht werden konnte, ansonsten den Wert 0. Zum Umbennen von Verzeichnissen und Dateien und zum Verschieben von Dateien wird die Funktion rename(pfadangabe) benutzt. Liefert die Funktion einen Wert ungleich 0, dann ist ein Fehler aufgetreten. Als Parameter werden in der Klammer zuerst der alte, danach der neue 79 Name angegeben. Weichen die Verzeichnisnamen in den beiden Pfadangaben voneinander ab, dann wird die Datei verschoben. Verzeichnisse k¨onnen nur umbenannt, nicht verschoben werden. Beispiel 7.5 ( L¨oschen und Umbenennen von Dateien) #include <stdio.h> int main() { /***** L¨ oschen der Datei matrix.txt ***********/ /* Der Backslash bei der Angabe des Pfadnames * muss doppelt eingegeben * werden, da er sonst als Steuerzeichen interpretiert wird. * Der Backslash als Verzeichnistrenner ist Windows-spezifisch. Fuer * LINUX ist es ein normaler Slash ’/’, fuer Macintosh ein Doppelpunkt. */ if (remove(".\\matrix.txt")==-1) { /* Ausgabe der Fehlermeldung; die Funktion perror() dient zur Ausgabe * von Fehlermeldungen auf dem Datenstrom stderr. Der Text in Klammern * wird um die interne Fehlermeldung erg¨ anzt. * Die Ausgabe auf dem Bildschirm in diesem Fall lautet : * Fehler beim L¨ oschen : No such file or directory */ perror("Fehler beim L¨ oschen "); } else { printf("Datei matrix.txt gel¨ oscht.\n"); } /***** Umbenennen der Datei matrix2.txt **********/ if (rename(".\\matrix2.txt","matrix.bin") != 0) { perror("Fehler beim Umbenennen "); } else { printf("Datei matrix2.txt umbenannt zu matrix.bin.\n"); } return 0; } 80 7.6 Positionierung in einem Datenstrom ¨ Nach dem Offnen eines Datenstroms wird die aktuelle Position in dem Datei-Pointer abgelegt. Werden Zeichen oder Zahlen aus dem Datenstrom in den Speicher eingelesen, so erh¨oht sich die Positionsangabe entsprechend der eingelesen Bytes. Zur Ansteuerung einer bestimmten Stelle im Datenstrom enth¨alt der Header stdio.h die Funktion fseek(). fseek(datei-pointer,offset,ursprung) Mit offset wird angegeben, um wieviele Bytes der datei-pointer sich vom ursprung entfernen soll. F¨ur ursprung stehen drei Alternativen zur Auswahl: 1.) SEEK SET: Anfang des Datenstroms 2.) SEEK CUR: Aktuelle Position 3.) SEEK END: Ende des Datenstroms (EOF) Die aktuelle Position im Datebstrom kann mit der Funktion ftell() abgefragt werden. Das n¨achste Beispiel zeigt die Funktionsweise der beiden Funktionen. Beispiel 7.6 ( Positionierung im Datenstrom) #include <stdio.h> #define zeilen 3 #define spalten 3 int main() { FILE * int i; double double void * dateiptr; A[zeilen][spalten]={{1.0,2.0,3.0}, {4.0,5.0,6.0}, {7.0,8.0,9.0}}; ausgabe; ptr; /***** Speichern von A im Bin¨ arformat in die Datei matrix.bin ******/ ptr=&A[0][0]; dateiptr=fopen("matrix.bin","wb"); fwrite(ptr,sizeof(A),1,dateiptr); fclose(dateiptr); /* Ausgabe der Diagonalen von A mit Angabe der aktuellen Position im * Datenstrom */ dateiptr=fopen("matrix.bin","rb"); ptr=&ausgabe; for (i=1;i<=spalten;i++) { 81 printf("Position in Datei matrix.bin : %i \t",(int) ftell(dateiptr)); /* Einlesen einer Zahl */ fread(ptr,sizeof(double),1,dateiptr); printf("A[%i][%i] : %f\n",i,i,ausgabe); /* Sprung zum n¨ achsten Diagonalelement */ fseek(dateiptr,(spalten)*sizeof(double),SEEK_CUR); } fclose(dateiptr); return 0; } Das Programm erzeugt die Ausgabe: Position in Datei matrix.bin : 0 A[1][1] : 1.000000 Position in Datei matrix.bin : 32 A[2][2] : 5.000000 Position in Datei matrix.bin : 64 A[3][3] : 9.000000 7.7 Dateiausgabe umlenken Wie schon in Abschnitt 7.1 bemerkt, gibt es zwei stets ge¨offnete Standardausgaben, n¨amlich stdout und stderr. Normalerweise geben beide die Daten auf dem Bildschirm aus. Wie kann man nun beide Datenstr¨ome so trennen, dass sie auf separate Einheiten gelenkt werden, beispielsweise stdout weiterhin auf den Bildschirm und stderr in eine Protokolldatei? Beispiel 7.7 ( Umleiten des stderr-Stroms in die Datei error.log) #include <stdio.h> int main() { /* Umleiten des Ausgabestroms stderr. * Anstatt der Ausgabe von stderr auf den Bildschirm, wird bei der * Ausgabe alles in die Datei error.log geschrieben (angeh¨ angt) */ freopen("error.log","a",stderr); if (remove(".\\matrix.bin")==-1) { /* Tritt ein Fehler beim L¨ oschen auf, * so wird der Text "Fehler ..." an die * Datei error.log angeh¨ angt */ fprintf(stderr,"Fehler beim L¨ oschen der Datei matrix.bin.\n"); } else { /* Wird die Datei ordentlich gel¨ oscht, so 82 * folgt die Ausgabe "Datei ..." auf dem * Bildschirm */ fprintf(stdout,"Datei matrix.bin erfolgreich gel¨ oscht.\n"); /* Aquivalent w¨ are printf("Datei ..."); */ } return 0; } Die Funktion freopen() hat als R¨uckgabewert wieder einen Dateipointer. Die exakte Deklaration ist: FILE * (const char * dateiname, const char * modus, FILE * datei-pointer); Die Funktion verbindet einen g¨ultigen Dateipointer mit einem neuen Datenstrom (Datei) und liefert als R¨uckgabewert einen Dateizeiger auf diesen Datenstrom. Die drei Parameter geben der Reihe nach den neuen Datenstrom, den neuen Bearbeitungsmodus und einen Dateipointer an, der zu einer ge¨offfneten Datei geh¨oren muss. Hier wird also der stets ge¨offnete Datenstrom stderr umgelenkt auf die Datei error.log im Modus append. Die R¨uckgabe ist ebenfalls der Dateipointer und kann auch alternativ verwendet werden. 83 8 Mehrdateiprojekte Die bisher gezeigten Programmbeispiele waren im Umfang noch recht u¨ berschaubar, was nat¨urlich auf ihre eingeschr¨ankte Funktionalit¨at zur¨uckzuf¨uhren ist. Gr¨oßere Softwareprojekte werden im Allgemeinen von mehreren Entwicklern bearbeitet, was eine Aufteilung des Problems erfordert. W¨are man in dieser Situation gezwungen, das Programm in einer einzigen Quelldatei zu halten, so w¨aren chaotische Zust¨ande im Quellcode praktisch unvermeidbar. Deshalb ist das Aufteilen der Aufgaben in C durch Mehrdateiprojekte erm¨oglicht. 1.) Pr¨aprozessordirektiven sowie die Deklaration eigener Strukturen, Unions, Typen und Subroutinen (Funktionen) werden in Headerdateien (Endung .h) zusammengefasst. 2.) Das Hauptprogramm wird in einer speziellen Datei, zumeist main.c gehalten. 3.) Die einzelnen Funktionen werden jeweils in entsprechenden Dateien gehalten (Endung .c). Inhaltlich zusammengeh¨orige Subroutinen k¨onnen gemeinsam in der jeweiligen Datei stehen. ¨ 4.) Die Ubersetzung der einzelnen Dateien zu Modulen (Endung .o) ist zu koordinieren. Hier wird auf den in Abschnitt 1.6 vorgestellten mehrstufigen Kompiliervorgang zur¨uckgegriffen. 5.) Optimierte und getestete Module k¨onnen schließlich zu eigenen Programmbibliotheken (Endung .a) zusammengefaßt werden. Andere Projekte k¨onnen dann auf die fertigen Module w¨ahrend des Linkvorgangs zugreifen. In diesem Kapitel wird ein solches Projekt (immer noch in u¨ berschaubarem Umfang) illustriert. Dazu sollen Routinen zum Erzeugen, Laden und Speichern von beliebigen m × n Matrizen implementiert. Zus¨atzlich soll die Matrix-Multiplikation und das Anzeigen des Inhalts einer Matrix auf dem Bildschirm zur Verf¨ugung stehen. Begonnen wird mit der Strukturierung der Funktionalit¨aten: 1.) Matrizen werden in einer Struktur matrix gespeichert. Die Struktur enth¨alt zwei int-Variablen f¨ur die Dimension der Matrix und einen double **-Zeiger f¨ur den Zugriff auf die einzelnen Elemente der Matrix: struct matrix { int anz_zeilen; int anz_spalten; double ** matrix; }; 84 2.) Laden einer Matrix von der Festplatte: Es wird vorausgesetzt, dass die Matrizen im Zeichenformat abgespeichert wurden. Vor den eigentlichen Matrixeintr¨agen besteht die erste Zeile der Datei aus zwei Integerwerten, die die Anzahl der Zeilen bzw. Spalten angibt. Jede Zahl wird durch ein Leerzeichen und jede Zeile wird durch einen Zeilenumbruch \n getrennt. Die Funktion soll als R¨uckgabewert eine Struktur matrix haben: struct matrix matrix laden (char * dateiname); 3.) Speichern einer Matrix auf der Festplatte: Eine Variable von Typ struct matrix soll in dem unter 2.) angegeben Format gespeichert werden. Formal soll die Subroutine folgende Eingabeparameter besitzen: void matrix speichern(char * dateiname, struct matrix A); 4.) Anzeigen einer Matrix auf dem Bildschirm: Diese Funktionen gibt eine Variable der Struktur matrix auf dem Bildschirm aus: void matrix anzeigen(struct matrix A); 5.) Matrixmultiplikation: Die Funktion hat als R¨uckgabewert das Matrixprodukt zweier Matrizen: struct matrix matrix multiplizieren(struct matrix A, struct matrix B); 6.) Erzeugen einer Matrix (Speicherreservierung): Unter Verwendung der Angabe der Dimensionen der Matrix soll entsprechend Speicher f¨ur eine Struktur matrix reserviert werden. struct matrix matrix allokieren(int anz zeilen,int anz spalten); Dieses Anforderungsprofil f¨uhrt auf die folgende eigene Headerdatei, die matrix.h genannt wird. /* Deklaration der Struktur matrix */ struct matrix { int anz_zeilen; int anz_spalten; double ** matrix; }; /* Deklaration der einzelnen Subroutinen */ struct matrix matrix_allokieren(int anz_zeilen,int anz_spalten); struct matrix matrix_laden(char * dateiname); 85 struct matrix matrix_multiplizieren(struct matrix A,struct matrix B); void matrix_speichern(char * dateiname,struct matrix A); void matrix_anzeigen(struct matrix A); 8.1 Verteilung des Codes auf Dateien Die Funktionalit¨aten sind nun auf die einzelnen Subroutinen verteilt und deren Deklarationen in der Headerdatei matrix.h zusammengefaßt. Inhaltlich zusammengeh¨orige Subroutinen (wie zum Beispiel matrix laden() und matrix speichern()) k¨onnen in einer Quelltextdatei zusammengefaßt werden. Da dieses Beispiel noch in einem u¨ berschaubarem Umfang vorliegt, wird f¨ur jede Funktion jedoch eine eigene Quelldatei erzeugt. Schließlich muss noch der Quellcode des Hauptprogramms in der Datei main.c abgelegt werden. Das Mehrdateiprojekt besteht mit dem Header aus den sieben Dateien: main.c matrix speichern.c matrix laden.c matrix allokieren.c matrix anzeigen.c matrix multiplizieren.c matrix.h ¨ Achtung!!! Jede einzelne Datei ist beim sp¨ateren Ubersetzen unabh¨angig von den anderen. Speziell wissen, die einzelnen Quelldateien nicht, welche Headerdateien von main.c eingebunden werden. Daraus folgt, dass jede einzelne Quelldatei diejenigen Headerdateien mit #include einf¨ugen muss, die f¨ur die jeweils verwendeten Bibliotheksroutinen notwendig sind. Der Quellcode der einzelnen Subroutinen k¨onnte wie unten angegeben lauten. (Auf Sicherheitsabfragen bzgl. Speicheranforderungen und Dateioperationen wurde aus Platzgr¨unden ver¨ zichtet, ebenso auf die Uberpr¨ ufung der Gr¨ossen der Matrizen.) 1.) matrix speichern.c #include <stdio.h> /* fopen, fclose, fprintf */ #include "matrix.h" /* struct matrix + Deklaration der Funktion */ void matrix_speichern(char * dateiname,struct matrix A) { FILE *file_ptr; int i,j; file_ptr=fopen(dateiname,"w"); fprintf(file_ptr,"%i %i \n",A.anz_zeilen,A.anz_spalten); for (i=0;i < A.anz_zeilen; i++) { 86 for (j=0;j < A.anz_spalten; j++) { fprintf(file_ptr,"%f ",A.matrix[i][j]); } fprintf(file_ptr,"\n"); } fclose(file_ptr); } 2.) matrix laden.c #include <stdio.h> /* fopen, fclose, fscanf */ #include "matrix.h" /* struct matrix + Deklaration der Funktion */ struct matrix matrix_laden(char * dateiname) { FILE *file_ptr; struct matrix A; int anz_zeilen, anz_spalten; int i,j; file_ptr=fopen(dateiname,"r"); fscanf(file_ptr,"%i %i",&anz_zeilen,&anz_spalten); A=matrix_allokieren(anz_zeilen,anz_spalten); for (i=0;i < A.anz_zeilen ; i++) { for (j=0; j < A.anz_spalten; j ++) { fscanf(file_ptr,"%lf",&A.matrix[i][j]); } } fclose(file_ptr); return A; } 3.) matrix anzeigen.c #include <stdio.h> /* printf */ #include "matrix.h" /* struct matrix + Deklaration der Funktion */ void matrix_anzeigen(struct matrix A) { int i,j; for (i=0;i< A.anz_zeilen;i++) { for (j=0;j < A.anz_spalten;j++) 87 { printf("%f ",A.matrix[i][j]); } printf("\n"); } } 4.) matrix allokieren.c #include "matrix.h" /* struct matrix + Deklaration der Funktion */ #include <malloc.h> /* malloc */ struct matrix matrix_allokieren(int anz_zeilen,int anz_spalten) { struct matrix a; int i; a.matrix=(double **) malloc(anz_zeilen*sizeof(double *)); for (i=0;i<anz_zeilen;i++) { a.matrix[i]=(double *) malloc(anz_spalten*sizeof(double)); } a.anz_zeilen=anz_zeilen; a.anz_spalten=anz_spalten; return a; } 5.) matrix multiplizieren.c #include "matrix.h" /* struct matrix + Deklaration der Funktion */ struct matrix matrix_multiplizieren(struct matrix A,struct matrix B) { struct matrix C; int i,j,k; C=matrix_allokieren(A.anz_zeilen,B.anz_spalten); for (i=0;i< A.anz_zeilen; i++) { for (j=0;j < B.anz_spalten;j++) { C.matrix[i][j]=0; for (k=0;k < A.anz_spalten;k++) { C.matrix[i][j]=C.matrix[i][j] +A.matrix[i][k]*B.matrix[k][j]; } 88 } } return C; } 6.) main.c #include "matrix.h" /* structmatrix */ int main() { struct matrix A,B,C; A=matrix_laden("matrixA.dat"); B=matrix_laden("matrixB.dat"); C=matrix_multiplizieren(A,B); matrix_anzeigen(C); matrix_speichern("matrixC.dat",C); return 0; } 8.2 Manuelles Kompilieren eines Mehrdateiprojektes Mehrdateiprojekte werden in mindestens zwei Stufen zu einem ausf¨uhrbaren Programm u¨ bersetzt: 1.) Die einzelnen Quelldateien werden mit der Option -c kompiliert. Es entstehen die Objektdateien, in diesem Zusammenhang auch Module genannt: gcc -c “Liste der .c-Dateien“ Aus jeder Quelltextdatei entsteht eine gleichnamige Datei mit der Endung .o. 2.) Die Objektdateien (Module) werden zu einer ausf¨uhrbaren Datei gelinkt. An dieser Stelle werden eventuell ben¨otigte Bibliotheken (z.B. die Mathebibliothek) eingebunden: gcc -o “Name des Programms“ “Liste der .o Dateien“ -lBibliothek 89 In dem einfachen Beispiel bedeutet dies: gcc -c main.c matrix speichern.c matrix laden.c matrix anzeigen.c gcc -c matrix allokieren.c matrix multiplizieren.c Mit diesen Anweisungen entstehen die Objektdateien main.o matrix speichern.o matrix laden.o matrix anzeigen.o matrix allokieren.o matrix multiplizieren.o Das Linken der Objektdateien zu einem ausf¨uhrbarem Programm erfolgt durch: gcc -o Matrix main.o matrix speichern.o matrix laden.o matrix anzeigen.o matrix allokieren.o matrix multiplizieren.o An dieser Stelle zeigt sich der Sinn des Aufteilens von Quelltexten auf mehrere Dateien sehr deutlich: Angenommen, man m¨ochte bei der Funktion matrix anzeigen() ein anderes Format f¨ur die Matrixeintr¨age. Dann editiert man die entsprechenden Datei matrix anzeigen.c, u¨ bersetzt nur diese, gcc -c matrix anzeigen.c und bindet die aktualisierte Objektdatei matrix anzeigen.o mit den bereits vom letzten Compileraufruf vorhandenen anderen Objektdateien zum Programm zusammen: gcc -o Matrix main.o matrix speichern.o matrix laden.o matrix anzeigen.o matrix allokieren.o matrix multiplizieren.o Man kann also Kompiliervorg¨ange durch sorgf¨altige Aufteilung auf das notwendige Maß reduzieren. Trotzdem ist es nicht immer ergonomisch, nur eine Funktion pro Datei zu halten. Macht man manuell von dieser M¨oglichkeit Gebrauch, so ist bei hinreichend großen Projekten die Gefahr gegeben, dass man etwa ein Modul vergisst und sich die Fehlermeldungen nicht erkl¨aren kann. Wichtiger ist, dass aus der Aktualisierung einer Quelldatei bzw. eines Moduls die Notwendigkeit folgen kann (und im Allgemeinen auch folgt !!), dass ein anderes Modul neu erzeugt werden muss, weil es auf das aktualisierte Modul zugreift. In großen Projekten werden diese Abh¨angigkeiten so komplex, dass man nur a¨ ußerst schwer ¨ den Uberblick bewahren kann (der Quellcode des Linuxkernels z. B. besteht aus mehr als 17.000 Dateien). 8.3 Automatisiertes Kompilieren mit make Um die am Ende des letzten Abschnitts genannten Probleme meistern zu k¨onnen, gibt es das Programm make, das unter Ber¨ucksichtigung der Abh¨angigkeiten der Programmmodule un¨ tereinander die notwendigen Ubersetzungsvorg¨ ange vornimmt. Die Abh¨angigkeiten werden dem Programm in einer Steuerdatei, die standardm¨aßig Makefile oder makefile heißt, mitgeteilt. 90 Das Konzept • Der Kompiliervorgang setzt sich aus der Erzeugung von Zielen (Targets) zusammen. Das ausf¨uhrbare Programm z.B. ist ein solches Target. • Zu jedem Target wird im Makefile festgehalten, von welchen anderen Targets es abh¨angt. • Anhand des Datums der letzten Datei¨anderung stellt das Programm make fest, ob ein Target a¨ lter ist als jene, von denen es abh¨angt. Ist dies der Fall, so muss das Target neu erzeugt werden. Im Makefile muss festgehalten werden, wie diese Erzeugung zu geschehen hat. • Rekursiv stellt make durch Auswertung des Makefiles die Abh¨angigkeiten fest. Auf der untersten Ebene befinden sich z.B. die Quell- und Headerdateien, die ja zumeist durch Editieren aktualisiert werden. Aufbau von Makefiles • Ein Target wird den Objekten, von denen es abh¨angt, durch einen Doppelpunkt : getrennt. • Die Liste der Kommandos zur Erzeugung des Targets folgt in einer neuen Zeile, die stets mit einem Tabulatorzeichen beginnen muss. • Ein Target kann mehrmals auftreten. • Ruft man an der Kommandozeile make auf, so wird das erste Target im Makefile erzeugt. M¨ochte man gezielt ein anderes Target erzeugen, so verwendet man make Name des Targets F¨ur das Beispielprojekt hat das Makefile die Darstellung: Beispiel 8.1 (Makefile 1) Matrix : main.o matrix_speichern.o matrix_laden.o matrix_anzeigen.o matrix_multiplizieren.o matrix_allokieren.o gcc -o Matrix main.o matrix_speichern.o matrix_laden.o\ matrix_anzeigen.o matrix_multiplizieren.o matrix_allokieren.o main.o : main.c matrix.h gcc -c main.c -Wall matrix_speichern.o : matrix_speichern.c matrix.h gcc -c matrix_speichern.c -Wall matrix_laden.o : matrix_laden.c matrix.h gcc -c matrix_laden.c -Wall 91 matrix_anzeigen.o : matrix_anzeigen.c matrix.h gcc -c matrix_anzeigen.c -Wall matrix_multiplizieren.o : matrix_multiplizieren.c matrix.h gcc -c matrix_multiplizieren.c -Wall matrix_allokieren.o : matrix_allokieren.c matrix.h gcc -c matrix_allokieren.c -Wall Makros mit make Makros dienen der Abk¨urzung und Zusammenfassung von mehreren Objekten sowie der Unterst¨utzung bei der plattformunabh¨angigen Programmierung. Muss man z.B. eine Liste von Objektdateien mehrmals im Makefile verwenden, so leistet dies Objekte = main.o matrix speichern.o matrix laden.o matrix anzeigen.o matrix multiplizieren.o matrix allokieren.o und den so zugewiesenen Inhalt des Makros Objekte liest man durch $(Objekte) Das obige Makefile l¨asst sich mit Hilfe von Makros vereinfachen: Beispiel 8.2 (Makefile 2) PROGRAMMNAME = Matrix OBJEKTE = main.o matrix_speichern.o matrix_laden.o matrix_anzeigen.o matrix_multiplizieren.o matrix_allokieren.o $(PROGRAMMNAME) : $(OBJEKTE) gcc -o $(PROGRAMMNAME) $(OBJEKTE) main.o : main.c gcc -c main.c -Wall matrix.h matrix_speichern.o : matrix_speichern.c matrix.h gcc -c matrix_speichern.c -Wall matrix_laden.o : matrix_laden.c matrix.h gcc -c matrix_laden.c -Wall matrix_anzeigen.o : matrix_anzeigen.c matrix.h gcc -c matrix_anzeigen.c -Wall matrix_multiplizieren.o : matrix_multiplizieren.c matrix.h 92 gcc -c matrix_multiplizieren.c -Wall matrix_allokieren.o : matrix_allokieren.c matrix.h gcc -c matrix_allokieren.c -Wall Der Aufruf make veranlasst also die Erzeugung des ersten Targets, in diesem Fall ist es das ausf¨uhrbare Programm Matrix. 8.4 Eigene Bibliotheken Sind die einzelnen Objekte (Module) auf ihre Zuverl¨assigkeit und Geschwindigkeit hin ausreichend getestet, so kann man erw¨agen, sie in einer eigenen Programmbibliothek zusammenzufassen. Wie bei der C-Standardbibliothek oder der Mathematikbibliothek ist dann bei der Implementierung von neuen Programmen nur darauf zu achten, dass die zugeh¨orige Headerdatei mit den Deklarationen an den entsprechenden Stellen im Quelltext auftaucht und die Bibliothek beim Linken eingebunden wird. Bibliotheken entstehen aus den Objektdateien, die mit dem ar-Kommando zusammengef¨ugt werden. Zur Illustration werden die Funktionen in der Bibliothek libmatrix.a zusammengefasst. Dies geschieht mit dem Aufruf ar -r libmatrix.a matrix speichern.o matrix laden.o matrix anzeigen.o matrix multiplizieren.o matrix allokieren.o und beim sp¨ateren Linken wird die Bibliothek einfach mit angegeben: gcc -o Matrix main.o libmatrix.a Auch diese Operation l¨asst sich mit make automatisieren. Das entsprechende Makefile sieht zum Beispiel folgendermaßen aus: Beispiel 8.3 (Makefile 3) PROGRAMMNAME = Matrix BIBLIOTHEK = ./libmatrix.a VERZ = ./ BIBOBJEKTE = matrix_speichern.o matrix_laden.o matrix_anzeigen.o matrix_multiplizieren.o matrix_allokieren.o $(PROGRAMMNAME) : main.o $(BIBLIOTHEK) $(VERZ)/matrix.h gcc -o $(PROGRAMMNAME) main.o $(BIBLIOTHEK) main.o : $(VERZ)/main.c $(VERZ)/matrix.h gcc -c $(VERZ)/main.c -Wall $(BIBLIOTHEK) : $(BIBOBJEKTE) 93 ar -r $(BIBLIOTHEK) $(BIBOBJEKTE) matrix_speichern.o : $(VERZ)/matrix_speichern.c $(VERZ)/matrix.h gcc -c $(VERZ)/matrix_speichern.c -Wall matrix_laden.o : $(VERZ)/matrix_laden.c $(VERZ)/matrix.h gcc -c $(VERZ)/matrix_laden.c -Wall matrix_anzeigen.o : $(VERZ)/matrix_anzeigen.c $(VERZ)/matrix.h gcc -c $(VERZ)/matrix_anzeigen.c -Wall matrix_multiplizieren.o : $(VERZ)/matrix_multiplizieren.c $(VERZ)/matrix.h gcc -c $(VERZ)/matrix_multiplizieren.c -Wall matrix_allokieren.o : $(VERZ)/matrix_allokieren.c $(VERZ)/matrix.h gcc -c $(VERZ)/matrix_allokieren.c -Wall Wird die Bibliothek anderswo untergebracht, um anderen Programmierprojekten den Zugriff zu erleichtern und handelt es sich (¨ahnlich wie bei den Headerdateien) nicht um ein Verzeichnis im Standardsuchpfad f¨ur C-Bibliotheken, so muss das betreffende Verzeichnis beim Linken mit der Option -L angegeben werden. Die Bibliothek wird dann mit der Option -l u¨ bergeben, wobei das Pr¨afix lib und das Suffix .a entfallen. Da das aktuelle Verzeichnis mit . bezeichnet wird, sind folgende Formulierungen a¨ quivalent gcc -o Matrix main.o -L. -lmatrix gcc -o Matrix main.o libmatrix.a 94 A A.1 Zahlendarstellung im Rechner und Computerarithmetik Prinzipiell ist die Menge der im Computer darstellbaren Zahlen endlich. Wie groß“ diese ” Menge ist, h¨angt von der Rechnerarchitektur ab. So sind bei einer 32Bit-Architektur zun¨achst maximal 232 = 4294967296 ganze Zahlen (ohne Vorzeichen) darstellbar. Innerhalb dieser Grenzen stellt die Addition und Multiplikation von ganzen Zahlen kein Problem dar. Ganz anders verh¨alt es sich mit rationalen und erst recht irrationalen Zahlen: Jede Zahl x , 0 l¨asst sich bekanntlich darstellen als ∞ X N x = sgn(x)B x−n B−n , (A.1) n=1 wobei 2 ≤ B ∈ N, N ∈ Z und xn ∈ {0, 1, . . . , B − 1} f¨ur alle n ∈ N (B-adische Entwicklung von x). Diese ist eindeutig, wenn gilt: • x−1 , 0, falls x , 0, • und es existiert ein n ∈ N mit x−n , B − 1. Eine Maschinenzahl dagegen hat die Form x˜ = sgn(x)B N l X x−n B−n n=1 wobei die feste Gr¨oße l ∈ N die Mantissenl¨ange ist. Als Mantisse bezeichnet man den Ausdruck l X m(x) = x−n B−n . (A.2) n=1 Hieraus folgt sofort, dass irrationale Zahlen u¨ berhaupt nicht und von den rationalen Zahlen nur ein Teil im Rechner dargestellt werden. Man unterscheidet zwei Arten der Darstellung: 1. Festkommadarstellung Sowohl die Anzahl N der zur Darstellung verf¨ugbaren Ziffern, als auch die Anzahl N1 der Vorkommastellen ist fixiert. Die Anzahl der maximal m¨oglichen Nachkommastellen N2 erf¨ullt notwendigerweise N = N1 + N2 . 95 2. Gleitkommadarstellung In (A.2) ist die Mantissenl¨ange l fixiert und der Exponent N ist begrenzt durch N− ≤ N ≤ N+ , N− , N+ ∈ Z. Alle Maschinenzahlen liegen dabei vom Betrag her im Intervall (BN− −1 , BN+ ). Zur Normalisierung der Darstellung verlangt man, dass x−1 , 0, wenn x , 0. Zahlen, die kleiner als BN− −1 sind, werden vom Computer als 0 angesehen. Zahlen, die gr¨oßer als BN+ sind, k¨onnen nicht verarbeitet werden (Exponenten¨uberlauf). Die Darstellung von irrationalen Zahlen erfordert eine Projektion auf die Menge der Maschinenzahlen, die so genannte Rundung. Man unterscheidet vier Rundungsarten: 1. Aufrundung: Zu x ∈ R w¨ahlt man die n¨achsth¨ohere Maschinenzahl x˜. 2. Abrundung: Zu x ∈ R w¨ahlt man die n¨achstniedrigere Maschinenzahl x˜. 3. Rundung (im engeren Sinne) Ausgehend von der Darstellung (A.1), setzt man P l n=1 x−n B−n , x˜ B sgn(x)BN P ln=1 x−n B−n + B−l , fallsx−(l+1) < B/2 fallsx−(l+1) ≥ B/2 4. Abschneiden Verwerfen aller x−n mit n > l in der Darstellung A.1. Die Gr¨oße | x˜ − x| heißt absoluter, die Gr¨oße | x˜ − x| |x| heißt relativer Rundungsfehler. Man wird erwarten, dass die Rundung im engeren Sinne die beste ist. In der Tat weist sie statistisch gesehen die geringsten Rundungsfehler auf. F¨ur die Rundung (im engeren Sinne) gilt: a) x˜ ist wieder eine Maschinenzahl. b) der absolute Fehler erf¨ullt | x˜ − x| ≤ 96 1 N−l B 2 c) Der relative Fehler erf¨ullt | x˜ − x| 1 −l+1 ≤ B = eps |x| 2 Die Zahl eps wird auch als Maschinengenauigkeit bezeichnet. A.2 IEEE Gleitkommazahlen Die IEEE (Institute of Electrical and Electronic Engineers) ist eine internationale Organisation, die bin¨are Formate f¨ur Gleitkommazahlen festlegt. Diese Formate werden von den meisten (aber nicht allen) heutigen Computern benutzt. Die IEEE definiert zwei unterschiedliche Formate mit unterschiedlicher Pr¨azision: Einzel- und Doppelpr¨azision (single and double precision). Single precision wird in C von float Variablen und double precision von double Variablen benutzt. Intel’s mathematischer Co-Prozessor benutzt eine dritte, h¨ohere Pr¨azision, die sogenannte erweiterte Pr¨azision (extended precision). Alle Daten im Co-Prozessor werden mit dieser erweiterten Pr¨azision behandelt. Werden die Daten aus dem Co-Prozessor im Speicher ausgelagert, so werden sie automatisch umgewandelt in single bzw. double precision. Die erweiterte Pr¨azision benutzt ein etwas anderes Format als die IEEE float- bzw double-Formate und werden in diesem Abschnitt nicht diskutiert. IEEE single precision Single precision Gleitkommazahlen benutzen 32 Bits (4 Byte) um eine Zahl zu verschl¨usseln: Bit 1-23: Hier wird die Mantisse gespeichert, d.h. die Mantissenl¨ange l betr¨agt bei single precision 23. Im single precision Format wird zur Mantisse noch eins addiert Beispiel A.1 (IEEE Mantisse) 23 Bit z }| { 10110110100000000000000 =0.1011011012 + 1 =1.1011011012 Bit 24-31: Hier wird der bin¨are Exponent N gespeichert. In Wirklichkeit wird der Exponent N plus 127 gespeichert. Bit 32: Gibt das Vorzeichen der Zahl an. 97 Beispiel A.2 (IEEE single precsion) Welche Dezimalzahl wird duch den Bin¨arcode 01000001 | {z } 11011011 | {z } 01000000 | {z } 00000000 | {z } darByte 3 Byte 2 Byte 1 Byte 0 gestellt? Bit 32 Bit 31-24 Bit 23-1 0 10000011 |{z} | {z } 10110110100000000000000 | {z } Zahl ist positiv N=131−127=4 Mantisse =1.1011011012 * 2N =11011.011012 =24 + 23 + 21 + 20 + 2−2 + 2−3 + 2−5 =27.406250 Folgendes Beispielprogramm testet ob der Computer die float Zahl 27.040625 im IEEE single precision Format speichert. Beispiel A.3 # # # # # # # # # include <stdio.h> define text1 "Der Computer testet ob die float Zahl 27.0406250 \n" define text2 "im IEEE single Format gespeichert wird.\n\n" define text3 " \t Byte 3 Byte 2 Byte 1 Byte 0 define text4 "27.0406250 = \t 01000001 11011011 01000000 00000000" define text5 " (im IEEE Format)\n" define text6 " = \t 65 219 64 0" define text7 " (Byte als unsigned char)\n\n\n" define text8 "Ihr Rechner betrachtet die Zahl als : %f \n" \n\n" int main() { unsigned char *p; float a; printf(text1); printf(text2); printf(text3); printf(text4); printf(text5); printf(text6); printf(text7); /* /* Byte 3 Byte 2 98 Byte 1 Byte 0 */ */ /* bin¨ ar : 01000001 /* dezimal : 65 11011011 219 01000000 64 00000000 */ 0 */ p=(unsigned char *) &a; p[0]= 0; p[1]= 64; p[2]= 219; p[3]= 65; printf(text8,a); return 0; } IEEE double precision Double precision Gleitkommazahlen benutzen 64 Bits (8 Byte) um eine Zahl zu verschl¨usseln. Bit 1-52: Mantisse Bit 53-63: Exponent Bit 64: Vorzeichen Mehr zu diesem Thema findet man zum Beispiel in [Car]. A.3 Computerarithmetik Das Rechnen mit ganzen Zahlen ist (innerhalb der erw¨ahnten Grenzen) kein Problem: Addition und Multiplikation sind kommutativ, assoziativ und das Distributivgesetz gilt. Beim Rechnen in der Fest- oder Gleitkommadarstellung gelten Assoziativ- und Distributivgesetz jedoch nicht! Ein weiterer Effekt, der zu beachten ist, ist die Ausl¨oschung von f¨uhrenden Stellen: Beipiel (Ausl¨oschnung): Sei B = 10, l = 3. Sei x = 0.9995, y = 0.9984 und es soll im Computer die Differenz x − y berechnet werden. Wie wirkt sich die Rundung auf das Resultat aus? Zun¨achst ist x˜ = 0.1 · 101 und y˜ = 0.998. Daher ist x˜ − y˜ = 0.2 · 10−2 , das exakte Resultat ist x − y = 0.11 · 10−2 und der relative Fehler ergibt sich zu 81.8% ! Dieser Effekt der Verst¨arkung des Rundungsfehlers tritt auf, wenn man fast identische Zahlen voneinander subtrahiert. 99 Eine Operation mit a¨ hnlich instabilen Verhalten ist die Division x/y mit x << y. Addition zweier Zahlen mit gleichem Vorzeichen und Multiplikation sind jedoch gutartige Operationen. 100 A.4 Die O-Notation Um eine objektive Grundlage f¨ur den Vergleich zweier Algorithmen zur L¨osung ein und derselben Aufgabe zu schaffen, untersucht man die Eigenschaften von Algorithmen anhand der Gr¨oße (Dimension) des Problems. Da man zum Vergleich verschiedener Algorithmen oft nur an deren asymptotischem Verhalten bez¨uglich Speicherbedarf bzw. Laufzeit interessiert ist und weniger an exakten Werten, hat sich zur Untersuchung die so genannte O-Notation (Landau-Symbole) bew¨ahrt und eingeb¨urgert. Definition A.4 Sei G ⊂ R, f, g : G → R und x0 ∈ G. a) Die Funktion f heißt von der Ordnung O(g(x)) f¨ur x → x0 , wenn es eine Konstante C > 0 und ein δ > 0 gibt, so dass ∀|x − x0 | < δ | f (x)| ≤ C. |g(x)| : (A.3) Dabei wird zus¨atzlich x , x0 gefordert, falls g (und f ) in x0 nicht sinnvoll definiert werden k¨onnen. b) Die Funktion f heißt von der Ordnung o (g(x)) f¨ur x → x0 , wenn es f¨ur jede Konstante C > 0 ein δ gibt, so dass A.3 erf¨ullt ist. Beispiel A.5 (O-Notation) 1.) Ein Polynom vom Grad N p(x) = N X an x n n=0 ist asymptotisch (d.h. f¨ur x → ∞) von der Ordnung O(xN ) bzw. f¨ur jedes > 0 von der Ordnung o(xN+ ). 2.) Der Satz von Taylor l¨asst sich f¨ur jede Funktion f ∈ C k (G) (G offen, k ∈ N) schreiben als f (x + h) = k−1 (n) X f (x) n h + O(hk ) n! n=0 bzw. k−1 (n) X f (x) n f (x + h) = h + o(hk−1 ). n! n=0 In dieser Form wird h¨aufig auch die Approximationsqualit¨at numerischer Verfahren untersucht und ausgedr¨uckt. 101 Satz A.6 Das Landausymbol O hat die folgenden Eigenschaften: (i) f (x) = O( f (x)). (ii) f (x) = o(g(x)) ⇒ f (x)=O(g(x)). (iii) Sei 0 < K ∈ R. Dann gilt: f (x) = K O(g(x)) ⇒ f (x)=O(g(x)). (iv) Wenn f1 (x)=O(g1 (x)) und f2 (x)=O(g2 (x)), dann gilt f1 (x) f2 (x) = O(g1 (x)g2 (x)). (v) Wenn f (x)=O(g1 (x)g2 (x)), dann gilt f (x) = g1 (x)O(g2 (x)). Der Beweis dieser Eigenschaften sowie die Untersuchung der Frage, in wie weit analoge Aus¨ sagen f¨ur das Symbol o(·) gelten, ist als Ubung u¨ berlassen. Beispiel A.7 (Komplexit¨at einer Polynomauswertung) Sei p das Polynom aus Beispiel A.5. F¨ur ein festes x0 ∈ R soll p0 (x0 ) auf die zwei folgenden Arten ausgewertet werden: /* Polynomauswertung 1.Methode */ 1.) Setze p0 := a0 2.) F¨ur n = 1, 2, ..., N: Setze Hilfsgr¨oße b := an ; F¨ur m = 1, ..., n: multipliziere b mit x0 p0 := p0 + b; /* Polynomauswertung 2.Methode */ 1.) Setze p0 = a0 Setze Hilfsgr¨oße b := x0 ; 2.) F¨ur n = 1, 2, ..., N: Setze Hilfsgr¨oße c := an ∗ b; p0 := p0 + c; b = b ∗ x0 ; 102 Die Komplexit¨at der 1. Methode lautet, wenn man nur die Multiplikation als wesentliche Operationen ber¨ucksichtigt: N X 1 n = N(N + 1) = O(N 2 ) 2 n=1 Die Komplexit¨at der 2. Methode hingegen ergibt: 2N = O(N) 103 ¨ A.5 Der Praprozessor Alle C-Compiler u¨ bersetzen die Quellprogramme in mehreren Durchg¨angen. Im ersten Durchgang werden die Programme vom sogenannten Pr¨aprozessor verarbeitet. Dieser Teil des CCompilers verarbeitet alle Anweisungen, die mit einem # beginnen, auch Pr¨aprozessor-Direktiven genannt. Er erzeugt aus der urspr¨unglichen Quelldatei einen tempor¨aren Quellcode, der anschließend von weiteren Hilfsprogrammen des Compilers verarbeitet wird (siehe Abbildung A.1 ). Pr¨aprozessor - Entfernen von Kommentaren Parser - Syntaxcheck Quellcode - Quellcode Quellcode - Ausf¨uhren von Direktiven Compiler, Assembler ¨ - Ubersetzen in Machinensprache Linker - Einbinden von Bibliotheksroutinen - Programm Objektdatei Abbildung A.1: Der Weg von der Quelldatei zum ausf¨uhrbaren Programm Dieser Vorgang bleibt f¨ur den Programmierer normalerweise unsichtbar. Man kann das Ergebnis aber auch speichern, um entweder seine Arbeit zu kontrollieren oder um sein Resultat anderweitig zu verarbeiten. gcc Quelltext.c -E -o dateiname Die so erzeugte Datei kann mit dem Editor betrachtet werden (vgl. Kapitel 1.6) ¨ A.5.1 Dateien einfugen (# include) Die wohl wichtigste Direktive f¨ur den Pr¨aprozessor ist #include. Mit ihrer Hilfe werden die Inhalte bestehender Dateien wie Textbausteine in das Programm eingef¨ugt. Dabei handelt es sich im Normalfall um Headerdateien. Headerdateien enthalten u¨ blicherweise die Endung .h 104 und d¨urfen keinen ausf¨uhrbaren Code enthalten, sondern nur Deklarationen. Dies liegt daran, dass sie unter bestimmten Umst¨anden in mehrere C-Dateien eingef¨ugt und damit mehrfach u¨ bersetzt werden. Enthielten sie fertige Funktionen, dann l¨agen die hinterher f¨ur den Linker mehrfach vor. Dadurch kann er keine eindeutige Bindung herstellen. Im folgenden Beispiel wird die Deklaration der Funktion summe in eine Headerdatei summe.h geschrieben. Im Quelltext des Programms wird dann die Deklaration der Funktion durch Einbinden des Headerfiles bewerkstelligt. Beispiel A.8 ( summe.h) float summe (float ,float); ¨ Beispiel A.9 ( Einfugen des Header summe.h) #include <stdio.h> #include "summe.h" /* Hauptprogramm */ int main() { float x=2.0; float y=3.3; printf("Summe von x+y = %f \n",summe(x,y)); return 0; } /* Definition summe-Funktion */ float summe (float x, float y) { return x+y; } Bemerkung A.10 • Die Dateinamen der Bibliotheksheader werden von spitzen Klammern <und >eingeschlossen. Derartig gekennzeichnete Dateien werden immer in den Bibliotheksverzeichnissen gesucht. Unter LINUX befindet sich die Header im Verzeichnis /usr/include. • Eigene Dateien werden durch Anf¨uhrungszeichen begrenzt. Sie werden dadurch vom Pr¨aprozessor im aktuellen Projektverzeichnis gesucht. 105 A.5.2 Konstanten definieren (#define) Der Pr¨aprozessorbefehl #define erlaubt es, eine Zeichenkette im Programm durch eine andere zu ersetzen. Die allgemeine Form sieht so aus: # define Name Zeichenkette Am Beispiel des folgenden Programms k¨onnen Sie die Arbeitsweise von #define nachvollziehen. Beispiel A.11 (#define) #include <stdio.h> #define PI 3.14 #define Mein_PI "Mein Pi ist :" int main() { float x=PI; printf(Mein_PI); printf(" %f\n",x); } Der Pr¨aprozessor manipuliert den Quellcode (vernachl¨assigt man die Direktive #include <stdio.h> zu: int main() { float x=3.14; printf("Mein Pi ist :"); printf(" %f\n",x); } Der Pr¨aprozessor kennt bereits einige eingebaute Konstanten, die nicht erst definiert werden m¨ussen und auch nicht per #define u¨ berschrieben werden sollten. Dies sind: LINE FILE DATE TIME f¨ur die aktuelle Zeilennummer f¨ur den Namen der kompilierten Datei f¨ur das aktuelle Datum f¨ur die aktuelle Uhrzeit Weitere n¨utzliche Anwendungen von Pr¨aprozessor-Direktiven k¨onnen der Literatur (z.B. in [Erl]) entnommen werden. 106 Literaturverzeichnis [Knu] D.E. Knuth: The Art of Computer Programming - Vol. 2,2nd ed., AddisonWesley [Kof] M. Kofler: Linux - Installation, Konfiguration, Anwendung, Addison-Wesley [Kru] G. Kr¨uger: Go To C-Programmierung, Addison Wesley [Oua] S. Oualline: Practical C Programming, O’Reilly [WeDaDa] M. Welsh, M.K. Dalheimer, T. Dawson: Linux- Wegweiser zur Installation & Konfiguration, O’Reilly [Erl] H. Erlenk¨otter: C - Programmieren von Anfang an, rororo [KerRit] B.W. Kernighan, D.M. Ritchie: Programmieren in C, Hanser Verlag [OraTal] A. Oram, S. Talbott: Managing Projects with make, O’Reilly [PrTeVeFl] W.H. Press, S.A. Teukolsky, W.T. Vetterling, B.P. Flannery: Numerical Recipes in C, Cambridge University Press [Kir] R. Kirsch: Einf¨uhrung in die Programmierung f¨ur Mathematiker, Vorlesungsskript Sommersemester 2004 [Car] P. A. Carter : PC Assembly http://www.drpaulcarter.com/pcasm/, 2003 107 language, Online , Index O-Notation 101 goto, 38 if(), 30 malloc(), 44 perror(), 80 printf(), 6, 26 read(), 78 realloc(), 44 remove(), 79 rename(), 79 scanf(), 27 sizeof(), 10 switch(), 31 while(), 36 call by reference, 53 call by value, 51 cast, 25 Compiler, 1 compilieren, 1 Computerprogramm, 1 cygwin, 5 Abrundung, 96 Abschneiden, 96 Adressoperator, 40, 41 Allokalisieren, 44 Arbeitsverzeichnis, 3 array, 11 ASCII-Tabelle, 38 Aufrundung, 96 Aufz¨ahlungstyp, 60 Ausl¨oschnung, 99 B-adische Entwicklung, 95 Befehl, 4 Bezeichner, 57 Bibliotheken, 93 Bibliotheksfunktionen, 47 C-Anweisung break, 38 calloc(), 44 case, 31 continue, 38 do-while(), 37 fclose(), 69 fgetc(), 72 fopen(), 69 for(), 33 fprintf(), 71 fputc(), 72 free(), 44 freopen(), 83 fscanf(), 71 fseek(), 81 ftell(), 81 fwrite(), 78 Datei, 2 Datenstrom, 69 Datenstrom:stderr, 70 Datenstrom:stdin, 70 Datenstrom:stdout, 70 Datentyp char, 9 signed, 9 unsigned, 9 const, 10 double, 9 long, 9 enum, 60, 67 FILE, 69 float, 9 108 int, 9 short, 9 signed, 9 unsigned, 9 pointer, 39 struct, 60 strukturierte, 60 typedef, 68 union, 60, 66 void, 9 zeiger, 39 Datentypen, 8 definiert, 8 Deklaration, 8 deklarieren, 8 Dereferenzoperator, 41 Distributionen, 2 string.h, 22 Headerdatei, 5 IEEE, 97 Initialisierung, 8 Interpreter, 1 interpretieren, 1 Kommandointerpreter, 4 Kommandozeilen-Parameter, 55 Kommentare, 5 Landau-Symbole, 101 libraries, 2 LINUX, 2 LINUX-Befehl ar, 93 cat, 4 cd, 4 cp, 4 ls, 4 make, 90 man, 4 mkdir, 4 more, 4 mv, 4 pwd, 4 rm, 4 Listen, 64 editieren, 1 Editor, 1 enum, 67 eps, 97 Exponenten¨uberlauf, 96 Felder, 11 dynamisch, 43 Festkommadarstellung, 95 Funktion call by reference, 53 call by value, 51 Funktionsparameter, 47 Funktionsrumpf, 48 R¨uckgabewert, 47 Funktionen, 47 Funktionsparameter, 47 Funktionsrumpf, 48 make, 90 Makros, 92 Manpage, 4 Mantisse, 95 Mantissenl¨ange, 95 Maschinencode, 1 Maschinengenauigkeit, 97 Maschinenzahl, 95 Mehrdateiprojekte, 84 Modul, 89 Modulen, 84 Monitor, 7 Gleitkommadarstellung, 96 Header float.h, 24 limits.h, 24 malloc.h, 44 math.h, 21 stdio.h, 5 offset, 81 Operationen, 8 109 Operator Adressoperator, 40 Assoziativit¨at, 19 Auswahloperator, 62 bitorientiert, 16 dekrement, 18 Dereferenzoperator, 41 inkrement, 18 logisch, 16 Priorit¨at, 19 Referenzoperator, 41 Strukturoperator, 61 Vergleichsoperator, 15 Zugriffsoperator, 41 Signum-Funktion, 30 stderr, 70 stdin, 70 stdout, 70 String, 11 Strings, 6 Struktur, 60 Suffix, 5 Target, 91 typedef, 68 Typkonversion, 25 underscore, 8 Union, 60 union, 66 UNIX, 2 ursprung, 81 Pfad, 3 absolut, 3 relativ, 3 Pointer, 39 Pointervariable, 40 portabel, 77 Postfixnotation, 19 Pr¨afixnotation, 18 Pr¨aprozessor, 5, 104 Pr¨aprozessor-Direktiven, 104 define, 106 include, 104 Pr¨aprozessordirektiven, 5 Programmflusskontrolle, 30 Variablen, 8 G¨ultigkeit, 49 global, 49 lokal, 49 Sichtbarkeit, 49 Verzeichnis, 2 Verzeichnisbaum, 3 Verzweigung, 30 Wildcard, 4 Zeichenketten, 6 Zeichenkettten, 12 Zeiger, 39 Zeiger auf Funktionen, 57 Zeigervariable, 40 Zugriffsoperator, 41 Zyklus abweisend, 36 nichtabweisend, 37 Z¨ahlzyklus, 33 Quelltext, 1 R¨uckgabewert, 47 Referenzoperator, 41 rekursiv, 54 Rundung, 96 Rundungsfehler, 96 absolut, 96 relativ, 96 Schleifen, 33 do-while, 37 for, 33 while, 36 Shell, 4 110
* Your assessment is very important for improving the work of artificial intelligence, which forms the content of this project
advertisement