Kapitel 4. Die Container-Bibliothek

Bist du ein Baum, ein Tier oder ein Mineral?
— Lewis Carroll, „Alice hinter den Spiegeln”

Eine der häufigsten Aufgaben in der Programmierung besteht darin, die Verarbeitung von Elementgruppen zu organisieren. Die Implementierung und das Debugging von Programmen mit solchen Datenstrukturen kostet Entwickler viel Zeit, da sie jedes Mal dieselben Aufgaben lösen müssen.

In diesem Kapitel betrachten wir die Container-Bibliothek von Qt6. Container sind Template-Klassen, die zum Speichern von Elementen eines Typs dienen. Sie bieten sichere und einfach zu verwendende Alternativen zu C-Arrays. Die hier besprochene Qt-Container-Bibliothek heißt Tulip.

Ein Container ist ein Objekt, das zum Speichern und Verwalten der darin enthaltenen Elemente dient. Er kümmert sich um die Speicherzuweisung und -freigabe sowie um das Hinzufügen und Entfernen von Elementen. Der Container „verbirgt” die Details der Speicherverwaltung vor dem Programmierer und bietet eine komfortable Schnittstelle für die Arbeit mit Daten.

Der Hauptzweck von Containern besteht darin, den Programmierer von der Notwendigkeit zu befreien, sich selbst um die Speicherzuweisung und -freigabe zu kümmern, und effiziente Algorithmen für die Arbeit mit Daten bereitzustellen. Dies ermöglicht es dem Entwickler, sich auf die Implementierung der Anwendung selbst zu konzentrieren und nicht in die Details der Implementierung der verwendeten Container-Klassen einzutauchen.

Die Tulip-Bibliothek ist Teil von Qt und wird dort aktiv verwendet. Diese Bibliothek ist nicht nur der STL (Standard Template Library) sehr ähnlich, sondern auch mit ihr kompatibel. Dabei kann der Entwickler frei wählen, was er verwenden möchte: Tulip oder STL. Ein zweites Argument für die Verwendung von Tulip kann die Aussage sein, dass diese Bibliothek entsprechend der Spezifik der Qt-Klassen hinsichtlich Leistung und Speicherverbrauch optimiert ist. Es ist erwähnenswert, dass die Verwendung von Template-Klassen, auf denen Container basieren, die Größe ausführbarer Programme deutlich erhöht. Dies liegt daran, dass sich in jeder Objektdatei der Klassenimplementierung, die Container verwendet, vom Compiler erstellter Code für Container mit den benötigten Typen befindet, und dieser Code kann sich wiederholen. Die Tulip-Bibliothek wurde genau unter Berücksichtigung dieser Umstände erstellt und ist außerdem für eine deutliche Reduzierung der Größe des Objektcodes optimiert.

Die Implementierung der Tulip-Klassen befindet sich im Modul QtCore. Die Basis der Tulip-Bibliothek (wie auch in STL) bilden drei Konzepte:

  • Container-Klassen (Container).
  • Algorithmen.
  • Iteratoren.

Ihre Beziehung ist in Abb. 4.1 dargestellt.

Diagramm der Beziehung zwischen Containern, Iteratoren und Algorithmen in der Qt6-Container-Bibliothek
Abb. 4.1: Beziehung zwischen Containern, Iteratoren und Algorithmen

Container-Klassen

Container-Klassen sind Klassen, die in der Lage sind, Elemente verschiedener Datentypen zu speichern. Fast alle Container-Klassen in Qt sind als Templates implementiert und können daher Daten jeden Typs speichern. Die Grundidee eines Templates besteht darin, eine generische Klasse zu erstellen, die bei der Erstellung eines Objekts dieser Klasse definiert wird. Container-Klassen können ganze Serien anderer Objekte enthalten, die wiederum auch Container sein können.

Um den richtigen Container für einen bestimmten Fall auszuwählen, ist es sehr wichtig, die Unterschiede zwischen den Container-Varianten richtig zu verstehen. Davon hängt in erheblichem Maße die Geschwindigkeit des Codes und die Effizienz der Speichernutzung ab. Qt bietet zwei Kategorien von Container-Klassen: sequenzielle (sequence containers) und assoziative (associative containers).

Sequenzielle Container sind geordnete Sammlungen, in denen jedes Element eine bestimmte Position einnimmt. Die Position eines Elements hängt von seiner Einfügestelle ab. Zu den sequenziellen Containern gehören: Liste (list), Stack (stack) und Warteschlange (queue). Dementsprechend enthält Qt drei Klassen dieser Kategorie:

  • QList — Liste.
  • QStack — Stack.
  • QQueue — Warteschlange.

Assoziative Container sind Sammlungen, in denen die Position eines Elements von seinem Wert abhängt, d. h. nach dem Eintragen von Elementen in die Sammlung wird ihre Reihenfolge durch ihre Werte bestimmt. Zu den assoziativen Containern gehören: Menge (set), Wörterbuch (map) und Hash (hash). Klassen dieser Kategorie:

  • QSet — Menge.
  • QMap<K,T> — Wörterbuch.
  • QMultiMap<K,T> — Multi-Wörterbuch.
  • QHash<K,T> — Hash.
  • QMultiHash<K,T> — Multi-Hash.

In allen Containern dieser beiden Gruppen sind die in Tabelle 4.1 aufgeführten Operationen verfügbar. Bitte beachten Sie einige Ausnahmen für die Klasse QSet.

Tabelle 4.1. Operatoren und Methoden, die in allen Container-Klassen definiert sind

Operator, Methode Beschreibung
== und != Vergleichsoperatoren, gleich und ungleich
= Zuweisungsoperator
[] Indexierungsoperator. Ausnahme ist nur die Klasse QSet — in ihr ist der Operator nicht definiert
begin() und constBegin() Methoden, die Iteratoren zurückgeben, die auf den Anfang der Elementsequenz des Containers gesetzt sind. Für die Klasse QSet werden nur konstante Iteratoren zurückgegeben
end() und constEnd() Methoden, die konstante Iteratoren zurückgeben, die auf das Ende der Elementsequenz des Containers gesetzt sind
clear() Löschen aller Elemente des Containers
insert() Operation zum Einfügen von Elementen in den Container
remove() Operation zum Entfernen von Elementen aus dem Container
size() und count() Beide Methoden sind identisch — geben die Anzahl der Elemente des Containers zurück, aber die Verwendung der ersten ist vorzuziehen, da sie STL entspricht
value() Gibt den Wert eines Container-Elements zurück. In QSet ist diese Methode nicht definiert
empty() und isEmpty() Geben true zurück, wenn der Container kein einziges Element enthält. Beide Methoden sind identisch, aber die Verwendung der ersten ist vorzuziehen, da sie STL entspricht

Iteratoren

Sicherlich werden Sie durch die Elemente des Containers navigieren müssen. Für diese Zwecke sind Iteratoren vorgesehen. Iteratoren ermöglichen es, von der Datenstruktur der Container zu abstrahieren, d. h., wenn Sie zu einem bestimmten Zeitpunkt entscheiden, dass die Verwendung eines anderen Container-Typs viel effizienter wäre, müssen Sie nur den Container-Typ durch den gewünschten ersetzen. Am restlichen Code, der Iteratoren verwendet, ändert sich nichts.

Qt bietet zwei Iterator-Stile:

  • Iteratoren im Java-Stil.
  • Iteratoren im STL-Stil.

Als moderner Ansatz wird empfohlen, Standard-Schleifen und die range-based for-Schleife (für C++11 und höher) zu verwenden.

Iteratoren im Java-Stil

Iteratoren im Java-Stil sind sehr einfach zu verwenden. Sie wurden speziell für Programmierer entwickelt, die keine Erfahrung mit STL-Containern haben. Ihr Hauptunterschied zu letzteren besteht darin, dass sie nicht auf das Element selbst zeigen, sondern auf seine beiden Nachbarn. So zeigt der Iterator zunächst auf die Position vor dem ersten Element des Containers, und mit jedem Aufruf der Methode next() (siehe Tabelle 4.2) wird der Zeiger um eine Position nach vorne verschoben. Aber in Wirklichkeit sind Iteratoren im Java-Stil Objekte und keine Zeiger. Ihre Verwendung macht in den meisten Fällen den Code kompakter als bei der Verwendung von Iteratoren im STL-Stil:

Listing 4.1. Iterator im Java-Stil

QList<QString> list = {"Within Temptation", "Night Rune", "Mantus"};
QListIterator<QString> it(list);
while(it.hasNext()) {
    qDebug() << "Element:" << it.next();
}

In Tabelle 4.2 sind die Methoden der Klasse QListIterator aufgeführt, die auch für die Klassen QHashIterator, QMapIterator und QSetIterator gelten. Diese Iteratoren sind konstant, daher sind das Ändern von Elementwerten, das Einfügen und Entfernen nicht möglich.

