9.3. Weitere String-Funktionen der Standardbibliothek

Da Zeichenketten in C als char-Arrays gehandhabt werden, ist es nicht möglich, ganze Strings mittels einer Zuweisung zu kopieren. Genausowenig lassen sich Zeichenketten mit Hilfe von Äquivalenzoperatoren vergleichen; deren Einsatzmöglichkeit beschränkt sich auf einzelne Zeichen. Damit dem C-Programmierer für solche täglichen Aufgaben nicht unnötige Mühen entstehen, beinhaltet die Standardbibliothek eine Reihe nützlicher Funktionen zur Zeichenketten-Bearbeitung.

Umwandlungs-Funktionen

Die folgenden Funktionen dienen dazu, Zeichenketten, die aus Ziffern bestehen, in ganze oder Fließkommazahlen umzuwandeln. Ihre Prototypen befinden sich in stdlib.h, die daher mit einem #include berücksichtigt werden muß.

Warum sollten Sie überhaupt eine Zeichenkette in einen numerischen Wert umwandeln wollen? Wenn Sie z.B. eine Datei Zeile für Zeile mit fgets() einlesen, dann liegen die Daten in Form von Zeichenketten vor. Wenn Sie die darin enthaltenen numerischen Informationen aber für Berechnungen heranziehen wollen, dann müssen Sie diese zuvor umwandeln.

Funktion Wandelt die Zeichenkette, auf die pszString zeigt,...
double atof(const char *pszString) in eine Zahl vom Typ double um.
int atoi(const char *pszString) in eine Zahl vom Typ int um.
long atol(const char *pszString) in eine Zahl vom Typ long um.
double strtod(const char *pszString, char **pszEnd) in eine Zahl vom Typ double um.
double strtol(const char *pszString, char **pszEnd, int iBasis) in eine Zahl vom Typ long um.
double strtoul(const char *pszString, char **pszEnd, int iBasis) in eine Zahl vom Typ unsigned long um.

Augenscheinlich überschneiden sich die Funktionen ato... und strto... bezüglich ihrer Fähigkeiten. Allerdings ist leicht ersichtlich, daß die strto...-Funktionen mehrere Argumente verlangen und deshalb auch mehr leisten als ihre ato...-Pendants. Folgendes Beispiel veranschaulicht die Verwendung von atof(), strtod() und strtol():


/*
 * str2num.c
 */
#include <stdio.h>
#include <stdlib.h>

int main(void){

double dResult;
long lResult;
char *pszMsg="53.6% Wahlbeteiligung wird erwartet";
char *pszEnd;

 puts("atof:");
 dResult = atof(pszMsg);
 printf("dResult: %f\n", dResult);

 puts("strtod:");
 pszMsg="68.2% Wahlbeteiligung wird erwartet";
 dResult = strtod(pszMsg, &pszEnd);
 printf("dResult: %f, Restzeichenkette: %s\n",
	dResult, pszEnd);

 puts("strtol:");
 pszMsg="0x3f% Wahlbeteiligung wird erwartet";
 lResult = strtol(pszMsg, &pszEnd, 16);
 printf("lResult: %ld, Restzeichenkette: %s\n",
	lResult, pszEnd);
return 0;
}

Die Ausgabe von str2num läßt die zusätzliche Information erkennen, welche die strto...-Funktionen gegenüber ihren ato...-Ensprechungen bieten. Während atof() beim Auftreten des ersten Zeichens, das keine Ziffer und kein Dezimalpunkt ist, seine Umwandlungstätigkeit beendet und die bis dahin konvertierte Zahl zurückgibt, liefern die strto...-Funktionen zusätzlich einen Zeiger auf das Zeichen, das nicht mehr umgewandelt werden konnte. Darüber hinaus zeigt sich, daß strtol() aufgrund des dritten Arguments (Basis 16) ohne weiteres in der Lage ist, 0x3f als Hexadezimalzahl zu interpretieren:


atof:
dResult: 53.600000
strtod:
dResult: 68.200000, Restzeichenkette: % Wahlbeteiligung wird erwartet
strtol:
lResult: 63, Restzeichenkette: % Wahlbeteiligung wird erwartet

Soll der umgekehrte Weg beschritten und numerische Werte in Zeichenketten verwandelt werden, dann steht Ihnen dafür eine ebenso mächtige wie inzwischen wohlvertraute Funktion zur Verfügung. Sie nutzen dafür allerdings eine Variante von printf(), nämlich sprintf(). Während printf() die formatierte Ausgabe auf stdout tätigt, sendet sie sprintf() (steht für "string printf()") in ein char-Array. Ansonsten ist der Gebrauch beider Funktionen völlig gleich. Folgendes Beispiel zeigt die Verwendung von sprintf():


/* sprintf.c */

#include <stdio.h>

