Zum Inhalt springen
Topos Uranos

Zusammenfassung

Diese Lektion bildet die Anfangseinheit des Kurses und legt seinen vollständigen Studienplan dar. Sie nennt den Zweck des Kurses, der das Programmieren selbst ist und nicht eine Sprache; sie erklärt, warum seine Grundlagen in C++ gelehrt werden, einer Sprache, die die Maschine nicht verbirgt, und nicht in einer mit sanfterer Lernkurve; sie nennt die Studierenden, an die er sich richtet, das Wenige, das er voraussetzt, und die Kurse, zu denen er führt; sie formuliert die sieben übergeordneten Lernziele und die Einheiten, die zu jedem beitragen; sie beschreibt seine acht inhaltlichen Einheiten und die Lektionen, aus denen sie bestehen; sie schätzt den gesamten Aufwand, den der Kurs erfordert, seine Verteilung auf die Einheiten, ein empfohlenes Lerntempo und seine Leistungspunkte; sie erklärt, wie seine Lektionen gebaut sind, und gibt schließlich Ratschläge dazu, wie man ihn studiert, wie man Assistenten mit künstlicher Intelligenz nutzt und wie man Versionsverwaltung einsetzt.

Lernziele

  1. Die Abfolge der acht Einheiten des Kurses beschreiben und erklären, was jede zu den folgenden beiträgt.
  2. Die sieben übergeordneten Lernziele des Kurses formulieren und erkennen, welche Einheiten zu jedem beitragen.
  3. An einem Beispiel erklären, warum der Kurs die Grundlagen der Programmierung in C++ lehrt und nicht in einer Sprache, die die Typen und den Speicher verbirgt.
  4. Aus den geschätzten Stunden je Einheit den Aufwand berechnen, den der Kurs oder ein Teil davon erfordert, und damit ein persönliches Lerntempo planen.

Zweck

Der Kurs lehrt, korrekte Programme von Grund auf zu schreiben: ein in Worten gestelltes Problem in eine genaue Folge von Anweisungen umzusetzen, zu wählen, wie die Daten dargestellt werden, die Reihenfolge zu steuern, in der das Geschriebene ausgeführt wird, und den Code in Funktionen mit einer klaren Schnittstelle zu gliedern. Wenn dem so ist, ist sein Gegenstand nicht eine Sprache, sondern die Programmierung selbst: einen Algorithmus ersinnen, die Daten darstellen, ein Problem zerlegen und über die Korrektheit und die Kosten des Geschriebenen nachdenken. Deshalb heißt er „Grundlagen der Programmierung“, und deshalb wird jedes Konstrukt von C++ zusammen mit dem Problem vorgestellt, das es löst, und mit der Art, über es nachzudenken, und niemals als Katalog von Syntax.

C++ ist also das Werkzeug des Kurses und nicht sein Zweck. Nun ist ein Werkzeug nicht gleichgültig gegenüber dem, was man mit ihm lernt, und die Wahl dieses Werkzeugs, die auf den ersten Blick die schwierigste scheint, ist bewusst getroffen; sie wird gleich im Anschluss begründet. Ihre sichtbarste Folge sei jedoch vorweggenommen: Jede Synthese einer Einheit enthält eine feste Rubrik, „Was in jeder Sprache gilt“, die das Gelernte von seiner Form in C++ trennt und sagt, wie es sich in anderen Sprachen ausdrückt oder was sich dort ändert. Daraus folgt, dass die Studierenden am Ende des Kurses nicht nur in C++ schreiben können, sondern in jeder Sprache dieselben Ideen in anderem Gewand erkennen.

Warum C++ und nicht eine freundlichere Sprache

Es gibt Sprachen mit sanfterer Lernkurve, etwa Python, in denen ein erstes nützliches Programm in wenigen Zeilen geschrieben ist, ohne Typen zu deklarieren, ohne zu kompilieren und ohne sich zu fragen, wo ein Datum lebt. Wäre der Zweck, schnell Programme zu schreiben, die funktionieren, so wäre das der Weg. Der Zweck dieses Kurses ist dagegen zu verstehen, was ein Programm tut, und dafür eignet sich eine Sprache, die die Maschine nicht verbirgt. C++ zwingt dazu, sechs Dinge zu sehen, die bequemere Sprachen stillschweigend erledigen:

  • Die Typen. Jedes Datum wird mit einem Typ deklariert, der festlegt, welche Werte es annehmen kann, wie viel Platz es belegt und welche Operationen es zulässt; der Compiler weist zurück, was nicht zusammenpasst, bevor irgendetwas ausgeführt wird.
  • Die Kompilierung. Schreiben, Kompilieren, Linken und Ausführen sind verschiedene Momente, und jeder Fehler gehört zu einem von ihnen; zu wissen, zu welchem, ist die halbe Korrektur.
  • Der Speicher. Eine Variable ist eine Speicherzelle mit Namen, mit einer Lebensdauer und einem Gültigkeitsbereich; eine Referenz ist ein anderer Name für dieselbe Zelle, und eine Referenz auf eine Zelle, die es nicht mehr gibt, ist ein echter Fehler.
  • Die Kosten des Kopierens. Wird ein Vektor an eine Funktion übergeben, kann er ganz kopiert werden; zwischen Wert und Referenz zu wählen ist eine Entscheidung der Programmierenden und nicht der Maschine.
  • Die Grenzen der Zahlen. Eine 32-Bit-Ganzzahl reicht nicht über 2 147 483 6472\,147\,483\,647 hinaus, und eine Gleitkommazahl speichert 0,10{,}1 nicht exakt.
  • Das undefinierte Verhalten. Was der Standard nicht definiert (ein Array verlassen, eine vorzeichenbehaftete Ganzzahl überlaufen lassen, eine nicht initialisierte Variable lesen), erzeugt keinen garantierten Fehler, sondern irgendetwas; deshalb kompiliert der Kurs immer mit Warnungen und mit Sanitizern.