Speichern Sie in Containern Zeiger auf Objekte, die von der Klasse QObject erben
Es ist wichtig, nicht zu vergessen, dass Klassen, die von der Klasse QObject erben, keinen zugänglichen Kopierkonstruktor und Zuweisungsoperator haben, da diese sich im Bereich private befinden. Folglich können ihre Objekte nicht in Containern gespeichert werden, daher müssen Sie nicht die Objekte selbst, die von der Klasse QObject erben, in Containern speichern, sondern Zeiger auf sie. In Qt6 wird für solche Zwecke empfohlen, QPointer oder std::shared_ptr für die automatische Speicherverwaltung zu verwenden.
Verwenden Sie zum Überprüfen des Vorhandenseins von Elementen im Container die Methode empty()
Wenn Sie überprüfen müssen, ob ein Container Elemente enthält oder nicht, verwenden Sie dafür die Methode empty() und nicht die Methode size(). Der Grund ist einfach: Die Methode empty() ist für alle Container mit konstanter Komplexität vorgesehen, was die Geschwindigkeit Ihres Algorithmus erheblich steigern kann.

Tabelle 4.2. Methoden von QListIterator, QHashIterator, QMapIterator, QSetIterator

Methode Beschreibung
toFront() Bewegt den Iterator zum Anfang der Liste
toBack() Bewegt den Iterator zum Ende der Liste
hasNext() Gibt true zurück, wenn sich der Iterator nicht am Ende der Liste befindet
next() Gibt den Wert des nächsten Listenelements zurück und bewegt den Iterator zur nächsten Position
peekNext() Gibt einfach den nächsten Wert zurück, ohne die Position des Iterators zu ändern
hasPrevious() Gibt true zurück, wenn sich der Iterator nicht am Anfang der Liste befindet
previous() Gibt den Wert des vorherigen Listenelements zurück und bewegt den Iterator zur vorherigen Position
peekPrevious() Gibt einfach den vorherigen Wert zurück, ohne die Position des Iterators zu ändern
findNext(const T&) Suche nach einem gegebenen Element in Vorwärtsrichtung
findPrevious(const T&) Suche nach einem gegebenen Element in Rückwärtsrichtung

Wenn es notwendig ist, während des Durchlaufens der Elemente mit dem Iterator Änderungen vorzunehmen, sollten Sie änderbare (mutable) Iteratoren verwenden. Ihre Klassen heißen ähnlich, aber mit dem Zusatz Mutable: QMutableListIterator, QMutableHashIterator, QMutableSetIterator und QMutableMapIterator. Die Methode remove() löscht das aktuelle Element, und die Methode insert() fügt ein Element an der aktuellen Position ein. Mit der Methode setValue() kann einem Element ein anderer Wert zugewiesen werden.

Lassen Sie uns dem Listenelement “Boney M” den Wert “Rolling Stones” zuweisen:

Listing 4.2. Änderbarer (mutable) Iterator

QList<QString> list = {"Beatles", "ABBA", "Boney M"};
QMutableListIterator<QString> it(list);
while(it.hasNext()) {
    if (it.next() == "Boney M") {
        it.setValue("Rolling Stones");
    }
    qDebug() << it.peekPrevious();
}

Der Hauptnachteil von Iteratoren im Java-Stil besteht darin, dass ihre Verwendung in der Regel die Größe des erstellten Objektmoduls im Vergleich zur Verwendung von Iteratoren im STL-Stil deutlich erhöht, die wir uns jetzt ansehen werden.

Iteratoren im STL-Stil

Iteratoren im STL-Stil sind etwas effizienter als Iteratoren im Java-Stil und können zusammen mit STL-Algorithmen verwendet werden. Vielleicht ist dies für C++-Entwickler der vertrauteste Iterator-Typ. Iteratoren im STL-Stil können als allgemeine Zeiger dargestellt werden, die auf Container-Elemente verweisen.

Der Aufruf der Methode begin() von einem Container-Objekt gibt einen Iterator zurück, der auf sein erstes Element zeigt, und der Aufruf der Methode end() gibt einen Iterator zurück, der auf das Ende des Containers zeigt. Beachten Sie: genau auf das Ende des Containers, nicht auf das letzte Element, d. h. auf die Position, an der das nächste Element platziert werden könnte. Mit anderen Worten, dieser Iterator zeigt nicht auf ein Element, sondern dient nur dazu, das Erreichen des Endes des Containers anzuzeigen (Abb. 4.2).

Darstellung der Methoden begin() und end() sowie der aktuellen Iterator-Position in einem Qt6-Container
Abb. 4.2: Methoden begin(), end() und aktuelle Position

Die Operatoren ++ und -- des Iterator-Objekts bewirken das Verschieben zum nächsten bzw. vorherigen Element. Der Zugriff auf das Element, auf das der Iterator zeigt, kann mit der Dereferenzierungsoperation * erfolgen. Zum Beispiel:

Listing 4.3. STL-Iterator, Vorwärtsdurchlauf

QList<QString> lst = {"In Extremo", "Blackmore's Night", "Night Rune"};
QList<QString>::iterator it = lst.begin();
for (; it != lst.end(); ++it) {
    qDebug() << "Element:" << *it;
}

Auf dem Bildschirm wird Folgendes angezeigt:

Element: "In Extremo"
Element: "Blackmore's Night"
Element: "Night Rune"

Beachten Sie, dass zum Erhöhen des Iterators it in der Schleife die Präinkrement-Operation verwendet wird: ++it, was es ermöglicht, bei jedem Schleifendurchlauf das Speichern des alten Werts zu vermeiden, wie es bei der Inkrement-Operation verdeckt durchgeführt wird, und dies macht die Schleife effizienter.

Beim Durchlaufen von Elementen in umgekehrter Reihenfolge mit dem Operator -- muss man bedenken, dass er nicht symmetrisch zum Durchlaufen mit dem Operator ++ ist. Daher sollte die Schleife in diesem Fall wie folgt aussehen:

QList<QString>::iterator it = lst.end();
for (;it != lst.begin();) {
    --it;
    qDebug() << "Element:" << *it;
}

Auf dem Bildschirm wird Folgendes angezeigt:

Element: "Night Rune"
Element: "Blackmore's Night"
Element: "In Extremo"

Wenn Sie nur die Werte der Elemente abrufen möchten, ohne sie zu ändern, ist es viel effizienter, einen konstanten Iterator const_iterator zu verwenden. Dabei müssen Sie anstelle der Methoden begin() und end() die Methoden constBegin() und constEnd() verwenden. Somit nimmt unser Beispiel folgende Form an:

QList<QString> lst = {"In Extremo", "Blackmore's Night", "Night Rune"};
QList<QString>::const_iterator it = lst.constBegin();
for (; it != lst.constEnd(); ++it) {
    qDebug() << "Element:" << *it;
}

Bemerkenswert ist auch, dass diese Iteratoren mit Standard-STL-Algorithmen verwendet werden können, die in der Header-Datei algorithm definiert sind. Zum Beispiel kann zum Sortieren einer Liste mit dem STL-Algorithmus sort() wie folgt vorgegangen werden:

QList<QString> lst = {"In Extremo", "Blackmore's Night", "Night Rune"};
std::sort(lst.begin(), lst.end());
qDebug() << lst;

Auf dem Bildschirm wird Folgendes angezeigt:

QList("Blackmore's Night", "Cultus Ferox", "Night Rune")

Qt bietet auch eigene Algorithmen, die wir später in diesem Kapitel betrachten werden.

Moderner Iterationsstil mit range-based for

In C++11 wurde die range-based for-Schleife eingeführt, die die Iteration über Container erheblich vereinfacht. Qt6 unterstützt diese Syntax vollständig für seine Container:

Listing 4.4. Range-based for-Schleife

QList<QString> list = {"In Extremo", "Blackmore's Night", "Night Rune"};
for (const QString &str : list) {
    qDebug() << "Element:" << str;
}

Dieser Ansatz ist in modernem C++-Code vorzuziehen, da er einen besser lesbaren und weniger fehleranfälligen Code gewährleistet.

Für Fälle, in denen Sie die Container-Elemente ändern müssen:

QList<QString> list = {"In Extremo", "Blackmore's Night", "Night Rune"};
for (QString &str : list) {
    str = str.toUpper();
}
qDebug() << list;

Verwendung von STL-Algorithmen mit Lambdas

Moderne C++-Funktionen ermöglichen nicht nur die Verwendung von range-based for, sondern auch die Anwendung von Standard-STL-Algorithmen wie std::for_each. Dies ist besonders praktisch, wenn Sie eine Funktion oder einen Lambda-Ausdruck auf Container-Elemente anwenden möchten. Zum Beispiel, um alle Elemente einer Liste auszugeben:

Listing 4.5. STL-Algorithmus mit Lambda-Ausdruck

QList<QString> list = {"In Extremo", "Blackmore's Night", "Night Rune"};
std::for_each(list.begin(), list.end(), [](const QString &str){
    qDebug() << str;
});

Dieser Ansatz macht den Code prägnant und deklarativ: Anstelle einer expliziten Schleife ist die gesamte Logik des Durchlaufens und Anwendens von Aktionen auf Elemente in einem Standard-Algorithmus verborgen. Dies erhöht die Lesbarkeit, erleichtert das Refactoring und die Integration mit anderen STL-Tools.

Das Schlüsselwort foreach