int main(void){

double dProzent =53.6;
char *pszMsg="Wahlbeteiligung wird";
char *pszEnd="erwartet\n";
char szBuffer[100];

/* Das doppelte %-Zeichen nach %.2f ist
 * notwendig, um ’%’ auszugeben (siehe 5.2)
 */
sprintf(szBuffer, "%.2f%% %s %s",
	dProzent, pszMsg, pszEnd);
 puts(szBuffer);

return 0;
}

Die Funktionsweise von sprintf() läßt sich einigermaßen leicht nachvollziehen: Alle an sie übergebenen Argumente werden unter Verwendung der jeweiligen Format-Zeichenkette bearbeitet und das Endergebnis in das Array szBuffer geschrieben. Dessen Inhalt kann dann z.B. puts() ausgeben. Es liegt auf der Hand, daß bei der Definition des Arrays szBuffer auf ausreichende Dimensionierung geachtet werden muß, weil sonst die Ausgabe von sprintf() keinen Platz darin finden kann. Anstelle eines Arrays akzeptiert sprintf() natürlich auch einen Pointer auf char. So sollte die Pointer-Variante allerdings nicht aussehen:


.
.
double dProzent =53.6;
char *pszMsg="Wahlbeteiligung wird";
char *pszEnd="erwartet\n";
char *pszBuffer;

/*>>>>>>>>>>>>>> falsch <<<<<<<<<<<<<<
 * pszBuffer zeigt auf keinen gültigen
 * Speicherbereich! Zuerst malloc()
 * verwenden!
 */
 sprintf(pszBuffer, "%.2f%% %s %s",
	dProzent, pszMsg, pszEnd);
.
.

Zeichenketten kopieren und zusammenfügen

Für die Manipulation von Zeichenketten stehen eine Reihe von Funktionen mit str... zur Verfügung. Sie sind in der Header-Datei string.h definiert. Für einige Aufgaben existieren zusätzlich mehr oder weniger äquivalente Funktionen der mem...-Sorte. Während die str...-Funktionen Speicherbereiche generell als Arrays vom Typ char behandeln, können mem...-Routinen beliebige Speicherbereiche verändern.

Folgende Tabelle stellt die wichtigsten Funktionen zusammen:

Funktion Beschreibung
char *strcpy( char *pszS1, const char *pszS2) kopiert die Zeichenkette pszS2 in das Array pszS1.
char *strncpy( char *pszS1, const char *pszS2, size_t n) kopiert n Zeichen von pszS2 in das Array pszS1. Der Datentyp size_t ist in stdlib.h definiert und i.a. identisch mit unsigned int.
void *memcpy( void *s1, void *s2, size_t n) kopiert n Zeichen vom Objekt, auf das s2 zeigt, an die Stelle, wohin s1 zeigt.
void *memmove( void *s1, void *s2, size_t n) wie memcpy(), funktioniert aber auch, wenn sich die Objekte überlappen
void *memset( void *s, int iChar, size_t n) konvertiert iChar nach unsigned char und setzt die ersten n Zeichen von s auf iChar.
char *strcat( char *pszS1, const char *pszS2) hängt pszS2 an pszS1 an. Das erste Zeichen von pszS2 überschreibt die abschließende '\0' von pszS1.
char *strncat( char *pszS1, const char *pszS2, size_t n) hängt die ersten n Zeichen von pszS2 an pszS1 an. Das erste Zeichen von pszS2 überschreibt die abschließende '\0' von pszS1.

Beim Gebrauch dieser Funktionen gilt entsprechend, was beim Umgang mit Arrays ganz allgemein zu beachten ist: Das Kopierziel muß ein gültiger Speicherbereich sein (also ein Array oder ein mit malloc() angeforderter Speicherblock) und ausreichend Platz bieten, um die Daten der Kopierquelle aufnehmen zu können. Probleme ergeben sich allgemein, sobald sich die Speicherbereiche von Kopierquelle und Kopierziel überlappen. Das Ergebnis ist für diesen Fall undefiniert, d.h. in der Praxis, unbrauchbare Ergebnisse oder Programmabstürze sind zu erwarten. Einzig memmove() kann mit dieser Situation zurechtkommen, da es den Inhalt des Kopierziels zuerst in einem temporären Puffer zwischenspeichert.


/* strcopy.c
 * verwendet Funktionen aus der Standard-
 * bibliothek zur Manipulation von Strings
 */

#include <stdio.h>
#include <string.h>      /* wg. str... und mem... */
#include <stdlib.h>      /* wg. exit() */

