Startseite

Programmieren lernen mit Qt

Mit dieser Anleitung (Tutorial) lernst du, wie man Computer programmiert, und zwar in der Sprache C++ mit dem Qt Framework.

Motivation

Warum braucht die Welt Programmierer?

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 das tun können, was ihnen vorher einprogrammiert wurde. Sie sind nicht wirklich intelligent.

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.

Arbeitsmittel

Programmiersprache C++

Programme bestehen aus einer großen Menge von numerischen Anweisungen, die von der CPU (dem "Gehirn" des Computers) ausgeführt werden:

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.

Qt Framework

Da Programmierer immer wieder ähnliche Aufgaben bekommen, haben sie in den 90er Jahren damit begonnen, sogenannte Frameworks (Rahmenwerke) aufzubauen. Das sind sehr umfangreiche Sammlungen nützlicher Bausteine aus denen man Programme zusammen setzt. Ein Beispiel ist der Drucker-Dialog, der in zahlreichen Programmen wieder verwendet wird:

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.

Entwicklungsumgebung

Bevor du deinen Computer programmieren kannst, musst du eine Entwicklungsumgebung (englische Abkürzung: IDE) installieren.

Installation unter Windows

Lade den Online-Installer von der Entwicklungsumgebung "Qt Creator" herunter. Unter Windows bekommst du beim Download eine .exe Datei, die du direkt starten kannst.

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

Mehr brauchst du nicht. Bringe die Installation zu Ende, dann kannst du die Entwicklungsumgebung Qt Creator starten.

Installation unter Linux

Lade den Online-Installer von der Entwicklungsumgebung "Qt Creator" herunter. Unter Linux bekommst du beim Download eine .run Datei, die du im Dateimanager (mit der rechten Maustaste, dann Eigenschaften) als ausführbar kennzeichnen musst, bevor du sie starten kannst.

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:

Das geht mit folgendem Befehl im Terminal-Fenster:
Debian, Ubuntu, Mint sudo apt install build-essential gdb libgl1-mesa-dev libxcb-cursor0
Fedora, RedHat, CentOS sudo yum groupinstall "C Development Tools and Libraries"
sudo yum install mesa-libGL-devel
SUSE sudo zypper install -t pattern devel_basis

Nun kannst du die Entwicklungsumgebung Qt Creator starten.

Installation auf dem Raspberry Pi

Für den Raspberry Pi gibt es keinen geeigneten Download auf der offiziellen Webseite von Qt. Gebe auf diesen Geräten stattdessen den Befehl "sudo apt install qtcreator" ein.

Einführung

Ich glaube, man lernt eine Programmiersprache am besten kennen, indem man sie benutzt. Falls du meine Erklärungen noch nicht komplett verstehst, mache dir keine Sorgen. Das Verständnis kommt mit der Zeit. Wichtig ist zunächst nur, dass du die Beispiele auf deinem Computer ans Laufen bringst.

Hallo Welt!

Eine alte Tradition besagt, daß das erste Programm "Hallo Welt!" anzeigen soll.

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();
}

Dann klicke links unten auf das grüne Dreieck, um das Programm auszuführen.

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>

sagt dem Compiler, dass er den Inhalt der genannten Textdatei hier einfügen soll. Diese befindet sich irgendwo im Installationsverzeichnis von Qt. Für jede Komponente des Frameworks gibt es eine entsprechende Datei.

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 obere Objekt hat den willkürlich gewählten Namen "out" und wird zur Ausgabe des Textes "Hallo Welt!" benutzt.

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();

Das obere Objekt ist mit dem Kanal "stdout" initialisiert. Dieser Kanal wird von deinem Betriebssystem bereitgestellt, damit das Programm Text ausgeben kann. Das untere Objekt ist mit dem Kanal "stdin" initialisiert, den dein Betriebssystem ebenfalls bereit stellt. Deine Tastatur ist mit diesem Kanal verbunden.

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;

Der Operator << schiebt den angegebenen Text in das Objekt "out". Da dieses Objekt mit dem Kanal "stdout" verbunden ist, der wiederum mit deinem Terminal-Fenster verbunden ist, erscheint der Text letztendlich im Terminal-Fenster.

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.

Konsolenanwendung

In diesem Kapitel schauen wir mal unter die "Motorhaube" wie so ein C++ Programm aussieht. Dabei wirst du auch Teile vom Qt Framework kennen lernen. Wir fangen ganz einfach an.

Text ausgeben und einlesen

Kopiere folgendes Programm in den Text-Editor der Entwicklungsumgebung:

#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();
}

So sieht die Ausgabe aus:

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;
}

Die Ausgabe sieht so aus:

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();

Mit der Methode in.readLine() wird eine Zeile vom Eingabekanal gelesen (read line = lese Zeile). Das Ergebnis dieser Methode wird in eine Variable gespeichert, die ich willkürlich "eingabe" genannt habe. Diese Variable speichert ein Objekt von der Klasse QString, das ist einfach gesagt ein Text.

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;

Dort habe ich die Variable "eingabe" in den Ausgabe-Text eingefügt. Deswegen wird an dieser Stelle die Zeichenkette aus der Variable "eingabe" angezeigt.

Jetzt haben wir nur noch eine Zeile übrig, die nicht erklärt wurde - genauer gesagt ein Block:

int main()
{
    ...
}

Das ist schlicht und ergreifend die Funktion, die dein Betriebssystem beim Start des Programms ausführt - der sogenannte Einsprungpunkt. Dein Programm kann viele Funktionen enthalten, aber die mit dem festgelegten Namen main() wird automatisch gestartet.

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;
}