Natürlich gibt es in der Sprache C++ kein solches Schlüsselwort, es wurde künstlich durch den Präprozessor erstellt und stellt eine Schleifenvariante dar, die zum Durchlaufen aller Container-Elemente dient. Diese Methode ist eine Alternative zum konstanten Iterator. Zum Beispiel:

Listing 4.6. Das Makro foreach

QList<QString> list = {"Subway to sally", "Rammstein", "After Forever"};
foreach(QString str, list) {
    qDebug() << "Element:" << str;
}
Achtung!
Das foreach-Makro wird zwar in Qt6 weiterhin unterstützt, gilt aber als veraltet. Stattdessen wird empfohlen, die range-based for-Syntax zu verwenden, die in C++11 und höher Standard ist.

In foreach können, wie in Schleifen, die Schlüsselwörter break, continue verwendet und Schleifen ineinander verschachtelt werden.

Änderungen an Elementwerten in der foreach-Schleife spiegeln sich nicht im ursprünglichen Container wider
Qt erstellt beim Eintritt in die foreach-Schleife eine Kopie des Containers, daher wirken sich Änderungen an Elementwerten in der Schleife nicht auf den ursprünglichen Container aus.

Sequenzielle Container

Sequenzielle Container sind geordnete Sammlungen, in denen jedes Element eine bestimmte Position einnimmt. Die für alle sequenziellen Container verfügbaren Operationen sind in Tabelle 4.3 aufgeführt.

Tabelle 4.3. Gemeinsame Methoden sequenzieller Container

Operator / Methode Beschreibung
+ Verbindet die Elemente zweier Container
+= Fügt ein Element zum Container hinzu (entspricht <<)
<< Fügt ein Element zum Container hinzu
at() Gibt das angegebene Element zurück
back() und last() Geben eine Referenz auf das letzte Element zurück. Diese Methoden setzen voraus, dass der Container nicht leer ist. Beide Methoden sind identisch, aber die Verwendung der ersten wird bevorzugt, da sie der STL entspricht
contains() Prüft, ob das als Parameter übergebene Element im Container vorhanden ist
erase() Löscht das Element, das sich an der als Parameter übergebenen Iterator-Position befindet
front() und first() Geben eine Referenz auf das erste Element des Containers zurück. Die Methoden setzen voraus, dass der Container nicht leer ist. Beide Methoden sind identisch, aber die Verwendung der ersten wird bevorzugt, da sie der STL entspricht
indexOf() Gibt die Position des ersten im Container gefundenen Elements entsprechend dem an die Methode übergebenen Wert zurück
lastIndexOf() Gibt die Position des letzten im Container gefundenen Elements entsprechend dem an die Methode übergebenen Wert zurück
mid() Gibt einen Container zurück, der Kopien der durch eine Anfangsposition und eine Anzahl definierten Elemente enthält
pop_back() Entfernt das letzte Element des Containers
pop_front() Entfernt das erste Element des Containers
push_back() und append() Fügen ein Element am Ende des Containers hinzu. Beide Methoden sind identisch, aber die Verwendung der ersten wird bevorzugt, da sie der STL entspricht
push_front() und prepend() Fügen ein Element am Anfang des Containers hinzu. Beide Methoden sind identisch, aber die Verwendung der ersten wird bevorzugt, da sie der STL entspricht
replace() Ersetzt das Element an der angegebenen Position durch den als zweiten Parameter übergebenen Wert

Beispiel:

QList<QString> lst;
lst.append("In Extremo");
lst.append("Blackmore's Night");
lst.append("Night Rune");
qDebug() << lst;

oder in einer Zeile:

list << "In Extremo" << "Blackmore's Night" << "Night Rune";

Auf dem Bildschirm sehen Sie:

QList("In Extremo", "Blackmore's Night", "Night Rune")

Wie aus Tab. 4.3 hervorgeht, sind die grundlegenden Operationen: Zugriff auf ein Element, Einfügen/Löschen eines Elements, Hinzufügen am Ende und Hinzufügen am Anfang.

Tab. 4.4 zeigt, wie schnell diese Operationen für jeden Container separat ausgeführt werden. Anhand dieser Tabelle ist es wichtig, je nach Aufgabenstellung den Container zu wählen, der schnell arbeitet.

Tabelle 4.4. Ausführungsgeschwindigkeit von Operationen für sequenzielle Container

Container Zugriff Einfügen / Löschen Hinzufügen (Ende) Hinzufügen (Anfang)
QList<T> / QQueue Schnell Langsam Schnell Schnell
QStack Schnell Langsam Schnell Langsam

Byte-Array: Klasse QByteArray

In Qt6 dient die Klasse QByteArray nach wie vor als Container zur Speicherung von Byte-Sequenzen (Werte mit einer Größe von 1 Byte), ist jedoch kein Template. Ihre wichtigsten Möglichkeiten werden im Folgenden beschrieben.

Initialisierung mit festgelegter Länge

Objekte des Typs QByteArray können überall dort verwendet werden, wo eine Zwischenspeicherung von Daten erforderlich ist. Die Anzahl der Array-Elemente kann im Konstruktor angegeben werden, und der Zugriff darauf erfolgt mit dem Operator []:

QByteArray arr(3, 0); // drei Null-Bytes
arr[0] = arr[1] = 0xFF;
arr[2] = 0x2;

Komprimierung und Dekomprimierung

Auf die Daten von QByteArray-Objekten kann auch die Komprimierungsoperation und die umgekehrte Transformation angewendet werden. Dies wird mit zwei globalen Funktionen erreicht: qCompress() und qUncompress(). Einfaches Komprimieren und Dekomprimieren von Daten:

QByteArray a           = "Test Data";
QByteArray aCompressed = qCompress(a);
qDebug() << qUncompress(aCompressed);

Auf dem Bildschirm erscheint: "Test Data".

Konvertierung nach Base64 und zurück

Manchmal ist es notwendig, Binärdaten in Textform zu konvertieren. Beispielsweise wenn Sie ein Rasterbild in den Text einer XML-Datei schreiben möchten (siehe Kapitel 40). Die Klasse QByteArray bietet dafür zwei Methoden: toBase64() und fromBase64(). Aus den Methodennamen ist ersichtlich, dass die Binärdaten in das Base64-Format konvertiert werden. Dieses Format wurde speziell für die Übertragung von Binärdaten in Textform entwickelt. Ein kleines Beispiel — damit die durchgeführten Konvertierungen nachvollziehbar sind, wenden wir sie auf eine gewöhnliche Textzeichenkette an:

QByteArray a       = "Test Data";
QByteArray aBase64 = a.toBase64();
qDebug() << aBase64;

Auf dem Bildschirm sehen wir: "VGVzdCBEYXRh".

Nun führen wir die umgekehrte Konvertierung mit der statischen Methode fromBase64() durch:

qDebug() << QByteArray::fromBase64(aBase64);

Auf dem Bildschirm erscheint: "Test Data".

Damit Binärdaten in einer Textdatei weniger Platz einnehmen, können sie vor der Base64-Kodierung komprimiert werden.

Bit-Array: Klasse QBitArray

Diese Klasse verwaltet ein Bit-Array (bzw. ein Boolean-Array). Jeder gespeicherte Wert belegt nur ein Bit und verbraucht keinen überschüssigen Speicher. Die Werte werden mithilfe der Klasse QByteArray in Bytes gepackt. Dieser Ansatz wird für die Speicherung einer großen Anzahl von bool-Variablen verwendet.

Für Bit-Operationen stellt die Klasse QBitArray folgende Methoden bereit: zum Lesen — testBit() und zum Schreiben — setBit(). Neben diesen Methoden gibt es auch den Operator [], mit dem auf jedes einzelne Bit zugegriffen werden kann:

QBitArray bits(3);
bits[0] = bits[1] = true;
bits[2] = false;

Listen QList<T>

Eine Liste ist eine Datenstruktur, die eine geordnete Menge miteinander verbundener Elemente darstellt. Im Allgemeinen stellt diese Klasse ein Array von sequenziell im Speicher angeordneten Elementen dar.

Um festzustellen, ob eine Liste leer ist oder nicht, verwenden Sie die Methoden empty() oder isEmpty().

Versuchen Sie, die Anzahl der Listenelemente so selten wie möglich durch Aufruf der Methode size() abzufragen, da bei jedem Aufruf eine Zählung durchgeführt wird, was die Programmgeschwindigkeit merklich beeinflussen kann. In den Fällen, in denen Sie feststellen müssen, ob die Liste leer ist oder nicht, verwenden Sie bedenkenlos nur die Methoden empty() oder isEmpty().

Das Einfügen und Löschen mit Extraktion von Elementen ist sehr ineffizient!
Versuchen Sie, für QList<T> die Methoden removeAt() und insert() nicht zu verwenden, da das Einfügen und Löschen mit Extraktion von Elementen sehr ineffizient ist (siehe Tab. 4.4).

Listen werden durch die Klasse QList<T> implementiert. Im Allgemeinen stellt diese Klasse ein Array von Zeigern auf Elemente dar (Abb. 4.3).

Interne Listenstruktur des Qt6-Containers QList mit sequenziell angeordneten Elementen
Abb. 4.3: Listenstruktur

Die grundlegenden Methoden für Listen sind in Tab. 4.5 aufgeführt.