int main(void){

char *pszFbThese_1 = "Die Philosophen haben die Welt nur "\
	"verschieden ";
char *pszFbThese_2 = "interpretiert, es kommt darauf "\
	 "an, sie zu verändern";
char *pszFbThese11;
char szBuffer1[200];
char szBuffer2[100];

 puts("memset():");
 /* die ersten 20 Zeichen von szBuffer1
  * mit '*' auffüllen
  */
 memset(szBuffer1, '*', 20);
 printf("Die ersten 20 Zeichen von szBuffer1: %20s\n\n",
	 szBuffer1);

 puts("strcpy() und strncpy():");
 /* Zeichenkette von pszFbThese_1 mit strcpy()
  * nach szBuffer1 kopieren
  */
 strcpy( szBuffer1, pszFbThese_1);

 /* sizeof(szBuffer2) Zeichen des Strings
  * pszFbThese_2 mit strncpy() nach szBuffer2
  * kopieren
  */
 strncpy( szBuffer2, pszFbThese_2, sizeof(szBuffer2));
 printf("szBuffer1: %s\nszBuffer2: %s\n\n", szBuffer1,  szBuffer2);

 puts("strcat():");
 /* szBuffer2 mit strcat() an szBuffer1 anhängen */
 strcat( szBuffer1, szBuffer2);
 printf("szBuffer1: %s\n\n", szBuffer1);

 puts("strncat():");
 /* sizeof(szBuffer1) ergibt die Gesamtgröße von
  * szBuffer1. strlen(szBuffer1) ermittelt die
  * Länge der Zeichenkette in szBuffer1.
  * Die Differenz ist mithin der freie Speicher
  * in szBuffer1
  */
 strncat( szBuffer1, "\n\t11. Feuerbach These",
	 sizeof(szBuffer1) - strlen(szBuffer1));
 printf("szBuffer1: %s\n\n", szBuffer1);

 pszFbThese11 = (char *) malloc( 
	 (strlen(szBuffer1) + 1) * sizeof (char));
 if( pszFbThese11 == NULL ){
	puts("Fehler bei Ausführung von malloc()");
	exit(EXIT_FAILURE);
	}

 puts("memcpy():");
 /* Die ganze Zeichenkette aus szBuffer1 (Länge
  * ermittelt durch strlen(szBuffer1)) nach 
  * pszFbThese11 kopieren
  */
 memcpy(pszFbThese11, szBuffer1, strlen(szBuffer1));
 printf("pszFbThese11: %s\n\n", pszFbThese11);
 free(pszFbThese11);
 pszFbThese11 = NULL;

 puts("memmove():");
 /* pszFbThese11 auf das 47. Element von
  * szBuffer1 richten
  */
 pszFbThese11 = &szBuffer1[47];
 /* 12 Zeichen vom 47.Element beginnend
  * (dorthin zeigt pszFbThese11) an die
  * Position des 84. Elements kopieren
  */
 memmove( &szBuffer1[84], pszFbThese11, 12);
 printf("szBuffer1: %s\n\n", szBuffer1);

return 0;
}

Es fällt auf, daß zum Kopieren und Konkatinieren (Aneinanderhängen) von Zeichenketten die populären Funktionen strcpy() und strcat() keine Sicherheit gegen Überschreiben des Ziel-Arrays bieten. Sowohl die damit verwandten Funktionen strncpy() und strncat() als auch memcpy() bieten dem Programmierer dagegen die Möglichkeit, die Anzahl der zu kopierenden Zeichen zu spezifizieren.

Damit dieser die Länge von Zeichenketten nicht an den Fingern abzählen muß, steht die Funktion strlen() zur Verfügung. Im Beispiel strcopy.c erweist sich strlen() in einigen Situationen als außerordentlich nützlich. Im Zusammenspiel mit dem sizeof-Operator kann so für die entsprechenden Funktionsaufrufe die Zahl der zu kopierenden Zeichen ohne großen Aufwand ermittelt werden.

Bei strncpy() können Sie (ebenso wie bei memcpy()) durch ein bloßes sizeof(szZielArray) an der dritten Argumentstelle sicherstellen, daß nicht mehr Zeichen geschrieben werden, als dort Platz haben. Bei strncat() gestaltet sich die Sache nicht ganz so einfach: Schließlich steht dort im Ziel-Array nicht mehr der ganze Platz zur Verfügung, weil ja der zweite Teil der String-Kette an den bestehenden ersten Teil-String angehängt werden soll. Die Größe des noch freien Speichers im Ziel-Array berechnet sich aus der Differenz zwischen seiner Gesamtgröße und der Länge des dort bereits abgelegten Strings: sizeof(szZielArray) — strlen(szZielArray).

Die Anwendung von memmove() zeigt, daß Teilketten innerhalb sich überlappender Speicherbereiche kopiert werden können, ohne daß mit unerwarteten Ergebnissen gerechnet werden muß. Auch hier liegt es nahe, allgemein das dritte Argument so zu wählen, daß die Grenzen des Zielbereichs gewahrt bleiben.