Nur die Zahl 0 hat eine festgelegte Bedeutung, nämlich "Alles OK". Bei allen anderen Zahlen darf sich Programmierer selber ausdenken, was sie bedeuten sollen. Wenn man die "return" Anweisung weg lässt, wird automatisch eine 0 zurück gegeben.

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.

Deine erste eigene Funktion

In diesem Abschnitt zeige ich Dir, wozu Funktionen gut sind.

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;
}

Es kann gut sein, dass dein Lösungsansatz etwas anders aussieht. Solange es funktioniert, ist das völlig OK. In der Programmierung gibt es immer mehrere Möglichkeiten, eine Aufgabe zu erledigen. Es kann nie schaden, unterschiedliche Lösungsansätze mit anderen Programmierern zu diskutieren.

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" );
}

Das kann man schon viel besser für die ganze Schule erweitern. Die Funktion ausgeben() erspart Dir eine Menge Tipparbeit.

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 objektorientierte Konzept

C++ ist eine objektorientierte Programmiersprache. Objekte kombinieren Funktionen und die dazugehörigen Daten zu einer Einheit. Dieses Konzept ist sehr hilfreich, um große Programme übersichtlich zu gestalten.

Free image of pixabay.com

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();
}

Probiere das Programm aus. Die Ausgabe auf dem Bildschirm sieht genau so aus, wie vorher.

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()
    {
        ...
    }
};

jedoch ohne konkrete Werte festzulegen.

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".

Objekte konstruieren

Jetzt ist die main() Funktion allerdings deutlich länger geworden, als zuvor. Muss das so sein? Natürlich nicht. Man kann die Schüler-Objekte auch mit kompakten Einzeilern erstellen. Dazu muss man zu der C++ Klasse einen Konstruktor hinzufügen. Konstruktoren erzeugen Objekte und legen ihre anfänglichen Eigenschaften fest.

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();
}

Die main() Funktion kann nun den Konstruktor der C++ Klasse benutzen, um Schüler-Objekte zu erzeugen und zugleich mit konkreten Daten zu initialisieren.

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();
}
Die Zeilen mit dem "this" (diesem) bedeuten soviel wie:

Flexible Ausgabe

Die C++ Klasse wäre etwas flexibler, wenn sie ihre Ausgaben nicht zwangsweise auf den Bildschirm schreiben würde. Was ist, wenn du die Ausgabe zum Beispiel in eine Text-Datei schreiben willst? Zu diesem Zweck ergänzen wir die Methode ausgeben() um einen Parameter, der bestimmt, wohin die Ausgabe geschrieben werden soll.

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);
}

Die Methode ausgeben() hat nun einen neuen Parameter, durch den ihr Aufrufer bestimmen kann, wohin die Ausgabe geschrieben werden soll.

Ausgabe in eine Datei

Die nächste Aufgabe lautet: Schreibe das Programm so um, dass es die Schüler nicht nur auf auf dem Bildschirm auflistet, sondern auch in eine Textdatei schreibt. Das ist einfach, du musst nur ein paar Zeilen in die Datei main.cpp einfügen:

#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();
    }
}

Ganz oben kommt eine neue #include Zeile dazu, weil wir die Klasse QFile benutzen. Diese Klasse stellt Methoden zum Zugriff auf Dateien bereit.

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:

Dieses ganze build Verzeichnis kannst du jederzeit einfach löschen. Wenn du das Programm in der Entwicklungsumgebung startest, wird es neu erzeugt.

Bedingte Ausführung

Kommen wir zur Erklärung der if-Anweisung. Man benutzt sie, um einen einzelnen Befehl oder einen Block von Befehlen nur dann auszuführen, wenn eine bestimmte Bedingung zutrifft. Ein paar Beispiele:

#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!
    }
}

Die Ausgabe ist:

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();
    }

Das heißt nämlich: Wenn das Öffnen der Datei erfolgreich war, dann gebe etwas aus und schließe sie wieder. Ansonsten eben nicht.

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;
    }

Der Fehlerfall wird dieses mal absichtlich provoziert, indem wir einen leeren Dateinamen benutzen:

Referenzen statt Kopien

Beachte, das "&" Zeichen an der markierten Stelle:

    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;
    }

Das ist neu.

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.

Fiese Fehlermeldung

Versuche den gleichen Quelltext mal ohne "&" zu compilieren, und schaue Dir die Fehlermeldungen an, die dadurch entstehen:

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 am 26.2.2020 mal mit Google ausprobiert: das erste Ergebnis liefert 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. Darauf bezieht sich der Text "call to deleted constructor".

Aufteilen in mehrere Dateien

Das Programm fängt langsam an, groß zu werden. Nun ist ein guter Zeitpunkt gekommen, es auf mehrere Dateien aufzuteilen. Klicke dazu mit der rechten Maustaste auf "Quelldateien" und wähle dann den Befehl "Hinzufügen...".

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;
}

Da so eine .cpp Datei theoretisch mehrere C++ Klassen enthalten kann (macht man aber selten), verlangt der Compiler, dass man den Namen der jeweiligen Klasse vor die Methoden schreibt. Ich habe das hier markiert.

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

Und schwupps - verschwinden die roten Meldungen in der anderen .cpp Datei.

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);
}

Fertig. So ist das ganze jetzt wieder ausführbar. Teste das Programm. Die Ausgabe ist wie gehabt immer noch:

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"