Tabelle 4.5. Einige Methoden des Containers QList

Methode Beschreibung
move() Verschiebt ein Element von einer Position auf eine andere
removeFirst() Entfernt das erste Element der Liste
removeLast() Entfernt das letzte Element der Liste
swap() Vertauscht zwei Elemente an den angegebenen Positionen
takeAt() Gibt das Element an der angegebenen Position zurück und löscht es
takeFirst() Löscht das erste Element und gibt es zurück
takeLast() Löscht das letzte Element und gibt es zurück
toSet() Gibt einen QSet<T>-Container mit den im QList<T>-Objekt enthaltenen Daten zurück
toStdList() Gibt eine Standard-STL-Liste std::list<T> mit den im QList<T>-Objekt enthaltenen Elementen zurück
reserve() Reserviert Speicher für die angegebene Anzahl von Elementen
resize() Ändert die Listengröße auf die angegebene Anzahl von Elementen

Wenn Sie die Elementwerte nicht ändern möchten, wird aus Effizienzgründen empfohlen, den Indexierungsoperator [] nicht zu verwenden. Verwenden Sie stattdessen die Methode at(), da diese eine konstante Referenz auf das Element zurückgibt.

Eine der häufigsten Operationen ist das Durchlaufen der Liste, um die Werte jedes Elements sequenziell zu erhalten. Zum Beispiel:

QList<int> list;
list << 10 << 20 << 30;

QList<int>::iterator it = list.begin();
while (it != list.end()) {
    qDebug() << "Element:" << *it;
    ++it;
}

Auf der Konsole wird folgendes angezeigt:

Element:10
Element:20
Element:30

Stapel: Klasse QStack<T>

Der Stapel QStack<T> implementiert eine Datenstruktur, die nach dem LIFO-Prinzip (Last In First Out — zuletzt eingefügt, zuerst entfernt) arbeitet, d. h. das zuletzt eingefügte Element wird als erstes aus dem Stapel entfernt (Abb. 4.4).

Schema des LIFO-Funktionsprinzips (Last In First Out) des Qt6-Stapelcontainers QStack
Abb. 4.4: Funktionsprinzip des Stapels

Die Klasse QStack<T> stellt eine Stapel-Datenstruktur dar. Diese Klasse ist von der Klasse QList<T> abgeleitet. Der Vorgang des Hinzufügens von Elementen auf den Stapel wird üblicherweise als Ablegen (pushing) bezeichnet, und das Entfernen des obersten Objekts als Entnehmen (popping). Jede Ablegeoperationen erhöht die Stapelgröße um 1, und jede Entnahmeoperation verringert sie um 1. Für diese Operationen sind in der Klasse QStack<T> die Methoden push() und pop() definiert. Die Methode top() gibt eine Referenz auf das Element an der Stapelspitze zurück. Das folgende Beispiel demonstriert die Verwendung der Stapel-Klasse:

QStack<QString> stk;
stk.push("Era");
stk.push("Night Rune");
stk.push("Gathering");

while (!stk.empty()) {
   qDebug() << "Element:" << stk.pop();
}

Auf der Konsole wird folgendes angezeigt:

Element:"Gathering"
Element:"Night Rune"
Element:"Era"

Warteschlange: Klasse QQueue<T>

Die Warteschlange implementiert eine Datenstruktur, die nach dem FIFO-Prinzip (First In First Out — zuerst eingefügt, zuerst entfernt) arbeitet, d. h. aus der Warteschlange wird nicht das zuletzt eingefügte Element entfernt, sondern das, das am frühesten eingefügt wurde (Abb. 4.5). Implementiert wird die Warteschlange in der Klasse QQueue<T>, die von der Klasse QList<T> abgeleitet ist.

Schema des FIFO-Funktionsprinzips (First In First Out) des Qt6-Warteschlangencontainers QQueue
Abb. 4.5: Funktionsprinzip der Warteschlange

Das folgende Beispiel demonstriert das Verwendungsprinzip einer Warteschlange:

QQueue<QString> que;
que.enqueue("Era");
que.enqueue("Corvus Corax");
que.enqueue("Gathering");

while (!que.empty()) {
   qDebug() << "Element:" <<que.dequeue();
}

Auf dem Bildschirm sollte erscheinen:

Element:"Era"
Element:"Corvus Corax"
Element:"Gathering"

Assoziative Container

Die Aufgabe assoziativer Container besteht in der Speicherung von Schlüssel-Wert-Zuordnungen. Dadurch können Elemente nicht über einen Index, sondern über einen Schlüssel angesprochen werden. Für alle Container dieses Typs (mit einigen Ausnahmen für den Container QSet<T>) stehen die in Tab. 4.6 aufgeführten Methoden zur Verfügung.

Tabelle 4.6. Gemeinsame Methoden assoziativer Container

Methode Beschreibung
contains() Gibt true zurück, wenn der Container ein Element mit dem angegebenen Schlüssel enthält. Andernfalls wird false zurückgegeben
erase() Löscht ein Element aus dem Container gemäß dem übergebenen Iterator
find() Sucht ein Element nach Wert. Bei Erfolg wird ein Iterator zurückgegeben, der auf dieses Element zeigt; bei Misserfolg zeigt der Iterator auf end()
insertMulti() Fügt ein neues Element in den Container ein. Wenn das Element bereits vorhanden ist, wird ein neues Element erstellt. Diese Methode fehlt in der Klasse QSet<T>
insert() Fügt ein neues Element in den Container ein. Wenn das Element bereits vorhanden ist, wird es durch das neue Element ersetzt. Diese Methode fehlt in der Klasse QSet<T>
key() Gibt den ersten Schlüssel entsprechend dem an diese Methode übergebenen Wert zurück. Diese Methode fehlt in der Klasse QSet<T>
keys() Gibt eine Liste aller im Container befindlichen Schlüssel zurück. Diese Methode fehlt in der Klasse QSet<T>
take() Löscht ein Element aus dem Container entsprechend dem übergebenen Schlüssel und gibt eine Kopie seines Wertes zurück. Diese Methode fehlt in der Klasse QSet<T>
unite() Fügt Elemente eines Containers einem anderen hinzu
values() Gibt eine Liste aller im Container befindlichen Werte zurück

Wörterbücher QMap<K,T> und QMultiMap<K,T>

„Programm-“Wörterbücher ähneln im Grunde den gewöhnlichen Wörterbüchern, die wir im Alltag verwenden. Sie speichern Elemente desselben Typs, die durch Schlüsselwerte indiziert sind. Der Hauptvorteil eines Wörterbuchs besteht darin, dass es einen schnellen Zugriff auf den einem bestimmten Schlüssel zugeordneten Wert ermöglicht. Die Schlüssel müssen eindeutig sein (Abb. 4.6), mit Ausnahme des Multi-Wörterbuchs, das Duplikate zulässt (Abb. 4.7).

Darstellung eines Wörterbuchs mit eindeutigen Schlüsseln im Qt6-Container QMap
Abb. 4.6: Wörterbuch
Darstellung eines Multi-Wörterbuchs mit doppelten Schlüsseln im Qt6-Container QMultiMap
Abb. 4.7: Multi-Wörterbuch

In Container dieses Typs werden Elemente zusammen mit Schlüsseln eingetragen, über die sie gefunden werden können. Als Schlüssel können Werte beliebigen Typs dienen. Im Fall des Wörterbuchs QMap<K,T> muss darauf geachtet werden, dass nicht zwei verschiedene Elemente mit demselben Schlüssel eingetragen werden — denn dann wäre eines dieser Elemente nicht auffindbar. Das heißt, jeder Schlüssel im Wörterbuch QMap<K,T> muss eindeutig sein. Tabelle 4.7 zeigt einige seiner Methoden.

Tabelle 4.7. Einige Methoden des Containers QMap<K,T>

Methode Beschreibung
lowerBound() Gibt einen Iterator zurück, der auf das erste Element mit dem angegebenen Schlüssel zeigt
toStdMap() Gibt ein Standard-STL-Wörterbuch mit den im QMap<T>-Objekt enthaltenen Elementen zurück
upperBound() Gibt einen Iterator zurück, der auf das letzte Element mit dem angegebenen Schlüssel zeigt

Eine der häufigsten Methoden für den Zugriff auf Wörterbuchelemente ist die Verwendung des Schlüssels im Operator []. Man kann jedoch auch darauf verzichten, da Schlüssel und Wert mit den Iterator-Methoden key() und value() abgerufen werden können, zum Beispiel:

QMap<QString, QString> mapPhonebook;
mapPhonebook["Piggy"]  = "+69 631322187";
mapPhonebook["Kermit"] = "+69 631322181";
mapPhonebook["Gonzo"]  = "+69 631322186";

QMap<QString, QString>::iterator it = mapPhonebook.begin();
for (;it != mapPhonebook.end(); ++it) {
    qDebug() << "Name:" << it.key()
             << " Phone:" << it.value();
}

Auf der Konsole wird folgendes angezeigt:

Name:Gonzo Phone:+69 631322186
Name:Kermit Phone:+69 631322181
Name:Piggy Phone:+69 631322187