Relativ einfach gestaltet sich die Verwendung von memset(). Dies ist eine ideale Funktion, um größere Bereiche mit einem Wert zu initialisieren. Hätten wir nicht nur die ersten 20 Zeichen von szBuffer mit * überschreiben, sondern gleich das ganze Array damit initialisieren wollen, dann wäre der Aufruf memset(szBuffer1, '*', 200) sicher nicht die erste Wahl gewesen. Die bessere Alternative wäre natürlich auch hier die Definition einer symbolischen Konstanten für die Größe des Arrays.

Die Ausgabe von strcopy.c soll die Wirkung der einzelnen Funktionen noch einmal zusammenfassend veranschaulichen:


memset():
Die ersten 20 Zeichen von szBuffer1: ********************

strcpy() und strncpy():
szBuffer1: Die Philosophen haben die Welt nur verschieden
szBuffer2: interpretiert, es kommt darauf an, sie zu verändern

strcat():
szBuffer1: Die Philosophen haben die Welt nur verschieden interpretiert, es kommt darauf an, sie zu verändern

strncat():
szBuffer1: Die Philosophen haben die Welt nur verschieden interpretiert, es kommt darauf an, sie zu verändern
	11. Feuerbach These

memcpy():
pszFbThese11: Die Philosophen haben die Welt nur verschieden interpretiert, es kommt darauf an, sie zu verändern
	11. Feuerbach These

memmove():
szBuffer1: Die Philosophen haben die Welt nur verschieden interpretiert, es kommt darauf an, sie interpretier
	11. Feuerbach These

Zeichenketten vergleichen

Wenn Sie zwei Zeichenketten auf Gleichheit überprüfen wollen, dann stehen Ihnen in ANSI C dafür drei Funktionen zur Verfügung:

Funktion Beschreibung
int strcmp(const char *pszS1, const char *pszS2) liefert den Wert 0, wenn beide Zeichenketten gleich sind, einen negativen Wert, wenn pszS1 kleiner als pszS2 ist und einen positiven Wert, wenn pszS1 größer als pszS2 ist. Eine Zeichenkette ist größer als eine andere, wenn ihr erstes nicht übereinstimmendes Zeichen einen höheren Wert im verwendeten Zeichensatz hat (siehe strcomp.c).
int strncmp(const char *pszS1, const char *pszS2, size_t n) vergleicht bis zu n Zeichen von pszS1 mit pszS2. Die Rückgabewerte entsprechen denen von strcmp().
int memcmp(void *pszS1, void *pszS2, size_t n) vergleicht die ersten n Zeichen von pszS1 mit den ersten n Zeichen von pszS2. Rückgabewerte wie strcmp().

Folgendes Beispiel zeigt einen typischen Anwendungsfall für strcmp(), nämlich eine Login-Routine. Mangels einer ANSI-konformen Funktion, die Zeichen ohne Bildschirm-Echo einlesen kann, benutzen wir die auf PC-Compilern weit verbreitete Funktion getch(). Sie steht auch auf UNIX-Systemen zur Verfügung, wobei dort die Header-Datei curses.h eingefügt werden muß. Einzelheiten über die Verwendung von getch() unter UNIX entnehmen Sie der entsprechenden Man-Page (Aufruf: man curses).


/* login.c */

#include <string.h>
#include <stdio.h>
#include <conio.h>

#define SIZE 15

int main(void) {

char szPassWd[SIZE], szLoginName[SIZE];
int iCount = 0;

 printf("login: ");
 gets(szLoginName);

 printf("password: ");
 /* bis zur Betätigung des Wagenrücklaufs
  * Zeichen einlesen
  */
 while( (szPassWd[iCount] = getch()) != '\r'){
	putchar('*');
	if(iCount++ == SIZE)
		 break;
	}

 if( strcmp(szPassWd, "password") != 0 )
	puts("\n\nlogin failed");

 return 0;
 }

Unser nächstes Beispiel demonstriert, wie bzw. worin sich zwei Zeichenketten unterscheiden müssen, damit für strncmp() bzw. memcmp() eine Zeichenkette größer als die andere ist:


/* strcomp.c */

#include <string.h>
#include <stdio.h>

int main(void) {

char *pszS1 = "abcde";
char *pszS2 = "abcdf";

 printf("pszS1 ist %s als pszS2\n",
	strncmp(pszS1, pszS2, 5) > 0 ? "größer":"kleiner");

 pszS2 = "abbcdefghijklmno";

 printf("pszS1 ist %s als pszS2\n",
	memcmp(pszS1, pszS2, 4) > 0 ? "größer":"kleiner");

 return 0;
 }

Wer würde erwarten, daß "abcde" größer als "abbcdefghijklmno" ist? Der Grund dafür ist relativ einfach: Das erste nicht übereinstimmende Zeichen ist das dritte, nämlich c versus b. Da c im verwendeten Zeichensatz (meist wohl ASCII) einen höheren Wert hat als b, ist "abcde" größer als "abbcdefghijklmno".

