String-Verarbeitung in C

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

In C gibt es, anders als in anderen Programmiersprachen, keinen eigenen String-Datentyp. Als Ersatz dafür werden Character-Arrays benutzt, in denen die einzelnen Character (=Zeichen) gespeichert werden. Allerdings gibt es noch einen Zusatz: Das letzte Zeichen eines Strings ist immer ein '\0'-Zeichen, dass das Ende des Strings markiert. Schließlich kann ja das Array wesentlich größer sein, als der in ihm gespeicherte String und irgendwie müssen ja diverse Funktionen das tatsächliche Ende eines Strings erkennen können.

Möchte man also die Zeichenkette "Hello World" in einem String speichern, so wird dafür ein Array mit mindestens der Länge 12 benötigt: 11 für die Zeichen die "Hello World" bilden, plus eine zusätzliche Position für das abschließende '\0'-Zeichen.

Strings sind Char-Arrays

Da Strings in char-Arrays gespeichert werden, können selbstverständlich normale Array Operationen dafür benutzt werden:

  char Test[12];

  Test[0] = 'H';
  Test[1] = 'e';
  Test[2] = 'l';
  Test[3] = 'l';
  Test[4] = 'o';
  Test[5] = ' ';
  Test[6] = 'W';
  Test[7] = 'o';
  Test[8] = 'r';
  Test[9] = 'l';
  Test[10] = 'd';
  // das abschliessende \0 nicht vergessen! Sonst ist
  // das kein String!
  Test[11] = '\0';

  char Temp[6];

  for (i = 0; i < 6; ++i)
    Temp[i] = Test[i + 6];
Hinweis
Das '\0' ist nichts anderes als eine binäre Null. Diese spezielle Schreibweise soll explizit die Verwendung dieser 0 als Stringende -Zeichen (char)0 hervorheben.

Wird im C-Quelltext ein String in der Form "Hello World" geschrieben, also nicht als einzelne Zeichen, so muss man sich um das abschliessende '\0' Zeichen nicht kümmern. Der Compiler ergänzt das stillschweigend von selbst.

Stringfunktionen der libc

Arrays sind in C keine vollwertigen Datentypen, z. B. ist es nicht möglich, einem Array in einem Rutsch ein anderes Array zuzuweisen oder 2 Arrays miteinander zu vergleichen. Genau das möchte man aber in der Stringverarbeitung häufig.

Zu diesem Zweck gibt es Standardfunktionen, die allesamt im Headerfile <string.h> zusammengefasst sind und deren Namen alle mit str... beginnen. Allen diesen Funktionen ist gemeinsam, dass sie sich nicht um die Bereitstellung des Speicherplatzes für Arrays kümmern, sondern davon ausgehen, dass dies vom Programmierer erledigt wird.

strcpy (char* dest, const char* src)

Kopieren eines Strings vom Speicherbereich, der bei src beginnt, zum Speicherbereich, der bei dest beginnt.

  char Ziel1[20];
  char Ziel2[20];

  strcpy( Ziel1, "Hallo Welt" );
  strcpy( Ziel2, Ziel1 );

strcat (char* dest, const char* src)

Anhängen eines Strings an einen anderen String.

  char Ziel[20];
  char Temp[20];

  strcpy( Ziel, "Hallo " );  // Ziel enthält jetzt den String "Hallo "
  strcat( Ziel, "Welt" );    // Ziel enthält jetzt den String "Hallo Welt"

  strcpy( Temp, "!" );
  strcat( Ziel, Temp );      // Ziel enthält jetzt den String "Hallo Welt!"

strcmp (const char* str1, const char* str2)

Lexikographischer Vergleich zweier Strings. Das Ergebnis ist 0, wenn die beiden Strings identisch sind, kleiner 0, wenn der erste String lexikographisch kleiner ist und größer 0, wenn der erste String lexikographisch größer ist.

  char Ziel[20] = "Hallo Welt";

  if (strcmp (Ziel, "Hallo Welt") == 0)
    printf ("Ziel war 'Hallo Welt'\n");

  if (strcmp (Ziel, "test") != 0)
    printf ("Ziel war NICHT 'test'\n");

strlen (const char* str)