Das folgende Beispiel zeigt es im Kleinen. Das Programm berechnet die Fakultäten von 11 bis 1515 in einer Variablen vom Typ int. In Python, dessen Ganzzahlen ohne Grenze wachsen, würde dieselbe Rechnung klaglos 13!=6 227 020 80013! = 6\,227\,020\,800 ausgeben; in C++ passt das Produkt von 12!12! mit 1313 nicht in einen int, und der Sanitizer für undefiniertes Verhalten, mit dem der Kurs alle seine Beispiele kompiliert, hält das Programm in der genauen Zeile an:

C++-Programm
#include <print>​int main(){    int factorial{1};    for (int n{1}; n <= 15; ++n) {        factorial = factorial * n;   // passt das Produkt in einen int?        std::println("{}! = {}", n, factorial);    }}
Ausgabe
1! = 12! = 23! = 64! = 245! = 1206! = 7207! = 50408! = 403209! = 36288010! = 362880011! = 3991680012! = 479001600main.cpp:7:31: runtime error: signed integer overflow: 479001600 * 13 cannot be represented in type 'int'

Das Programm endete mit dem Code 1.

Wenn dem so ist, hat die Sprache gezwungen, eine Frage zu stellen, die die andere verborgen hätte: wie viel in den gewählten Typ passt und was zu tun ist, wenn es nicht passt. Keine der beiden Antworten ist abstrakt besser; worauf es ankommt, ist zu wissen, dass es die Frage gibt. Die Lektion über ganze Zahlen und ihre Darstellung beantwortet sie, und die Rubrik über das undefinierte Verhalten in jeder Synthese kommt auf sie zurück.

Daraus folgt eine Asymmetrie, die der eigentliche Grund der Wahl ist. Wer die Grundlagen in C++ lernt, versteht danach, was die bequemeren Sprachen für ihn tun: die automatische Speicherbereinigung, die Ganzzahlen ohne Grenze, die Prüfung der Indizes, die stillschweigende Umwandlung der Typen. Wer dagegen zuerst in einer von ihnen lernt, lässt diese Schichten ungesehen und stolpert später über sie, wenn ein echtes Programm aus einem der Gründe versagt, die die Sprache ihm verborgen hatte. Daher ist der Weg, den der Kurs wählt, am Anfang anspruchsvoller, aber besser übertragbar: Was man hier lernt, gilt in jeder Sprache, und was man in einer Sprache lernt, die es verbirgt, gilt hier nicht immer.

Nun ist C++ eine große Sprache, und ein Grundlagenkurs muss der Versuchung widerstehen, sie ganz zu lehren. Dieser erste Kurs beschränkt sich auf die strukturierte Programmierung (Daten, Ausdrücke, Kontrollfluss, Folgen, Text und Funktionen) im Standard C++23, mit seinen modernen Formen: die Ausgabe mit std::println, die Folgen mit std::array und std::vector, der Text mit std::string, die Algorithmen der Bibliothek in ihrer Ranges-Form. Für Programmieren in C++ II bleibt, was diese Grundlage voraussetzt: die Zeiger und der dynamische Speicher, die Klassen mit Methoden, die eigenen Templates, die anonymen Funktionen und die Ausnahmen.

Zielgruppe und Vorkenntnisse

Der Kurs richtet sich an alle, die noch nie programmiert haben, aus jedem Studienpfad und ab dem ersten Jahr: an Studierende der Informatik, die vor den Kursen über Datenstrukturen und über Systeme ein festes Fundament brauchen, an Studierende der Ingenieur- und der Naturwissenschaften, die mit einem Computer rechnen müssen, ohne ihm blind zu vertrauen, und an Studierende der Mathematik, die ihre Algorithmen in Programme verwandelt sehen wollen, die laufen.

Es ist ein Kurs der Stufe 1, ohne Voraussetzungen und in sich geschlossen. Er setzt nur zweierlei voraus: die Schularithmetik, mit der ganzzahligen Division und ihrem Rest, und den elementaren Umgang mit einem Computer (einen Ordner anlegen, eine Datei speichern, ein Programm öffnen). Was er von der Binärdarstellung, vom Schließen durch Fallunterscheidung, von der Logik der Bedingungen oder von der Induktion braucht, lehrt er an der Stelle, an der er es braucht: das Binärsystem in der Lektion über die ganzen Zahlen; die De Morganschen Gesetze in der Lektion über die Relationen und die Logik; die Induktion in der Lektion über die Rekursion. Die Verweise auf andere Kurse der Website, die in den Lektionen vorkommen, sind daher Vertiefungen und keine Voraussetzungen.

Wohin er führt

Aus dem Vorigen ergibt sich der Platz des Kurses auf der Website. Er hat keine Voraussetzungen und ist Ziel zweier Studienpfade, Informatik und Berechnung sowie Ingenieurwesen, weil in beiden das Programmieren zugleich Werkzeug und Gegenstand ist. Deshalb sollte er einer der ersten Kurse dieser Studienpfade sein, zusammen mit LaTeX für das wissenschaftliche Schreiben und Mathematische Logik I.

Er hat einen ergänzenden Kurs, den man vorher, parallel oder danach belegen kann. Mathematische Logik I untersucht als Theorie, was hier als Praxis verwendet wird: die zusammengesetzten Bedingungen, die Wahrheitstafeln und die De Morganschen Gesetze der Einheit 3 sowie die Idee, zu beweisen, dass ein Verfahren korrekt ist, die in diesem Kurs die Gestalt der Invariante einer Schleife und der Induktion über eine Rekursion annimmt. Algebra und Geometrie I beweist seinerseits die Ergebnisse über die Zahlen, die der Kurs programmiert: die euklidische Division, den euklidischen Algorithmus, die Zahlensysteme und das Horner-Schema. Und LaTeX für das wissenschaftliche Schreiben lehrt in seiner Lektion Im Team schreiben die Versionsverwaltung mit Git, die dieser Kurs vom ersten Programm an zu verwenden empfiehlt.

