Ein Zeichen ist ein einzelner Buchstabe, eine Ziffer, ein Satzzeichen oder ein beliebiges anderes Symbol. Unter einem String versteht man eine beliebige Folge von Zeichen. Strings dienen dazu, Textdaten aufzunehmen, die aus Buchstaben, Ziffern, Satzzeichen und anderen Symbolen bestehen. Zweifelsohne können Zeichen und Strings in vielen Programmanwendungen extrem nützlich sein. Heute lernen Sie:
char
zur Aufnahme einfacher Zeichen verwendet
char
erzeugt, die Zeichenketten enthalten
C verwendet für die Aufnahme von Zeichen den Datentyp char
. Wie Sie am Tag 2,
»Die Komponenten eines C-Programms: Quellcode und Daten«, gesehen haben,
gehört char
zu den numerischen Integer-Datentypen von C. Wenn aber char
vom Typ
her nummerisch ist, wie kann er dann dazu verwendet werden, Zeichen
aufzunehmen?
Um diese Frage zu beantworten, muss man sich anschauen, wie in C Zeichen gespeichert werden. Im Speicher Ihres Computers sind alle Daten in numerischer Form abgelegt. Es gibt keinen direkten Weg, Zeichen zu speichern. Es gibt jedoch für jedes Zeichen einen numerischen Code. Dieser wird ASCII-Code oder ASCII- Zeichensatz genannt (ASCII steht für American Standard Code for Information Interchange, auf gut Deutsch: »amerikanischer Standardcode für den Informationsaustausch«). Dieser Code weist den Groß- und Kleinbuchstaben, den Ziffern, Satzzeichen und anderen Symbolen die Werte von 0 bis 255 zu. Sie finden den ASCII-Zeichensatz in Anhang A.
So stellt zum Beispiel die Zahl 97 den ASCII-Code für den Buchstaben a dar. Wenn
Sie also das Zeichen a in einer Variablen vom Typ char
speichern, speichern Sie
eigentlich den Wert 97. Da der zulässige Zahlenbereich für den Datentyp char
mit
dem Standard-ASCII-Zeichensatz übereinstimmt, ist char
optimal zum Speichern von
Zeichen geeignet.
Vielleicht sind Sie noch etwas verwirrt? Wenn C Zeichen als Zahlen speichert, woher
weiß dann Ihr Programm, ob eine gegebene Variable vom Typ char
ein Zeichen oder
eine Zahl ist? Wie Sie später noch lernen werden, reicht es nicht, eine Variable vom
Typ char
zu deklarieren; es wird noch etwas mehr verlangt:
char
-Variable an einer Stelle im Programm auftaucht, wo ein Zeichen
erwartet wird, wird diese Variable als Zeichen interpretiert.
char
-Variable an einer Stelle im Programm auftaucht, wo eine Zahl
erwartet wird, wird diese Variable als Zahl interpretiert.
Damit konnten Sie einen kleinen Eindruck gewinnen, wie in C ein numerischer Datentyp Zeichen speichert. Jetzt können wir zu den Details übergehen.
char
-Variablen müssen, wie andere Variablen auch, vor ihrer Verwendung deklariert
werden. Sie können bei der Deklaration initialisiert werden. Hier einige Beispiele:
char a, b, c; /* Deklariert 3 nicht-initialsierte char-Variablen */
char code = 'x'; /* Deklariert eine char-Variable namens code */
/* und speichert in ihr den Wert x */
code = '!'; /* Speichert ! in der Variablen namens code */
Um eine literale Zeichenkonstante zu erzeugen, setzen Sie das betreffende Zeichen in einfache Anführungszeichen. Der Compiler übersetzt literale Zeichenkonstanten automatisch in den entsprechenden ASCII-Code, so dass der Variablen der numerische Code zugewiesen wird.
Symbolische Zeichenkonstanten können Sie entweder mit Hilfe der #define
-Direktive
oder mit dem Schlüsselwort const
einrichten:
#define EX 'x'
char code = EX; /* Setzt code gleich 'x' */
const char A = 'Z';
Nachdem Sie jetzt wissen, wie man Zeichenvariablen deklariert und initialisiert, werde
ich Ihnen ein Beispiel zeigen. Listing 9.1 veranschaulicht mit Hilfe der Funktion
printf()
, die Sie bereits am Tag 6, »Grundlagen der Ein- und Ausgabe«, kennen
gelernt haben, wie »nummerisch« die Speicherung von Zeichen ist. Mit der Funktion
printf()
können sowohl Zeichen als auch Zahlen ausgegeben werden. Der
Formatstring %c
weist printf()
an, ein Zeichen auszugeben, während mit %d
ein
Dezimalinteger ausgegeben wird. Listing 9.1 initialisiert zwei Variablen vom Typ char
und gibt beide aus, einmal als Zeichen und einmal als Zahl.
Listing 9.1: Die numerische Natur der Variablen vom Typ char.
1: /* Beispiel für die numerische Natur von char-Variablen */
2:
3: #include <stdio.h>
4:
5: /* Deklariert und initialisiert zwei char-Variablen */
6:
7: char c1 = 'a';
8: char c2 = 90;
9:
10: int main(void)
11: {
12: /* Gibt Variable c1 erst als Zeichen, dann als Zahl aus */
13:
14: printf("\nAls Zeichen lautet Variable c1: %c", c1);
15: printf("\nAls Zahl lautet Variable c1: %d", c1);
16:
17: /* Das gleiche für Variable c2 */
18:
19: printf("\nAls Zeichen lautet Variable c2: %c", c2);
20: printf("\nAls Zahl lautet Variable c2: %d\n", c2);
21:
22: return 0;
23: }
Als Zeichen lautet Variable c1: a
Als Zahl lautet Variable c1: 97
Als Zeichen lautet Variable c2: Z
Als Zahl lautet Variable c2: 90
Wenn Sie mit Zeichen arbeiten, können Sie in der Regel davon ausgehen, dass die Werte von 0 bis 127 immer die gleichen Zeichen codieren. Bei der Auswahl der Zeichen, die durch die Zahlen von 0 bis 127 codiert werden, wurden allerdings vor allem die Zeichen der englischen Sprache berücksichtig. Zur Unterstützung der Zeichen anderer Sprachen (beispielsweise auch der deutschen Umlaute) gibt es etliche Erweiterungen (Zeichenwerte größer als 128). Stellen Sie aber bitte keine Vermutungen darüber an, welche Zeichen durch die erweiterten Zeichenwerte codiert werden - diese variieren von Computer zu Computer, je nach installierter Zeichentabelle.
Variablen vom Typ char
können nur ein einziges Zeichen aufnehmen. Deshalb sind
sie auch nur begrenzt einsetzbar. Darüber hinaus benötigen Sie eine Möglichkeit, um
Strings - das heißt, eine Folge von Zeichen - zu speichern. Name und Adresse einer
Person sind zum Beispiel Strings. Da es keinen besonderen Datentyp für Strings gibt,
behandelt C diese Art von Information als Arrays von Zeichen.
Um zum Beispiel einen String von sechs Zeichen unterzubringen, müssen Sie ein
Array vom Typ char
mit sieben Elementen deklarieren. Arrays vom Typ char
werden
wie alle anderen Arrays auch deklariert. So deklariert zum Beispiel die Anweisung
char string[10];
ein Array vom Typ char
mit zehn Elementen. Dieses Array kann einen String von
neun oder weniger Zeichen aufnehmen.
»Warten Sie«, werden Sie sagen, »es ist ein Array mit zehn Elementen. Warum kann es
nur neun Zeichen aufnehmen?« Ein C-String ist definiert als eine Sequenz von
Zeichen, die mit einem Nullzeichen endet. Ein Nullzeichen ist ein besonderes Zeichen,
das durch \0
dargestellt wird. Obwohl es im Code durch zwei Zeichen (Backslash und
Null) dargestellt wird, ist das Nullzeichen ein einzelnes Zeichen, das den ASCII-Wert 0
hat. Es ist eines der Escape-Sequenzen von C, die bereits am Tag 6 besprochen
wurden.
Wenn ein C-Programm zum Beispiel den String Alabama
speichert, speichert es die
sieben Zeichen A
, l
, a
, b
, a
, m
und a
, gefolgt von dem Nullzeichen \0
- was insgesamt
acht Zeichen macht. Demzufolge kann ein Zeichenarray nur Zeichenstrings
aufnehmen, die um eins kleiner sind als die Gesamtzahl der Elemente im Array.
Wie andere Datentypen in C können Zeichenarrays bei ihrer Deklaration initialisiert werden. Den Zeichenarrays können wie folgt Element für Element Werte zugewiesen werden:
char string[10] = { 'A', 'l', 'a', 'b', 'a', 'm', 'a', '\0' };
Es ist jedoch wesentlich bequemer, einen literalen String zu verwenden. Darunter verstehen wir eine Folge von Zeichen in doppelten Anführungszeichen:
char string[10] = "Alabama";
Wenn Sie einen literalen String in Ihrem Programm verwenden, hängt der Compiler automatisch das abschließende Nullzeichen an das Ende des Strings. Wenn Sie bei der Deklaration des Arrays die Anzahl der Indizes nicht angeben, errechnet der Compiler für Sie sogar noch die erforderliche Größe des Arrays. Die folgende Zeile erzeugt und initialisiert ein Array mit acht Elementen:
char string[] = "Alabama";
Denken Sie immer daran, dass Strings ein abschließendes Nullzeichen erfordern. Alle C-Funktionen zur Stringbearbeitung (die am Tag 16, »Stringmanipulation«, besprochen werden) ermitteln die Länge eines übergebenen Strings dadurch, dass sie nach dem Nullzeichen suchen. Diese Funktionen haben keine andere Möglichkeit, das Ende des Strings zu erkennen. Wenn das Nullzeichen fehlt, geht Ihr Programm davon aus, dass der String sich bis zum nächsten Nullzeichen im Speicher erstreckt. Wenn Sie das Nullzeichen vergessen, kann dies ziemlich eklige Programmfehler verursachen.
Sie haben gelernt, dass Strings in Arrays vom Typ char
gespeichert werden und das
Ende eines Strings (der nicht das gesamte Array belegen muss) durch ein Nullzeichen
markiert ist. Da das Ende eines Strings bereits markiert ist, benötigt man zur
vollständigen Definition eines Strings eigentlich nur noch etwas, das auf den Anfang
des Strings zeigt. (Ist zeigt hier das richtige Wort? Ich glaube ja!)
Vielleicht ahnen Sie schon dank des obigen Hinweises, worauf dieser Abschnitt abzielt. Wie Sie von Tag 8, »Zeiger«, wissen, ist der Name eines Arrays ein Zeiger auf das erste Element im Array. Deshalb benötigen Sie für den Zugriff auf einen String, der in einem Array gespeichert ist, nur den Array-Namen. Diese Methode ist in C der übliche Weg, um auf einen String zuzugreifen.
So unterstützen beispielsweise auch die C-Bibliotheksfunktionen den Zugriff über den
Array-Namen. Die C-Standardbibliothek enthält eine Reihe von Funktionen zur
Manipulation von Strings. (Diese Funktionen werden am Tag 16 ausführlich
behandelt.) Um einer dieser Funktionen einen String zu übergeben, übergeben Sie
den Array-Namen. Dies gilt auch für die beiden Funktionen zur Ausgabe von Strings,
printf()
und puts()
. Hierauf werden wir später in diesem Kapitel noch eingehen.
Vielleicht ist Ihnen aufgefallen, dass ich oben von »Strings, die in einem Array gespeichert werden« gesprochen habe. Soll damit angedeutet werden, dass es Strings gibt, die nicht in Arrays gespeichert werden? Die Antwort lautet ja, und der nächste Abschnitt soll dies erläutern.
Aus den vorangehenden Abschnitten wissen Sie, dass ein String durch den Namen
eines Zeichenarrays und ein Nullzeichen definiert ist. Der Array-Name ist ein Zeiger
vom Typ char
auf den Anfang des Strings. Die Null markiert das Stringende. Der
eigentliche Platz, der von dem String in einem Array belegt wird, ist nebensächlich.
Ehrlich gesagt, dient das Array lediglich dazu, einen allokierten Speicherbereich für
den String bereitzustellen.
Was wäre, wenn Sie, ohne ein Array zu allokieren, genügend Speicherbereich finden
würden? Sie könnten dann in diesem Speicherbereich einen String mit seinem
abschließenden Nullzeichen ablegen. Ein Zeiger auf das erste Zeichen würde - wie bei
einem String in einem Array - dazu dienen, den Anfang des Strings zu kennzeichnen.
Wie aber kann man feststellen, wo genügend Speicherplatz vorhanden ist? Es gibt
zwei Möglichkeiten: Entweder man allokiert Speicher für einen literalen String (die
Speicherreservierung erfolgt dann bei der Kompilation des Programms) oder man
bedient sich der Funktion malloc()
, um während der Programmausführung Speicher
zu reservieren. Den letzteren Weg bezeichnet man auch als dynamische
Speicherreservierung.
Der Beginn eines Strings wird, wie bereits erwähnt, durch einen Zeiger auf eine
Variable vom Typ char
angezeigt. Vielleicht erinnern Sie sich noch, wie ein solcher
Zeiger deklariert wird:
char *botschaft;
Diese Anweisung deklariert einen Zeiger namens botschaft
, der auf Variablen vom
Typ char
zeigen kann. So deklariert, weist der Zeiger noch auf keinen definierten
Speicherbereich. Was aber, wenn Sie die Zeigerdeklaration wie folgt ändern:
char *botschaft = "Der Geist des großen Cäsar!";
Wenn diese Anweisung kompiliert wird, wird der String »Der Geist des großen Cäsar!«
(inklusive abschließenden Nullzeichens) irgendwo im Speicher abgelegt, und der Zeiger
botschaft
zeigt nach der Initialisierung auf das erste Zeichen des Strings. Kümmern
Sie sich nicht darum, wo genau im Speicher der String liegt - dies ist Aufgabe des
Compilers. Nach seiner Definition ist botschaft
ein Zeiger auf den String und kann als
solcher verwendet werden.
Die obige Deklaration/Initialisierung entspricht der folgenden Deklaration. Auch die
beiden Notationen *botschaft
und botschaft[]
sind äquivalent. Beide bedeuten »ein
Zeiger auf«.
char botschaft[] = "Der Geist des großen Cäsar!";
Diese Methode, Speicher für die Aufnahme von Strings zu reservieren, ist praktisch,
wenn Sie beim Schreiben des Programms wissen, was Sie benötigen. Was aber, wenn
das Programm aufgrund von Benutzereingaben oder anderen Faktoren, die beim
Erstellen des Programms noch unbekannt sind, unterschiedlichen Speicherplatz für
seine Strings benötigt? In solchen Fällen verwendet man die Funktion malloc()
, mit
der man Speicherplatz zur Laufzeit reservieren kann.
Die malloc()
-Funktion ist eine der C-Funktionen zur Speicherallokation. Wenn Sie
malloc()
aufrufen, übergeben Sie der Funktion die Anzahl der benötigten
Speicherbytes. malloc()
sucht und reserviert einen Speicherblock der erforderlichen
Größe und gibt die Adresse des ersten Byte im Block zurück. Wie der Speicher
gefunden wird, braucht Sie nicht zu kümmern - das ist Aufgabe der Funktion malloc()
und des Betriebssystems.
Die malloc()
-Funktion liefert eine Adresse zurück, und zwar als Zeiger auf void
.
Warum void
? Ein Zeiger vom Typ void
zu mit allen Datentypen kompatibel. Da der
von malloc()
reservierte Speicher zur Speicherung von Daten beliebiger Datentypen
verwendet wird, ist der Rückgabetyp void
korrekt.
#include <stdlib.h>
void *malloc(size_t groesse);
malloc()
reserviert einen Speicherblock, der die in groesse
angegebene Anzahl von
Bytes umfasst. Wenn Sie Speicher mit Hilfe von malloc()
bei Bedarf reservieren,
anstatt gleich bei Programmbeginn großzügig Speicher für zukünftige Aufgaben zu
reservieren, können Sie den Arbeitsspeicher des Rechners effizienter nutzen. Für die
Verwendung von malloc()
müssen Sie die Header-Datei stdlib.h
einbinden.
malloc()
liefert einen Zeiger auf den reservierten Speicherblock zurück. Wenn
malloc()
den erforderlichen Speicherplatz nicht reservieren kann, liefert die Funktion
NULL
zurück. Aus diesem Grund sollten Sie immer, wenn Sie Speicher zuweisen, den
Rückgabewert testen, auch wenn der Speicher, der zugewiesen werden soll, klein ist.
Wir werden NULL
und seine Anwendung in Kürze diskutieren.
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
/* Speicher für einen String mit 100 Zeichen reservieren */
char *str;
if (( str = (char *) malloc(100)) == NULL)
{
printf( "Nicht genug Speicher, um den Puffer zu allokieren\n");
exit(1);
}
printf( "Speicher für den String wurde reserviert!\n" );
return 0;
}
/* Speicher für ein Array mit 50 Integer reservieren */
int *zahlen;
zahlen = (int *) malloc(50 * sizeof(int));
/* Speicher für ein Array mit 10 float-Werten reservieren */
float *zahlen;
zahlen = (float *) malloc(10 * sizeof(float));
Sie können mit malloc()
einen Speicherplatz für ein einziges Zeichen vom Typ char
reservieren. Deklarieren Sie dazu zuerst einen Zeiger auf den Typ char
:
char *zgr;
Danach rufen Sie die Funktion malloc()
auf und übergeben ihr die Größe des
gewünschten Speicherblocks. Da ein Zeichen in der Regel nur ein Byte belegt,
benötigen Sie lediglich einen Block von einem Byte. Der von malloc()
zurückgelieferte Wert wird dann dem Zeiger zugewiesen:
zgr = malloc(1);
Diese Anweisung reserviert einen Speicherblock von einem Byte und weist dessen
Adresse zgr
zu. Im Gegensatz zu den Variablen, die in dem Programm deklariert sind,
besitzt dieses Speicher-Byte keinen Namen - es bildet ein namenloses Speicherobjekt,
auf das man nur über den Zeiger zugreifen kann. Um zum Beispiel in dem
Speicherobjekt das Zeichen 'x
' zu speichern, würde man schreiben:
*zgr = 'x';
In der gleichen Weise, in der man mit malloc()
Speicher für eine Variable vom Typ
char
reserviert, kann man mit malloc()
auch Speicher für einen String allokieren. Der
Hauptunterschied liegt darin, dass Sie berechnen müssen, wie viel Speicher reserviert
werden muss - das heißt, Sie müssen die maximale Anzahl der Zeichen im String
kennen. Dies Maximum hängt von den Bedürfnissen Ihres Programms ab.
Angenommen Sie wollten für dieses Beispiel einen Speicherbereich für einen String
von 99 Zeichen plus dem abschließenden Nullzeichen (also insgesamt 100 Zeichen)
zuweisen. Dann deklarieren Sie zuerst einen Zeiger auf den Typ char
und rufen
danach malloc()
wie folgt auf:
char *zgr;
zgr = malloc(100);
Jetzt zeigt zgr
auf einen reservierten Block von 100 Byte, der zur Speicherung und
Manipulation eines Strings verwendet werden kann. Für die Arbeit mit zgr
ist es
unerheblich, ob der zugehörige Speicher mit malloc()
oder im Zuge einer Array-
Deklaration reserviert und zugewiesen wurde.
char zgr[100];
Mit Hilfe von malloc()
können Sie Speicher nach Bedarf reservieren. Dabei versteht
es sich von selbst, dass der verfügbare Speicherbereich nicht unbegrenzt ist. Wie viel
Speicher zur Verfügung steht, hängt davon ab, wie viel Speicher Sie in Ihrem Rechner
installiert haben und wie viel Platz vom Betriebssystem und den laufenden
Programmen belegt wird. Wenn nicht genug Speicher verfügbar ist, liefert malloc()
eine nicht initialisierte Adresse (das heißt NULL
) zurück. Ihr Programm sollte den
Rückgabewert von malloc()
daher stets überprüfen, um sicherzustellen, dass der
erforderliche Speicherbereich auch erfolgreich zugewiesen wurde. Vergleichen Sie
den Rückgabewert von malloc()
mit der symbolischen Konstanten NULL, die in
stdlib.h
definiert ist. Listing 9.2 veranschaulicht den Einsatz von malloc()
. Jedes
Programm, das von malloc()
Gebrauch macht, muss die Header-Datei stdlib.h
mit
#include
einbinden.
Viele C-Programmierer weisen allen Zeigern, die nicht anderweitig initialisiert wurden, einen speziellen Wert zu. Dies ist der Wert
NULL
, der innerhalb des Systems tatsächlich eine Sonderstellung einnimmt. Indem Sie einem ZeigerNULL
zuweisen, sorgen Sie dafür, dass der Zeiger nirgendwohin zeigt. In den meisten Betriebssystemen, einschließlich Linux, lautet dieser Wert Null und ist für die Benutzung durch das Betriebssystem reserviert, das heißt, er wäre normalerweise von keinem besonderen Nutzen.Das Schöne an dieser Vorgehensweise ist, dass Sie feststellen können, ob ein Zeiger initialisiert wurde oder nicht, indem Sie prüfen, ob er gleich
NULL
ist. Der WertNULL
ist instdio.h
sowie instdlib.h
deklariert, so dass Sie eine dieser Header-Dateien einbinden müssen, wenn SieNULL
verwenden wollen.
Listing 9.2: Mit der malloc()-Funktion Speicher für String-Daten reservieren.
1: /* Beispiel für die Verwendung von malloc() zur */
2: /* Speicherreservierung für String-Daten. */
3:
4: #include <stdio.h>
5: #include <stdlib.h>
6:
7: char count, *zgr, *z;
8:
9: int main(void)
10: {
11: /* Reserviert einen Block von 35 Bytes. Testet auf Erfolg. */
12:
13:
14: zgr = malloc(35 * sizeof(char));
15:
16: if (zgr == NULL)
17: {
18: puts("Fehler bei der Speicherzuweisung.");
19: return 1;
20: }
21:
22: /* Füllt den String mit Werten von 65 bis 90, */
23: /* was den ASCII-Codes von A-Z entspricht. */
24:
25: /* z ist ein Zeiger, mit dem der String durchlaufen wird. */
26: /* zgr soll weiterhin unverändert auf den Anfang */
27: /* des Strings zeigen. */
28:
29: z = zgr;
30:
31: for (count = 65; count < 91 ; count++)
32: *z++ = count;
33:
34: /* Fügt das abschließende Nullzeichen ein. */
35:
36: *z = '\0';
37:
38: /* Zeigt den String auf dem Bildschirm an. */
39:
40: puts(zgr);
41:
42: return 0;
43: }
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Dieses Programm zeigt ein einfaches Beispiel für den Einsatz von malloc()
. Das
Programm selbst scheint zwar lang, besteht aber zu einem großen Teil aus
Kommentaren, die in den Zeilen 1, 2, 11, 22 bis 27, 34 und 38 detailliert darüber
informieren, was das Programm macht. Zeile 5 bindet die für malloc()
notwendige
Header-Datei stdlib.h
und Zeile 4 die für die puts()
-Funktionen notwendige Header-
Datei stdio.h
ein. Zeile 7 deklariert zwei Zeiger und eine Zeichenvariable, die später
im Listing eingesetzt werden. Keine dieser Variablen wird initialisiert. Deshalb sollten
Sie (noch!) nicht verwendet werden.
Die malloc()
-Funktion wird in Zeile 14 aufgerufen. Übergeben wird ihr der Wert 35
multipliziert mit der Größe eines char
. Hätten Sie auch nur 35 übergeben können? Ja,
doch dann würden Sie davon ausgehen, dass jeder Benutzer Ihres Programms einen
Computer hat, der Variablen vom char
-Typ in einem Byte abspeichert. Am Tag 2
wurde bereits erwähnt, dass verschiedene Compiler unterschiedliche Variablengrößen
verwenden können. Mit dem sizeof
-Operator können Sie Code schreiben, der
portabel ist.
Gehen Sie nie davon aus, dass malloc(
) den von Ihnen gewünschten Speicher auch
reservieren kann, denn im Grunde ist Ihr Allokations-Befehl lediglich eine Anfrage.
Zeile 16 zeigt Ihnen, wie Sie am einfachsten überprüfen können, ob malloc()
Speicher bereitstellen konnte. Wenn der Speicher zugewiesen wurde, zeigt zgr
darauf,
andernfalls ist zgr
gleich NULL
. Wenn es dem Programm nicht gelingt, Speicher zu
finden, geben die Zeilen 18 und 19 eine Fehlermeldung aus und sorgen für einen
würdigen Programmabbruch.
Zeile 29 initialisiert den anderen Zeiger, z
, der in Zeile 7 deklariert wurde. Diesem
wird der gleiche Adresswert zugewiesen wie zgr
. Eine for
-Schleife verwendet diesen
neuen Zeiger, um in dem reservierten Speicher Werte abzulegen. Wenn Sie sich Zeile
31 anschauen, sehen Sie, dass count
mit 65
initialisiert und dann in jedem
Schleifendurchgang um 1 inkrementiert wird, bis der Wert 91
erreicht ist. Bei jedem
Durchlauf der for
-Schleife wird der Wert von count
an der Adresse abgelegt, auf die z
gerade zeigt. Beachten Sie, dass jede Inkrementierung von count
mit einer
Inkrementierung der Adresse, auf die z
zeigt, einhergeht. Das bedeutet, dass alle
Werte hintereinander im Speicher abgelegt werden.
Es sollte Ihnen aufgefallen sein, dass hier der Variablen count
, die vom Typ char
ist,
Zahlen zugewiesen werden. Erinnern Sie sich noch an die Diskussion über die ASCII-
Zeichen und ihre numerischen Entsprechungen? Die Zahl 65 entspricht A
, 66
entspricht B
, 67 entspricht C
und so weiter. Die for
-Schleife wird beendet, nachdem
das gesamte Alphabet in dem reservierten Speicherbereich abgelegt wurde. Zeile 36
schließt das Schreiben der Zeichenwerte ab, indem eine Null in die letzte Adresse, auf
die z
zeigt, geschrieben wird. Durch das Anhängen der Null können Sie die Werte nun
als String verwenden. Doch denken Sie daran, dass der Zeiger zgr
immer noch auf
den ersten Wert, A
, verweist. Wenn Sie den Zeiger als String verwenden, werden alle
Zeichen bis zur Null ausgegeben. Zeile 40 verwendet puts()
, um diesen Punkt zu
prüfen und das Ergebnis unserer Bemühungen auszugeben.
Wenn Ihr Programm mit Strings arbeitet, wird es diese wahrscheinlich irgendwann auf
dem Bildschirm ausgeben müssen. Dies geschieht in der Regel mit den Funktionen
puts()
oder printf()
.
Die Bibliotheksfunktion puts()
ist Ihnen bereits in einigen Programmen dieses Buches
begegnet. Mit dieser Funktion können Sie einen String auf dem Bildschirm ausgeben.
puts()
übernimmt als einziges Argument einen Zeiger auf den auszugebenden String.
Da ein literaler String als ein Zeiger auf einen String zu betrachten ist, kann man mit
puts()
sowohl literale Strings als auch String-Variablen ausgeben. Die puts()
-
Funktion fügt automatisch am Ende jedes ausgegebenen Strings ein Neue-Zeile-
Zeichen an, so dass jeder weitere mit puts()
ausgegebene String in einer eigenen
Zeile steht.
Listing 9.3 gibt ein Beispiel für die Verwendung von puts()
.
Listing 9.3: Mit der Funktion puts() Text auf dem Bildschirm ausgeben.
1: /* Beispiel für die Ausgabe von Strings mit puts(). */
2:
3: #include <stdio.h>
4:
5: char *meldung1 = "C";
6: char *meldung2 = "ist ";
7: char *meldung3 = "die";
8: char *meldung4 = "beste";
9: char *meldung5 = "Programmiersprache!!";
10:
11: int main(void)
12: {
13: puts(meldung1);
14: puts(meldung2);
15: puts(meldung3);
16: puts(meldung4);
17: puts(meldung5);
18:
19: return 0;
20: }
C
ist
die
beste
Programmiersprache!!
Dies Listing lässt sich ziemlich einfach nachvollziehen. Für die
Standardausgabefunktion puts()
muss die Header-Datei stdio.h
eingebunden
werden, was in Zeile 3 geschieht. Die Zeilen 5 bis 9 deklarieren und initialisieren fünf
verschiedene Variablen. Jede dieser Variablen ist ein Zeichenzeiger, eine
Stringvariable. Die Zeilen 13 bis 17 verwenden die puts()
-Funktion, um jeden String
auszugeben.
Sie können Strings aber auch mit der Bibliotheksfunktion printf()
ausgeben. Am Tag
6 wurde bereits beschrieben, wie printf()
seine Ausgabe mit Hilfe eines
Formatstrings und verschiedener Konvertierungsspezifizierer in Form bringt. Für die
Ausgabe von Strings verwendet man den Konvertierungsspezifizierer %s
.
Wenn printf()
in seinem Formatstring auf ein %s
trifft, ersetzt sie %s
durch das
zugehörige Argument aus der Argumentliste. Für Strings muss dieses Argument ein
Zeiger auf den auszugebenden String sein. Die printf()
-Funktion gibt den String
Zeichen für Zeichen auf dem Bildschirm aus und hört damit erst auf, wenn sie das
abschließende Nullzeichen erreicht.
char *str = "Eine anzuzeigende Nachricht";
printf("%s", str);
Sie können auch mehrere Strings ausgeben oder die Strings zusammen mit literalem Text und/oder numerischen Variablen ausgeben:
char *bank = "Sparkasse";
char *name = "Hans Schmidt";
int konto = 1000;
printf("Das Konto von %s bei der %s steht auf %d DM.",name,bank,konto);
Das Konto von Hans Schmidt bei der Sparkasse steht auf 1000 DM.
Damit haben Sie jetzt erst einmal ausreichend Informationen an der Hand, um Strings
auszugeben. Eine vollständige Beschreibung der Funktion printf()
finden Sie am Tag
13, »Mit Bildschirm und Tastatur arbeiten«.
Oft müssen Programme Strings nicht nur ausgeben, sondern auch vom Benutzer über
die Tastatur eingegebene Strings aufnehmen. In der C-Bibliothek stehen dafür drei
Funktionen zur Verfügung - gets()
, fgets()
und scanf()
. Hiermit habe ich alle
erwähnt, doch möchte ich Ihnen von der Verwendung von gets()
abraten, da diese
Funktion einen schwerwiegenden Fehler enthält. Glücklicherweise können Sie ohne
Bedenken fgets()
anstelle von gets()
verwenden.
Bevor Sie einen String von der Tastatur einlesen, müssen Sie einen Ort schaffen, wo
Sie ihn ablegen können. Um Speicherplatz für einen String bereitzustellen, können
Sie eine der beiden bereits beschriebenen Methoden verwenden - die Array-
Deklaration oder die Verwendung der malloc()
-Funktion.
Die Aufgabe von gets()
und fgets()
besteht darin, Zeichen von der Tastatur
einzulesen und als String an das aufrufende Programm weiterzureichen. Es werden
dabei alle Zeichen bis zum ersten Neue-Zeile-Zeichen (das durch die Eingabetaste
erzeugt wird) eingelesen. Beide Funktionen schließen den String mit einem
Nullzeichen ab, bevor Sie ihn an den Aufrufer zurückliefern. Der Unterschied
zwischen beiden Funktionen ist, dass fgets()
im Gegensatz zu gets()
das Neue-Zeile-
Zeichen mit aufnimmt.
Die Funktionsprototypen dieser zwei Funktionen sind in der Header-Datei stdio.h
wie
folgt definiert:
char *gets(char *s);
char *fgets(char *s, int size, FILE *stream);
Lassen Sie den letzten Parameter von fgets()
einmal außer Acht und richten Sie Ihr
Augenmerk auf den vorangehenden Parameter, in dem die Größe des Arrays s
angegeben werden kann. Dieser Parameter fehlt der Funktion gets()
, wodurch sie so
gefährlich ist. Wenn Sie ein char
-Array mit zehn Zeichen deklarieren und es an gets()
übergeben, wird diese Funktion so viele Zeichen einlesen, wie der Anwender eingibt,
bevor er die Eingabetaste betätigt. Das können auch mehr als zehn Zeichen sein.
Folglich wird über das Ende Ihres Arrays hinausgeschrieben. Da die Funktion gets()
eine potentielle Gefahrenquelle darstellt, werden wir sie in diesem Buch nicht
benutzen. Wenn Sie sie aber trotzdem in einem Ihrer Programme verwenden, werden
Sie wahrscheinlich feststellen, dass Sie auch vom GNU-C-Compiler vor dem
Gebrauch der Funktion gewarnt werden.
In Listing 9.4 finden Sie ein Beispiel für fgets()
. Der über die Tastatur eingegebene
String wird an der Position gespeichert, auf die der an fgets()
übergebene Zeiger auf
char
verweist. Die maximal zulässige Stringlänge (einschließlich des Nullzeichens), die
fgets()
einlesen darf, wird als zweiter Parameter übergeben. Die externe Variable
stdin
bildet den dritten Parameter. Diese Variable ist zusammen mit fgets(
) in der
Header-Datei stdio.h
definiert. Sie ist bereits initialisiert, und Sie sollten daran keine
Änderung vornehmen, sondern die Variable einfach nur verwenden.
Listing 9.4: Stringdaten mit fgets() von der Tastatur einlesen.
1: /* Beispiel für die Bibliotheksfunktion fgets(). */
2:
3: #include <stdio.h>
4:
5: /* Ein Zeichen-Array für die Aufnahme der Eingabe allokieren. */
6:
7: char eingabe[40];
8:
9: int main(void)
10: {
11: puts("Bitte Text eingeben und dann die Eingabetaste drücken: ");
12: fgets(eingabe, 40, stdin);
13: printf("Ihre Eingabe lautete: %s\n", eingabe);
14:
15: return 0;
16: }
Bitte Text eingeben und dann die Eingabetaste drücken:
Dies ist ein Test
Ihre Eingabe lautete: Dies ist ein Test
In diesem Beispiel lautet das erste Argument an fgets()
eingabe
und ist der Name
eines Arrays vom Typ char
und damit ein Zeiger auf das erste Array-Element. Das
Array wird in Zeile 7 mit 40 Elementen deklariert, weshalb als zweites Argument der
Wert 40
übergeben wird. Mit dem dritten Argument, stdin
, werden wir uns am Tag
13 näher befassen.
fgets()
hat auch einen Rückgabewert, der aber in dem obigen Beispiel ignoriert
wurde. Dieser Rückgabewert ist ein Zeiger vom Typ char
. Im Erfolgsfall weist dieser
Zeiger auf die Adresse, an der der Eingabestring gespeichert ist; falls ein Fehler
auftrat, wird ein NULL
-Zeiger zurückgeliefert. Wenn von der Tastatur eingelesen wird,
sind Fehler eher selten. In anderen Anwendungsbereichen hat der Rückgabewert von
fgets() aber durchaus seine Berechtigung. Diese kommen allerdings erst in
Kapitel 15, »Mit Dateien arbeiten«, zur Sprache.
Am Tag 6 haben Sie gelernt, dass die Bibliotheksfunktion scanf()
numerische Daten
von der Tastatur einliest. Die Funktion kann aber auch Strings einlesen. Denken Sie
daran, dass scanf()
einen Formatstring verwendet, der ihr mitteilt, wie die Eingabe zu
lesen ist. Um einen String zu lesen, müssen Sie im Formatstring von scanf()
den
Spezifizierer %s
verwenden. Wie schon bei gets()
wird auch scanf()
ein Zeiger auf
den Speicherbereich für den Strings übergeben.
Wie stellt scanf()
fest, wo der String anfängt und wo er aufhört? Der Anfang wird
durch das erste Nicht-Whitespace-Zeichen gebildet. Das Ende kann auf zweifachem
Weg angegeben werden. Wenn Sie %s
im Formatstring verwenden, reicht der String
bis zum nächsten Whitespace-Zeichen (Leerzeichen, Tabulator oder Neue-Zeile-
Zeichen). Wenn Sie %ns
verwenden (wobei n
eine Integer-Konstante ist, die eine
Feldbreite angibt), liest scanf()
die nächsten n
Zeichen oder bis zum nächsten
Whitespace-Zeichen ein - je nachdem, was zuerst erreicht wird.
Sie können mit scanf()
auch mehrere Strings einlesen. Dazu müssen Sie mehr als ein
%s
in den Formatstring einbauen. Für jedes %s
im Formatstring folgt scanf()
den
nachstehenden Regeln, um die angeforderte Anzahl von Strings in der Eingabe zu
finden. Betrachten wir das Beispiel
scanf("%s%s%s", s1, s2, s3);
Wenn Ihre Eingabe zu diesem Aufruf Januar Februar März
lautet, wird Januar
dem
String s1
, Februar
s2
und März
s3
zugeordnet.
Und wie sieht das mit dem Spezifizierer für die Feldbreite aus? Wenn Sie die Anweisung
scanf("%3s%3s%3s", s1, s2, s3);
ausführen und als Antwort September
eingeben, wird Sep
s1
zugewiesen, tem
s2
und
ber
s3
.
Beachten Sie, dass bei Verwendung der Funktion
scanf()
, ebenso wie bei der Funktiongets()
, die Gefahr besteht, über das Ende des Arrays hinauszuschreiben.
Was passiert, wenn Sie weniger oder mehr Strings eingeben, als die Funktion scanf()
erwartet? Wenn Sie weniger Strings eingeben, wartet scanf()
auf die fehlenden
Strings und hält das Programm an, bis diese Strings eingegeben wurden. Wenn Sie
zum Beispiel als Antwort auf die Anweisung
scanf("%s%s%s", s1, s2, s3);
Januar Februar
eingegeben hätten, hätte das Programm auf den dritten String, der im
scanf()
-Formatstring spezifiziert wurde, gewartet. Haben Sie mehr Strings als
erforderlich eingegeben, bleiben die nicht übernommenen Strings im Tastaturpuffer
stehen. Sie werden dann von der nachfolgenden scanf()
-Funktion oder von anderen
Eingabeanweisungen eingelesen. Wenn Sie zum Beispiel als Antwort auf die
Anweisung
scanf("%s%s", s1, s2);
scanf("%s", s3);
Januar Februar März
eingegeben hätten, würde im ersten Aufruf von scanf()
Januar
dem String s1
und Februar
dem String s2
zugewiesen. Mär
z wird automatisch
übertragen und im zweiten Aufruf von scanf()
s3
zugewiesen.
Die scanf()
-Funktion hat als Rückgabewert einen Integer, der die Anzahl der
erfolgreich eingelesenen Elemente angibt. Der Rückgabewert wird oft ignoriert. Wenn
Sie reinen Text einlesen, werden Sie scanf()
normalerweise die Funktion fgets()
vorziehen. scanf()
sollte man am besten nur dann verwenden, wenn eine
Kombination aus Text und numerischen Daten einzulesen ist. Listing 9.5 soll dies
illustrieren. Denken Sie daran, dass Sie den Adressoperator (&)
verwenden müssen,
wenn Sie mit scanf()
numerische Variablen einlesen wollen (siehe Tag 6).
Listing 9.5: Mit scanf() numerische Daten und Text einlesen.
1: /* Beispiel für scanf(). */
2:
3: #include <stdio.h>
4:
5: char nname[81], vname[81];
6: int count, id_num;
7:
8: int main(void)
9: {
10: /* Aufforderung an den Anwender. */
11:
12: puts("Geben Sie, durch Leerzeichen getrennt, Nachnamen, Vornamen");
13: puts("und Kennnummer ein. Dann die Eingabetaste drücken.");
14:
15: /* Einlesen der drei Elemente. */
16:
17: count = scanf("%s%s%d", nname, vname, &id_num);
18:
19: /* Daten ausgeben. */
20:
21: printf("%d Elemente wurden eingegeben: %s %s %d \n",
count, vname, nname, id_num);
22:
23: return 0;
24: }
Geben Sie, durch Leerzeichen getrennt, Nachnamen, Vornamen
und Kennnummer ein. Dann die Eingabetaste drücken.
Meier Johann 12345
3 Elemente wurden eingegeben: Johann Meier 12345
Wie Sie bereits wissen, muss man scanf()
als Argumente Adressen übergeben. In
Listing 9.5 sind nname
und vname
Zeiger (das heißt Adressen), so dass sie den
Adressoperator nicht benötigen. Im Gegensatz dazu ist id_num
ein regulärer
Variablenname, so dass hier das &
bei der Übergabe an scanf()
erforderlich ist (Zeile
17).
Manche Programmierer sind der Meinung, dass das Einlesen von Daten mit scanf()
fehleranfällig sei. Sie ziehen es vor, alle Daten - sowohl numerische Daten als auch
Text - mit fgets()
einzulesen und im Programm die Zahlenwerte herauszufiltern und
in numerische Variablen zu konvertieren. Solche Techniken gehen weit über den
Rahmen dieses Buches hinaus, sind jedoch eine gute Programmierübung. Sie
benötigen dafür allerdings die Funktionen zur Stringmanipulation, die am Tag 17,
»Die Bibliothek der C-Funktionen«, behandelt werden.
In der heutigen Lektion haben wir uns ausführlich mit dem Datentyp char
beschäftigt.
Ein Einsatzgebiet für char
-Variablen ist das Speichern einzelner Zeichen. Sie haben
gelernt, dass Zeichen eigentlich als Zahlen gespeichert werden. Der ASCII-Code weist
jedem Zeichen einen numerischen Code zu. Deshalb können Sie char
-Variablen auch
dazu nutzen, um kleine Integerwerte abzuspeichern. Variablen vom Typ char
gibt es
sowohl mit als auch ohne Vorzeichen.
Ein String ist eine Folge von Zeichen, die mit einem Nullzeichen abgeschlossen wird.
Strings können für Textdaten verwendet werden. C speichert Strings in Arrays vom
Typ char
. Um einen String der Länge n
zu speichern, benötigen Sie ein Array vom
Typ char
mit n+1
Elementen.
Sie können Ihre Programme mit Speicherzuweisungsfunktionen wie malloc()
dynamischer machen. Die Funktion malloc()
erlaubt es Ihnen, für Ihre Programme
den benötigten Speicher zu reservieren. Ohne diese Funktionen müssten Sie raten,
wie viel Speicherplatz Ihr Programm benötigt. Sicherheitshalber würden Sie diese
Zahl sehr hoch ansetzen, so dass mehr Speicher als nötig reserviert wird.
Frage:
Was ist der Unterschied zwischen einem String und einem Zeichenarray?
Antwort:
Ein String wird als eine Folge von Zeichen definiert, die mit einem
Nullzeichen endet. Ein Array ist eine Folge von Zeichen. Deshalb ist ein
String ein Array von Zeichen, der mit einer Null abgeschlossen wird.
Wenn Sie ein Array vom Typ char
definieren, ergibt sich der eigentliche
Speicherbereich, der für das Array reserviert wurde, aus der angegebenen
Größe und nicht der Größe minus 1. Sie sind auf diese Größe festgelegt und
können keine größeren Strings in dem Array ablegen. Sehen Sie ein Beispiel:
char land[10]="Philippinen"; /* Falsch! String länger als Array. */
char land2[10]="Polen"; /* OK, aber verschwendet Speicher, da */
/* String kürzer ist als das Array. */
Wenn Sie jedoch eine Zeigervariable auf den Typ char
definieren, gelten diese
Beschränkungen nicht mehr. Die Variable ist lediglich ein Speicherplatz für
den Zeiger. Die eigentlichen Strings sind anderswo im Speicher gespeichert
(wo genau, braucht Sie nicht zu kümmern). Es gibt keine
Längenbeschränkung und auch keinen verschwendeten Speicherplatz. Der
eigentliche String ist ja woanders gespeichert. Ein Zeiger kann auf einen
String beliebiger Länge zeigen.
Frage:
Warum deklariere ich zum Ablegen der Werte nicht einfach große Arrays, anstatt
eine Funktion zur Speicherallokation wie malloc()
zu verwenden?
Antwort:
Auch wenn es vielleicht leichter scheint, große Arrays zu deklarieren, ist zu
bedenken, dass dies nicht gerade eine effektive Nutzung des Speicherplatzes
darstellt. Kleine Programme wie die der heutigen Lektion lassen die
Verwendung einer Funktion wie malloc()
anstelle von Arrays unter
Umständen unnötig komplex erscheinen. Aber mit zunehmendem
Programmumfang werden Sie Speicher sicher nur nach Bedarf reservieren
wollen. Darüber hinaus können Sie dynamisch reservierten Speicher, den Sie
nicht mehr benötigen, wieder zurückgeben, indem Sie ihn freigeben. Der
freigegebene Speicher kann dann von einer anderen Variablen oder einem
Array in einem anderen Teil des Programms belegt werden. (Tag 18, »Vom
Umgang mit dem Speicher«, behandelt die Freigabe von dynamisch
reserviertem Speicher.)
Frage:
Was passiert, wenn Sie in einem Zeichenarray einen String ablegen, der länger als
das Array ist?
Antwort:
Dies kann zu Fehlern führen, die nur sehr schwer aufzuspüren sind. Sie
können das zwar in C machen, aber alles, was im Speicher direkt nach dem
Zeichenarray steht, wird dann überschrieben. Wenn Sie Glück haben, wird
der betreffende Speicherbereich nicht genutzt. Genauso gut können dort aber
auch Daten liegen, die Ihr Programm benötigt. Was genau passiert, hängt
davon ab, was Sie überschreiben. Oft passiert erst einmal eine ganze Weile
gar nichts. Trotzdem sollten Sie nichts riskieren.
Frage:
Warum sollten Sie fgets()
anstelle von gets()
verwenden?
Antwort:
Die Funktion gets()
bietet keine Möglichkeit, die Länge des Zeichenarrays, in
das eingelesen wird, anzugeben. Bei fgets()
können Sie dagegen die Länge
angeben und so verhindern, dass über das Ende des Arrays hinausgeschrieben
wird.
Der Workshop enthält Quizfragen, die Ihnen helfen sollen, Ihr Wissen zu festigen, sowie Übungen, die Sie anregen sollen, das Gelernte umzusetzen und eigene Erfahrungen zu sammeln. Die Lösungen zu den Fragen und den Übungen finden Sie in Anhang C.
n
Zeichen zu speichern, benötigen Sie ein Zeichenarray von
n+1
Elementen. Wozu benötigen Sie das zusätzliche Element?
char *str1 = { "String 1" };
char str2[] = { "String 2" };
char string3;
char str4[20] = { "Dies ist String 4" };
char str5[20];
char *string = "Ein String!";
string[0]
*string
string[11]
string[33]
*(string+10)
string
char
-Variable namens buchstabe
und
initialisieren Sie sie mit dem Zeichen $
.
char
und initialisieren Sie es mit
»Zeiger machen Spass
!«. Das Array soll gerade groß genug sein, um den String
aufzunehmen.
Zeiger machen Spass!
«, aber
verwenden Sie diesmal kein Array.
malloc()
-Funktion, um ausreichend Speicher für die
Verkettung der beiden Strings zu reservieren. Liefern Sie einen Zeiger auf diesen
neuen String zurück.
Hallo
» und »Welt!
« übergeben, soll die Funktion einen
Zeiger auf »Hallo Welt!
« zurückliefern. Den verketteten Wert als dritten String zu
verwenden, ist dabei die geringste Schwierigkeit. (Möglicherweise können Sie Ihre
Antworten zu den Übungen 5 und 6 verwenden.)
char ein_string[10] = "Dies ist ein String";
char *zitat[100] = { "Lächeln, bald ist Freitag!" };
char *string1;
char *string2 = "Zweiter";
string1 = string2;
char string1[];
char string2[] = "Zweiter";
string1 = string2;
In der heutigen Lektion haben Sie gelernt, wie man mit
malloc()
dynamisch Speicher reserviert. Nachdem Sie auf diese Weise Speicher reserviert haben, sollten Sie ihn, wenn Sie ihn nicht mehr benötigen, dem Computersystem wieder zur Verfügung stellen. Dies geschieht, indem Sie ihn freigeben. Am Tag 18, »Vom Umgang mit dem Speicher«, wird die Freigabe von reserviertem Speicher behandelt.