Die Länge eines Strings feststellen. Die Länge beinhaltet nicht das abschliessende 0-Zeichen.

Unter Länge wird hier die tatsächliche Länge des Strings, also die Anzahl der im String gespeicherten Zeichen, verstanden und nicht die Länge des Arrays, in dem der String gespeichert ist. Wird der Text "test" in einem char-Array der Größe 20 gespeichert, so lautet das Ergebnis von strlen() 4 und nicht etwa 20.

  char string[20] = "test";

  // i bekommt hier den Wert 4
  size_t i = strlen (string);

Beispiele

#include <string.h>

int main()
{
  char Meldung[14];
  strcpy( Meldung, "Hello World" );
}

Mittels der Definition

  char Meldung[14];

wird ein Array bereitgestellt, welches maximal 14 Zeichen aufnehmen kann. "Hello World" verbraucht für die lesbaren Zeichen 11 Array-Positionen, dazu noch das obligatorische abschliessende '\0'-Zeichen, macht in Summe 12 Positionen. Eine Definition von 14 Zeichen ist also mehr als minimal notwendig wäre. Das macht aber nichts, da durch das abschliessende '\0' Zeichen immer feststellbar ist, an welcher Stelle der tatsächliche String zu Ende ist. Die restlichen 2 Array-Positionen sind zurzeit unbenutzt.

strcpy() kopiert den zweiten angegebenen String an die Position, auf die sein erstes Argument zeigt. Im obigen Beispiel zeigt das erste Argument auf den Beginn von Meldung, also auf das Array. Folgerichtig wird der String "Hello World" in das Array Meldung kopiert. Man beachte auch, dass der Compiler den direkt angegebenen String "Hello World" automatisch mit einem '\0'-Zeichen ergänzt hat. Nach Ausführung der strcpy() Funktion enthält also Meldung den Inhalt:

    +---+---+---+---+---+---+---+---+---+---+---+---+---+---+
    | H | e | l | l | o |   | W | o | r | l | d | \0|   |   |
    +---+---+---+---+---+---+---+---+---+---+---+---+---+---+
    

Möchte man an diesen Text jetzt noch etwas anfügen, z. B. ein "?", so würde das so aussehen:

#include <string.h>

int main()
{
  char Meldung[14];
  strcpy( Meldung, "Hello World" );
  strcat( Meldung, "?" );

  return 0;
}
Man beachte
Auch wenn hier scheinbar nur ein einzelnes Zeichen angehängt wird, so handelt es sich doch um einen String. Strings werden in C immer mit einem " eingeleitet und abgeschlossen. Im Gegensatz zu einzelnen Zeichen, die in einfache ' eingefasst werden. "?" ist also nicht dasselbe wie '?'. Das erste ist ein String, der mit dem obligatorischen '\0'-Zeichen insgesamt aus 2 Zeichen besteht, während letzteres ein einzelnes Zeichen darstellt. Die meisten str... Funktionen arbeiten nur mit Strings.

Da Meldung maximal 14 Zeichen umfassen kann, der Text "Hello World?" aber nur aus 13 Zeichen besteht, funktioniert obiges auch ohne Probleme. Der Array-Inhalt sieht dann wie folgt aus:

    +---+---+---+---+---+---+---+---+---+---+---+---+---+---+
    | H | e | l | l | o |   | W | o | r | l | d | ? | \0|   |
    +---+---+---+---+---+---+---+---+---+---+---+---+---+---+

Ein schwerwiegender Fehler wäre es, wenn der komplette String nach dem strcat() aus mehr als 14 Zeichen (das '\0'-Zeichen nicht vergessen!) bestehen würde.

#include <string.h>

void foo (void)
{
  char Meldung[14] = "Hello World";

  strcat( Meldung, " von mir" );
}

würde also das Array überlaufen lassen.

    +---+---+---+---+---+---+---+---+---+---+---+---+---+---+
    | H | e | l | l | o |   | W | o | r | l | d |   | v | o | n       m   i   r   \0
    +---+---+---+---+---+---+---+---+---+---+---+---+---+---+

