9.3    Strings (Zeichenketten)

Vielleicht haben Sie sich schon gefragt, was passiert, wenn Sie ein Array vom Typ char (oder auch wchar_t) verwenden. Sie werden es schon vermuten: Mit einer Folge von char-Zeichen können Sie einen kompletten Text, einen sogenannten String, speichern, verarbeiten und ausgeben. Auf Ihre Frage, wie Sie Text in C verarbeiten können, bekommen Sie in diesem Kapitel also die Antworten. In C gibt es, wie Sie wahrscheinlich schon vermutet haben, keinen eigenen Datentyp für Strings und daher auch keine Operatoren, die Strings als Operanden verwenden können. Aber ganz so dramatisch ist dies alles nicht, denn wenn Sie dieses und das nächste Kapitel über Zeiger durchgearbeitet haben, haben Sie vor allem zusammen mit der String-Bibliothek string.h Werkzeuge zur Hand, die viele andere Programmiersprachen »alt aussehen« lassen.

Für Arrays vom Typ char gelten aber leider erst einmal ein paar Einschränkungen – nicht nur die Einschränkungen herkömmlicher Arrays, sondern es muss auch darauf geachtet werden, dass die zusammenhängende Folge von Zeichen mit dem Null-Zeichen '\0' (auch Stringende-Zeichen genannt) abgeschlossen wird. Genau genommen heißt dies, dass die Länge eines char-Arrays immer um eins größer sein muss als die Anzahl der relevanten Zeichen. Um mit Strings zu arbeiten, bietet die Standardbibliothek außerdem viele Funktionen in der Header-Datei string.h an.

9.3.1    Strings initialisieren

Um char-Arrays zu initialisieren, können Sie String-Literale in Anführungszeichen verwenden, anstatt ein Array-Zeichen für Zeichen zu initialisieren. Somit wären die folgenden beiden char-Array-Definitionen gleichwertig:

00  // Kapitel9/init_strings.c
01 #include <stdio.h>
02 #include <stdlib.h>

03 int main(void) {
04 char string1[20] = "String";
05 char string2[20] = {'S', 't', 'r', 'i', 'n', 'g', '\0'};
06 printf("%s\n", string1);
07 printf("%s\n", string2);
08 return EXIT_SUCCESS;
09 }

Listing 9.8    Das Listing zeigt, wie Sie einen String initialisieren.

Beide Initialisierungen in den Zeilen (04) und (05) sind äquivalent. Es wird jeweils ein char-Array definiert, das darstellbare 19 Zeichen (!) enthalten kann. Die beiden Strings selbst enthalten davon nur sechs Zeichen. Die restlichen Zeichen werden auch hier, wie schon bei den bisher kennengelernten Arrays, mit 0 vorbelegt. Es wäre in dem letzten Beispiel allerdings falsch, die Strings mit einer Längenangabe von 6 wie string1[6] zu definieren, weil dann kein Platz mehr für das Null-Zeichen übrig wäre. Im letzten Beispiel können Sie auch sehen, wie Sie mit printf() und der Formatangabe %s den kompletten String ausgeben können.

Vergessen Sie niemals das Stringende-Zeichen!

Ein char-Array, das einen String speichert, muss immer um mindestens ein Element länger sein, als die Anzahl der relevanten (lesbaren) Zeichen. Nur dann kann es noch das Stringende-Zeichen (oder auch Null-Zeichen) '\0' aufnehmen. Haben Sie also beispielsweise einen Text mit exakt 10 Zeichen, müssen Sie dafür ein char-Array mit 11 Zeichen verwenden. Dieses Stringende-Zeichen ist von enormer Bedeutung bei String-Verarbeitungsfunktionen.

Etwas muss hier jedoch richtiggestellt werden: Es ist nicht falsch, wenn Sie bei einem char-Array kein abschließendes '\0' verwenden. Das gilt allerdings nur dann, wenn Sie die einzelnen Elemente im char-Array verwenden wollen. Sobald Sie das char-Array als String – also als Ganzes – verwenden wollen, und sei es nur zur Ausgabe auf dem Bildschirm mit printf(), müssen Sie das Array mit '\0' abschließen.