ersetzt. An dieser Stelle fügt der Compiler gedanklich den Inhalt der genannten Header-Datei ein. In dieser Header-Datei ist die C++ Klasse mit ihren Methoden und Attributen beschrieben, jedoch ohne den konkreten Quelltext der Methoden. Der wiederum befindet sich in der dazugehörigen .cpp Datei.

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 

Dieses Konstrukt verhindert Fehlermeldungen für den Fall, dass die Datei schueler.h mehrfach inkludiert (eingebunden) wird. Das sind sogenannte Präprozessor Makros. Ich empfehle Dir, das zu diesem Zeitpunkt einfach mal so hinzunehmen. Später kannst du dir mal den verlinkten Wikipedia Artikel durchlesen, um das Thema zu vertiefen.

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.

Umgang mit Unicode

Ich glaube, wir sollten uns endlich mal mit Umlauten beschäftigen. Du hast bestimmt bemerkt, dass ich Umlaute bisher konsequent vermieden habe. Das ändert sich ab jetzt.

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;

QStringLiteral() akzeptiert Quelltext im Unicode Format. Das solltest du dir gut merken, denn es löst das Problem mit den Umlauten - in diesem Fall mit dem "ö".

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;

Jetzt enthält dein Quelltext UTF-8 und Qt konvertiert das zu CP850, damit die Umlaute in der Konsole richtig erscheinen.

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;

Dieser Quelltext funktioniert nun sowohl unter Linux als auch unter Windows richtig. Bei Windows wird der Codec gesetzt, bei Linux nicht.

Datei einlesen

Die nächste Aufgabe besteht darin, die Adressen aus einer separaten Textdatei zu laden anstatt sie direkt in den Quelltext zu schreiben. Klicke mit der rechten Maustaste auf den Projektnamen, dann auf "Hinzufügen...":

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

Das folgende Programm öffnet diese Datei mit Hilfe der QFile Klasse, und liest sie dann mit Hilfe der QTextStream Klasse zeilenweise ein. Lass uns erst einmal damit anfangen:

#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();
    }
}

Alle verwendeten Klassen und Anweisungen kennst du bereits. Der Dateiname ist relativ zum build Verzeichnis, wo dein ausführbares Programm liegt. Wenn du möchtest kannst du auch den vollständigen Pfadnamen angeben, zum Beispiel: C:/Users/Stefan/Documents/Programmierung/Qt/test/schueler.csv

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:

Wiederholschleife

Leider liest das Programm nur eine Zeile aus der Datei. Du brauchst eine Wiederhol-Schleife, die einen Teil des Programms so oft wiederholt, bis die ganze .csv Datei eingelesen wurde.

#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())
        {
            ...
        }

Dieser Ausdruck bedeutet so viel wie "wiederhole, solange nicht das Ende des Datenstromes erreicht wurde". Also eigentlich das Gleiche, nur anders geschrieben.

Text parsen

Jetzt wollen wir diese Zeilen in ihre Bestandteile zerlegen, um mit den einzelnen Werten viele Objekte der C++ Klasse "Schueler" zu befüllen. Diesen Vorgang nennt man "parsen" - wir parsen die Datei.

#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();
    }
}

Hier benutzen wir die Methode split() von der QString Klasse, um die Zeile in ihre Bestandteile zu zerlegen. Die split() Methode liefert eine Liste von Zeichenketten, in Form der Klasse QStringList.

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();
    }
}

Der Ausdruck teile[n] liefert einen Teil der Zeile als QString. Darauf wenden wir die Methode QString::trimmed() an, um Leerzeichen (vor und hinter dem Text) abzuschneiden. Damit initialisieren wir ein Objekt der C++ Klasse "Schueler", deren Methode Schueler::ausgeben() für eine schöne Anzeige auf dem Bildschirm sorgt.

Listen

Als wir weiter oben die Schüler-Daten direkt im Quelltext hatten, da hatte jeder Schüler eine eigene Objekt-Instanz der C++ Klasse "Schueler". Wir hatten ihnen die Namen a, b, und c gegeben. Wir hatten exakt so viele Variablen, wie Schüler. Wie würdest du die Variablen nennen wollen, wenn du 2000 Schüler in der .csv Datei hättest?

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);
    }
}

Die Ausgabe sieht so aus:

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);

QList ist eine Template-Klasse. Bei Templates schreibt man in spitze Klammern, welche Klasse sie aufnehmen sollen. Wir haben jetzt also eine Liste von Schülern, die wir durch wiederholte Aufrufe ihrer append() Methode befüllen.

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 Syntax (Schreibweise) der for-Schleife sieht kompliziert aus, aber man gewöhnt sich dran. Was passiert hier?

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".

Debuggen

Um den Programmablauf besser nachzuvollziehen, kannst du den Debugger von deiner Entwicklungsumgebung benutzen. Mit einem Debugger kannst du das Programm Zeile für Zeile ausführen und dabei den Inhalt aller Objekte anschauen. Der Debugger gibt dir Einblick in die inneren Abläufe deines Programms. Die Anleitung dazu ist dort.

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:

Klicke jetzt langsam mehrmals auf die "Einzelschritt über" Schaltfläche, bis du bei "liste[i].ausgeben(out)" angekommen bist.

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.

Kommandozeilen-Argumente

Konsolen-Programme werden häufig mit Kommandozeilenargumenten gestartet, die das Verhalten des Programms steuern. So kennst du womöglich den Befehl "rm" zum Löschen einer Datei, der als Parameter den Namen der zu löschen Datei erwartet. Ich zeige Dir jetzt, wie dein eigenes Programm solche Kommandozeilenargumente empfangen kann:

#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;
    }
}

Starte das Programm in der Entwicklungsumgebung, dann wirst du folgende Ausgabe erhalten:

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.

Grafik ausgeben

Ein leeres Fenster

Dieses Kapitel baut auf den vorherigen auf!

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

Drücke Strg-S, um die Änderung zu speichern. Die Entwicklungsumgebung braucht nun ein paar Sekunden, um die grafischen Bibliotheken einzubinden. Das erste grafische Programm soll so aussehen:

#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:

Ereignisse steuern den Ablauf

Die erste wichtige Änderung hier ist der Einsatz von QApplication:

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.

Viele Fenster

Probiere mal aus, zwei Fenster zu starten:

#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();
}

Jetzt hat das Programm tatsächlich insgesamt drei Fenster:

Meldungen ohne Konsole ausgeben

Für die weiteren Experimente mit Grafik brauchen wir das Konsolen-Fenster nicht mehr, deswegen entferne in der Datei test.pro die "console" aus der "CONFIG" Zeile:

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();
}

Wir haben jetzt nur noch ein Fenster übrig gelassen, und die Ausgabe von QTextStream auf spezielle Funktionen für Debug-Meldungen umgestellt.

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}"

Solche Umgebungsvariablen stellt man in der Regel zentral in der Systemsteuerung von Windows ein, bzw. unter Linux in der Datei /etc/profile. Für den Anfang ist es aber besser, das erst einmal nur in der Entwicklungsumgebung für dieses eine Projekt zu tun. Ich habe im folgenden Bild dargestellt, wie du dort hin kommst:

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.

Bunte Grafik zeichnen

Wir wollen jetzt eine bunte Grafik in das Widget zeichnen.

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
Anschließend musst du dem Quellcode der Methode paintEvent() in die generierte Datei meinwidget.cpp einfügen:
#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);
}

Wir haben hier eine eigene Klasse von der QWidget abgeleitet, welche die Methode paintEvent() durch eine eigene Variante ersetzt. In dieser Methode benutzen wir ein QPainter, um in das Fenster ein Rechteck hinein zu zeichnen.

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();
}

Probiere das Programm aus:

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);
}

Lass uns noch ein paar weitere grafische Elemente zur Methode paintEvenet() hinzufügen:

#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
    ...
}

Beim Programmstart wird die Methode einmal aufgerufen, was logisch ist:

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.

Ein Button zum Anklicken

Ich zeige Dir jetzt, wie man eine Schaltfläche hinzufügt, die eine Funktion auslöst. Du musst dazu eine Methode zur Datei meinwidget.h hinzufügen.

#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

Diese Methode soll aufgerufen werden, wenn die Schaltfläche (die wir gleich einfügen) das Signal "ich wurde angeklickt" aussendet. Weil die Methode das Signal empfängt wird sie "Slot" genannt.

Ä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);
}

Wir haben zum Konstruktor von MeinWidget zwei Zeilen hinzugefügt, um einen neuen QPushButton zu erstellen und um diesen mit der Slot-Methode angeklickt() zu verbinden.

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 das Programm aus.

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);
}

Stack versus Heap

Beachte, dass im Konstruktor von MeinWidget das Schlüsselwort "new" benutzt wurde:

QPushButton* button=new QPushButton(...);

Normale Objekte belegen Speicherplatz im Stack. Alles was sich auf dem Stack befindet, wird am Ende der Methode wieder entfernt. Die Konsequenz wäre, dass der Button nach Ausführung des Konstruktor nicht mehr existieren würde.

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.

Qt Designer

In diesem Kapitel zeige ich Dir, wie du ein Dialog-basiertes Programm mit dem Qt Designer erstellen kannst. Du kannst das vorherige "test" Projekt schließen, wir werden ein neues Projekt anlegen.

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");
}

Starte das Programm erneut, um es auszuprobieren. Klicke auf den Button.

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);
}

Wie du an diesem Beispiel sehen kannst, lässt sich die qDebug() Funktion genau so benutzen, wie das QTextStream Objekt - mit dem << Operator.

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.

Programm Teilen

Wenn du dein Programm mit jemandem teilen oder ohne Entwicklungsumgebung starten möchtest, musst du je nach Betriebssystem unterschiedlich vorgehen.

Programm Teilen unter Windows

Du findest die Ausgabe des Compilers im gleichen Ordner, wo auch dein Projekt gespeichert ist. Der Ordner hat einen auffällig langen Namen, der mit "build" beginnt. Darin befindet sich eine Datei mit der Endung .exe, in diesem Fall "test.exe". Das ist dein Maschinencode in ausführbarer Form, diese Datei musst du teilen.

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.

Programm Teilen unter Linux

Du findest die Ausgabe des Compilers im gleichen Ordner, wo auch dein Projekt gespeichert ist. Der Ordner hat einen auffällig langen Namen, der mit "build" beginnt. Darin befindet sich eine Datei ohne Endung, in diesem Fall "test". Das ist dein Maschinencode in ausführbarer Form, diese Datei musst du teilen.

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.

Lehrstoff

Die vorherigen Kapitel zur Einführung haben dir einen ersten Überblick verschafft wie man Programme mit Qt Creator erstellt.

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.

Literale

Ein Literal ist ein konkreter Wert, der direkt im Quelltext steht. Beispiel:

#include <QTextStream>

static QTextStream out(stdout);

int main()
{
    int durchmesser = 145;
    out << "Der Umfang ist " << (durchmesser * 3.14) << Qt::endl;
}