Nun lässt der Kurs bewusst beiseite, was seine Grundlage voraussetzt. Seine vorgesehene Fortsetzung ist Programmieren in C++ II, das die Zeiger und den dynamischen Speicher, die Klassen und die Objekte, die Templates, die anonymen Funktionen, die Ausnahmen und die Standardbibliothek in der Tiefe behandeln wird. Folglich ist der Kurs nicht das erste Teilstück eines C++-Handbuchs, sondern ein vollständiger Grundlagenkurs, der hier enden kann; wer dagegen weitermacht, findet im Kurs II dieselbe Disziplin, angewandt auf Programme, deren Größe es nicht mehr erlaubt, auf sie zu verzichten.

Übergeordnete Lernziele

Die folgenden Lernziele beschreiben, was Studierende, die den Kurs abschließen, können sollen. Jedes ist als beobachtbare Leistung formuliert, sodass es sich an einer konkreten Aufgabe überprüfen lässt; jede inhaltliche Lektion nennt ihrerseits eigene Lernziele, die einem oder mehreren der übergeordneten dienen, und die Abschlusslektion zeigt, in welchen Einheiten und Lektionen jedes bearbeitet wird. Die sieben Lernziele sind nummeriert, damit man sie zitieren kann.

  1. Lernziel 1. Probleme mit Programmen lösen: festlegen, was hineingeht und was herauskommt, einen Algorithmus entwerfen, ihn von Hand nachvollziehen und ihn schreiben, kompilieren, ausführen und debuggen, wobei die Meldungen und die Warnungen des Compilers gedeutet werden.
  2. Lernziel 2. Für jedes Datum den passenden Typ wählen und über die Grenzen seiner Darstellung nachdenken: den Wertebereich der Ganzzahlen, den Überlauf, die Rundung bei Gleitkommazahlen und die Kodierung des Textes.
  3. Lernziel 3. Korrekte Ausdrücke bilden und dabei den Vorrang, die Typkonvertierungen, die Nebeneffekte und das kennen, was der Standard undefiniert lässt.
  4. Lernziel 4. Den Kontrollfluss mit korrekten Entscheidungen und Schleifen entwerfen, die terminieren, und ihre Korrektheit mit einer Invariante begründen.
  5. Lernziel 5. Ein Problem in Funktionen mit klaren Schnittstellen zerlegen, wählen, wie die Parameter übergeben werden, und Probleme durch Rekursion lösen.
  6. Lernziel 6. Zusammengesetzte Daten (Folgen, Tabellen, Datensätze, Text und Dateien) mit den grundlegenden Algorithmen und mit der Standardbibliothek darstellen und verarbeiten und dabei ihre Kosten abschätzen.
  7. Lernziel 7. Größere Programme in mehrere Dateien gliedern, sie mit einer Vorbedingung und einer Nachbedingung spezifizieren, sie systematisch testen und ihre Korrektheit begründen.

Fünf Lernziele haben ihre eigene Einheit: Lernziel 2 die über Daten und Typen; Lernziel 3 die über Ausdrücke; Lernziel 4 die über den Kontrollfluss; Lernziel 5 die über Funktionen; und Lernziel 6 die über zusammengesetzte Daten, die sich in der über Text und Dateien fortsetzt, wo das Lernziel 2 mit der Kodierung UTF-8 wiederkehrt. Die beiden anderen dagegen durchziehen den ganzen Kurs. Das Lernziel 1 hat als Haupteinheit die erste, die den Zyklus aus Spezifizieren, Schreiben, Kompilieren und Ausführen lehrt, dazu die Lektion über das Debuggen in der letzten; geübt wird es aber in allen, weil jedes Programm des Kurses kompiliert, ausgeführt und korrigiert wird. Das Lernziel 7 hat als Haupteinheit die letzte, die über größere Programme; es beginnt aber in der Stilrichtlinie der ersten, setzt sich in den Konstanten der zweiten fort, nimmt Gestalt an in den Verträgen der fünften, die von da an jede Funktion begleiten, und in den Invarianten der vierten und der sechsten. Daraus folgt, dass sich ihr Erreichen keiner einzelnen Einheit zuschreiben lässt und dass man sie über den ganzen Kurs hinweg pflegen sollte.

Inhalte

Der Kurs gliedert sich in zehn Einheiten: diese Vorstellung, acht inhaltliche Einheiten und einen Abschluss. Jede inhaltliche Einheit wird mit einer Einführungslektion eröffnet, die ihre Lektionen, das Vorausgesetzte und das Problem, das sie löst, vorstellt, und beginnt mit einem Eingangstest; sie schließt mit einer Synthese-Lektion, die die Kette der Begriffe der Einheit rekonstruiert, das in jeder Sprache Gültige von seiner Form in C++ trennt, das undefinierte Verhalten der Einheit zusammenführt, in einem Abschlusstest die Fertigkeiten des Eingangstests wiederholt und Herausforderungen stellt, deren Schwierigkeit über der der Aufgaben in den Lektionen liegt.

Einheit 0. Vorstellung des Kurses

Sie besteht aus dieser Lektion, die den Zweck, die Wahl der Sprache, die übergeordneten Lernziele, die Inhalte, den Aufwand und die Gestaltung des Kurses festlegt.

Einheit 1. Erste Schritte