Man sieht sehr schön, daß in diesem Fall die weiteren Zeichen einfach an die Folgepositionen im Speicher geschrieben werden und dadurch ungewollt Speicher verändern, der nicht zu Meldung gehört. Abhängig von den Details des Programmes können aber an dieser Stelle im Speicher z. B. ganz andere Variablen liegen, die dann verändert werden. Dies ist ein klassischer Fehler, der wahrscheinlich für mehr Programmabstürze und ungewollte Sicherheitslücken verantwortlich war, als jeder andere Bug. Man nennt ihn auch einen Buffer-Overflow.

strcpy(), strcat() und alle anderen String-Funktionen können den Programmierer gegen diesen Fall nicht schützen! Dazu müssten sie die Größe des Speicherbereichs kennen, was sie nicht tun. Es obliegt einzig und alleine der Sorgfalt des Programmierers, das Programm gegen solche Fälle abzusichern!

Seit einiger Zeit wurde das Sammelsurium der str... Funktionen durch Varianten ergänzt, die sich anschicken, dieses Manko zu entschärfen. Diesen Funktionen wird die maximale Anzahl der zu bearbeitenden Zeichen mitgegeben. Mit der Kenntnis der Größe des Zielbereichs und der Länge des bereits darin enthaltenen Strings ist es damit möglich, eine Obergrenze auszurechnen, wieviele Zeichen von einer Funktion gefahrlos bearbeitet werden dürfen, ehe der Zielbereich überläuft.

Diese Funktionen heißen grundsätzlich gleich wie die str... Funktionen, nur befindet sich ein kleines 'n' im Funktionsnamen. Aus strcpy wird so strncpy, aus strcat wird strncat usw. Für Details dazu sei auf Literatur oder Web-Recherche verwiesen. Auch wenn einen diese Funktionen gegen die gefürchteten Array-Overflows schützen können, so muß man sich trotzem klarmachen, daß dieser Schutz nur die halbe Miete ist. Denn was soll strncpy denn tun, wenn der zu kopierende String nicht in das Zielarray passt?

strncpy kopiert soviel wie es kann und gibt dann auf. Aber: Dadurch ist der String nicht komplett in den Zielbereich kopiert worden. Programmteile, die darauf angewiesen sind, daß der String vollständig kopiert wurde, werden dann nicht mehr oder nicht richtig funktionieren usw. Auch wenn die strn... Funktionen eine gewisse Abhilfe bringen und zumindest den Absturz eines Programmes verhindern können, stellen sie dennoch keine Allheilmittel dar. Um die korrekte Abschätzung der benötigten Arraygrößen kommt man nicht umhin.

strlen() liefert die Länge eines Strings. Die Längenangabe beinhaltet dabei nicht das abschliessende '\0'-Zeichen:

#include <string.h>

void foo (void)
{
  char Meldung[14] = "Hello World";
  int  Len;
  
  Len = strlen( Meldung );
  /* Jetzt enthaelt Len den Wert 11 */

  Len = strlen( "Hallo Welt" );
  /* Jetzt enthält Len den Wert 10 */
}

strcmp() schlussendlich vergleicht 2 Strings auf Gleichheit. Der Rückgabewert spiegelt dabei die Position des ersten Unterschiedes in den beiden Strings wieder. Folgerichtig sagt ein Wert von 0 daher aus, dass die beiden Strings identisch sind:

#include <string.h>

void foo (void)
{
  char Meldung1[14] = "Hello World";
  char Meldung2[14] = "Hallo Welt";

  if (strcmp( Meldung1, Meldung2 ) == 0)
  {
    /* die Strings sind identisch */
  }
  else
  {
    /* die Strings sind nicht identisch */
  }
  
  if (strcmp( Meldung2, "Hallo Welt" ) == 0)
  {
    /* Meldung2 war "Hallo Welt" */
  }
}

Es gibt noch weitere String-Funktionen. Dafür sei auf die Verwendung der zum Compiler gehörenden Dokumentation bzw. auf einführende Literatur zum Thema 'Programmieren in C' verwiesen.

Wie schreibt man eine Funktion, die einen String liefert?

Will man eine Funktion schreiben, die einen String liefert, so hat man ein Problem. So gehts nicht:

  char string[10];
  c = uart_get_line();

...