Die markierten Teile sind Literale. Die Programmiersprache unterscheidet zwischen unterschiedliche Typen von Literalen:

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)

(Es gibt noch ein paar mehr, die habe ich aber noch nie benutzt)

Beispiel:

#include <QTextStream>

static QTextStream out(stdout);

int main()
{
    out << QStringLiteral("Der Name ist \"Rumpelstielzchen\"") << Qt::endl;
}
Gibt aus:
Der Name ist "Rumpelstielzchen"

Datentypen

Alle Variablen und Parameter müssen einen Typ haben. Die Programmiersprache kennt neben Klassen folgende einfache Typen.

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 -9223372036854775808 bis +9223372036854775807
long long int -9223372036854775808 bis +9223372036854775807 -9223372036854775808 bis +9223372036854775807

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

Wobei hier die Besonderheit gilt, dass der Computer alle Zahlen ungleich 0 für wahr hält.

Aufzählungen

Aufzählungen (englisch: enumerations) werden gerne benutzt, wenn man nur ganz bestimmte wenige Werte zulassen will. Zum Beispiel für die Farben einer Ampel:

#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);
}

Für rot benutzt der Compiler intern die Zahl 0, für gelb die 1 und für grün die 2. Man kann die Zahlen alternativ dazu selber festlegen:

#include <QTextStream>

static QTextStream out(stdout);

enum AmpelFarbe {rot=10, gelb=20, gruen=30};

int main()
{
    out << rot << Qt::endl; // gibt 10 aus
}

Variablen

Variablen reservieren einen Platz im Arbeitsspeicher des Computers, um dort konkrete Werte zu speichern.

int main()
{
    int x;
    x=3;
    x=4;
    x=5;
}

Dieses Programm definiert in der markierten Zeile eine Variable vom Typ "int" mit dem Namen "x". Der Typ int legt fest, dass die Variable Zahlen (ohne Nachkommastellen) speichern kann. Im Kapitel Datentypen findest du weitere Typen erklärt.

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();
}

Das Programm erzeugt die Ausgabe:

1
2
3

Beim 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);

Die Variable "out" befindet sich außerhalb aller Funktionen. Solche Variablen nennt man globale Variablen. Sie sind für das gesamte Programm erreichbar, es sei denn, du schreibst zusätzlich das Wort "static" davor, wie im obigen Beispiel. An dieser Stelle bedeutet das Wort "static", daß die Variable für andere Quelltext-Dateien unsichtbar sein soll.

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();
}

Die Variable x wurde innerhalb der main() Funktion deklariert, deswegen ist sie nur dort verwendbar. Andere Funktionen haben darauf keinen direkten Zugriff.

Arrays

Normale Variablen speichern jeweils nur genau einen Wert. Arrays speichern viele Werte:

#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;
}

Hier wird eine Array-Variable (oder kurz: ein Array) erstellt, das Platz für drei Elemente hat. Danach werden die drei Speicherplätze mit Werten belegt. Zum Schluss wird das erste Element auf dem Bildschirm ausgegeben.

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};

Es gibt auch mehrdimensionale Arrays, aber die erkläre ich hier nicht, weil sie in Qt Anwendungen sehr selten vorkommen.

Zeichenketten

Eine besondere Variante von Arrays sind die Zeichenketten. Zeichenketten sind Arrays von Zeichen.

#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;
}

Der Datentyp "char" kann ein Zeichen speichern. Ein Array aus Zeichen nennt man Zeichenkette, wobei Zeichenketten am Ende mit einer 0 Angeschlossen werden müssen (nicht verwechseln mit dem Zeichen '0'). Die 0 kennzeichnet das Ende der Zeichenkette.

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;
}

Die Ausgabe ist:

Schere belegt 7 Bytes im Speicher
Löffel belegt 8 Bytes im Speicher

Das 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;
}

Die Klassen QString und QStringLiteral speichern nicht Arrays von chars, sondern sie können mit echten Unicode Zeichenketten umgehen.

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.

Funktionen

Funktionen dienen dazu, das Programm zu strukturieren und Teile des Codes wiederverwendbar zu machen.

#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);
}

Hier wird die Funktion ausgeben() mehrmals benutzt, um die Werte der Variablen x, y, sowie des Ausdrucks "100+34" auszugeben. Die Ausgabe sieht so aus:

3
10
134

Die 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
}

Das obige Beispiel zeigt außerdem, dass Funktionen Ergebnisse mit einem bestimmten Typ zurück liefern können. Funktionen ohne Ergebnis haben den Typ "void", das hast du weiter oben bei der ausgeben() Funktion gesehen.

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;
}

Der erste Parameter der main() Funktion ist die Anzahl der Kommandozeilen-Argumente. Der zweite Parameter enthält die Kommandozeilen-Argumente. Auf das Sternchen gehe ich später im Kapitel Zeiger ein.

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;
}

Falls der Parameter ein Objekt ist, bewirkt das Schlüsselwort const, dass man nur Methoden aufrufen kann, die ebenfalls als const gekennzeichnet sind. Solche Methoden versprechen, das Objekt nicht zu verändern. Beispiel:

#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);
}

Du kannst hier nicht die Methode clear() aufrufen, weil der Parameter als const gekennzeichnet wurde aber die Methode clear() nicht. Die Methode clear() kann auch gar nicht const sein, weil sie den String verändert.

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
}

Da der Parameter von der Funktion ausgeben() einen definierten Standardwert hat, kann man die Funktion ohne Parameter aufrufen. Bei Funktionen mit vielen Parametern klappt das aber nur am rechten Ende:

#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
}

Hier ist es nicht möglich, die beiden Parameter zu vertauschen (links den "text" und rechts das "wohin").

Operatoren

Rechnen

Beispiel Ergebnis Beschreibung
+ 2 3 Addition
- 1 4 Subtraktion
* 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
}

Im oberen Fall wird zuerst der Ausdruck "2 * 5" berechnet und danach die Variable a erhöht. Im unteren Fall wird zuerst die Variable x erhöht und dann "2 * 6" berechnet.

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).

Zuweisungen

Zuweisungen benutzen die Variable auf ihrer linken Seite als ersten Operanden und speichern das Ergebnis in diese Variable:

Beispiel Ergebnis Beschreibung
= 5 x ist 5 Direkte Zuweisung (hier wird nicht gerechnet)
+= 5 x wurde um 5 erhöht Addiere Wert
-= 5 x wurde um 5 verringert Subtrahiere Wert
*= 2 x wurde verdoppelt Multipliziere mit dem Wert
/= 2 x wurde halbiert Dividiere durch den Wert
%= 3 x wurde auf den Reset der Divison von x / 3 gesetzt Rest einer Division
&= 0b11110000 Verknüpfe alle Bits mit UND
|= 0b11110000 Verknüpfe alle Bits mit ODER
^= 0b11110000 Verknüpfe alle Bits mit Exklusiv-ODER
<<= 3 Alle Bits in x sind 3 Schritte nach links verschoben Verschiebe die Bits nach links
>>= 3 Alle Bits in x sind 3 Schritte nach rechts verschoben Verschiebe die Bits nach rechts

Beispiel:

#include <QTextStream>

static QTextStream out(stdout);

int main()
{
    int i = 5;
    i += 30;
    out << i << Qt::endl;  // gibt 35 aus
}

Vergleiche

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

Logische Operatoren

Logische Operatoren verknüpfen Aussagen die wahr oder falsch sind:

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; 
    }
}

Bitweise Operatoren

Die Bitweisen Operatoren werden in den meisten C++ Programmen nur selten verwendet. Sie funktionieren wie elektronische Logik-Gatter, allerdings für viele Bits parallel. Elektroniker sollten damit vertraut sein.

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.

Der Ternäre Operator

Der Ternäre Operator "? :" testet, ob eine Bedingung wahr ist. Wenn ja, liefert er den ersten Wert (vor dem Doppelpunkt) zurück, ansonsten liefert er den zweiten Wert (hinter dem Doppelpunkt). Beispiel:

#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;
}

Die Ausgabe lautet

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.

If-Else Bedingung

Computer treffen Entscheidungen: Wenn das Konto leer ist, kannst du kein Geld holen. Dafür dient in der Programmiersprache C++ das Schlüsselwort if:

#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;
    }
}

Der obere Teil wird ausgeführt, wenn der Ausdruck in den Klammern wahr oder nicht 0 ist. Den else-Teil kann man weg lassen, wenn man ihn nicht braucht.

Switch-Case

Die Switch-Case Anweisung kann bei numerischen Werten verwendet werden, um lange Ketten von if-Abfragen zu ersetzen:

#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;
    }
}

Die break Anweisung sorgt dafür, dass der Switch-Block an dieser Stelle abgebrochen wird. Ohne Break Anweisung würde der Computer bei Eingabe von 1 sämtliche Wochentage ausgeben.

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.

Schleifen

Schleifen dienen dazu, Teile des Programms zu wiederholen.

While Schleife

Die while Schleife wiederholt ihren Block solange der Ausdruck in den Klammern wahr oder nicht 0 ist. Das folgende Beispiel gibt die Zahlen 0, 1, 2, 3 und 4 aus.

#include <QTextStream>

static QTextStream out(stdout);

int main()
{
    int zaehler=0;

    while (zaehler < 5)
    {
        out << zaehler << Qt::endl;
        zaehler=zaehler+1;
    }
}

Dieses Beispiel gibt die Zahlenfolge 0, 1, 2, 3, 4 aus. Wenn du als Bedingung true schreibst, bekommst du eine Endlosschleife, denn true ist immer wahr.

Do-While Schleife

Die do-while Schleife wiederholt ihren Block solange der Ausdruck in den Klammern wahr oder nicht 0 ist. Der Unterschied zum vorherigen Beispiel ist, dass die do-while Schleife mindestens einmal ausgeführt wird, da die Bedingung erst am Ende geprüft wird.

#include <QTextStream>

static QTextStream out(stdout);

int main()
{
    int zaehler=0;

    do
    {
        out << zaehler << Qt::endl;
        zaehler=zaehler+1;
    }
    while (zaehler < 5)
}

Dieses Beispiel gibt ebenfalls die Zahlenfolge 0, 1, 2, 3, 4 aus.

For Schleife

Die for-Schleife ist eine kompaktere Variante der oben gezeigten while-Schleifen. Ein typischer Anwendungsfall ist das Hochzählen einer Variable:

#include <QTextStream>

static QTextStream out(stdout);

int main()
{
    for (int zaehler=0; zaehler<5; zaehler++)
    {
        out << zaehler << Qt::endl;
    }
}

Auch dieses Beispiel gibt die Zahlenfolge 0, 1, 2, 3, 4 aus.