Besondere Aufmerksamkeit verdient die Verwendung des Operators [], der sowohl zum Einfügen als auch zum Abrufen von Elementwerten verwendet werden kann. Man muss jedoch vorsichtig sein, da die Angabe eines Schlüssels, für den kein Element existiert, dazu führt, dass ein Element erstellt wird. Um dies zu vermeiden, muss die Existenz des dem Schlüssel zugeordneten Elements geprüft werden. Eine solche Prüfung kann mit der Methode contains() durchgeführt werden. Zum Beispiel:

if(mapPhonebook.contains("Kermit")) {
    qDebug() << "Phone:" << mapPhonebook["Kermit"];
}

In der Praxis kommt es vor, dass für dieselbe Person mehrere Telefonnummern eingetragen werden müssen — etwa Privat-, Büro- und Mobilnummer. Dafür ist das gewöhnliche Wörterbuch QMap<K,T> nicht mehr geeignet, und es muss das Multi-Wörterbuch QMultiMap<K,T> verwendet werden. Ergänzen wir das Beispiel um Programmcode und fragen die Telefonnummern von Piggy ab:

QMultiMap<QString, QString> mapPhonebook;
mapPhonebook.insert("Kermit", "+69 631322181");
mapPhonebook.insert("Gonzo", "+69 631322186");
mapPhonebook.insert("Gonzo", "+69 631322000");
mapPhonebook.insert("Gonzo", "+69 631322010");
mapPhonebook.insert("Piggy", "+69 631322187");
mapPhonebook.insert("Piggy", "+69 631322999");

QMultiMap<QString, QString>::iterator it =
                                mapPhonebook.find("Piggy");
for (; it != mapPhonebook.end() && it.key() == "Piggy"; ++it) {
    qDebug() << it.value() ;
}

Hashes QHash<K,T> und QMultiHash<K,T>

Die Funktionalität von Hashes ähnelt stark der des Wörterbuchs QMap<K,T>, mit dem Unterschied, dass diese Klasse anstelle der Sortierung nach Schlüssel eine Hash-Tabelle verwendet. Dieser Ansatz ermöglicht es, Schlüsselwerte deutlich schneller zu suchen als das Wörterbuch QMap<K,T>.

Ebenso wie beim Wörterbuch QMap<K,T> ist beim Einsatz des Indexierungsoperators [] Vorsicht geboten, da die Angabe eines Schlüssels, für den kein Element existiert, dazu führt, dass ein Element erstellt wird. Daher ist es wichtig, die Existenz des dem Schlüssel zugeordneten Elements mit der Methode contains() des Containers zu prüfen.

Wenn Sie in den Hash QHash<K,T> Objekte eigener Klassen platzieren möchten, müssen Sie den Vergleichsoperator == und eine spezialisierte Funktion qHash() für Ihre Klasse implementieren. Hier ein Beispiel für die Implementierung des Vergleichsoperators:

inline bool operator==(const MyClass& mc1, const MyClass& mc2)
{
    return (mc1.firstName() == mc2.firstName()
            && mc1.secondName() == mc2.secondName()
           );
}

Die Funktion qHash() gibt eine Zahl zurück, die für jedes im Hash befindliche Element eindeutig sein muss. Zum Beispiel:

inline uint qHash(const MyClass& mc)
{
    return qHash(mc.firstName()) ^ qHash(mc.secondName());
}

Die Klasse QMultiHash<K,T> ist von QHash<K,T> abgeleitet. Sie ermöglicht die Speicherung von Werten mit identischen Schlüsseln und ähnelt insgesamt der Klasse QMultiMap<K,T>, berücksichtigt dabei jedoch die Besonderheiten ihrer Elternklasse. Die ausschließlich für diese Container spezifischen Methoden sind in Tab. 4.8 aufgeführt.

Tabelle 4.8. Einige Methoden der Container QHash<K,T> und QMultiHash<K,T>

Methode Beschreibung
capacity() Gibt die Größe der Hash-Tabelle zurück
reserve() Legt die Größe der Hash-Tabelle fest
squeeze() Verringert den Umfang der internen Hash-Tabelle, um den verwendeten Speicher zu reduzieren
Wichtiger Hinweis zur Größe von Hash-Tabellen
Die Empfehlung, Primzahlen bei der Festlegung der Größe zu verwenden, gilt für Hash-Tabellen (z. B. QHash), nicht jedoch für QMap. Bei QMap wird keine Größe angegeben, solche Parameter sind nicht erforderlich. QMap verwendet intern eine Rot-Schwarz-Baum-Struktur, die automatisch balanciert wird.

Menge QSet<T>

Wie der deutsche Mathematiker Georg Cantor feststellte: Unter einer Menge verstehe man jede Zusammenfassung von bestimmten wohlunterschiedenen Objekten unserer Anschauung oder unseres Denkens zu einem Ganzen. Dieses „Ganze” ist im Kontext von Tulip nichts anderes als der Container QSet<T>, der Elemente in einer bestimmten Reihenfolge speichert und die Möglichkeit bietet, Werte sehr schnell zu durchsuchen und mengenspezifische Operationen auszuführen — wie Vereinigung, Schnittmenge und Differenz. Voraussetzung ist die Eindeutigkeit der Schlüssel.

Die Klasse QSet<T> basiert auf der Verwendung der Hash-Tabelle QHash<K,T>, ist jedoch eine degenerierte Variante davon, da mit den Schlüsseln keine Werte verknüpft sind. Die Hauptaufgabe dieser Klasse besteht in der Speicherung von Schlüsseln. Der Container QSet<T> kann als ungeordnete Liste für schnelle Datensuchanfragen verwendet werden. Ein Beispiel für Mengen ist in Abb. 4.8 dargestellt, wo zwei Mengen mit je drei Elementen abgebildet sind.

Darstellung zweier Mengen mit je drei Elementen im Qt6-Container QSet
Abb. 4.8: Zwei Mengen

Die mit Mengen durchführbaren Operationen sind in Abb. 4.9 veranschaulicht.

Darstellung der Mengenoperationen Vereinigung, Schnittmenge und Differenz mit dem Qt6-Container QSet
Abb. 4.9: Einige Operationen auf Mengen

Erstellen wir zwei Mengen und füllen sie gemäß Abb. 4.8 mit Elementen.

QSet<QString> set1;
QSet<QString> set2;
set1 << "Therion" << "Nightwish" << "Xandria";
set2 << "Mantus" << "Haggard" << "Therion";

Führen wir die Vereinigungsoperation (siehe Abb. 4.9, links) dieser beiden Mengen durch. Damit die Elemente der Mengen unverändert bleiben, führen wir die Zwischenmenge setResult ein:

QSet<QString> setResult = set1;
setResult.unite(set2);
qDebug() << "Vereinigung = " << setResult.toList();

Auf dem Bildschirm sollte folgendes angezeigt werden:

Vereinigung = ("Xandria", "Haggard", "Mantus", "Nightwish", "Therion")

Führen wir nun die Schnittmengenoperation durch (siehe Abb. 4.9, Mitte):

setResult = set1;
setResult.intersect(set2);
qDebug() << "Schnittmenge set1 mit set2 = " << setResult.toList();

Da die beiden Mengen nur ein gemeinsames Element haben, sehen wir auf dem Bildschirm:

Schnittmenge set1 mit set2 = ("Therion")

Die letzte Operation ist die Differenz zweier Mengen (siehe Abb. 4.9, rechts):

setResult = set1;
setResult.subtract(set2);
qDebug() << "Differenz set1 mit set2 = " << setResult.toList();

Die Menge set1 unterscheidet sich von der Menge set2 durch zwei Elemente, daher sollte auf dem Bildschirm angezeigt werden:

Differenz set1 mit set2 = ("Xandria", "Nightwish")

In Tab. 4.9 sind die Methoden für den Container QSet<T> zusammengefasst.

Tabelle 4.9. Einige Methoden des Containers QSet

Methode Beschreibung
intersect() Entfernt Elemente der Menge, die in der übergebenen Menge nicht vorhanden sind
reserve() Legt die Größe der Hash-Tabelle fest
squeeze() Verringert den Umfang der internen Hash-Tabelle, um den verwendeten Speicher zu reduzieren
subtract() Entfernt alle Elemente der Menge, die in der übergebenen Menge vorhanden sind
toList() Gibt ein QList<T>-Container-Objekt zurück, das die Elemente aus dem QSet<T>-Container-Objekt enthält
unite() Vereinigt die Elemente der Mengen

Algorithmen

Algorithmen sind in der Header-Datei QtAlgorithms definiert und bieten Operationen, die auf Container angewendet werden — beispielsweise: Sortieren, Suchen, Datentransformation usw. Es ist anzumerken, dass Algorithmen nicht als Methoden der Containerklassen implementiert sind, sondern als Template-Funktionen, was es ermöglicht, sie sowohl für jede Tulip-Containerklasse als auch für gewöhnliche Arrays zu verwenden. Um beispielsweise Elemente von einem Array in ein anderes zu kopieren, kann der Algorithmus std::copy() verwendet werden:

QString values[] = {"Xandria", "Therion", "Nightwish", "Night Rune"};
const int n = sizeof(values) / sizeof(QString);
QString copyOfValues[n];
std::copy(std::begin(values), std::end(values), copyOfValues);