Sie müssen auch immer zwischen einer Zeichenkonstante und einer String-Konstante unterscheiden. Folgende Definitionen sind nicht äquivalent:

// Zeichenkonstante mit einem Zeichen
char ch = 'X';
// String-Konstante mit zwei Zeichen: 'X' und '\0'
char ch[] = "X";

Natürlich können Sie auch bei den Strings bzw. char-Arrays bei der Definition mit der Initialisierungsliste auf die Längenangabe verzichten. Folgende äquivalente Möglichkeiten stehen Ihnen dabei zur Verfügung:

char str[] = { 'S', 'T', 'R', 'I', 'N', 'G', '\n', '\0' };
char str[] = "STRING\n";

9.3.2    Einlesen von Strings

Zwar wird das Thema Ein-/Ausgabe noch gesondert in Kapitel 14, »Eingabe- und Ausgabefunktionen«, behandelt, aber trotzdem soll hier kurz auf die Eingabe von Strings eingegangen werden. Es ist nämlich möglich, char-Arrays formatiert mit scanf() einzulesen. Dies ist möglich, weil scanf() nur die Anfangsadresse einer Variablen benötigt. Die scanf()-Funktion liest allerdings einen String nur bis zum ersten Whitespace-Zeichen ein. Alle restlichen Zeichen dahinter werden somit (erst einmal) ignoriert. Außerdem ist scanf() nicht unbedingt die sicherste Alternative und anfällig für einen Pufferüberlauf (Buffer-Overflow), wenn keine oder eine falsche Längenbegrenzung verwendet wird. Eine Längenbegrenzung für scanf() können Sie wie folgt angeben:

01  char name[20];
02 printf("Bitte Ihren Namen: ");
03 if( scanf("%19s", name) != 1 ) {
04 printf("Fehler bei der Eingabe\n");
05 return EXIT_FAILURE;
06 }
07 printf("Ihr Name ist %s\n", name);

In Zeile (03) legen Sie die Längenbegrenzung für die einzulesenden Zeichen auf 19 Zeichen (%19s) fest, damit es nicht zu einem Pufferüberlauf kommen kann. Allerdings muss hierbei noch angemerkt werden: Wenn mehr als 19 Zeichen eingegeben wurden, liegen die darüber hinausgehenden Zeichen im Eingabepuffer des Programms. Dies sollten Sie wissen, sofern Sie vorhaben, gleich einen weiteres scanf() aufzurufen. Der Codeausschnitt bei der Ausführung gibt z. B. Folgendes aus:

Bitte Ihren Namen: Jürgen Wolf
Ihr Name ist Jürgen

Wie bereits eingangs erwähnt, liest die Funktion scanf() nur bis zum ersten Whitespace-Zeichen ein, weshalb in diesem Beispiel bei der Ausführung der Nachname nicht mehr mit eingelesen wird. Führende Whitespace-Zeichen haben hingegen keine Bedeutung.

In den meisten Fällen ist die Standardfunktion fgets()die bessere Alternative zum Einlesen von Strings. Die Syntax von fgets() lautet:

#include <stdio.h> // Benötigter Header
char *fgets(char *str, int n_chars, FILE *stream);

Den String geben Sie mit dem ersten Parameter str an. Im zweiten Parameter geben Sie an, wie viele Zeichen eingelesen werden. Das Lesen wird abgebrochen, wenn das Zeilenende '\n' oder nchar_n-1 Zeichen eingelesen wurden. Von wo Sie etwas einlesen wollen, geben Sie mit dem dritten Parameter stream an. In unserem Fall sollte es die Standardeingabe sein, die Sie mit dem Stream stdin angeben können. Die Funktion fgets() kann neben Strings also auch zum zeilenweisen Lesen aus Dateien verwendet werden. Die Funktion gibt bei Erfolg die Anfangsadresse auf den eingelesenen String str, im Fehlerfall NULL zurück. Mehr dazu erfahren Sie in Kapitel 14, »Eingabe- und Ausgabefunktionen«.