Beachte, dass die drei Parameter der for-Schleife ausnahmsweise mit Semikolon getrennt werden. Die drei Parameter haben folgende Bedeutung:

  1. Der erste Teil wird einmal am Anfang ausgeführt.
  2. Der zweite Teil entscheidet, ob die Schleife ausgeführt werden soll. Nämlich wenn der Ausdrauch wahr oder nicht 0 ist.
  3. Der dritte Teil wird am Ende des Blockes bei jedem Durchlauf ausgeführt.

Hier mal ein anderes Beispiel, wo die Variable i in 2er Schritten verringert wird:

#include <QTextStream>

static QTextStream out(stdout);

int main()
{
    for (int i=10; i>0; i-=2)
    {
        out << i << Qt::endl;
    }
}

Dieses Beispiel gibt die Zahlenfolge 10, 8, 6, 4, 2 aus.

For Schleife mit Arrays und Listen

Um den Inhalt eines Arrays auszugeben, würde man traditionell so vorgehen:

#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;
    }
}

Dieser Lösungsansatz ist ein bisschen riskant, denn du musst darauf achten, dass die Schleife genau so oft (vier mal) wiederholt wird, wie das Array Elemente enthält. Deutlich eleganter ist diese Variante:

#include <QTextStream>

static QTextStream out(stdout);

int main()
{
    int array[]={12,13,14,15};

    for (int wert : array)
    {
        out << wert << Qt::endl;
    }
}

Das Gleiche funktioniert auch mit praktisch allen Listen-Objekten:

#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;
    }
}

Schleifen abbrechen

Mit dem Schlüsselwort continue kannst du den aktuellen Durchlauf abbrechen und mit dem nächsten Wert fortfahren. Das folgende Beispiel gibt alle Wochentage aus, außer den Mittwoch.

#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;
    }
}

Das Schlüsselwort "break" würde hingegen die ganze Schleife abbrechen, so dass nur "Montag" und "Dienstag" ausgegeben würden.

Die Schlüsselworte "break" und "continue" lassen sich ebenso in while und do-while Schleifen verwenden.

Zeiger und Referenzen

Bei "normalen" Zuweisungen und Funktionsaufrufen werden Objekte kopiert. Beispiel:

#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);
}

Hier ist a ein Objekt vom Typ QString, welcher mit dem Wort "Hallo" initialisiert wurde. b ist formell eine Kopie von a, also ein zweites Objekt das seinen eigenen Speicherplatz belegt. Beim Funktionsaufruf werden weitere Kopien angelegt: Das Objekt a wird in den Funktionsparameter x kopiert und das Objekt b wird in den Funktionsparameter y kopiert.

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);
}

In diesem Beispielprogramm gibt es nur ein einziges QString Objekt (a). Die Variable b und auch die Parameter x und y sind Referenzen auf dieses eine Objekt. Die Konsequenz daraus ist: wenn a, b, x oder y verändert wird, betrifft das alle vier gleichzeitig. Letztendlich verweisen alle vier auf das selbe Objekt im Speicher.

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);
}
Ausgabe:
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);
}

Mal kommt ein Sternchen vor den Namen der Variable, mal ein Und-Zeichen und dann haben wir noch diesen komischen Pfeil "->".

Beim Zugriff auf Zeiger (nicht bei ihrer Deklaration!) bedeuten die Zeichen folgendes:

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;
    }
}

Die dritte Ausgabe findet nicht statt, weil der Zeiger c auf nichts zeigt. Mit Referenzen wäre das nicht umsetzbar.

New und Delete

Weiter oben habe ich erklärt, das Variablen nur in dem Block gültig sind, wo sie deklariert wurden. An Ende jeder Funktion wird der Speicher (Stack) dieser Variablen wieder frei gegeben.

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;
}

Die Funktion erzeuge() Erzeugt ein QString Objekt auf dem Heap Speicher und liefert einen Zeiger zurück, der auf das Objekt zeigt. Die Funktion benutze() gibt das Objekt auf dem Bildschirm aus. In der main() Funktion werden diese beiden Schritte nacheinander aufgerufen. Damit hast du den Beweis, dass das Objekt auch nach Beendigung der Funktion erzeuge() existiert. Ganz zum Schluss wird der Speicher wieder freigegeben.

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.

Klassen und Objekte

Klassen fassen Daten und Funktionen zu sinnvollen Einheiten zusammen. Damit kann man große Programme übersichtlich gestalten. Wenn man Speicher für eine Klasse belegt, hat man ein Objekt erstellt. Das Objekt ist eine konkrete Ausprägung der Klasse.

Um das mal mit Dingen aus dem echten Leben zu vergleichen:

Du siehst hier 10 Objekte von der Klasse Würfel.

Attribute und Methoden

Die Klasse Würfel hat eine einstellbare Anzahl von Seiten (Rollenspieler brauchen das) und eine Funktion, die einen Wurf berechnet:

#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;
}

Dieses Programm gibt 5 Zufallszahlen zwischen 1 und 6 aus. Die Funktionen von Klassen (in diesem Fall "wurf") bezeicnet man als Methoden. Die Variable w ist ein Objekt (oder eine Instanz) von der Klasse Wuerfel.

Konstruktoren

Konstruktoren haben die Aufgabe, ein Objekt beim Erstellen zu initialisieren.

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;
}

Der Konstruktor initialisiert den Zufallszahlen-Generator mit der aktuellen Uhrzeit. Wenn du das Programm jetzt mehrmals ausführst, erzeugt es daher immer wieder andere Zahlenfolgen.

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;
}

So legen wir direkt beim Anlegen des Objektes die Anzahl seiner Seiten fest.

Objekt-Instanzen

Das Programm kann beliebig viele Objekte von der Klasse Wuerfel erzeugen:

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;
}