Beim Kopieren von Containern ist es wichtig sicherzustellen, dass der Ziel-Container groß genug ist, um die Kopie aufzunehmen. In unserem Beispiel haben wir dafür gesorgt, dass der Ziel-Container die gleiche Größe wie der Quell-Container hat.

Hinweis
In Qt6 wird empfohlen, für die meisten Operationen STL-Algorithmen anstelle der Qt-Algorithmen zu verwenden. Viele Qt-Algorithmen (wie qSort, qCopy u. a.) sind als veraltet markiert, und stattdessen sollten ihre STL-Äquivalente eingesetzt werden. Zum Beispiel: anstelle von qSort sollte std::sort, anstelle von qCopystd::copy verwendet werden usw.

Sortierung

Die Sortierung übernimmt die Algorithmus-Funktion std::sort(). Für die Sortierung müssen auf die Elementtypen des Containers Vergleichsoperatoren anwendbar sein, da diese für die Entscheidungen des Algorithmus selbst benötigt werden. Für QString stehen diese Operatoren zur Verfügung. Führen wir die Sortierung für eine Liste mit QString-Elementen durch:

QList<QString> list;
list << "Within Temptation" << "Anubis" << "Night Rune";
std::sort(list.begin(), list.end());
qDebug() << "Sorted list=" << list;

Auf dem Bildschirm sehen wir folgendes:

Sorted list=("Anubis", "Night Rune", "Within Temptation")

Es ist auch möglich, eine Sortierbedingung anzugeben, z. B. mithilfe eines Lambda-Ausdrucks:

std::sort(list.begin(), list.end(), [](const QString &a, const QString &b) {
    return a.toLower() < b.toLower();
});

Um Zahlen in absteigender Reihenfolge zu sortieren, kann der Funktor std::greater<T> verwendet werden, zum Beispiel:

QList<int> list;
list << 1 << 2 << 3 << 4 << 5 << 6;
std::sort(list.begin(), list.end(), std::greater<int>());
qDebug() << "Sorted list=" << list;

Auf dem Bildschirm sehen wir:

Sorted list=(6, 5, 4, 3, 2, 1)

Es ist auch möglich, eine Sortierbedingung in Form einer Funktion anzugeben:

bool lessThan(const QString& str1, const QString& str2)
{
    return QString::compare(str1, str2, Qt::CaseInsensitive) < 0;
}

QList<QString> list;
list << "Within Temptation" << "Anubis" << "anubis" << "Mantus";
std::sort(list.begin(), list.end(), lessThan);
qDebug() << list;

Auf dem Bildschirm erscheint:

("anubis", "Anubis", "Mantus", "Within Temptation")

Suche

Für die Suche nach Elementen ist die Algorithmus-Funktion std::find() zuständig. Sie gibt einen Iterator zurück, der auf das erste gefundene Element gesetzt ist, oder auf end(), wenn das Element nicht gefunden wurde:

QList<QString> list;
list << "Within Temptation" << "Anubis" << "Mantus";
QList<QString>::iterator it =
    std::find(list.begin(), list.end(), "Anubis");
if (it != list.end()) {
    qDebug() << "Found=" << *it;
}
else {
    qDebug() << "Not Found";
}

Auf dem Bildschirm erscheint:

Found=Anubis

Vergleich

Manchmal ist es notwendig, den Inhalt von Containern unterschiedlicher Typen zu vergleichen. Dies kann mit der Algorithmus-Funktion std::equal() durchgeführt werden. Wie bei der Sortierung müssen auf die Elemente des Containers Vergleichsoperatoren anwendbar sein:

QList<QString> list;
list << "Within Temptation" << "Anubis" << "Night Rune";

QList<QString> list2;
list2 << "Within Temptation" << "Anubis" << "Night Rune";
qDebug() << "Equal="
         << std::equal(list.begin(), list.end(), list2.begin());

Auf dem Bildschirm sehen Sie:

Equal=true

Wenn Sie in einem der Container eine Zeichenkette ändern, z. B. Night Rune in Night Run, gibt die Funktion std::equal() den Wert false zurück.

Befüllen mit Werten

In manchen Fällen kann es notwendig sein, Elementen eines bestimmten Teils eines Containers Werte zuzuweisen. Dafür ist der Algorithmus std::fill() vorgesehen. Weisen wir allen Elementen der Liste den Wert Beatles zu:

QList<QString> list;
list << "Within Temptation" << "Anubis" << "Night Rune";
std::fill(list.begin(), list.end(), "Beatles");
qDebug() << list;

Auf dem Bildschirm sollte erscheinen:

("Beatles", "Beatles", "Beatles")

Kopieren von Elementwerten

Um Elementwerte von einem Container in einen anderen zu kopieren, kann der Algorithmus std::copy() verwendet werden. So können Sie beispielsweise alle Elementwerte aus einer Liste in eine andere kopieren:

QList<QString> list;
list << "Within Temptation" << "Anubis" << "Night Rune";
QList<QString> list2(3);
std::copy(list.begin(), list.end(), list2.begin());
qDebug() << list2;

Auf dem Bildschirm erscheint:

QList("Within Temptation", "Anubis", "Night Rune")

Zählen von Werten

Um die Anzahl der Elemente eines Containers mit einem bestimmten Wert zu zählen, kann der Algorithmus std::count() verwendet werden. Zählen wir beispielsweise die Anzahl der Zeichenketten "Night Rune" in der Liste:

QList<QString> list;
list << "Within Temptation" << "Night Rune" << "Anubis" << "Night Rune";
int n = std::count(list.begin(), list.end(), "Night Rune");
qDebug() << n;

Auf dem Bildschirm erscheint die Zahl 2.

Zeichenketten

Nahezu alle Anwendungen arbeiten mit Textdaten. In Qt ist die Klasse QString implementiert, deren Objekte Zeichenketten im Unicode-Format speichern können, wobei jedes Zeichen zwei Bytes belegt. Das Speicherprinzip ähnelt der Klasse QList, der einzige Unterschied besteht darin, dass die Elemente immer vom Zeichentyp QChar sind — man kann also sagen: Eine Zeichenkette ist ein Container zur Speicherung von Zeichen. Die Klasse QString bietet eine ganze Reihe von Methoden und Operatoren, die verschiedenartige Operationen mit Zeichenketten ermöglichen — z. B.: Zeichenketten verbinden, Teilzeichenketten suchen, in Groß- oder Kleinschreibung umwandeln und vieles mehr.

Zeichenketten können mit den Vergleichsoperatoren ==, !=, <, >, <= und >= miteinander verglichen werden. Das Ergebnis des Vergleichs hängt von der Groß-/Kleinschreibung der Zeichen ab, zum Beispiel:

QString str = "Lo";
bool b1  = (str == "Lo"); // b1 = true
bool b2  = (str != "LO"); // b2 = true

Mit der Methode isEmpty() kann festgestellt werden, ob eine Zeichenkette leer ist. Das gleiche Ergebnis lässt sich durch Prüfen der Zeichenkettenlänge mit der Methode length() erzielen. In der Klasse QString gibt es Unterschiede zwischen leeren und Null-Zeichenketten — so stellt eine mit dem Standard-Konstruktor erstellte Zeichenkette eine Null-Zeichenkette dar. Zum Beispiel:

QString str1 = "";
QString str2;
str1.isNull(); // false
str2.isNull(); // true

Das Verbinden von Zeichenketten ist eine der häufigsten Operationen. Es kann auf verschiedene Weisen durchgeführt werden: mit den Operatoren += und + oder durch Aufruf der Methode append(). Zum Beispiel:

QString str1 = "Lo";
QString str2 = "stris";
QString str3 = str1 + str2; // str3 = "Lostris"
str1.append(str2); //str1 = "Lostris"

Um einen bestimmten Teil einer Zeichenkette durch eine andere zu ersetzen, bietet die Klasse QString die Methode replace(). Zum Beispiel:

QString str = "Lostris";
str.replace("stris", "gic"); // str = "Logic"

Zur Konvertierung der Zeichenkettendaten in Groß- oder Kleinschreibung werden die Methoden toLower() oder toUpper() verwendet. Zum Beispiel:

QString str1 = "LoStRiS";
QString str2 = str1.toLower(); // str2 = "lostris"
QString str3 = str1.toUpper(); // str3 = "LOSTRIS"

Mit der Methode setNum() können numerische Werte in Zeichenketten konvertiert werden. Das gleiche Ergebnis lässt sich durch Aufruf der statischen Methode number() erzielen. Zum Beispiel:

QString str = QString::number(35.123);

Ein analoges Ergebnis lässt sich auch mit dem Qt-Textstrom erzielen. Zum Beispiel:

QString str;
QTextStream(&str) << 35.123;

Die Konvertierung von einem Zeichenkettenwert in einen numerischen Wert erfolgt mit Methoden, deren Name den Typnamen enthält. Als zweiten Parameter kann diesen Methoden eine Referenz auf eine bool-Variable übergeben werden, um Informationen darüber zu erhalten, ob die Operation erfolgreich war. Zum Beispiel:

bool ok;
QString str = "234";
double  d   = str.toDouble(&ok);
int     n   = str.toInt(&ok);