Hier sehen Sie an einem einfachen Anwendungsbeispiel, wie Sie mit der Funktion fgets() Strings einlesen können:

00  // Kapitel9/fgets_beispiel.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 #define MAX 20

04 int main(void) {
05 char string1[MAX];
06 printf("Eingabe machen: ");
07 if (fgets(string1, sizeof(string1), stdin) == NULL ) {
08 printf("Fehler beim Einlesen\n");
09 return EXIT_FAILURE;
10 }
11 printf("Ihre Eingabe: %s", string1);
12 return EXIT_SUCCESS;
13 }

In Zeile (07) werden mit fgets() von der Standardeingabe (stdin) maximal sizeof(string1) Zeichen in das char-Array string1 eingelesen. Die Funktion fgets() garantiert außerdem, dass immer das Stringende-Zeichen an das Ende angefügt wird. Maximal werden immer sizeof(string1) Zeichen an string1 übergeben. Wenn noch Platz vorhanden ist, wird außerdem das Newline-Zeichen '\n' angehängt. Geben Sie im obigen Beispiel einen String mit 20 Zeichen ein, wird kein Newline-Zeichen mehr hinzugefügt, weil das letzte Zeichen dem Null-Zeichen vorbehalten ist. Anstelle der Ausgabe in Zeile (11) mit printf() könnten Sie auch fputs(), das Gegenstück von fgets(), verwenden.

Das Programm gibt bei der Ausführung Folgendes aus:

Eingabe machen: Hallo Welt
Ihre Eingabe: Hallo Welt

9.3.3    Unicode-Unterstützung

Mit C11 wurde die Unterstützung für Unicode-Zeichen und String-Literale hinzugefügt. Mithilfe verschiedener Präfixe können UTF-8, UTF-16 und UTF-32 verwendet werden. Mit dem Präfix u8 erstellen Sie einen UTF-8-encodierten String. Ähnliches gilt für die Präfixe u und U, mit denen Sie UTF-16- bzw. UTF-32-Strings verwenden können.

Es folgt ein Beispiel mit Unicode-Zeichen:

#include <uchar.h>

/* UTF-8 */
char u8str[] = u8"ΩΩ UTF-8-String ΩΩ";
char u8chr = u8'Ω';

/* UTF-16 */
#ifdef __STD_UTF_16__
char16_t u16str[] = u"ΩΩ UTF-16-String ΩΩ.";
char16_t u16char = u'Ω';
#endif

/* UTF-32 */
#ifdef __STD_UTF_32__
char32_t u32str[] = U"ΩΩ UTF-32-String ΩΩ";
char32_t u32char = U'Ω';
#endif

char16_t und char32_t sind ebenfalls seit C11 dabei und eignen sich, um die UTF-16- und UTF-32-kodierten Zeichensequenzen zu speichern. Wie schon bei der Einführung zu char16_t und char32_t in Abschnitt 3.5.3, »Unicode-Unterstützung«, erwähnt, ist die Verwendung von Unicode-Zeichen keineswegs ein triviales Thema, und C liefert Ihnen hier im Grunde nur ein Fundament. Sie müssen sich als Programmierer in der Regel selbst darum kümmern, dass die richtige Codierung verfügbar ist. Zum Wechseln zwischen den Codierungen bietet Ihnen die Standardbibliothek wiederum Funktionen an. Die Typen und weitere Umwandlungsfunktionen sind in der Header-Datei uchar.h definiert.