char uart_get_line()
{
  char  string[10];
  uint8_t i = 0;

  for( ... )
  {
    ...
    string[i] = ...;
    ...
  }
  string[i] = '\0';
  return *string;
}

Strings sind ja nichts anderes als Zeichen in einem Array und Arrays kann man nicht als Ganzes in C einfach so zuweisen. Das bedeutet aber auch, dass uart_get_line gar nicht ein Array als solches liefern kann! Daher hat auch der Programmierer versucht, sich mit einem char als Rückgabewert bzw. dem *string im return sich aus der Affäre zu ziehen. Nur: so funktioniert das nicht. uart_get_line würde hier nur ein einziges Zeichen zurückliefern und keinen String!

Eine naheliegende Lösung wäre:

// liest eine Zeile (maximal 9 Zeichen) und liefert sie als
// nullterminierten String zurück:
char * uart_get_line()
{
  char string[10];
  uint8_t i = 0;

  for( ... )
  {
    ...
    string[i] = ...;
    ...
  }
  string[i] = '\0';
  return string;  // nicht *string
}

Das ist jetzt schon etwas besser, aber immer noch falsch.

Gebessert hat sich ja schon der Rückgabetyp; dazu passt das return string statt return *string.

Mit *string würde man ja ein char liefern, und zwar das unterste in string, mithin string[0]. Jetzt will ma ja aber nicht das eine Zeichen liefern, sondern den ganzen String. Und das geschieht in C, indem man einen Zeiger auf das erste Zeichen nimmt. Das ist string.

Falsch (und zwar richtig falsch!) ist diese Lösung aber aus folgendem Grund: string ist eine automatische Variable (alle lokalen Variablen in einer Funktion oder in einem Block sind automatisch, wenn nicht static davor steht). Eine automatische Variable existiert aber nur, solange der umgebende Block (hier die Funktion) abgearbeitet wird. Sofort danach kann der Speicherplatz für etwas anderes genutzt werden, meist für Parameter einer aufgerufenen Funktion, oder deren lokale Variablen, oder Rücksprungadressen, temporäre Zwischenwerte oder was auch immer.

Normalerweise ist das in Ordnung, weil man ja auf eine Variable mit ihrem Namen nur zugreifen kann, solange man in demselben Block ist.

    {
       int i = 12;
       i += 12;
    }
    printf( "%d", i ); // geht nicht

So etwas verhindert der Compiler aus gutem Grund: i ist eine Variable in dem Block mit den geschweiften Klammern und existiert nur solange der Block abgearbeitet wird. Das printf() steht außerhalb, hier lässt der Compiler die Verwendung von i nicht mehr zu - die Variable existiert dann auch gar nicht mehr. Ändert man das etwas ab, hat man den Compiler überlistet:

    int* zeiger_auf_i;
    {
       int i = 12;
       i += 12;
       zeiger_auf_i = &i;
    }
    ...
    printf( "%d", *zeiger_auf_i );

Jetzt macht man im Prinzp dasselbe wie eben, nur etwas umständlicher. In zeiger_auf_i merkt man sich die Adresse von i, und im printf() benutzt man die Adresse um auf i zuzugreifen. Jetzt verhindert Compiler das nicht. Syntaktisch geht alles mit rechten Dingen zu. Es ist aber noch genauso falsch: wenn das printf() läuft, wird über den Zeiger auf i zugegriffen, obwohl i gar nicht mehr existiert bzw. möglicherweise schon lange für etwas anderes benutzt wird.

Mit solchen Konstruktionen kann man sich in C schön selbst verarschen, wenn man nicht weiß was man tut.

Im Prinzip etwas ähnliches macht man mit dem obigen falschen Beispiel:

// liest eine Zeile (maximal ... Zeichen) und liefert sie als
// nullterminierten String zurück:
char* uart_get_line()
{
  char string[10];
  uint8_t i = 0;

  for( ... )
  {
    ...
    string[i] = ...;
    ...
  }
  string[i] = '\0';
  return string;  // nicht *string
}

   ...
   char* meinezeile;
   meinezeile = uart_get_line();
   printf( "gelesen: %s", meinezeile );
   ...