Beachten Sie, daß alle drei Funktionen den Wert 0 zurückgeben, wenn die beiden Zeichenketten gleich sind. Dies kann zu unerwünschten Resultaten in einer if-Anweisungen führen: Die Bedingung der Gleichheit von zwei Zeichenketten ist ausgerechnet dann "wahr", wenn der Rückgabewert dieser drei Funktionen logisch "falsch

" ist

.

Zusätzlich ist allen drei Funktionen gemeinsam, daß sie Zeichenketten auf exakte Übereinstimmung prüfen. In unserem Beispiel login darf der Benutzer das Paßwort also nicht als Password oder PASSWORD eingeben, da strcmp() wie die anderen beiden Funktionen auch zwischen Groß- und Kleinschreibung unterscheidet. ANSI definiert leider keine Funktion, die Zeichenketten vergleicht und dabei Groß- und Kleinschreibung ignoriert. Viele Hersteller von C-Compilern bieten für diesen Zweck daher eigene Funktionen an. Relativ weit verbreitet sind die Funktionen stricmp(), strnicmp() oder memicmp(). Falls Sie auf Portierbarkeit Ihrer Programme Wert legen, dann sollten Sie eine entsprechende Funktion jedoch gleich selbst schreiben. Dies ist gar nicht so schwer, wie folgendes Beispiel andeutet:


/* str_icmp.c
 * vergleicht Zeichenketten ohne Rücksicht
 * auf Groß- und Kleinschreibung
 */

#include <string.h>

#include <stdio.h>
#include <stdlib.h>     	/* wg. abs() */

int main(void) {

char *pszS1 = "abcdE";
char *pszS2 = "abCde";
/* iOffset speichert die Differenz zwischen
 * Klein- und Großbuchstaben
 */
const int iOffset = 'a'-'A'; 

/* Schleifendurchlauf, solange die Zeichen beider
 * Strings gleich sind oder sich nur durch den
 * Wert 'a'-'A' unterscheiden
 */
 while( (*pszS1 == *pszS2) ||
	 abs(*pszS1 —
 *pszS2) == iOffset){

/* ist der 1. String abgearbeitet und die
 * Schleife immer noch aktiv, dann sind beide
 * Strings gleich
 */
	if(*pszS1 == '\0'){
		 puts("pszS1 und pszS2 sind gleich");
		 return 0;
		 }

	pszS1++;
	pszS2++;
	}

 printf("pszS1 ist %s als pszS2\n",
	*pszS1 > *pszS2 ? "größer":"kleiner");

 return (*pszS1 - *pszS2);
 }

Aufgrund der bisher fehlenden Kenntnisse über Funktionen haben wir den Code für den String-Vergleich direkt in main() realisiert; in der Praxis würde man dafür natürlich eine eigene Funktion vorsehen. Das Beispiel vergleicht in der while-Anweisung die beiden Strings Zeichen für Zeichen. Die Schleife läuft so lange, wie diese übereinstimmen oder sich nur durch die Differenz 'a'-'A' unterscheiden. Die Funktion abs() liefert den Absolutwert einer Ganzzahl, d.h. ihren Abstand von 0. Ohne sie müßten wir gegebenenfalls auch einen negativen Wert aus der Subtraktion *pszS1 — *pszS2 berücksichtigen. In der vorliegenden Form weist str_icmp.c noch einige Defizite auf: Es funktioniert nur mit Klein- und Großbuchstaben zuverlässig, Umlaute und andere Sonderzeichen werden nicht separat behandelt.

In Zeichenketten suchen

Auch für diese Aufgabe stellt die Standardbibliothek eine ganze Reihe von Funktionen zur Verfügung. Die folgende Tabelle gibt einen Überblick:

Funktion Beschreibung
char *strchr(const char *pszString, int iChar) sucht nach dem ersten Vorkommen des Zeichens iChar in pszString. Sobald sie das Zeichen findet, liefert sie einen Zeiger auf die entsprechende Position in pszString, andernfalls NULL.
char *strrchr(const char *pszString, int iChar) sucht nach dem letzten Vorkommen des Zeichens iChar in pszString. Rückgabewerte wie strchr().
void *memchr(const char *pszStr, int iChar, size_t n) sucht nach dem ersten Vorkommen von iChar in den ersten n Zeichen von pszString. Rückgabewerte wie strchr(), allerdings vom Typ void *.
char *strstr(const char *pszS1, const char *pszS2) sucht nach dem ersten Vorkommen von pszS2 in pszS1. Rückgabewerte analog zu strchr().
size_t strcspn(const char *pszS1, const char *pszS2) gibt von links beginnend die Länge des Teilstrings von pszS1 zurück, der nur aus Zeichen besteht, die nicht in pszS2 enthalten sind.
size_t strspn(const char *pszS1, const char *pszS2) gibt von links beginnend die Länge des Teilstrings von pszS1 zurück, der nur aus Zeichen besteht, die auch in pszS2 enthalten sind.
char *strpbrk(const char *pszS1, const char *pszS2) sucht nach dem ersten Vorkommen irgendeines Zeichens aus pszS2 in pszS1. Findet sie ein Zeichen, das auch in pszS2 vorkommt, dann liefert sie einen Pointer auf seine Position in pszS1, sonst NULL.