Allerdings ist hier sehr wichtig, dass Ihr System auch die entsprechenden Codepages besitzt, was z. B. bei Windows erst ab der Version XP mit installiertem Service Pack 3 der Fall ist. Andernfalls können Sie nur UTF8 und einige 16-Bit-Unicode-Codepages verwenden. Außerdem muss Ihr Compiler auf jeden Fall C11 beherrschen. Unter Linux müssen Sie z. B. bei Debian aller Versionen unter 7.0 die 32-Bit-Codepages per Hand nachinstallieren. Dies ist nicht trivial und kann auch an dieser Stelle nicht erklärt werden. Hier helfen Ihnen die entsprechenden Manpages weiter, oder aber ein vollständiges System-Upgrade mit

sudo apt-get update
sudo apt-get dist-upgrade

Der letzte Schritt kann einige Stunden in Anspruch nehmen.

9.3.4    String-Funktionen der Standardbibliothek string.h

Funktionen, mit denen Sie Strings kopieren, zusammenfügen oder vergleichen können, sind in der Standard-Header-Datei string.h definiert. Das folgende Beispiel soll die drei häufig verwendeten Funktionen strncat() zum Aneinanderhängen, strncpy() zum Kopieren und strncmp() zum Vergleichen von char-Arrays bzw. Strings demonstrieren. In einem C‐Grundkurs können wir allerdings nicht sehr detailliert auf die einzelnen Funktionen eingehen. Wir empfehlen Ihnen deshalb, zusätzlich eine der Online-Referenzen, die Manpages oder das Dokument zum C11-Standard zurate zu ziehen.

00  // Kapitel9/string_h_demo.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 #include <string.h>
04 #define MAX 50

05 void nl2space( char str[] ) {
06 int n = strlen(str);
07 for(int i = 0; i < n; i++) {
08 if( str[i] == '\n' ) {
09 str[i] = ' ';
10 }
11 }
12 }

13 void nl2null( char str[] ) {
14 int n = strlen(str)-1;
15 if(str[n] == '\n') {
16 str[n] = '\0';
17 }
18 }

19 int main(void) {
20 char name[MAX*2];
21 char vname[MAX], nname[MAX];
22 printf("Vorname: ");
23 if( fgets(vname, MAX, stdin) == NULL ) {
24 printf("Fehler bei der Eingabe\n");
25 return EXIT_FAILURE;
26 }
27 nl2space( vname );
28 printf("Nachname: ");
29 if( fgets(nname, MAX, stdin) == NULL ) {
30 printf("Fehler bei der Eingabe\n");
31 return EXIT_FAILURE;
32 }
33 nl2null( nname );

34 // Strings vergleichen
35 if( strncmp( vname, nname, MAX ) == 0) {
36 printf("Vorname und Nachname sind identisch\n");
37 return EXIT_FAILURE;
38 }
39 // vname nach name kopieren
40 if( strncpy(name, vname, MAX) == NULL ) {
41 printf("Fehler bei strncpy\n");
42 return EXIT_FAILURE;
43 }
44 // noch vorhandenen Platz in name ermitteln
45 size_t len = MAX*2 - strlen(name)+1;
46 // nname an name anhängen
47 if( strncat(name, nname, len) == NULL ) {
48 printf("Fehler bei strncat\n");
49 return EXIT_FAILURE;
50 }
51 // gesamten String ausgeben
52 printf("Ihr Name: %s\n", name);
53 return EXIT_SUCCESS;
54 }

Listing 9.9    string_h_demo.c demonstriert die String-Funktionen strncat(), strncpy() und strncmp() aus string.h und implementiert auch zwei eigene String-Funktionen.

Hier wurde ein etwas umfangreicheres Beispiel erstellt. Zunächst werden Sie nach dem Vor- und Nachnamen gefragt. Beide werden jeweils mit fgets() (in den Zeilen (23) und (29)) in ein char-Array eingelesen. Von beiden Strings wird in den Zeilen (27) und (33) die Anfangsadresse an die Funktion nl2space() bzw. nl2null() übergeben, die ein eventuell vorhandenes Newline-Zeichen von fgets() durch ein Leerzeichen (bei nl2space()) bzw. ein Stringende-Zeichen (nl2null()) ersetzen. In den Zeilen (06) und (14) wird die Funktion strlen() verwendet, die ebenfalls in der Header-Datei string.h definiert ist. Sie gibt die Anzahl der Zeichen eines Strings ohne das Stringende-Zeichen zurück.