Ihr Ziel ist es, die Studierenden von einem in Worten gestellten Problem zu einem Programm zu führen, das kompiliert, läuft, Daten liest und Ergebnisse schreibt, und dabei zu wissen, was jedes Werkzeug unterwegs getan hat. Sie beginnt ohne Code, weil der Algorithmus dem Programm vorausgeht; am Ende wissen die Studierenden noch wenig über C++, aber sie wissen, wie man damit arbeitet.

  1. Einführung: erste Schritte.
  2. Vom Problem zum Algorithmus: die Spezifikation von Eingaben und Ausgaben, die nummerierten Schritte, das Nachvollziehen von Hand, die Sequenz, die Entscheidung und die Wiederholung.
  3. Programme, Compiler und C++: der Präprozessorlauf, die Kompilierung und das Linken; das Kompilierte gegenüber dem Interpretierten; die Standards der Sprache.
  4. Die Umgebung unter Windows, macOS und Linux installieren: die drei vollständigen Anleitungen, der Compiler, Visual Studio Code und seine C++-Erweiterung, die Überprüfung und die häufigen Installationsfehler.
  5. Kompilieren, Ausführen und Konfigurieren: die Optionen des Kurses, die Fehler und die Warnungen, die Sanitizer, der Build-Task des Editors und die Kodierung der Konsole.
  6. Anatomie eines Programms: die Einbindung, main, die Anweisungen, die Blöcke, die Kommentare und die Stilrichtlinie des Kurses.
  7. Eingabe, Ausgabe und Fehler: std::cin, std::println und die Formatangaben, die fehlerhaft geformte Eingabe und die Einteilung der Fehler.
  8. Synthese und Herausforderungen: erste Schritte.

Einheit 2. Daten und Typen

Ihr Ziel ist es zu lehren, dass jedes Datum im Speicher mit einem Typ lebt, der festlegt, welche Werte es annehmen kann, wie viel Platz es belegt und welche Operationen es zulässt, und dass eine Zahl im Programm keine Zahl der Mathematik ist, sondern eine Darstellung mit Grenzen.

  1. Einführung: Daten und Typen.
  2. Variablen und Speicher: die Deklaration und die Initialisierung mit geschweiften Klammern, die Zuweisung, das Nachvollziehen des Zustands, die Namen und die nicht initialisierte Variable.
  3. Ganze Zahlen und ihre Darstellung: die Basen 2 und 16, der Wertebereich jedes Typs, das Zweierkomplement und der Überlauf mit und ohne Vorzeichen.
  4. Gleitkommazahlen: warum 0,10{,}1 nicht exakt ist, die akkumulierte Rundung, der Vergleich mit Toleranz, das Unendliche und der Nicht-Zahl-Wert.
  5. Zeichen, boolesche Werte, Umwandlungen und Konstanten: der Code eines Zeichens, die impliziten und expliziten Konvertierungen, die Zurückweisung der Konvertierungen, die Information verlieren, const, constexpr und auto.
  6. Synthese und Herausforderungen: Daten und Typen.

Einheit 3. Ausdrücke und Operatoren

Ihr Ziel ist es zu lehren, Ausdrücke zu bilden, deren Wert sich mit Gewissheit vorhersagen lässt: welche Operation zuerst ausgeführt wird, welchen Typ das Ergebnis hat, welche Nebeneffekte sie haben und welche Ausdrücke keine Bedeutung haben. Die Einheit hat noch keine Entscheidungen und keine Schleifen: Die Bedingungen werden ausgewertet und ausgegeben.

  1. Einführung: Ausdrücke und Operatoren.
  2. Arithmetik: der Quotient und der Rest bei negativen Zahlen, der Baum eines Ausdrucks, der Vorrang, die Assoziativität und der Typ des Ergebnisses.
  3. Relationen und Logik: die zusammengesetzten Bedingungen, ihre Negation nach De Morgan, die Kurzschlussauswertung und der Fehler von 1 < x < 5.
  4. Zuweisung, Nebeneffekte und undefiniertes Verhalten: der Wert und die Wirkung eines Ausdrucks, die Inkremente, die Auswertungsreihenfolge und der Katalog dessen, was der Standard nicht definiert.
  5. Bits und Mathematikbibliothek: die Bitoperatoren, die Verschiebungen, die Masken, <cmath> und die Konstanten von <numbers>.
  6. Synthese und Herausforderungen: Ausdrücke und Operatoren.

Einheit 4. Kontrollfluss

Ihr Ziel ist es zu lehren, die Reihenfolge der Ausführung mit Entscheidungen und Schleifen zu steuern und über ihre Korrektheit nachzudenken: dass jeder Fall behandelt wird, dass jede Schleife tut, was sie soll, und dass sie terminiert. Sie verwirklicht, was die erste Lektion des Kurses angekündigt hat: die Entscheidung und die Wiederholung, nun geschrieben und überprüft.

  1. Einführung: Kontrollfluss.
  2. Entscheidungen: verkettete und verschachtelte if und else, die disjunkten und vollständigen Fälle, switch mit enum class und seine Fallen.
  3. Schleifen: while, do und for, das Nachvollziehen einer Schleife, die Umformung zwischen ihren Formen und die verschachtelten Schleifen.
  4. Korrekte Schleifen entwerfen: die Muster Zähler, Akkumulator, Maximum und Wächter, die Invariante, die Terminierung und ein erster Kontakt mit dem Debugger.
  5. Numerische Algorithmen: die Primalität, der euklidische Algorithmus, der Basiswechsel, das Horner-Schema und die Bisektion, jeder mit seiner Begründung.
  6. Synthese und Herausforderungen: Kontrollfluss.

Einheit 5. Funktionen

Ihr Ziel ist es zu lehren, ein Problem in benannte Teile zu zerlegen, jeder mit einer Schnittstelle und einem Vertrag, und Probleme durch Rekursion zu lösen. Sie verwandelt die langen Programme der Einheit 4 in Programme aus Teilen, die einzeln überprüft werden.

  1. Einführung: Funktionen.
  2. Funktionen: die Deklaration, die Definition und der Aufruf, der Aufrufstapel, die Vorbedingung und die Nachbedingung sowie die Funktion, die keinen Wert zurückgibt.
  3. Parameter und Gültigkeitsbereich: die Wertübergabe, die Referenzübergabe und die Übergabe per konstanter Referenz, mehrere Ergebnisse mit strukturierten Bindungen, der Gültigkeitsbereich und die Speicherdauer der Variablen.
  4. Mit Funktionen entwerfen: die schrittweise Verfeinerung, die Schnittstelle vor dem Rumpf, die Regel „eine Funktion, eine Aufgabe“ und die Überladung.
  5. Rekursion: der Basisfall und der Rekursionsfall, die Korrektheit und die Terminierung durch Induktion, der Stapel und die Kosten der Rekursion.
  6. Synthese und Herausforderungen: Funktionen.