Bei den Suchfunktionen wollen wir uns auf zwei Beispiele beschränken: Das erste zeigt, wie Sie mit Hilfe von strchr() einen Zeilenvorschub aus Zeichenketten entfernen können, die mit fgets() von der Standardeingabe gelesen wurden:


/* fgets.c */

#include <stdio.h>
#include <string.h>

int main(void){

char szHello[21], *pszTemp;

 puts("\nGeben Sie eine originelle Begrüßung ein! "
		 "(max. 20 Zeichen)\n");

 fgets(szHello, sizeof(szHello), stdin);
 /* strchr() liefert, sobald es fündig wird, einen
  * Zeiger auf das 1. Vorkommen des gesuchten
  * Zeichens. Diesen speichern wir in pszTemp...
  */
 if( (pszTemp = strchr(szHello, '\n')) != NULL)
 /*... und schreiben an die Stelle von '\n' eine
  * abschließende '\0'
  */
	*pszTemp = '\0';

 puts(szHello);
 return 0;
 }

Unser zweites Beispiel zeigt, wie mit Hilfe von strstr() eine Suche nach Teilstrings durchgeführt werden kann. Auch diese Funktion unterscheidet wie bereits die diversen ...cmp()-Funktionen zwischen Groß- und Kleinbuchstaben, d.h., Zeichenkette pszS2 wird in pszS1 nur bei exakter Übereinstimmung der Schreibweise gefunden.


/* strstr.c */

#include <stdio.h>
#include <string.h>

int main(void){

char *pszSubStr;

 pszSubStr = strstr("FC Bayern München", "Bay");
 /* Ausgabe von puts():"Bayern München" */
 puts(pszSubStr);

 return 0;
}

Die Funktion strstr() ist ein praktisches Werkzeug für kleinere Suchvorgänge, bei denen Suchbegriffe in nicht allzu langen Strings gefunden werden sollen. Sie ist sicher nicht gedacht für die Suche in großen Textmengen. Suchen und Sortieren sind Vorgänge, für deren Optimierung in der Informatik erheblicher Forschungsaufwand betrieben wurde. Falls Sie vor dem Problem stehen, in großen Textmengen effizient nach bestimmten Zeichenfolgen suchen zu müssen, dann bietet sich dafür der Rückgriff auf bewährte Algorithmen wie jene von Knuth/Morris/Pratt (siehe dazu das Beispiel tsearch.c in Kapitel 10) oder Boyer/Moore an.

Zerlegen von Zeichenketten

Was den Stellenwert von Parsern in der Informatik betrifft, gilt ähnliches wie das, was zuletzt über Suchen und Sortieren gesagt wurde. Wir nehmen deshalb hier keinerlei Syntax-Analysen vor, sondern beschränken uns auf die Einsatzmöglichkeiten von zwei Bibliotheks-Funktionen, die für die Entwicklung von Parsern nützlich sein können. Die eine ist eine weitere str...-Funktion, die zweite eine Variante der mittlerweile gut bekannten Funktion scanf().

Wie wir schon im Abschnitt

9.1

feststellen konnten, eignet sich die Funktion für die formatierte Eingabe, scanf(), auch zum Einlesen von Zeichenketten. Besonders praktisch daran sind die mächtigen Formatierungs-Möglichkeiten, die es erlauben, eingegebene Daten zu filtern, umzuwandeln und zu zerlegen. Allerdings stellt ihr spezieller Umgang mit den sogenannten "Whitespace"-Zeichen ein Problem dar, weil es zum vorzeitigen Abbruch der Eingabe kommen kann. Die "String"-Variante von scanf(), sscanf(), liest ihre Daten nicht mehr von der Standardeingabe, sondern - wie Sie wahrscheinlich vermuten werden — aus einer Zeichenkette. Wenn diese mit einer Funktion wie fgets() eingelesen wurde, ist zumindest schon einmal gewährleistet, daß alle gewünschten Daten "im Kasten", also in einer Zeichenkette sind. Anschließend kann sich dann sscanf() daran machen, diesen String zu zerlegen oder beispielsweise daraus numerische Informationen zu entnehmen. Folgendes kurzes Beispiel soll die Einsatzmöglichkeiten von sscanf() andeuten:


/* sscanf.c */

#include <stdio.h>