In Zeile (35) wird überprüft, ob die beiden eingegebenen Strings identisch sind. Ist dies der Fall, gibt die Funktion strncmp() (ebenfalls als Teil der Standardbibliothek in string.h enthalten) den Wert 0 zurück, und Sie beenden das Programm mit EXIT_FAILURE. In Zeile (40) wird der String vname mit der Funktion strncpy() in den String name kopiert. Mit dem dritten Parameter geben Sie an, wie viele Zeichen Sie maximal in name kopieren können.

In Zeile (45) wird mit der Funktion strlen() nachgezählt, wie viele Zeichen sich bereits im String name befinden, um dann in Zeile (47) mittels der Funktion strncat() maximal len Zeichen vom String nname an name anzuhängen.

Das Programm gibt bei der Ausführung z. B. Folgendes aus:

Vorname: Juergen
Nachname: Wolf
Ihr Name: Juergen Wolf
Buffer-Overflow (Pufferüberlauf)

Die beiden String-Funktionen strncpy() und strncat() haben jeweils eine Schwesterfunktion mit strcpy() und strcat() ohne das n (das für numerable steht) im Namen. Zwar haben diese Versionen nur zwei Parameter und lassen sich einfacher verwenden, aber sie verursachen auch das Problem, dass nicht auf die Größe des Zielstrings geachtet wird. So kann bei falscher Verwendung ein Pufferüberlauf (Buffer-Overflow) ausgelöst und von Hackern auch fremder Code in Ihr Programm eingeschleust werden.

9.3.5    Sicherere Funktionen zum Schutz vor Speicherüberschreitungen

Gerade bezüglich der String-Funktionen (und auch anderen Bibliotheksfunktionen) gibt es seit dem C11-Standard sicherere, im Annex K des C11‐Standards beschriebene optionale Erweiterungen, um Speicherüberschreitungen (engl. boundary crossings) zu reduzieren. Dabei handelt es sich um bekannte Funktionen der C-Standardbibliothek, denen die Endung _s hinzugefügt wurde. Die Bounds-Checking-Versionen von strcat() und strncpy() lauten dann beispielsweise strcat_s() und strncpy_s(). Ob Annex K überhaupt unterstützt wird, können Sie über das Makro __STDC_LIB_EXT1__ testen. Ist __STDC_LIB_EXT1__ definiert, wurde die Bibliothek standardkonform nach Annex K implementiert. Dies war z. B. schon sehr früh bei Visual Studio der Fall.

An dieser Stelle sollte auch noch hinzugefügt werden, dass viele Funktionen wie strncpy() oder strncat() nicht direkt als »unsicher« gelten, wenn man ihre Funktion und Besonderheiten kennt und beachtet.

9.3.6    Umwandlungsfunktionen zwischen Zahlen und Strings

Sollten Sie auf der Suche nach Funktionen sein, mit denen Sie einen String in einen numerischen Wert konvertieren können, werden Sie in der Header-Datei stdlib.h fündig. Hier finden Sie z. B. die Möglichkeit, einen String mit der Funktion strtod() in einen double-Wert oder mit strtol()in einen long-Wert zu konvertieren. Den umgekehrten Fall können Sie sehr leicht mit der Funktion sprintf() erledigen, denn diese Funktion funktioniert wie printf(), nur dass sie die Ausgaben in einen String schreibt. So können Sie z. B. den double-Wert für PI wie folgt in einen String schreiben:

sprintf(string,"%lf\n",PI);

Mehr dazu finden Sie in der Online-Dokumentation zu stdlib.h und stdio.h.