Einheit 6. Zusammengesetzte Daten

Ihr Ziel ist es zu lehren, Sammlungen von Daten darzustellen und sie mit den grundlegenden Algorithmen zu verarbeiten, von Hand geschrieben und aus der Bibliothek genommen, und dabei abzuschätzen, was sie kosten. Sie führt vom einzelnen Datum zur Sammlung und von der Frage „Ist es korrekt?“ zur Frage „Was kostet es?“.

  1. Einführung: zusammengesetzte Daten.
  2. Arrays: std::array, der Durchlauf mit Indizes und bereichsbasiert, der Index innerhalb der Grenzen und die C-Arrays als Gegenstück.
  3. Vektoren: std::vector, das Einlesen einer Reihe unbekannter Länge, das Akkumulieren, das Filtern und das Transformieren, die Größe und die Kapazität.
  4. Die Kosten eines Algorithmus: das Zählen der dominanten Operationen, die Wachstumsordnungen und die Messung der Zeit.
  5. Suchen und Sortieren: die lineare und die binäre Suche, das Sortieren durch Auswahl und durch Einfügen mit ihren Invarianten und die Algorithmen der Bibliothek.
  6. Tabellen: die zweidimensionalen Tabellen, ihre Durchläufe nach Zeilen und nach Spalten, die Transponierte, das Produkt und die Tabelle in einem flachen Vektor.
  7. Datensätze: struct, die designierten Initialisierer, die Datensätze als Parameter und das Sortieren nach einem Feld.
  8. Synthese und Herausforderungen: zusammengesetzte Daten.

Einheit 7. Text und Dateien

Ihr Ziel ist es zu lehren, den Text als Datum zu behandeln und ein Programm Dateien lesen und schreiben zu lassen, ohne dass es an einer fehlerhaft geformten Eingabe zerbricht. Am Ende können die Studierenden Programme schreiben, die echte Daten verarbeiten, Akzente und Fehler eingeschlossen.

  1. Einführung: Text und Dateien.
  2. Zeichenketten: std::string und seine Operationen, npos, die Views std::string_view und die C-Strings als Gegenstück.
  3. Text ist nicht ASCII: die Kodierung UTF-8, die Bytes und die Zeichen sowie die Operationen, die den Text zerbrechen.
  4. Richtig einlesen: std::getline, der Stream-Zustand, das Erholen nach einer ungültigen Eingabe und die Validierung mit std::from_chars.
  5. Textdateien: öffnen, zeilenweise lesen, schreiben, anhängen und eine Datei mit kommagetrennten Werten verarbeiten.
  6. Synthese und Herausforderungen: Text und Dateien.

Einheit 8. Größere Programme

Ihr Ziel ist es zu lehren, Programme zu gliedern, zu überprüfen und zu debuggen, die weder in eine Datei noch in den Kopf mehr passen. Sie schließt den Kreis, den die erste Lektion geöffnet hat: spezifizieren, bauen, überprüfen; ihre Synthese endet mit einem Abschlussprojekt, das den ganzen Kurs zusammenführt.

  1. Einführung: größere Programme.
  2. Mehrere Dateien und eigene Bibliotheken: der Header und die Implementierung, die Include-Guards, die One-Definition-Rule und die getrennte Kompilierung.
  3. Testen: die Testfälle aus der Spezifikation, die automatisierten Tests, assert und static_assert.
  4. Debuggen: der Debugger von Visual Studio Code, die Haltepunkte, der Aufrufstapel und eine systematische Strategie, um einen Fehler zu lokalisieren.
  5. Synthese und Herausforderungen: größere Programme.

Einheit 9. Abschluss des Kurses

Sie besteht aus der Lektion Abschlussbilanz des Kurses Programmieren in C++ I, die an die sieben übergeordneten Lernziele erinnert, in einer Abbildung zeigt, in welchen Einheiten und Lektionen jedes bearbeitet wird, den ersten Versuch der Abschlusstests nach Lernziel zusammenführt und angibt, wohin das Studium weitergeht.

Aufwand und Leistungspunkte

Der Aufwand eines Kurses bemisst sich nicht nach Vorlesungsstunden, die es in einem asynchronen Kurs nicht gibt, sondern nach der gesamten Arbeit der Studierenden: lesen, die Beispiele schreiben und kompilieren, Aufgaben lösen und wiederholen. Die Schätzung folgt in allen Kursen der Website derselben Methode: Sie zählt die Lesezeit des Textes jeder Lektion, einschließlich des Codes ihrer Beispiele, mit hundert Wörtern pro Minute, zwanzig Minuten pro gelöster Aufgabe, fünfunddreißig pro Übungsaufgabe, fünfundvierzig pro Herausforderung und zwölf pro Eingangs- oder Abschlusstest, und schlägt einen Spielraum von einem Zehntel für die Wiederholung auf.

Ergebnisse

Die Lektionen des Kurses enthalten insgesamt 105105 gelöste Aufgaben, 105105 Übungsaufgaben und 5252 Herausforderungen, dazu sechzehn Einheitentests und siebzig kurze Übungen, die die Website sofort prüft. Der geschätzte Aufwand je Einheit, auf volle Stunden gerundet, ist folgender:

  • Einheit 0, Vorstellung des Kurses: 11 Stunde.
  • Einheit 1, erste Schritte: 2828 Stunden.
  • Einheit 2, Daten und Typen: 2222 Stunden.
  • Einheit 3, Ausdrücke und Operatoren: 2222 Stunden.
  • Einheit 4, Kontrollfluss: 2222 Stunden.
  • Einheit 5, Funktionen: 2222 Stunden.
  • Einheit 6, zusammengesetzte Daten: 3030 Stunden.
  • Einheit 7, Text und Dateien: 2121 Stunden.
  • Einheit 8, größere Programme: 2020 Stunden.
  • Einheit 9, Abschluss des Kurses: weniger als 11 Stunde.