Eine Zeichenkette kann mit der Methode split() in ein Zeichenketten-Array aufgeteilt werden. Das folgende Beispiel erstellt eine Liste aus zwei Zeichenketten: Ringo und Star:

QString str = "Ringo Star";
QStringList list = str.split(" ");

Das Zusammenfügen einer Zeichenkettenliste zu einer einzigen Zeichenkette erfolgt mit der Methode join(). Um beispielsweise eine Liste aus zwei Elementen (Ringo und Star) zu einer einzigen, durch ein Leerzeichen getrennten Zeichenkette zusammenzufügen:

str = list.join(" "); //"Ringo Star"

In Tab. 4.10 sind einige Methoden der Klasse QString aufgeführt, die ebenfalls sehr nützlich sein können.

Tabelle 4.10. Einige Methoden der Klasse QString

Methode Beschreibung
endsWith() Nimmt eine Zeichenkette als Parameter und gibt true zurück, wenn die Zeichenkette mit dieser Zeichenkette endet. Andernfalls wird false zurückgegeben
startsWith() Nimmt eine Zeichenkette als Parameter und gibt true zurück, wenn die Zeichenkette mit dieser Zeichenkette beginnt. Andernfalls wird false zurückgegeben
contains() Nimmt eine Zeichenkette oder einen regulären Ausdruck als Argument und gibt true zurück, wenn eine Übereinstimmung innerhalb der Zeichenkette gefunden wird. Andernfalls wird false zurückgegeben
indexOf() Durchsucht vom Anfang nach einer Zeichenkette oder einem regulären Ausdruck und gibt bei Erfolg die Position zurück. Wenn der reguläre Ausdruck oder die Zeichenkette nicht gefunden wurde, wird -1 zurückgegeben
lastIndexOf() Durchsucht vom Ende nach einer Zeichenkette oder einem regulären Ausdruck und gibt bei Erfolg die Position zurück. Wenn der reguläre Ausdruck oder die Zeichenkette nicht gefunden wurde, wird -1 zurückgegeben
left() Gibt den Teil der Zeichenkette mit der angegebenen Anzahl von Zeichen von links zurück
right() Gibt den Teil der Zeichenkette mit der angegebenen Anzahl von Zeichen von rechts zurück
mid() Gibt den Teil der Zeichenkette mit der angegebenen Anzahl von Zeichen ab der festgelegten Position zurück
simplified() Entfernt aus der Zeichenkette aufeinanderfolgende Leerzeichen
leftJustified() Ergänzt die Zeichenkette links mit dem angegebenen Zeichen. Nimmt zwei Argumente: Anzahl der Füllzeichen und das Zeichen selbst
rightJustified() Ergänzt die Zeichenkette rechts mit dem angegebenen Zeichen. Nimmt zwei Argumente: Anzahl der Füllzeichen und das Zeichen selbst

Reguläre Ausdrücke

Reguläre Ausdrücke sind ein leistungsstarkes Mittel zur Analyse und Verarbeitung von Zeichenketten. Sie enthalten ein Muster, das zur Suche in einer Zeichenkette bestimmt ist. Für die Arbeit mit regulären Ausdrücken bietet Qt die Klasse QRegularExpression, die die veraltete Klasse QRegExp aus Qt5 ersetzt. Reguläre Ausdrücke ermöglichen eine schnelle und flexible Extraktion von mit dem Muster übereinstimmendem Text. Es sei jedoch angemerkt, dass die Arbeit mit regulären Ausdrücken langsamer ist als die in der Klasse QString definierten Methoden, weshalb ihr Einsatz begründet sein sollte.

Die Klasse QRegularExpression unterstützt den Standard der Perl-kompatiblen regulären Ausdrücke (PCRE), der mehr Möglichkeiten und bessere Leistung im Vergleich zum veralteten QRegExp bietet.

In Tab. 4.11 sind die wichtigsten Mustermeta-Zeichen aufgeführt, die von der Klasse QRegularExpression unterstützt werden.

Tabelle 4.11. Muster regulärer Ausdrücke

Zeichen Beschreibung Beispiel
. Beliebiges Zeichen a.b
$ Ende der Zeichenkette Abc$
[] Beliebiges Zeichen aus dem angegebenen Zeichensatz [abc]
- Definiert einen Bereich innerhalb einer Zeichenklasse [0-9A-Za-z]
^ Am Anfang eines Zeichensatzes: beliebiges Zeichen, das nicht im Satz enthalten ist [^def]
* Zeichen muss null- oder mehrfach vorkommen A*b
+ Zeichen muss mindestens einmal vorkommen A+b
? Zeichen muss genau einmal vorkommen oder gar nicht A?b
{n} Zeichen muss genau n-mal vorkommen A{3}b
{n,} Mindestens n Übereinstimmungen a{3,}b
{,n} Bis zu n Übereinstimmungen a{,3}b
{n,m} Zwischen n und m Übereinstimmungen a{2,3}b
| Sucht nach einem von zwei Zeichen ac|bc
\b An dieser Stelle befindet sich eine Wortgrenze a\b
\B An dieser Stelle befindet sich keine Wortgrenze a\Bd
( ) Sucht und speichert eine Gruppe gefundener Zeichen (ab\|ac)ad
\d Ziffer von 0 bis 9
\D Alles außer Ziffern
\s Beliebiger Whitespace
\S Alles außer Whitespace
\w Beliebiger Buchstabe, Ziffer oder Unterstrich
\W Alles außer Buchstaben
\A Zeilenanfang
\b Ganzes Wort
\B Kein Wort
\Z Zeilenende (stimmt mit dem Zeilenende-Zeichen oder vor dem Wagenrücklauf-Zeichen überein)
\z Zeilenende (stimmt nur mit dem Zeilenende überein)
Hinweis zum Zeichen ^
Das Zeichen ^ hat in regulären Ausdrücken je nach Kontext zwei Bedeutungen:

  • Am Anfang eines regulären Ausdrucks bedeutet es den Anfang der Zeichenkette.
  • Am Anfang eines Zeichensatzes in eckigen Klammern bedeutet es eine Negation (beliebiges Zeichen, das NICHT im Satz enthalten ist).

Um nach einem von mehreren Zeichen zu suchen, müssen diese in eckige Klammern gesetzt werden. Zum Beispiel stimmt [ab] mit a oder b überein. Um nicht alle Zeichen hintereinander schreiben zu müssen, kann ein Bereich angegeben werden — zum Beispiel stimmt [A-Z] mit jedem Großbuchstaben überein, [a-z] mit jedem Kleinbuchstaben und [0-9] mit jeder Ziffer. Solche Schreibweisen können kombiniert werden — zum Beispiel stimmt [a-z7] mit jedem Kleinbuchstaben und der Ziffer 7 überein.

Es können auch Zeichen ausgeschlossen werden, indem das Zeichen ^ vorangestellt wird. Zum Beispiel stimmt [^0-9] mit allen Zeichen außer Ziffern überein.

Die in Tab. 4.11 in geschweiften Klammern angegebenen Werte werden Quantifikatoren genannt. Quantifikatoren ermöglichen die genaue Angabe, wie oft ein Zeichen im Text vorkommen soll. Zum Beispiel stimmt a{4,5} mit Text überein, wenn der Buchstabe a darin mindestens 4-, aber höchstens 5-mal hintereinander vorkommt. Im folgenden Ausschnitt ist ein regulärer Ausdruck für eine IP-Adresse angegeben — er kann beispielsweise verwendet werden, um eine Zeichenkette auf das Vorhandensein einer IP-Adresse zu prüfen:

QRegularExpression reg("[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}");
QString str("this is an ip-address 123.222.63.1 lets check it");
qDebug() << str.contains(reg); // true

Beachten Sie, dass zur Angabe des Punkt-Zeichens im regulären Ausdruck ein Backslash (\) vorangestellt wird. Gemäß den Regeln der Sprache C++ muss er in einer Zeichenkette verdoppelt werden. Ohne den Backslash hätte der Punkt gemäß Tab. 4.11 die Bedeutung „beliebiges Zeichen”, und der reguläre Ausdruck würde beispielsweise die Zeichenkette 1z2y3x4 als IP-Adresse erkennen, was natürlich falsch wäre.

Muster können mit dem Zeichen | kombiniert werden, um Verzweigungen im regulären Ausdruck zu erstellen. Ein regulärer Ausdruck mit zwei Zweigen stimmt mit einer Teilzeichenkette überein, wenn einer der Zweige übereinstimmt. Zum Beispiel:

QRegularExpression rxp("(.com|.de)");
QRegularExpressionMatch match = rxp.match("www.qt-book.com");
int n1 = match.capturedStart(); // n1 = 10 (Übereinstimmung an Position 10)
match = rxp.match("www.qt-book.ru");
int n2 = match.capturedStart(); // n2 = -1 (keine Übereinstimmungen gefunden)

Die in Tab. 4.11 angegebenen Zeichen mit Backslash ermöglichen eine erhebliche Vereinfachung regulärer Ausdrücke. Zum Beispiel ist der reguläre Ausdruck [a-zA-Z0-9_] identisch mit dem Ausdruck \w.