int main(void){

char *pszMsg="wichtig: 5 mal 5 ergibt 25";
char szText[3][10];
int iZahlen[3], iCount;

 iCount = sscanf(pszMsg,"%7s%*c %d %s %d %s %d",
	szText[0],
	&iZahlen[0],
	szText[1],
	&iZahlen[1],
	szText[2],
	&iZahlen[2]);

 for( iCount = 0; iCount < 3; iCount++)
	printf("%s %d\n", szText[iCount], iZahlen[iCount]);

 return 0;
}

Die Verwendung von sscanf() bedarf wohl keiner großen Erläuterungen: Mit Ausnahme des ersten Arguments, das ein Pointer auf eine Zeichenkette sein muß, aus der sscanf() lesen soll, unterscheidet sie sich nicht von scanf(). Beachten Sie aber, daß den einzelnen Elementen des Arrays iZahlen ein Adreßoperator vorangestellt werden muß, während z.B. szText[1] bereits eine Adresse repräsentiert.

Im Gegensatz zu sscanf() kann strtok() keinerlei Konvertierung der einzelnen Teilketten leisten. Seine Stärke liegt vielmehr darin, daß es eine beliebige Liste von Zeichen akzeptiert, die als Trenner zwischen den einzelnen Bestandteilen ("Token") einer Zeichenkette interpretiert werden sollen. Anhand dieser Trennzeichen (engl. delimiter) nimmt strtok() die Zerlegung eines Strings vor. Für die komplette Zerlegung einer Zeichenkette, die aus mehr als einem Token besteht, bedarf es mehrerer Aufrufe von strtok(). Folgendes Beispiel führt dies vor:


/* strtok.c */

#include <string.h>
#include <stdio.h>
 
int main(void){

char szStrtokInfo[] = "strtok(): zerlegt-bei Bedarf-"\
 "Strings!Pro Token ist ein Aufruf notwendig.Die "\
 "Delimiter werden einfach in einer Zeichenkette "\
 "aufgeführt.";
char *pszNextToken;

	/* strtok fügt NULL vor jedem Token ein,
	 * das es gefunden hat
	 */
	 pszNextToken = strtok(szStrtokInfo, "' ':;.-");

	/* NULL als erstes Argument bewirkt, daß strtok()
	 * an der Position in der Zeichenkette fortsetzen
	 * soll, die es beim letzten Aufruf markiert hat
	 */
	while(pszNextToken != NULL){
		printf("%s\n", pszNextToken);
		pszNextToken = strtok(NULL, "' ':;.-");
		}
	return 0;
 }

Trifft strtok() auf ein Zeichen aus der Liste "' ':;.-", dann interpretiert es dieses als Beginn eines neuen Tokens. Da in unserem Beispiel das Ausrufezeichen nicht als Delimiter erscheint, gilt der Substring "Strings!Pro" entsprechend als ein Token. Wenn strtok() kein weiteres Token ausfindig machen kann, dann gibt es einen NULL-Pointer zurück. Bei der Anwendung dieser Funktion ist zu beachten, daß sie die bearbeitete Zeichenkette verändert, indem sie vor jedem Token eine '\0' einfügt. Soll die ursprüngliche Zeichenkette weiterhin zur Verfügung stehen, dann müssen Sie eine Kopie derselben an strtok() übergeben (die Sie z.B. mit strcpy() anfertigen können).

Zeichenketten sortieren

Wenn Zeichenketten sortiert werden sollen, dann ist im allgemeinen nicht gemeint, daß die Elemente einer Zeichenkette zu einer wohlgeordneten Buchstabensuppe sortiert werden. Vielmehr geht es darum, mehrere Zeichenketten in alphabetische Reihenfolge zu bringen. Als Datentypen für mehrere Strings kommen nur zweidimensionale Arrays, Arrays aus Pointern und Pointer auf Pointer in Frage. Bekanntlich reservieren Arrays automatisch den Platz für alle Elemente, unabhängig davon, ob dieser dann auch gebraucht wird: Deswegen benutzen wir in unserem Beispiel Pointer auf Pointer, um nicht unnötigen Speicher zu beanspruchen. Zum Sortieren benutzen wir diesmal die ANSI-konforme Funktion qsort(), die einen Quicksort-Algorithmus implementiert.


/* strsort.c
 * liest Zeichenketten von der Standardeingabe
 * und sortiert diese in aufsteigender Ordnung
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>      /* wg. qsort() und exit() */

#define MAXLEN 128
#define INIT 500

/* Deklaration der für qsort() notwendigen
 * Funktion comp() */
int comp( const void *arg1, const void *arg2);