Insgesamt sind es etwa 188188 Stunden Arbeit der Studierenden. Diese Zahl sollte man als das lesen, was sie ist: eine Schätzung für Studierende, die alle Beispiele schreiben und kompilieren und alle Aufgaben und Herausforderungen lösen. Wer schon in einer anderen Sprache programmiert hat, braucht für die ersten Einheiten deutlich weniger; wer noch nie ein Programm installiert oder eine Fehlermeldung gelesen hat, kann mehr brauchen, vor allem in der Einheit 1, die zusammen mit der über zusammengesetzte Daten auch die umfangreichste ist.

Nun ist die Zahl nominell: Sie ist es, die die Leistungspunkte des Kurses festlegt, und sie unterstellt ein Lesen in gleichbleibendem Tempo und eine Dreiviertelstunde pro Herausforderung. Solange sie nicht an echten Studierenden gemessen ist, sollte man ihr eine realistische Spanne beigeben. Ginge das Lesen nur mit halbem angenommenem Tempo voran, wie es geschieht, wenn jedes Beispiel von Hand abgetippt, kompiliert und verändert wird, um zu sehen, was sich ändert, und verlangte jede Herausforderung fünfundsiebzig statt fünfundvierzig Minuten, so ergäbe dieselbe Methode etwa 250250 Stunden. Daher liegt der zu erwartende Aufwand zwischen etwa 188188 und etwa 250250 Stunden. Das obere Ende ist eine vorsichtige Schranke, keine Messung; die Spanne wird korrigiert, sobald bekannt ist, wie viel Zeit diejenigen aufwenden, die den Kurs studieren, und bis dahin werden die Leistungspunkte mit der nominellen Zahl berechnet.

Empfohlenes Tempo

Um die Tempi zu benennen, verwendet dieser Plan dasselbe Kriterium wie die übrigen Pläne der Website: Vollzeit ist das Pensum eines Studienjahres von 6060 Leistungspunkten, etwa 16501650 Stunden, verteilt auf rund vierzig Wochen, also etwa 4040 Stunden pro Woche, und Teilzeit ist die Hälfte davon, etwa 2020 Stunden pro Woche. Daraus ergibt sich als empfohlenes Tempo ein Semester von sechzehn Wochen mit etwa 1212 Stunden Studium pro Woche, etwas weniger als ein Drittel der Vollzeit; mit der realistischen Spanne 1212 bis 1616 Stunden pro Woche. Dieses Semester lässt sich mit Mathematische Logik I teilen, deren Wahrheitstafeln und deren Idee des Beweises man in den Bedingungen und in den Invarianten dieses Kurses wiedererkennt.

Ein intensives Tempo von etwa 2020 Stunden pro Woche, also Teilzeit, schließt den Kurs in neun oder zehn Wochen ab. Ein gemächlicheres Tempo von etwa 66 Stunden pro Woche dehnt ihn auf zwei Semester aus, die sich ihn fast hälftig teilen: Die Einheiten 0 bis 4, vom Algorithmus bis zum Kontrollfluss, beanspruchen etwa 9595 Stunden, die Einheiten 5 bis 9, von den Funktionen bis zum Abschlussprojekt, etwa 9393. Der Schnitt ist natürlich: Am Ende der Einheit 4 schreiben die Studierenden bereits vollständige Programme in einem einzigen main, und die zweite Hälfte lehrt sie, diese zu gliedern.

Leistungspunkte

Die Leistungspunkte dieser Website messen den gesamten Aufwand, den ein Kurs von denen verlangt, die ihn studieren (das Lesen, die Aufgaben, die Herausforderungen und die Wiederholung), und nicht die Unterrichtsstunden. Ein Leistungspunkt entspricht etwa 27,527{,}5 Arbeitsstunden und ein Jahr in Vollzeit 6060 Leistungspunkten, also etwa 16501650 Stunden; das ist dasselbe Kriterium, nach dem oben die Tempi benannt werden.

Teilt man nun die geschätzten 188188 Stunden durch 27,527{,}5, so erhält man 6,8, was auf 77 gerundet wird. Lässt man für den Leistungspunkt eine Spanne von 2525 bis 3030 Arbeitsstunden zu, so ist die Zahl CC der Leistungspunkte des Kurses zwischen 6,3 und 7,5 eingegrenzt:

(18830)≤C≤(18825)\left( \frac{188}{30} \right) \leq C \leq \left( \frac{188}{25} \right)

Daher hat der Kurs 77 Leistungspunkte, etwas mehr als ein Neuntel eines Jahres in Vollzeit. Die Zahl ist kein Versehen: Der Kurs dauert so lange, wie er dauern muss, um die Grundlagen zu lehren, und wurde nicht gekürzt, um auf eine runde Zahl von Leistungspunkten zu kommen.

Ihr Nutzen ist praktischer Art: Sie erlauben es, den Aufwand der Kurse der Website miteinander zu vergleichen und zu entscheiden, wie viele man gleichzeitig studieren sollte, ohne die verfügbare Zeit zu überschreiten. Wer zum Beispiel während eines Semesters zu sechzehn Wochen in Teilzeit studieren kann, also über etwa 320320 Stunden verfügt, kann Kurse angehen, die zusammen etwa 1212 Leistungspunkte umfassen; in Vollzeit im selben Semester etwa 2323.

Diese Leistungspunkte sind ein eigenes Maß der Website zur Planung des Studiums: Sie entsprechen keinem offiziellen Leistungspunktesystem und bedeuten keine akademische Anerkennung durch irgendeine Einrichtung.

Gestaltung des Kurses

Studienform

