String-Objekte II
In diesem Kapitel sehen wir uns die Klassen string und string_view der Standardbibliothek detaillierter an. Im Folgenden wird davon ausgegangen, dass die im Kapitel String-Objekte I aufgeführten Operationen bekannt sind.
Bisher haben wir der Einfachheit halber stets Objekte der Klasse string verwendet. Diese Klasse verwendet zur Speicherung der Zeichen intern den Datentyp char. Um auch Zeichen vom Datentyp wchar_t ablegen zu können, gibt es eine weitere Klasse, die Klasse wstring.
#include <string>
std::string charString;
std::wstring wcharString;
Da string und wstring die gleichen Eigenschaften und Schnittstellen besitzen, wird im Folgenden nicht mehr zwischen diesen beiden Typen unterschieden.
Speicherverwaltung
Zur Ermittlung des von einem String belegten Speichers stehen folgende Methoden zur Verfügung:
size_type size() const;
size_type length() const;
Liefert die Anzahl der im String abgelegten Zeichen zurück.
size_type capacity();
Liefert die Anzahl der Zeichen zurück, die in einem String abgelegt werden können, ohne dass erneut Speicher angefordert wird.
Sehen wir uns dazu die nachfolgenden Anweisungen sowie die dazugehörigen Ausgaben an.
#include <print>
#include <string>
using namespace std::string_literals;
int main()
{
std::string myString = "abcdefg"s;
std::println("string: {}, length: {}, capacity: {}",
myString, myString.length(),myString.capacity());
myString = "xyz"s;
std::println("string: {}, length: {}, capacity: {}",
myString, myString.length(),myString.capacity());
}
string: abcdefg, length: 7, capacity: 15
string: xyz, length: 3, capacity: 15
Wie zu sehen ist, verändert sich nicht die maximale Anzahl von Zeichen im String die ohne erneute Speicheranforderung abgelegt werden kann. Je nach Compiler können hier andere Werte stehen, aber die grundsätzliche Aussage bleibt die gleiche: Der von einem String einmal belegte Speicherplatz wird in der Regel aus Gründen der Ausführungsgeschwindigkeit nicht verringert.
Um die zeitintensive Speicheranforderung für eine Stringerweiterung zu minimieren, enthält die string Klasse die Methode reserve().
void reserve (size_type mem = 0);
Erhält im Parameter mem die Anzahl der Zeichen, für die mindestens Speicher zu reservieren ist. Wird für mem der Wert 0 eingesetzt, wird der vom String belegte Speicherplatz so bemessen, dass mindestens der aktuelle String darin Platz findet.
Ist also die maximale Länge eines Strings im Voraus bekannt, kann mithilfe dieser Methode gleich von Anfang an genügend Speicher reserviert werden und somit die zeitintensiven Speicheranforderungen vermieden werden.
string-Iteratoren
Bei einigen der nachfolgenden string-Operationen werden sogenannte Iteratoren eingesetzt. Iteratoren werden zwar erst später ausführlich behandelt, für den Augenblick können Sie sich unter einem Iterator eine Art Zeiger vorstellen, der auf eine beliebige Position innerhalb des Strings verweist.
Beginnen wir mit der Definition eines string-Iterators.
std::string::iterator iter;
Für die Initialisierung eines Iterators stehen u.a. die string-Methoden begin() und end() zur Verfügung. begin() liefert einen Iterator auf den Anfang des Strings und end() einen Iterator auf das (Ende+1) des Strings, d.h., begin() und end() definieren einen offenen Bereich [begin...end).
Um über einen Iterator auf ein Zeichen im String zuzugreifen, wird der Iterator, genauso wie ein Zeiger, dereferenziert. Mit den String-Iteratoren sind nur die Operationen Addition und Subtraktion sowie Vergleichsoperationen erlaubt.
#include <print>
#include <string>
using namespace std::string_literals;
int main()
{
std::string myString{"Ein Text"s};
// String zeichenweise ausgeben
for (auto iter = myString.begin();
iter != myString.end(); ++iter)
std::print("{},",*iter);
}
E,i,n, ,T,e,x,t,
Das Beispiel gibt den Inhalt des String-Objekts myString zeichenweise aus. Beachten Sie in der for-Schleife das Abbruch-Kriterium! end() liefert einen Iterator der hinter das letzte Zeichen im String verweist.
Mithilfe des Reverse-Iterators kann ein String rückwärts durchlaufen werden. Anstelle von begin() und end() sind dann die Methoden rbegin() und rend() zu verwenden. Um den Reverse-Iterator auf das vorhergehende Zeichen zu positionieren, ist der Iterator zu inkrementieren!
#include <print>
#include <string>
using namespace std::string_literals;
int main()
{
std::string myString{"Ein Text"s};
// String zeichenweise ausgeben
// Der Datentyp von riter ist
// std::string::reverse_iterator
for (auto riter = myString.rbegin();
riter != myString.rend(); ++riter)
std::print("{},",*riter);
}
t,x,e,T, ,n,i,E,
Verwenden Sie bei Iteratoren nach Möglichkeit die Präfix-Operatoren ++iter bzw. --iter und nicht die Suffix-Operatoren iter++ bzw. iter--. Die Suffix-Operatoren benötigen intern ein temporäres Iterator-Objekt, was sich negativ auf die Laufzeit auswirkt.
Alle bisher genannten Iteratoren lassen Lese- und Schreibzugriffe zu. Sollen nur Lesezugriffe erlaubt sein, wird ein const-Iterator eingesetzt. Um einen const-Iterator zu definieren, ist vor den entsprechenden begin() und end() bzw. rbegin() und rend() Methoden der Buchstabe 'c' zu setzen. So liefert die Methode cbegin() einen const-Iterator auf den Beginn des Strings zurück.
Iteratoren besitzen nach der Durchführung von string-Operationen, die eine erneute Speicheranforderung verursachen, einen nicht mehr gültigen Inhalt! Zu diesen Operationen gehören zum Beispiel alle Operationen, die die Länge eines Strings verändern.
Vergleichen von strings
Bei den meisten der nachfolgenden Methoden kann als Argument entweder ein const string& oder ein const char* angegeben werden. Damit die Methoden hier nicht zweimal aufgeführt werden müssen, wird bei solchen Methoden hierfür als Datentyp STR_CHR angegeben.
Erfordert eine Methode eine Positionsangabe, so ist diese stets 0-basierend.
Strings können mit bekannten Vergleichsoperatoren verglichen werden. Zusätzlich enthält die string-Klasse für Vergleiche die Methode compare().
int compare (STR_CHR s2) const;
Vergleicht den String s2 mit dem aktuellen String. Als Returnwert liefert compare() 0, wenn beide Strings identisch sind, <0 wenn s2 kleiner ist und >0, wenn s2 größer ist. Größer bzw. kleiner bezieht sich nicht auf die Länge des Strings, sondern es wird lexikografisch verglichen, d.h., "A" ist größer als "aaa".
int compare (size_type pos, size_type len, STR_CHR s2) const;
Vergleicht einen Teilstring mit der Länge len ab der Position pos mit dem zweiten String s2.
int compare (size_type pos, size_type len,
const string& s2, size_type pos2,
size_type len2);
Vergleicht einen Teilstring ab der Position pos und der Länge len mit dem zweiten Teilstring s2 und der Länge len2 ab der Position pos2.
int compare (size_type pos, size_type len, const char* pstr, size_type len2);
Vergleicht einen Teilstring ab der Position pos und der Länge len mit len2 Zeichen aus dem C-String pstr.
Das nachfolgende Beispiel zeigt einen möglichen Einsatz der compare() Methode auf. Es werden im String ab der Position 12 die nächsten 5 Zeichen mit dem im dritten Parameter angegebenen Text verglichen.
#include <print>
#include <string>
using namespace std::string_literals;
int main()
{
// Zu pruefender Text
std::string anrede("Sehr geehrte Frau Ypsilon"s);
// Pruefen ob Empfaenger maennlich/weiblich
if (anrede.compare(12,5," Frau") == 0)
std::println("Empfaenger ist weiblich");
else
std::println("Empfaenger ist maennlich");
}
Empfaenger ist weiblich
Suchen in strings
Zum Suchen von Zeichen und Strings in string-Objekten stehen über 40 verschiedene Methoden zur Verfügung. Wir werden nicht alle Methoden ansehen, sondern nur deren 'Grundformen'.
Ist ein gesuchtes Zeichen bzw. ein gesuchter String nicht im zu durchsuchenden String vorhanden, liefern die Methoden std::string::npos zurück.
size_type find (char ch, size_type pos = 0);
size_type rfind (char ch, size_type pos = npos);
Liefert die erste bzw. letzte Position ab pos zurück, an der das Zeichen ch auftritt.
#include <print>
#include <string>
using namespace std::string_literals;
int main()
{
// Zu durchsuchender String
std::string path("c:/corba/doc/readme.doc");
std::println("Durchsuche string '{}'",path);
// Suche nach erstem Slash
// string::size_type first = path.find('/');
// oder wieder einfacher
auto first = path.find('/');
std::println("Erster '/' an Position {}",first);
// Suche nach letztem Slash
auto last = path.rfind('/');
std::println("Letzter '/' an Position {}",last);
// Suche nach nicht vorhandenem Zeichen
if (path.find('X') == std::string::npos)
std::println("Zeichen X nicht gefunden");
else
std::println("Zeichen X gefunden");
}
Durchsuche string 'c:/corba/doc/readme.doc'
Erster '/' an Position 2
Letzter '/' an Position 12
Zeichen X nicht gefunden
size_type find (STR_CHR str, size_type pos = 0);
size_type rfind (STR_CHR str, size_type pos = npos);
Liefert die erste bzw. letzte Position ab pos zurück, an der die Zeichenfolge str auftritt.
size_type find_first_of (STR_CHR str, size_type pos = 0);
size_type find_first_not_of (STR_CHR str, size_type pos = 0);
Liefert die erste Position ab pos zurück, an der ein beliebiges Zeichen aus der Zeichenfolge str auftritt bzw. nicht mehr auftitt.
#include <print>
#include <string>
using namespace std::string_literals;
int main()
{
// Zeichenmengen definieren
std::string lowerChar("abcdefghijklmnopqrstuvwxyz");
std::string upperChar("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
std::string anyChar(lowerChar+upperChar);
// Zu durchsuchender String
std::string street("Winkelgasse 3");
std::println("Zu durchsuchender String '{}'",street);
// Suchen nach dem ersten Kleinbuchstaben
auto pos = street.find_first_of(lowerChar);
std::println("Erste Kleinbuchstaben an Position {}",pos);
// Suche nach dem ersten Nicht-Buchstaben
pos = street.find_first_not_of(anyChar);
std::println("Erster Nicht-Buchstabe an Position {}",pos);
}
Zu durchsuchender String 'Winkelgasse 3'
Erste Kleinbuchstaben an Position 1
Erster Nicht-Buchstabe an Position 11
size_type find_last_of (STR_CHR str, size_type pos = npos);
size_type find_last_not_of (STR_CHR str, size_type pos = npos);
Liefert die letzte Position ab der Position pos zurück, an der ein beliebiges Zeichen aus der Zeichenfolge str auftritt bzw. nicht mehr auftritt.
bool contains(STR_CHR str);
Prüft ob str im String vorhanden ist.
bool starts_with(STR_CHR str);
bool ends_with(STR_CHR str);
Prüft ob der String mit str beginnt bzw. endet.
#include <print>
#include <string>
using namespace std::string_literals;
int main()
{
// String mit Dateipfad
std::string path="c:/temp/file.dat"s;
// Prueft ob Laufwerksangabe vorhanden
if (path.contains(":/"s))
std::println("Laufwerksangabe gefunden.");
else
std::println("Laufwerksangabe nicht gefunden.");
// Prueft ob .bak Datei angegeben
if (path.ends_with(".bak"))
std::println(".bak-Datei angegeben.");
else
std::println("Keine .bak-Datei angegeben.");
}
Laufwerksangabe gefunden.
Keine .bak-Datei angegeben.
Teilstrings und char-Felder
string substr (size_type pos = 0, size_type count = npos);
Kopiert ab der Position pos count Zeichen. Enthält count den Wert npos, wird ab der Position pos der restliche String kopiert. Die Methode liefert einen string mit dem Teilstring zurück.
#include <print>
#include <string>
using namespace std::string_literals;
int main()
{
// Strings definieren
std::string street("Winkelgasse 13");
std::string lowerChar("abcdefghijklmnopqrstuvwxyz");
std::string upperChar("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
std::string anyChar(lowerChar+upperChar);
// Suche nach dem ersten 'Nicht-Buchstaben'
auto pos = street.find_first_not_of(anyChar);
// Strassennamen extrahieren
auto part1 = street.substr(0,pos);
// Hausnummer extrahieren
auto part2 = street.substr(pos+1);
std::println("Strasse: {}\nHausnummer: {}",part1,part2);
}
Strasse: Winkelgasse
Hausnummer: 13
const char* c_str();
Liefert einen const char-Zeiger auf den String. D.h. mithilfe dieser Funktion z.B. kann ein string an eine Funktion übergeben werden, die einen const char-Zeiger als Parameter erwartet.
size_type copy (char* dest, size_type count, size_type pos = 0);
Erhält im ersten Parameter dest einen Zeiger auf den Speicherbereich, in dem der Stringinhalt ab der Position pos abgelegt werden soll. Der Parameter count enthält die maximale Anzahl der zu kopierenden Zeichen. Als Returnwert liefert die Methode die tatsächliche Anzahl der kopierten Zeichen.
Die kopierte Zeichenfolge wird nicht mit einer '0' abgeschlossen. Soll der Inhalt des Feldes als C-String weiterverwendet werden, ist explizit die '0' hinzuzufügen (siehe nachfolgendes Beispiel).
Anhängen und Ersetzen in Strings
Soll ein einzelnes Zeichen oder ein C-String an einen String angefügt werden, kann dies mithilfe des Operators += erfolgen. Zusätzlich stehen folgende Methoden zur Verfügung:
string& append (size_type count, char ch);
Fügt count Zeichen ch an den String an.
string& append (STR_CHR str);
Fügt den String str an den String an.
string& append (const string& str, size_type pos, size_type count = npos);
Fügt aus dem String str ab der Position pos count Zeichen zum String hinzu.
#include <print>
#include <string>
using namespace std::string_literals;
int main()
{
std::string string1("eins zwei ");
std::string string2("drei vier fuenf");
// Kompletten string2 anfuegen
string1 += string2;
std::println("string1: {}",string1);
// 3 Punkte anhaengen
string1.append(3,'.');
std::println("string1: {}",string1);
// Teilstring aus string2 anfügen
string1.append(string2,5,4);
std::println("string1: {}",string1);
}
string1: eins zwei drei vier fuenf
string1: eins zwei drei vier fuenf...
string1: eins zwei drei vier fuenf...vier
string& replace (size_type pos, size_type count, STR_CHR str);
Ersetzt count Zeichen ab der Position pos durch den String str.
string& replace (size_type pos, size_type count,
STR_CHR str,
size_type pos2,
size_type count2 = npos );
Ersetzt count Zeichen ab der Position pos durch den String str ab der Position pos2 mit der Länge count2.
#include <print>
#include <string>
#include <cstring>
using namespace std::string_literals;
int main()
{
std::string gruss{"Hallo #"s};
std::string anrede("Frau Maier"s);
const char* morgen = "Guten Morgen";
std::println("gruss: {}",gruss);
// Nach Zeichen # suchen
auto pos = gruss.find('#');
// und durch den String anrede ersetzen
gruss.replace(pos,1,anrede);
// 'Hallo' durch den C-String morgen ersetzen
gruss.replace(0,5,morgen,std::strlen(morgen));
std::println("gruss: {}",gruss);
}
gruss: Hallo #
gruss: Guten Morgen Frau Maier
Einfügen und Löschen in Strings
string& insert (size_type pos, size_type count, char ch);
Fügt ab der Position pos count Zeichen ch ein.
string& insert (size_type pos, STR_CHR str);
Fügt ab der Position pos den String str ein.
#include <print>
#include <string>
using namespace std::string_literals;
int main()
{
// String definieren
std::string s1("eins zwei vier"s);
std::println("s1: {}",s1);
// Position von 'vier' suchen
// Datentyp von pos diesmal ausgeschrieben
std::string::size_type pos = s1.find("vier"s);
// und davor 'drei ' einfügen
s1.insert(pos,"drei ");
std::println("s1: {}",s1);
}
s1: eins zwei vier
s1: eins zwei drei vier
void clear();
Löscht den Inhalt des Strings. Ob dabei der bisher durch den String belegten Speicher freigegeben wird ist implementierungsabhängig.
string& erase (size_type pos = 0, size_type count = npos);
Löscht ab der Position pos count Zeichen. Wird für count der Wert npos eingesetzt, wird ab der Position pos der Rest des Strings gelöscht.
#include <print>
#include <string>
using namespace std::string_literals;
int main()
{
std::string s1("Einen wunderschoenen guten Morgen");
std::string toErase("wunderschoenen");
std::println("s1: {}",s1);
// Position des zu loeschenden Strings suchen
auto pos1 = s1.find(toErase);
// String nun entfernen
s1.erase(pos1,toErase.length()+1);
std::println("s1: {}",s1);
}
s1: Einen wunderschoenen guten Morgen
s1: Einen guten Morgen
Beenden wir damit die Übersicht über die Klasse string. Mehr zur string-Klasse finden Sie auf https://en.cppreference.com unter dem Stichwort basic_string. string selbst ist eine Spezialisierung von basic_string zur Verarbeitung von char Zeichen.
string_view Operationen
Der Einsatz eines string_view anstelle eines string ist immer dann sinnvoll, wenn nur eine Sicht auf einen bestehenden (C-)String benötigt wird.
Die meisten Methoden der Klasse string_view sind identisch mit denen der Klasse string, bis auf folgende drei Ausnahmen.
Erste Ausnahme: Die Klasse string_view enthält aus naheliegenden Gründen keine Methoden, die einen string modifizieren, wie z.B. append() oder replace().
Das obige Beispiel zur string-Methode substr() kann unter Verwendung eines string_view wie folgt umgeschrieben werden:
#include <print>
#include <string>
using namespace std::string_literals;
int main()
{
// Strings definieren
std::string street("Winkelgasse 13");
std::string lowerChar("abcdefghijklmnopqrstuvwxyz");
std::string upperChar("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
std::string anyChar(lowerChar+upperChar);
// Suche nach dem ersten 'Nicht-Buchstaben'
auto pos = street.find_first_not_of(anyChar);
// Strassennamen extrahieren
std::string_view part1 = street.substr(0,pos);
// Hausnummer extrahieren
std::string_view part2 = street.substr(pos+1);
std::println("Strasse: {}\nHausnummer: {}",part1,part2);
}
Strasse: Winkelgasse
Hausnummer: 13
Der Vorteil dieser Variante gegenüber der ursprünglichen Version ist, dass für part1 und part2 keine string-Objekte instanziiert werden; part1 und part2 enthalten hier nur eine Sicht auf die Teilstrings.
Die zweite Ausnahme betrifft die Konvertierung eines string_view in einen const char-Zeiger. Anstelle der string-Methode c_str() ist hierfür die string_view-Methode data() zu verwenden. Dabei ist zu beachten, dass der zurückgelieferte const char-Zeiger nicht unbedingt auf einen mit 0 abgeschlossenen C-String verweist.
// ptr1 verweist auf einen mit
// mit 0 abgeschlossenen C-String
std::string_view sv1 = "Hallo";
const char* ptr1 = sv1.data();
// ptr2 verweist hier auf einen
// nicht mit 0 abgeschlossenen String
char buffer[]{ 'A','B','C','\n' };
std::string_view sv2 = buffer;
const char* ptr2 = sv2.data();
Und die dritte Ausnahme betrifft die Methode substr(). Während die Methode der string-Klasse einen neuen Teilstring zurückliefert, liefert die substr() Methode der string_view Klasse 'nur' eine neue Sicht auf den Teilstring, d.h., es wird für den Teilstring kein string-Objekt instanziiert.
#include <print>
#include <string_view>
using namespace std::string_literals;
int main()
{
std::string_view adr1 = "http://www.anydomain.de"s;
std::string_view adr2 = "https://www.anotherdomain.eu"s;
std::println("Die Domain '{}' ist sicher: {}\n"
"\tund ist eine de-Domain: {}",
adr1, adr1.starts_with("https"),
adr1.ends_with(".de"));
std::println("Die Domain '{}' ist sicher: {}\n"
"\tund ist eine de-Domain: {}",
adr2, adr2.starts_with("https"),
adr2.ends_with(".de"));
}
Die Domain 'http://www.anydomain.de' ist sicher: false
und ist eine de-Domain: true
Die Domain 'https://www.anotherdomain.eu' ist sicher: true
und ist eine de-Domain: false
Übungen
string2_01:
Erstellen Sie mithilfe der string-Methoden einen Serienbrief.
Die Datei sbrief.txt enthält den Text für einen Serienbrief. Für die Anrede sind die Platzhalter # und % enthalten. Der Platzhalter # steht für das einzusetzende Geschlecht und der Platzhalter % für den Namen.
Bei der Ausgabe des Serienbriefs sind die Platzhalter entsprechend zu ersetzen. Zusätzlich ist nach jedem Punkt ein neuer Absatz zu beginnen umd am Schluss ist eine Grußformel anzufügen.
Lesen Sie die Datei ein und geben diese zur Kontrolle unverändert aus.
Erstellen Sie anschließend den Serienbrief, indem Sie die Steuerzeichen durch "Frau" und "Maier" ersetzen. Geben Sie den fertigen Serienbrief erneut aus.
=== Orignal-Text war ===
Hallo # %
schoen dass Sie diese Uebung durchfuehren wollen. # %, wenn Sie diese Uebung richtig durchgefuehrt haben, dann sollte jetzt Ihr Name, # %, im Text erscheinen.
=== Serienbrief ist ===
Hallo Frau Maier
schoen dass Sie diese Uebung durchfuehren wollen.
Frau Maier, wenn Sie diese Uebung richtig durchgefuehrt haben, dann sollte jetzt Ihr Name, Frau Maier, im Text erscheinen.
Mit freundlichen Gruessen
string2_02:
Erstellen Sie eine Klasse SplitFullPath, die einen vollqualifizierten Dateinamen, bestehend aus Laufwerk, Pfad und Dateiname, in seine Einzelteile aufspaltet.
Die Angabe des Dateinamens soll dabei als C-String wie auch als string-Objekt möglich sein.
Definieren Sie Methoden, die das Laufwerk, den Pfad sowie den Dateinamen zurückliefern.
Extrahieren Sie aus der Dateiangabe 'c:/userdata/temp/data.dat' das Laufwerk, den Pfad und den Dateinamen.
Ersetzen Sie im Anschluss daran im Dateinamen die ursprüngliche Erweiterung .dat durch die Erweiterung .bak.
Da die Dateiangabe in der Klasse SplitFullPath nur analysiert und nicht modifiziert wird, bietet es sich an, entsprechende string_view für das Laufwerk usw. zu verwenden.
Der zu zerlegende Dateiname ist:
c:/userdata/temp/data.dat
Laufwerk : c:
Verzeichnis: /userdata/temp
Dateiname : data.dat
Tausche Erweiterung gegen ".bak"
Neuer Name : data.bak