26. Strings II

sscanf und snprintf

scanf und printf gibt es auch in Varianten, bei denen das erste Argument ein String ist – sie operieren dann nicht auf Standardein- und Ausgabe, sondern eben auf dem übergebenen String.

Auch diese Varianten sind in stdio.h deklariert, was, da sie mit I/O kaum etwas zu tun haben, vielleicht etwas seltsam anmuten mag.

sscanf bietet die korrekte Art, Zahlen aus Strings zu lesen. Die alten Funktionen atoi, atof und Freunde, die in manchen C-Einführungen noch empfohlen werden, sind demgegenüber Mist, in allererster Linie, weil sie keine brauchbare Möglichkeit bieten, falsche Argumente zu erkennen (also das zu tun, was in Python durch Auslösen eines ValueErrors realisiert wird).

Um also aus einem String str einen int i zu lesen, könnt ihr etwas tun wie:

if (1!=sscanf(str, "%d", &i)) {
  /* Fehlerbehandlung */
}

Analog geht das natürlich für Fließkommazahlen.

Dann und wann bekommt man Strings in bestimmten Formaten geliefert. Wenn z.B. definiert ist, dass drei ints durch Kommata getrennt ankommen, lassen sie sich per

if (3!=sscanf(str, "%d,%d,%d", &i1, &i2, &i3)) {
  /* Fehlerbehandlung */
}

parsen – da diese Strings häufig bereits durch Rechner generiert werden und bei sscanf eine vernünfigte Fehlerbehandlung möglich ist, ist hier die Verwendung fixer Bestandteile (wie den Kommata im Beispiel) in Formatstrings nicht selten sinnvoll. Das steht im Gegensatz zur Empfehlung, beim normalen scanf keine großen Tricks zu schmeißen. Hintergrund ist, dass beim normalen scanf meist nicht klar ist, wie viel Eingabe verbraucht wurde, bevor ein Fehler auftrat, was also das nächste Zeichen ist, das ein erneutes scanf bekommen würde – man kann sich also schlecht von einem Fehler erholen. Mit sscanf hingegen kann man einfach den kompletten String wegwerfen und einen neuen bestellen.

