Versuche mal, irgend einer Sprachassistentin den Satz "Brate mir einen Storch!" zu befehlen. Während drei Assistenten völlig unpassende Antworten liefern, reagiert Siri einfach gar nicht (Stand Feb. 2020). Fragen nach dem Wetterbericht können alle drei jedoch sehr gut beantworten. Wie kommt das?
Der Knackpunkt ist, dass Computer nur die Fragen beantworten können, die ihnen vorher einprogrammiert wurden. Mit unerwarteten Anfragen können sie nichts anfangen, denn Computer sind sind nicht intelligent. Deswegen wird es wohl noch lange dauern, bis autonome Autos ohne Fahrer auf die Straßen dürfen.
Auf der anderen Seite arbeiten unsere Computer mit mathematischer Präzision, schnell und unermüdlich. Sie nehmen uns langweilige Arbeiten ab, und sie unterhalten uns. Wir stehen so sehr auf diese kleinen Helfer, dass es bereits Zahnbürsten und Kochtöpfe mit Internet Anbindung gibt.
Um all diese Maschinen zu entwickeln, werden auf der ganzen Welt ständig gute verantwortungsbewusste Programmierer gesucht. Programmierer haben Spaß am Job, weil sie etwas sinnvolles schaffen. Außerdem werden sie gut bezahlt.
1037000 7878 3131 3231 6162 6973 5f63 7473 6972 1037020 676e 7749 7453 3131 6863 7261 745f 6172 1037040 7469 4973 4577 6153 7749 4545 4c70 5245 1037060 534b 5f34 5f00 4e5a 3031 5351 7274 6e69 1037100 5267 6665 5361 5045 374b 5351 7274 6e69 1037120 0067 5a5f 534e 3774 5f5f 7863 3178 3131 1037140 6232 7361 6369 735f 7274 6e69 4967 7344 1037160 7453 3131 6863 7261 745f 6172 7469 4973 1037200 7344 5345 4961 7344 4545 5f39 5f4d 756d 1037220 6174 6574 6d45 506d 444b 6d73 5f00 4e5a 1037240 5137 7453 6972 676e 3331 6873 6972 6b6e ...
So eine "Matrix" können nur Helden aus Phantasie-Filmen lesen. Deswegen schreiben Programmierer ihre Anweisungen in einer Programmiersprache auf, die sowohl Maschine als auch Mensch verstehen können:
if (age < 16) showMessage("Du darfst Whatsapp noch nicht benutzen"); else registerAccount(username, password);
Diesen Text nennt man Quelltext. Zu den Arbeitsmitteln der Programmierer gehört ein Compiler, welcher solche Quelltexte in numerischen Maschinencode übersetzt.
In dieser Anleitung wirst du die Programmiersprache C++ kennen lernen. Die Stärke dieser Programmiersprache ist, dass sie auf allen Plattformen zur Verfügung steht: Von winzig kleinen 8bit Mikrocontrollern bis hin zu Mainframe Rechnern.
Jedes Framework bringt hier seine eigene Variante mit sich. Qt ist so ein plattform-unabhängiges Framework. Mit Qt kannst du Programme für zahlreiche Betriebssysteme schreiben, zum Beispiel: Windows, Linux, Android, macOS, iOS.
Sollte das Installtionsprogramm nicht starten und dabei melden, dass eine DLL Datei mit "140" im Namen fehlt, dann installiere das "C++ Redistributable für Visual Studio 2015" Paket von Microsoft, und zwar beide Versionen für x64 und x86.
Das Installationsprogramm wird dich als erstes darum bitten, einen "Qt Account" anzulegen, ohne den man nicht weiter kommt. Danach muss man die Vertragsbedingungen akzeptieren. Schließlich kommst du zur Auswahl der Qt Komponenten. Die Standardinstallation enthält
Das Installationsprogramm wird dich als erstes darum bitten, einen "Qt Account" anzulegen, ohne den man nicht weiter kommt. Danach muss man die Vertragsbedingungen akzeptieren. Schließlich kommst du zur Auswahl der Qt Komponenten. Die Standardinstallation enthält
Zusätzlich musst du folgendes von deiner Linux Distribution installieren:
Debian, Ubuntu | sudo apt install build-essential gdb libgl1-mesa-dev |
---|---|
Fedora, RedHat, CentOS | sudo yum groupinstall "C Development Tools and Libraries" sudo yum install mesa-libGL-devel |
openSUSE | sudo zypper install -t pattern devel_basis |
Nun kannst du die Entwicklungsumgebung Qt Creator starten.
Um so ein Programm zu schreiben, gehe in der Entwicklungsumgebung ins Menü Datei/Neu...
Stelle in diesem Dialog ein, dass du eine "Qt Konsolenanwendung" erstellen willst. Der Begriff Konsole bezieht sich auf das oben gezeigte Fenster, welches für Text-Basierte Ein- und Ausgaben vorgesehen ist. Man nennt es Konsole oder Terminal-Fenster.
Es erscheint ein weiterer Dialog, wo du deinem Projekt einen Namen geben sollst und festlegst, in welchem Ordner das Projekt abgespeichert werden soll.
Klicke danach ein paar mal auf "weiter" bis der Text-Editor für den Quelltext erscheint.
Ersetze den dort vorgegebenen Quelltext durch den folgenden, und zwar ganz genau so, mit all den komischen Sonderzeichen:
#include <QTextStream> int main() { QTextStream out(stdout); out << "Hallo Welt!" << Qt::endl; QTextStream in(stdin); in.readLine(); }
In Qt Versionen vor 5.14 musste man "endl" anstatt "Qt::endl" schreiben.
Das Ergebnis sollte ungefähr so aussehen:
Falls bei dir die Ausgabe stattdessen innerhalb der IDE im Bereich "Ausgabe der Anwendung" erscheint, gehe am linken Rand auf "Projekte". Dort kannst du unter "Ausführen" ein Häkchen bei "Im Terminal Ausführen" setzen. Gehe danach am linken Rand zurück auf "Editieren" um zur vorherigen Ansicht zurück zu gelangen.
Die erste Zeile im Quelltext
#include <QTextStream>
Die Komponente QTextStream stellt Funktionen zum Ausgeben und Einlesen von Text bereit. In der Fachsprache werden solche Komponenten Klassen (englisch: class) genannt.
Dein Hallo Welt! Programm benutzt zwei Objekte von der Klasse QTextStream:
QTextStream out(stdout); out << "Hallo Welt!" << Qt::endl; QTextStream in(stdin); in.readLine();
Das untere Objekt hat den Namen "in" und wird benutzt, um eine Zeile zu lesen. Wirklich? Probiere es aus!
Falls du das Programmfenster inzwischen geschlossen hast, starte es nochmal. Du siehst den Text "Hallo Welt!" und einen blinkenden Cursor. Der Cursor signalisiert, dass das Programm auf eine Eingabe wartet. Versuche es, gebe irgend etwas ein:
Aha, der Computer hat tatsächlich auf die Eingabe reagiert. Die Meldung "Betätigen Sie die <RETURN> Taste, um das Fenster zu schließen..." kommt übrigens nicht von deinem Programm, sondern von der Entwicklungsumgebung. Sie wurde als Komfort-Faktor hinzugefügt, damit sich das Fenster nicht von alleine schließt. Dein Programm ist zu diesem Zeitpunkt bereits beendet.
Dein Programm benötigt zwei individuelle Objekte von der QTextStream Klasse, weil sie mit unterschiedlichen Parametern initialisiert werden - die markierten Teile:
QTextStream out(stdout); out << "Hallo Welt!" << Qt::endl; QTextStream in(stdin); in.readLine();
Versuche mal, den Ausgabe-Text zu verändern. Achte darauf, nur den Text innerhalb der doppelten Anführungsstriche zu verändern. Zum Beispiel:
QTextStream out(stdout); out << "Jo, wir Schaffen das!" << Qt::endl;
Nach der Ausgabe des Textes wird noch "Qt::endl" nachgeschoben. Dieses spezielles Schlüsselwort löst einen Zeilenumbruch aus. Der Blinkende Cursor geht runter in die nächste Zeile.
#include <QTextStream> /* Dieses Programm zeigt etwas auf dem Bildschirm an. */ int main() { QTextStream out(stdout); out << "Die Polizei" << Qt::endl << "dein Freund" << Qt::endl << "und Helfer"; QTextStream in(stdin); in.readLine(); // auf Eingabe warten }
Der Quelltext enthält Kommentare vom Programmierer, die der Compiler ignoriert. Mehrzeilige Kommentare beginnen mit /* und enden mit */. Einzeilige Kommentare beginnen mit //.
Die Ausgabe entspricht allerdings nicht der Erwartung:
Da fehlt die letzte Zeile, weil im Quelltext ein "Qt::endl" für den abschließenden Zeilenumbruch fehlt. Textausgaben werden immer so lange zurück gehalten, bis ein Zeilenumbruch oder ein "flush" kommt. Probiere beides ("Qt::endl" und "Qt::flush") aus, um den Unterschied zu sehen.
Hier ein Beispiel mit flush:
#include <QTextStream> int main() { QTextStream out(stdout); out << "Die Polizei" << Qt::endl << "dein Freud" << Qt::endl << "und Helfer" << Qt::flush; QTextStream in(stdin); in.readLine(); }
Du siehst, dass der blinkende Cursor hinter der dritten Zeile stehen geblieben ist, anstatt in die nächste Zeile zu springen. Sinnvoll wird das zum Beispiel in so einem Fall:
#include <QTextStream> int main() { QTextStream out(stdout); out << "Name: " << flush; QTextStream in(stdin); QString eingabe=in.readLine(); out << "Danke " << eingabe << "." << Qt::endl; }
Dort kannst du jetzt deinen Namen eingeben:
Bei dieser Gelegenheit habe ich dir ein Beispiel unter geschoben, wie man die Eingabe von stdin weiter verarbeiten kann.
QTextStream in(stdin); QString eingabe=in.readLine();
Oder anders herum von links nach rechts gelesen: Es gibt da eine Variable von der Klasse QString mit Namen "eingabe", und sie wird mit dem Ergebnis der Methode in.readLine() befüllt.
Eine Variable ist ein Platzhalter für irgendwelche Daten, so wie das "X" in mathematischen Formeln. In der Programmiersprache C++ hat jede Variable immer einen definierten Typ, der ganz genau festlegt, was man darin speichern kann. In diesem Fall ist es eben eine Zeichenkette (string).
Schau Dir die letzte Zeile an:
out << "Danke " << eingabe << "." << Qt::endl;
Jetzt haben wir nur noch eine Zeile übrig, die nicht erklärt wurde - genauer gesagt ein Block:
int main() { ... }
Zwischen den geschweiften Klammern schreibt man die Befehle (=Anweisungen), die ausgeführt werden sollen.
Es ist festgelegt, dass die main() Funktion einen Rückgabewert als integer-Zahl liefern muss. Das sind positive oder negative Zahlen ohne Nachkommastellen. Manche Programme nutzen den Rückgabewert, um Fehler zu melden:
int main() { ... return 99; }
Zeichenketten (englisch: strings) muss man zwischen Anführungsstriche schreiben. Zahlen schreibt man ohne Anführungsstriche.
Achte darauf, dass deine Zahlen nicht mit einer führenden 0 beginnen, weil der Compiler diese sonst als Oktal-Zahl interpretiert.
Verändere das "Hallo Welt!" Programm so, das es Name, Geburtsdatum, Schulklasse und Rufnummer von drei Schülern auf dem Bildschirm ausgibt.
Bevor du weiter liest, versuche die Aufgabe alleine zu lösen. Die nötigen Befehle kennst du bereits. Das Ergebnis soll so aussehen:
Vergleiche deinen Lösungsansatz danach mit meinem:
#include <QTextStream> int main() { QTextStream out(stdout); out << "Name: Lisa Lob" << Qt::endl; out << "Geburtdatum: 01.05.2007" << Qt::endl; out << "Klasse: 8a" << Qt::endl; out << "Rufnummer: 0211/1234567" << Qt::endl; out << Qt::endl; out << "Name: Hanna Kornblut" << Qt::endl; out << "Geburtdatum: 12.08.2007" << Qt::endl; out << "Klasse: 8b" << Qt::endl; out << "Rufnummer: 0211/234567" << Qt::endl; out << Qt::endl; out << "Name: Max Robinson" << Qt::endl; out << "Geburtdatum: 20.11.2006" << Qt::endl; out << "Klasse: 8a" << Qt::endl; out << "Rufnummer: 0211/345678" << Qt::endl; out << Qt::endl; }
Wenn du das so für die ganze Schule fortsetzt, wirst du dir einen Wolf tippen. Besonders fragwürdig wäre dabei die wiederholte Eingabe der Beschriftungen. Kann dein Computer das nicht automatisieren? Sicher doch, du musst es nur programmieren.
Damit kommen wir zum Konzept der Funktionen. Eine Funktion ist ein Stück wiederverwendbarer Programmcode. Ich zeige Dir ein Beispiel:
#include <QTextStream> static QTextStream out(stdout); // Ausgabefunktion: void ausgeben(QString name, QString geburtsdatum, QString schulklasse, QString rufnummer) { out << "Name: " << name << Qt::endl; out << "Geburtdatum: " << geburtsdatum << Qt::endl; out << "Klasse: " << schulklasse << Qt::endl; out << "Rufnummer: " << rufnummer << Qt::endl; out << Qt::endl; } int main() { ausgeben("Lisa Lob", "01.05.2007", "8a", "0211/1234567"); ausgeben("Hanna Kornblut", "12.08.2007", "8b", "0211/234567" ); ausgeben("Max Robinson", "20.11.2006", "8a", "0211/345678" ); }
Die erste Zeile der Funktion definiert eine Liste von Parametern, welche von außen an die Funktion übergeben werden und dann innerhalb der Funktion benutzt werden. Jeder Parameter hat einen bestimmten Typ (in diesem Fall QString) und einen Namen, den du dir selbst ausdenken kannst. In der main() Funktion wird ausgeben() drei mal aufgerufen, jedoch jedes mal mit unterschiedlichen Werten für die Parameter.
Ich habe mir hier die Freiheit genommen, den Quelltext durch Einrückungen mit Leerzeichen übersichtlich zu gestalten. Für den C++ Compiler haben diese zusätzlichen Leerzeichen keine Bedeutung. Theoretisch könnte man sogar das ganze Programm in eine einzige Zeile schreiben, aber davon kann ich nur dringend abraten. Quelltexte sollen so weit wie möglich übersichtlich gestaltet werden, damit man sie gut lesen kann.
Links vom Funktionsnamen ausgeben() steht das komische Wort "void". Es bedeutet, dass diese Funktion keinen Rückgabewert hat. Weiter unten werden wir Funktionen mit Rückgabewert schreiben.
Beachte, dass ich das Objekt "out" weiter nach oben verschoben habe. Wenn "out" immer noch innerhalb von main() stehen würde, könnte man es nur innerhalb der main() Funktion nutzen. Jetzt befindet es sich aber außerhalb der main() Funktion und ist dadurch für beide Funktionen erreichbar.
Das Schlüsselwort "static" sagt dem C++ Compiler, dass dieses Objekt nur in dieser einen Quelltext-Datei erreichbar sein soll. Größere Programme bestehen aus vielen Dateien, da kann diese Beschränkung hilfreich sein, um Konflikte mit gleich benannten Objekten zu vermeiden. Dementsprechend zeigt die IDE einen Hinweis an, wenn du das Wort "static" weg lässt. Probiere es aus.
Das Foto zeigt drei Schüler in einer Bibliothek. In der Informatik würde man sagen: Wir haben drei Objekte von der Klasse "Schüler". Außerdem sehen wir im Hintergrund viele Objekte von der Klasse "Buch".
Die folgende C++ Klasse beschreibt die für unser Programm relevanten Eigenschaften eines Schülers. Darunter werden drei Schüler-Objekte mit individuellen Eigenschaften angelegt:
#include <QTextStream> static QTextStream out(stdout); class Schueler { public: // Eigenschaften: QString name; QString geburtsdatum; QString schulklasse; QString rufnummer; // Funktion: void ausgeben() { out << "Name: " << name << Qt::endl; out << "Geburtdatum: " << geburtsdatum << Qt::endl; out << "Klasse: " << schulklasse << Qt::endl; out << "Rufnummer: " << rufnummer << Qt::endl; out << Qt::endl; } }; int main() { Schueler a; a.name="Lisa Lob"; a.geburtsdatum="01.05.2007"; a.schulklasse="8a"; a.rufnummer="0211/1234567"; Schueler b; b.name="Hanna Kornblut"; b.geburtsdatum="12.08.2007"; b.schulklasse="8b"; b.rufnummer="0211/234567"; Schueler c; c.name="Max Robinson"; c.geburtsdatum="20.11.2006"; c.schulklasse="8a"; c.rufnummer="0211/345678"; a.ausgeben(); b.ausgeben(); c.ausgeben(); }
Die C++ Klasse "Schueler" legt fest, welche Eigenschaften und Funktionen ein Schüler haben kann:
class Schueler { public: // Eigenschaften: QString name; QString geburtsdatum; QString schulklasse; QString rufnummer; // Funktion: void ausgeben() { ... } };
Das Schlüsselwort "public" bestimmt, dass die Sachen darunter außerhalb der C++ Klasse sichtbar sein sollen.
Von dieser Klasse werden mehrere Objekte erstellt, die konkrete individuelle Eingenschafts-Werte erhalten:
int main() { Schueler a; a.name="Lisa Lob"; a.geburtsdatum="01.05.2007"; a.schulklasse="8a"; a.rufnummer="0211/1234567"; Schueler b; b.name="Hanna Kornblut"; b.geburtsdatum="12.08.2007"; b.schulklasse="8b"; b.rufnummer="0211/234567"; Schueler c; c.name="Max Robinson"; c.geburtsdatum="20.11.2006"; c.schulklasse="8a"; c.rufnummer="0211/345678"; ... }
Die drei Schueler Objekte geben sich selbst auf dem Bildschirm aus, wenn ihre ausgeben() Methode aufgerufen wird:
int main() { ... a.ausgeben(); b.ausgeben(); c.ausgeben(); }
Klassen definieren die Eigenschaften von Objekten ohne konkrete Werte. Danach erstellt man Objekte von der Klasse, die individuelle Eigenschafts-Werte haben. Danach kann man die Funktionen der Objekte benutzen, um mit den Werten etwas anzustellen.
Das ist das Grundprinzip der objektorientierten Programmierung.
Die Funktionen von Klassen heissen in der Fachsprache "Methoden".
Das geht so:
#include <QTextStream> static QTextStream out(stdout); // speichert die Daten eines Schülers class Schueler { public: QString name; QString geburtsdatum; QString schulklasse; QString rufnummer; Schueler(QString n, QString g, QString k, QString r) { name=n; geburtsdatum=g; schulklasse=k; rufnummer=r; } void ausgeben() { out << "Name: " << name << Qt::endl; out << "Geburtdatum: " << geburtsdatum << Qt::endl; out << "Klasse: " << schulklasse << Qt::endl; out << "Rufnummer: " << rufnummer << Qt::endl; out << Qt::endl; } }; int main() { Schueler a("Lisa Lob", "01.05.2007", "8a", "0211/1234567"); Schueler b("Hanna Kornblut", "12.08.2007", "8b", "0211/234567" ); Schueler c("Max Robinson", "20.11.2006", "8a", "0211/345678" ); a.ausgeben(); b.ausgeben(); c.ausgeben(); }
Konstruktoren sehen so ähnlich aus, wie Methoden. Sie müssen aber genau so heißen, wie die C++ Klasse und sie haben keinen Rückgabewert - nicht einmal void.
Die Ausgabe hat sich nicht verändert:
Wie gefallen Dir die Namen der Parameter (n, g, k und r) des Konstruktors?:
Schueler(QString n, QString g, QString k, QString r) { name=n; geburtsdatum=g; schulklasse=k; rufnummer=r; }
Sie sind nicht aussagekräftig. Das sollten wir ganz schnell verbessern. Du kannst sie aber nicht einfach komplett ausschreiben, weil das sonst Ausdrücke wie "name=name" ergeben würde, was dem C++ Compiler nicht eindeutig genug wäre. Welcher Name ist dann das Ziel und welcher die Quelle? Man weiß es nicht.
Das Schlüsselwort "this" löst dieses Problem:
#include <QTextStream> static QTextStream out(stdout); // speichert die Daten eines Schülers class Schueler { public: QString name; QString geburtsdatum; QString schulklasse; QString rufnummer; Schueler(QString name, QString geburtsdatum, QString schulklasse, QString rufnummer) { this->name=name; this->geburtsdatum=geburtsdatum; this->schulklasse=schulklasse; this->rufnummer=rufnummer; } void ausgeben() { out << "Name: " << name << Qt::endl; out << "Geburtdatum: " << geburtsdatum << Qt::endl; out << "Klasse: " << schulklasse << Qt::endl; out << "Rufnummer: " << rufnummer << Qt::endl; out << Qt::endl; } }; int main() { Schueler a("Lisa Lob", "01.05.2007", "8a", "0211/1234567"); Schueler b("Hanna Kornblut", "12.08.2007", "8b", "0211/234567" ); Schueler c("Max Robinson", "20.11.2006", "8a", "0211/345678" ); a.ausgeben(); b.ausgeben(); c.ausgeben(); }
Später werden wir das ausnutzen, um den Text woanders hin auszugeben - nämlich in eine Datei.
#include <QTextStream> static QTextStream out(stdout); // speichert die Daten eines Schülers class Schueler { public: QString name; QString geburtsdatum; QString schulklasse; QString rufnummer; Schueler(QString name, QString geburtsdatum, QString schulklasse, QString rufnummer) { this->name=name; this->geburtsdatum=geburtsdatum; this->schulklasse=schulklasse; this->rufnummer=rufnummer; } void ausgeben(QTextStream& wohin) { wohin << "Name: " << name << Qt::endl; wohin << "Geburtdatum: " << geburtsdatum << Qt::endl; wohin << "Klasse: " << schulklasse << Qt::endl; wohin << "Rufnummer: " << rufnummer << Qt::endl; wohin << Qt::endl; } }; int main() { Schueler a("Lisa Lob", "01.05.2007", "8a", "0211/1234567"); Schueler b("Hanna Kornblut", "12.08.2007", "8b", "0211/234567" ); Schueler c("Max Robinson", "20.11.2006", "8a", "0211/345678" ); a.ausgeben(out); b.ausgeben(out); c.ausgeben(out); }
#include <QTextStream> #include <QFile> static QTextStream out(stdout); // speichert die Daten eines Schülers class Schueler { public: QString name; QString geburtsdatum; QString schulklasse; QString rufnummer; Schueler(QString name, QString geburtsdatum, QString schulklasse, QString rufnummer) { this->name=name; this->geburtsdatum=geburtsdatum; this->schulklasse=schulklasse; this->rufnummer=rufnummer; } void ausgeben(QTextStream& wohin) { wohin << "Name: " << name << Qt::endl; wohin << "Geburtdatum: " << geburtsdatum << Qt::endl; wohin << "Klasse: " << schulklasse << Qt::endl; wohin << "Rufnummer: " << rufnummer << Qt::endl; wohin << Qt::endl; } }; int main() { Schueler a("Lisa Lob", "01.05.2007", "8a", "0211/1234567"); Schueler b("Hanna Kornblut", "12.08.2007", "8b", "0211/234567" ); Schueler c("Max Robinson", "20.11.2006", "8a", "0211/345678" ); a.ausgeben(out); b.ausgeben(out); c.ausgeben(out); QFile datei("test.txt"); if ( datei.open(QIODevice::WriteOnly) ) { QTextStream strom(&datei); a.ausgeben(strom); b.ausgeben(strom); c.ausgeben(strom); datei.close(); } }
Weiter unten erstellen wir ein Objekt von dieser Klasse, welches mit dem Dateinamen "test.txt" initialisiert wird. Dann wird die Datei zum schreiben (WriteOnly) geöffnet.
Wenn (if) das geklappt hat, erzeugen wir eine zweites Objekt von der Klasse QTextStream mit dem Namen "strom" und geben alle drei Schüler damit aus. Mit der if-Anweisung beschäftigen wir uns gleich noch mehr.
Zum Schluss wird die Datei mit close() geschlossen. Spätestens danach kannst du sie mit einem Text-Editor anschauen.
Mache das mal. Starte das Programm und suche dann die Ausgabe-Datei text.txt im build Verzeichnis. Es befindet sich dort, wo auch dein Projektverzeichnis liegt. Der Name des Verzeichnisses ist ziemlich lang und fängt mit "build" an:
Öffne die Datei test.txt, um sie zu kontrollieren:
Sieht gut aus, oder?
In dem build Verzeichnis befinden sich noch weitere Dateien:
#include <QTextStream> static QTextStream out(stdout); int main() { if (4+5 == 9) { out << "4 plus 5 ist 9" << Qt::endl; } if (7 > 5) { out << "7 ist groesser als 5" << Qt::endl; } if (3 == 4) { out << "3 ist gleich 4" << Qt::endl; // nein! } if (3 != 4) { out << "3 ist ungleich 4" << Qt::endl; } if (1) { out << "1 ist wahr" << Qt::endl; } if (0) { out << "0 ist wahr" << Qt::endl; // nein! } }
Wie du siehst, wurden die beiden falschen Aussagen übersprungen. Genau dazu dient die if-Anweisung.
Beachte, dass man zum Vergleichen von Zahlen das doppelte "==" Zeichen braucht. Das einfache "=" ist nämlich dazu reserviert, einer Variable etwas zuzuweisen, wie wir das oben schon ein paar mal gemacht haben.
Beachte auch, dass man anstelle eines Vergleichs-Ausdruckes auch immer einfach nur eine simple Zahl benutzen kann. Wobei 0 immer wie "unwahr" behandelt wird und alle anderen Zahlen "wahr" sind.
Jetzt gucken wir mal in die Dokumentation, was eigentlich QFile::open() zurück liefert:
Ach so, bei Erfolg liefert die Methode true (wahr) zurück. Dann ist unser if-Ausdruck in main.cpp ja richtig:
if ( datei.open(QIODevice::WriteOnly) ) { QTextStream strom(&datei); a.ausgeben(strom); b.ausgeben(strom); c.ausgeben(strom); datei.close(); }
Du könntest auch Befehlen, das der Computer ansonsten etwas anderes tun soll:
QFile datei(""); if ( datei.open(QIODevice::WriteOnly) ) { QTextStream strom(&datei); a.ausgeben(strom); b.ausgeben(strom); c.ausgeben(strom); datei.close(); } else { out << "Die Datei kann nicht geöffnet werden" << Qt::endl; }
void ausgeben(QTextStream& wohin) { wohin << "Name: " << name << Qt::endl; wohin << "Geburtdatum: " << geburtsdatum << Qt::endl; wohin << "Klasse: " << schulklasse << Qt::endl; wohin << "Rufnummer: " << rufnummer << Qt::endl; wohin << Qt::endl; }
Ohne "&" Zeichen würde eine Kopie des QTextStream an die Methode übergeben werden. Du könntest dann die Kopie innerhalb der Methode verändern, ohne das Original zu betreffen.
Mit "&" Zeichen wird hingegen eine Referenz (=Verweis) auf das bereits existierende Objekt übergeben. Die Referenz belegt keinen zusätzlichen Speicher.
Es ist egal, ob das Leezeichen vor oder hinter das "&" Zeichen geschrieben wird.
Beim QTextStream macht eine Kopie keinen Sinn, denn Kanäle wie "stdout" können nicht mehrfach benutzt werden. Es darf nur ein einziges Objekt geben, dass diesen "stdout" Kanal benutzt. Damit hast du nun auch die Begründung, warum hier eine Referenz auf das bereits bestehende Objekt übergeben werden muss.
Das ist ein perfektes Beispiel für eine ganz fiese schwer verständliche Fehlermeldung 🙁
Zunächst solltest du wissen, dass der Compiler trotz Fehler immer versucht, irgendwie weiter zu arbeiten. Deswegen ist die erste Fehlermeldung die wichtigste, alle weiteren könnten Folgefehler sein. Konzentriere dich bei solchen Ketten von Fehlermeldungen immer auf die erste.
Des Weiteren besteht eine sehr große Chance, Hilfe im Internet zu finden, weil irgend jemand das gleiche Problem in einem Diskussionsforum thematisiert hat. Suche also mit Google nach dem Text der Fehlermeldung. Wenn Google nichts findet, dann lasse alle Namen weg. In diesem Fall, suche nach "call to deleted constructor of 'QTextStream'" oder nach "call to deleted constructor", falls der erste Versuch nichts bringt.
Ich habe das gerade (am 26.2.2020) mal ausprobiert - der erste Versuch liefert bereits als erstes Ergebnis eine passende Erklärung.
Die Programmiersprache C++ erzeugt automatisch für jedes Objekt einen sogenannten Kopier-Konstruktor, der unter der Haube benutzt wird, wenn das Objekt kopiert wird. Also genau dein Fall, weil das "&" Zeichen fehlt. Nun bietet das Qt Framework aber auch die Möglichkeit, solche Kopier-Konstruktoren mit dem Makro Q_DISABLE_COPY zu löschen.
Das wird immer dann gemacht, wenn Kopien von dem Objekt nicht sinnvoll funktionieren würden, was auf QTextStream zutrifft.
Wähle im folgenden Dialog die Vorlage "C++ Klasse" aus. Gebe als Klassenname "Schueler" ein und klicke dann auf "weiter" und dann auf "abschließen". Deine IDE erstellt dann zwei neue Dateien (schueler.h und schueler.cpp), die links in der Projekt-Struktur erscheinen.
Eine C++ Klasse, aber zwei Dateien, warum das?
In der Programmiersprache C++ kann man man Bibliotheken ohne ihren Quelltext veröffentlichen. In Windows sind das die zahlreichen .dll Dateien, unter Linux haben sie die Endung .so.
Damit Programmierer diese Bibliotheken richtig benutzen können, brauchen sie eine eindeutige Auflistung der darin befindlichen Klasse(n) samt ihrer Attribute. Der Compiler braucht diese Information ebenfalls. Die Header-Datei mit der Endung .h ist für diese Beschreibung vorgesehen.
Der Programmcode von den Methoden befindet sich dann entweder als numerischer Maschinencode in der Bibliothek, oder als Quelltext in den .cpp Dateien. Diese Aufteilung auf zwei Dateien wird üblicherweise auch bei selbst geschriebenen C++ Klassen angewendet.
Befülle also nun die Datei schueler.cpp mit den Methoden der C++ Klasse. Der Konstruktor gehört auch dazu:
#include "schueler.h" Schueler::Schueler(QString name, QString geburtsdatum, QString schulklasse, QString rufnummer) { this->name=name; this->geburtsdatum=geburtsdatum; this->schulklasse=schulklasse; this->rufnummer=rufnummer; } void Schueler::ausgeben(QTextStream& wohin) { wohin << "Name: " << name << Qt::endl; wohin << "Geburtdatum: " << geburtsdatum << Qt::endl; wohin << "Klasse: " << schulklasse << Qt::endl; wohin << "Rufnummer: " << rufnummer << Qt::endl; wohin << Qt::endl; }
Die Entwicklungsumgebung zeigt momentan ganz viele rote Hinweise an, weil die zugehörige Header-Datei schueler.h noch leer ist:
Als nächstes widmen wir uns daher der Header-Datei, kopiere folgenden Text dort hinein:
#ifndef SCHUELER_H #define SCHUELER_H #include <QTextStream> // speichert die Daten eines Schülers class Schueler { public: QString name; QString geburtsdatum; QString schulklasse; QString rufnummer; Schueler(QString name, QString geburtsdatum, QString schulklasse, QString rufnummer); void ausgeben(QTextStream& wohin); }; #endif // SCHUELER_H
Wir schauen uns den Inhalt der beiden Dateien gleich noch genauer an. Lass uns erst einmal das Hauptprogramm main.cpp so umschreiben, dass es diese beiden neuen Dateien benutzt.
#include <TextStream> #include "schueler.h" static QTextStream out(stdout); int main() { Schueler a("Lisa Lob", "01.05.2007", "8a", "0211/1234567"); Schueler b("Hanna Kornblut", "12.08.2007", "8b", "0211/234567" ); Schueler c("Max Robinson", "20.11.2006", "8a", "0211/345678" ); a.ausgeben(out); b.ausgeben(out); c.ausgeben(out); }
Schau Dir die markierte Änderung in main.cpp an. Wir haben den ganzen Quelltext der C++ Klasse "Schueler" aus dem Hauptprogramm entfernt und durch die Anweisung
#include "schueler.h"
Bei der #include Anweisung ist der Dateiname dieses mal nicht in spitzen Klammern ("<>") eingeschlossen worden, sondern in doppelte Anführungszeichen. Dadurch weiß der Compiler, dass er die Datei in deinem Projektverzeichnis suchen muss. Dateien in spitzen Klammern sucht er hingegen im Installationsverzeichnis von Qt.
Die Header-Dateien von Qt Klassen heißen immer genau so wie die darin befindlichen Klassen, und zwar ohne Endung. Deine eigenen Header-Dateien haben hingegen klein geschriebene Dateinamen mit der Endung .h.
Die Header Datei schueler.h enthält ein paar interessante Zeilen, die von der IDE vorgegeben wurden:
#ifndef SCHUELER_H #define SCHUELER_H ... #endif // SCHUELER_H
Beachte, dass in der Datei schueler.h die Datei QTextStream inkludiert wird.
Wenn schon QTextStream inkludiert wird, weil die C++ Klasse "QTextStream" benutzt wird, müsste man dann nicht auch QString inkludieren, weil die Klasse "QString" verwendet wird?
Ja, kann man machen. Du darfst das Inkludieren von QString aber auch weg lassen, weil sich genau so eine #include Anweisung bereits in der Header-Datei von QTextStream befindet. Schau es dir an! Klicke mit gedrückter Strg-Taste auf den Dateinamen:
Hoppla, da steht aber wenig drin! Lustigerweise wird hier wiederum eine Header-Datei inkludiert. Klicke auch darauf mit gedrückter Strg-Taste, um hinein zu schauen:
Aha, da wird also die Datei Header-Datei qstring.h inkludiert. Das ist der Grund, warum du in deinem Programm die Zeile "#include <QString>" weg lassen darfst. Klicke oben auf das kleine "x" um die Dateien wieder zu schließen.
Vor vielen Jahren begann die schrittweise Umstellung unserer Computersysteme auf Unicode, um die Schriftzeichen aller Sprachen weltweit darstellen zu können. Diese Umstellung ist praktisch abgeschlossen, aber der C++ Compiler erwartet immer noch, dass einfache Zeichenketten in 8-Bit kodiert sind, um alte Programme nicht kaputt zu machen.
Du kannst allerdings die fragliche Zeile so umschreiben, damit der ganze Unicode Zeichensatz funktioniert:
out << QStringLiteral("Die Datei kann nicht geöffnet werden") << Qt::endl;
Wenn da nicht die lästigen Altlasten von Windows wären ...
Obwohl auch Windows schon lange auf Unicode umgestellt ist, benutzt die Konsole weiterhin einen alten 8-Bit Zeichensatz, und zwar die Codepage CP850. Qt kann automatisch von Unicode auf diese Codepage konvertieren, allerdings musst du das manuell angeben:
out.setCodec("CP850"); out << QStringLiteral("Die Datei kann nicht geöffnet werden") << Qt::endl;
Wenn du die folgenden Zeilen hinzufügst, dann wird setCode() bei allen anderen Betriebssystemen automatisch weg gelassen:
#ifdef Q_OS_WIN out.setCodec("CP850"); #endif out << QStringLiteral("Die Datei kann nicht geöffnet werden") << Qt::endl;
Wähle im nächsten Dialog die Vorlage Allgemein/Leere Datei aus. Die Datei soll den Namen schueler.csv bekommen. Der Inhalt soll sein:
Lisa Lob, 01.05.2007, 8a, 0211/1234567 Hanna Kornblut, 12.08.2007, 8b, 0211/2345678 Max Robinson, 20.11.2006, 8a, 0211/3456789 Natalia Safran, 03.04.2007, 8a, 0211/4567890 Murat Amir, 24.06.2007, 8c, 0211/5678901 Sasha Nimrod, 03.02.2007, 8c, 0211/6789012 Melina Grohe, 11.07.2007, 8a, 0211/7890123 Robert Schmitz, 14.12.2006, 8b, 0211/8901234 Thomas Hörner, 04.01.2007, 8a, 0211/9012345
#include <QFile> #include <QTextStream> #include <QTextCodec> int main() { QTextStream out(stdout); #ifdef Q_OS_WIN out.setCodec("CP850"); #endif QFile datei("../test/schueler.csv"); if (datei.open(QIODevice::ReadOnly)) { QTextStream strom(&datei); strom.setCodec("UTF-8"); // weil die Datei ein "ö" in UTF-8 enthält QString zeile=strom.readLine(); out << zeile << Qt::endl; datei.close(); } }
Im Fall von Windows gibt es hier zwei Besonderheiten zu beachten: Erstens haben einige Ordner (Users und Documents) englische Namen, aber der Dateimanager (Explorer) zeigt sie auf deutsch an. Zweitens musst du als Trennzeichen entweder die gezeigten Schrägstriche "/" (slashes) verwenden, oder doppelte "\\" (backslashes). Denn einfache Backslashes leiten Escape Sequenzen ein.
Die Ausgabe sieht so aus:
#include <QFile> #include <QTextStream> #include <QTextCodec> int main() { QTextStream out(stdout); #ifdef Q_OS_WIN out.setCodec("CP850"); #endif QFile datei("../test/schueler.csv"); if (datei.open(QIODevice::ReadOnly)) { QTextStream strom(&datei); strom.setCodec("UTF-8"); while (strom.atEnd()==false) { QString zeile=strom.readLine(); out << zeile << Qt::endl; } datei.close(); } }
Die while() Anweisung sorgt dafür, dass folgende Block wiederholt ausgeführt wird, und zwar solange die angegebene Bedingung wahr ist. Die Methode atEnd() von der QTextStream Klasse meldet, ob das Ende des Datenstromes erreicht wurde. Dieser Ausdruck bedeutet daher so viel wie "wiederhole, solange das Ende des Datenstromes nicht erreicht wurde".
Eine andere Variante mit exakt dem geichen Ergebnis wäre die Negation:
while (! strom.atEnd()) { ... }
#include <QFile> #include <QTextStream> #include <QTextCodec> int main() { QTextStream out(stdout); #ifdef Q_OS_WIN out.setCodec("CP850"); #endif QFile datei("../test/schueler.csv"); if (datei.open(QIODevice::ReadOnly)) { QTextStream strom(&datei); strom.setCodec("UTF-8"); while (strom.atEnd()==false) { QString zeile=strom.readLine(); QStringList teile=zeile.split(","); out << "Name: " << teile[0] << Qt::endl; out << "Geburtsdatum: " << teile[1] << Qt::endl; out << "Klasse: " << teile[2] << Qt::endl; out << "Rufummer: " << teile[3] << Qt::endl; out << Qt::endl; } datei.close(); } }
Zur Kontrolle gibt das Programm die Teile aus. Beachte, wie wir auf die einzelnen Elemente (Teile) der Liste zugreifen: Indem wir in eckigen Klammern angeben, welchen Teil wird haben wollen. Wobei [0] das erste Element liefert.
Die Ausgabe sieht so aus:
Auffällig ist hier, dass da noch ein paar Leerzeichen zu viel sind. Die kommen aus der .csv Datei. Als Nächstes schneiden wir also noch die Leerzeichen weg, und dann können wir die Objekte der C++ Klasse "Schueler" befüllen.
#include "schueler.h" #include <QFile> #include <QTextStream> #include <QTextCodec> int main() { QTextStream out(stdout); #ifdef Q_OS_WIN out.setCodec("CP850"); #endif QFile datei("../test/schueler.csv"); if (datei.open(QIODevice::ReadOnly)) { QTextStream strom(&datei); strom.setCodec("UTF-8"); while (strom.atEnd()==false) { QString zeile=strom.readLine(); QStringList teile=zeile.split(","); Schueler schueler( teile[0].trimmed(), // Name teile[1].trimmed(), // Geburtsdatum teile[2].trimmed(), // Schulklasse teile[3].trimmed()); // Rufnummer schueler.ausgeben(out); } datei.close(); } }
Eine fortlaufende Nummerierung ist hier wohl naheliegend, vielleicht so ähnlich, wie das oben schon mit den Teilen der Zeilen gemacht wurde. Das geht so:
#include "schueler.h" #include <QFile> #include <QTextStream> #include <QTextCodec> int main() { QTextStream out(stdout); #ifdef Q_OS_WIN out.setCodec("CP850"); #endif QList<Schueler> liste; out << "Lese die Datei ein..." << Qt::endl; QFile datei("../test/schueler.csv"); if (datei.open(QIODevice::ReadOnly)) { QTextStream strom(&datei); strom.setCodec("UTF-8"); while (strom.atEnd()==false) { QString zeile=strom.readLine(); QStringList teile=zeile.split(","); Schueler schueler( teile[0].trimmed(), // Name teile[1].trimmed(), // Geburtsdatum teile[2].trimmed(), // Schulklasse teile[3].trimmed()); // Rufnummer liste.append(schueler); } datei.close(); } out << QStringLiteral("Jetzt sind alle Schüler im Speicher") << Qt::endl; out << QStringLiteral("Liste die Schüler auf...") << Qt::endl; int i; for (int i=0; i<liste.size(); i++) { liste[i].ausgeben(out); } }
Als wir die Zeilen aus der .csv Datei in Teile zerlegten, lieferte uns die Methode QString::split() ein QStringList Objekt zurück, also eine Liste von QStrings.
Um viele Schüler im Speicher abzulegen, brauchst du eine Liste von Schülern. Praktischerweise enthält das Qt Framework eine universelle Listen-Klasse, die beliebige Objekte aufnehmen kann.
QList<Schueler> liste; ... liste.append(schueler);
Zur Ausgabe benutzen wir die for-Schleife. For-Schleifen wiederholen einen Block, während sie die Durchläufe zählen:
for (int i=0; i<liste.size(); i++) { liste[i].ausgeben(out); }
Die Liste nummeriert ihre 9 Elemente beginnend mit der 0 durch, also: [0], [1], [2], [3], [4], [5], [6], [7] und [8]. Deswegen soll die For-Schleife mit Zahlen von 0 bis 8 wiederholt werden.
Der Teil "int i=0" erzeugt eine Variable vom Typ int, die mit 0 initialisiert wird. Das ist der Startwert für den ersten Schleifen-Durchlauf.
Der Teil "i<liste.size()" sagt aus, wie lange die Schleife ausgeführt werden soll. In diesem Fall soll sie so lange ausgeführt werde, wie i kleiner als die Größe der Liste ist. Wenn i die 9 erreicht, stopp die Wiederholschleife, denn der Ausdruck ist dann nicht mehr wahr.
Der Teil "i++" legt fest, in welcher Schrittweite i erhöht wird. i++ ist eine häufig benutzte Abkürzung für "i=i+1".
Ich zeige dir jetzt, wie man den Debugger benutzt. Zuerst musst du sicher stellen, dass unten Links der Debug-Modus eingestellt ist:
Im Debug Modus fügt der Compiler Informationen in das Programm ein, die der Debugger benötigt, um zu funktionieren. Danach kannst du oben auf den grauen Käfer klicken, um die Debug-Ansicht zu öffnen.
Jetzt musst du dem Debugger sagen, wo er das Programm pausieren soll. Klicke dazu links neben die Zeilennummer von der Zeile "Liste die Schüler auf...". Dann erscheint dort ein roter Haltepunkt.
Starte jetzt das Programm, indem du ganz unten links auf den grünen Pfeil mit dem Käfer klickst (über dem Hammer). Es wird bis zum Haltepunkt laufen und dann pausieren. Die Ausgabe im Terminalfenster bestätigt das:
Die Debugger-Ansicht sieht nun ungefähr so aus:
Vor der Zeile 37 befindet sich ein gelber Pfeil, der die aktuelle Programmzeile anzeigt. Die Zeilen darüber sind bereits abgearbeitet worden. Im rechten Bereich wird der Inhalt der gerade bekannten Variablen angezeigt. Hier kannst du sehen, dass die Liste der Schüler 9 Elemente hat. Das erste Element habe ich aufgeklappt, damit du sehen kannst, welche Daten dort gespeichert sind.
Im unteren Bereich ist eine sehr schmale Leiste mit wichtigen Schaltflächen:
Hier kannst du den weiteren Ablauf des Programms manuell steuern. Wenn du mit der Maus auf die Symbole zeigst, erscheint eine hilfreiche Beschriftung. So findest du die Knöpfe für Einzelschritte:
Schaue Dir nochmal die Ausgabe im Terminalfenster an, es ist eine neue Zeile hinzu gekommen: "Liste die Schüler auf...". Das ist logisch, du hast die entsprechende Zeile vom Programm ja gerade ausgeführt.
Inzwischen befindest du dich innerhalb der for-Schleife. Der Debugger zeigt Dir ganz rechts oben an, dass eine neue Variable erreichbar geworden ist, nämlich i. Und i hat gerade den Wert 0.
Setze das Programm jetzt fort, indem du langsam wiederholt auf die "Einzelschritt über" Schaltfläche klickst. Beobachte, wie sich dabei der gelbe Pfeil und die Anzeige der Variablen rechts oben verändern.
Wenn i den Wert 8 erreicht, und du dann noch einmal klickst, endet das Programm. Klicke weiter auf den Button, dann wirst du sehen, dass der gelbe Pfeil jetzt ein paar mal wild hin und her springt. Das kommt daher, daß das Programm jetzt jedem Objekt die Gelegenheit gibt, eine abschließende Aufräum-Methode auszuführen, den sogenannten Destruktor.
Schließlich landest du in dieser seltsamen Ansicht:
Das ist der Maschinencode, der die Meldung "Betätigen Sie die <RETURN> Taste, um das Fenster zu schließen..." ausgibt. Hier erscheint der Maschinencode, weil du die Quelltext-Dateien von diesem Teil des Qt Frameworks nicht installiert hast.
Du kannst das Programm trotzdem durch Anklicken der "Einzelschritt über" Schaltfläche fortsetzen, oder du klickst auf die erste Schaltfläche mit der Beschriftung "GDB für test fortsetzen", dann läuft das Programm von alleine bis zum finalen Ende durch.
Beim Debuggen ist Dir vielleicht aufgefallen, dass die Variable i am Anfang noch nicht (rechts oben) angezeigt wurde. Das kommt daher, dass jede Variable nur in dem Block existiert, in dem sie erstellt wurde. In diesem Fall wurde die Variable i in der for-Schleife erstellt und existiert deswegen nur innerhalb dieser Schleife. Wenn das Ende der Schleife erreicht wurde, verschwindet die Variable wieder. Diese begrenze Sichtbarkeit nennt man Scope.
#include <QTextStream> int main(int argc, char* argv[]) { QTextStream out(stdout); out << "Anzahl der Parameter: " << argc << Qt::endl; for (int i=0; i<argc; i++) { out << "Parameter " << i << ": "<< argv[i] << Qt::endl; } }
Das Betriebssystem befüllt argc immer mit der Anzahl der Kommandozeilenargumente. Die Argumente werden als argv an das Programm übergeben und beginnend mit der 0 durch nummeriert. Das erste Argument ist immer das Programm selbst. Danach können weitere Argumente folgen. Wir benutzen hier die bereits bekannte for-Schleife, um die Argumente im Konsolen-Fenster auszugeben.
Um mehr Argumente auszuprobieren, kannst du das Projekt in der Entwicklungsumgebung konfigurieren:
Wechsle zurück in die "Editieren" Ansicht und starte das Programm erneut.
Ich denke, wir haben jetzt genug mit der Konsole experimentiert. Ich möchte Dir als nächstes zeigen, wie man bunte Grafik ausgibt.
Das Konsolen-Fenster kann nur Text ausgeben. Wir fügen jetzt ein weiteres Fenster basierend auf der Klasse QWidget hinzu, das grafische Ausgaben ermöglicht.
Dazu müssen wir zuerst in der Projekt-Datei test.pro eine Zeile ändern, um die Bibliotheken für grafische Anwendungen und Widgets einzubinden. Ändere "QT -= gui" in:
QT += gui widgets
#include <QApplication> #include <QTextStream> #include <QWidget> int main(int argc, char* argv[]) { QApplication app(argc, argv); QTextStream out(stdout); out << "Starte grafische Ausgabe..." << Qt::endl; QWidget widget; widget.show(); return app.exec(); }
Wenn du das leere grafische Fenster schließt, endet auch das Programm. Das erkennst du an der entsprechenden Meldung im Konsolen-Fenster:
int main(int argc, char* argv[]) { QApplication app(argc, argv); ... return app.exec(); }
Während Konsolen-Programme einmal von oben nach unten durch laufen, warten grafische Programme auf Ereignisse (z.B. Mausklicks und Tastendrücke), die sie abarbeiten.
Jedes grafische Programm, das auf dem Qt Framework basiert, muss genau ein QApplication Objekt enthalten. Der Konstruktor von QApplication initialisiert die Verarbeitung der Ereignisse. Die Methode QApplication::exec() enthält eine Warteschleife, welche die Ereignisse empfängt und verarbeitet.
Dazwischen können wir Befehle einfügen, die beim Programmstart ausgeführt werden sollen. In diesem Fall wird ein Objekt von der Klasse QWidget erstellt, das ist das grafische Programm-Fenster.
#include <QApplication> #include <QTextStream> #include <QWidget> int main(int argc, char* argv[]) { QApplication app(argc, argv); QTextStream out(stdout); out << "Starte grafische Ausgabe..." << Qt::endl; QWidget widget; widget.show(); QWidget widget2; widget2.show(); return app.exec(); }
Diese Zeile sieht bei Dir womöglich ein bisschen anders aus, wenn du eine andere Version von QT verwendest. Lasse die anderen Einträge unverändert, entferne nur die "console". Drücke dann wieder Strg-S, um die Änderung zu speichern. Die Entwicklungsumgebung braucht ein paar Sekunden, um die Änderung zu verarbeiten.
Starte das Programm erneut, um den Erfolg zu überprüfen. Die Konsole ist jetzt weg, es öffnen sich nur noch zwei grafische Fenster.
Beachte den rot markierten Text. Was vorher in die Konsole geschrieben wurde, erscheint jetzt im unteren Bereich der Entwicklungsumgebung. Das ist sehr praktisch, um Details zur Fehleranalyse auszugeben, die normalerweise (in den grafischen Fenstern) nicht sichtbar sein sollen.
Tausche den ganzen Quelltext von main.c durch folgenden aus:
#include <QApplication> #include <QWidget> int main(int argc, char* argv[]) { QApplication app(argc, argv); qDebug("starte Widget..."); qWarning("Warnung vor dem bissigen Hund!"); qCritical("Das ist eine Fehlermeldung."); QWidget widget; widget.show(); return app.exec(); }
Unter Linux erscheinen diese Meldungen jetzt in rot:
Das liegt daran, dass die Meldung nicht mehr über den Ausgabekanal "stdout" fließen, sondern über den Kanal "stderr". Den hättest du auch mit QTextStream verwenden können.
Der eigentliche Vorteil dieser Debug-Meldungen besteht allerdings darin, dass sie automatisch um weitere Informationen ergänzt werden können und dass man sie nach Wichtigkeit filtern kann.
Die Formatierung der Debug Meldungen lässt sich mit mit einer Umgebungsvariable steuern. Die Dokumentation von Qt schlägt folgende Zeile vor:
QT_MESSAGE_PATTERN="[%{type}] %{appname} (%{file}:%{line}) - %{message}"
Gebe dort als Name "QT_MESSAGE_PATTERN" ein, und als Wert "[%{type}] %{appname} (%{file}:%{line}) - %{message}".
Wechsele am linken Rand der Entwicklungsumgebung wieder zurück in die "Editieren" Ansicht und starte dein Programm erneut. Jetzt sehen die Debug-Meldungen anders aus:
Weil jetzt jede Zeile am Anfang mit der Wichtigkeit gekennzeichnet ist, kannst du danach filtern. Wenn du in das weiße Filter-Feld zum Beispiel "[critical]" eingibst, siehst du nur noch kritische Meldungen.
Alternativ zur Umgebungsvariable kannst du das Ausgabeformat direkt in deinem Quelltext mit der Funktion qSetMessagePattern() festlegen. Schau Dir diesen Artikel mal an, da sind noch mehr Formatier-Anweisungen aufgelistet, die dich interessieren könnten.
Die Ablaufsteuerung deines Betriebssystems verlangt, dass Fenster ihren Inhalt auf Kommando selbst zeichnen, und zwar zu beliebigen Zeitpunkten und beliebig oft. Bei Bedarf sendet das Betriebssystem ein Signal mit dem Kommando "Zeichne dich neu" an das Fenster. Dann muss das Fenster seinen eigenen Inhalt zeichnen.
Das heißt: Du kannst in deinem Quelltext nicht einfach aktiv in das Fester zeichnen, sondern musst auf so ein Signal warten. Das Warten erledigt die Klasse QApplication, es bleibt also nur die Notwendigkeit, eine Methode zu schreiben, die das Zeichnen erledigt.
Zur objektorientierten Programmierung gehört die Möglichkeit, eigene Klassen von vorhandenen abzuleiten und zusätzliche Eigenschaften (Attribute) und Methoden hinzuzufügen. Man kann auch vorhandene Methoden durch eigene ersetzen. Genau das müssen wir tun, denn die vorhandene Methode vom QWidget, die auf das Kommando "Zeichne dich neu" reagiert, macht einfach gar nichts. Deswegen ist das Fenster zur Zeit noch völlig leer.
Erstelle eine neue Klasse mit dem Namen MeinWidget:
Benutze die Vorlage für C++ Klassen und stelle dort ein, dass die Basisklasse QWidget sein soll.
Öffne die generierte Datei meinwidget.h und füge darin die Methode paintEvent() ein. Diese Methode wird aufgerufen, wenn das Fenster das Kommando "Zeichne dich neu" empfängt.
#ifndef MEINWIDGET_H #define MEINWIDGET_H #include <QWidget> class MeinWidget : public QWidget { Q_OBJECT public: explicit MeinWidget(QWidget* parent = nullptr); void paintEvent(QPaintEvent* event); signals: public slots: }; #endif // MEINWIDGET_H
#include "meinwidget.h" #include <QPainter> MeinWidget::MeinWidget(QWidget* parent) : QWidget(parent) { } void MeinWidget::paintEvent(QPaintEvent* event) { QPainter painter(this); painter.drawRect(50,50,150,100); }
Wie die Sache mit dem Ableiten genau funktioniert und was die Zeilen bedeuten, die deine Entwicklungsumgebung generiert hat, werde ich weiter unten im Kapitel Klassen und Objekte detailliert erklären.
Jetzt musst du noch die main.cpp umschreiben, damit das eigene MeinWidget anstelle von QWidget als Hauptfenster benutzt wird.
#include "meinwidget.h" #include <QApplication> int main(int argc, char* argv[]) { QApplication app(argc, argv); MeinWidget widget; widget.show(); return app.exec(); }
Wenn du aufmerksam warst, hast du die Hinweismeldung "Unused parameter 'event'" bemerkt:
Das stimmt sogar, der Parameter event wird innerhalb der Methode paintEvent() tatsächlich nicht benutzt. Das ist in diesem Fall Ok, denn wir brauchen ihn nicht. Aber wir dürfen den Parameter nicht einfach entfernen, weil das Qt Framework diese Funktion sonst nicht mehr aufrufen würde.
Was wir aber tun können ist, den Namen des Parameter durch den Kommentar "unused" ersetzen. Dann verschwindet die Warnung:
void MeinWidget::paintEvent(QPaintEvent* /*unused*/) { QPainter painter(this); painter.drawRect(50,50,150,100); }
#include "meinwidget.h" #include <QPainter> MeinWidget::MeinWidget(QWidget* parent) : QWidget(parent) { } void MeinWidget::paintEvent(QPaintEvent* /*unused*/) { QPainter painter(this); // Kopf painter.drawRect(50,50,150,100); // Augen painter.setBrush(QBrush(Qt::blue)); painter.drawEllipse(75,75,20,20); painter.drawEllipse(150,75,20,20); // Mund painter.setPen(QColor(Qt::red)); painter.drawArc(60,80,130,50,-30*16,-120*16); // Haare painter.setPen(QColor(Qt::darkGreen)); painter.drawLine(123,50,115,30); painter.drawLine(125,50,125,30); painter.drawLine(128,50,135,30); }
Jetzt wollen mir mal kontrollieren, wann und wie oft diese paintEvent() Methode eigentlich ausgeführt wird. Füge dazu eine Zeile in die paintEvent() Methode ein:
void MeinWidget::paintEvent(QPaintEvent* /*unused*/) { qDebug("paintEvent() wurde aufgerufen"); QPainter painter(this); // Kopf ... }
Wenn du die Größe des Fensters änderst, wird die paintEvent() Methode mehrfach aufgerufen. Probiere es aus und achte dabei auf die Debug-Meldungen.
Wenn du Lust hast, kannst du noch weitere Figuren in das Fenster zeichnen oder Text hinzufügen. Mache Dich mit den Funktionen von QPainter vertraut.
#ifndef MEINWIDGET_H #define MEINWIDGET_H #include <QWidget> class MeinWidget : public QWidget { Q_OBJECT public: explicit MeinWidget(QWidget* parent = nullptr); void paintEvent(QPaintEvent* event); signals: public slots: void angeklickt(); }; #endif // MEINWIDGET_H
Ändere nun auch die Datei meinwidget.cpp, um dort die Schaltfläche einzufügen und die Methode angeklickt() zu implementieren:
#include "meinwidget.h" #include <QPainter> #include <QMessageBox> #include <QPushButton> MeinWidget::MeinWidget(QWidget* parent) : QWidget(parent) { QPushButton* button=new QPushButton("Klick mich!",this); connect(button, &QPushButton::clicked, this, &MeinWidget::angeklickt); } void MeinWidget::angeklickt() { QMessageBox::information(this,"Info","Die Schaltfläche wurde betätigt"); } void MeinWidget::paintEvent(QPaintEvent* /*unused*/) { qDebug("paintEvent() wurde aufgerufen"); QPainter painter(this); // Kopf painter.drawRect(50,50,150,100); // Augen painter.setBrush(QBrush(Qt::blue)); painter.drawEllipse(75,75,20,20); painter.drawEllipse(150,75,20,20); // Mund painter.setPen(QColor(Qt::red)); painter.drawArc(60,80,130,50,-30*16,-120*16); // Haare painter.setPen(QColor(Qt::darkGreen)); painter.drawLine(123,50,115,30); painter.drawLine(125,50,125,30); painter.drawLine(128,50,135,30); }
Das Ergebnis sieht schlecht aus:
Das Hauptfenster (Widget) ist jetzt winzig klein geworden. Offenbar hat es sich automatisch an die Größe des Button angeglichen. Wir wollen aber, daß das Fenster groß bleibt, deswegen fügen wir eine weitere Zeile in den Konstruktor von MeinWidget ein:
MeinWidget::MeinWidget(QWidget* parent) : QWidget(parent) { setFixedSize(400,300); QPushButton* button=new QPushButton("Klick mich!",this); connect(button, &QPushButton::clicked, this, &MeinWidget::angeklickt); }
Probiere auch aus, was passiert, wenn der Button angeklickt wird: Die Slot-Methode angeklickt() öffnet eine QMessageBox um folgende Info-Meldung anzuzeigen.
Wir können den Button an eine bessere Position verschieben:
MeinWidget::MeinWidget(QWidget* parent) : QWidget(parent) { setFixedSize(400,300); QPushButton* button=new QPushButton("Klick mich!",this); button->move(80,190); connect(button, &QPushButton::clicked, this, &MeinWidget::angeklickt); }
QPushButton* button=new QPushButton(...);
Er soll aber viel länger Existieren, und zwar mindestens so lange, bis dieses Fenster geschlossen wurde. Genau das wird mit "new" erreicht. Damit erzeugte Objekte werden im Heap gespeichert. Das ist ein anderer Speicherbereich, der für langlebige Objekte vorgesehen ist. Der Heap umfasst den gesamten freien Arbeitsspeicher deines Computers. Er wird also von den laufenden Programmen gemeinsam verwendet. Dabei passt das Betriebssystem auf, dass die Programme sich nicht gegenseitig in die Daten gucken.
Die Variable "button" ist nun ein Zeiger auf den Speicherplatz im Heap. Das Sternchen vor "button" bedeutet so viel wie "Zeiger auf button". Man kann die Attribute und Methoden des Buttons erreichen, indem man sie indirekt über den Zeiger dereferenziert. Das bewirkt der Operator "->". Wenn du "button->move()" schreibst, rufst du die Methode move() von dem Objekt auf, auf das button zeigt.
Weiter unten im Kapitel Zeiger und Referenzen erkläre ich dieses Thema detaillierter.
Dadurch, daß das Fenster einen Button erhalten hat, ist es zu einem interaktiven Dialog geworden. Qt Creator enthält ein sehr bequemes Hilfsmittel, um solche Dialog-Fenster mit einer Art Malprogramm zu gestalten, so dass du nicht die Größen und Positionen der ganzen Eingabe-Elemente (Buttons, Textfelder, Beschriftungen, ...) alle mühsam mit Quelltext festlegen musst. In nächsten Kapitel werden wir diesen sogenannten "Qt Designer" ausprobieren.
Gehe ins Menü Datei/neu... und wähle die Vorlage "Qt Widgets Application". Der Projektname soll "test2" lauten. Klicke dann ein paar mal auf "weiter", bis das neue Projekt erstellt wurde. Das Programm ist bereits ausführbar, probiere es aus.
Öffne jetzt die Formulardatei mainwindow.ui per Doppelklick, dann kommst du in den Qt Desginer.
Die gepunktete graue Arbeitsfläche repräsentiert den Inhalt des Fensters. Dort kannst du Dialogelemente einfügen. Doch bevor du das machst, wechsle ganz links am Rand einmal kurz in die Editieren-Ansicht. Dann siehst du nämlich, dass diese .ui Datei eine lesbare XML Datei ist. Diese Ansicht ist interessant, um zu sehen, welche Eigenschaften von den Standardvorgaben abweichen, denn nur diese werden in die XML Datei geschrieben.
Zurück in der Designer-Ansicht sollst du nun einen PushButton aus der linken Leiste in die Arbeitsfläche ziehen und mit "Klick mich!" beschriften. Benutze dazu nach dem Ziehen die rechte Maustaste, dann den Befehl "Text ändern".
Das sollte jetzt ungefähr so aussehen:
Markiere rechts oben (neben der Arbeitsfläche) das Objekt "MainWindow", danach suchst du darunter im gelben Bereich die Eigenschaft "windowTitle". Ändere den Wert auf "Rette die Welt!". Starte das Programm zur Kontrolle, es sollte jetzt so aussehen:
Die Schaltfläche hat noch keine Funktion, was wir jetzt ändern. Klicke mit der rechten Maustaste aufdie Schaltfläche und wähle den Befehl "Slot anzeigen...". Wähle den Slot clicked() per Doppelklick aus. Nun wechselt die Entwicklungsumgebung in die Editor-Ansicht, welche den C++ Quelltext der Klasse mainwindow.cpp anzeigt.
Du siehst im Konstruktor einen Aufruf der Methode setupUi(this). Das ist die Stelle, wo die Konfiguration aus dem Qt Designer geladen wird.
Weiter unten hat die IDE eine neue leere Methode mit dem Namen on_pushButton_clicked() eingefügt. Das ist der Slot, der aufgerufen wird, wenn die Schaltfläche ihr Signal "clicked()" aussendet. Füge die markierten Zeilen ein:
#include "mainwindow.h" #include "ui_mainwindow.h" #include <QMessageBox> MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); } MainWindow::~MainWindow() { delete ui; } void MainWindow::on_pushButton_clicked() { QMessageBox::information(this,"Info","Die Schaltfläche wurde betätigt"); }
Ich zeige Dir jetzt, wie man Dialogfenster mit Hilfe von Layouts gestaltet.
Layouts richten Dialogelemente automatisch ordentlich aus und sorgen dafür, dass sie sich an unterschiedliche Bildschirmgrößen und Schriftgrößen anpassen.
Öffne erneut die Datei mainwindow.ui. Die Arbeitsfläche hat am oberen und unteren Rand Platz für eine Menüleiste und eine Statusleiste reserviert. Wir brauchen sie nicht. Lösche daher am rechten Rand der IDE die beiden Objekte "menuBar" und "statusBar" mit der rechten Maustaste.
Schiebe den Button weit nach unten und Ziehe dann ein "Form" Layout in die Arbeitsfläche, etwa so:
Ziehe zwei "Labels" an den linken Rand des Form Layouts und ändere ihre Beschriftungen (mittels Doppelklick) auf "Vorname:" und "Nachname:"
Ziehe zwei "Line Edit" Felder in die roten Markierung rechts neben die Labels:
Ganz rechts in der IDE sollst du nun die Objekt-Namen der beiden Eingabefelder ändern. Sie heißen derzeit "lineEdit" und "lineEdit_2". Klicke mit der rechten Maustaste auf diese Namen und dann auf "Objektnamen ändern", um sie auf "vorname" und "nachname" zu ändern:
Ziehe ein "Horizontal Layout" unter das bestehende Layout in die Arbeitsfläche und schiebe den Button dort hinein:
Links neben den Button ziehst du nun einen "Horizontal Spacer", der Button wird dadurch an den rechten Rand gedrückt.
Du hast ein Formular mit Labels und Eingabefelder, und du hast einen Button rechts ausgerichtet.
Klicke rechts in der Objektliste mit der rechten Maustaste auf MainWindow und wähle dann den Befehl "Layout -> Objekte Tabellarisch anordnen". Dadurch wird das Formular automatisch an die Fenstergröße angepasst.
Mache das Fenster ein bisschen kleiner. Etwa so:
Starte das Programm, um es zu testen.
Sieht gut aus, oder?
Wechsle am linken Rand der Entwicklungsumgebung in die Editieren-Ansicht und öffne dort die Datei mainwindow.cpp. Dort soll nun der Slot on_pushButton_clicked() so geändert werden, dass der Name aus dem Formular ausgegeben wird:
#include "mainwindow.h" #include "ui_mainwindow.h" #include <QMessageBox> #include <QDebug> MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); } MainWindow::~MainWindow() { delete ui; } void MainWindow::on_pushButton_clicked() { qDebug() << "Der Vorname ist " << ui->vorname->text(); qDebug() << "Der Nachname ist " << ui->nachname->text(); QString meldung = ui->vorname->text() +" "+ ui->nachname->text() + ", du bist ein Held!"; QMessageBox::information(this,"Info",meldung); }
Darunter wird der neue Meldungstext für die MessageBox zusammen gestellt, indem die beiden Namen zusammen gefügt werden. Das fertige Programm sieht so aus:
Nun hast du die grundsätzliche Arbeitsweise mit dem Qt Designer kennen gelernt, und wie dessen generierter Code mit selbst geschriebenem zusammen kommt.
Die anderen Dateien kannst du löschen, sie werden nicht benötigt. Allerdings kannst du das Programm noch nicht per Doppelklick starten, weil zusätzlich einige .dll Dateien (Bibliotheken) benötigt werden. Die Fehlermeldungen zeigen es:
Der Hinweis mit der "Neuinstallation" ist allerdings nicht hilfreich, denn wir haben ja gar kein Installationsprogramm. Also muss eine manuelle Prozedur her. Du findest die fehlenden Dateien im Installationsordner von Qt unter Qt/x.x.x/mingwxx_64.
Kopiere die benötigten .dll Dateien (oder einfach alle) aus dem "bin" Verzeichnis in deinen Programm-Ordner. Kopiere außerdem den ganzen "plugins" Ordner. Das Ergebnis soll so aussehen:
Jetzt ist das Programm vollständig und per Doppelklick ausführbar. Du kannst den Ordner im ZIP-Format komprimieren und dann z.B. per E-Mail verschicken.
Die anderen Dateien kannst du löschen, sie werden nicht benötigt. Manche Dateimanager können das Programm per Doppelklick starten. Falls deiner das nicht kann, öffne ein Terminal Fenster, wechsle mit dem "cd" Befehl in den richtigen Ordner und starte dann das Programm durch Eingabe von "./test".
Das Programm benutzt die "libqt5" Bibliotheken aus dem Installationsordner von Qt, oder zentral installierte Bibliotheken. Wenn beides fehlt, was auf fremden Computern recht wahrscheinlich ist, bekommst du so eine Fehlermeldung:
Die benötigten Bibliotheken findest du im Installationsorder von Qt unter Qt/x.x.x/gcc/. Vom lib Ordner brauchst du nur die Dateien, deren Name ".so" enthält. Kopiere außerdem den ganzen "plugins" Ordner. Das Ergebnis soll so aussehen:
Jetzt musst du noch den Suchpfad (LD_LIBRARY_PATH) angeben, damit Linux diese Dateien findet:
Damit man diesen Befehl nicht immer wieder neu eintippen muss, kannst du im Programmordner ein Start-Script erstellen. Das ist eine Textdatei, z.B. mit dem Namen start.sh und folgendem Inhalt:
#!/bin/bash LD_LIBRARY_PATH=./lib ./test
Kennzeichne dieses Start-Script im Dateimanager als ausführbar, danach kannst du es per Doppelklick starten.
Jetzt hast du alle Dateien zusammen. Du kannst den Ordner im .tar.gz Format komprimieren und dann z.B. per E-Mail verschicken.
In den folgenden Kapitel möchte ich Dir die Teile von der Programmiersprache C++ zeigen, die man zur Nutzung des Qt Frameworks kennen muss. Du solltest dir die Zeit nehmen, die folgenden Themen in eigenen Programmen auszuprobieren. Das Meiste lässt sich wohl am einfachsten in Konsole-Anwendungen austesten.
#include <QTextStream> static QTextStream out(stdout); int main() { int durchmesser = 145; out << "Der Umfang ist " << (durchmesser * 3.14) << Qt::endl; }
Beispiel | Beschreibung | Kommentar |
---|---|---|
"Hallo" | Zeichenkette | Gehört in Anführungszeichen. Es wird automatisch ein unsichtbares Zeichen mit dem Wert 0 angehängt, welches das Ende der Zeichenkette markiert. |
'H' | Ein einzelnes Zeichen (char) | Gehört in Hochkommata. Kann nur 8-Bit aufnehmen, kein Unicode! |
123 | Integer Zahl in dezimaler Schreibweise | Ganze Zahl, ohne Nachkommastellen, darf nicht mit 0 beginnen |
015 | Integer Zahl in oktaler Schreibweise | Sehr ungewöhnlich, muss mit 0 beginnen, oktal 015 = dezimal 13 |
0xFF | Integer Zahl in hexadezimaler Schreibweise | hexadezimal 0xFF = dezimal 255 |
0b10010111 | Integer Zahl in binärer Schreibweise | binär 0b10010111 = dezimal 151 |
12.3 | Fließkomma Zahl | Immer in amerikanischer Schreibweise, mit Punkt als Dezimal-Trennzeichen |
1.2e3 | Fließkomma Zahl in exponentieller Schreibweise | Entspricht 1,2·103 oder 1200 |
true / false | Boolescher Wert | Alle zahlen ungleich 0 werden ebenfalls als wahr behandelt |
Da wir inzwischen bei 64 Bit Rechnern angekommen sind, können Integer Zahlen inzwischen sehr große Werte haben. Früher hat man manchmal ein "l" (kleines L) hinter Integer Literale geschrieben, um zu bestimmen, dass es ein "long integer" sein soll. Man kann auch "ul" schreiben, um anzuzeigen, dass es ein "unsgined long integer" sein soll. Im Zeitalter von 64 Bit Rechnern sieht man das allerdings nur noch selten. Siehe dazu auch das nächste Kapitel Datentypen.
Innerhalb von Zeichenketten kann man sogenannte Escape Sequenzen benutzen, um spezielle Zeichen darzustellen, die man nicht direkt hinschreiben kann:
Escape-Sequenz | Beschreibung |
---|---|
\" | Anführungsstriche |
\n | Neue Zeile (Line Feed, LF) |
\r | Wagenrücklauf (Carriage Return, CR) |
\t | Tabulator |
\x40 | Zeichen mit ASCII Code in Hexadezmal, in diesem Fall ein "@" |
\\ | Das Zeichen "\" selbst (Backslash) |
Beispiel:
#include <QTextStream> static QTextStream out(stdout); int main() { out << QStringLiteral("Der Name ist \"Rumpelstielzchen\"") << Qt::endl; }
Der Name ist "Rumpelstielzchen"
Integer Zahlen können positiv und negativ sein, ohne Nachkommastellen. Die C/C++ Spezifikation legt nur eine Mindest-Größe fest. Daneben habe ich geschrieben, welche Werte der GNU C++ Compiler auf 64 Bit PC tatsächlich erlaubt.
Typ | Spezifikation | auf 64 Bit PC |
---|---|---|
char | -128 bis +127 | -128 bis +127 |
short int | -32768 bis +32747 | -32768 bis +32747 |
int | -32768 bis +32747 | -2147483648 bis +2147483647 |
long int | -2147483648 bis +2147483647 | -922337203685 |
long long int | -922337203685 |
-922337203685 |
All diese Typen gibt es auch als "unsigned" Variante (z.B. unsigned int), dann geht der Wertebereich von 0 bis doppelt so hoch wie in der obigen Tabelle angegeben. Der Typ char ist für Textzeichen gedacht, kann aber auch numerisch verwendet werden.
Manchmal braucht man Integer Typen die unabhängig von der Maschine eine ganz bestimmte Größe im Speicher haben. Diese sind in der Header-Datei <cstdint> definiert:
Typ | Größe | Bereich |
---|---|---|
int8_t | 8 Bit | -128 bis +127 |
int16_t | 16 Bit | -32768 bis +32747 |
int32_t | 32 Bit | -2147483648 bis +2147483647 |
int64_t | 64 Bit | -9223372036854775808 bis +9223372036854775807 |
Auch hier gibt es wieder "unsigned" Varianten, die heißen dann uint8_t, uint16_t, uint32_t und uint64_t.
Für Fließkommazahlen gibt es folgende Typen:
Typ | Größe | Bereich | Beschreibung |
---|---|---|---|
float | 32 Bit | -1,17·1038 bis +3,40·1038 | mit 6 Stellen Genauigkeit |
double | 64 Bit | -2,22·10308 bis +1,79·10308 | mit 15 Stellen Genauigkeit |
long double | 80 Bit | -3,36·104932 bis +1,18·104932 | mit 18 Stellen Genauigkeit |
Zu guter Letzt gibt es noch den booleschen Datentyp:
Typ | Größe | Bereich | Beschreibung |
---|---|---|---|
bool | 8-Bit | true (1) oder false (0) | Boolescher Wert |
#include <QTextStream> static QTextStream out(stdout); enum AmpelFarbe {rot, gelb, gruen}; void losfahren(AmpelFarbe farbe) { if (farbe==rot) { out << "Stopp! Die Ampel ist doch rot!" << Qt::endl; } } int main() { losfahren(rot); }
#include <QTextStream> static QTextStream out(stdout); enum AmpelFarbe {rot=10, gelb=20, gruen=30}; int main() { out << rot << Qt::endl; // gibt 10 aus }
int main() { int x; x=3; x=4; x=5; }
Die darunter folgenden Zeilen weisen der Variable x nacheinander unterschiedliche Werte zu. Zuerst hat sie den Wert 3, dann 4 und schließlich 5. Variablen können verändert werden, deswegen heißen sie so. Die Zuweisung eines Wertes erfolgt mit dem "=" Zeichen. Links davon steht immer der Name der Variable, rechts davon der Wert, der zugewiesen werden soll.
Man kann Variablen schon während der Deklaration mit einem Wert initialisieren:
int main() { int x=5; }
Variablen, die innerhalb einer Funktion oder Methode deklariert sind, werden auf dem Stack gespeichert, das ist quasi das Kurzzeitgedächtnis deines Computers. Der vergisst seine Inhalte am Ende der Funktion.
Dazu gibt es eine Ausnahme: Wenn man "static" vor die Variablen-Deklaration schreibt, wird sie stattdessen im Heap gespeichert, dem Langzeit-Gedächtnis des Computers. Der Heap vergisst seine Inhalte erst, wenn das Programm endet. Beispiel für eine statische Variable:
#include <QTextStream> static QTextStream out(stdout); void ausgeben() { static int x=0; x=x+1; out << x << Qt::endl; } int main() { ausgeben(); ausgeben(); ausgeben(); }
1 2 3Beim Ersten Aufruf erstellt die Funktion ausgeben() eine Variable vom Typ "int" mit dem Namen "x" und initialisiert sie mit dem Wert 0. Danach wird 1 addiert und ausgegeben. Bei allen folgenden Aufrufen benutzt die Funktion die bereits bekannte Variable. Sie wird nicht erneut initialisiert. Deswegen erhöht sich ihr Wert mit jeder Addition.
Eine andere Möglichkeit, Variablen auf dem Heap anzulegen siehst du in diesem Beispiel beim QTextStream:
static QTextStream out(stdout);
Das Schlüsselwort "static" hat also je nach Position zweierlei Bedeutung:
Global sichtbare Variablen sollte man tunlichst vermeiden, denn sie bergen das Risiko, sich zu verzetteln. Bei einem größeren Programm mit 100 Quelltext-Dateien kann es allzu leicht passieren, dass globale Variablen versehentlich doppelt deklariert werden oder dass man auf falsche globale Variablem zugreift, die zu anderen Dateien gehören. Die IDE fordert dich auf, globale Variablen als "static" zu kennzeichnen, um solche Fehler zu vermeiden.
Variablen sind nur in dem Block gültig, wo sie deklariert wurden:
#include <QTextStream> static QTextStream out(stdout); void ausgeben() { out << x << Qt::endl; // geht nicht } int main() { int x=3; ausgeben(); }
#include <QTextStream> static QTextStream out(stdout); int main() { int zahlen[3]; zahlen[0]=300; zahlen[1]=110; zahlen[2]=575; out << "Die erste Zahl ist " << zahlen[0] << Qt::endl; }
Beim Erzeugen des Arrays gibt man in eckigen Klammern an, wie groß das Array sein soll.
Beim Zugriff auf den Speicher gibt man in eckigen Klammern an, auf welches Element man zugreifen will. Diese Angabe nennt man Index. Hier gilt zu beachten, dass der Index immer bei 0 beginnt.
Arrays bergen das Risiko, versehentlich falsche Indices zu verwenden und damit auf physikalische Speicherzellen zuzugreifen, die nicht mehr zum Array gehören. Das ist ein großer Schwachpunkt dieser Programmiersprache. Aber es gibt einen besseren Ersatz: Die Array- und Listen-Klassen wie zum Beispiel QByteArray und QList verhindern derartige Fehler durch eingebaute Kontrollen.
Man kann Arrays schon während der Deklaration mit Werten initialisieren. Die Größe [3] ergibt sich dann automatisch:
int zahlen[] = {300,110,575};
#include <QTextStream> static QTextStream out(stdout); int main() { char erster[] = {'H','a','l','l','o',0}; char zweiter[] = "Hallo"; out << erster << Qt::endl; out << zweiter << Qt::endl; }
Beide Fälle erzeugen genau die gleiche Ausgabe. Der zweite Fall ist einfach eine bequemere alternative Schreibweise für Zeichenketten. Die Abschließende Null wird im zweiten Fall automatisch angehängt.
Beachte die Unterschiedlichen Anführungsstriche. Einfache Anführungsstriche sind für Zeichen, doppelte Anführungsstriche sind hingegen für Zeichenketten.
Zeichen und Zeichenketten leiden an einer alten Designschwäche. Sie wurden von Anfang an auf exakt 8-Bit Größe festgelegt. Für Amerikanische Texte genügt das, aber andere Sprachen brauchen mehr Schriftzeichen, als in char hinein passt. Heutige Computer nutzen fast ausschließlich den Unicode Zeichensatz, der alle Schriftzeichen der Welt einschließlich Emoticons darstellen kann. Aber die passen halt nicht alle in char rein.
Am weitesten verbreitet ist die Methode, Unicode Zeichen nach der UTF-8 Methode zu speichern (oder zu codieren). Bei UTF-8 belegen die US-Amerikanischen Zeichen nach wie vor ein char. Alle anderen Zeichen belegen zwei bis vier chars. Die Konsequenz daraus demonstriert das folgende Programm:
#include <QTextStream> static QTextStream out(stdout); int main() { #ifdef Q_OS_WIN out.setCodec("CP850"); // Für die Windows Konsole #endif char erster[] = "Schere"; char zweiter[] = "Löffel"; out << erster << " belegt " << sizeof(erster) << " Bytes im Speicher" << Qt::endl; out << zweiter << " belegt " << sizeof(zweiter) << " Bytes im Speicher" << Qt::endl; }
Schere belegt 7 Bytes im Speicher Löffel belegt 8 Bytes im SpeicherDas zweite Wort "Löffel" kann nicht korrekt dargestellt werden, weil der Buchstabe "ö" zwei chars belegt. Man sieht das in der Ausgabe, da erscheinen anstelle des "ö" plötzlich zwei komische Zeichen.
Demzufolge versagen auch zahlreiche alte Funktionen, die zur Verarbeitung von Zeichenketten vorgesehen waren. Herkömmliche Zeichenketten kannst du nur noch gebrauchen, wenn du dich auf US-Amerikanische Schriftzeichen beschränkst - aber wer will das schon?
Jetzt kommt die Lösung: Benutze stattdessen die Klasse QString und QStringLiteral.
#include <QTextStream> static QTextStream out(stdout); int main() { #ifdef Q_OS_WIN out.setCodec("CP850"); // Für die Windows Konsole #endif QString erster = QStringLiteral("Schere"); QString zweiter = QStringLiteral("Löffel"); out << erster << QStringLiteral(" hat die Länge ") << erster.size() << Qt::endl; out << zweiter << QStringLiteral(" hat die Länge ") << zweiter.size() << Qt::endl; }
In den oberen beiden Zeilen, wo das QStringLiteral einer QString Variable zugewiesen wird, könnte man QStringLiteral auch weglassen, aber so wie oben dargestellt läuft der Code schneller. Die Begründung steht dort: "In diesem Fall werden die internen Daten vom QString beim Compilieren generiert, nicht zur Laufzeit".
Beachte, dass unter Windows (und nur dort) die Ausgabe in die Konsole nochmal auf CP850 umkodiert werden muss, damit sie die Umlaute richtig darstellt. Sonst sieht das so aus:
Asiatische und Kyrillische Schriftzeichen kann die Windows Konsole generell gar nicht darstellen. Die Linux Konsole kann das aber, sie unterstützt Unicode. Ich erwarte, dass die Windows Konsole ebenfalls bald auf Unicode umgestellt wird.
#include <QTextStream> static QTextStream out(stdout); void ausgeben(int was) { out << was << Qt::endl; } int main() { int x=3; ausgeben(x); int y=10; ausgeben(y); ausgeben(100+34); }
3 10 134Die ausgeben() Funktion hat einen Parameter vom Typ "int" mit dem Namen "was". Innerhalb der Funktion kann man den Parameter über seinen Namen verwenden. Funktionen können mehrere Parameter haben, dann werden sie mit Komma getrennt:
#include <QTextStream> static QTextStream out(stdout); int addiere(int a, int b) { return a+b; } int main() { int ergebnis=addiere(10, 5); out << ergebnis << Qt::endl; return 0; // optional }
Bei der main() Funktion gibt es zwei spezielle Ausnahmen. Sie hat immer einen Rückgabewert vom Typ int, trotzdem darf man auf die "return" Anweisung verzichten. In diesem Fall geht der Compiler davon aus, dass man eine 0 zurück geben wollte.
Die zweite Ausnahme bezieht sich auf die Parameter. Und zwar darf man die main() Funktion wahlweise mit oder ohne Parameter definieren. Beides ist richtig, aber es darf nur eine von beiden im Programm geben:
#include <QTextStream> static QTextStream out(stdout); int main() { out << "Main ohne Parameter" << Qt::endl; } int main(int argc, char* argv[]) { out << "Main mit " << argc << " parametern" << Qt::endl; }
Die Parameter von Funktionen können als "const" gekennzeichnet werden, um anzuzeigen, dass sie nicht verändert werden.
#include <QTextStream> static QTextStream out(stdout); int addiere(const int a, const int b) { a=3; // geht nicht return a+b; } int main() { int ergebnis=addiere(10, 5); out << ergebnis << Qt::endl; }
#include <QTextStream> static QTextStream out(stdout); void tuwas(const QString parameter) { out << parameter << Qt::endl; // geht QString grossbuchstaben=parameter.toUpper(); // geht out << grossbuchstaben << Qt::endl; parameter.clear(); // geht nicht } int main() { QString text("Hallo"); tuwas(text); }
Die Parameter von Funktionen können Standardwerte haben:
#include <QTextStream> static QTextStream out(stdout); void ausgeben(QString text="Hallo") { out << text << Qt::endl; } int main() { ausgeben(); // gibt "Hallo" aus ausgeben("Welt!"); // gibt "Welt!" aus }
#include <QTextStream> static QTextStream out(stdout); void ausgeben(QTextStream wohin, QString text="Hallo") { wohin << text << Qt::endl; } int main() { ausgeben(out); // gibt "Hallo" aus ausgeben(out,"Welt!"); // gibt "Welt!" aus }
Beispiel | Ergebnis | Beschreibung |
---|---|---|
1 + 2 | 3 | Addition |
5 - 1 | 4 | Subtraktion |
3 * 4 | 12 | Multiplikation |
15 / 3 | 5 | Division |
13 % 6 | 1 | Rest einer Division |
i++ | i=i+1 | Post-increment: erhöhe i nach dem Auswerten des Ausdruckes |
++i | i=i+1 | Pre-increment: erhöhe i vor dem Auswerten des Ausdruckes |
j-- | j=j-1 | Post-decrement: verringere j nach dem Auswerten des Ausdruckes |
--j | j=j-1 | Pre-decrement: verringere j vor dem Auswerten des Ausdruckes |
Man kann Ausdrücke mit mehreren Operatoren verketten und durch Klammerung die Rangfolge der Operationen vorgeben:
#include <QTextStream> static QTextStream out(stdout); int main() { int ergebnis = 3 * (4 + 5) - 1; out << "Ergebnis=" << ergebnis << Qt::endl; // gibt 26 aus }
Wenn man in C++ zwei Zahlen miteinander verrechnet, wird ein Algorithmus passend zu dem komplexeren Datentyp verwendet. Das Ergebnis entspricht vom Typ her immer dem komplexeren Operanden. Bei Integer wird immer auf die nächste ganze Zahl abgerundet. Beispiele:
Das folgende Beispiel verdeutlicht den Unterschied zwischen Pre- und Post- Operationen:
#include <QTextStream> static QTextStream out(stdout); int main() { int a = 5; int b = 2 * a++; out << "a=" << a << Qt::endl; // gibt 6 aus out << "b=" << b << Qt::endl; // gibt 10 aus int x = 5; int y = 2 * ++x; out << "x=" << x << Qt::endl; // gibt 6 aus out << "y=" << y << Qt::endl; // gibt 12 aus }
Die Header Datei <cmath> stellt mathematische Funktionen zur Verfügung wie z.B. sqrt() um eine Wurzel zu ziehen, und Winkelfunktionen (Sinus, Cosinus und Tangens).
Beispiel | Ergebnis | Beschreibung |
---|---|---|
x = 5 | x ist 5 | Direkte Zuweisung (hier wird nicht gerechnet) |
x += 5 | x wurde um 5 erhöht | Addiere Wert |
x -= 5 | x wurde um 5 verringert | Subtrahiere Wert |
x *= 2 | x wurde verdoppelt | Multipliziere mit dem Wert |
x /= 2 | x wurde halbiert | Dividiere durch den Wert |
x %= 3 | x wurde auf den Reset der Divison von x / 3 gesetzt | Rest einer Division |
x &= 0b11110000 | Verknüpfe alle Bits mit UND | |
x |= 0b11110000 | Verknüpfe alle Bits mit ODER | |
x ^= 0b11110000 | Verknüpfe alle Bits mit Exklusiv-ODER | |
x <<= 3 | Alle Bits in x sind 3 Schritte nach links verschoben | Verschiebe die Bits nach links |
x >>= 3 | Alle Bits in x sind 3 Schritte nach rechts verschoben | Verschiebe die Bits nach rechts |
#include <QTextStream> static QTextStream out(stdout); int main() { int i = 5; i += 30; out << i << Qt::endl; // gibt 35 aus }
Beispiel | Ergebnis | Beschreibung |
---|---|---|
1 == 2 | false | Liefert true, wenn beide Werte gleich sind |
1 != 2 | true | Liefert true, wenn beide Werte ungleich sind |
3 < 4 | true | Liefert true, wenn der linke Wert kleiner ist |
3 > 4 | false | Liefert true, wenn der linke Wert größer ist |
5 <= 5 | true | Liefert true, wenn der linke Wert kleiner oder gleich ist |
5 >= 5 | true | Liefert true, wenn der linke Wert größer oder gleich ist |
Beispiel | Ergebnis | Beschreibung |
---|---|---|
! (1>2) | true | NICHT: Kehrt die Wahrheit des Ausdrucks dahinter um |
(2>1) && (3>4) | false | UND: Liefert true, wenn beide Ausdrücke wahr sind |
(2>1) || (3>4) | true | ODER: Liefert true, wenn mindestens einer der beiden Ausdrücke wahr ist |
#include <QTextStream> static QTextStream out(stdout); int main() { int zahl = 35; if ( zahl>5 && zahl<100 ) { out << "Die Zahl liegt zwischen 5 und 100" << Qt::endl; } }
Ich demonstriere die Bitweisen Operatoren anhand von Binärzahlen, weil man so ihre Wirkung besser sehen kann:
Beispiel | Ergebnis | Beschreibung |
---|---|---|
0b00000011 << 3 | 0b00011000 | Alle Bits n Schritte nach links verschieben (und rechts mit 0 auffüllen) |
0b11000000 << 2 | 0b00110000 | Alle Bits n Schritte nach rechts verschieben (und links mit 0 auffüllen) |
0b11110000 & 0b11000011 | 0b11000000 | UND-Verknüpfung: Im Ergebnis sind die Bits 1, die in beiden Operanden 1 sind |
0b11110000 | 0b11000011 | 0b11110011 | ODER-Verknüpfung: Im Ergebnis sind die Bits 1, die in wenigstens einem Operand 1 sind |
0b11110000 ^ 0b11000011 | 0b00110011 | Exklusiv-ODER-Verknüpfung: Im Ergebnis sind die Bits 1, die in genau einem Operand 1 sind |
~ 0b11000011 | 0b00111100 | NICHT-Verknüpfung: Im Ergebnis sind alle Bits umgepolt (0->1 und 1->0) |
Bei Klassen der objektorientierten Programmierung werden die Operatoren << und >> gerne für Ein- und Ausgabe "missbraucht", wie du das bereits vom QTextStream kennt. Das ist deswegen möglich, weil Klassen die Bedeutung von Operatoren neu definieren können.
#include <QTextStream> static QTextStream out(stdout); int main() { int a=4; int b=5; QString ergebnis = (a==b) ? "gleich" : "nicht gleich"; out << "A und b sind " << ergebnis << Qt::endl; }
A und b sind nicht gleich.
Ich benutze den ternären Operator gerne, manche anderen Entwickler meiden ihn konsequent, was auch OK ist. Man kann stattdessen nämlich auch if-else Konstrukte benutzen.
#include <QTextStream> static QTextStream out(stdout); static QTextStream in(stdin); int main() { int eingabe; out << "Gebe eine Zahl ein" << Qt::endl; in >> eingabe; if (eingabe > 100) { out << "das war mehr als 100" << Qt::endl; } else { out << "das war nicht mehr als 100" << Qt::endl; } }
#include <QTextStream> static QTextStream out(stdout); static QTextStream in(stdin); int main() { int eingabe; out << "Gebe die Nummer eines Wochentages (1-7) ein:" << Qt::endl; in >> eingabe; switch (eingabe) { case 1: out << "Montag" << Qt::endl; break; case 2: out << "Dienstag" << Qt::endl; break; case 3: out << "Mittwoch" << Qt::endl; break; case 4: out << "Donerstag" << Qt::endl; break; case 5: out << "Freitag" << Qt::endl; break; case 6: case 7: out << "Wochenende" << Qt::endl; break; default: out << "Die Eingabe ist fehlerhaft" << Qt::endl; } }
Beachte die Besonderheit für das Wochenende. Hier haben wir nur eine Ausgabe für zwei Fälle. Ganz unten kann man hinter "default:" angeben, was der Computer tun soll, wenn keiner der Fälle zutrifft. Wenn er nichts tun soll, lässt man den Teil weg.
#include <QTextStream> static QTextStream out(stdout); int main() { int zaehler=0; while (zaehler < 5) { out << zaehler << Qt::endl; zaehler=zaehler+1; } }
#include <QTextStream> static QTextStream out(stdout); int main() { int zaehler=0; do { out << zaehler << Qt::endl; zaehler=zaehler+1; } while (zaehler < 5) }
#include <QTextStream> static QTextStream out(stdout); int main() { for (int zaehler=0; zaehler<5; zaehler++) { out << zaehler << Qt::endl; } }
Beachte, dass die drei Parameter der for-Schleife ausnahmsweise mit Semikolon getrennt werden. Die drei Parameter haben folgende Bedeutung:
#include <QTextStream> static QTextStream out(stdout); int main() { for (int i=10; i>0; i-=2) { out << i << Qt::endl; } }
#include <QTextStream> static QTextStream out(stdout); int main() { int array[]={12,13,14,15}; for (int i=0; i<4; i++) { out << array[i] << Qt::endl; } }
#include <QTextStream> static QTextStream out(stdout); int main() { int array[]={12,13,14,15}; for (int wert : array) { out << wert << Qt::endl; } }
#include <QTextStream> static QTextStream out(stdout); int main() { QStringList liste={"Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"}; for (QString s : liste) { out << s << Qt::endl; } }
#include <QTextStream> static QTextStream out(stdout); int main() { QStringList liste={"Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"}; for (QString s : liste) { if (s=="Mittwoch") { continue; } out << s << Qt::endl; } }
Die Schlüsselworte "break" und "continue" lassen sich ebenso in while und do-while Schleifen verwenden.
#include <QTextStream> static QTextStream out(stdout); void ausgeben(QString x, QString y) { out << "x=" << x << Qt::endl; out << "y=" << y << Qt::endl; } int main() { QString a("Hallo"); QString b=a; ausgeben(a,b); }
Die vielen Kopien sind nicht unbedingt schlimm. Zum einen können unsere Computer solche Kopien sehr schnell anlegen. Zum Anderen enthalten die meisten Qt Klassen intern raffinierte Methoden, um unnötiges Duplizieren großer Datenmengen zu vermeiden.
Dennoch macht es Sinn, sich mit Referenzen und Zeigern zu befassen. Alleine schon deswegen, weil das Qt Framework dich an vielen Stellen dazu zwingt.
Eine Referenz ist eine Variable, die auf ein anderes bereits bestehendes Objekt verweist. Referenzen verbrauchen nur minimal Arbeitsspeicher, nicht mehr als eine Integer Zahl.
Referenzen werden bei der Deklaration der Variable durch das "&" Zeichen vor dem Namen der Variable gekennzeichnet. Das gilt ebenso für die Parameter von Funktionen. Das folgende Beispiel demonstriert dies:
#include <QTextStream> static QTextStream out(stdout); void ausgeben(QString& x, QString& y) { out << "x=" << x << Qt::endl; // gibt Hallo aus out << "y=" << y << Qt::endl; // gibt Hallo aus } int main() { QString a("Hallo"); QString& b = a; ausgeben(a,b); }
Probiere es aus:
#include <QTextStream> static QTextStream out(stdout); void ausgeben(QString& x, QString& y) { out << "x=" << x << Qt::endl; out << "y=" << y << Qt::endl; y.append(" Welt!"); } int main() { QString a("Hallo"); QString& b = a; out << "a=" << a << Qt::endl; out << "b=" << b << Qt::endl; ausgeben(a,b); out << "---------------" << Qt::endl; out << "a=" << a << Qt::endl; out << "b=" << b << Qt::endl; ausgeben(a,b); }
a=Hallo b=Hallo x=Hallo y=Hallo --------------- a=Hallo Welt! b=Hallo Welt! x=Hallo Welt! y=Hallo Welt!Die ausgeben() Funktion verändert y, indem sie " Welt!" an den String anhängt. Die danach folgenden Ausgaben unter der gestrichelten Linie beweisen, dass sich diese Änderung sowohl auf das Objekt a als auch auf alle drei Referenzen ausgewirkt hat.
Es ist nicht möglich, eine Referenz auf "nichts" verweisen zu lassen.
An dieser Stelle kommen Zeiger ins Spiel. Zeiger können auf Objekte zeigen oder auch auf nichts. Das entsprechende Schlüsselwort dazu ist "nullptr". Auch Zeiger verbrauchen nur minimal Arbeitsspeicher, so wenig wie eine Integer Zahl. Genau genommen sind Zeiger schlicht und ergreifend Zahlen, nämlich die Position des Objektes im Arbeitsspeicher.
Zeiger-Variablen erkennt man an dem Sternchen vor ihrem Namen:
#include <QTextStream> static QTextStream out(stdout); void ausgeben(QString* x, QString* y) { out << "x=" << *x << Qt::endl; out << "y=" << *y << Qt::endl; y->append(" Welt!"); } int main() { QString a("Hallo"); QString* b = &a; ausgeben(&a,b); ausgeben(&a,b); }
Eklig wird es, wenn man sich den Rest drumherum genauer anschaut:
#include <QTextStream> static QTextStream out(stdout); void ausgeben(QString* x, QString* y) { out << "x=" << *x << Qt::endl; out << "y=" << *y << Qt::endl; y->append(" Welt!"); } int main() { QString a("Hallo"); QString* b = &a; ausgeben(&a, b); ausgeben(&a, b); }
Findest du das verwirrend? Ich schon! Diese Zeiger sind kompliziert, allerdings ermöglichen sie auch sehr effiziente Lösungen, die andere Programmiersprachen gar nicht drauf haben. Insofern sind Zeiger wie extrem scharfe Messer gleichzeitig gut und schlecht.
Man sollte Zeiger sparsam verwenden - beschränkt auf die Fälle, wo sie wirklich von Vorteil sind.
Zeiger kann man auf "nichts" zeigen lassen:
#include <QTextStream> static QTextStream out(stdout); int main() { QString a("Hallo"); out << "a=" << a <<Qt::endl; QString* b=&a; if (b!=nullptr) { out << "b=" << *b <<Qt::endl; } QString* c=nullptr; if (c!=nullptr) { out << "c=" << *c <<Qt::endl; } }
Mit "new" reservierst du hingegen ein Stück vom Heap für das angegebene Objekt. Im Heap verbleiben Objekte so lange, bis du sie ausdrücklich mit "delete" wieder entfernst.
#include <QTextStream> static QTextStream out(stdout); QString* erzeuge() { QString* s=new QString("Hallo"); return s; } void benutze(QString* was) { out << *was << Qt::endl; } int main() { QString* zeiger=erzeuge(); benutze(zeiger); delete zeiger; }
Der Heap umfasst den gesamten freien Arbeitsspeicher deines Computer. Die laufenden Programme teilen sich den Heap. Allerdings verhindert das Betriebssystem, dass Programme sich gegenseitig in ihre fremden Objekte gucken.
Die Benutzung von new und delete ist riskant. Wenn du zum Beispiel versehentlich auf Speicher zugreifst, der vorher mit delete freigegeben wurde, kann alles Mögliche passieren - von Fehlfunktionen bis hin zum Absturz des Programms. Früher konnten solche Fehler sogar den ganzen Rechner zum abstürzen bringen. Ebenso gefährlich ist es, delete für die selbe Speicheradresse mehrfach aufzurufen. Dann kann es durchaus passieren, dass falscher Speicher freigegeben wird.
Ein weiteres Risiko ist, das Freigeben zu vergessen. So ein Programm nimmt dem Computer fortlaufend immer mehr Arbeistspeicher weg, bis der Rechner zum Stillstand kommt. Das nennt man Memory-Leak.
Einige andere Programmiersprachen haben anstelle von delete einen sogenannten Garbage Collector, der automatisch herausfindet, wann welcher Speicher freigegeben werden kann. Garbage Collectoren sind aber auch keine 100% saubere Lösung, da sie manchmal versagen und unter ungünstigen Umständen viel Rechenzeit verbrauchen. Qt hat dafür eine andere Lösung, die ich weiter unten vorstelle.
Um das mal mit Dingen aus dem echten Leben zu vergleichen:
Du siehst hier 10 Objekte von der Klasse Würfel.
#include <QTextStream> #include <QRandomGenerator> static QTextStream out(stdout); class Wuerfel { private: QRandomGenerator generator; public: int seiten; int wurf() { return 1 + generator.generate() % seiten; // Zufallszahl berechnen } }; int main() { Wuerfel w; w.seiten=6; out << w.wurf() << Qt::endl; out << w.wurf() << Qt::endl; out << w.wurf() << Qt::endl; out << w.wurf() << Qt::endl; out << w.wurf() << Qt::endl; }
Wenn du das Programm mehrmals ausführst, wirst du bemerken, dass es immer wieder die gleichen Zahlenfolgen ausgibt. Wir haben nämlich versäumt, den Zufallsgenerator zu initialisieren. Vernünftige Ergebnisse liefert er nur, wenn er mit wechselnden Zahlen initialisiert wird.
Dazu brauchen wir den sogenannten Konstruktor, hier markiert:
#include <QTextStream> #include <QRandomGenerator> #include <QDateTime> static QTextStream out(stdout); class Wuerfel { private: QRandomGenerator generator; public: int seiten; Wuerfel() { generator.seed(QDateTime::currentMSecsSinceEpoch()); } int wurf() { return 1 + generator.generate() % seiten; // Zufallszahl berechnen } }; int main() { Wuerfel w; w.seiten=6; out << w.wurf() << Qt::endl; out << w.wurf() << Qt::endl; out << w.wurf() << Qt::endl; out << w.wurf() << Qt::endl; out << w.wurf() << Qt::endl; }
Man kann dem Konstruktor Parameter übergeben, wie jeder anderen Funktion auch:
#include <QTextStream> #include <QRandomGenerator> #include <QDateTime> static QTextStream out(stdout); class Wuerfel { private: QRandomGenerator generator; public: int seiten; Wuerfel(int seitenZahl) : seiten(seitenZahl) { // alternativ: seiten=seitenZahl; generator.seed(QDateTime::currentMSecsSinceEpoch()); } int wurf() { return 1 + generator.generate() % seiten; // Zufallszahl berechnen } }; int main() { Wuerfel w(6); out << w.wurf() << Qt::endl; out << w.wurf() << Qt::endl; out << w.wurf() << Qt::endl; out << w.wurf() << Qt::endl; out << w.wurf() << Qt::endl; }
int main() { Wuerfel a(6); // ein sechs-seitiger Würfel Wuerfel b(6); // ein sechs-seitiger Würfel Wuerfel c(10); // ein zehn-seitiger Würfel out << a.wurf() << Qt::endl; out << b.wurf() << Qt::endl; out << c.wurf() << Qt::endl; }
int main() { Wuerfel a(6); // ein sechs-seitiger Würfel Wuerfel b=a; // Kopie vom Würfel a Wuerfel b(a) // Alternative Schreibweise, bewirkt das Gleiche out << a.wurf() << Qt::endl; out << b.wurf() << Qt::endl; }
class Wuerfel { private: QRandomGenerator generator; public: int seiten; Wuerfel(int seitenZahl) : seiten(seitenZahl) { generator.seed(QDateTime::currentMSecsSinceEpoch()); } Wuerfel(Wuerfel& original) { seiten=original.seiten; } int wurf() { return 1 + generator.generate() % seiten; } };
#include <QTextStream> #include <QRandomGenerator> #include <QDateTime> static QTextStream out(stdout); class Wuerfel { private: QRandomGenerator generator; public: int seiten; Wuerfel(int seitenZahl) : seiten(seitenZahl) { generator.seed(QDateTime::currentMSecsSinceEpoch()); } Wuerfel(Wuerfel&) = delete; int wurf() { return 1 + generator.generate() % seiten; } }; int main() { Wuerfel a(6); Wuerfel b=a; // geht nicht mehr }
#include <QTextStream> #include <QRandomGenerator> #include <QDateTime> static QTextStream out(stdout); class Wuerfel { private: QRandomGenerator generator; public: int seiten; Wuerfel(int seitenZahl) : seiten(seitenZahl) { generator.seed(QDateTime::currentMSecsSinceEpoch()); } Wuerfel(Wuerfel &) = delete; int wurf() { return 1 + generator.generate() % seiten; } }; class FarbigerWuerfel : public Wuerfel { public: QString farbe; FarbigerWuerfel(int seitenZahl, QString dieFarbe) : Wuerfel(seitenZahl), farbe(dieFarbe) {} }; int main() { FarbigerWuerfel w(6,"gelb"); out << w.wurf() << Qt::endl; }
class FarbigerWuerfel : public Wuerfel
Der neue Würfel bekommt einen neuen Konstruktor:
FarbigerWuerfel(int seitenZahl, QString dieFarbe)...
FarbigerWuerfel(int seitenZahl, QString dieFarbe) : Wuerfel(seitenZahl), farbe(dieFarbe)
#include <QTextStream> #include <QRandomGenerator> #include <QDateTime> static QTextStream out(stdout); class Wuerfel { private: int seiten; QRandomGenerator generator; public: Wuerfel(int seitenZahl) : seiten(seitenZahl) { generator.seed(QDateTime::currentMSecsSinceEpoch()); } void setSeiten(int seiten) { this->seiten=seiten; } int getSeiten() { return seiten; } int wurf() { return 1 + generator.generate() % seiten; } }; class FarbigerWuerfel : public Wuerfel { private: QString farbe; public: FarbigerWuerfel(int seitenZahl, QString dieFarbe) : Wuerfel(seitenZahl), farbe(dieFarbe) {} void setFarbe(QString neueFarbe) { farbe=neueFarbe; } QString getFarbe() { return farbe; } }; int main() { FarbigerWuerfel w(6,"gelb"); out << w.wurf() << Qt::endl; out << QStringLiteral("Die Farbe des Würfels ist ") << w.getFarbe() << Qt::endl; // Farbe und Seitenzahl ändern: w.setFarbe("blau"); w.setSeiten(20); out << w.wurf() << Qt::endl; out << QStringLiteral("Die Farbe des Würfels ist ") << w.getFarbe() << Qt::endl; }
#include <QTextStream> static QTextStream out(stdout); class Mama { public: virtual void einmal() { out << "Mama" << Qt::endl; } void dreimal() { einmal(); einmal(); einmal(); } }; class Papa : public Mama { public: virtual void einmal() { out << "Papa" << Qt::endl; } }; int main() { Papa p; p.dreimal(); }
Genau das hängt davon ab, ob vor der Methode einmal() das Schlüsselwort "virtual" steht. Mit "virtual" kommt die erwartete Ausgabe heraus: Papa Papa Papa
Wenn man aber das Schlüsselwort "virtual" entfernt, gibt der Papa fälschlicherweise "Mama Mama Mama" aus.
Gewöhne dir deswegen folgendes an: Markiere jede Methode die überschrieben werden darf als virtual, und unterlasse das Überschreiben von nicht virtuellen Methoden.
#include <QTextStream> static QTextStream out(stdout); class Mama { public: Mama() { out << "Mama wurde erzeugt" << Qt::endl; } virtual ~Mama() { out << "Mama wurde entfernt" << Qt::endl; } void lachen() { out << "hahaha" << Qt::endl; } }; class Papa : public Mama { public: Papa() { out << "Papa wurde erzeugt" << Qt::endl; } virtual ~Papa() { out << "Papa wurde entfernt" << Qt::endl; } void furzen() { out << "brrrt..." << Qt::endl; } }; int main() { Papa* p=new Papa(); p->lachen(); p->furzen(); delete p; }
Mama wurde erzeugt Papa wurde erzeugt hahaha brrrt... Papa wurde entfernt Mama wurde entferntBeim Erzeugen des Objektes (egal ob mit oder ohne new) wird sein Konstruktor aufgerufen und außerdem auch der Konstruktor der Vorlagen-Klasse(n). Beim löschen des Objektes werden dementsprechend die Destruktoren aufgerufen, und zwar in umgekehrter Reihenfolge.
Probiere jetzt das folgende fehlerhafte Programm aus:
#include <QTextStream> static QTextStream out(stdout); class Mama { public: Mama() { out << "Mama wurde erzeugt" << Qt::endl; } ~Mama() { out << "Mama wurde entfernt" << Qt::endl; } void lachen() { out << "hahaha" << Qt::endl; } }; class Papa : public Mama { public: Papa() { out << "Papa wurde erzeugt" << Qt::endl; } ~Papa() { out << "Papa wurde entfernt" << Qt::endl; } void furzen() { out << "brrrt..." << Qt::endl; } }; int main() { Mama* m=new Papa(); m->lachen(); delete m; }
Probiere das Programm, dann siehst du den Fehler in der Ausgabe:
Mama wurde erzeugt Papa wurde erzeugt hahaha Mama wurde entferntDa fehlt ein Destruktor-Aufruf! Der Speicher von Papa wurde nicht freigegeben. In größeren Programmen führen solche Fehler früher oder später zum Absturz.
Was habe ich falsch gemacht? Ich habe bei den Destruktoren das Schlüsselwort "virtual" vergessen. Weil das Wort fehlt wurde nur der Destruktor von m (also Mama) aufgerufen. Der Compiler hat sich dumm gestellt und nicht berücksichtigt, dass der Zeiger in Wirklichkeit auf ein anderes Objekt von der Klasse Papa zeigt.
Deswegen merke: Klassen von denen man weitere Klassen ableitet, brauchen einen virtuellen Destruktor. Man muss ihn hinschreiben, selbst wenn er leer sein soll, denn der automatisch erzeugte Destruktor ist nicht virtuell!
Konstruktoren sind übrigens niemals virtuell, die brauchen das nicht.
Jedes Qt Objekt kann beim Konstruieren mit einem sogenannten "parent" verknüpft werden. Wir hatten das zum Beispiel an dieser Stelle gleich zweimal:
MeinWidget::MeinWidget(QWidget* parent) : QWidget(parent) { QPushButton* button= new QPushButton("Klick mich!",this); // QPushButton(text, parent) connect(button, &QPushButton::clicked, this, &MeinWidget::angeklickt); }
Aus diesem Grund ist es in Qt Programmen nur selten nötig, Speicher aktiv mittels "delete" freizugeben. Das musst du nur bei Objekten machen, die ohne parent konstruiert wurden
Das Qt Framework benutzt Reflexion, um zur Laufzeit Objekte miteinander zu verknüpfen. Dadurch wird das Programmieren von grafischen Anwendungen mit Qt genau so bequem, wie mit anderen modernen Programmiersprachen. Da der C++ Standard aber keine Reflexion unterstützt, benutzt Qt einen Code-Generator namens Meta Object Compilers (MOC), der zusätzliche Quelltext-Dateien (im build Ordner) erzeugt. Darin stehen alle Informationen, die für Reflexion notwendig sind. Darauf wiederum baut das System der Signale und Slots auf.
Der Meta Object Compiler setzt voraus, dass die betroffenen Klassen in separate .h und .cpp Dateien aufgeteilt sind, wie ich das oben in der Einführung anhand der Schüler Klasse erklärt habe. Weiterhin müssen die gewünschten Klassen das Makro Q_OBJECT enthalten. Dieses Makro aktiviert den Meta Object Compiler.
In Qt können Objekte miteinander kommunizieren, indem sie sich Signale senden. Um ein Signal zu senden, ruft das Objekt eine seiner eigenen Signal-Methoden auf. Die gleiche oder eine andere Klasse kann das Signal empfangen, wenn sie eine Slot-Methode hat, deren Parameter zum Signal passt.
Die Verbindungen zwischen Signal und Slot werden erst zur Laufzeit des Programms hergestellt, und zwar mit der Funktion connect().
Die Namen der Signal- und Slot-Methoden kannst du dir selber nach belieben ausdenken. Ein Beispiel:
#ifndef MEINWIDGET_H #define MEINWIDGET_H #include <QWidget> class MeinWidget : public QWidget { Q_OBJECT public: explicit MeinWidget(QWidget* parent = nullptr); void paintEvent(QPaintEvent* event); signals: void timeOver(); // Signal public slots: void quitGame(); // Slot }; #endif // MEINWIDGET_H
Die connect() Funktion kann Signale mit dazu passenden Slot-Funktionen verbinden. Auf beiden Seiten sind mehrfache Zuordnungen möglich. Beispiel für eine einzelne Verbindung:
MeinWidget quelle(...); MeinWidget ziel(...); connect(quelle, &MeinWidget::timeOver, ziel, &MeinWidget::quitGame);
Wenn also das Objekt "quelle" seine eigene Methode timeOver() aufruft, dann sorgt das Qt Framework dafür, dass nun die Methode quitGame() vom Objekt "ziel" aufgerufen wird. Signal und Slots können Parameter haben, dann müssen sie aber zusammen passen.
C++ ist eine gute Basis, auf der man aufbauen kann.
Als professioneller Programmierer kann ich mir nur selten aussuchen, mit welcher Programmiersprache ich arbeite. Das wird meistens vom Projekt oder der Firma vorgegeben. Ich habe mit Basic und Pascal angefangen - beides Sprachen, die heute kaum noch von Bedeutung sind. Danach lernte ich C++, wovon ich bis heute profitiere. Die Grundlagen sind nämlich bei zahlreichen anderen Programmiersprachen sehr ähnlich, insbesondere bei Java, C# und Swift. Im übrigen wurden alle aktuellen Betriebssysteme überwiegend in C/C++ programmiert. Auch die Script-Sprache Python wurde mit C gebaut.
Manche Entwickler wundern sich, dass viele Qt Klassen scheinbar den Funktionsumfang der Standard-Template-Bibliothek (STL) duplizieren. In Wirklichkeit ist es aber anders herum, denn Qt gab es bereits ein paar Jahre vor der STL. Man kann Qt mit der STL kombinieren, ich würde das allerdings eher vermeiden.
Zum weiteren Lernen empfehle ich, ein dickes Fachbuch zu kaufen. Zum Beispiel:
Für das QT Framework brauchst du kein Buch, weil die Webseite von Qt schon sehr umfangreich ist. Ich hoffe, mein Artikel hat dich ausreichend vorbereitet, mit diesen Materialien weiter zu arbeiten.
Viel Erfolg!