int main(void){

/* Platz für 500 Pointer auf char anfordern */
char **pszLines = (char **) malloc (
	INIT * sizeof(char *) );

char *pszTemp, szInput[MAXLEN];
int iCount, iCount1, iNoOfLines = INIT;

 for( iCount=0 ;; iCount++ ){
	/* Falls alle Pointer auf Strings
	 * bereits vergeben sind ...	 */
	if( iCount == iNoOfLines ){

	/* ... mit realloc() Speicher für zusätzliche
	 * 500 Pointer anfordern */
		pszLines = (char **) realloc( pszLines,
			(iNoOfLines += 500) * sizeof(char *));

		if( pszLines == NULL ){
			puts("Fehler bei realloc()");
			exit(EXIT_FAILURE);
			}
		}
	/* Strings von der Standardeingabe lesen und
	 * bei Auftreten von EOF Eingabe beenden
	 */
	if (fgets( szInput, MAXLEN, stdin ) == NULL){
		/* letzten Pointer auf NULL setzen  */
		 pszLines[iCount] = NULL;
		 break;
		 }
	/* ev. vorhandenen '\n' entfernen */
	 if(( pszTemp = strchr(szInput,'\n')) != NULL )
		 *pszTemp = '\0';

	pszTemp = szInput;
	/* Führende Leerzeichen entfernen */
	while( *pszTemp == ' ')
		pszTemp++;

	/* strlen zählt die abschließende '\0' nicht
	 * mit. Deshalb noch Speicher für ein 
	 * zusätzliches Zeichen anfordern
	 */
	pszLines[iCount] = (char *) malloc(
		strlen(pszTemp) * sizeof(char) + sizeof(char));
	if( pszLines[iCount] == NULL ){
		puts("Fehler bei Zuteilung von Speicher");
		exit(EXIT_FAILURE);
		 }
	/* Zeichenkette vom Eingabepuffer kopieren */
	strcpy( pszLines[iCount], pszTemp );
	 }

 puts("Sortiere mit qsort...");

 qsort((void *) pszLines,  /* Basisadresse */
	 (size_t) iCount,      /* Anzahl der Elemente */
	 sizeof(char *),       /* Größe der Elemente */
	 comp);                /* Vergleichsfunktion */

 /* Zeichenketten sortiert ausgeben */

 for( iCount = 0; pszLines[iCount] != NULL; iCount++){
	puts( pszLines[iCount] );
	free( pszLines[iCount] );
	 }

 free(pszLines);

return 0;
}

int comp( const void *pszS1, const void *pszS2){

	 return (strcmp( *(char **)pszS1, *(char **)pszS2));
	}

Das Programm eignet sich in erste Linie für den Einsatz als Filter, ganz nach dem Vorbild der Sortier-Programme, die Teil der diversen Betriebssysteme sind. Entsprechend verzichtet es auf die Ausgabe von Benutzer-Prompts, da es in der Regel seine Eingabe über die umgeleitete Standardeingabe erhält, also z.B. strsort < readme.

Die unmittelbar gestellte Aufgabe, Zeichenketten alphabetisch zu sortieren, wurde in strsort.c zum Anlaß genommen, noch einmal wesentliche Pointertechniken und die Verwendung der dynamischen Speicherverwaltung vorzuführen. Beachten Sie, daß es einiger type-cast-Operationen bedarf, um unsere Daten an den von verschiedenen Bibliotheksfunktionen geforderten Typ anzupassen. Außerdem sei daran erinnert, daß bei Pointern auf Pointer ein einzelner Aufruf von malloc() nicht ausreicht, um Speicherplatz für sämtliche Zeichenketten zu reservieren. Abb. 9.1 veranschaulicht die Speichersituation bei Verwendung eines solchen Datentyps.

Beim eigentlichen Sortieren werden nur noch Pointer ausgetauscht, es müssen keine Zeichenketten im Speicher herumkopiert werden. Dies läßt sich einfach dadurch bewerkstelligen, indem jeder Pointer, der auf eine größere Zeichenkette zeigt als ein nachfolgender, während des Durchlaufs durch die Sortierroutine auf die Zeichenkette dieses folgenden Pointers gerichtet wird und umgekehrt. In unserem Fall übernimmt die Bibliotheksfunktion qsort() diese Aufgabe. Ihr Einsatz ist nicht ganz unkompliziert: Sie verlangt vier Argumente, eines davon ist ein Pointer auf eine Funktion. Diese Funktion (hier comp()) muß der Programmierer selbst definieren. Für sie besteht die Auflage, daß sie 0 zurückgeben muß, wenn beide Argumente gleich groß sind, einen Wert größer 0, wenn Argument1 größer als Argument2 ist und einen Wert kleiner 0, wenn Argument1 kleiner als Argument2 ist. Beim Vergleich von Zeichenketten ist die Funktion strcmp() wie geschaffen für diesen Zweck, da sie eben diese geforderten Werte als Rückgabewert liefert. Um ganzzahlige Werte zu sortieren, würde etwa return( *iArg1 — *iArg2) ausreichen.