Strings

Zeichen

Im Gegensatz zu anderen Programmiersprachen kennt C keinen Datentyp zur Darstellung von Zeichenketten (Strings), sondern bildet diese einfach als ein Array von Zeichen ab. Damit ist der einzige von C zur Verfügung gestellte Datentyp, um Zeichenketten zu bilden, das einzelne Zeichen: char.

  • Der Datentyp char hat 8 Bit und kann so nur ASCII-Zeichen darstellen
  • für Unicode gibt es spezielle Datentypen (wchar_t aus wchar.h)
  • ctype.h bietet Funktionen, um Zeichen auf ihre Art zu prüfen
Funktion Test Zeichen
isalnum(ch) alphanumerisch [a-zA-Z0-9]
isalpha(ch) Buchstabe [a-zA-Z]
isdigit(ch) Ziffer [0-9]
ispunct(ch) Satzzeichen [~!@#%^&…]
isspace(ch) Whitespace [ \t\n]
isupper(ch) Großbuchstabe [A-Z]
islower(ch) Kleinbuchstabe [a-z]

Die Bitbreite des Datentyps char ist fest in der C-Spezifikation mit 8 Bit verankert und kann nicht geändert werden. Deswegen muss man bei der Verwendung von Unicode auf andere Datentypen ausweichen.

Ein einzelnes Zeichen, das durch den Datentyp char repräsentiert wird, kann auch durch ein Literal direkt im Quelltext angegeben werden.

Zeichenliterale repräsentieren ein einziges Zeichen, umschlossen von einfachen Anführungszeichen '

Bestimmte Sonderzeichen müssen escaped, d. h. speziell notiert, werden

Escape-Sequenz Bedeutung
\' Anführungszeichen '
\" Anführungszeichen "
\\ Backslash \
\f Seitenvorschub
\t Tabulator
\n Zeilenvorschub (newline)
\r Wagenrücklauf (carriage return)
\b Backspace
\f Seitenvorschub (form feed)

Beispiel:

char a = 'a';
char b = 'b';
char tab = '\t';
char newline = '\n';

printf("%c%c%c%c", a, tab, b, newline); /* a  b */

Strings

C-Strings sind eine ausgesprochene Enttäuschung, wenn man andere Programmiersprachen gewöhnt ist. Sie sind hochgradig speichereffizient, erfordern im Gegenzug aber viel Arbeit und Sorgfalt vom Programmierer.

  • In Java sind Strings Objekte mit Methoden etc.
  • In C gibt es keinen Datentyp für Strings
  • Strings sind nur ein Array von char, das durch ein NUL ('\0') beendet wird
  • Ein String-Literal (z. B. "pr3 rocks")
    • alloziert automatisch Speicher, inklusive Platz für das '\0'-Zeichen
    • hat als Wert die Adresse des ersten Zeichens (Pointer)
    • kann nicht verändert werden (beliebter Fehler)
  • für andere Strings, die keine Literale sind, muss vom Programm Speicher alloziert werden

Beachten Sie, dass NUL nicht dasselbe wie NULL ist. NUL bezeichnet das Zeichenliteral '\0', NULL hingegen den void*-Pointer (void*) 0. Beide enthalten zwar den Wert 0, aber sie haben unterschiedliche Datentypen, nämlich char und void*.

An einer Stelle haben die C-Entwickler Gnade gezeigt und ein Literal für Strings in C spezifiziert. D. h. man kann Strings im Programm direkt angeben ("pr3 rocks") und muss sie nicht als char[]-Literal ({ 'p', 'r', '3', ' ', 'r', 'o', 'c', 'k', 's', '\0' } schreiben.

char s[] = "pr3 rocks";

printf("%ld\n", sizeof(s)); /* -> 10 */

s[3] = '2'; /* Pfui, Fehler. Ist ein Literal!!! */

Bei der Deklaration eines solchen Literals passieren drei Dinge:

  • Es wird ein char-Array mit dem Platz für die angegebenen Zeichen + 1 Byte für das NUL am Ende alloziert.
  • Es wird ein char*-Pointer alloziert.
  • Dem Pointer wird die Adresse des Arrays zugewiesen.

Eine Definition wie char s[] = "a" benötigt deshalb auf einer 64-Bit-Plattform 10 Byte: 8 Byte für den Pointer s und zwei Byte für die Zeichen {'a', '\0' }.

Pointer auf Strings

Da C-Strings Arrays sind und Arrays in C als Pointer auf das erste Element aufgefasst werden können, kann man C-Strings ebenfalls als Pointer auf das erste Zeichen verstehen.

  • Strings werden einfach über einen Pointer auf das erste Zeichen verwaltet
    • man kann sie wie Arrays behandeln
      char s[] = "xyz";
    • man kann sie wie char*-Pointer behandeln
      char* s = "xyz";
  • Länge ergibt sich nur über das '\0' am Ende
Pointer-Style
char* s = "pr3 rocks";
char* p;

for (p = s; *p; p++) {
    printf("%c", *p);
}
Array-Style
char s[] = "pr3 rocks";
int i;

for (i = 0; s[i] != '\0'; i++) {
    printf("%c", s[i]);
}

Das Konstrukt *p in der while-Schleife mag auf den ersten Blick erstaunlich erscheinen, es funktioniert aber, weil in C das Ende eines Strings mit einem Null-Byte (0x00) angezeigt wird und 0 grundsätzlich false ist.

Strings kopieren

Will man in C einen String kopieren, kann man dies nicht einfach durch eine Zuweisung machen. Denn bei einer Zuweisung würden die Pointer kopiert und nicht der Inhalt. Dieses Verhaltens entspricht dem von Java, nur dass dies dort nicht auffällt, weil Strings unveränderlich sind und sich deshalb nicht erkennen lässt, dass beide Variablen auf dasselbe Objekt zeigen.

  • Zuweisung s = t
    • kopiert den Pointer, nicht den Inhalt (→ Java)
    • zwei Pointer zeigen auf denselben String (→ Java)
  • Kopieren von Strings mit strcpy (t, s)
    • Speicher für Ziel-String muss vorher angelegt seine
    • Speicher muss groß genug sein (Vorsicht)
  • Kopieren von Strings mit strdup (s)
    • legt den Speicher selbständig an
    • gibt Pointer auf den neuen String zurück
    • Speicher muss mit free () freigegeben werden
char s[] = "pr3 rocks";

/* Pointer wird kopiert */
char* t = s;
printf("t=%p\n", t); /* t=0x7fff5fa4179e */
printf("s=%p\n", s); /* s=0x7fff5fa4179e */

/* Inhalt wird kopiert */
t = strdup(s);
printf("t=%p\n", t); /* t=0x7f7f524027d0 */
printf("s=%p\n", s); /* s=0x7fff5fa4179e */

strcpy(t, "tpe rocks"); /* es gibt bessere Funktionen */
printf("t=%p\n", t); /* t=0x7f7f524027d0 */
printf("%s\n", t);   /* tpe rocks */

free(t); /* Speicher wieder freigeben */

Das Beispiel verwendet zwar die Funktion strcpy, diese sollte man aber in der Realität nicht einsetzen, weil sie keine Überprüfung der Länge durchführt. Beim Kopieren eines Strings in einen Buffer, sollte immer die Funktion strncopy eingesetzt werden, der man zusätzlich noch die Länge des Puffers mitgeben kann.

#define BUFFER_SIZE 20
char buffer[BUFFER_SIZE];
char s[] = "pr3 rocks";

strncpy(buffer, s, BUFFER_SIZE);

/* Wenn der string genau BUFFER_SIZE lang oder länger ist, schreibt
   strncpy kein NUL ans Ende. Deswegen setzen wir dies manuell,
   sodass der String auf jeden Fall terminiert ist */
buffer[BUFFER_SIZE - 1] = '\0';

String-Funktionen

C lässt einen nicht ganz alleine, sondern bietet für den Umgang mit C-Strings eine Reihe von Funktionen an, die die wichtigsten Aufgaben im Umgang mit Strings lösen.

string.h enthält eine Reihe von String-Funktionen

  • #include <string.h>
  • Strings müssen NULL-terminiert sein
  • alle Zielarrays müssen groß genug sein → keine Prüfung bei den Standardfunktionen

Einige gängige Funktionen sind

  • char *strcpy(char *dest, char *source)
    Kopiert source nach dest
  • char *strlcpy(char *dest, char *source, int num)
    Kopiert source nach dest aber maximal num Zeichen
  • size_t strlen(const char *source)
    Länge des Strings (ohne NUL)
  • char *strchr(const char *source, const char ch)
    Pointer auf erstes Auftreten von ch in source
  • char *strstr(const char *source, const char *search)
    Pointer auf erstes Auftreten von search in source

Beispiel für die Funktionen:

char s[] = "pr3 ist super!";

printf("Länge: %lu\n", strlen(s));    /* Länge: 14 */

char* zahl = strchr(s, '3');
printf("Index '3': %ld\n", zahl - s); /* Index '3': 2 */
printf("Reststring: %s\n", zahl);     /* Reststring: 3 ist super! */

char* super = strstr(s, "super");
printf("Index: %ld\n", super - s);    /* Index: 8 */
printf("Reststring: %s\n", super);    /* Reststring: super! */

String-Formatierung

Umwandlung von Strings ↔ Daten mit

  • sscanf: int sscanf(char *string, char *format, ...)
    • liest den Inhalt des Strings entsprechend format
    • schreibt die Ergebnisse in das 3., 4., 5. etc. Argument
    • gibt die Anzahl der erfolgreichen Umwandlungen zurück
  • sprintf: int sprintf(char *buffer, char *format, ...)
    • erzeugt einen String entsprechend format
    • formatiert 3., 4., 5. etc. Argument
    • schreibt den String in den Puffer (Größe!)
    • gibt die Anzahl der erfolgreichen Umwandlungen zurück

Auch sprintf sollte heute nicht mehr verwendet werden, sondern snprintf, das eine Längenprüfung zulässt.

Es gibt neben den sehr vielseitigen sscanf und sprintf-Funktionen noch einfachere Umwandlungen in Zahlen mit den atoX-Funktionen:

  • int atoi(const char *nptr);
  • long atol(const char *nptr);
  • long long atoll(const char *nptr);
  • double atof(const char *nptr);

Die Funktionsweise dürfte sich aus den Signaturen ergeben.

Die String-Formatierung bzw. Umwandlung wird in C durch sogenannte Formatstrings gesteuert, die eine Reihe von speziellen Zeichen enthalten, die man am % erkennt.

Codes für den Format-String von sscanf (Auswahl)

Code Bedeutung Datentyp
%c einzelnes Zeichen char
%d Ganzzahl int
%f Fließkommazahl float
%s String char*
%[^c] alles bis um nächsten Zeichen c char*
Beispiel: sscanf
char* message = "Hallo Freunde";
char* zahlen = "1 2 3 4";
char s[10], t[10];
int a, b, c, d;

sscanf(message, "%s %s", s, t);
printf("s=%s\n", s); /* Hallo */
printf("t=%s\n", t); /* Freunde */

sscanf(zahlen, "%d %d %d %d", &a, &b, &c, &d);
printf("%d-%d-%d-%d\n", a, b, c, d); /* 1-2-3-4 */

Codes für sprintf

  • Codes für den Format-String von sprintf (Auswahl)
  • Syntax: %[width][.precision][length]type
Code Bedeutung Datentyp
%nc Zeichen mit n Leerzeichen char
%nd Integer mit n Leerzeichen int
%nld Long mit n Leerzeichen long
%n.mf Fließkommazahl mit n, m float, double
%n.ms m Zeichen eines Strings der Breite n char*
%p Pointer-Adresse void*
Beispiel sprintf
char s[] = "Text1";
char t[] = "Text2";
char buffer[255];

sprintf(buffer, "%10s %-10s %s", s, t, "Huhu");
printf("%s\n", buffer);

sprintf(buffer, "%10.2f", 21.12345678);
printf("%s\n", buffer);
Ausgabe
     Text1 Text2      Huhu
     21.12

Der POSIX-Standard erweitert die Format-Strings um ein sogenanntes Parameter-Feld, mit dem man die jeweiligen Parameter direkt über ihren Index (beginnend bei 1) adressieren kann.

Da POSIX sich nur auf Unix-Betriebssysteme bezieht, findet man diese Erweiterung unter Windows in einer speziellen Funktion printf_p.

POSIX-Erweiterung: Parameter-Feld

  • Syntax: %n$[width][.precision][length]type
  • adressiert den Parameter mit dem Index n, beginnend bei 1
  • unter Windows printf_p-Funktion
printf("%2$d %1$d %2$d %1$d", 1, 2);
Ausgabe
2 1 2 1

Anzahl der geschriebenen Zeichen

  • Syntax: %n
  • Schreibt die Anzahl der bisher ausgegebenen Zeichen in die Variable
int count;
printf("012345678%n", &count);
printf(" -> %d", count);
Ausgabe
012345678 -> 9
Achtung: Öffnet das Tor für diverse Angriffe

Die Möglichkeit, durch das %n-Formatzeichen eine Schreiboperation auszulösen, eröffnet die Möglichkeit über Fehler im Formatstring erhebliche Sicherheitslücken aufzureißen. Außerdem macht es den Format-String von printf zu einer vollständigen Programmiersprache (turing-vollständig).

Der Gewinner des International Obfuscated C Code Contest 2020 hat ein Tic-Tac-Toe nur als Formatstring unter Verwendung von %n implementiert.

Gewinner des International Obfuscated C Code Contest 2020
#include <stdio.h>

#define N(a)       "%"#a"$hhn"
#define O(a,b)     "%10$"#a"d"N(b)
#define U          "%10$.*37$d"
#define G(a)       "%"#a"$s"
#define H(a,b)     G(a)G(b)
#define T(a)       a a
#define s(a)       T(a)T(a)
#define A(a)       s(a)T(a)a
#define n(a)       A(a)a
#define D(a)       n(a)A(a)
#define C(a)       D(a)a
#define R          C(C(N(12)G(12)))
#define o(a,b,c)   C(H(a,a))D(G(a))C(H(b,b)G(b))n(G(b))O(32,c)R
#define SS         O(78,55)R "\n\033[2J\n%26$s";
#define E(a,b,c,d) H(a,b)G(c)O(253,11)R G(11)O(255,11)R H(11,d)N(d)O(253,35)R
#define S(a,b)     O(254,11)H(a,b)N(68)R G(68)O(255,68)N(12)H(12,68)G(67)N(67)

char* fmt = O(10,39)N(40)N(41)N(42)N(43)N(66)N(69)N(24)O(22,65)O(5,70)O(8,44)N(
            45)N(46)N    (47)N(48)N(    49)N( 50)N(     51)N(52)N(53    )O( 28,
            54)O(5,        55) O(2,    56)O(3,57)O(      4,58 )O(13,    73)O(4,
            71 )N(   72)O   (20,59    )N(60)N(61)N(       62)N (63)N    (64)R R
            E(1,2,   3,13   )E(4,    5,6,13)E(7,8,9        ,13)E(1,4    ,7,13)E
            (2,5,8,        13)E(    3,6,9,13)E(1,5,         9,13)E(3    ,5,7,13
            )E(14,15,    16,23)    E(17,18,19,23)E(          20, 21,    22,23)E
            (14,17,20,23)E(15,    18,21,23)E(16,19,    22     ,23)E(    14, 18,
            22,23)E(16,18,20,    23)R U O(255 ,38)R    G (     38)O(    255,36)
            R H(13,23)O(255,    11)R H(11,36) O(254    ,36)     R G(    36 ) O(
            255,36)R S(1,14    )S(2,15)S(3, 16)S(4,    17 )S     (5,    18)S(6,
            19)S(7,20)S(8,    21)S(9    ,22)H(13,23    )H(36,     67    )N(11)R
            G(11)""O(255,    25 )R        s(C(G(11)    ))n (G(          11) )G(
            11)N(54)R C(    "aa")   s(A(   G(25)))T    (G(25))N         (69)R o
            (14,1,26)o(    15, 2,   27)o   (16,3,28    )o( 17,4,        29)o(18
            ,5,30)o(19    ,6,31)o(        20,7,32)o    (21,8,33)o       (22 ,9,
            34)n(C(U)    )N( 68)R H(    36,13)G(23)    N(11)R C(D(      G(11)))
            D(G(11))G(68)N(68)R G(68)O(49,35)R H(13,23)G(67)N(11)R C(H(11,11)G(
            11))A(G(11))C(H(36,36)G(36))s(G(36))O(32,58)R C(D(G(36)))A(G(36))SS

#define arg d+6,d+8,d+10,d+12,d+14,d+16,d+18,d+20,d+22,0,d+46,d+52,d+48,d+24,d\
            +26,d+28,d+30,d+32,d+34,d+36,d+38,d+40,d+50,(scanf(d+126,d+4),d+(6\
            -2)+18*(1-d[2]%2)+d[4]*2),d,d+66,d+68,d+70, d+78,d+80,d+82,d+90,d+\
            92,d+94,d+97,d+54,d[2],d+2,d+71,d+77,d+83,d+89,d+95,d+72,d+73,d+74\
            ,d+75,d+76,d+84,d+85,d+86,d+87,d+88,d+100,d+101,d+96,d+102,d+99,d+\
            67,d+69,d+79,d+81,d+91,d+93,d+98,d+103,d+58,d+60,d+98,d+126,d+127,\
            d+128,d+129

char d[538] = {1,0,10,0,10};

int main(int argc, char** argv) {
    while(*d) printf(fmt, arg);
}

Sichere String-Funktionen

Stringverarbeitung sollte nur mit Funktionen erfolgen, welche die Länge des Zielpuffers prüfen, wie z. B. snprintf, anstelle von sprintf. Diese sicheren String-Funktionen bieten einen Schutz vor Pufferüberläufen, die auftreten können, wenn der Ausgabepuffer nicht groß genug ist, um den gesamten generierten String aufzunehmen. Funktionen wie snprintf stellen sicher, dass der generierte String in den angegebenen Puffer passt und keinen Speicher außerhalb des Puffers überschreibt.

Man sollte nur sichere String-Funktionen verwenden

  • strcpystrlcpy
  • strcatstrncat
  • strcmpstrncmp
  • sprintfsnprintf
  • strlenstrnlen

Tatsächlich ist die sichere Variante von strcpy nicht strncpy, sondern strlcpy. Der Grund liegt darin, dass strncpy Strings erzeugt, die nicht nullterminiert sind, wenn der Eingabestring n oder mehr Zeichen enthält. Damit laufen dann andere Funktionen, wie z. B. strlen über das Ende des Strings hinaus, weil der Nullterminator fehlt.


Copyright © 2025 Thomas Smits