(Es soll jetzt mal egal sein, daß es ein printf() in dieser Form gar nicht auf einem AVR gibt; es geht nur darum, den String irgendwie zu verwenden.)

Was passiert bei diesem Beispiel? in uart_get_line() gibt es eine lokale (automatische) Variable string; das ist ein Feld mit 10 Zeichen. Beim Rücksprung wird die Adresse des ersten Elements zurückgegeben und beim Aufrufer in meinezeile gespeichert. meinezeile ist also ein Zeiger auf das erste Zeichen von string. Der springede Punkt ist: Die Variable string existiert nach Verlassen der Funktion gar nicht mehr! Man hat daher einen Pointer, der auf etwas Ungültiges zeigt. Da muss nicht unbedingt heißen, dass es nicht so aussehen kann, als ob so etwas funktionieren würde. Solange der Speicherplatz der ehemaligen Variablen string nicht für andere Zwecke gebraucht wurde, kann man unter Umständen sogar über den Pointer an die korrekten Zeichen kommen. Der physikalische Speicher verschwindet ja nicht irgendwie magisch. Der ist schon noch da. Nur haben wir keine Garantie, dass auch das drinnen steht, was unserer Meinung nach drinnen stehen sollte, denn dieser Speicher ist ja nicht mehr für die ausschliessliche Verwendung durch die Variable string reserviert.

Bei soetwas muß man in C ziemlich nervös werden! So ist es jedenfalls Murks.

Es gibt mehrere Möglichkeiten, das zu verbessern, Leider sind sie alle nicht richtig schick, jede Variante ist irgendwie etwas doof.

Erste Möglichkeit: Den String static machen

Dann ist es keine automatische Variable mehr, die daher auch nicht beim Verlassen der Funktion zerstört wird:

// liest eine Zeile (maximal ... Zeichen) und liefert sie als
// nullterminierten String zurück:
char* uart_get_line()
{
  static char string[10];
  uint8_t i = 0;

  for( ... )
  {
    ...
    string[i] = ...;
    ...
  }
  string[i] = '\0';
  return string;  // nicht *string
}

   ...
   char* meinezeile;
   meinezeile = uart_get_line();
   printf( "gelesen: %s", meinezeile );
   ...

Damit klappt es auf einmal wundersamerweise. Eine static-Variable wird nicht immer neu angelegt und wieder wegegworfen, sondern existiert einmal von Programmstart bis Programmende.

Aber wo ist dabei der Haken? Daß sie eben nur einmal existiert.

   ...
   char* meinezeile1;
   char* meinezeile2;

   meinezeile1 = uart_get_line();
   printf( "gelesen1: %s", meinezeile1 ); // gibte erste Zeile aus
   meinezeile2 = uart_get_line();
   printf( "gelesen2: %s", meinezeile2 ); // gibte zweite Zeile aus

   printf( "gelesen1: %s", meinezeile1 ); // gibte zweite Zeile aus (!)
   printf( "gelesen2: %s", meinezeile2 ); // gibte zweite Zeile aus
   ...


In diesem Beispiel sieht es erst vernünftig aus: mit meinezeile1 wird die erste gelesene Zeile ausgegeben, mit meinezeile2 dann die zweite. Mit der folgenden Ausgabe wird sowohl für meinezeile1 als auch für meinezeile2 nur noch die letzte gelesene Zeile (also die zweite) ausgegeben, weil in der static-Variable string ja mit der zweiten Zeile die erste überschrieben wurde und sowohl meinezeile1 als auch meinezeile2 ja Zeiger auf dieses eine Array sind.

              meinezeile1
              +--------------+
              |   o------------------------+  string (in der Funktion uart_get_line)
              +--------------+             |  +---+---+---+---+---+---+
                                           +->|   |   |   |   |   |   
                                              +---+---+---+---+---+--
                                              ^
           meinezeile2                        |
           +--------------+                   |
           |    o-----------------------------+
           +--------------+


Das wird der Aufrufer so wahrscheinlich nicht erwarten; das ist also doch etwas problematisch. Richtig krank werden static-Variablen bei Programmen, die mit mehreren Threads arbeiten, ebenso wie bei rekursiven Funktionsaufrufen. Leider sind mehrere Funktionen der Standard-Lib von C mit statischen Variablen gebaut und deshalb nicht benutzbar in multitasking-Programmen (strtok() z.B. fällt in diese Kategorie).