Kopier-Konstruktor

Der Compiler erzeugt automatisch einen Kopier-Konstruktor, mit dem man das Objekt (den Würfel) kopieren kann:

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;
}

Falls dieser automatisch erzeugte Kopierkonstruktor mal nicht tut, was er soll, kann man ihn durch einen eigenen überschreiben:

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;
    }
};

Man kann das Kopieren verbieten, indem man den Kopier-Konstruktor löscht:

#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
}

Das QT Framework stellt für den gleichen Zweck das Makro Q_DISABLE_COPY zur Verfügung.

Vererbung

Klassen können voneinander abgeleitet werden. Man sagt dann, dass die neue Klasse die Eigenschaften der alten erbt. Sie kann dann weitere Eigenschaften hinzufügen oder vorhandene Methoden überschreiben (durch neue ersetzen). Als Beispiel erstellen wir einen neuen Würfel der als neue Eigenschaft eine Farbe hat:

#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;
}

Der FarbigeWuerfel erbt alle Eigenschaften von Wuerfel, und zwar so, dass die geerbten öffentlichen Eigenschaften weiterhin öffentlich bleiben:

class FarbigerWuerfel : public Wuerfel

Wenn man das "public" weglassen würde, dann wären alle geerbten Eigenschaften von außen nicht mehr zugänglich.

Der neue Würfel bekommt einen neuen Konstruktor:

FarbigerWuerfel(int seitenZahl, QString dieFarbe)...

Beim Initialisieren ruft dieser zuerst den geerbten Konstruktor des originalen Würfels auf und initialisiert danach sein Attribut "farbe":

FarbigerWuerfel(int seitenZahl, QString dieFarbe) : 
        Wuerfel(seitenZahl), farbe(dieFarbe)

Getter und Setter

Viele Entwickler (so auch die Macher von Qt) haben sich angewöhnt, Attribute als private zu kennzeichnen und Zugriffe darauf über sogenannte getter und setter Methoden zu kapseln:

#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;
}

Beachte die Zeile "this->seiten=seiten". Weil hier das Argument der Funktion genau so heißt, wie das Attribut des Objektes müssen wir dem Compiler mit "this->" mitteilen, das sich die linke Seite der Zuweisung auf die Farbe von diesem (this) Objekt bezieht.

Virtuelle Methoden

Virtuelle Methoden ermöglichen abgeleiteten Klassen, die Methoden ihrer Vorlage zu überschreiben. Probiere dazu bitte folgendes Beispiel aus:

#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();
}
  

Die main() Funktion ruft die Methode dreimal() auf, welche der Papa von Mama geerbt hat. Diese dreimal() Methode ruft wiederum 3 mal die Methode einmal() auf, aber von welcher Klasse?

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.

Destruktoren

Als Gegenstück zu den Konstruktoren können Klassen auch Destruktoren haben. Diese werden automatisch ausgeführt, wenn das Objekt aus dem Speicher entfernt wird. Die Aufgabe des Destruktors ist, aufzuräumen. Falls das Objekt irgend etwas erzeugt hat, was wieder abgebaut werden muss, ist der Destruktor der richtige Platz für den entsprechenden Code. Beispiel:

#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;
}

Der Papa erbt alle Eigenschaften von Mama, und zusätzlich kann er furzen (Frauen furzen nicht). Das Programm erzeugt die Ausgabe:

Mama wurde erzeugt
Papa wurde erzeugt
hahaha
brrrt...
Papa wurde entfernt
Mama wurde entfernt

Beim 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;
}

Hier benutze ich einen Zeiger auf eine Mama, lasse ihn tatsächlich aber auf ein Objekt von der Klasse Papa zeigen. Das ist erlaubt, weil Papa von Mama geerbt hat. Bis hierhin ist noch alles OK.

Probiere das Programm, dann siehst du den Fehler in der Ausgabe:

Mama wurde erzeugt
Papa wurde erzeugt
hahaha
Mama wurde entfernt

Da 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.

Speicherverwaltung von Qt

Weiter oben erwähnte ich, dass die Nutzung von Heap mittels "new" und "delete" fehlerträchtig ist, weil man das "delete" leicht vergisst oder zum falschen Zeitpunkt ausführt. Das Qt Framework entschärft diese Situation, indem es die Aufrufe von "delete" weitgehend automatisiert.

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);
}

Objekte, die mit einem parent verknüpft sind, werden automatisch zusammen mit dem parent gelöscht. Dafür sorgt dessen Destruktor. Die Konsequenz daraus ist: Wenn ein Fenster geschlossen wird, verschwinden auch alle anderen Elemente, die darin eingefügt wurden.

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

Signale in Qt

Das Konzept der Signale und Slots ist im Artikel Signal & Slots dokumentiert. Für den Einstieg fasse ich mal die wichtigsten Punkte zusammen:

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 erst ab Version 23 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

Den Quelltext für die Signal-Methode erzeugt der Meta Object Compiler. Deswegen lässt man sie in der .cpp Datei weg.

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);

Hier wird das Signal "timeOver" von dem Objekt "quelle" mit dem Slot "quitGame" von dem Objekt "ziel" verbunden.

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. Signale und Slots können Parameter haben, dann müssen sie aber zusammen passen.

Wie geht es weiter?

Du hast hier einen Einblick in die Programmiersprache C++ erhalten. Wenn du bis hierhin tapfer durchgehalten hast, ohne durch zu drehen, stehen die Chancen gut, dass du das notwendige Talent zum Programmieren hast.

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#, Swift und Go. 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!