Der Kurs ist asynchron, online und im eigenen Tempo: Er hat keine Termine, keine Live-Sitzungen und keine Fristen, und alle Studierenden kommen so schnell voran, wie ihre Vorbildung und ihre Zeit es erlauben. Er wird in den vierzehn Sprachen der Website angeboten werden. Die Bezeichner der Programme sind englisch und in allen Fassungen gleich; die Kommentare und die Meldungen, die die Programme ausgeben, sind dagegen in der Sprache der Seite, sodass jede Fassung mit ihrer eigenen Ausgabe kompiliert und ausgeführt wird. Die Lektionen geben an, welche anderen Lektionen man vorher studiert haben sollte, sodass die empfohlene Reihenfolge ausdrücklich ist.

Der Aufbau einer Lektion

Alle inhaltlichen Lektionen folgen demselben Aufbau. Sie beginnen mit einer Zusammenfassung und mit einer Liste eigener Lernziele, die als beobachtbare Leistungen formuliert sind. Es folgt die Darlegung, deren Kern die Programme sind. Jedes vollständige Programm des Kurses wurde vor dem Bau der Website mit dem Standard C++23, mit allen eingeschalteten Warnungen und mit den Sanitizern für Speicherfehler und für undefiniertes Verhalten kompiliert und ausgeführt; was unter dem Code veröffentlicht wird, ist seine echte Ausgabe, oder die echten Meldungen des Compilers, wenn ein Fehler oder eine Warnung gelehrt wird, oder der echte Bericht des Sanitizers, wenn das Programm undefiniertes Verhalten begeht. Wenn dem so ist, ist keine Ausgabe des Kurses von Hand geschrieben: Die Website prüft beim Bau, dass zu jedem Programm die Ausgabe seiner aktuellen Fassung gespeichert ist, und sie würde nicht veröffentlicht, wenn sie einem fehlte. Andere Abbildungen animieren Schritt für Schritt, was der Text begründet: die Speicherzellen mit ihrem Namen und ihrem Wert, den Aufrufstapel, den Durchlauf einer Schleife, die Bits einer Zahl.

Jede Lektion schließt mit drei gelösten Aufgaben mit ihrer schrittweisen Lösung und drei Übungsaufgaben, jede mit einem eingeklappten Hinweis und danach der Antwort, manchmal begleitet von einer ausgearbeiteten Lösung, die man erst öffnen sollte, nachdem man die eigene geschrieben hat. Die Aufgaben verlangen eine Idee, nie die mechanische Wiederholung eines Beispiels, und verwenden nur, was bis zu dieser Lektion gelehrt wurde. Jede Lektion enthält außerdem zwei kurze Übungen, die die Website sofort prüft: In der einen wird die genaue Ausgabe eines Programms vorhergesagt, in der anderen ein Fragment ergänzt oder korrigiert. Man sollte wissen, was diese Prüfung tut und was nicht: Die Website kompiliert den Code der Studierenden nicht, sondern prüft mit Mustern, dass das Wesentliche vorhanden ist und dass der gesuchte Fehler verschwunden ist. Daher muss eine auf der Website bestandene Übung noch auf dem eigenen Computer kompiliert und ausgeführt werden, der der einzige endgültige Richter ist.

Die Einführungs- und die Synthese-Lektionen haben einen eigenen Aufbau. Die Einführung stellt die Einheit vor, ihre Lektionen und den Faden, der sie verbindet, das Vorausgesetzte und das Problem, das sie löst, und enthält den Eingangstest. Die Synthese rekonstruiert die Kette der Begriffe der Einheit, mit der Lektion, in der jedes Glied begründet wird, und dem, was jedes verwendet; sie enthält zwei feste Rubriken, „Was in jeder Sprache gilt“ und „Das undefinierte Verhalten der Einheit“, den Abschlusstest und sechs Herausforderungen (zehn in der letzten Einheit, deren Abschlussprojekt in Etappen zerlegt ist), jede mit Hinweis, Antwort und, wenn der Gedankengang es verdient, einer eingeklappten ausgearbeiteten Lösung.

Die Vorlesefunktion

Jede Lektion lässt sich mit der Vorlesefunktion der Website anhören, die die Stimmen des eigenen Geräts verwendet: Der Text wird gelesen, wie er geschrieben ist, der darin zitierte Code wird sinnvoll ausgesprochen, und die Codeblöcke werden angekündigt, ohne Zeichen für Zeichen gelesen zu werden. Das erlaubt es, die Darlegung fern vom Bildschirm zu wiederholen; die Programme dagegen verlangen immer den Editor.

Bewertung

Der Kurs vergibt keine Noten: Seine Bewertung ist formativ und stützt sich auf die Selbsteinschätzung. Die Übungsaufgaben erlauben es zu prüfen, ob die Lernziele jeder Lektion erreicht sind, und die Herausforderungen, ob die jeder Einheit erreicht sind; in allen ist der letzte Richter der Compiler mit seinen Warnungen und seinen Sanitizern, weil ein Programm, das ohne Warnungen kompiliert, seine Testfälle besteht und keinen Sanitizer weckt, ein Beleg ist, der keinen Widerspruch zulässt, wenn auch, wie die Lektion über das Testen lehrt, kein Beweis.

Hinzu kommen die Einheitentests. Der Eingangstest in der Einführung ist eine kurze Selbsteinschätzung, die den Ausgangspunkt misst; der Abschlusstest in der Synthese, vor den Herausforderungen, misst das Erreichte und wird mit dem Eingangstest verglichen. Beide werden im Browser selbst ausgewertet: Jede Antwort erhält eine Erklärung, jeder typische Fehler eine Bemerkung, die ihn benennt, und jede Frage verweist auf die Lektion, die man wiederholen sollte. Keiner von beiden benotet oder zertifiziert, und ihre Ergebnisse werden nur im Browser der Studierenden gespeichert; die Validierung der erworbenen Fähigkeiten erfolgt später auf einer anderen Plattform. Die Abschlussbilanz führt den ersten Versuch jedes Abschlusstests zu einem Profil nach übergeordnetem Lernziel zusammen.

Die Assistenten mit künstlicher Intelligenz

