C -Programmierung/ Weitere Grundelemente/ Zeichenketten
Einleitung
[Bearbeiten]In C gibt es keinen eingebauten Datentyp für Zeichenketten, lediglich einen für einzelne Zeichen. Da es in C noch keine Klassen gab, bediente man sich dort der einfachsten Möglichkeit, aus Zeichen Zeichenketten zu bilden: Man legte einfach einen Array von Zeichen an.
C bietet eine komfortablere Lösung an: Die C -Standardbibliothek enthält eine Klasse namens string
. Um diese Klasse nutzen zu können, müssen Sie die gleichnamige Headerdatei string
einbinden.
Wir werden uns in diesem Kapitel mit der C -Klasse string
auseinandersetzen. Am Ende des Kapitels beleuchten wir den Umgang mit C-Strings (also char
-Arrays) etwas genauer. Natürlich liegt auch string
, wie alle Teile der Standardbibliothek, im Namensraum std
.
Wie entsteht ein string
-Objekt?
[Bearbeiten]Zunächst sind einige Worte zur Notation von Zeichenketten in doppelten Anführungszeichen nötig. Wie Ihnen bereits bekannt ist, werden einzelne Zeichen in einfachen Anführungszeichen geschrieben. Dieser Zeichenliteral ist dann vom Typ char
. Die doppelten Anführungszeichen erzeugen hingegen eine Instanz eines char
-Arrays. "Hallo Welt!"
ist zum Beispiel vom Typ char[12]
.
Es handelt sich also um eine Kurzschreibweise, zum Erstellen von char
-Arrays, damit Sie nicht {'H', 'a', 'l', 'l', 'o', ' ', 'W', 'e', 'l', 't', '!', '\0'
} schreiben müssen, um eine einfache Zeichenkette zu erstellen. Was ist das '\0'
und warum ist das Array 12 char
s lang, obwohl es nur 11 Zeichen enthält?
Wie bereits erwähnt, ist ein C-String ein Array von Zeichen. Da ein solcher C-String natürlich im Programmablauf Zeichenketten unterschiedlicher Längen enthalten konnte, beendete man die Zeichenkette durch ein Endzeichen: '\0'
(Zahlenwert 0). Somit musste ein Array von Zeichen in C immer ein Zeichen länger sein, als die längste Zeichenkette, die im Programmverlauf darin gespeichert wurde.
Diese Kurzschreibweise kann aber noch mehr, als man auf den ersten Blick vermuten würde. Die eben genannte lange Notation zur Initialisierung eines Arrays funktioniert im Quelltext nur, wenn der Compiler auch weiß, von welchem Datentyp die Elemente des Arrays sein sollen. Da Zeichenliterale jedoch implizit in größere integrale Typen umgewandelt werden können, kann er den Datentyp nicht vom Typ der Elemente, die für die Initialisierung genutzt wurden ableiten:
#include <string>
int main() {
// char-Array mit 12 Elementen
char a[] = {'H', 'a', 'l', 'l', 'o', ' ', 'W', 'e', 'l', 't', '!', '\0'};
// int-Array mit 12 Elementen
int b[] = {'H', 'a', 'l', 'l', 'o', ' ', 'W', 'e', 'l', 't', '!', '\0'};
// char-Array mit 12 Elementen
std::string z = {'H', 'a', 'l', 'l', 'o', ' ', 'W', 'e', 'l', 't', '!', '\0'};
}
Bei der Notation mit Anführungszeichen ist dagegen immer bekannt, dass es sich um ein char
-Array handelt. Entsprechend ist die Initialisierung eines int
-Arrays damit nicht möglich. Folgendes dagegen schon:
#include <string>
int main() {
// char-Array mit 12 Elementen
char a[] = "Hallo Welt!";
// char-Array mit 12 Elementen
std::string z = "Hallo Welt!";
}
Bei der Erzeugung eines string
-Objekts wird eine Funktion aufgerufen, die sich Konstruktor nennt. Was genau ein Konstruktor ist, erfahren Sie im Kapitel über Klassen. In unserem Fall wird also der Konstruktor für das string
-Objekt z
aufgerufen. Als Parameter erhält er das char
-Array "Hallo Welt!"
. Wie Ihnen bereits bekannt ist, können an Funktionen keine Arrays übergeben werden. Stattdessen wird natürlich ein Zeiger vom Arrayelementtyp (also char
) übergeben. Dabei geht aber die Information verloren, wie viele Elemente dieses Array enthält und an dieser Stelle kommt das '\0'
-Zeichen (Nullzeichen) ins Spiel. Anhand dieses Zeichens kann auch innerhalb des Konstruktors erkannt werden, wie lang die übergebene Zeichenkette ist.
Damit wissen Sie nun, wie aus dem einfachen char
-Array das fertige string
-Objekt wird. Jetzt ist es an der Zeit zu erfahren, was Sie mit diesem Objekt alles machen können.
string
und andere Datentypen
[Bearbeiten]Wie Sie bereits im Beispiel von eben gesehen haben, lässt sich die string
-Klasse problemlos mit anderen Datentypen und Klassen kombinieren. Im ersten Beispiel dieses Kapitels wurde zunächst eine Zuweisung eines char
-Arrays vorgenommen. Anschließend wurde das string
-Objekt über cout
ausgegeben. Auch die Eingabe einer Zeichenkette über cin
ist mit einem string
-Objekt problemlos möglich:
#include <iostream>
#include <string>
int main() {
std::string zeichenkette;
std::cin >> zeichenkette;
std::cout << zeichenkette;
}
Diese Art der Eingabe erlaubt es lediglich, bis zum nächsten Whitespace einzulesen. Es kommt jedoch häufig vor, dass man eine Zeichenkette bis zum Zeilenende oder einem bestimmten Endzeichen einlesen möchte. In diesem Fall ist die Funktion getline
hilfreich. Sie erwartet als ersten Parameter einen Eingabestream und als zweiten ein string
-Objekt.
#include <iostream>
#include <string>
int main() {
std::string zeichenkette;
// Liest bis zum Zeilenende
std::getline(std::cin, zeichenkette);
std::cout << zeichenkette;
}
Als optionalen dritten Parameter kann man das Zeichen angeben, bis zu dem man einlesen möchte. Im Fall von eben wurde als der Default-Parameter '\n'
(Newline-Zeichen) benutzt. Im folgenden Beispiel wird stattdessen bis zum ersten kleinen y eingelesen.
#include <iostream>
#include <string>
int main() {
std::string zeichenkette;
// Liest bis zum nächsten y
std::getline(std::cin, zeichenkette, 'y');
std::cout << zeichenkette;
}
Zuweisen und Verketten
[Bearbeiten]Genau wie die Basisdatentypen, lassen sich auch string
s einander zuweisen. Für die Verkettung von string
s wird der
-Operator benutzt und das Anhängen einer Zeichenkette ist mit =
möglich.
#include <iostream>
#include <string>
int main() {
std::string string1, string2, string3;
string1 = "ich bin ";
string2 = "klug";
string3 = string1 string2;
std::cout << string3 << std::endl;
string3 = " - " string1 "schön";
std::cout << string3 << std::endl;
std::cout << string1 "schön und" string2 << std::endl;
}
ich bin klug
ich bin klug - ich bin schön
ich bin schön und klug
Spielen Sie einfach ein wenig mit den Operatoren, um den Umgang mit ihnen zu lernen.
Nützliche Methoden
[Bearbeiten]Die string
-Klasse stellt einige nützliche Methoden bereit. Etwa um den String mit etwas zu füllen, ihn zu leeren oder über verschiedene Eigenschaften Auskunft zu bekommen. Eine Methode wird mit folgender Syntax aufgerufen:
Die Methoden size()
und length()
erwarten keine Parameter und geben beide die aktuelle Länge der gespeicherten Zeichenkette zurück. Diese Doppelung in der Funktionalität existiert, da string
in Analogie zu den anderen Containerklassen der C -Standardbibliothek size()
anbieten muss, der Name length()
für die Bestimmung der Länge eines Strings aber natürlicher und auch allgemein üblich ist. empty()
gibt true
zurück falls der String leer ist, andernfalls false
.
Mit clear()
lässt sich der String leeren. Die resize()
-Methode erwartet ein oder zwei Parameter. Der erste ist die neue Größe des Strings, der zweite das Zeichen, mit dem der String aufgefüllt wird, falls die angegebene Länge größer ist, als die aktuelle. Wird der zweite Parameter nicht angegeben, wird der String mit '\0'
(Nullzeichen) aufgefüllt. In der Regel werden Sie dieses Verhalten nicht wollen, geben Sie also ein Füllzeichen an, falls Sie sich nicht sicher sind, was Sie tun. Ist die angegebene Länge geringer, als die des aktuellen Strings, wird am Ende abgeschnitten.
Um den Inhalt zweier Strings auszutauschen existiert die swap()
-Methode. Sie erwartet als Parameter den String mit dem ihr Inhalt getauscht werden soll. Dies ist effizienter, als das Vertauschen über eine dritte, temporäre string-Variable.
#include <iostream>
#include <string>
int main() {
std::string zeichenkette1 = "Ich bin ganz lang!";
std::string zeichenkette2 = "Ich kurz!";
std::cout << zeichenkette1 << std::endl;
std::cout << zeichenkette2 << std::endl;
zeichenkette1.swap(zeichenkette2);
std::cout << zeichenkette1 << std::endl;
std::cout << zeichenkette2 << std::endl;
}
Ich bin ganz lang!
Ich kurz!
Ich kurz!
Ich bin ganz lang!
Zeichenzugriff
[Bearbeiten]Genau wie bei einem Array können Sie den []
-Operator (Zugriffsoperator) verwenden, um auf einzelne Zeichen im String zuzugreifen. Allerdings wird, ebenfalls genau wie beim Array, nicht überprüft, ob der angegebene Wert noch innerhalb der enthaltenen Zeichenkette liegt.
Alternativ existiert die Methode at()
, die den Index als Parameter erwartet und eine Grenzprüfung ausführt. Im Fehlerfall löst sie eine out_of_range
-Exception aus. Da Sie den Umgang mit Exceptions wahrscheinlich noch nicht beherrschen, sollten Sie diese Methode vorerst nicht einsetzen und stattdessen genau darauf achten, dass Sie nicht versehentlich über die Stringlänge hinaus zugreifen.
#include <iostream>
#include <string>
int main() {
std::string zeichenkette = "Ich bin ganz lang!";
std::cout << zeichenkette[4] << std::endl;
std::cout << zeichenkette.at(4) << std::endl;
std::cout << zeichenkette[20] << std::endl; // Ausgabe von Datenmüll
std::cout << zeichenkette.at(20) << std::endl; // Laufzeitfehler
}
b
b
terminate called after throwing an instance of 'std::out_of_range'
what(): basic_string::at
Abgebrochen
Die Fehlerausgabe kann bei Ihrem Compiler anders aussehen.
Beachten Sie beim Zugriff, dass das erste Zeichen den Index 0 hat. Das letzte Zeichen hat demzufolge den Index zeichenkette.length() - 1
.
Manipulation
[Bearbeiten]Suchen
[Bearbeiten]Die Methode find()
sucht das erste Vorkommen eines Strings und gibt die Startposition (Index) zurück. Der zweite Parameter gibt an, ab welcher Position des Strings gesucht werden soll.
Wird ein Substring nicht gefunden, gibt find()
den Wert std::string::npos
zurück.
Das Gegenstück zu find()
ist rfind()
. Es ermittelt das letzte Vorkommen eines Strings. Die Parameter sind die gleichen wie bei find()
.
Löschen
[Bearbeiten]Mit der Methode erase()
können Zeichen im String gelöscht werden. Der erste Parameter gibt den Startwert an. Zusätzlich kann man mit dem zweiten Parameter die Anzahl der Zeichen festlegen. Wird die Methode nur mit dem Startwert aufgerufen, löscht sie alle Zeichen ab dieser Position.
Ersetzen
[Bearbeiten]Sie können replace()
verwenden, um Strings zu ersetzen. Dafür benötigen Sie die Anfangsposition und die Anzahl der Zeichen, die anschließend ersetzt werden sollen.
Wie Sie sehen, verwenden wir find()
, um die Startposition zu ermitteln. Der zweite Parameter gibt die Länge an. Hier soll die alternative Schreibweise verdeutlicht werden; Sie müssen nicht eine zusätzliche Variable deklarieren, sondern können die std::string
-Klasse wie eine Funktion verwenden und über Rückgabewert auf die Methode length()
zugreifen. Im dritten Parameter spezifizieren Sie den String, welcher den ursprünglichen String zwischen der angegebenen Startposition
und Startposition Laenge
ersetzt.
Einfügen
[Bearbeiten]Die Methode insert()
erlaubt es Ihnen, einen String an einer bestimmten Stelle einzufügen.
Kopieren
[Bearbeiten]Mit der Methode substr()
kann man sich einen Zeichenketten-Teil zurückgeben lassen. Der erste Parameter gibt den Startwert an. Zusätzlich kann man mit dem zweiten Parameter noch die Anzahl der Zeichen festlegen. Wird die Methode nur mit dem Startwert aufgerufen, gibt sie alle Zeichen ab dieser Position zurück.
Das -0
soll verdeutlichen, dass der Startwert abgezogen werden muss, an dieser Stelle ist es natürlich überflüssig.
Vergleiche
[Bearbeiten]C -Strings können Sie, genau wie Zahlen, miteinander vergleichen. Was Gleichheit und Ungleichheit bei einem String bedeutet, wird Ihnen sofort klar sein. Sind alle Zeichen zweier Strings identisch, so sind beide gleich, andernfalls nicht. Die Operatoren <
, >
, <=
und >=
geben da schon einige Rätsel mehr auf.
Im Grunde kennen Sie die Antwort bereits. Zeichen sind in C eigentlich Zahlen. Sie werden zu Zeichen, indem den Zahlen entsprechende Symbole zugeordnet werden. Der Vergleich erfolgt also einfach mit den Zahlen, welche die Zeichen kodieren. Das erste Zeichen der Strings, das sich unterscheidet, entscheidet darüber, welcher der Strings größer bzw. kleiner ist.
Die meisten Zeichenkodierungen beinhalten in den ersten 7 Bit den ASCII-Code, welchen die nachfolgende Tabelle zeigt.
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
#include <string>
int main(){
std::string gross = "Ich bin ganz groß!";
std::string klein = "Ich bin ganz klein!";
gross == klein; // ergibt false ('g' != 'k')
gross != klein; // ergibt true ('g' != 'k')
gross < klein; // ergibt true ('g' < 'k')
gross > klein; // ergibt false ('g' < 'k')
gross <= klein; // ergibt true ('g' < 'k')
gross >= klein; // ergibt false ('g' < 'k')
}
Zahl zu string
und umgekehrt
[Bearbeiten]In C gibt es, im Gegensatz zu vielen anderen Programmiersprachen, keine Funktion, um direkt Zahlen in Strings oder umgekehrt umzuwandeln. Es ist allerdings nicht besonders schwierig, eine solche Funktion zu schreiben. Wir haben für die Umwandlung zwei Möglichkeiten:
- Die C-Funktionen
atof()
,atoi()
,atol()
undsprintf()
- C -String-Streams
Die C-Variante wird in Kürze im Zusammenhang mit C-Strings besprochen. Für den Moment wollen wir uns der C -Variante widmen. Stringstreams funktionieren im Grunde genau wie die Ihnen bereits bekannten Ein-/Ausgabestreams cin
und cout
mit dem Unterschied, dass sie ein string
-Objekt als Ziel benutzen.
#include <iostream> // Standard-Ein-/Ausgabe
#include <sstream> // String-Ein-/Ausgabe
int main() {
std::ostringstream strout; // Unser Ausgabe-Stream
std::string str; // Ein String-Objekt
int var = 10; // Eine ganzzahlige Variable
strout << var; // ganzzahlige Variable auf Ausgabe-Stream ausgeben
str = strout.str(); // Streaminhalt an String-Variable zuweisen
std::cout << str << std::endl; // String ausgeben
}
Der vorliegende Code wandelt eine Ganzzahl in einen String um, indem die Ganzzahl auf dem Ausgabe-Stringstream ausgegeben und dann der Inhalt des Streams an den String zugewiesen wird. Die umgekehrte Umwandlung funktioniert ähnlich. Natürlich verwenden wir hierfür einen Eingabe-Stringstream (istringstream
statt ostringstream
) und übergeben den Inhalt des Strings an den Stream, bevor wir ihn von diesem auslesen.
#include <iostream> // Standard-Ein-/Ausgabe
#include <sstream> // String-Ein-/Ausgabe
int main() {
std::istringstream strin; // Unser Eingabe-Stream
std::string str = "17"; // Ein String-Objekt
int var; // Eine ganzzahlige Variable
strin.str(str); // Streaminhalt mit String-Variable füllen
strin >> var; // ganzzahlige Variable von Eingabe-Stream einlesen
std::cout << var << std::endl; // Zahl ausgeben
}
Statt istringstream
und ostringstream
können Sie übrigens auch ein stringstream
-Objekt verwenden, welches sowohl Ein-, als auch Ausgabe erlaubt, allerdings sollte man immer so präzise wie möglich angeben, was der Code machen soll. Daher ist die Verwendung eines spezialisierten Streams zu empfehlen, wenn Sie nur die speziellen Fähigkeiten (Ein- oder Ausgabe) benötigen.
Sicher sind Sie jetzt bereits in der Lage, zwei Funktionen zu schreiben, welche diese Umwandlung durchführt. Allerdings stehen wir in dem Moment, wo wir andere Datentypen als int
in Strings umwandeln wollen vor einem Problem. Wir können der folgenden Funktion zwar ohne weiteres eine double
-Variable übergeben, allerdings wird dann der Nachkommateil einfach abgeschnitten. Als Lösung kommt Ihnen nun eventuell in den Sinn, einfach eine double
-Variable von der Funktion übernehmen zu lassen.
#include <iostream> // Standard-Ein-/Ausgabe
#include <sstream> // String-Ein-/Ausgabe
std::string zahlZuString(double wert) {
std::ostringstream strout; // Unser Ausgabe-Stream
std::string str; // Ein String-Objekt
strout << wert; // Zahl auf Ausgabe-Stream ausgeben
str = strout.str(); // Streaminhalt an String-Variable zuweisen
return str; // String zurückgeben
}
int main() {
std::string str;
int ganzzahl = 19;
double kommazahl = 5.55;
str = zahlZuString(ganzzahl);
std::cout << str << std::endl;
str = zahlZuString(kommazahl);
std::cout << str << std::endl;
}
19
5.55
Nun, so weit so gut. Das funktioniert. Leider gibt es da aber auch noch die umgekehrte Umwandlung und obgleich es möglich ist, sie auf ähnliche Weise zu lösen, wird Ihr Compiler sich dann ständig mit einer Warnung beschweren, wenn das Ergebnis Ihrer Umwandlung an eine ganzzahlige Variable zugewiesen wird.
Besser wäre es, eine ganze Reihe von Funktionen zu erzeugen, von denen jede für einen Zahlentyp verantwortlich ist. Tatsächlich können Sie in C mehrere Funktionen gleichen Namens erzeugen, die unterschiedliche Parameter(typen) übernehmen. Diese Vorgang nennt sich Überladen von Funktionen. Der Compiler entscheidet dann beim Aufruf der Funktion anhand der übergebenen Parameter, welche Version gemeint war, während der Programmierer immer den gleichen Namen verwendet.
Im Moment haben wir obendrein einen Sonderfall der Überladung. Alle unsere Funktionen besitzen exakt den gleichen Code. Lediglich der Parametertyp ist unterschiedlich. Es wäre ziemlich zeitaufwendig und umständlich, den Code immer wieder zu kopieren, um dann nur den Datentyp in der Parameterliste zu ändern. Noch schlimmer wird es, wenn wir eines Tages eine Änderung am Funktionsinhalt vornehmen und diese dann auf alle Kopien übertragen müssen.
Glücklicherweise bietet C für solche Fälle so genannte Templates, die es uns erlauben, den Datentyp vom Compiler ermitteln zu lassen. Wir teilen dem Compiler also mit, was er tun soll, womit muss er dann selbst herausfinden. Die Funktion zahlZuString()
(umbenannt in toString()
) sieht als Template folgendermaßen aus:
#include <iostream> // Standard-Ein-/Ausgabe
#include <sstream> // String-Ein-/Ausgabe
template <typename Typ>
std::string toString(Typ wert) {
std::ostringstream strout; // Unser Ausgabe-Stream
std::string str; // Ein String-Objekt
strout << wert; // Zahl auf Ausgabe-Stream ausgeben
str = strout.str(); // Streaminhalt an String-Variable zuweisen
return str; // String zurückgeben
}
int main() {
std::string str;
int ganzzahl = 19;
double kommazahl = 5.55;
std::string nochnString = "Blödsinn";
str = toString(ganzzahl);
std::cout << str << std::endl;
str = toString(kommazahl);
std::cout << str << std::endl;
str = toString(nochnString);
std::cout << str << std::endl;
}
19
5.55
Blödsinn
Die letzte Ausgabe zeigt deutlich warum die Funktion in toString
umbenannt wurde, denn sie ist nun in der Lage, jeden Datentyp, der sich auf einem ostringstream
ausgeben lässt, zu verarbeiten und dazu zählen eben auch string
-Objekte und nicht nur Zahlen. Sie werden später noch lernen, welches enorme Potenzial diese Technik in Zusammenhang mit eigenen Datentypen hat. An dieser Stelle sei Ihnen noch die Funktion zur Umwandlung von Strings in Zahlen (oder besser: alles was sich von einem istringstream
einlesen lässt) mit auf den Weg gegeben:
#include <iostream> // Standard-Ein-/Ausgabe
#include <sstream> // String-Ein-/Ausgabe
template <typename Typ>
void stringTo(std::string str, Typ &wert) {
std::istringstream strin; // Unser Eingabe-Stream
strin.str(str); // Streaminhalt mit String-Variable füllen
strin >> wert; // Variable von Eingabe-Stream einlesen
}
int main() {
std::string str = "7.65Blödsinn";
int ganzzahl;
double kommazahl;
std::string nochnString;
stringTo(str, ganzzahl);
std::cout << ganzzahl << std::endl;
stringTo(str, kommazahl);
std::cout << kommazahl << std::endl;
stringTo(str, nochnString);
std::cout << nochnString << std::endl;
}
7
7.65
7.65Blödsinn
Die Variable, die mit dem Wert des Strings belegt werden soll, wird als Referenz an die Funktion übergeben, damit der Compiler ihren Typ feststellen kann. Die Ausgabe zeigt, dass immer nur so viel eingelesen wird, wie der jeweilige Datentyp (zweiter Funktionsparameter) fassen kann. Für eine ganzzahlige Variable wird nur die Zahl Sieben eingelesen, die Gleitkommavariable erhält den Wert 7.65
und das string
-Objekt kann die gesamte Zeichenkette übernehmen.
C-Strings
[Bearbeiten]Wie bereits erwähnt, handelt es sich bei einem C-String um ein Array von char
s. Das Ende eines C-Strings wird durch ein Nullzeichen (Escape-Sequenz '\0'
) angegeben. Das Arbeiten mit C-Strings ist mühsam, denn es muss immer sichergestellt sein, dass das Array auch groß genug ist, um den String zu beinhalten. Da in C/C jedoch auch keine Bereichsüberprüfung durchgeführt wird, macht sich ein Pufferüberlauf (also eine Zeichenkette die größer ist als das Array, das sie beinhaltet) erst durch einen eventuellen Programmabsturz bemerkbar. Allein um dies zu vermeiden sollten Sie, wann immer es Ihnen möglich ist, die C -string-Klasse verwenden.
Ein weiteres Problem beim Umgang mit C-Strings ist der geringe Komfort beim Arbeiten. Ob Sie einen String mit einem anderen vergleichen wollen, oder ihn an ein anderes Array „zuweisen“ möchten, in jedem Fall benötigen Sie unintuitive Zusatzfunktionen. Diese Funktionen finden Sie in der Standardheaderdatei „cstring“. Wie diese Funktionen heißen und wie man mit ihnen umgeht können Sie im C -Referenz-Buch nachlesen, falls Sie sie einmal benötigen sollten.
Wenn Sie sich eingehender mit der Thematik auseinandersetzen möchten, sei Ihnen das Buch C-Programmierung ans Herz gelegt. Wenn Sie in C mit der C-Standard-Bibliothek arbeiten möchten, müssen Sie den Headerdateien ein „c“ voranstellen und das „.h“ weglassen. So wird beispielsweise aus dem C-Header „string.h“ der C -Header „cstring“.