Zur Überprüfung der korrekten Eingabe einer E-Mail-Adresse kann der folgende reguläre Ausdruck im Objekt regEmail verwendet werden:

QRegularExpression regEmail("([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_.-])+\\.([a-zA-Z]{2,4}|[0-9]{1,3})");
QString strEmail1 = "Max.Schlee@neonway.com";
QString strEmail2 = "Max.Schlee#neonway.com";
QString strEmail3 = "Max.Schlee@neonway";
bool b1 = regEmail.match(strEmail1); //b1 = true
bool b2 = regEmail.match(strEmail2); //b2 = false
bool b3 = regEmail.match(strEmail3); //b3 = false

Hier ein Beispiel, wie festgestellt werden kann, ob eine Zeichenkette eine positive Zahl von 0 bis 999 ist. Wenn die Zeichenkette keine solche ist, gibt die Methode capturedStart() den Wert -1 zurück, da die Zeichenkette keine Position enthält, die dem regulären Ausdruck entsprechen würde. Andernfalls gibt diese Methode den Wert 0 zurück:

QRegularExpression rxp("^\\d\\d?\\d?$");
qDebug() << rxp.match("567").capturedStart(); // 0
qDebug() << rxp.match("3GB").capturedStart(); // -1
qDebug() << rxp.match("111B").capturedStart();// -1
qDebug() << rxp.match("010").capturedStart(); // 0
qDebug() << rxp.match("10").capturedStart();  // 0
qDebug() << rxp.match("2").capturedStart();   // 0
qDebug() << rxp.match("-2").capturedStart();  // -1

Reguläre Ausdrücke können auch für Zeichenkettenlisten verwendet werden. Um eine Zeichenkettenliste zu filtern, kann folgender Ansatz angewendet werden:

QStringList lst;
lst << "576" << "3GB" << "111B" << "010" << "10" << "2" << "-2";
QRegularExpression rxp("^\\d\\d?\\d?$");
QStringList lstNumbers;
for (const QString &str : lst) {
    if (rxp.match(str).hasMatch()) {
        lstNumbers << str;
    }
}
qDebug() << lstNumbers;

Auf dem Bildschirm sehen wir: ("576", "010", "10", "2")

Beliebiger Typ: Klasse QVariant

Objekte der Klasse QVariant können Daten verschiedener Typen enthalten, einschließlich Container. Zu diesen Typen gehören: int, unsigned int, double, bool, QString, QStringList, QImage, QPixmap, QBrush, QColor, QRegularExpression u. a. Dabei ist zu beachten, dass die häufige Verwendung dieses Typs die Programmgeschwindigkeit und die Effizienz des Speichereinsatzes negativ beeinflussen sowie die Lesbarkeit des Programms erheblich verringern kann. Daher sollten Objekte der Klasse QVariant ohne besonderen Bedarf nicht verwendet werden.

Zur Erstellung von Objekten der Klasse QVariant muss dem Konstruktor eine Variable des gewünschten Typs übergeben werden. Zum Beispiel:

QVariant v1(34);
QVariant v2(true);
QVariant v3("Lostris");

Die Methode typeId() in Verbindung mit QMetaType::typeName() ermöglicht es, den Typ der im QVariant-Objekt gespeicherten Daten zu ermitteln. Zum Beispiel:

QVariant v(5.0);
qDebug() << QMetaType::typeName(v.typeId()); // =>double

Um aus einem QVariant-Objekt Daten des gewünschten Typs zu erhalten, gibt es eine Reihe spezieller Methoden toT(), wobei T der Typname ist. Die Methode toT() erstellt ein neues Objekt des Typs T und kopiert die Daten aus dem QVariant-Objekt in das gewünschte Objekt. Zum Beispiel:

QVariant v2(23);
int a = v2.toInt() + 5; // a = 28
Einschränkung des Objekts QVariant
Da QVariant im Modul QtCore implementiert ist, werden die entsprechenden Methoden toT() für Klassen wie QColor, QImage und QPixmap u. a., die sich im Modul QtGui befinden, nicht bereitgestellt.

Anstelle der Methoden toT() kann zum Typumwandeln auch die Template-Methode value<T>() verwendet werden. Unser Beispiel mit der Konvertierung eines QVariant-Objekts in einen Integer-Typ kann dann wie folgt dargestellt werden:

QVariant v2(23);
int a = v2.value<int>() + 5; // a = 28

oder beispielsweise für ein QPixmap-Objekt:

QPixmap pix(":/myimg.png"); // QPixmap-Objekt erstellen
QVariant vPix = pix; // in QVariant konvertieren (impliziter Aufruf von QPixmap::operator QVariant())
QPixmap pix2 = vPix.value<QPixmap>(); // QPixmap-Objekt aus QVariant zurückgewinnen

Modell zur gemeinsamen Datennutzung

Aus Effizienzgründen wird in vielen Qt-Klassen versucht, das Kopieren von Daten zu vermeiden — stattdessen wird eine Referenz auf die benötigten Daten verwendet (Abb. 4.10). Dieses Prinzip heißt gemeinsame Datennutzung (shared data). In Qt wird das Modell der impliziten gemeinsamen Daten eingesetzt. Bei diesem Modell führt der Aufruf des Copy-Konstruktors oder des Zuweisungsoperators nicht zum Kopieren der Daten, sondern erhöht lediglich den Referenzzähler für diese Daten um 1. Entsprechend wird beim Löschen eines Elements der Referenzzähler um 1 verringert. Wenn der Referenzzähler den Wert 0 erreicht, werden die Daten vernichtet. Das Kopieren von Daten erfolgt nur bei Änderungen — entsprechend wird dabei der Referenzzähler verringert.

In Abb. 4.10 werden im ersten Schritt zwei Objekte erstellt. Da ihnen noch keine Daten zugewiesen wurden, zeigen beide auf shared_null (gemeinsame Null). Im zweiten Schritt werden dem ersten Objekt Daten zugewiesen, und der Referenzzähler wird auf eins gesetzt. Im dritten Schritt wird dem zweiten Objekt das erste Objekt zugewiesen, und beide zeigen nun auf dieselben Daten; der Referenzzähler wird dabei um eins erhöht. Im vierten Schritt werden die Daten des ersten Objekts geändert, was zur Erstellung einer separaten Kopie für dieses Objekt führt, und der Referenzzähler der alten Daten wird um eins verringert, da nun ein Objekt weniger auf diese Daten verweist. Wenn wir in einem fünften Schritt die Daten des zweiten Objekts geändert hätten, wäre der Referenzzähler der alten Daten nach der Erstellung einer Kopie für die neuen Daten auf 0 gefallen, was zur Freigabe des Speichers und zur Vernichtung der alten Daten geführt hätte.

Veranschaulichen wir die in Abb. 4.10 dargestellte Situation mit Programmcode:

QString str1;          // Verweist auf shared_null
QString str2;          // Verweist auf shared_null
str1 = "Neuer String"; // Verweist auf Daten, Referenzzähler = 1
str2 = str1;           // str1 und str2 zeigen auf dieselben Daten
                       // Referenzzähler = 2
str1 += " Anhang";     // Datenkopie wird für str1 erstellt
Vier Schritte des Copy-on-Write-Modells der impliziten gemeinsamen Datennutzung in Qt6
Abb. 4.10: Vier Schritte der gemeinsamen Datennutzung

Zusammenfassung

In diesem Kapitel haben wir erfahren, dass ein Container ein Objekt ist, das zur Speicherung und Verwaltung der in ihm enthaltenen Elemente dient. Er übernimmt die Speicherzuweisung und -freigabe und ist für das Hinzufügen und Entfernen von Elementen verantwortlich. Containerklassen werden in sequenzielle und assoziative unterteilt. Zu den sequenziellen Containern gehören Liste, Stapel und Warteschlange, zu den assoziativen — Menge, Wörterbuch und Hash.

Zum Durchlaufen der Elemente eines Containers werden Iteratoren verwendet. Qt bietet Iteratoren im Java- und STL-Stil. Als moderner Ansatz wird für die Iteration über Container-Elemente die range-based-for-Schleife empfohlen, die in C++11 und höher verfügbar ist.

Mithilfe von Algorithmen können Operationen auf den Inhalt von Containern durchgeführt werden — wie Sortieren, Suchen und viel mehr. In Qt6 wird zwar die Verwendung eigener Algorithmen unterstützt, empfohlen wird jedoch der Einsatz von STL-Algorithmen.

Die Klasse QString stellt eine Implementierung von Zeichenketten dar und enthält eine ganze Reihe von Methoden für verschiedene Operationen mit ihnen.

Reguläre Ausdrücke stellen einen leistungsstarken Mechanismus zur Prüfung von Zeichenketten auf Übereinstimmung mit einem Muster dar.

Objekte der Klasse QVariant können Daten verschiedener Typen enthalten, einschließlich Container.


Entfalten Sie das volle Potenzial dieses Kapitels! Nutzen Sie die zu diesem Kapitel bereitgestellten Zusatzmaterialien, stellen Sie eine Frage, teilen Sie Ihre Erfahrungen oder nehmen Sie an der Diskussion teil unter: https://qt-book.com/04-69-de/

Leave a Reply

Your email address will not be published. Required fields are marked *