Kurzskript zum Thema: ANSI C Alexander Rehbein Prof. Jörn Sesterhenn 20. November 2012 Stanford CS Education Library Dieses Kurzskript basiert auf dem frei erhältlichen Dokumenten #101, Essential C sowie #102, Pointers and Memory der Stanford CS Education Library, welche zusammen mit anderen Dokumenten unter http://cslibrary.stanford.edu frei erhältlich sind. 1 Vorwort Dieses C-Kurzskript ist als Begleitmaterial für die Lehreveranstaltung Einführung in die Informationstechnik für Ingenieure (EDVI) bei Prof. Sesterhenn an der TU Berlin zu sehen. Es sollen alle bedeutenden Themen in der relevanten Tiefe enthalten sein. Ggf. ist dieses Skript etwas ausführlicher, um das Verständnis zu fördern. Zusammen mit den unten erwähnten Kurzskripten bilden Programming in ANSI C 1 2 von E . Balagurusamy, The C Programming Language von Kernighan und Ritchie sowie Wikipedia den gesamten Quellliteraturbestand. Zum Teil steht hier auch nur die halbe Wahrheit, da die ganze Wahrheit zu umfassend für diese Vorlesung wäre. Dieses Skript ist kein Ersatz für den Gang zu Vorlesung, Übung oder Tutorium! Es soll als Lern- und vor allem Verständnisstütze dienen. Inhaltsverzeichnis 1 Vorwort 1 2 Einführung 5 2.1 3 4 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.1.1 Wichtiger Hinweis vorab . . . . . . . . . . . . . . . . . . . . . 5 2.1.2 Der erste Quelltext . . . . . . . . . . . . . . . . . . . . . . . . 5 Datentypen, Konstanten, Variablen und Operatoren 6 3.1 Grundlegende Datentypen . . . . . . . . . . . . . . . . . . . . . . . . 6 3.2 Konstanten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 3.3 Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 3.4 Kommentare 3.5 Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 3.5.1 Zuweisungs-Operator `=` 3.5.2 Mathematische Operatoren 3.5.3 Einserinkrement/-dekrement-Operatoren ++ -- 3.5.4 Vergleichs-Operatoren 3.5.5 Logische Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 . . 12 . . . . . . . . . . . . . . . . . . . . 13 . . . . . . . . . . . . . . . . . . . . . 13 Kontrollstrukturen 14 4.1 Geschweifte Klammern 4.2 Verzweigungen 4.3 10 . . . . . . . . . . . . . . . . . . . . . . . . . 15 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 4.2.1 Die if-Anweisung 4.2.2 Die switch-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . 15 . . . . . . . . . . . . . . . . . . . . . . 16 Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 4.3.1 Die while-Anweisung 4.3.2 Die do-while-Anweisung . 4.3.3 Die for-Anweisung . . . . . . . . . . . . . . . . . . . . . . 17 . . . . . . . . . . . . . . . . . . . . 17 . . . . . . . . . . . . . . . . . . . . . . . . 17 3 5 6 7 Pointer 18 5.1 Der Adress-Operator & . . . . . . . . . . . . . . . . . . . . . . . . . . 18 5.2 Der Inhalts-Operator * . . . . . . . . . . . . . . . . . . . . . . . . . . 20 Array von Elementen vom Datentyp <Datentyp> 21 6.1 Der Name eines Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . 23 6.2 Mehrdimensionale Arrays . . . . . . . . . . . . . . . . . . . . . . . . 24 6.3 Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 Funktionen 26 7.1 Funktionsdenition . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 7.1.1 Der Funktionsheader . . . . . . . . . . . . . . . . . . . . . . . 27 7.1.2 Der Funktionskörper . . . . . . . . . . . . . . . . . . . . . . . 29 7.2 Funktionsaufrufe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 7.2.1 Funktionsdeklaration . . . . . . . . . . . . . . . . . . . . . . . 29 7.2.2 Der Funktionsaufruf . . . . . . . . . . . . . . . . . . . . . . . 31 7.2.3 Call by Value . . . . . . . . . . . . . . . . . . . . . . . . . . 33 7.2.4 Call by Reference . . . . . . . . . . . . . . . . . . . . . . . . 34 A Schlüsselwörter 35 B Einige der Standardfunktionen aus B.1 B.2 B.3 Bildschirmausgaben printf stdio.h 36 . . . . . . . . . . . . . . . . . . . . . . 36 B.1.1 Zeichenketten mit Platzhalter der 1. Parameter von printf 37 B.1.2 Zeichenketten mit Platzhalter der 2. Parameter von printf 38 B.1.3 Wie die Zeichen ausgegeben werden . . . . . . . . . . . . . . 40 fgets . . . . . . . . . . . 40 Einlesen in char-Arrays Einlesen in char-Arrays oder Variablen von Strings oder char-Arrays von der Tastatur sscanf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . char-Arrays B.4 Einlesen in Variablen oder B.5 Önen und Schlieÿen von Dateien FILE* 40 von Tastatur . . . . . . . . . 41 fopen und fclose . . . . . . . . 43 B.5.1 Der Datentyp . . . . . . . . . . . . . . . . . . . . . . 43 B.5.2 fopen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 B.5.3 fclose . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 4 fprintf . . . . . . . . . . . . . . . . . . . . . . B.6 Ausgabe in Dateien B.7 Einlesen von Dateien fscanf . . . . . . . . . . . . . . . . . . . . . 45 46 5 2 2.1 Einführung Motivation Die Programmiersprache C wurde vor mittlerweile mehr als 20 Jahren von Kernighan und Ritchie erstmalig in ihrem Buch The C Programing Language deniert und daraufhin 1989 erstmalig vom American National Standards Institute als ANSI-Standard adoptiert. Seitdem sind einige neuere C-Standards eingeführt worden; zum Erlernen der Grundlagen reicht es jedoch, sich an den einschlägigen ANSI-Standard aus dem Jahr 1990 zu halten. Die Neuerungen seitdem sind für Anfänger wohl eher uninteressant. Für diejenigen, welche sich fragen, wofür sie C mal gebrauchen können: Bei eingebetteten Systemen, in der Systemprogrammierung und nicht zuletzt in der OpenSource-Programmierung wird die C-Programmiersprache gerne genutzt. Ein Vorteil ist, dass sie auf verschiedenen Rechnerarchitekturen leicht zu implementieren ist sie sollte per Design leicht kompilierbar sein. Dies ist wohl mit einer der Gründe, warum C zu den am weitesten verbreiteten Programmiersprachen zählt. Gleichzeitig orientiert sich die Syntax sehr vieler Programmiersprachen an jener von C. Das heiÿt, wer einmal C gelernt hat, wird sich zumindest von der Syntax einiger neuer Programmiersprachen nicht allzu schnell verwirren lassen und ggf. ein paar Anweisungen schon verstehen, ohne die Sprache zu kennen. 2.1.1 Wichtiger Hinweis vorab Manche Programmiersprachen können leicht genutzt werden, auch wenn man kaum versteht, was man schreibt. Man schreibt ein paar Zeilen, schaut, ob es funktioniert - und wenn nicht, dann probiert man nochmal ein bisschen rum, bis es endlich klappt. Ggf. korrigiert der Compiler bestimmte Fehler schon von selbst oder die Fehler werden behoben, während das Programm läuft (man nennt diese Zeitspanne auch Laufzeit ). Für C ist das wohl eine der schlechtesten Herangehensweisen. C ist eine Programmiersprache, die davon ausgeht, dass der Programmierer genau weiÿ, was er tut. Daher ist sehr viel erlaubt und vieles, was man gar nicht möchte, passiert einfach. Genau aus diesem Grund ist C schwer für Anfänger. Daher ist der beste Ratschlag, den man bei C geben kann, nichts einzugeben, wovon man nicht weiÿ, was es tut. Die Fehlersuche (das sog. Debugging ) kann sehr mühselig sein und daher ist es besser, sich vorab beim Schreiben der jeweiligen Anweisungen etwas mehr Zeit zum Überlegen zu nehmen. Da im weiteren Verlauf des Textes erstmal viel (wichtige) Theorie behandelt wird, ohne dass man sie sofort ausprobieren könnte, folgt: 2.1.2 Der erste Quelltext Ein C-Programm wird erstellt, indem man Quelltext schreibt und diesen dann in eine ausführbare Datei übersetzt. Quelltext entspricht vom Datenformat her einer gewöhnlichen Textdatei, in welcher die Anweisungen stehen, welche der Computer ausführen soll. Was für Anweisungen es gibt, wann und wie man diese schreibt etc., das soll in diesem Skript erklärt werden. Hier das Beispiel es sei nahe gelegt, dieses abzutippen und in einer Textdatei zu speichern: 6 #i n c l u d e < s t d i o . h> 1 2 i n t main ( void ) { 3 p r i n t f ( " Ich 4 5 return 0 ; 6 } bin eine Z e i l e \n" ) ; Quelltext 1: Ein erstes Beispiel In ANSI-C werden fast alle Anweisungen in Funktionen zusammengefasst. Damit die Anweisungen einer Funktion durchgeführt werden, muss die Funktion ausgeführt bzw. aufgerufen werden. Funktionsaufrufe selbst sind Anweisungen. Damit immer klar ist, welche Anweisungen zuerst durchgeführt werden, ist in C festgelegt, dass zu allererst die Funktion main aufgerufen wird. Diese muss in jedem C-Programm deniert sein! Diese Denition geschieht hier durch die Zeilen 3 bis 6, wobei der Text zwischen dem geschweiften Klammerpaar, welches in Zeile 3 geönet wird, die Anweisun- main-Funktion festlegt. Zeile 4 beinhaltet den Befehl, die Buchstabenkette Ich bin eine Zeile mit einem darauf folgenden Zeilenumbruch darzustellen (dafür steht das '\n'), denn automatisch geschieht das hier nicht. gen der Dieser Befehl oder genauer die verwendete Funktion muss jedoch irgendwo deniert sein, da er keine elementare Anweisung der Programmiersprache C darstellt. Die Anweisung in Zeile 1 sorgt dafür, dass ein vorgeschriebener Quelltext (die Datei stdio.h), welcher diese Denitionen mitbringt, return 0; berichtet dem Betriebssystem, dass eingebunden wird. Die Anweisung das Programm fehlerfrei durchge- laufen ist (sofern die Zeile des Quelltextes auch ausgeführt werden konnte). Wichtig: Hinter jeder Anweisung (also auch jedem Funktionsaufruf ) muss ein Semikolon stehen um das Ende der Anweisung bzw. des Funktionsaufrufes zu markieren. Auf einem Linux-basierten Betriebssystem kann man den Quelltext 1 mit folgendem Befehl in der Kommandozeile in die ausführbare Datei gcc <Quelltextdatei> −o und dann durch Eingabe von programmEins übersetzen programmEins programmEins ausgeführt werden. Das Zeichen ` ' soll dabei andeuten, dass an dieser Stelle ein Leerzeichen stehen muss. Hinweis: Für das gesamte Skript gilt genau wie bei allen Unterlagen zur EDV1, Namen in eckigen Klammern müssen für die Implementierung durhc sinnvolle Werte ersetzt werden. 3 Datentypen, Konstanten, Variablen und Operatoren 3.1 Grundlegende Datentypen Wer programmiert, möchte Daten bzw. Werte durch den Computer verarbeiten, bearbeiten und erstellen lassen dieser kennt wiederum nur jene Informationen aus dem ihm zur Verfügung stehenden Speicher. Dort müssen also die zu verarbeitenden Werte gespeichert sein. 7 Jeder Speicherbereich im Computer kann logisch als einfache, endliche Folge von Bits betrachtet werden. Bits 1 sind Unbestimmte, welche die Werte Eins oder Null annehmen können. Worauf es ankommt, ist, wie diese Einsen und Nullen interpretiert werden. Diese Vorgabe wird gemacht, indem man Datentypen zuordnet. C kennt nur ein paar grundlegende Datentypen (aus diesen kann man sich bei Bedarf jedoch komplexere Datenstrukturen basteln). Wichtig sollen erstmal nur folgende sein: int2 Das C-Standardformat für Ganzzahlen. Je ein int wird in mindestens 16 Bits gespeichert. Normal sind 32 Bits. char3 Dieser Datentyp steht für Buchstaben aus der ASCII-Kodierung. float Der Datentyp für Gleitkommazahlen 5 In der Regel wird ein 6 Bits gespeichert. Die Genauigkeit liegt bei circa 6 Ziern. double 4 float in 32 Der Datentyp für Gleitkommazahlen mit doppelter Genauigkeit ; es stehen also normalerweise 64 Bits zur Verfügung. Die Genauigkeit liegt bei circa 15 6 Ziern. Hinweis: Wie viel Speicherplatz zum jeweiligen Datentypen gehört, hängt vom Compiler dem Programm, mit welchem der vom Programmierer geschriebene Quelltext (dieser ist eine gewöhnliche Textdatei) in eine ausführbare Datei (diese ist im allgemeinen keine gewöhnliche Textdatei) übersetzt wird ab. Der ANSI-Standard legt für die Gröÿe der obigen Datentypen keine genauen Zahlen, sondern nur Untergrenzen fest. Dadurch ist es einfacher, C-Compiler auf vielen verschiedenen Hardwaresystemen umzusetzen. Deshalb ist für den Programmierer besondere Vorsicht geboten, wenn sein Programm auf verschiedenen Systemarchitekturen laufen soll. Wie man diese Probleme umgehen kann, soll hier nicht weiter behandelt werden. Zu den Datentypen float und double sollte man sich merken, dass sie alles andere float- oder double-Werten als genau sind. Daher sollte man bei Vergleichen von 7 nie auf Gleichheit prüfen es sei auf die Vorlesung verwiesen. 3.2 Konstanten Konstanten sind feste Werte. Abstrakt besteht kein Erklärungsbedarf dazu, was ein Wert ist; in der Programmierung sollte man wissen, dass ein Wert aus einer endlichen Abfolge von Einsen und Nullen (bzw. `gesetzten' Bits) zusammen mit der 1 Abkürzung für `binary digit' (engl.: binäre Zier) 2 Abkürzung für `integer' (engl.: Ganzzahl) 3 Abkürzung für `character' (engl.: Buchstabe) 4 Es handelt sich um Ganzzahlen, welche nur für Ein- und Ausgaben auf dem Bildschirm über die ASCII-Codierung ein Buchstabe zugeordnet wird. Man kann also theoretisch in C mit Buchstaben rechnen. Je ein 5 Dieser char wird in einem Byte, also acht Bits, gespeichert. Ausdruck beschreibt die Tatsache, dass bei Gleitkommazahlen die signikanten Ziern einer Zahl unabhängig davon, wo das Komma liegt, im Speicher abgelegt sind. Wo das Komma, dann hingehört, wird in den vorhergehenden Bits festgelegt das Komma `gleitet' also in die Ziern rein. 6 Diese werden nicht ab dem Komma, sondern ab der ersten Zier die ungleich Null ist von links gezählt. 7 Statt direkt die Gleichheit zu überprüfen, würde man in der Praxis schauen, wie weit zwei Gleitkommazahlen auseinander liegen. Würde der Unterschied klein genug ausfallen, könnte man die Gleichheit annehmen und den Unterschied der mangelhaften Rechengenauigkeit zuschieben. 8 Vorgabe, wie diese interpretiert werden (also dem Datentypen) besteht. Eine Folge gesetzter Bits alleine legt noch keinen Wert fest, es gehört immer ein Datentyp dazu! Kurz gesagt: Ein fester Wert, oder auch eine Konstante besteht aus einer endlichen Folge gesetzter Bits und dem zugehörigen Datentypen. Aus den in Abschnitt 3.1 gelisteten Datentypen entstehen: char-Konstanten: char-Konstanten stehen immer in einzelnen Anführungszei- chen. Zum Beispiel wäre das Alphabet (in Konstanten geschrieben): 'a' 'b' 'c' 'y' ... 'z'. Dabei wird zwischen Groÿ- und Kleinbuchstaben unterschieden, d.h. nicht gleich 'a'. 'A' ist Zu den Buchstaben werden auch Sonderzeichen gezählt, wie z.B. Zeilenumbrüche: Tabstops: '\n' 8 '\t' das Nullzeichen: '\0'. Es handelt sich dabei nicht um die Null als Buch'0'). Vielmehr ist das Nullzeichen '\0' staben (das wäre die Konstante ein Kontrollzeichen, welches das Ende von Zeichenketten markiert aber dazu später mehr. int-Konstanten: Diese Konstanten werden in der gewöhnlichen Dezimalschreib- weise geschrieben, wobei es sich natürlich um ganze Zahlen handeln muss, also z.B.: -3 -2 -1 0 1 2 3. Gleitkommazahl-Konstanten: Konstanten für Gleitkommazahlen werden fast wie in der gewöhnlichen Dezimalschreibweise geschrieben. Jedoch wird der angelsächsischen Konvention entsprechend statt eines Kommas ein Punkt double-Konstante. Möchte man stattdessen float-Konstante, so ist ein `f ' dahinterzuschreiben. Beispielsweise würde man für die Zahl 2, 71 gesetzt. Dann ist die Zahl eine eine 2.71 double. Möchte man unbedingt eifloat mit float-Werten arbeitet man eigentlich nur, wenn die verfügbare Hardware zu wenig Speicher zum Arbeiten mit doubleWerten hat so schreibt man stattdessen 2.71f. Es sei nochmals angemerkt, schreiben, das ist eine Konstante vom Typ ne Konstante vom Typ dass Gleitkommazahlen im allgemeinen ungenau sind (siehe weiter oben). Zum Beispiel kann die Zahl 0.4 nicht exakt als float dargestellt werden. Dies soll jedoch nicht weiter Thema des Skriptes sein. Hier sei auf die Vorlesung zur Zahlen- und Zeichendarstellung verwiesen. 3.3 Variablen Der Begri `Variable' erweckt in der Regel erstmal eine mathematische Assoziation. Diese sollte man jedoch zunächst vergessen. In ANSI-C (und in den meisten anderen Programmiersprachen) ist eine Variable nichts weiteres, als ein während der 8 `n' wie newline 9 Ausführung des Programmes 9 reservierter Speicherbereich, dem ein Datentyp und ein Name zugeordnet wurden. Hat man eine Variable, welche auf einem der grundlegenden Datentypen 10 basiert, dann kann man den aktuellen Inhalt dieses Speicherbereiches eine endliche Folge von Einsen und Nullen über den Namen der Variablen ansprechen. Dieser Inhalt wird dann dem Datentypen der Variablen gemäÿ interpretiert. Über den Namen der Variablen erhält man also einen Wert 11 . Erstellen (man sagt oft denieren ) kann man sich Variablen der grundlegenden Datentypen wie folgt: <Datentyp> <Name>; Dies geschieht in der main-Funktion oder innerhalb der jeweiligen anderen Funktion. Die Variable wird dann zunächst nur der Funktion, in welcher sie deniert wurde, 12 bekannt sein. Der <Name> der Variablen muss ein sogenannter Bezeichner sein siehe Abbildung 1. Es handelt sich dabei um einen durch den Programmierer denierten Namen, welcher aus einer Abfolge von Buchstaben und Ziern besteht, wobei Unterstriche auch erlaubt sind diese werden gerne als Ersatz für Leerzeichen genutzt, welche nicht erlaubt sind. Es müssen alle Regeln aus Abbildung 1 erfüllt sein. Groÿ- und Kleinbuchstaben werden unterschieden! 1. Das erste Zeichen muss ein lateinischer Buchstabe, eine Zier oder ein Unterstrich sein. 2. Die Zeichenfolge darf nur lateinische Buchstaben, Ziern oder Unterstriche enthalten. 3. Maximal 31 Zeichen. Alles ab dem 32. Zeichen wird ignoriert. 4. Darf kein Schlüsselwort a sein. 5. Darf kein Leerzeichen enthalten. a Eine Liste von Schlüsselwörtern bendet sich in Anhang A. Abbildung 1: Regeln für Bezeichner Es folgt ein Beispiel mit dem Datentypen int und dem Wort `zahl' als Namen: int zahl ; . Andere gültige Beispiele sind: char c; char platzhalter ; char Platzhalter; oat f ; double z;. 9 dieser Zeitraum wird auch Laufzeit bzw. im Englischen runtime genannt 10 Siehe Abschnitt 3.1 11 siehe Abschnitt 3.2 12 In ANSI C gibt es weitere Variablentypen, die aber den Rahmen von der EDV1 Veranstaltung sprengen würden. Für interessierte Studenten sei auf die Literatur zum Thema scope verwiesen 10 Was mit diesen Anweisungen bezweckt wird, sollte sich der Leser an Hand der bisherigen Ausführungen verständlich machen können. Dies sei zur Übung empfohlen. Insbesondere mache man sich klar, warum die Variable zur Variablen Platzhalter platzhalter nicht identisch ist. Nach der Denition einer Variablen kann bei den grundlegenden Datentypen ihr Name als Platzhalter für ihren aktuellen Wert aufgefasst werden. Wichtig: Man beachte, dass nach einer Variablendenition nicht klar ist, welchen Wert die Variable hat! Es wurde ein Speicherbereich reserviert, aber nicht beschrieben; d.h. in dem Speicherbereich steht irgendeine Sequenz von Einsen und Nullen, welche unbekannt ist. Es ist ein typischer Anfängerfehler, sich eine Gleitkommazahl- oder Ganzzahlvariable zu denieren und später dann anzunehmen, der Wert sei 0. Dies ist jedoch im allgemeinen nicht gegeben; man muss der Variable diesen oder ggf. einen anderen ersten Wert zuweisen; sie initialisieren. Wie man dies macht, wird im Folgenden noch beschrieben. 3.4 Kommentare In den nächsten Abschnitten kommen Code-Beispiele, welche Kommentare enthalten, das heiÿt Text, welcher Anmerkungen des Programmierers enthält, aber beim Übersetzen des Quelltextes in eine ausführbare Datei ignoriert wird. Kommentare benden sich zwischen den Zeichenpaaren /* und */ nach dem Schema /* beliebiger Kommentar */ Achtung: Viele Compiler erlauben einzeilige Kommentare mit C++-Syntax // obwohl diese sind und nicht zu ANSI-C gehören. //Kommentar Kommentare sind zusammen mit gut gewählten Variablen- und Funktionsnamen ein wichtiger Bestandteil von gutem Programmierstil. Der Zweck ist nicht, die einzelnen Anweisungen in eigene Worte zu übersetzen. 13 Vielmehr geht es darum, komplizierte Stellen zu erläutern oder das, was nicht oensichtlich ist, hervorzuheben. 3.5 Operatoren Operatoren sind Grundbausteine von Anweisungen. 3.5.1 Zuweisungs-Operator `=` Der Zuweisungs-Operator ist das einzelne Gleichheitszeichen `=` . Eine komplette Zuweisungsanweisung sieht so aus: <Variable> = <2. Variable oder beliebige Konstante>; wobei die erste und die zweite Variable identisch sein können. Der ersten Variablen wird dann der Wert des Speicherbereiches der zweiten Variablen bzw. die Konstante auf der rechten Seite zugewiesen. Dabei müssen die Datentypen auf beiden Seiten 13 Anfängern kann dies natürlich beim Lernen der Sprache helfen. Es sollte jedoch keine Dauer- angewohnheit darstellen. 11 des Operators zueinander passen. Wichtig: Der Operator `=` ist nicht zu verwechseln mit dem Gleichheitszeichen in der Mathematik. So ist eine Anweisung i = i + 1 mathematisch falsch, als Operator in der Programmierung aber sehr verbreitet. 1 2 3 4 i n t i , j ; / * m e h r e r e V a r i a b l e n vom g l e i c h e n D a t e n t y p * / char c ; double f ; f l o a t I n i = 0 ; /* V a r i a b l e b e i d e r D e f i n i t i o n i n i t i a l i s i e r e n */ 5 6 /* Leerzeilen , Leerzeichen , im C− Q u e l l t e x t werden 7 i mit Tabs und Zeilenumbrueche w e n i g e n Ausnahmen beliebig koennen gesetzt */ = 4; /* 8 j = i ; 9 c = 'x ' ; 10 f = 2.45; j wird damit den Wert 4 haben */ Quelltext 2: Beispiele von Zuweisungen Hinweis: Der Zuweisungsoperator zusammen mit beiden Operanden ist ein Ausdruck, welcher den zugewiesenen Wert zurückgibt. Das heiÿt, der Zuweisungsoperator mit seinen beiden Operanden kann im Quelltext wie eine Konstante vom Datentyp des zugewiesenen Wertes genutzt werden. Sie sollte aber in Klammern gesetzt werden, z.B. mit den Variablen aus Quelltext 2 so: j = ( i = 4 ) ; Damit erreicht man das gleiche für die Variablen sungen betreend i und j i und j wie in den beiden Anwei- in Quelltext 2. Allerdings ist die Variante in Quelltext 2 uneingeschränkt vorzuziehen, da sie übersichtlicher ist. Dieses Beispiel soll nur zeigen, was das `Zurückgeben' bzw. die `Rückgabe' eines Wertes bedeutet. Wie schon zuvor erwähnt, haben Variablen, nachdem sie deniert wurden, keinen 14 Variablen der grundlegenden Datentypen kann Wert, welcher uns bekannt ist. man entsprechend Zeile 4 in Quelltext 2 gleich bei der Denition initialisieren oder getrennt von der Denition in den darauolgen Zeilen vom Wert her festlegen. Dies sollte man immer tun, denn dies erleichtert die Fehlersuche. Mathematische Operatoren 3.5.2 Es gibt folgende mathematische Operatoren, mit welchen Zahlenwerte verknüpft werden können: + Addition - Subtraktion * Multiplikation / Division 14 Siehe S. 10. 12 Verknüpft man Werte mit unterschiedlichen (Zahl-)datentypen mit diesen Operatoren, so macht der Compiler aus dem Wert mit dem `niederen' Datentypen den entsprechenden Wert des `höheren' Datentypen und rechnet mit diesem weiter. Die Hierarchie, aufsteigend von links nach rechts, ist: char, int , oat , double. Wichtig: Der Divisionsoperator verhält sich je nach Datentyp der Operanden un- int-Werte, so wird eine Integer-Division durchgeführt, 7/4 gibt dann den Wert 1 zurück, da die Vier einmal ganz in der Sieben enthalten ist, analog gibt der Ausdruck 1/3 den terschiedlich. Teilt man zwei d.h. eine Division ohne Rest: Der Ausdruck Wert 0 zurück, da die Drei kein Teiler der Eins ist. Möchte man die `gewöhnliche' Division durchgeführt haben, so muss mindestens einer der Operanden eine Gleitkommazahl-Konstante sein. Dies erreicht man dadurch, dass man die Zeichen .0 oder nur einen Punkt hinter die ganze Zahl schreibt. Also z.B. 1.0/3 oder 1./3 an Stelle von 3.5.3 1/3. Einserinkrement/-dekrement-Operatoren ++ -- Bei der Programmierung nutzt man Integer-Variablen oft zum Zählen. In diesem Zusammenhang möchte man häug den Wert eines sei i ein int): int um den Wert 1 erhöhen (es i = i + 1 ; /* Zuweisung und Additionsoperator zusammen */ Alternativ kann man auch den Operator für das Einserinkrement nutzen, das spart Schreibarbeit: i ++ ; Weitere derartige Operatoren und was sie bewirken: 1 /* v a r = v a r + 1 * nach * Benutzung des Wertes von v a r : */ Benutzung des Wertes von v a r : */ * Benutzung des Wertes von v a r : */ * Benutzung des Wertes von v a r : */ v a r ++; 2 3 4 /* var = var − 1 * nach * v a r −−; 5 6 7 /* var = var + 1 * vor ++v a r ; 8 9 10 /* var = var − 1 * vor −−v a r ; 11 12 definiere und int i =4 j =0 , 15 j = i ++; 16 j_ = ++i ; 13 14 /* , initialisiere i , j , j_ * / j_ =0; j hat nun den Wert 4, i den Wert 5 */ / * j_ /* hat nun den Wert 5, i den Wert 5 */ 13 3.5.4 Vergleichs-Operatoren Mit diesen Operatoren kann man Werte von Zahlendatentypen sowie char-Werte untereinander vergleichen. Ist der entstehende Ausdruck wahr, so wird an Stelle des Ausdrucks der Integer-Wert 1 zurückgegeben, ansonsten eine 0. == gleich != ungleich < kleiner > gröÿer <= kleiner oder gleich => gröÿer oder gleich Wichtig: `=` (Zuweisung) und `==` (Vergleich) sind unbedingt auseinanderzuhalten! Beide zu verwechseln ist eine typische Fehlerquelle beim Programmieren. Eine genauere Erklärung des resultierenden Problems folgt später. 3.5.5 Logische Operatoren In ANSI-C gibt es wie in anderen Programmiersprachen eine 1 if ( <l o g . Ausdruck> ) <Anweisung ( en ) 2 if-Anweisung: { 1> 3 } 4 else { <Anweisung ( en ) 5 1> 6 } Die <Anweisung(en) 1> wird/werden dabei ausgeführt, wenn der logische Aus- druck wahr ist, ansonsten werden die Anweisunge(en) 2 ausgeführt. Als logischer Ausdruck kann irgendein Ausdruck eingesetzt werden, welcher einen Wert vom Datentyp int zurückgibt, z.B. für eine Integer-Variable i der Ausdruck (i <= 100). Zum Verknüpfen mehrerer solcher Ausdrücke zu einem neuen logischen Ausdruck kann man die logischen Operatoren: ! Logische Verneinung, meistens genutzt in der Form && Logisches `und'. || Logisches `oder' (nicht exklusives `oder'). !(<logischer Ausdruck>) verwenden. Ein logischer Ausdruck in C gilt als falsch, wenn er eine 0 zurückgibt int-Wert ungleich 0 zurückgibt. Möchte man beii==4 (sei i eine int-Variable) eine Anweisung durchführen, und als wahr, wenn er irgendeinen spielsweise für den Fall so schreibt man 14 if ( i == 4 ) <Anweisung> { } (den else-Teil darf man weglassen, wenn für Nichtzutreen der if-Bedingung nichts zu tun ist). Schreibt man aus Versehen stattdessen /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * DAS IST EIN FEHLERBEISPIEL * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ if ( i = 4 ) { <Anweisung> } so passiert folgendes: Zuerst wird der Variablen Die Zuweisung gibt dann den Wert zwischen den 4 i die int-Konstante 4 zugewiesen. zurück und daher ist der logische Ausdruck if-Klammern wahr, weil ein Wert ungleich <Anweisung> ausgeführt. 0 zurückgegeben wird. Damit wird also jedes Mal Wichtig: Ein typischer Fehler ist, aus Versehen statt zwei Kaufmannsund-Zeichen (&&) ein einzelnes (&) zu schreiben, selbiges gilt für die Pipes (statt zwei Pipes (||) eine einzelne (|) ). Als Einzelzeichen geschrieben handelt es sich aber nicht mehr um logische Operatoren, sondern um Operatoren mit einer anderen Funktion d.h. es entsteht ein gültiger Ausdruck, aber mit einer anderen Bedeutung welche an dieser Stelle nicht weiter erläutert werden soll. Zusätzlich gibt es noch folgende Erweiterungen zum Zuweisungs-Operator: /* 1 2 3 4 5 Anweisung <Variable> += < <Wert> <Wert> > * / <Variable> −= < <Wert> <Wert> > * / <Variable> *= < <Wert> <Wert> > * / <Variable> /= < <Wert> <Wert> > * / /* was die Anweisung macht >; /* <Variable> = <Variable> + < >; /* <Variable> = <Variable> − < >; /* <Variable> = <Variable> * < >; /* <Variable> = <Variable> / < */ 6 /* 7 konkretes 8 float Beispiel : */ dieser_float_ist_gut ; 9 dieser_float_ist_gut = 50; 10 d i e s e r _ f l o a t _ i s t _ g u t += 5 ; / * Wert der Variablen : 55 * / 11 dieser_float_ist_gut / * Wert der Variablen : 110 * / *= 2; Sie sollten jedoch aus Übersichtlichkeitsgründen nur dann eingesetzt werden, wenn der Variablenname sehr lang ist. 4 Kontrollstrukturen Beim Programmieren möchte man oft eine bestimmte Sequenz von Anweisungen nur unter bestimmten Bedingungen ausführen, mehrfach ausführen oder mehrfach ausführen bis eine gewisse Bedingung zutrit ggf. auch eine Kombination des gerade genannten. Kontrollstrukturen sind Anweisungen, welche dies ermöglichen. 15 4.1 Geschweifte Klammern Damit klar ist, welche Sequenz von Anweisungen gemeint ist, setzt man die betroenen Anweisungen in geschweifte Klammern. Zusammen mit den geschweiften Klammern bilden diese Anweisungen dann einen Anweisungsblock. 4.2 Verzweigungen 4.2.1 Die if-Anweisung einfache Verzweigung 1 /* 1. 2 if ( <l o g . */ Ausdruck> ) { <Anweisungen> 3 4 Moeglichkeit } 5 6 /* 2. 7 if ( <l o g . 9 { } else { <Anweisungen> 11 12 */ Ausdruck> ) <Anweisungen> 8 10 Moeglichkeit } 15 Ist der logische Dabei steht `log. Ausdruck' für irgendeinen logischen Ausdruck. Ausdruck wahr, so werden die Anweisungen in den geschweiften Klammern direkt nach dem `if' ausgeführt, ansonsten ggf. diejenigen nach dem `else''. Handelt es sich bei den Anweisungen nur um eine einzelne, so können die geschweiften Klammern weggelassen werden. mehrfache Verzweigung Man kann die if-Anweisung natürlich ineinander verschachteln, wenn man möchte, also nach dem Muster 1 if ( <l o g . A u s d r u c k 1> ) { <A n w e i s u n g e n 1> 2 3 } 4 else if ( <l o g . A u s d r u c k 2> ) { A u s d r u c k N> ) { <A n w e i s u n g e n 2> 5 6 } 7 else if ( ... ) { 8 ... 9 10 } 11 else if 13 } 14 else { <Anweisungen> 15 16 ( <l o g . <A n w e i s u n g e n N> / * 12 } 15 Siehe S. 13 d i e N− t e n An w e i su n g e n */ 16 Ein Spezialfall davon wäre if 1 <Variable> == <Wert 1> ( ) { <A n w e i s u n g e n 1> 2 3 } 4 else if ( <Variable> == <Wert 2> ) { <A n w e i s u n g e n 2> 5 6 } 7 else if ( ... ){ 8 ... 9 10 else 11 if ( <Variable> == <Wert N> ) { d i e N− t e n <A n w e i s u n g e n N> / * 12 13 } 14 else { An w e i su n g e n */ <Anweisungen> 15 } 16 Quelltext 3: Spezialfall der if-else-Verzeigung Man prüft hier immer wieder den Wert ein- und derselben Variablen. Das lässt sich 1 mit der switch-Anweisung 4.2.2 Die vereinfachen. switch-Anweisung switch ( <Variable> ) { 2 3 case <Wert 1> : <Anweisungen 1> case <Wert 2> : <Anweisungen 2> 4 break ; 5 6 7 break ; 8 9 ... 10 11 case 12 13 <Wert N> : <Anweisungen N> break ; 14 15 default : 16 <Anweisungen> 17 18 } Quelltext 4: der switch als Alternative zu Quelltext 3 In den Quelltexten 3 und 4 ist dabei der selbe Wert für den jeweiligen Platzhalter einzusetzen. Wichtig: Die break-Anweisung (siehe Zeile 5, 8 und 14) darf nicht vergessen wer- den, ansonsten werden die Anweisungen aus dem darunter liegenden `case' ebenfalls abgearbeitet! 17 4.3 Schleifen 4.3.1 Die while-Anweisung Es handelt sich hierbei um eine sogenannte Schleife, da Vorgänge ggf. wiederholt werden. <log. Ausdruck> <Anweisungen> while ( ) { } Die while-Anweisung prüft den logischen Ausdruck und wenn dieser wahr ist, so werden die Anweisungen ausgeführt. Danach wird erneut geprüft, ob der logische Ausdruck zutrit und wenn dieser wahr ist, werden die Anweisungen wieder ausgeführt etc. Man beachte, dass die Anweisungen ggf. kein einziges Mal ausgeführt werden. Wichtig: Die while-Schleife ist kein Ersatz für die if-Anweisung! Man bastelt sich sehr schnell eine Endlosschleife. 4.3.2 Die do-while-Anweisung while-Anweisung Diese Anweisung funktioniert wie die mit dem Unterschied, dass der logische Ausdruck erst nach der letzten Anweisung geprüft wird. Die Anweisungen werden also mindestens einmal ausgeführt. do { } 4.3.3 <Anweisungen> <log. Ausdruck> while ( Die for ( { ) ; for-Anweisung <Initialisierung> ; <log. Ausdruck> ; <Anweisung> ) <Anweisungen> } Es wird zuerst die Initialisierungsanweisung ausgeführt. Danach wird der log. Ausdruck überprüft. Wenn dieser wahr ist, werden die Anweisungen zwischen den danach <Anweisung> {}-Klammern und ausgeführt. Im nächsten Schritt wird der <log. Ausdruck> erneut geprüft. Ist er wahr, werden {}-Klammern und danach <Anweisung> ausgeführt. die Anweisungen zwischen den So wird weiter verfahren, bis irgendwann der logische Ausdruck nicht mehr wahr ist. Sehr oft wird diese Schleife genutzt, um einen Anweisungsblock eine xe Anzahl oft zu wiederholen: 1 /* Definition die ( mit funktioniert 2 int i ; Initialisierung ) f o r −Anweisung . die Sehr wichtig ! f o r −Anweisung des ' Zaehlers ' Sonst n i c h t */ fuer 18 3 /* w i e d e r h o l e 4 for ( 5 i =1 ; 100 Mal , i <=100 <Anweisungen> ; was in i++ ) den {}−Klammern s t e h t */ { } 6 Wie bei der if-Anweisung {}-Klammern können die im Fall einer einzelnen An- weisung weggelassen werden. In Zeile 2 wird die Zählvariable deniert. Dies ist sehr wichtig, damit die for-Anweisung i für die for-Schleife diese Variable zur Verfü- gung hat. 5 Pointer Die deutsche, etwas unbeholfene Übersetzung für `Pointer' lautet Zeiger 16 . Pointer sind Variablen die die Speicheradresse einer Variablen enthält. Einem Pointer ist genau wie einer Variablen, immer ein Name und ein Datentyp zugeordnet. Das Konzept des Pointers ermöglicht die Manipulation ein und derselben Variablen 17 . in verschiedenen `Programmbausteinen' Man deniert sich eine Pointer-Variable wie folgt: 18 <Datentyp>* <Name>; Der * macht hier den Unterschied zur schon bekannten Art und Weise, Variablen zu denieren. Quelltext 5 liefert ein Beispiel. double * m o e c h t e _ a u f _ d o u b l e _ z e i g e n ; Quelltext 5: Beispiel eines Pointers für den Datentyp 5.1 double Der Adress-Operator & 19 noch Nach der Denition eines Pointers ist sein Wert wie bei den Variablen nicht festgelegt. Das bedeutet bei Pointern, dass noch nicht klar ist, welche Speicheradresse er enthält bzw. auf welche Variable er zeigt. Quelltext 6 liefert zwei Beispiele für einen Pointer, dem ein Wert zugewiesen wird ein allgemeines und ein konkretes: 1 2 3 4 /* allgemein : */ <Datentyp>* <Name 1> ; / * e r s t e l l t <Datentyp> <Name 2> ; / * e r s t e l l t <Name 1> = &<Name 2> ; den die Pointer Variable <Name 1> * / <Name 2> * / 5 6 /* 7 double * z e i g t _ g l e i c h _ a u f _ d o u b l e V a r ; double * z a h l ; 8 9 10 konkret : */ z e i g t _ g l e i c h _ a u f _ d o u b l e V a r = &z a h l ; /* z e i g t _ g l e i c h _ a u f _ d o u b l e V a r Variable ' zeigt ' jetzt Quelltext 6: Beispiel für einen Pointer für den Datentyp 16 Von `to point' (engl.: zeigen, verweisen) 17 Siehe Abschnitt 7.2.4 18 Pointer auf Arrays siehe Abschnitt 6 S. 10 die double werden anders deniert, aber solche Pointer werden Sie im Rahmen dieser Vorlesungen auch nicht benötigen. 19 Siehe auf z a h l */ 19 In den Zeilen 4 bzw. 9 ndet dabei die Zuweisung der Speicheradresse der Variablen <Name> statt. Dazu wird der Adress-Operator `&' direkt vor den jeweiligen Variablennamen geschrieben. Er wird immer nach dem Schema &<Variable> genutzt. Der obige Ausdruck gibt dann als Wert die Speicheradresse der Variablen zurück, kann also als Synonym für diese Speicheradresse verstanden werden. Er steht also für eine Konstante vom Datentyp `Pointer auf Datentyp von Konvention: Der Adressoperator soll nur vor Variablen der Datentypen siehe Abschnitt 3.1 gestellt werden. a Alles a <Variable>'. grundlegenden andere wird im Rahmen dieser Lehrveranstaltung nicht benötigt werden und gilt als falsch, sofern Sie nicht eine gute Erklärung geben können, warum sie was gemacht haben. Das konkrete Beispiel aus Quelltext 6 hat verbildlicht die Situation aus Abbildung 2 geschaen. Abbildung 2: Stark vereinfachte Verbildlichung eines Zeigers mit zugewiesenem Wert. Die Rechtecke stellen den jeweiligen Speicherbereich für die Variable dar. Die Rechtecksegmente stehen für die Bytes des Speicherbereiches ihre Anzahl soll keinesfalls als repräsentativ für die tatsächliche Anzahl der Bytes stehen. 20 5.2 Der Inhalts-Operator * Hat man eine Pointer-Variable, so ist man an dem Wert der Variablen, dessen Speicheradresse gemeint ist man spricht auch von dem referenzierten Wert bzw. der referenzierten Variablen interessiert. Als Anfänger gerät man schnell in Versu- 20 als Synonym für den Namen der referenzierten chung, den Namen eines Zeigers Variablen zu sehen. Dies ist jedoch falsch, denn der Name des Zeigers steht für seinen Wert, das heiÿt, die Speicheradresse der referenzierten Variablen. Möchte man sich auf den Wert bzw. den Inhalt der referenzierten Variablen beziehen, so stellt man den Inhalts-Operator * vor den jeweiligen Zeiger; Quelltext 7 liefert ein konkretes Beispiel, welches man zur Übung nachtippen und kompilieren kann. 1 2 #include < s t d i o . h> i n t main ( void ) { /* D e f i n i t i o n 3 Datentyp eines 5 int * i n t Z e i g e r ; int i ; /* D e f i n i t i o n 6 i 7 i n t Z e i g e r = &i ; /* j e t z t 4 /* = 34; jetzt Zeigers f " ur den i n t */ eines wissen zeigt wir Integers */ den Wert von ' intZeiger auf ' i ' */ ' i ' */ 8 if 9 ( == * intZeiger i ) p r i n t f ( "* i n t Z e i g e r 10 /* I n h a l t s o p . == 34\ n" ) ; != 34) angewendet */ 11 if 12 ( intZeiger != i ) printf ("( intZeiger 13 , denn ( intZeiger == &i ) \n" ) ; 14 * intZeiger 15 /* d e r = 50; referenzierten 'i ' if 16 ( der von intZeiger betraegt jetzt 50 , also hat den Wert 50 * / i == 50 printf (" i 17 Wert Variablen ) == 50\ n" ) ; return 0 ; 18 19 } Quelltext 7: Beispielprogramm zur Anwendung und Wirkung des Inhaltsoperators Als Erstes nach den Variablendenitionen von intZeiger und i wird der Variablen intZeiger die Speicheradresse von i zugewiesen. Diese Anweisung ist einwandfrei, denn i ist genau von dem Datentypen, auf dem intZeiger basiert (welcher Datentyp ist das?). Jetzt `zeigt' intZeiger also auf i. Wenn wir jetzt den Inhaltsoperator auf intZeiger i zurückgegeben. Dieser ist jedoch noch nicht bekannt! anwenden, wird der Wert von Das wird in Zeile 6 behoben. Nun wissen wir, dass die Variable i intZeiger referenziert *intZeiger schreiben. von der Variablen und wissen zudem, was herauskommt, wenn wir wird In Zeile 9 wird der Inhaltsoperator angewendet, um seinen Wert auszulesen. In Zeile 15 wird er genutzt, um der referenzierten Variablen einen Wert zuzuweisen. Allgemeines Schema: *<Zeiger> 20 Ab jetzt wird statt `<Datentyp>-Variable einfach nur Zusammenhang hervorgeht, was gemeint ist. <Datentyp> geschrieben, sofern aus dem 21 Quelltext 8 zeigt, was man nicht machen darf. 1 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 2 DAS IST EIN FEHLERBEISPIEL 3 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 4 int * Z e i g e r ; 5 * Zeiger /* n e i n n e i n n e i n ! = 10; IRGENDWO h i n , da haben wissen und wir wir Zeiger den Wert nicht , referenziert nicht festgelegt wo h i n ! * / Quelltext 8: Typischer Fehler mit Zeigern: Es wird versucht, den Wert für die Speicheradresse zu setzen, obwohl noch gar nicht festgelegt wurde, welche Variable referenziert werden soll. Der panische Kommentar in Zeile 5 ist berechtigt. Das resultierende Problem wird in der Regel der bekannte `Speicherzugrisfehler' sein das Programm versucht in irgendeine Speicheradresse des Betriebssystems zu schreiben, und das wird im allgemeinen keine sein, welche ihm vom Betriebssystem zugeordnet wurde. Das Betriebssystem beendet in dem Fall das Programm. Wenn man Pech hat, war die Speicheradresse legal, das heiÿt für das eigene Programm reserviert, und das Programm läuft weiter, aber liefert falsche Ergebnisse. Dies ist natürlich nur ein einfaches Fehlerbeispiel; so direkt wird man das Problem nicht immer sehen, da der Inhaltsoperator oft viel später genutzt wird, als direkt nach der Pointer-Denition. Wichtig: Damit man den Inhaltsoperator sinnvoll nutzen kann, muss dem Zeiger eine Speicheradresse einer Variablen vom Datentyp des Zeigers zugewiesen sein und für den Fall, dass man Werte auslesen möchte, dieser Variablen auch schon ein Wert zugewiesen worden sein 6 21 ! Array von Elementen vom Datentyp <Datentyp> char, int, float und double behandelt. Wenn man jedoch eine groÿe Anzahl von Daten Abgesehen von Zeigern wurden bislang nur die grundlegenden Datentypen verarbeiten möchte, stöÿt man schnell an seine Grenzen mit diesen einfachen Datentypen. Möchte man beispielsweise mit 1000 den bislang bekannten Mitteln 1000 double-Variablen rechnen, so müsste man mit double-Variablen denieren mit 1000 ver- schiedenen Namen. Denn die Variablen der grundlegenden Datentypen können zu jedem Zeitpunkt nur einen einzelnen Wert speichern. Damit dies nicht notwendig ist, gibt es Arrays, zu Deutsch: (Daten-)felder. Ein Array ist eine Sequenz von Variablen des gleichen Datentypes, also eine Art von Gruppierung von Variablen des gleichen Typs. Anstatt sich also die 1000 Variablen des eben angeführten Beispieles zu denieren kann man sich ein Array vom Datentyp double mit der Gröÿe 1000 denieren und ist fertig. Wichtig: Es sei vorab davor gewarnt, Arrays wie Variablen von einem der grundlegenden Datentypen zu behandeln. Der Name eines Arrays steht nur für die Spei- cheradresse des ersten Elementes des Arrays, ist insbesondere eine (Pointer-) Kon- 21 Siehe Hinweis auf S.10. 22 stante und steht in keinem direkten Zusammenhang mit den Werten der Elemente 22 des Arrays. Man deniert sich ein eindimensionales Array d.h. ein Array, dessen Elemente über nur einen Index angesprochen werden nach der Syntax <Datentyp> <Name>[n]; wobei `n' eine natürliche Zahl mit n>0 ist und kein Array-Datentyp eingesetzt wer- den darf. Die einzelnen Variablen dieses Arrays kann man dann über <Name>[ i ] ansprechen. i ist dabei der (ganzzahlige) Index der Variablen an der Stelle Arrays, d.h. i+1 des 0 ≤ i ≤ n-1 Andere Werte von i in obiger Zeile führen zu einer Referenz auf Speicherbereich auÿerhalb des Arrays und führen ggf. zum Abbruch des Programmes oder zu unvorhersehbarem Verhalten. Abbildung 3 veranschaulicht die Situation. Abbildung 3: Schematische Veranschaulichung eines Arrays im Speicher. Nur <Name>[0] `bis' <Name>[n-1] gehören zum Array! <Name>[n] beispielsweise gehört <Name>[0], siehe Abschnitt nicht zum Array. &<Name>[0] ist die Speicheradresse von 5.1. Ein konkretes Beispiel wäre int k [100]; Damit deniert man ein Array, das 100 int-Variablen hält, siehe Abbildung 4. Abbildung 4: Schematische Veranschaulichung des Arrays adresse von 22 Siehe k[0]. Abschnitt 6.1. k. &k[0] ist die Speicher- 23 Mit der Denition des Arrays allein ist jedoch noch nicht klar, welche Werte diese Variablen haben, sondern nur, wo sie im Speicher (hintereinander) abgelegt sind. Deshalb sollte man Arrays immer initialisieren damit ist eine Initialisierung der Elemente des Arrays gemeint. Dies kann man bei kleineren Arrays wie in der nächsten Zeile gleich zu Beginn der Initialisierung machen: <Datentyp> <Name>[n]={<Wert 1>,<Wert 2>,...,<Wert N>}; Die Werte müssen dabei Konstanten vom Datentyp des Arrays sein. Z.B. kann man ein int-Array int-Konstanten mit den 1,2,3,4 wie folgt denieren und initialisieren: int zahlen [4]={1,2,3,4}; zahlen[0] Dann ist der Wert von 1, der von zahlen[1] 2 usw.. Auch hier beachte man die Indexverschiebung der erste Wert hat den Index 0. Wichtig: Diese Art von Initialisierung kann nur in einer Anweisung mit der Denition stattnden! Für groÿe Arrays ist dies jedoch oensichtlich zu umständlich. Man kann sich die Aufgabe hier mit einer for-Anweisung vereinfachen. Quelltext 9 zeigt das allgemeine Muster. 1 2 3 <Datentyp> <Name> [ n ] ; int i ; for ( i = 0 ; <Name> [ 4 i <n ; / * Def . eines Arrays / * Def . von fuer i n E l e m e n t e n */ f o r −S c h l e i f e */ i++ ) / * A n we i s u ng e n i ]=0; mit die der f o r −S c h l e i f e */ Quelltext 9: Initialisierung eines eindimensionalen Arrays Für das Array k, welches in Abbildung 4 dargestellt ist, würde dieser Quelltext so aussehen: int k [ 1 0 0 ] ; i n t i ; // D e f i n i t i o n 1 2 der Variablen i fuer die for− Schleife for ( 3 i = 0 ; i <100 ; i++ ) k [ i ]=0; 4 6.1 Der Name eines Arrays Die Speicheradresse des ersten Elements spielt eine besondere Rolle bei Arrays. In den Abbildungen 3 und 4 wurde diese daher hervorgehoben. Nach ANSI-C ist nämlich der Wert eines Arrays array[0], array nichts weiteres, als die Speicheradresse von also ein Verweis auf den ersten Eintrag von array. Dennoch ist ein Array nicht mit einer Pointer-Variablen zu verwechseln. Die sogenannte Basisadresse array bzw. &array[0] des Arrays ist schon mit der Denition 23 sinnvoll festgelegt und kann zudem nicht verändert werden. 23 Bei einer Pointer-Variablen hingegen ist nach ihrer Denition nicht klar, wohin sie zeigt und man kann jederzeit verändern, wohin sie zeigt. 24 1 2 3 4 #include < s t d i o . h> i n t main ( void ) { double b e i s p i e l A r r a y [ 2 0 ] ; / * Array m i t 20 E i n t r a e g e n * / i f ( b e i s p i e l A r r a y == &b e i s p i e l A r r a y [ 0 ] ) p r i n t f ( " Das 5 Skript hat Recht ! \ n" ) ; else 6 p r i n t f ( " Diese 7 Zeile wird sowieso nicht ausgegeben . " ) ; return 0 ; 8 9 } `beispielArray' `double'. Für andere Datentypen funktioniert dieses Beispiel natürlich genauso Quelltext 10: Ein kleines Beispielprogramm. Der Datentyp von ist Abbildung 5: Das Array aus Quelltext 10 verbildlicht Dabei ist zu beachten, dass die Speicheradresse `Pointer auf <Datentyp von array>' array eine Konstante vom Datentyp 24 Quelltext 10 ist, sie ist nicht veränderbar. und Abbildung 5 geben ein Beispiel. 6.2 Mehrdimensionale Arrays Man kann sich neben eindimensionalen auch zweidimensionale oder gar N-dimensionale Arrays erstellen (`N' sei dabei eine natürliche Zahl), d.h. Arrays, deren Elemente über zwei oder n Indizes angesprochen werden. Zweidimensionale Arrays kann man sich dann wie eine Matrix von Elementen des Datentyps des Arrays vorstellen; man sollte nur wie immer im Kopf behalten, dass ab dem Index 0 adressiert wird. Man deniert sich zweidimensionale Arrays mit folgender Syntax: <Datentyp> <Name>[n1][n2]; n1 und n2 natürliche Zahlen sind mit 0 < n1 und 0 < n2 auch hier dürfen <Datentyp> alle Datentypen mit Ausnahme von Arrays eingesetzt werden. wobei für Bemerkung: N -dimensionale Arrays für N∈ N können analog deniert werden: <Datentyp> <Name>[n1][n2]...[nN]; Je nach Compiler liegt die obere Grenze für N zwischen 7 und 10. Im Speicher sind die Elemente dabei alle hintereinander angeordnet, und zwar so, dass beim Durchlaufen des Arrayelemente entsprechend der Speicherstruktur zuerst der letzte Index durchlaufen wird, dann der vorletzte usw.. Bei zweidimensionalen Arrays entspräche dies dem Durchlaufen der Spaltenindizes für die Zeile 0, dann 24 Wo das Array im Speicher abgelegt ist, wird beim Kompilieren festgelegt. 25 dem Durchlaufen der Spaltenindizes für die Zeile 1 usw.. Daher kann man ein zweidimensionales Array auch als ein eindimensionales Array von eindimensionalen Arrays des jeweiligen Datentypen auassen. Analoges gilt für höherdimensionale Arrays. Auch bei Arrays höherer Dimensionen ist es sinnvoll, die Elemente zu initialisieren, bevor man mit dem Array arbeitet: 1 <Datentyp> <Name> [ n1 ] [ n2 ] ; i n t i ; /* i f u e r d i e f o r −S c h l e i f e */ i n t j ; /* j f u e r d i e f o r −S c h l e i f e */ f o r ( i = 0 ; i <n1 ; i++ ) f o r ( j = 0 ; j <n2 ; j++ ) 2 3 4 5 <Name> [ 6 i ] [ j ]=0; / * A n we i s u ng e n f o r −S c h l e i f e */ der Quelltext 11: Initialisierung eines zweidimensionalen Arrays während der Laufzeit Man braucht hier zwei Zählschleifen, wobei die Zählschleife für die Variable Anweisung innerhalb der Zählschleifen für die Variable i j die 25 Analog kann man ist. mit entsprechend vielen Zählvariablen höherdimensionale Arrays initialisieren. Ein konkretes Beispiel: double m a t r i x [ 2 0 ] [ 4 0 ] ; i n t i ; /* i f u e r d i e f o r −S c h l e i f e */ i n t j ; /* j f u e r d i e f o r −S c h l e i f e */ f o r ( i = 0 ; i <20 ; i++ ) f o r ( j = 0 ; j <40 ; j++ ) 1 2 3 4 5 matrix [ i ] [ j ]=0; 6 Wie beim eindimensionalen Array steht der Wert eines Arrays `array' wieder für die Speicheradresse des ersten Eintrags: &<array>[0][0]\... [0] Die Punkte deuten an, dass die Anzahl der Nullen der Dimension des Arrays entspricht. 6.3 Strings Oft möchte man ein Programm schreiben, das Menüs enthält oder dem Nutzer bestimmte Fragen stellt, Antworten einliest etc. Man möchte in dem Fall Zeichenketten oder zu Englisch Strings ausgeben, einlesen oder anders verarbeiten. Strings werden in C durch Abfolgen von char-Konstanten, welche auf das Spezial- zeichen `\0' enden, repräsentiert. Dies wurde vereinbart um das Ende der Zeichen- kette zu markieren und dies ist auch notwendig. Denn beispielsweise kann ein char-Array der Gröÿe 100 einen String mit 99 Buchstaben speichern der letzte Eintrag des Arrays enthält dann das Zeichen `\0'. Es kann aber auch einen String mit 50 Buchstaben speichern. Damit klar ist, bis zu welchem Element des Arrays der String gehen soll, hat man den Marker `\0'. Die Denition ggf. mit Initialisierung muss nicht nach dem Schema auf S. 23 erfolgen. Möchte man das Array gleich mit einem String initialisieren, so kann man schreiben: 25 Indem die innerste Schleife den letzten Index des Arrays durchläuft, wird hier auch die Struktur des Arrays im Speicher berücksichtigt. 26 char <Name>[n]="<Zeichenkette mit n−1 Buchstaben>" Es folgt ein Beispiel: char bsp[5]="Hey!"; Abbildung 6: Das Bild zu obiger Zeile Es sei angemerkt, dass die Syntax array="<String>" getrennt von der Denition eines Arrays nicht gültig ist, d.h. man kann bei obigem Beispiel nach der Denition nicht schreiben bsp="Neu"! Das geht schon allein deshalb nicht, weil man versucht, einer Konstante (bsp) einen Wert zuzuweisen. Für Operationen wie das Setzen einer Sequenz von Buchstaben in ein Array hat man spezielle Funktionen für Strings, welche jedoch nicht Gegenstand dieses Skriptes sein werden. Es werden im Anhang nur Funktionen zum Einlesen und Ausgeben von Strings behandelt. char-Array mit der passenden Gröÿe deniert. Die Gröÿe a+1 betragen, wobei `a' der Anzahl der Zeichen des längsten Wichtig ist, dass man das des Arrays sollte immer zu verarbeitenden Strings entspricht. Wichtig: Für irgendein ausgebbares Zeichen "x" x gilt: x, \0. ist die String-Konstante mit den Zeichen Sie ist nicht zu verwechseln mit der char-Konstanten 'x', welche einen einzelnen Buchstaben darstellt! Diese wird mit einzelnen Anführungsstrichen geschrieben. 7 Funktionen Alle Programmiersprachen haben Konstrukte, welche die Separierung von QuellcodeBlöcken in Quellcode-Einheiten ermöglichen. C benutzt hierzu das Konzept der Funktion. Jedes Programm in C muss eine Funktion mit dem Namen main haben; denn dort stehen die ersten Anweisungen, welche ausgeführt werden. Es ist zwar möglich, alle Anweisungen des Programmes nur in die main-Funktion zu schreiben. Dies würde jedoch bei groÿen Programmen zu sehr unübersichtlichem, groÿem und komplexem Quelltext führen, so dass die Fehlersuche das sogenannte Debugging , das Testen und die Wartung des Programmes viel zu schwer werden würden. Wenn man ein Programm in funktionale Module mit kleineren Quelltexten aufteilen kann, dann werden diese Prozesse leichter. Nach dem Schreiben der Quelltexte können diese Module zu einem Programm zusammengefügt werden. Diese Module enthalten in der Regel eine (manchmal auch mehrere) Funktionen. Ein weiterer Vorteil der Modularisierung ist, dass man einen öfter wiederkehrenden Quelltextabschnitt als Funktion schreiben kann diese kann dann beliebig oft genutzt man sagt auch aufgerufen werden. Der Funktionsaufruf steht dann in einer einzigen Zeile. Natürlich kann diese Funktion dann ggf. auch für neue, andere Programme genutzt werden. 27 7.1 Funktionsdenition Funktionen sind innerhalb eines Programmes eindeutig identizierbar durch ihren Namen. Dieser muss durch einen Bezeichner 26 gegeben sein. Man kann eine Funktion so schreiben, dass sie einen Wert zurück gibt dann kann man den Funktionsaufruf im Quelltext wie eine Konstante von diesem Datentypen nutzen. 27 Dies ist beispielsweise sinnvoll, wenn man die Berechnung eines bestimm- ten Wertes in einer Funktion modularisiert und das Ergebnis in der aufrufenden Funktion weiter benutzen möchte. Damit Fehler vermieden werden, muss klar gemacht werden, von welchem Datentyp dieser Rückgabewert ist. Diesen Datentyp nennt man auch den Funktionstyp oder Datentyp der Funktion. Analog zu gewöhnlichen Variablen haben Funktionen also einen Namen und einen Datentypen. Die Ähnlichkeit geht sogar weiter: Funktionen müssen vor ihrer Verwendung auch deniert werden. Die Denition sieht bei Funktionen jedoch etwas anders aus. In einer Funktionsdenition müssen folgende Informationen enthalten sein: Funktionstyp Name der Funktion Parameterliste Denition von Variablen der Funktion Anweisungen der Funktion ggf. die Rückgabeanweisung Die ersten drei Elemente werden im sogenannten Funktionsheader zusammengefasst. Die letzten drei Elemente stehen im sogenannten Funktionskörper. 4 <Funktionstyp> <Name>( <Parameterliste> ) <Variable>n d e f i n i t i o n e n <Anweisungen> <Rueckgabeanweisung> 5 } 1 2 3 { Quelltext 12: Allgemeines Schema der Funktionsdenition In Zeile 1 steht der Funktionsheader nur die {-Klammer gehört nicht dazu. In den drei Zeilen ab Zeile 2 steht der Funktionskörper. 7.1.1 Der Funktionsheader Der Funktionsheader besteht aus Funktionstyp, Name und Parameterliste. Die Anordnung ist in Quellcode 12, Zeile 1 beschrieben. 26 Siehe Abschnitt 3.3, S. 9 27 Ausgenommen sind hierbei Arrays, siehe Abschnitt 6. 28 Parameterliste Die Parameterliste zählt nacheinander die Parameter auf, welche beim Aufruf der Funktion übernommen (und erwartet) werden: <Parameter 1>, <Parameter 2>, ..., <letzter Parameter> Dabei wird jeder Parameter in der Form <Datentyp> <Name> oder <Datentyp> <Name> [ n ] bzw. <Datentyp> <Name> [ n1 ] [ n2 ] für Variablen bzw. ein- und zweidimensionale Arrays geschrieben. Die Syntax ähnelt also stark der Denition von Variablen bzw. Arrays, mit dem Unterschied, dass am Ende kein Semikolon steht. Wie so oft folgt ein konkretes Beispiel: double p1, int* p2, int p3 [30], char p4 doubleint' , als dritten Eine Funktion mit dieser Parameterliste würde als ersten Parameter eine Variable, als zweiten Parameter eine Variable vom Typ `Zeiger auf Parameter ein eindimensionales Array von eine char-Variable int-Variablen und als vierten Parameter übernehmen. Ein kompletter Funktionsheader mit dieser Parameterliste sieht so aus: <Funktionstyp> <Name>( double p1 , i n t * p2 , i n t p3 [ 3 0 ] , char p4 ) Quelltext 13: Fast komplettes Beispiel für einen Funktionsheader Man kann sich auch dafür entscheiden, dass die Funktion gar keine Parameter übernimmt, das schreibt man so: <Funktionstyp> <Name>( Funktionstyp void ) Als Funktionstyp können wir alle bekannten Datentypen mit Aus- nahme von Arrays nehmen. Warum es nicht notwendig ist, Arrays zurückzugeben, wird in Abschnitt 7.2.3 erläutert. Für den Fall, dass man gar keinen Wert zurückgeben möchte, gibt es noch den Datentyp Funktionstypen double double <Name>( double Name der Funktion void. Quelltext 13 würde mit dem so aussehen: p1 , i n t * p2 , i n t p3 [ 3 0 ] , char p4 ) Der Name muss wie bei Variablen durch einen Bezeichner 28 gegeben sein. Beim gerade aufgeführten Beispiel könnte die Funktion zum Beispiel `funk' heiÿen: double f u n k ( double p1 Quelltext 14: vervollständigt 28 Siehe S. 9 Das Beispiel aus , i n t * p2 Quelltext 13 , i n t p3 [ 3 0 ] zu einem , char p4 ) konkreten Beispiel 29 7.1.2 Der Funktionskörper Im Funktionskörper stehen alle Anweisungen, welche die Funktion durchlaufen soll. Gewöhnlicherweise ordnet man die Anweisungen so, dass zuerst alle Variablendenitionen geschrieben sind und dann alle anderen Anweisungen folgen. So hat man oben im Quelltext eine Übersicht über alle Variablen der Funktion. Die Parameter aus der Parameterliste können im Funktionskörper ohne jegliche Denition wie gewöhnliche Variablen bzw. Arrays genutzt werden. die return-Anweisung. 29 Am Ende steht return <Wert vom Datentyp der Funktion>; in welcher angegeben wird, welcher Wert zurückgegeben werden soll. Beim Funktionstyp void kann man diese weglassen oder schreibt einfach return ; Die return-Anweisung bewirkt neben der ggf. durchzuführenden Rückgabe eines Wertes, dass die Funktion beendet wird und die Kontrolle an die aufrufende Funktion zurückgegeben wird. Quelltext 15 liefert ein einfaches Beispiel für eine komplette Funktionsdenition. Es werden zwei Integer-Parameter übernommen und ihre Summe wird in der Variablen summe gespeichert. Danach gibt die Funktion diese Summe zurück. Das kann gemacht werden, weil der Funktionstyp 1 int addition ( int i , int ) /* hier 3 /* V a r i a b l e n d e f i n i t i o n ( en ) 4 i n t summe ; /* 6 summe = A n we i s u ng e n i der j 2 5 beginnt int ist, so wie der Datentyp von /* { Funktionsheader Funktionskoerper summe. */ */ */ */ + j ; r e t u r n −Anweisung 7 /* 8 return summe ; */ 9 } Quelltext 15: Einfaches Beispiel für eine komplette Funktionsdenition 7.2 Funktionsaufrufe Als nächstes wird man sich evtl. fragen, wie denn jetzt beispielsweise das in Quelltext 15 beschriebene Modul genutzt werden kann. Dies geschieht in einer anderen, aufrufenden Funktion durch eine bestimmte Anweisung, den Funktionsaufruf. 7.2.1 Funktionsdeklaration Bevor der Funktionsaufruf stattnden kann, muss dem Modul der aufrufenden Funktion erklärt werden, wie dieser Aufruf auszusehen hat denn diese Information ist zunächst nur im Modul der Unterfunktion enthalten. Diese Deklaration ist nicht Bestandteil der Funktionsdenition des Moduls, sie steht wie die in Quelltext 1 über der Funktionsdenition. 29 Siehe auch Abschnitt 7.2.3 #include-Zeile 30 Das Beispiel aus Quelltext 1 könnte man mit dem Modul aus Quelltext 15 wie im folgenden Quelltext 16 exemplarisch verbinden: 1 2 #i n c l u d e < s t d i o . h> int addition int ( i int , j ); /* F u n k t i o n s d e k l a r a t i o n */ 3 4 5 i n t main ( void ) { i n t summerueck , z a h l =3 ; 6 printf ( 7 summerueck = a d d i t i o n " Ich 8 printf ( eine " Variable und in Z e i l e \n" ); ( 15 zahl summerueck zahl und , ) ; /* F u n k t i o n s a u f r u f */ Konstante g e s p e i c h e r t \n" 15 wurden addiert ); return 0 ; 9 10 bin } Quelltext 16: Das erste Beispiel abgewandelt In Zeile 2 ndet man die Funktionsdeklaration. Sie sieht immer aus wie der Funktionsheader der aufgerufenen Funktion in diesem Beispiel ist das die Funktion addition mit dem Unterschied, dass ein Semikolon dahinter steht. Das ist das allgemeine Schema für Funktionsdeklarationen. Die Picht, solche Deklarationen einzubinden, soll den Programmierer daran erinnern, wie er die Unterfunktion aufzurufen hat auÿerdem wird durch Abgleich mit dem Funktionsheader geprüft, ob das aufrufende und das aufgerufene Modul `sich darüber einig sind`, wie die aufgerufene Funktion zu funktionieren hat. In Zeile 7 ndet der Aufruf statt. Wie schon erwähnt kann und wird hier der Aufruf wie ein Wert vom Funktionstyp benutzt der Wert zahl+15 int-Variablen summerueck wird der zugewiesen. Würde man die Module aus den Quelltexten 15 und 16 in zwei Textdateien, z.B. add.c und main.c speichern, so könnte man diese in der Kommandozeile mit dem Befehl −o gcc add.c main.c in die ausführbare Datei programmEinsB programmEinsB kompilieren. Es folgen einige Beispiele für Funktionsdeklarationen verschiedener Art. 1 /* 2 double f u n k 1 ( void ) ; gibt gibt double 3 /* 4 void f u n k 2 ( void ) ; gibt nichts zurueck , 5 /* 6 void f u n k 3 ( double ) ; 7 /* gibt nichts zurueck , char ganzzahl , zurueck , zurueck , als */ uebernimmt nichts */ uebernimmt double */ uebernimmt char f u n k 4 ( i n t g a n z z a h l 9 /* bei nichts zweites 8 nur uebernimmt ein , * Deklarationen * weggelassen werden duerfen f l o a t funk4b ( int /* 12 void f u n k 5 ( char z e i c h e n [ 1 0 0 ] [100][100] , Argument doubles */ Bezeichner auch */ 11 denken erstes von double z a h l e n f e l d [ 1 0 0 ] ) ; 10 selbst als Array double [ 1 0 0 ] ) ; */ , double z a h l e n f e l d 2 x 2 ); Quelltext 17: Beispiele für Funktionsdeklarationen 31 Es sei nochmals betont, dass die Funktionsdeklarationen von aufgerufenen Funktionen im Quelltext des aufrufenden Modules über der Funktionsdenition der auf- rufenden Funktion zu stehen hat, siehe Beispiel 16. 7.2.2 Der Funktionsaufruf Hat man eine Funktion deniert und in dem aufrufenden Modul auch schon deklariert, so kann man die Funktion guten Gewissens aufrufen. Die Funktionsdeklaration ist dabei maÿgeblich dafür, wie der Aufruf auszusehen hat. Wenn der Funktionstyp nicht void ist, dann wird ein Wert zurückgegeben. Man kann diesen wie in Quelltext 16 dann in einer Variablen der aufrufenden Funktion speichern. Dabei muss darauf geachtet werden, dass die Datentypen übereinstimmen im Beispiel von Quelltext 16 sind beide Datentypen int und daher gibt es keine Probleme. Stimmen die Datentypen nicht überein, so wird der Rückgabewert falsch interpretiert und man wundert sich unter Umständen, warum das Programm merkwürdige Ergebnisse liefert sofern es nicht schon beim Kompilieren Fehlermeldungen gibt. Hat man eine void-Funktion oder ist gar nicht am Rückgabewert interessiert, so braucht man den Funktionsaufruf nicht mit einer Zuweisung zu verbinden. Der Aufruf sieht schematisch so aus: <Name der Funktion>( <Wert 1> , <Wert 2> , ... , <Wert N> ) ; Arrays werden mit ihrem Namen übergeben. Das ist auch ein Wert (Was für einer?). N entspricht dabei der Anzahl der Parameter in der Funktionsdeklaration <Wert 1> entspricht vom Datentypen her dem Wert des ersten Parameters in der Parameterliste, <Wert 2> entspricht vom Datentypen her dem Wert des zweiten Die Zahl und Parameters in der Parameterliste etc. für die Übergabe von Arrays schreibt man ihren Namen hin. 32 Vorsichtshalber nochmal ein paar konkrete Beispiele. Angenommen, man hat die Deklarationen aus Quelltext 17. Dann könnte man die Funktionen aus der Funktion beispiel wie folgt aufrufen: 1 void 2 /* der Variablen , welche 3 double z a h l R u e c k , zahlPara1 , zahlPara2 4 int zahlParaGanz ; char b u c h s t R u e c k , beispiel Definition ( void ) { uebergeben , werden */ feldPara [100] , feldPara2x2 [ 1 0 0 ] [ 1 0 0 ] ; 5 charArray [ 1 0 0 ] ; 6 7 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 8 Vor 9 Initialisierungen Uebergabe die der Werte Variablen folgen der natuerlich sinnvollerweise welche 11 irgendwie 12 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ Das uebergebenen erstmal Anweisungen , 10 aendern . zu sollten und wird hier Variablen ausgelassen . 13 /* Funktionsaufrufe 16 /* funk1 17 /* Parameterliste 18 funk1 ( 19 /* Funktionstyp 20 /* in 21 zahlRueck = funk1 ( 14 entsprechend der Deklarationen */ 15 */ ) ist void , also nichts double , also kann in den Klammern * / ; einer ist d o u b l e −V a r i a b l e n ) der gespeichert Rueckgabewert werden */ */ ; 22 23 /* 24 funk2 ( funk2 25 /* */ ); zahlRueck = funk2 ( zurueck ) ; geht nicht ! Denn f u n k 2 gibt nichts */ 26 27 /* 28 funk3 ( funk3 zahlPara1 */ 29 funk3 ( zahlPara2 ) ; 30 funk3 ( 2.4562 /* ) ; ) ; hier wird eine Konstante uebergeben */ 31 32 /* funk4 33 /* Arrays 34 funk4 35 / * s o NICHT : funk4 ( zahlParaGanz , 36 / * s o NICHT : funk4 ( i n t 37 /* m i t 38 buchstRueck = funk4 ( ( */ werden * mit zahlParaGanz Speichern des i h r e m Namen * , feldPara ) u e b e r g e b e n : */ ; feldPara [100]) ; zahlParaGanz , double Rueckgabewertes : zahlParaGanz , */ feldpara ); 39 40 /* 41 funk5 ( funk5 */ charArray , feldPara2x2 */ f e l d p a r a [ 1 0 0 ] ) ; */ ); Quelltext 18: Funktionsaufrufe gemäÿ der Deklarationen aus Quelltext 17 33 7.2.3 Call by Value In C werden Parameter mit ihrem Wert übergeben. In C geschieht dabei in der aufgerufenen Funktion mehreres (es sei 1≤i≤N und N die Anzahl der Parameter): im Funktionsheader steht in der Parameterliste an i-ter Stelle so etwas ähn- 30 . Tatsächlich liches wie eine Variablendenition oder Denition eines Arrays werden alle Parameter bis auf Arrays zu Variablen der aufgerufenen Funktion (ihr Variablenname steht in der Parameterliste). diese Variablen werden automatisch mit dem übergebenen Wert initialisiert. Sie sind nur dieser Funktion bekannt. Selbst wenn sie den gleichen Namen haben wie Variablen einer anderen Funktion, so ist ihr Wert unabhängig von dem Wert der Variablen der anderen Funktion. Man kann sich das so vorstellen, dass jede Funktion ihren lokalen Speicherbereich hat. Da jede Variable nichts anderes als ein Speicherbereich mit zugeordnetem Namen und Datentyp darstellt, kann man sich die Situation wie in Abbildung 7 visualisieren. (a) ueberFunk #include void void int double if int void unterFunk ( ueberFunk ( i =0; == i printf /* ); ) { zahl ; unterFunk ( (b) unterFunk < s t d i o . h> i ) (" hat else ( 0 ); Die den Pfad void char unterFunk Variable Wert 0\ n" ) ; unnoetig i */ } = buchst ; i ++; int ( i ) { /* Inkrementieren v. i */ } (c) ueberFunk Quelltext (d) ueberFunk Quelltext Abbildung 7: Die Anweisung `i++' im Quelltext der Funktion nur die Variable i im Speicherbereich von unterFunk unterFunk Diese Übergabe von Parametern mit ihrem Wert wird Call by Value C kennt eigentlich nichts anderes, als dieses Konzept. 30 Siehe S. 28. 31 engl.: Aufruf nach Wert verändert 31 genannt. 34 7.2.4 Call by Reference Man hat nach den Ausführungen aus vorigem Abschnitt folgendes: Jede Funktion kann nur einen Wert zurückgeben Arrays können nicht zurückgegeben werden Variablen können nur mit ihrem Wert übergeben werden In Quelltext 18, Zeilen 34, 38 und 41 wurden Funktionen aufgerufen, welche laut Deklaration als Parameter Arrays empfangen. Die Arrays wurden übergeben, indem der jeweilige Name des Arrays übergeben wurde. Dieser ist nach 6.1, S. 23 ein Zeiger auf das erste Element des Arrays. Man übergibt in C Arrays, indem man den Namen des Arrays übergibt. Da es sich hier um eine Speicheradresse handelt, übergibt man also eine Referenz bzw. einen Verweis. Dies nennt man Call by Reference . Im Speicherbereich der Unterfunktion wird also kein lokales Array erstellt, sondern die Unterfunktion manipuliert den Speicherbereich des Arrays in der aufrufenden Funktion, siehe Abbildung 8. (a) ueberFunk #include <s t d i o . h> void unterF ( int [ 3 ] , double ) ; void ueberFunk ( void ) { int f [ 3 ] = { 1 , 2 , 3 } ; double z a h l =0; unterFunk ( f , z a h l ) ; if ( f [ 0 ] == 1 ) (b) unterFunk void /* else Pfad unnoetig */ (c) ueberFunk Quelltext int f [3] , double zahl ){ /* erste Element von f auf 5 setzen */ z a h l ++; /* Inkrementieren von lokalem i */ f [ 0 ] = 5; p r i n t f ( " Die V a r i a b l e f [ 0 ] hat den Wert 1\n" ) ; } unterF ( } (d) ueberFunk Quelltext zahl++; verändert nur den Wert der zu unterFunk zahl und lässt die Variable gleichen Namens in ueberFunk unverändert. Gleichzeitig wird durch die Anweisung f[0]=5; das Array von ueberFunk verändert es besteht gar kein lokales Array f. Abbildung 8: Die Anweisung lokalen Variablen Bei Arrays hat man also in C im Gegensatz zu Variablen standardmäÿig Call by Reference. Man kann sich auch überlegen, dass das Kopieren eines ganzen Arrays bei entsprechend groÿen Arrays schnell zu einem sehr groÿen Schreibaufwand und damit zu einer Verlangsamung des Programms führen kann. 35 Es kann aber vorkommen, dass man mehrere Variablen, ggf. auch unterschiedlichen Datentypes, übergeben und es der Unterfunktion ermöglichen möchte, die Werte der Variablen in der aufrufenden Funktion zu verändern. Da man standardmäÿig nur eine einzige Variable zurückgeben kann und auf Variablen immer Call by Value angewandt wird, muss man sich eines kleinen Knies bedienen, um die Werte in der aufrufenden Funktion zu verändern. Die Lösung ist, dass man an Stelle der Variablen selbst ihre Speicheradresse übergibt. Das erreicht man, indem man den Adress-Operator 32 vor den Variablenna- men stellt und sowohl Funktionsheader als Funktionsdeklarationen entsprechend schreibt. Dabei muss man beachten, dass statt dem Datentypen Datentyp <Datentyp>*33 <Datentyp> der benutzt wird. Es folgt ein kleines Beispiel in Abbildung 9. (a) ueberFunk #include <s t d i o . h> void unterF ( double * ) ; void ueberFunk ( void ) double z a h l =0; (b) unterFunk { /* es wird Speicheradresse von zahl uebergeben diese i s t vom Typ double , erreicht wird dies durch Vorstellen von & */ void double * Adr_von_zahl ) { /* Nutzung des Inhaltsoperators , um den Wert an der Speicheradresse Adr_von_zahl zu veraendern */ unterFunk ( &z a h l ) ; ( z a h l >2.4 ) p r i n t f ( " z a h l hat den Wert 2 . 4 5 \ n" ) ; if } unterF ( /* else Pfad unnoetig */ } (c) ueberFunk Quelltext *Adr_von_zahl = 2 . 4 5 ; (d) ueberFunk Quelltext &zahl wird übergeben in unterFunk wird Adr_von_zahl gemacht. Durch Anwendung des Abbildung 9: daraus die lokale Zeiger- Variable Inhaltsoperators können wir den Inhalt der referenzierten Adresse verändern also die Variable zahl der Überfunktion. Man beachte auch, dass in Funktionsheader und -deklaration von unterFunk A statt dem Datentyp double der Datentyp double* verwendet wurde. Schlüsselwörter Im Englischen `keywords'. Jedes Wort in ANSI-C ist entweder ein Schlüsselwort oder ein Bezeichner. Allen Schlüsselwörtern ist durch den ANSI-C-Standard schon eine Bedeutung zugeordnet und diese kann nicht geändert werden. Schlüsselwörter gehören zu den Grundsteinen für Programmanweisungen. Alle Schlüsselwörter 32 Siehe 5.1 33 Also der Datentyp `Pointer auf <Datentyp>'. 36 sind in Kleinbuchstaben gehalten in ANSI-C wird zwischen groÿen und kleinen Buchstaben genau unterschieden. Es folgt eine Auistung der Schlüsselwörter nach ANSI-C in Tabelle 1. Je nach Compiler können Schlüsselwörter hinzukommen. auto double int struct break else long switch case enum register typedef char extern return union const oat short unsigned continue for signed void default goto sizeof volatile do if static while Tabelle 1: Schlüsselwörter in ANSI-C B Einige der Standardfunktionen aus stdio.h In diesem Abschnitt soll eine Auswahl der Standardfunktionen aus der Standardbibliothek stdio vorgestellt werden. Es wird die jeweilige Deklaration vorgestellt um zu erklären, wie die Funktion aufgerufen werden soll. Sämtliche Deklarationen der Funktionen dieses Anhangs werden ausgeführt, indem man die Zeile #include <stdio.h> ganz oben im Quelltext eingibt. B.1 Bildschirmausgaben printf Die Deklaration lautet: int printf ( const char* format, ...) ; und ist wie alle Funktionen dieses Anhangs #include <stdio.h> printf verwenden möchte in der Anweisung enthalten diese Anweisung schreibt man also, wenn man dahin, wo man sonst gewöhnliche Deklarationen hinschreibt (über der Funktionsdenition der aufrufenden Funktion). Funktionstyp: Die Funktion ist vom Datentypen int, aber die Rückgabewerte sollen im Rahmen dieser Vorlesung nicht interessieren. 1. Parameter const char*: Der erste Parameter ist vom Typen const char*. Erwartet wird als erster Parameter ein Ausdruck der Form "<Zeichenkette>" (wobei die Anführungszeichen tatsächlich dazu gehören). Die Zeichenkette entspricht einfach dem, was ausgegeben werden soll. Man darf nur nicht zwischendurch ein RETURN getippt haben: 37 p r i n t f ( " Das 1 das 2 Ausgabe die mit Zeilenumbruch \n" ) ; /* Z e i l e um * / NICHT w i e in den naechsten Zeilen ! ! ! ! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 5 6 eine bricht /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 3 4 ist '\n ' hier p r i n t f ( " Anfang der geht w e i t e r . . . . . \ n" ) ; die Zeile Fehlermeldung und Zeil . . . . . ist falsch ! /* das fuehrt zu einer */ Die Ausgabe von konstanten Zeichenketten ist ziemlich einfach, man muss ggf. nur wissen, dass es Sonderzeichen gibt wie z.B. \n, welches nicht als `\n' ausgegeben wird, sondern als Zeilenumbruch. Möchte man aus irgendwelchen Gründen tatsächlich mal die Zeichenkette `\n' und keinen Zeilenumbruch ausgeben lassen, muss man einen Backslash vorstellen: p r i n t f ( " Ich /* Ausgabe ist : zeige Ich ein zeige \\ n ein an \ n " ) ; \n an * / Das Komma nach dem ersten Parameter sowie der zweite Parameter ... werden bei konstanten Zeichenketten einfach weggelassen. B.1.1 Zeichenketten mit Platzhalter der 1. Parameter von printf Zunächst etwas komplizierter sind die Ausgaben von Zeichenketten, deren Inhalt variabel sein soll; er hängt von irgendwelchen Variablen oder Werten ab. Zunächst einmal schreibt man den 1. Parameter fast wie bei konstanten Zeichenketten. Der Unterschied ist, dass an den Stellen, wo variable Inhalte ausgegeben werden sollen, Platzhalter hingeschrieben werden. Platzhalter Die Platzhalter sind von der Form %<− oder +><Mindestbreite><Praezision><Umwandlungszeichen> Das liest sich sehr kompliziert, aber ist gar nicht so schwer. Man muss nur richtig einsetzen: <Umwandlungszeichen>: Legt fest, was für eine Art von Wert bzw. ob ein String ausgegeben wird. Dieses muss immer gegeben sein, wenn man einen Platzhalter mit Zeichen i f lf e E c s % anfängt. Datentyp int float double double/float double/float char Array von char-Variablen Anmerkung Standard: 6 Nachkommastellen Standard: 6 Nachkommastellen wissenschaftl. mit wissenschaftl. mit e E String siehe 6.3, S. 25 Tabelle 2: Umwandlungszeichen für Formatangaben 38 Wissenschaftliche Schreibweise: Der double-Wert oder float-Wert wird mit einer Zehnerpotenz multipliziert, so dass genau eine Vorkommastelle ungleich Null geschrieben steht. exp wird eexp bzw. Eexp mit mindestens zwei Ziern und Vorzeichen es wird Der Exponent vom multiplikativ Inversen dieser Zehnerpotenz als mit Nullen aufgefüllt hinten angestellt. Man kennt dies eventuell von Taschenrechnern bei sehr groÿen oder sehr kleinen Zahlen. Die Gleitkommazahl 103.4536473 würde z.B. (mit der Standardpräzision) als 1.034536+e02 dargestellt bzw. als 1.034536+E02. Optional:<- oder +>: Man setzt keines, eines oder beide dieser Zeichen. Das Zeichen - sorgt dafür, dass der Wert oder der String bzw. Zeichenkette links- gbündig ausgegeben werden (standardmäÿig wird er rechtsbündig ausgegeben). Das Zeichen + sorgt dafür, dass Zahlenausgaben immer mit Vorzeichen stattnden. Optional:<Mindestbreite der Ausgabe>: Mindestanzahl der Zeichen, mit denen der Wert oder String ausgegeben wird. Es handelt sich hierbei um alle Zeichen, welche zur Ausgabe des Platzhalters gehören. Hat der auszugebende Wert oder String in der Ausgabe gemäÿ Umwandlungszeichen normalerweise weniger Zeichen, so wird die Dierenz mit Leerzeichen aufgefüllt bei linksbündiger Ausgabe nach rechts und standardmäÿig nach links. Optional:<Präzision>: Ganzzahl mit einem vorgestellten über die Umwandlungszeichen f, lf , e oder E .. Bei der Ausgabe wird über die Präzision die Anzahl der angezeigten Nachkommastellen eingestellt. Bei der Ausgabe über i wird damit die Mindestanzahl von Ziern angeint-Wert ausgegeben werden soll. Hat der int-Wert das Umwandlungszeichen geben, mit welcher der weniger Ziern, so wird die Dierenz nach links mit Nullen aufgefüllt. B.1.2 Zeichenketten mit Platzhalter der 2. Parameter von 34 printf Hat man Zeichenketten mit Platzhaltern, so wird der 2. Parameter der printf- Funktion benötigt. Er listet auf, welche Variablen bzw. Werte / Strings ausgegeben werden sollen. Es handelt sich beim 2. Parameter von printf selbst also wieder um eine Parameterliste. Die Reihenfolge dieser Sub-Parameter henfolge der Platzhalter im 1. Parameter von printf. entspricht der Rei- Sie werden wie gewöhnliche Parameter mit Kommata voneinander getrennt. Das Umwandlungszeichen tet ein Array von char-Variablen s erwar- dieses übergibt man einfach mit dem Namen. Quelltext 19 liefert ein Beispiel. 34 Ist die Präzision beim Umwandlungszeichen i kleiner als die Mindestbreite der Ausgabe und wird rechtsbündig ausgegeben, so wird die Dierenz zwischen Mindestbreite der Ausgabe und Präzision mit Nullen aufgefüllt. Ansonsten wird nur mit Nullen nach links aufgefüllt. 39 1 / * angenommen , 2 i n t i =0; double z a h l = 0 . 0 2 5 4 3 4 5 , z a h l A r r [ 3 ] = { 1 , 2 . 5 6 , 3 . 0 0 } ; char z K e t t e [ 2 0 ] = " Das s i n d 1 9 Z e i c h e n " ; / * mehr B u c h s t a b e n 3 4 passen man h a t i n Form folgende eines Definitionen Strings auch nicht */ in dieses Array */ 5 6 i =19; /* setze i auf 19 * / 7 8 p r i n t f ( " zKette enthaelt f o l g e n d e %i Z e i c h e n : \ n%s \ n " , i , zKette ) ; 9 /* in 10 in " Parameter " Parameter dann 1" 2" finden folgen sich in die P l a t z h a l t e r %i , entsprechender analog 12 printf (" zahl hat Wert % l f \ n " , 13 printf (" zahl hat Wert %+.7 l f \ n " , 14 printf (" zahl hat Wert 15 printf (" zahl hat Wert % − 11.7 l f ! \ n " , 16 printf (" zahl hat Wert %.7 e \ n " , zahl ) ; 17 printf (" zahl hat Wert %.7E\ n " , zahl ) ; 18 printf (" zahl hat Wert %10.2E\ n " , 19 p r i n t f ( " der geht es printf (" int Quelltext 19: zahl ) ; zahl ) ; %11.7 l f ! \ n " , von zahl ) ; zahl ) ; zahl ) ; zKette ist %c \ n " , zKette [ 2 ] ) ; */ 1. 0! */ Buchstabe 2! printf (" die Index weiter . . . 3. /* I n d e x 21 i , zKette 11 20 dann %s . Reihenfolge Zahl von zahlArr ist %l f \n" , z a h l A r r [ 0 ] ) ; /* */ hat den Wert %5.3 i \ n " , Beispiel für Aufrufe von i) ; printf für variable Ausgaben. Siehe Abbildung 10 für die resultierenden Ausgaben Man hat dann die Ausgaben aus Abbildung 10. zKette Das enthaelt sind 19 folgende 19 Zeichen : Zeichen zahl hat Wert 0.025434 zahl hat Wert +0.0254345 zahl hat Wert zahl hat Wert 0.0254345 zahl hat Wert 2 . 5 4 3 4 5 0 0 e −02 zahl hat Wert 2 . 5 4 3 4 5 0 0 E−02 zahl hat Wert 0.0254345! der 3. Buchstabe die 1. Zahl von ! 2 . 5 4 E−02 von zKette zahlArr ist ist s 1.000000 Abbildung 10: Die aus den Aufrufen in Quelltext 19 resultierenden Ausgaben. Möchte man tatsächlich ein %-Zeichen ausgeben, so kann man dieses wie in B.1 beschrieben mit einem vorgestellten \ tun (also statt `%' die Zeichen `\%'). 40 B.1.3 Wie die Zeichen ausgegeben werden Die vorgegebene Ausgabe ndet Zeichen für Zeichen von links nach rechts statt. Zeilenumbrüche müssen mit dem Spezialzeichen B.2 \n manuell vorgenommen werden. Einlesen in char-Arrays von der Tastatur fgets char-Arrays eingelesen RETURN mit eingelesen, so dass der String im char-Array Mit dieser Funktion können Strings von der Tastatur in werden. Allerdings wird das 35 Die Eingabe soll von der Tastatur kommen. nicht direkt verwendet werden soll. Die Deklaration lautet: char* fgets(char* s, int size, FILE* stream); Funktionstyp: Die Funktion ist vom Datentypen char* , aber auch hier soll das nicht weiter von Bedeutung sein und Rückgabewerte nicht übernommen werden. 1. Parameter char* s: Hier soll ein Array von char-Variablen übergeben werden. In dieses wird ein String eingelesen. 2. Parameter int size: Hier soll der Wert die Gröÿe des Arrays sein, welches als 1. Parameter übergeben wird. array, welches mit der Anchar array[20] deniert wurde, so ist der 2. Parameter die int-Konstante Übergibt man zum Beispiel als 1. Parameter das Array weisung 20 . Diese Angabe verhindert Schreibversuche des Programms in ihm nicht zuge- ordneten Speicherbereich. 3. Parameter FILE* stream: Bei fgets soll hier immer stehen. Das bewirkt das Einlesen von Tastatur. B.3 36 stdin geschrieben Einlesen in char-Arrays oder Variablen von Strings oder char-Arrays sscanf Diese Funktion wird genutzt, um aus einem String oder riablen oder char-Arrays char-Array Werte in Va- einzulesen. Die Deklaration lautet: int sscanf(const char* str, const char* format, ...); Funktionstyp: Die Funktion ist vom Datentypen int, aber das soll in diesem Skript nicht von Bedeutung sein. Die Rückgabewerte können Sie ignorieren. 35 Es gibt Knie, dies zu vermeiden, aber diese werden hier nicht beschrieben, sondern es wird auf B.3 verwiesen. 36 Zusatz-Anmerkung: Dateien, einzulesen. Es besteht auch die Möglichkeit von anderen Quellen, wie z.B. geöneten 41 1. Parameter const char* str: Hier soll ein Array von char-Variablen über- geben werden, welches einen String enthält. Aus diesem werden der String oder die Variablen eingelesen. Man übergibt auch hier das Array mit seinem Namen. const char* format: 2. Parameter printf Es handelt sich hier ähnlich wie bei um einen Ausdruck der Form <Zeichenkette> Mit dieser Zeichenkette gibt man vor, nach was man in dem aus dem 1. Parameter erhaltenen sucht. Für die Zwecke dieser Vorlesung werden hier nur einfache Konstrukte erörtert. Erlaubt sind Leerzeichen, Tabs und Platzhalter, welche wie in printf mit einem %-Zeichen anfangen. Ein Leerzeichen zwischen zwei Platzhaltern gibt an, dass nach einem dem 1. Platzhalter entsprechenden Wert beliebig viele Leerzeichen, Tabs oder Zeilenumbrüche folgen können auch gar keines dieser Zeichen und dann ein Wert, welcher dem 2. Platzhalter entspricht. Die Platzhalter fangen wie bei printf mit einem %-Zeichen an. Danach folgt eines der Zeichen i , c , f , lf , s Diese sind stellvertretend für die Datentypen entsprechend Tabelle 2 auf S. 37. Wichtig: Strings aus Strings korrekt einzulesen ist etwas komplizierter, wenn sie Leerzeichen enthalten. Das wird in dieser Vorlesung auch nicht vorkommen. Möch- s gemäÿ charArray, in welches eingelesen wird, muss dabei min37 destens so groÿ sein, wie das char-Array, aus welchem eingelesen wird. . Möchte man also aus dem String des 1. Parameters einen int einlesen, so schreibt man im 2. Parameter "%i" und sscanf wird den ersten Integer in der Zeichenkette des 1. te man einzelne Wörter einlesen, so kann man das Umwandlungszeichen Tabelle 2 benutzen. Das Parameters einlesen. 3. Parameter: printf handelt es sich auch hier um eine Sub-Parameterprintf so geordnet, wie die Platzhalter des vorherigen Para- Wie bei liste. Sie ist wie bei meters. Anstatt aber die Datentypen der einzulesenen Werte hinzuschreiben, muss char-Arrays werden mit int einlesZahl; de&einlesZahl, damit in die man Pointer auf die jeweiligen Datentypen hinschreiben ihrem Namen angegeben. Hat man z.B. eine Variable mit niert, in welche man einlesen möchte, so schreibt man Speicheradresse von B.4 einlesZahl 38 eingelesen wird. Einlesen in Variablen oder char-Arrays von Tastatur Man möchte des Öfteren Eingaben des Nutzers von der Tastatur in Variablen oder char-Arrays einlesen können. char-Array zu machen: Die beste Methode ist, dazu den Umweg über ein 1. String von Tastatur einlesen (in ein Array von char-Variablen), siehe B.2 2. Werte für die Variablen aus dem gerade eingelesenen String einlesen, siehe B.3 37 Es gibt auch 38 Siehe 5.1 andere Lösungen, diese ist jedoch einfacher... 42 und man hat, was man wollte. Abbildung 11 verdeutlicht dies. Quelltext 20 liefert ein Beispiel. Abbildung 11: Das Einlesen von Tastatur verdeutlicht 43 1 #include <s t d i o . h> 2 3 4 5 i n t main ( void ) { char b u f f e r [ 1 0 0 ] , double z a h l ; papagei [ 1 0 0 ] ; 6 p r i n t f ( " Geben 7 Sie eine Gleitkommazahl zum Einlesen ein : " ) ; /* E i n l e s e n 8 von der fgets ( buffer 9 kompletten Eingabe b i s RETURN i n buffer T a s t a t u r */ , 10 /* E i n l e s e n 11 sscanf ( buffer , 12 p r i n t f ( "% l f 100 , vom Wert stdin fuer ); zahl aus "% l f " , &z a h l ) ; die eingegebene ist b u f f e r */ Zahl \n" , zahl ) ; 13 /* E i n l e s e n 14 der kompletten Eingabe bis Return in das Array name * / p r i n t f ( " Ich 15 bin ein Papagei . Schreiben Sie mir ein Wort . \ n" ) ; /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 16 17 papagei soll 18 Groesse 100 mind . Groesse 100 19 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 20 fgets ( buffer , 21 s s c a n f ( papagei , 100 , da buffer stdin ) ; , s t d i n ) ; /* 22 p r i n t f ( "BWAAAAK! %s " , papagei ) ; 23 return 0 ; 100 haben , hat 100 Groesse von Papagei ist */ 24 } Quelltext 20: Kompilierbares Beispiel für das Einlesen einer Variable und eines Strings von Tastatur B.5 Önen und Schlieÿen von Dateien fopen und fclose fopen önet Dateien, fclose schlieÿt sie. Man sollte nie vergessen, geönete Dateien zu schlieÿen. Gewöhnlicherweise tut man dies, nachdem man sie nicht mehr braucht. B.5.1 Der Datentyp FILE* Man benötigt für den Zugri auf Dateien einen neuen Datentypen, den Dateizeiger FILE* welcher mit der stdio-Bibliothek kommt. Sie brauchen nur zu wissen, wie man sich Variablen diesen Typs deniert. Und zwar mit FILE * <Name> ; /* k o n k r e t e s FILE * B e i s p i e l : */ datZeiger ; datZeiger /* erstellt Dateizeiger m i t dem Namen */ Innerhalb eines Programmes ist jede Datei eindeutig identiziert durch ihren Dateizeiger. 44 B.5.2 fopen Die Deklaration lautet für fopen FILE* fopen(const char* path, const char* mode); FILE*: Funktionstyp Die Funktion gibt bei erfolgreichem Önen den Datei- zeiger auf die geönete Datei zurück. Konnte die angegebene Datei nicht geönet werden (Gründe wären da z.B. falscher Pfad, fehlende Rechte etc.), dann wird als FILE* -Wert die Konstante NULL zurückgegeben. Damit man was mit der geöneten Datei anfangen kann, muss der Rückgabewert immer in einer Variable vom Typen 1. Parameter FILE* const char* path: gespeichert werden! Hier soll zwischen doppelten Anführungs- inputDat.inp z.B. bei ei/home/studiname/programm/inputDat.inp strichen der Pfad zu einer Datei stehen. Hat die Datei nem Linux-basierten System den Pfad , so übergibt man die Zeichenkette "/home/studiname/programm/inputDat.inp" 2. Parameter const char* mode: Hier wird festgelegt, ob die Datei zum Lesen geönet wird dann übergeben Sie die Zeichenkette "r" (inklusive der Anführungszeichen) oder zum Schreiben geönet wird dann übergeben Sie einfach "w" Ein Beispiel folgt in B.5.3. B.5.3 fclose Die Deklaration lautet für fclose int fclose( FILE* ); Funktionstyp int: Soll hier nicht von Bedeutung sein, d.h. Rückgabewerte können ignoriert werden. Einziger Parameter FILE* Hier wird der Dateizeiger für die zu schlieÿende Datei angegeben. Wichtig: Jedes Önen mit fopen im Schreibmodus, also mit "w" als zweiten Parameter löscht die Inhalte der Datei, wenn sie schon existiert! Existiert sie noch 45 nicht, wird sie angelegt. Ein exemplarischer Aufruf mit Prüfung, ob die Datei erfolgreich geönet werden konnte, könnte so aussehen: 1 FILE * datLes , // datSchreib ; Definition der D a t e i <Zeiger> 2 3 "<Datei datLes = fopen ( 1>" , "r" //<Datei ) 1> zum L e s e n oeffnen 4 5 i f ( d a t L e s != NULL ) { /* d i e 6 folgenden A n we i s u ng e n werden nur dann ausgefuehrt , wenn 7 das 8 printf ( Oeffnen " Oeffnen erfolgreich /* wird der war * / Datei e r f o l g r e i c h \n" ) ; 9 10 hier <Anweisungen> 11 irgendwas mit der Datei <Datei 1> gemacht */ 12 fclose ( 13 datLes ) /* s c h l i e " s e n ; der Datei */ 14 } 15 else 16 p r i n t f ( " Datei konnte ohne */ nicht geoffnet werden " ) ; 17 18 /* 19 datSchreib = fopen ( Pruefung Schreiben "<Datei 2>" , "w" ) //<Datei 2> zum oeffnen 20 21 /* hier wird 22 <Anweisungen> 23 fclose ( B.6 irgendwas datSchreib ) ; mit /* der Datei schliessen <Datei 2> von gemacht <Datei 2> */ */ Ausgabe in Dateien fprintf Die Deklaration lautet für fprintf int fprintf(FILE *stream, const char *format, ...); Funktionstyp int: 1. Parameter FILE* stream: Für diese Vorlesung nicht weiter von Bedeutung. Gibt an, in welche (geöneten) Datei eingelesen werden soll (diese muss im Schreibmodus geönet worden sein). 2. Parameter const char *format: Wie der 2. Parameter in B.1, nur wird hier nicht auf dem Bildschirm, sondern in eine Datei ausgegeben. 3. Parameter const char *format: Genau wie der 3. Parameter in B.1. 46 Die vorgegebene Ausgabe ndet in die Textdatei Zeichen für Zeichen von links nach rechts statt solange die Datei geönet geblieben ist, wird weiter ab dem letzten ausgegebenen Zeichen des vorherigen Aufrufes von Zeilenumbrüche müssen mit dem Spezialzeichen \n fprintf ausgegeben. manuell vorgenommen werden. Quelltext 21 gibt ein Beispiel für die Ausgabe in eine Datei. 1 #include < s t d i o . h> 2 3 i n t main ( void ) { 4 FILE * datout ; char b u f f e r [ 1 0 0 ] , int zahl ; speicher [100] 8 printf (" Bitte eine 9 fgets ( buffer 5 6 , wort [ 1 0 0 ] ; 7 Einlesen gib , 10 /* 11 wie 12 sscanf ( buffer , buffer 100 , Datei c h a r −Array in Groesse 100 "%s " zum Hineinschreiben an : ") ; stdin ) ; , speicher , wort hat ! */ speicher ) ; 13 14 printf (" Bitte 15 fgets ( buffer gib 16 sscanf ( buffer , , i n t −Z a h l eine 100 s t d i n ) ; /* , "%i " , &z a h l ); ein : ") ; Einlesen /* in Einlesen buffer in Zahl */ */ 17 18 p r i n t f (" Schreib 19 /* Einlesen in 20 fgets ( buffer 21 /* 22 wie 23 sscanf ( buffer , Einlesen buffer ein Wort : buffer , 100 in , stdin ) ; Array Groesse wort , 100 "%s " ") ; */ , wort hat ! */ wort ) ; 24 25 / * zum 26 datout = fopen Schreiben ( 27 /* Zahl Ausgabe 28 fprintf 29 /* ( fprintf 31 /* 32 fclose ( der ( , , der datout mit mit "w" ) ; , zahl ) ; ) ; \n * / "%s \ n " Datei , \n * / "%i \ n " Zahl datout schliessen */ speicher datout Ausgabe 30 der oeffnen , wort */ ) ; 33 34 return 0 ; 35 } Quelltext 21: Kompilierbares Beispiel für B.7 fprintf. Einlesen von Dateien fscanf Die Deklaration lautet für fscanf int fscanf(FILE* stream, const char *format, ...); 1. Parameter FILE* stream: Gibt an, von welcher (geöneten) Datei einge- lesen werden soll (diese muss im Lesemodus geönet worden sein). 47 2. Parameter const char *format: Wie der 2. Parameter in B.3, nur wird hier Text aus einer Datei eingelesen. 3. Parameter const char *format: int: Funktionstyp Genau wie der 3. Parameter in B.3 . Die Funktion gibt an, in wie viele der durch die Sub- Parameter des 3. Parameters referenzierten Variablen bzw. char-Arrays erfolgreich eingelesen werden konnte. Es handelt sich also um die Anzahl der erfolgreichen Einlesevorgänge des Funktionsaufrufes. Es folgt das letzte Beispiel dieses Skriptes. Angenommen, die Inhalte der Datei textdatei, welche (auf einem Linux-System) im Programmverzeichnis liegt, sind Fabian 1.8 Wenn das Programm nach folgendem Quelltext liefe, würden Name und Körpergröÿe eingelesen: 1 #include < s t d i o . h> 2 3 i n t main ( void ) { 4 /* Variablendefinitionen 5 FILE * 6 7 */ datin ; char <Name> [ 1 0 0 ] ; double g r o e s s e ; 8 9 /* O e f f n e n zum Lesen , des 10 DateiZeigers 11 datin = fopen 12 /* E i n l e s e n Es muss 13 14 gemacht 15 <Name> 16 groesse 17 fscanf 18 printf ( ( des hier Uebernahme */ " ./ textdatei " Strings und der k e i n Umweg u e b e r , "r" ); Zahl fgets werden ! ist schon noch ( eine nicht , datin , Speicheradresse , daher "%s % l f " "Name : %s , d a s &−Z e i c h e n , <Name> */ , &g r o e s s e ) K o e r p e r g r o e s s e : %l f \n" , ; <Name> , groesse ) ; 19 20 return 0 ; 21 } Quelltext 22: Aufruf von fscanf für das Einlesen der Werte aus der Datei textdatei