Will man per sscanf Strings in Teilstrings zerlegen, muss man wie immer aufpassen, dass die Zielstrings nicht überlaufen. Deshalb muss bei der Verwendung von %s bei allen scanf-Varianten immer die Länge angegeben werden, und zwar eins weniger als die Länge des Strings, in den eingelesen wird (die ’[0’ braucht ja auch Platz). Bekommt man etwa Eingabe der Art key = value, wobei key keinen Whitespace enhält, vor dem = mindestens ein Whitespace kommt und value, sagen wir, eine Dezimalzahl ist, so ließe sich das so parsen:

#define MAXKEYLEN 20
...
char key[MAXKEYLEN];
int value;

/* Warning: Magic 19 here is MAXKEYLEN-1 ! */
sscanf(str, "%19s = %d", key, &value);

Das ist natürlich Bockmist – wer MAXKEYLEN ändert, müsste auch dran denken, den Formatstring zu ändern, und das wird fast sicher nicht passieren. Wir brauchen also eine bessere Art, den Formatstring zu erzeugen. Sowas würde im Prinzip mit Präprozessorhacks gehen (vgl. unten), einfacher und allgemeiner geht das aber, wenn wir den Formatstring im Programm berechnen. Das wiederum ist ein Klassiker für snprintf.

snprintf unterscheidet sich von printf dadurch, dass es als erstes Argument den String nimmt, in den ausgegeben werden soll, und als zweites Argument dessen Größe. Danach geht es wie gewohnt mit dem Formatstring weiter. Die Funktion gibt die Zahl der hinterlassenen Zeichen (ausgenommen der abschließenden Null) zurück. Die Funktion schreibt aber nie wirklich mehr Zeichen, als ihr zweites Argument angibt, so dass man einfach prüfen kann, ob die Ausgabe abgeschnitten wurde.

Um einen Formatstring des oben benötigten Typs zu erzeugen, können wir etwas wie

  char format[FORMAT_LENGTH];

  if (FORMAT_LENGTH<=snprintf(format, FORMAT_LENGTH,
      "%%%ds = %%d", MAXKEYLEN)) {
    printf("Format string too short.\n");
    return 1;
  }

machen. Besonders interessant ist snprintf auch, wenn Programme formatierte Ausgabe nicht direkt in Dateien schreiben wollen.

Warnung: Es gibt auch sprintf, das keine Längenkontrolle eingebaut hat, d.h. so viele Zeichen schreibt, wie es eben schreiben möchte. Die Verwendung von sprintf ist praktisch immer ein Bug bzw. eine Sicherheitslücke, weil es fast immer möglich ist, eine Eingabe so zu drechseln, dass sehr viele Zeichen geschrieben werden. Also: sprintf nicht verwenden.

In Summe: Die Kiste mit dem einlesen von Schlüssel-/Wertpaaren könnte etwa entlang der in folgendem Programm vorgezeichneten Linien gelöst werden:

/* A little hack to demonstrate either snprintf or the preprocessor's
 * stringify operation */
#include <stdio.h>
#include <string.h>

#define MAXKEYLEN 4
#define KEYBUFSIZE MAXKEYLEN+1

#ifdef USE_STRINGIFY
#    define STRINGIFY(x) #x
#    define EXPAND_AND_STRINGIFY(x) STRINGIFY(x)
#else
#    define FORMAT_LENGTH 20
#endif


int main(void)
{
  char key[KEYBUFSIZE];
  int val, itemsRead;

#ifdef USE_STRINGIFY
  itemsRead = scanf("%" EXPAND_AND_STRINGIFY(MAXKEYLEN) "s = %d", key, &val);
#else
  char format[FORMAT_LENGTH];

  if (FORMAT_LENGTH<=snprintf(format, FORMAT_LENGTH,
      "%%%ds = %%d", MAXKEYLEN)) {
    printf("Format string too short.\n");
    return 1;
  }
  itemsRead = scanf(format, key, &val);
#endif

  switch(itemsRead) {
    case 0:
      printf("All botched.\n");
      break;
    case 1:
      printf("Key overflowed or bad format: %s, buffer size: %d\n",
        key, KEYBUFSIZE);
      break;
    case 2:
      printf("%s = %d\n", key, val);
      break;
    default:
      printf("You're kidding me, no?\n");
  }
  return 0;
}

Ich habe dabei gleich noch die alternative Lösung mit dem so genannten Stringify-Operator des Präprozessors eingeflochten. Welche Fassung verwendet wird, entscheidet der Compiler anhand des Präprozessorsymbols USE STRINGIFY. Ist es nicht definiert (wie abgedruckt), wird unsere snprintf-basierte Lösung einkompiliert, ist es definiert, die andere, die ihr mit dem, was ihr in diesem Kurs lernt, nicht verstehen könnt. Makrosprachen haben allerdings ihren eigenen Reiz, es ist also bestimmt kein Fehler, sich mal mit dem ISO-C-Standard oder auch einem ausreichend tiefschürfenden Lehrbuch hinzusetzen und nachzuvollziehen, was ich da eigentlich treibe.

Wenn ihr den Kram mit dem Stringify-Code bauen wollt, könnt ihr entweder irgendwas wie

#define USE_STRINGIFY

in das Programm schreiben oder, wenn ihr make und eine sh-abgeleitete Shell verwendet, zum Compilieren

CFLAGS=-DUSE_STRINGIFY make stringifyhack

sagen (wenn ihr das Programm unter stringifyhack.c abgespeichert habt). Was dabei vorgeht, wird im Kapitel zu make etwas genauer erklärt. Entscheidend ist jedenfalls, dass der Präprozessor beim Aufruf durch make (bzw. den C-Compiler) die Option -DUSE_STRINGIFY übergeben bekommt. Das hat die gleiche Wirkung wie das define oben.

Probiert das Programm mit allen möglichen Eingabevariationen und seht nach, wann das klappt und wann nicht.

Das Fazit von all dem sollte sein: Die Verarbeitung und das Parsen von Eingaben sollte man, wann immer möglich, von Python oder einer ähnlichen Sprache aus machen oder einer spezialisierten Bibliothek überlassen. Solche Sachen in C wasserdincht hinzukriegen ist eine hohe Kunst.

Klassifikation von Zeichen

Die C-Bibliothek kann Zeichen klassifizieren, die Funktionen dazu sind in ctype.h deklariert. Beispiele:

  • isalpha Wahr, wenn Argument ein Buchstabe ist.
  • islower Wahr, wenn Argument ein Kleinbuchstabe ist.
  • isspace Wahr, wenn Argument ein Leerzeichen ist (z.B. blank, cr, lf oder tab).
  • isdigit Wahr, wenn Argument eine Dezimalziffer ist

Ebenfalls in ctype.h sind Funktionen toupper und tolower zur Umwandlung von Groß- und Kleinbuchstaben.

locales

Natürlich sind die Klassifikations-Funktionen wieder sprachabhängig. C kennt, ebenso wie Python, locales, um damit zurecht zu kommen. Die Benutzung der Locales ist in C nicht viel anders als in Python:

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
#include <ctype.h>

int main(void)
{ int c;

  setlocale(LC_ALL, "");
  while ((c=fgetc(stdin))!=EOF)
    if (!isspace(c))
      printf("l:%c u:%c ia: %d\n", tolower(c),
        toupper(c), !!isalpha(c));
  return 0;
}

Dieser Programmierstil kann nicht empfohlen werden – Ausdrücke wie der in der while-Schleife sparen zwar Platz, laden aber zu Schüssen in den Fuß ein, das Fehlen der Klammern wird bei späteren Änderungen zur Falle, das !! ist ein Hack (es ist allerdings garantiert, dass er wahr und falsch auf 1 und 0 normiert). Wer Code von anderen Leuten wartet, kann sich aber fast sicher auf Schlimmeres einstellen.

Das setenv in diesem Beispiel dient zum Setzen einer Umgebungsvariablen (Environment Variable) – quasi eine Variable, die das Betriebssystem verwaltet. Mit sh-ähnlichen Shells (heute die meisten) würde das so gehen:

export LC_ALL=de_DE

Unter der DOS-Shell von Windows könnte das so aussehen:

set LC_ALL=de_DE

Allerdings haben die Locales unter Windows ganz andere Namen, und überhaupt ist es für locales die bessere Idee, die einschlägige GUI zu verwenden. Vgl. auch die einschlägige Wiki-Seite.

Übungen zu diesem Abschnitt

Ihr solltet euch wenigstens an den rötlich unterlegten Aufgaben versuchen

(1)

In den guten, alten Zeiten, als das Netz noch das Netz war und Mä also, damals fanden Leute es lustig, Texte rot13 zu verschlüsseln – die Idee war, einfach alle Buchstaben um 13 Plätze zu rotieren, so dass also aus einem A ein N (N ist der 13. Buchstabe des Alphabets, wenn man bei Null mit dem Zählen anfängt), aus einem O aber ein B wird (weil O der 14. Buchstabe ist, 14+13 aber 27 ist und wir nur sechsundzwanzig Buchstaben haben – wie bei unsigned ints fangen wir bei 26 einfach wieder bei Null an. 27%26 ist aber 1 und entspricht damit einem B).

Schreibt eine Funktion, die nachsieht, ob ein Zeichen zwischen a und z oder zwischen A und Z ist und die dann die nötige Transformation macht (Hinweis: ihr tut euch leichter, wenn ihr ein zu transformierendes Zeichen zunächst auf seinen Index (a=A=0, b=B=1 usf) bringt).

rot13 ist selbstinvers: Eine doppelte Anwendung von rot13 bringt den Ursprungstext hervor. Testet euer Programm auch mit einer Konstruktion wie

rot13 < rot13.c | rot13

Und: Hier ist die rot13-“verschlüsselte” Lösung:

#vapyhqr <fgqvb.u>

vag ebgngr13(vag p)
{
        vs ('n'<=p && p<='m') {
                p = ((p-'n')+13)%26+'n';
        } ryfr vs ('N'<=p && p<='M') {
                p = ((p-'N')+13)%26+'N';
        }
        erghea p;
}

vag znva(ibvq)
{
        vag p;

        juvyr ((p=strgp(fgqva))!=RBS) {
                schgp(ebgngr13(p), fgqbhg);
        }
        erghea 0;
}


Markus Demleitner

Copyright Notice