Es gibt heute Assistenten mit künstlicher Intelligenz, die in Sekunden die Lösung fast jedes Problems dieses Kurses schreiben können. Man sollte klar sagen, welcher Platz ihnen zukommt, und der Grund ist derselbe, der alles Übrige rechtfertigt: Der Kurs trainiert nicht die Herstellung von Programmen, sondern eine Fähigkeit, die, ein Problem so lange zu durchdenken, bis daraus ein korrektes Programm wird. Wenn dem so ist, hebt es die Übung auf, sie einem Assistenten zu überlassen, so wie der Blick auf die Antwort einer Aufgabe vor dem eigenen Versuch sie in Lektüre verwandelt. Das entstandene Programm mag korrekt sein; wer es abgibt, hat jedoch nicht gelernt, es zu schreiben, und wird es nicht können, wenn sich der Assistent irrt, also gerade dann, wenn es am meisten gebraucht wird.

Daraus folgt eine Nutzungsregel und kein Verbot. Ein Assistent dient dazu, eine Fehlermeldung zu erklären, die man nicht versteht, Testfälle vorzuschlagen, an die man nicht gedacht hatte, eine bereits fertige eigene Lösung mit einer anderen zu vergleichen oder eine Idee der Darlegung aus einem anderen Blickwinkel zu klären. Er dient nicht dazu, die Übungen, die Aufgaben oder die Herausforderungen zu schreiben. Und in allen Fällen gilt ein Grundsatz, der für jeden fremden Code gilt, ob er von einem Assistenten, aus einem Forum oder von Mitstudierenden stammt: Er wird gelesen, getestet und verstanden, bevor man ihn übernimmt. Ein Assistent kann Code erzeugen, der kompiliert und korrekt scheint, aber undefiniertes Verhalten begeht oder eine veraltete Form der Sprache verwendet; wer den Kurs gemacht hat, wird das erkennen, und wer den Kurs delegiert hat, nicht.

Wie man diesen Kurs studiert

Der Kurs ist dafür gedacht, der Reihe nach studiert zu werden. Jede Einheit setzt die vorigen voraus: Die Ausdrücke werden mit den Typen der zweiten Einheit geschrieben, die Schleifen mit den Ausdrücken der dritten, die Funktionen kapseln die Schleifen der vierten, und die zusammengesetzten Daten werden mit Funktionen verarbeitet; wer eine Lektion überspringt, muss früher oder später zu ihr zurückkehren.

Die Hauptregel ist eine einzige: jedes Beispiel schreiben und kompilieren. Programmieren lernt man nicht durch Lesen, sondern durch Schreiben, Kompilieren und Lesen dessen, was der Compiler antwortet. Deshalb installieren die Studierenden ihre eigene Umgebung nach der Lektion Die Umgebung unter Windows, macOS und Linux installieren und geben jedes Programm in ihrem Visual Studio Code wieder, mit den Optionen, deren Einstellung die Lektion Kompilieren, Ausführen und Konfigurieren lehrt; wer einen Computer benutzt, auf dem keine Programme installiert werden können, kann sich vorläufig mit einem Online-Compiler behelfen, den die Installationslektion beschreibt. Man sollte die Beispiele von Hand abtippen statt sie zu kopieren, weil die Hand behält, was das Auge übersieht; man sollte die Ausgabe jedes Programms vorhersagen, bevor man es ausführt, weil der Unterschied zwischen dem Vorhergesagten und dem Erhaltenen genau das ist, was noch zu lernen bleibt; und man sollte jede Warnung des Compilers als eine Frage lesen, die eine Antwort verdient, und nicht als ein Geräusch, das man hinnimmt. Wenn ein Programm funktioniert, sollte man es außerdem absichtlich verändern (ein Semikolon löschen, einen Typ ändern, einen Vektor verlassen) und lesen, was der Compiler oder der Sanitizer sagt, weil jeder herbeigeführte Fehler ein Fehler ist, den man später wiedererkennt.

Jede Einheit hat zwei Kontrollpunkte. Der Eingangstest in der Einführung ist der Ausgangspunkt: Man sollte ihn beantworten, bevor man die Einheit studiert, ohne etwas nachzuschlagen, und die Lektionen notieren, auf die die Fehler verweisen. Der Abschlusstest in der Synthese, vor den Herausforderungen, ist der Vergleichspunkt: Man beantwortet ihn am Ende der Lektionen, ebenfalls ohne nachzuschlagen, und vergleicht ihn mit dem Eingangstest. Bei beiden zählt der erste Versuch, der verglichen wird und den das Profil der Abschlussbilanz zusammenführt. Bei den Übungsaufgaben und den Herausforderungen sollte man den Hinweis erst öffnen, wenn der eigene Versuch ausgeschöpft ist, und die Antwort erst am Schluss, zur Kontrolle. Eine Aufgabe, deren Antwort man nachgesehen hat, bevor man die eigene kompiliert hat, ist nicht gelöst, sondern gelesen worden.

Versionsverwaltung

Schließlich sollte man die Arbeit des Kurses vom ersten Programm an in einem System zur Versionsverwaltung sichern. Git hält jeden Zustand eines Arbeitsordners fest, erlaubt es, zu jedem von ihnen zurückzukehren, und zeigt, was sich zwischen zweien geändert hat; für alle, die programmieren, bedeutet das, ohne Angst experimentieren zu können, das Funktionierende zu verlieren, und durch den Vergleich von Fassungen die Änderung zu finden, die einen Fehler eingeführt hat, was die Bisektion, die die Lektion über das Debuggen lehrt, auf die Geschichte des Programms überträgt. Der Kurs lehrt Git nicht, weil ein anderer Kurs der Website das schon tut: Die Lektion Im Team schreiben aus dem Kurs LaTeX für das wissenschaftliche Schreiben erklärt den minimalen Arbeitsablauf, welche Dateien gesichert und welche ausgeschlossen werden und wie man einen Konflikt löst. Was dort für die Quellen eines Dokuments gilt, gilt ebenso für die eines Programms; man muss nur statt der Hilfsdateien von LaTeX die ausführbaren Dateien und die Objektdateien ausschließen, die die Kompilierung neu erzeugt.