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
auswchar.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 einNUL
('\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 dasNUL
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 behandelnchar* s = "xyz";
- Länge ergibt sich nur über das
'\0'
am Ende
char* s = "pr3 rocks";
char* p;
for (p = s; *p; p++) {
printf("%c", *p);
}
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)
Kopiertsource
nachdest
char *strlcpy(char *dest, char *source, int num)
Kopiertsource
nachdest
aber maximalnum
Zeichensize_t strlen(const char *source)
Länge des Strings (ohne NUL)char *strchr(const char *source, const char ch)
Pointer auf erstes Auftreten vonch
insource
char *strstr(const char *source, const char *search)
Pointer auf erstes Auftreten vonsearch
insource
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* |
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* |
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);
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 bei1
- unter Windows
printf_p
-Funktion
printf("%2$d %1$d %2$d %1$d", 1, 2);
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);
012345678 -> 9
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.
#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
strcpy
→strlcpy
strcat
→strncat
strcmp
→strncmp
sprintf
→snprintf
strlen
→strnlen
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.