![]() |
|
|||||
wert = 7; zahl = 5 / 2;
Kommentare Neben den Anweisungen kann ein C-Programm Kommentare enthalten. Ein Kommentar steht zwischen den Zeichenfolgen /* und */ und kann beliebig viele Zeilen umfassen. Der Compiler ignoriert Kommentare; sie dienen dazu, Ihnen die Orientierung im Programmcode zu erleichtern. Kommentare dürfen nicht ineinander verschachtelt werden, da das erste Auftreten von */ den Kommentar bereits aufhebt. Leere Zeilen im Programmcode werden ignoriert, auch vor Anweisungen und zwischen den einzelnen Elementen einer Programmzeile dürfen beliebig viele Leerzeichen stehen. Der Ausdruck a + b ist äquivalent zu a+b. Allerdings dürfen Sie innerhalb von Bezeichnern keine Leerzeichen einfügen, auch einige Operatoren bestehen aus mehreren Zeichen, die nicht voneinander getrennt werden dürfen (beispielsweise <= oder &&). Bezeichner für Variablen und Funktionen dürfen aus Buchstaben, Ziffern und _ (Unterstrich) bestehen. Sie dürfen allerdings nicht mit einer Ziffer beginnen. Der ANSI-C-Standard, an den sich im Prinzip alle aktuellen C-Versionen halten, schreibt vor, dass mindestens die ersten 31 Zeichen der Bezeichner ausgewertet werden müssen. Sollten zwei Bezeichner erst beim 32. Zeichen voneinander abweichen, halten manche Compiler sie für ein und denselben. Bei Bezeichnern wird zwischen Groß- und Kleinschreibung unterschieden. VariablenWie bereits erwähnt, ist eine Variable ein benannter Speicherplatz. Anders als in der Mathematik besitzt eine Variable in einer Programmiersprache jederzeit einen definierten Wert; es handelt sich also zur Laufzeit des Programms nicht um einen Platzhalter. Variablen müssen zu Beginn jeder Funktion deklariert werden. Dies geschieht durch die Angabe des Datentyps und des Bezeichners; optional kann ein Anfangswert zugewiesen werden. Das folgende Beispiel deklariert die beiden Variablen a und b, wobei nur b ein Wert zugewiesen wird: int a; double b = 2.5; a wird als int (Integer oder Ganzzahl) deklariert, dient also der zukünftigen Speicherung einer ganzen Zahl. b erhält den Datentyp double, der zur Ablage doppelt genauer Fließkommazahlen verwendet wird. Tabelle 5.1 zeigt eine Übersicht über die verschiedenen Datentypen und ihre Bedeutung.
Ganzzahlige Datentypen Die ganzzahligen Datentypen int, short, long und char speichern einen Integer-Wert je nach Bedarf mit oder ohne Vorzeichen. Sie können der Deklaration signed voranstellen, um Werte mit Vorzeichen zu speichern, oder unsigned für Werte ohne Vorzeichen. Dies bedeutet beispielsweise für einen 32-Bit-Integer, dass bei signed Werte zwischen -2.147.483.648 und +2.147.483.647 möglich sind, während unsigned die Werte 0 bis 4.294.967.295 zulässt. Eine Erläuterung dieser Werte finden Sie in Kapitel 2, Mathematische und technische Grundlagen. Standardmäßig sind alle Integer-Werte signed, ein Sonderfall ist lediglich char: Da dieser Typ in der Regel zur Darstellung einzelner ASCII-Zeichen eingesetzt wird, ist er zumindest in diesem Zusammenhang unsigned. Für die Zeichen modernerer Zeichensätze wie Unicode, die mehr Speicher benötigen als 8 Bit, könnten Sie einfach unsigned int benutzen, empfehlenswerter ist jedoch die Verwendung des speziellen Typs wchar_t, wofür Sie allerdings mittels #include die Header-Datei stddef.h einbinden müssen. Die verschiedenen Zeichensätze werden in Kapitel 11, Datei- und Datenformate, besprochen. Auffällig ist, dass es in C keinen separaten Datentyp für boolesche Wahrheitswerte gibt. Folgerichtig gelten Ausdrücke mit dem Wert 0 als falsch und alle anderen als wahr. Gültigkeitsbereich von Variablen Ein weiteres Merkmal von Variablen ist ihr Gültigkeitsbereich (englisch scope), der festlegt, in welchen Programmteilen eine Variable definiert bleibt. Grundsätzlich werden zwei verschiedene Arten von Variablen unterschieden:
Eine besondere Form der lokalen Variablen sind die statischen Variablen: Wenn Sie in einer Funktion eine Variablendeklaration mit dem Schlüsselwort static einleiten, gilt die Variable zwar nur innerhalb dieser Funktion, behält aber ihren Wert bis zum nächsten Aufruf dieser Funktion bei. Ausdrücke und OperationenZu den wichtigsten Fähigkeiten von Programmiersprachen gehört die Auswertung beziehungsweise Berechnung von Ausdrücken. An jeder Stelle, an der ein bestimmter Wert erwartet wird, kann stattdessen ein komplexer Ausdruck stehen, der zunächst ausgewertet und anschließend als Wert eingesetzt wird – Voraussetzung ist allerdings, dass der Ausdruck einen passenden Datentyp besitzt. Literale Die einfachsten Bestandteile von Ausdrücken sind die Literale. Es handelt sich dabei um Werte, die nicht weiter berechnet oder ersetzt werden müssen. C unterscheidet die folgenden vier Arten von Literalen:
Ein weiterer Bestandteil von Ausdrücken sind Variablen, die bei der Auswertung jeweils durch ihren aktuellen Wert ersetzt werden: a = 5; b = a + 7; /* b hat nun den Wert 12 */ Datentypkonvertierung Mitunter besitzt eine Variable, die Sie in einem Ausdruck verwenden möchten, den falschen Datentyp. C konvertiert den Datentyp immer dann automatisch, wenn keine Gefahr besteht, dass dabei Werte verloren gehen oder verfälscht werden. Beispielsweise wird ein int ohne weiteres akzeptiert, wo ein Fließkommatyp erwartet wird. In Fällen, in denen diese Gefahr besteht, müssen Sie die Typumwandlung dagegen explizit anordnen. Dies geschieht durch so genanntes Typecasting, dabei wird der gewünschte Datentyp in Klammern vor die umzuwandelnde Variable oder auch vor ein Literal geschrieben:
Sie können innerhalb eines Ausdrucks sogar eine Funktion aufrufen; vorausgesetzt, sie liefert einen Wert mit dem passenden Datentyp zurück: a = sin (b); /* a enthält den Sinus von b */ Operatoren Neben all diesen Elementen, die jeweils einen einzelnen Wert ergeben, können Ausdrücke auch noch Operatoren enthalten. Diese dienen dazu, verschiedene Werte arithmetisch oder logisch miteinander zu verknüpfen. Beachten Sie, dass nicht jeder Operator zu jedem Datentyp passt. Die arithmetischen Operatoren sind + (Addition), – (Subtraktion), * (Multiplikation), / (Division) und % (Modulo; der Rest einer ganzzahligen Division). Logische Operatoren Die nächste Gruppe bilden die logischen Operatoren. Sie dienen dazu, Werte nach logischen Kriterien miteinander zu verknüpfen:
Bit-Operatoren Ähnlich wie die logischen Operatoren, aber auf einer anderen Ebene, arbeiten die Bit-Operatoren: Sie manipulieren den Wert ihrer Operanden bitweise, betrachten also jedes einzelne Bit. Im Einzelnen sind die folgenden Bit-Operatoren definiert:
0101 111 & 0100 1001 ----------- 0100 1000
0101 111 | 0100 1001 ----------- 0101 1111
0101 111 ^ 0100 1001 ----------- 0001 0111
Vergleichsoperatoren Für die Flusskontrolle von Programmen sind die Vergleichsoperatoren von besonderer Bedeutung: Sie vergleichen Ausdrücke miteinander und liefern je nach Ergebnis 0 oder einen wahren Wert. Es sind folgende Vergleichsoperatoren definiert:
Zu guter Letzt gibt es noch den Wertzuweisungs-Operator =, der dem Operanden auf der linken Seite den Wert des Ausdrucks auf der rechten Seite zuweist. Bei dem linken Operanden handelt es sich in der Regel um eine Variable. Allgemein werden Elemente, die auf der linken Seite einer Wertzuweisung stehen können, als LVALUE bezeichnet. Sehr häufig kommt es vor, dass eine Wertzuweisung den ursprünglichen Wert des LVALUE ändert, sodass das LVALUE selbst in dem Ausdruck auf der rechten Seite auftaucht. Für diese speziellen Fälle wurden einige Abkürzungen definiert, die wichtigsten von ihnen werden in Tabelle 5.2 aufgeführt.
Neben den in der Tabelle angegebenen Abkürzungen gibt es auch für die logischen Operatoren und die Bit-Operatoren entsprechende Schreibweisen. Eine Sonderstellung nehmen die Operatoren ein, die ein LVALUE um 1 erhöhen beziehungsweise vermindern: Sie können ++ oder -- entweder vor oder hinter das LVALUE schreiben. Wenn Sie dies als einzelne Anweisung hinschreiben, besteht zwischen diesen Varianten kein Unterschied. Werden sie dagegen im Rahmen eines komplexen Ausdrucks verwendet, dann ist der Unterschied folgender:
a = 1; b = ++a; /* a hat den Wert 2, b auch. */
a = 2; b = a++; /* a hat den Wert 3, b bleibt 2 */
Ein besonderer Operator ist der Fallentscheidungs-Operator: Die Schreibweise Ausdruck1 ? Ausdruck2 : Ausdruck3 hat Ausdruck2 als Ergebnis, wenn Ausdruck1 wahr ist, ansonsten ist der Wert Ausdruck3. Beispiele: a = 2; b = (a == 1 ? 3 : 5); /* a ist nicht 1, also erhält b den Wert 5 */ a = 1; b = (a == 1 ? 3 : 5); /* a ist 1, also erhält b den Wert 3 */ Rangfolge der Operatoren Bei der Arbeit mit Operatoren ist zu beachten, dass sie mit unterschiedlicher Priorität ausgewertet werden. Die folgende Liste stellt die Rangfolge der Operatoren in absteigender Reihenfolge dar. Die weiter oben stehenden Operatoren binden also stärker und werden zuerst aufgelöst:
Sie können die Rangfolge der Operatoren durch die Verwendung von Klammern verändern. Beispielsweise besitzt der Ausdruck 3 * 4 + 7 den Wert 19, während 3 * (4 + 7) das Ergebnis 33 liefert. Beachten Sie, dass zu diesem Zweck immer nur runde Klammern verwendet werden dürfen, egal wie tief sie verschachtelt werden! KontrollstrukturenEine der wesentlichen Aufgaben von Computerprogrammen besteht darin, den Programmablauf in Abhängigkeit bestimmter Bedingungen zu steuern. Zu diesem Zweck definiert C eine Reihe so genannter Kontrollstrukturen, die man grob in Fallentscheidungen und Schleifen unterteilen kann. Eine Fallentscheidung überprüft die Gültigkeit einer Bedingung und führt in Abhängigkeit davon bestimmte Anweisungen aus; eine Schleife sorgt dagegen dafür, dass bestimmte Anweisungsfolgen mehrmals hintereinander ausgeführt werden. if( )-Fallentscheidungen Die einfachste und wichtigste Kontrollstruktur ist die Fallentscheidung mit if(). Der Ausdruck, der hinter dem Schlüsselwort if in Klammern steht, wird ausgewertet; wenn er wahr (nicht 0) ist, wird die auf das if folgende Anweisung ausgeführt. Das folgende Beispiel überprüft, ob die Variable a größer als 100 ist und gibt in diesem Fall »Herzlichen Glückwunsch!« aus:
Mitunter kommt es vor, dass mehrere Anweisungen von einem einzelnen if() abhängen. In diesem Fall müssen Sie hinter der Bedingungsprüfung einen Anweisungsblock schreiben, also eine Sequenz von Anweisungen in geschweiften Klammern. Folgendes Beispiel überprüft, ob die Variable b kleiner als 0 ist. In diesem Fall setzt sie b auf 100 und gibt eine entsprechende Meldung aus:
Die öffnende geschweifte Klammer schreiben manche Programmierer lieber in die nächste Zeile. Beide Varianten sind üblich und zulässig, Sie sollten sich allerdings konsequent an eine davon halten. In diesem Buch wird die Klammer bei Kontrollstrukturen in dieselbe und bei Funktions- oder Klassendefinitionen in die nächste Zeile gesetzt – auch diese Variante ist gebräuchlich. Letzten Endes lohnt es sich übrigens, auch bei if-Abfragen, von denen nur eine einzige Anweisung abhängt, geschweifte Klammern zu verwenden. Erstens kann Ihnen so nicht der Fehler passieren, dass Sie die Klammern vergessen, wenn später weitere Anweisungen dazukommen. Zweitens gibt es andere Programmiersprachen wie etwa Perl, bei denen die Klammern zwingend vorgeschrieben sind. Bedingungsausdrücke Für die Formulierung des Bedingungsausdrucks bieten sich einige Abkürzungen an, die mit der logischen Interpretation von 0 und anderen Werten zu tun haben. Wollen Sie beispielsweise Anweisungen ausführen, wenn die Variable a den Wert 0 hat, können Sie entweder die ausführliche Bedingung a == 0 schreiben oder die Abkürzung !a verwenden: Die Negation von a ist genau dann wahr, wenn a gleich 0 ist. Sollen dagegen Anweisungen ausgeführt werden, wenn a nicht 0 ist, genügt sogar ein einfaches a als Bedingung. Diese Nachlässigkeit bei der Überprüfung von Datentypen macht C zu einer so genannten schwach typisierten Sprache: Variablen besitzen festgelegte Datentypen, diese werden aber bei Bedarf sehr großzügig ineinander konvertiert. else Es kommt sehr häufig vor, dass auch bei Nichtzutreffen einer Bedingung spezielle Anweisungen ausgeführt werden sollen. Zu diesem Zweck besteht die Möglichkeit, hinter einer if-Abfrage einen else-Teil zu platzieren. Die Anweisung oder der Block hinter dem else wird genau dann ausgeführt, wenn die Bedingung der if-Abfrage nicht zutrifft. Das folgende Beispiel gibt »a ist positiv.« aus, wenn a größer als 0 ist, ansonsten wird »a ist nicht positiv.« ausgegeben:
Auch hinter dem else kann alternativ ein Block von Anweisungen in geschweiften Klammern folgen. Sie können hinter dem else sogar wieder ein weiteres if unterbringen, falls eine weitere Bedingung überprüft werden soll, wenn die ursprüngliche Bedingung nicht erfüllt ist. Die folgende verschachtelte Abfrage erweitert das vorige Beispiel so, dass auch die Fälle 0 und negativer Wert unterschieden werden:
Das kleine Beispielprogramm in Listing 5.2 verwendet verschachtelte if-else-Abfragen, um aus einer eingegebenen Punktzahl in einer Prüfung die zugehörige Note nach dem IHK-Notenschlüssel zu berechnen. Listing 5.2 Notenberechnung nach dem IHK-Schlüssel #include <stdio.h>
int main()
{
int punkte;
int note;
printf ("Ihre Punktzahl, bitte: ");
scanf ("%d", &punkte);
if (punkte < 30)
note = 6;
else if (punkte < 50)
note = 5;
else if (punkte < 67)
note = 4;
else if (punkte < 81)
note = 3;
else if (punkte < 92)
note = 2;
else
note = 1;
printf ("Sie haben die Note %d erreicht.\n", note);
return 0;
}
Die Funktion scanf() dient übrigens dazu, Daten verschiedener Formate einzulesen – im Gegensatz zu der weiter oben verwendeten Funktion gets(), die nur zum Einlesen von Strings verwendet wird. switch/case-Fallentscheidungen In anderen Fällen kann es vorkommen, dass Sie eine Variable nacheinander mit verschiedenen festen Werten vergleichen müssen, nicht mit Wertebereichen wie im obigen Beispiel. Für diesen Verwendungszweck bietet C die spezielle Struktur switch/case an. Das Argument von switch muss ein LVALUE sein, das nacheinander mit einer Liste von Werten verglichen wird, die hinter dem Schlüsselwort case stehen. Die einzelnen case-Unterscheidungen stellen dabei Einstiegspunkte in den switch-Codeblock dar. Wenn das LVALUE einem der Werte in der Liste entspricht, wird von dieser Stelle an der gesamte Block ausgeführt. Da dieses Verhalten oft unerwünscht ist, wird der Block vor jedem neuen case meist mittels break verlassen. Das folgende Beispiel ermittelt aus einer numerischen Note die entsprechende Zensur in Textform:
Hinter der optionalen Markierung default können Anweisungen stehen, die ausgeführt werden, wenn das geprüfte LVALUE keinen der konkreten Werte hat. Dies eignet sich insbesondere, um Fehleingaben abzufangen. Die grundlegend andere Art von Kontrollstrukturen sind Schleifen. Sie sorgen dafür, dass ein bestimmter Codeblock mehrmals ausgeführt wird, entweder abhängig von einer Bedingung oder mit einer definierten Anzahl von Durchläufen. while( )-Schleifen Die einfachste Form der Schleife ist die while()-Schleife. In den Klammern hinter dem Schlüsselwort while wird genau wie bei if() eine Bedingung geprüft; wenn sie zutrifft, wird der Schleifenrumpf (die Anweisung oder der Block hinter dem while) ausgeführt. Nach der Ausführung wird die Bedingung erneut überprüft – solange sie zutrifft, wird der Schleifenrumpf immer wieder ausgeführt. Das folgende Beispiel überprüft vor jedem Durchlauf, ob die Variable i noch kleiner als 10 ist und erhöht sie innerhalb des Schleifenrumpfes jeweils um 1:
i ist der bevorzugte Name für Schleifenzählervariablen. Diese Tradition stammt aus der Mathematik, wo i oft als Zähler bei Summenformeln oder Ähnlichem eingesetzt wird (Abkürzung für Index). Wenn mehrere Schleifen ineinander verschachtelt werden, heißen deren Zählervariablen j, k, l und so fort. Die Ausgabe dieses kurzen Beispiels sieht folgendermaßen aus (\t steht für einen Tabulator): 0 1 2 3 4 5 6 7 8 9 Da die Überprüfung der Bedingung vor dem jeweiligen Durchlauf erfolgt, findet der Abbruch statt, sobald i nicht mehr kleiner als 10 ist. Eine solche Schleifenkonstruktion wird als kopfgesteuerte Schleife bezeichnet. Eine andere Art der Schleife überprüft die Bedingung erst nach dem jeweiligen Durchlauf und heißt deshalb fußgesteuert. In C wird sie durch die Schreibweise do ... while() realisiert. Diese Art der Schleife ist nützlich, wenn die zu überprüfende Bedingung sich erst aus dem Durchlauf selbst ergibt, beispielsweise bei der Prüfung von Benutzereingaben. Das obige Beispiel sieht als fußgesteuerte do-while-Schleife so aus:
Interessanterweise sieht die Ausgabe dieser Schleife etwas anders aus: 0 1 2 3 4 5 6 7 8 9 10 Da die Bedingung erst nach der Ausgabe geprüft wird, erfolgt der Abbruch erst einen Durchlauf später. Anders als die kopfgesteuerte Schleife wird die fußgesteuerte mindestens einmal ausgeführt. Beachten Sie, dass hinter dem while() in diesem Fall ein Semikolon stehen muss. for( )-Schleifen Eine alternative Schreibweise für Schleifen ist die for-Schleife. Sie wird bevorzugt in Fällen eingesetzt, in denen eine festgelegte Anzahl von Durchläufen erwünscht ist. Die allgemeine Syntax dieser Schleife ist folgende: for (Initialisierung; Wertüberprüfung; Wertänderung) Anweisung Die Initialisierung wird genau einmal vor dem Beginn der Schleife ausgeführt. Die Wertüberprüfung findet vor jedem Durchlauf statt – wenn sie einen wahren Wert ergibt, wird der Schleifenrumpf ein weiteres Mal ausgeführt. Nach jedem Durchlauf findet die Wertänderung statt. Beispiel:
Dies erzeugt exakt dieselbe Ausgabe wie das obige kopfgesteuerte while-Beispiel; der Code ist sogar absolut äquivalent. Jede for-Schleife lässt sich auf diese Weise durch eine while-Schleife ersetzen – es handelt sich lediglich um eine spezielle Formulierung, die für determinierte Schleifen (Schleifen mit festgelegter Anzahl von Durchläufen) besser geeignet ist. FunktionenWie bereits erwähnt, besteht ein C-Programm aus beliebig vielen Funktionen, die Sie innerhalb Ihres Programms von jeder Stelle aus aufrufen können. Die wichtigste Funktion ist main(), weil sie die Aufgabe des Hauptprogramms übernimmt. Eine Funktion kann jeden der weiter oben für Variablen genannten Datentypen innehaben; es wird erwartet, dass eine Funktion mit einem bestimmten Datentyp mittels return einen Wert dieses Typs an die aufrufende Stelle zurückgibt. Eine Funktion, die »nur« bestimmte Anweisungen ausführen, aber keinen Wert zurückgeben soll, kann den speziellen Datentyp void haben. Strukturierung und Modularisierung Die wichtigste Aufgabe von Funktionen ist die Strukturierung des Programms. Es lohnt sich, häufig benötigte Anweisungsfolgen in separate Funktionen zu schreiben und bei Bedarf aufzurufen. Eine solche Modularisierung des Codes macht das Programm übersichtlicher, weil Sie sich in jeder Funktion auf eine einzelne Aufgabe konzentrieren können. Auf diese Weise lassen sich mehrere Abstraktionsebenen in ein Programm einführen: Grundlegende Bausteine können einmal implementiert und zu immer komplexeren Einheiten zusammengesetzt werden. Eine Funktion kann nicht nur einen Rückgabewert haben, sondern auch einen oder mehrere Eingabewerte, die in Form von Parametervariablen in die Klammern hinter dem Funktionsnamen geschrieben werden. Eine Funktion mit Parametern erwartet die Übergabe entsprechend vieler Werte mit dem korrekten Datentyp. Aus der Sicht der aufrufenden Stelle werden diese Werte als Argumente der Funktion bezeichnet, innerhalb der Funktion können sie wie normale lokale Variablen verwendet werden. Das folgende Beispiel zeigt eine Funktion namens verdoppeln(), die einen Wert vom Datentyp int entgegennimmt und das Doppelte dieses Werts zurückgibt:
Funktionsaufrufe Diese Funktion kann von einer beliebigen Programmstelle aus innerhalb eines Ausdrucks aufgerufen werden, wo ein Integer-Wert zulässig ist. Im folgenden Beispiel wird verdoppeln() aus einer printf()-Anweisung heraus aufgerufen, um das Doppelte der Variablen b auszugeben:
Eine Funktion vom Datentyp void wird dagegen als einzelne Anweisung aufgerufen. Das folgende Beispiel definiert eine Funktion namens begruessen(), die einen Gruß für den angegebenen Namen ausgibt:
Ein Aufruf dieser Funktion sieht etwa folgendermaßen aus:
Die Ausgabe sieht natürlich so aus: Hallo, Klaus! Übrigens kann auch die Funktion main() so geschrieben werden, dass sie Argumente entgegennimmt. Dies dient der Annahme von Kommandozeilenparametern. Die standardisierte Syntax für die Parameter von main() lautet folgendermaßen: int main (int argc, char *argv[]) Die Variable argc enthält dabei die Anzahl der übergebenen Argumente, während das Array argv[] die einzelnen Argumentwerte als Strings enthält. argv[0] enthält dabei kein echtes Argument, sondern den Namen des aufgerufenen Programms selbst. Arrays werden im folgenden Unterabschnitt näher erläutert. Zeiger und ArraysDer wichtigste Grund, warum C als schwierig zu erlernen und zu benutzen gilt, ist die Tatsache, dass in dieser Sprache mit Zeigern operiert werden kann. Ein Zeiger ist eine spezielle Variable, deren Wert eine Speicheradresse ist. Im Grunde handelt es sich dabei also um eine Art indirekte Variable: Eine »normale« Variable ist ein benannter Speicherplatz, in dem unmittelbar ein konkreter Wert abgelegt wird, ein Zeiger verweist dagegen auf den Ort, an dem sich der konkrete Wert befindet. Zeiger-Deklaration Zeiger sind unter anderem wichtig, damit Funktionen einander den Speicherort bestimmter Werte mitteilen können, um diese Werte gemeinsam manipulieren zu können. Ein Zeiger verweist jeweils auf einen Speicherplatz mit einem bestimmten Datentyp. Er unterscheidet sich von einer Variablen dieses Datentyps durch ein vorangestelltes *: int a; /* normale int-Variable */ int *b; /* Zeiger auf int */ Der Wert, der einer Zeigervariablen zugewiesen wird, ist normalerweise die Adresse einer anderen Variablen. Diese wird durch den Dereferenzierungs-Operator, ein vorangestelltes &, ermittelt. Im folgenden Beispiel wird der Zeigervariablen a die Adresse von b als Wert zugewiesen: int b = 9; int *a = &b; /* a zeigt auf b */ Wenn Sie daraufhin versuchen würden, den Wert von a selbst auszugeben, wäre das Ergebnis die unvorhersagbare und völlig sinnfreie Nummer einer Speicheradresse. Wenn Sie dagegen den Wert von *a ausgeben, erhalten Sie den Inhalt von b. Referenzübergabe Die interessante Frage ist natürlich, wozu man so etwas überhaupt benötigt. Ein gutes Beispiel ist eine Funktion, die den tatsächlichen Wert einer Variablen ändert, die ihr als Argument übergeben wird. Ein solcher Funktionsaufruf wird als Call by Reference bezeichnet, im Gegensatz zur einfachen Wertübergabe, die auch Call by Value heißt. Die folgenden beiden Funktionen demonstrieren diesen Unterschied:
Wenn die erste Funktion mit einer Variablen als Argument aufgerufen wird, ändert diese Variable selbst ihren Wert nicht: b = 3; doppel1 (b); /* Wert von b: 3 */ Die andere Funktion wird dagegen mit der Adresse einer Variablen aufgerufen und manipuliert unmittelbar den Inhalt dieser Speicherstelle: b = 3; doppel2 (&b); /* Wert von b: 6 */ Nahe Verwandte der Zeiger sind die Arrays. Es handelt sich dabei um Variablen, die mehrere durch einen numerischen Index ansprechbare Werte besitzen. Realisiert werden Arrays durch hintereinander liegende Speicherstellen, in denen die einzelnen Werte abgelegt werden. Jedes Array lässt sich alternativ durch einen Zeiger auf die Speicherstelle des ersten Elements beschreiben, die weiteren Elemente können angesprochen werden, indem zu dieser Adresse die Anzahl der Bytes addiert wird, die ein einzelnes Element einnimmt. Array-Deklaration Ein Array wird deklariert, indem hinter dem Variablennamen die gewünschte Anzahl von Elementen in eckigen Klammern angegeben wird: int a[10]; /* 10 int-Werte */ Die zehn Elemente des Arrays a[] werden als a[0] bis a[9] angesprochen. Alternativ können Sie die Zeiger-Schreibweise wählen: Die Elemente heißen dann *a bis *(a + 9). Sie können einem Array bei der Deklaration auch Anfangswerte zuweisen und dabei die Anzahl der Elemente weglassen, weil sie implizit feststeht:
Das Beispiel in Listing 5.3 definiert ein Array mit zehn Werten vom Datentyp int, die nacheinander vom Benutzer eingegeben werden. Anschließend gibt das Programm das gesamte Array sowie den kleinsten und den größten enthaltenen Wert aus. Listing 5.3 Ein einfaches Array-Beispiel #include <stdio.h>
int main()
{
int werte[10];
int ein;
int i, min, max;
printf ("Bitte 10 Werte zwischen 1 und 100!\n");
for (i = 0; i < 10; i++) {
printf ("%d. Wert: ", i + 1);
scanf ("%d", &ein);
werte[i] = ein;
}
/* max und min auf das Anfangselement setzen: */
min = werte[0];
max = werte[0];
printf ("Ihre Werte: ");
for (i = 0; i < 10; i++) {
printf ("%d ", werte[i]);
if (werte[i] > max)
max = werte[i];
if (werte[i] < min)
min = werte[i];
}
printf ("\n");
printf ("Kleinster Wert: %d\n", min);
printf ("Größter Wert: %d\n", max);
return 0;
}
C-Strings Eine der wichtigsten Aufgaben von Arrays besteht darin, den nicht vorhandenen String-Datentyp zu ersetzen. An Stelle eines Strings verwendet C ein Array von char-Werten, dessen Ende durch das Zeichen \0 (ASCII-Code 0) gekennzeichnet wird. Das Byte für diese Endmarkierung müssen Sie bei der Deklaration des char-Arrays mit einplanen: Ein char[10] ist maximal ein String mit neun nutzbaren Zeichen. StrukturenMitunter ist es nützlich, mehrere Werte verschiedener Datentypen unter einem »gemeinsamen Dach« zu verwalten. Zu diesem Zweck stellt C einen speziellen komplexen Datentyp namens struct bereit. In einer Struktur können sich beliebig viele Variablen verschiedener Datentypen befinden, was besonders nützlich ist, um komplexe Datenstrukturen zwischen Funktionen hin- und herzureichen. Das folgende Beispiel definiert eine Struktur namens person, die verschiedene Daten über Personen verwaltet:
Beachten Sie, dass eine struct-Definition mit einem Semikolon enden muss, anders als andere Blöcke in geschweiften Klammern. Eine Variable dieses Datentyps wird folgendermaßen deklariert: struct person klaus; Wenn Sie die einzelnen Elemente innerhalb einer Strukturvariablen ansprechen möchten, wird dafür die Form variable.element verwendet. Hier sehen Sie beispielsweise, wie die soeben definierte Variable klaus mit Inhalt versehen wird: klaus.vorname = "Klaus"; klaus.nachname = "Schmitz"; klaus.alter = 42; Oftmals werden Zeiger auf Strukturen als Funktionsargumente eingesetzt. Für die relativ unhandliche Schreibweise (*strukturvariable).element, die man verwenden müsste, um aus der Funktion heraus auf die Elemente einer Strukturvariablen zuzugreifen, wird die Kurzfassung strukturvariable->element definiert. Die folgende Funktion kann beispielsweise aufgerufen werden, um die angegebene Person ein Jahr älter zu machen:
Der Aufruf dieser Funktion erfolgt beispielsweise so: geburtstag (&klaus); 5.1.3 Die C-Standardbibliothek
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| printf (Format, Wert1, Wert2, ...) Diese Funktion dient der Ausgabe von Inhalten auf die Konsole. Das erste Argument ist ein String mit Format-Platzhaltern, die für die anschließend aufgelisteten Werte stehen. Die wichtigsten Format-Platzhalter sind %s für einen String, %d für einen Integer und %f für Fließkommawerte. |
| scanf (Format, Adresse) scanf() dient der Eingabe eines Werts über die Standardeingabe (meist Tastatur); der eingegebene Wert wird unter der angegebenen Speicheradresse abgelegt. Die Adresse wird in der Regel durch Dereferenzierung einer Variablen (vorangestelltes &) angegeben, um die Eingabe in der entsprechenden Variable zu speichern. Die Formatangabe besteht in der Regel nur aus einem einzelnen Format-Platzhalter (siehe printf()). |
| gets (Variable) Mit Hilfe von gets() wird ein String von der Standardeingabe gelesen und in der angegebenen Variablen gespeichert. |
| getchar () liest ein einzelnes Zeichen von der Standardeingabe. Beachten Sie, dass die Eingabe mit den Mitteln der Standardbibliothek dennoch immer zeilenorientiert verläuft: Sie können zwar in einer Schleife einzelne Zeichen einlesen, erhalten aber erst bei einem Zeilenende (wenn der Benutzer (Enter) drückt) ein Ergebnis. Echte zeichenorientierte Eingabe ist eine Angelegenheit plattformabhängiger Bibliotheken. |
| fopen (Dateiname, Modus) Diese Funktion öffnet eine Datei auf einem Datenträger. Damit Sie auf diese Datei zugreifen könnnen, wird das Funktionsergebnis von fopen() einer Variablen vom Typ FILE zugewiesen – der Wert ist ein eindeutiger Integer, der als Dateideskriptor oder Dateihandle bezeichnet wird. Der Dateiname kann ein beliebiger Pfad im lokalen Dateisystem sein; beachten Sie unter Windows lediglich, dass das Pfadtrennzeichen \ in einem C-String verdoppelt werden muss, weil es normalerweise Escape-Sequenzen wie \n einleitet. Der Modus kann unter anderem eines der Zeichen "r" (lesen), "w" (schreiben) oder "a" (anfügen) sein. Beispiel: |
fh = fopen ("test.txt", "r");
/* test.txt zum Lesen öffnen */
| fclose (Dateideskriptor) schließt die angegebene Datei. |
| fprintf (Deskriptor, Format, Werte) besitzt dieselbe Syntax wie printf(), schreibt aber in die angegebene Datei. |
| fscanf (Deskriptor, Format, Variable) funktioniert wie scanf(), liest aber aus der angegebenen Datei. |
| fgets (Variable, Zeichenzahl, Deskriptor) liest einen String aus der angegebenen Datei mit der entsprechenden maximalen Zeichenzahl oder bis zum ersten Zeilenumbruch. |
Die Header-Datei string.h enthält verschiedene Funktionen zur String-Manipulation und -Analyse. Zu den wichtigsten gehören folgende:
| strcmp (String1, String2) vergleicht die beiden angegebenen Strings miteinander. Das Ergebnis ist 0, wenn sie gleich sind, negativ, wenn String1 alphabetisch vor String2 kommt, und positiv, wenn es umgekehrt ist. |
| strcpy (String1, String2) kopiert den Wert von String2 an die Adresse von String1. |
| strcat (String1, String2) hängt den Wert von String2 an das Ende von String1 an. |
Diese Header-Datei definiert verschiedene Funktionen für die Arbeit mit Datum und Uhrzeit:
| time (NULL) fragt die aktuelle Systemzeit ab und liefert sie als Wert vom Typ time_t zurück. Als Argument in den Klammern wird eigentlich ein Zeiger auf time_t erwartet, da das Ergebnis aber bereits die Zeit enthält, wird in der Regel der spezielle Wert NULL (Zeiger auf gar nichts!) übergeben. |
| localtime (*Zeitangabe) wandelt die Rückgabe von time() – die Sekunden seit EPOCH – in eine vorformatierte Ortszeit um. Das Argument ist ein Zeiger auf time_t, der Rückgabewert eine komplexe Struktur namens struct tm. Oft wird localtime() nur als »Zwischenwert« für strftime() verwendet. |
| strftime (String, Zeichenzahl, Format, *localtime-Wert) formatiert die Zeitangabe nach der Vorschrift des angegebenen Formats und speichert das Ergebnis in der String-Variablen ab, die als erstes Argument vorliegt. Die Formatangaben entsprechen dem im vorigen Kapitel besprochenen UNIX-Befehl date. Das folgende Beispiel liest das aktuelle Datum und gibt es formatiert aus: |
time_t jetzt;
char zeit[20];
...
jetzt = time(NULL);
strftime (zeit, 19, "%d.%m.%Y, %H:%M",
localtime (&jetzt));
printf ("Heute ist der %s.\n", zeit);
| Die Ausgabe lautet beispielsweise folgendermaßen: | |
Heute ist der 14.05.2003, 13:34.
| difftime (Zeitangabe1, Zeitangabe2) gibt die Differenz zwischen zwei Zeitangaben in Sekunden an. |
Formal hat der Präprozessor zwar nichts mit der Standardbibliothek zu tun, wird aber trotzdem hier kurz angeschnitten, weil er unter anderem für das Einbinden der Header-Dateien mittels #include zuständig ist. Viele C-Programme bestehen aus mehr Präprozessor-Direktiven als aus gewöhnlichen Anweisungen, weil der Präprozessor die Definition bestimmter Abkürzungen ermöglicht.
Header einbinden
Die wichtigste Präprozessor-Direktive haben Sie bereits kennen gelernt: #include bindet eine Header-Datei ein, die Schnittstellendefinition einer Bibliothekskomponente. Sie können auch eigene häufig genutzte Funktionen in selbst geschriebene Header-Dateien auslagern, müssen dabei aber Folgendes beachten: #include <Datei> sucht im standardisierten Include-Verzeichnis Ihres Compilers oder Betriebssystems nach der angegebenen Header-Datei. Wenn Sie auf eine Datei im Verzeichnis des C-Programms selbst verweisen möchten, wird stattdessen die Schreibweise #include "Datei" verwendet.
Symbolische Konstanten
Eine weitere wichtige Funktion des Präprozessors ist die Definition symbolischer Konstanten mit Hilfe der Direktive #define. Diese werden vor allem verwendet, um konstante Werte tief im Inneren des Programms zu vermeiden, wo sie sich später nur schwer auffinden und ändern lassen. Angenommen, Sie möchten in Ihrem Programm den Umrechnungsfaktor von DM nach + verwenden, dann können Sie ihn folgendermaßen als symbolische Konstante festlegen:
#define DM 1.95583
In Ihrem Programm wird nun jedes Vorkommen von DM noch vor der eigentlichen Kompilierung durch 1.95583 ersetzt – außer innerhalb der Anführungszeichen von String-Literalen. Beachten Sie, dass am Ende einer Konstantendefinition kein Semikolon stehen darf.
Diese Fähigkeit des Präprozessors wird auch oft zur bedingten Kompilierung eingesetzt: Die Direktive #ifdef fragt ab, ob die angegebene symbolische Konstante existiert, und kompiliert nur in diesem Fall alle Zeilen bis zum Auftreten von #endif. Dies wird zum Beispiel zur Unterscheidung verschiedener Rechnerplattformen verwendet. Im folgenden Beispiel wird eine zusätzliche Anweisung mitkompiliert, wenn eine symbolische Konstante namens DEBUG definiert ist:
#ifdef DEBUG
printf ("Debug-Modus aktiviert.\n");
#endif
| << zurück |
| ||||||||||||
| ||||||||||||
| ||||||||||||
| ||||||||||||
| ||||||||||||
Copyright © Galileo Press GmbH 2004
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.