Zweite Möglichkeit: dynamische Allokierung

Eine andere Lösung wäre: in der Funktion wird für den gelesenen String Speicher allokiert und der Zeiger darauf zurückgegeben:

// liest eine Zeile (maximal ... Zeichen) und liefert sie als
// nullterminierten String zurück:
char * uart_get_line()
{
  uint8_t i = 0;

  char* string = malloc( genugzeichen.... );
  for( ... )
  {
    ...
    string[i] = ...;
    ...
  }
  string[i] = '\0';
  return string;
}

   ...
   char* meinezeile;
   meinezeile = uart_get_line();
   printf( "gelesen: %s", meinezeile );
   free( meinezeile ); // Speicher nach letzter Benutzung freigeben
   ...


Das würde jetzt auch bei Multitasking funktionieren ebenso wie bei Rekursion.

Und wo ist hier der Haken? In der Funktion wird Speicher allokiert, und erst der Aufrufer kann den Speicher freigeben - wenn er es nicht vergisst.

Leider wird es mehr oder weniger häufig vergessen, weswegen es als höchst unelegant gilt, in einer Funktion Speicher zu allokieren, der vom Aufrufer wieder freigegeben werden muß.

Auf einem Controller wird man diese Lösung auch nicht gerne sehen, weil es da wegen des knappen Speichers selten ratsam ist, dynamische Speicherverwaltung mit malloc() zu nutzen. Ich hatte hier ja auch die Fehlerbehandlung unterschlagen: Was soll passieren, wenn gar kein Speicher allokiert werden kann? Auf einem Controller kann man ja nicht einfach eine kurze Meldung ausgeben und das Programm beenden. Ein Absturz wird auch nicht gern gesehen und nicht so leicht akzeptiert wie unter Windows.

Dritte Möglichkeit: Allokierung durch den Aufrufer

Der Aufrufer selbst beschafft den Platz, übergibt ihn und die Funktion schreibt nur rein:

// liest eine Zeile (maximal ... Zeichen) und liefert sie als
// nullterminierten String zurück:
char * uart_get_line( char* string, size_t l_puffer )
{
  uint8_t i = 0;

  for( ... && i<l_puffer-1 )
  {
    ...
    string[i] = ...;
    ...
  }
  string[i] = '\0';
  return string;
}

   ...
   char meinezeile[10];
   uart_get_line( meinezeile, 10 );
   printf( "gelesen: %s", meinezeile );
   ...

Weil die Funktion nicht feststellen kann, wie groß der Puffer ist, muß der Aufrufer auch gleich die Länge übergeben.

Das hat nebenbei den Vorteil, daß man in der Funktion nicht mehr spekulieren muß, wieviel Platz nötig ist (wie kommt man gerade auf die 10? Woher soll man das in der Funktion wissen?). Die Funktion kann also je nach Situation mal mit einem großen oder mal mit einem kleinen Puffer aufgerufen werden. Dafür hat man aber wieder das umgekehrte Problem: Was ist, wenn der Aufrufer nicht weiß oder wissen kann, wie groß ein String werden kann. Besonders problematisch ist das zb. bei Benutzereingaben oder in Situationen in denen der String durch Datenübertragung entsteht. Niemand kann zuverlässig vorhersagen, wieviele Zeichen ein Benutzer eingeben wird. Nur: Wie soll der Aufrufer einer entsprechenden Funktion dies dann wissen?

Nachteil dieser Lösung ist ausserdem, daß der Aufruf etwas umständlich ist (2 Parameter mehr).

Kurz gesagt: Egal wie man es dreht und wendet, in C gibt es für dieses Problem keine allumfassende, allein selig machende Lösung. Auf einem µC wird die letzte Variante im Regelfall bevorzugt, wobei der Aufrufer der Funktion mit einer vernünftigen Schätzung der maximal notwendigen Arraygröße leben muss. In vielen Fällen ist das nicht so schwer zu tun und zur Not macht man als Aufrufer eben ein wenig größer als geschätzt.

Weblinks