Zum Inhalt springen

Notes 1

Aktualisiert am 20. Oktober. Seit dem Entwurf haben wir fast jeden Abschnitt überarbeitet; die Seite entspricht jetzt der Vorlesung vom 19. Oktober. Geänderte und neue Absätze haben links einen Strich.

Sie können diese Seite ausdrucken oder mit einem PDF-Drucker in ein PDF umwandeln, um Ihre eigenen Notizen hinzuzufügen.

Sie können bereits C? Schauen Sie sich doch einmal die Shorts zu dieser Vorlesung an. Gerade in den „Drei Extra-Minuten“ gibt es ein paar anspruchsvollere Inhalte.

Willkommen!

  • In der ersten Sitzung haben Sie nach Scratch Ihr erstes Programm in C gesehen: hello.c, geschrieben, kompiliert, ausgeführt und einmal repariert. Das steht in den Notes 0: Maschinencode und Quelltext, Hello World in C, Funktionen, Fehlermeldungen lesen und verstehen und Variablen.
  • In dieser Sitzung geht es mit C weiter, mit Typen, Bedingungen, Schleifen und eigenen Funktionen.
  • Alle grundlegenden Programmierkonzepte, die in Scratch vorgestellt werden, können beim Erlernen einer beliebigen Programmiersprache verwendet werden.
  • Heute werden Sie nicht nur lernen, wie man programmiert, sondern auch, wie man guten Code schreibt.
    • Die Codequalität kann nach drei Kriterien bewertet werden. Erstens, hinsichtlich der Korrektheit, was die Frage “Läuft der Code wie beabsichtigt?” betrifft. Zweitens kann man sich das Design ansehen, also sich fragen: “Wie gut ist der Code aufgebaut?”. Schließlich bezieht sich die Frage nach dem Stil darauf, wie ästhetisch ansprechend und konsistent der Code geschrieben ist. Beim Programmieren können drei Hauptkategorien von Problemen auftreten:
    • Syntaxfehler: Der Code lässt sich nicht ausführen, weil er gegen die Regeln der Programmiersprache verstößt. Beispiele: fehlendes Semikolon, falsche Klammern, Tippfehler in Funktionsnamen. Diese Fehler sind am leichtesten zu finden, weil es Fehlermeldungen gibt (z.B. vom C-Compiler, wie wir heute noch öfter sehen werden).
    • Logikfehler: Der Code kompiliertProgramm, das Quellcode in Maschinencode übersetzt; diesen Vorgang nennt man kompilieren. Jedes C-Programm muss kompiliert werden, bevor es laufen kann. Glossar →, aber verhält sich zur Laufzeit nicht wie gewünscht. Beispiel: Das Programm berechnet eine falsche Summe oder beendet sich vorzeitig. Diese Fehler sind schwieriger zu finden, weil das Programm läuft – nur eben falsch.
    • Designfehler: Der Code kompiliert und funktioniert korrekt, ist aber schlecht strukturiert. Beispiele: unnötige Wiederholungen, redundante Prüfungen, unklare Variablennamen, fehlende Abstraktion. Diese Fehler sind am schwierigsten zu erkennen, weil das Programm oberflächlich betrachtet “funktioniert”. Aber schlechtes Design macht Code schwer wartbar, fehleranfällig und unverständlich für andere (und für Ihr zukünftiges Ich!).

In diesem Kurs lernen Sie, alle drei Problemkategorien zu erkennen und zu vermeiden.

Nach welchen drei Kriterien bewertet man Codequalität?

Antwort

Korrektheit (läuft der Code wie beabsichtigt?), Design (wie gut ist er aufgebaut?) und Stil (wie ästhetisch und konsistent ist er geschrieben?).

Warum sind Syntaxfehler am leichtesten zu finden?

Antwort

Weil es Fehlermeldungen gibt, etwa vom C-Compiler.

Wie nennt man einen Fehler, bei dem der Code kompiliert, sich zur Laufzeit aber nicht wie gewünscht verhält?

Antwort

Logikfehler.

Warum sind Designfehler am schwierigsten zu erkennen?

Antwort

Weil das Programm oberflächlich betrachtet funktioniert: Es kompiliert und arbeitet korrekt, ist aber schlecht strukturiert.

Anführungszeichen und Backslash: Escape-Sequenzen

In hello.c steht \n für einen Zeilenumbruch. Der Backslash (\) leitet eine Escape-Sequenz ein: Er und das Zeichen dahinter stehen zusammen für ein einziges Zeichen, das man sonst nicht in den Text schreiben könnte. Das braucht man öfter, als man denkt:

  • Ein Anführungszeichen im Text. printf("Heute ist aber "schönes" Wetter\n"); lässt sich nicht übersetzen: Das Anführungszeichen vor „schönes“ beendet den Text, der Compiler meldet expected ')'. Richtig ist \": printf("Heute ist aber \"schönes\" Wetter\n");
  • Ein Backslash im Text, etwa in einem Windows-Pfad. printf("C:\Windows\n"); bricht mit unknown escape sequence '\W' ab, denn \W gibt es nicht. Richtig ist \\: printf("C:\\Windows\n"); gibt C:\Windows aus.
Sie schreibenEs erscheint
\nein Zeilenumbruch
\tein Tabulator
\"ein Anführungszeichen "
\\ein Backslash \

Tückisch wird es, wenn die Escape-Sequenz zufällig existiert. printf("C:\neu\tabelle\n"); übersetzt ohne jede Meldung und gibt C: aus, dann einen Zeilenumbruch, dann eu, einen Tabulator und abelle. Der Compiler kann nicht wissen, dass Sie einen Ordner „neu“ und eine Datei „tabelle“ meinten. Das ist ein Logikfehler (siehe oben): Das Programm läuft, nur nicht so, wie gewünscht.

Was gibt printf("C:\neu\tabelle\n"); aus, und wie schreiben Sie es richtig?

Antwort

C:, dann einen Zeilenumbruch, dann eu, einen Tabulator und abelle: \n und \t sind Escape-Sequenzen. Für einen Backslash im Text schreiben Sie \\, also printf("C:\\neu\\tabelle\n");.

Aufwärmübungen: Scratch-Programme tracen

Bevor Sie mit C weitermachen, können Sie das Wissen aus der letzten Woche mit zwei Tracing-Übungen in Scratch auffrischen. Beide sind freiwillig und zum Selbermachen gedacht: Überlegen Sie jeweils auf Papier, was das Programm tun wird, und klicken Sie erst danach auf die grüne Flagge.

  1. Pen Down/Pen Up: Bauen Sie ein Programm, das in einer Schleife abwechselnd mit pen down ein Stück zeichnet und mit pen up ein Stück weitergeht (dafür brauchen Sie die Pen-Erweiterung, die Sie mit dem Icon links unten aktivieren). Sagen Sie vorher, was die Katze zeichnet: Es entsteht eine gestrichelte Linie, denn der „Stift“ zeichnet nur, solange er unten ist. Drehen Sie die Katze danach vor der Schleife um 45 Grad und sagen Sie wieder vorher, was herauskommt.

  2. Das 3×3-Raster-Problem: Schreiben Sie mit zwei verschachtelten repeat-Schleifen ein Programm, das ein Raster aus 3 × 3 Punkten zeichnet, indem es die x- und die y-Koordinate der Katze ändert. Ein typischer Fehler dabei: Nach der inneren Schleife muss x wieder auf den Anfangswert gesetzt werden, bevor die äußere Schleife weiterläuft. Sonst steht die zweite Reihe nicht unter der ersten. Gehen Sie Ihr Programm Zeile für Zeile durch und notieren Sie nach jedem Block, wo die Katze steht.

So üben Sie algorithmisches Denken: Man führt Code im Kopf oder auf Papier Schritt für Schritt aus, um zu verstehen, was passiert. Diese Fähigkeit werden Sie auch in C ständig brauchen.

Was bedeutet es, ein Programm zu „tracen“?

Antwort

Man führt den Code mental oder auf Papier Schritt für Schritt aus und verfolgt dabei den Zustand der Variablen mit.

Variablen und Datentypen

  • Als Nächstes schreiben wir ein Programm, das zwei Zahlen einliest und sagt, welche größer ist. Dafür brauchen wir einen Platz für jede Zahl: eine Variable, wie in Scratch.

  • In C kann man einem int, also einer Variable, die ganze Zahlen aufnimmt, wie folgt einen Wert zuweisen:

    int counter = 0;
  • Dadurch weisen wir der Variablen counter vom Typ int den Wert 0 zu.

  • Nun können wir zu counter eine Eins addieren

    counter = counter + 1;
  • Dadurch wird 1 zum Wert von counter hinzugefügt.

  • Dies kann auch kürzer dargestellt werden:

    counter += 1;
  • Oder noch kürzer:

    counter++;
  • Beachten Sie, dass auch diese Anweisung “1” zum Wert von counter addiert, aber deutlich kürzer ist.

  • Sie können auch “1” subtrahieren, z.B. so:

    counter--;
  • Beachten Sie, dass “1” vom Wert von counter abgezogen wird.

  • Mit Datentypen legen wir beim Programmieren fest, wie die Bits, die in einer Variablen gespeichert werden, interpretiert werden sollen. In der allerersten Vorlesung hatten wir gesehen, dass eine bestimmte Bitfolge sehr unterschiedlich interpretiert werden kann, je nachdem ob es eine Zahl, ein Buchstabe oder eine Farbe ist.

  • Für den Anfang genügen vier Typen:

    • int, eine ganze Zahl wie 42 oder -3
    • char, ein einzelnes Zeichen wie a oder 2
    • string, eine Zeichenkette wie "Hallo"
    • bool, ein Wahrheitswert: entweder wahr (true) oder falsch (false)
  • Weitere Typen (long, float und double) lernen Sie am Ende dieser Vorlesung kennen, zusammen mit den Grenzen, an die int stößt (siehe Typen).

Was bewirkt die Anweisung counter++;?

Antwort

Sie addiert 1 zum Wert von counter, genau wie counter = counter + 1; oder counter += 1;.

Was legen Sie mit einem Datentyp fest?

Antwort

Wie die Bits, die in einer Variablen gespeichert sind, interpretiert werden sollen.

Die Manual Pages: Ihre Dokumentation beim Programmieren

Wenn Sie nicht genau wissen, wie eine Funktion funktioniert oder welche Parameter sie benötigt, hilft Ihnen die offizielle Dokumentation. Für diesen Kurs sind die CS50 Manual Pages besonders nützlich:

  • Speziell für Anfänger: Diese Manual Pages sind genau für Menschen wie Sie gemacht, die gerade C lernen. Sie enthalten weniger technischen Ballast als die offizielle C-Dokumentation.

  • Der “less comfortable” Modus: Auf jeder Seite finden Sie einen Schiebeschalter mit “less comfortable”. Standardmäßig ist dieser aktiviert und blendet komplizierte technische Details aus. Später im Semester können Sie ihn ausschalten und die “volle Wahrheit” sehen.

  • Was Sie dort finden: Für jede Funktion (wie printf, get_string, etc.) finden Sie:

    • Was die Funktion tut
    • Welche Parameter sie erwartet
    • Was sie zurückgibt
    • Beispielcode
  • Tipp: Machen Sie sich mit den Manual Pages vertraut – professionelle Programmierer verbringen viel Zeit damit, Dokumentation zu lesen!

Wo schlagen Sie im Kurs nach, was eine Funktion tut, welche Parameter sie erwartet und was sie zurückgibt?

Antwort

In den CS50 Manual Pages (manual.cs50.io).

Formatcodes

  • printf kennt viele Formatcodes. Für die Typen, die wir bisher kennen, sind es diese (die Codes für long, float und double folgen bei den Typen am Ende):

    %c    für char (einzelnes Zeichen)
    %i    für int (Ganzzahl)
    %s    für string (Zeichenkette)
  • %s wird für String-Variablen verwendet. %i wird für int oder Integer-Variablen verwendet. Sie können mehr darüber auf den Manual Pages herausfinden.

Mehrere Platzhalter verwenden

Sie können beliebig viele Formatcodes in einem printf-Aufruf verwenden. Die Werte werden in der Reihenfolge eingesetzt, in der sie nach dem Format-String angegeben werden:

string name1 = get_string("Erster Name: ");
string name2 = get_string("Zweiter Name: ");
printf("Hello, %s and %s\n", name1, name2);

Dies würde zum Beispiel Hello, Maria and Josef ausgeben, wenn die entsprechenden Namen eingegeben wurden.

Sie können auch verschiedene Typen kombinieren:

char initial = 'M';
int age = 25;
printf("Initial: %c, Age: %i\n", initial, age);

Wichtig: Die Anzahl der Formatcodes muss exakt mit der Anzahl der Argumente übereinstimmen. Geben Sie zu viele oder zu wenige Argumente an, bricht der Compiler mit einer Fehlermeldung ab.

Bedingte Anweisungen

  • Ein weiterer Baustein, den Sie in Scratch verwendet haben, waren die Bedingten Anweisungen. Man möchte zum Beispiel etwas tun, wenn x größer als y ist. Außerdem möchte man vielleicht etwas anderes tun, wenn diese Bedingung nicht erfüllt ist.
  • Wir sehen uns ein paar Beispiele aus Scratch an.
  • Mit zwei Variablen vom Typ int, die Sie mit get_int füllen, können Sie Ihre erste bedingte Anweisung programmieren.
  • Geben Sie im Terminalfenster code compare.c ein und schreiben Sie folgenden Code:
#include <cs50.h>
#include <stdio.h>

int main(void)
{
	int x = get_int("What's x? ");
	int y = get_int("What's y? ");

	if (x < y)
	{
		printf("x is less than y\n");
	}
}
  • Beachten Sie, dass wir zwei Variablen erstellen, einen int (Ganzzahl) namens x und einen weiteren namens y. Die Werte dieser Variablen werden mit der Funktion get_int eingelesen.

  • Sie können Ihren Code ausführen, indem Sie make compare im Terminalfenster ausführen, gefolgt von ./compare. Wenn Sie eine Fehlermeldung erhalten, überprüfen Sie Ihren Code auf Fehler.

  • Wir schauen uns in der Vorlesung Flussdiagramme an; damit kann man die Funktionsweise eines Computerprogramms zu untersuchen und prüfen, ob das Design des Codes gut ist.

  • Wenn wir uns ein Flussdiagramm des obigen Codes ansehen, können wir zahlreiche Mängel feststellen.

  • Wir können Ihr Programm verbessern, indem wir es wie folgt ändern:

    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        int x = get_int("What's x? ");
        int y = get_int("What's y? ");
    
        if (x < y)
        {
            printf("x is less than y\n");
        }
        else if (x > y)
        {
            printf("x is greater than y\n");
        }
        else
        {
            printf("x is equal to y\n");
        }
    }
  • Beachten Sie, dass nun alle möglichen Ergebnisse berücksichtigt werden.

  • Sie können Ihr Programm nun erneut kompilieren, ausführen und testen.

  • Im Flussdiagramm sehen wir, dass der Code nun ein besseres Design hat.

Was ist bei compare.c besser an der Fassung mit if, else if und else als an der mit nur if (x < y)?

Antwort

Es werden alle möglichen Ergebnisse berücksichtigt: kleiner, größer und gleich.

Wozu schauen wir uns Flussdiagramme an?

Antwort

Um die Funktionsweise eines Programms zu untersuchen und zu prüfen, ob das Design des Codes gut ist.

Mit welcher Funktion der CS50-Bibliothek lesen Sie eine Ganzzahl (int) vom Benutzer ein?

Antwort

get_int (aus cs50.h).

Übung: Bedingungen in drei Stufen

Drei Aufgaben zum selben Thema. Wählen Sie die Stufe, die Sie schaffen; wer fertig ist, nimmt die nächste. Versuchen Sie es erst selbst und klappen Sie die Lösung danach auf.

  • Stufe 1: Ergänzen Sie compare.c so, dass bei gleichen Zahlen x is equal to y erscheint. Übersetzen Sie das Programm und testen Sie es mit 2 und 2. Die Lösung ist das Programm oben mit if, else if und else.
  • Stufe 2: Schreiben Sie ein Programm sign.c, das eine ganze Zahl einliest und positiv, negativ oder null ausgibt.
  • Stufe 3: Schreiben Sie ein Programm max.c, das drei ganze Zahlen einliest und die größte ausgibt.
Lösung zu Stufe 2

Das Muster ist dasselbe wie in compare.c: zwei Fragen, der dritte Fall steht im else.

#include <cs50.h>
#include <stdio.h>

int main(void)
{
    int n = get_int("Zahl: ");

    if (n > 0)
    {
        printf("positiv\n");
    }
    else if (n < 0)
    {
        printf("negativ\n");
    }
    else
    {
        printf("null\n");
    }
}
Eine Lösung zu Stufe 3

Die Variable max merkt sich die größte Zahl, die das Programm bisher gesehen hat. Am Anfang ist das a; ist b oder c größer, wird max überschrieben. Es gibt andere Lösungen, etwa mit &&.

#include <cs50.h>
#include <stdio.h>

int main(void)
{
    int a = get_int("a: ");
    int b = get_int("b: ");
    int c = get_int("c: ");

    int max = a;
    if (b > max)
    {
        max = b;
    }
    if (c > max)
    {
        max = c;
    }
    printf("%i\n", max);
}

Optionale geschweifte Klammern (wichtig zu wissen!)

In C können die geschweiften Klammern bei if, else if, else, while und for weggelassen werden, wenn nur eine einzige Anweisung folgt. Das bedeutet, folgender Code ist syntaktisch korrekt:

if (x < y)
    printf("x is less than y\n");
else
    printf("x is not less than y\n");

Aber: In diesem Kurs werden wir immer geschweifte Klammern verwenden, auch bei nur einer Anweisung, weil:

  • Es ist übersichtlicher und weniger fehleranfällig
  • Wenn Sie später eine zweite Anweisung hinzufügen, vergessen Sie vielleicht die Klammern
  • Es ist ein verbreiteter Stil in der professionellen Softwareentwicklung

Sie werden Code ohne geschweifte Klammern im Internet finden – jetzt wissen Sie, warum das funktioniert. Aber schreiben Sie Ihren eigenen Code bitte immer mit geschweiften Klammern!

  • Lassen Sie uns einen anderen Datentyp namens char betrachten.
  • Wir schreiben ein neues Programm, indem wir code agree.c in das Terminalfenster eingeben.
  • Während ein string eine Reihe von Zeichen ist, ist ein char ein einzelnes Zeichen (engl. character).
  • Schreiben Sie im Texteditor folgenden Code:

    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
    	// Prompt user to agree
    	char c = get_char("Do you agree? ");
    
    	// Check whether agreed
    	if (c == 'Y' || c == 'y')
    	{
    		printf("Agreed.\n");
    	}
    	else if (c == 'N' || c == 'n')
    	{
    		printf("Not agreed.\n");
    	}
    }
  • Beachten Sie, dass bei char immer einfache (nicht doppelte) Anführungszeichen verwendet werden. Beachten Sie außerdem, dass == prüft, dass etwas gleich mit etwas anderem ist, während ein einzelnes Gleichheitszeichen in C eine ganz andere Funktion hätte (nämlich die Zuweisung von Werten zu Variablen).

  • Das || bedeutet so viel wie “oder”: Mindestens eine der beiden Bedingungen ist wahr. Das Gegenstück ist && (“und”): Beide Bedingungen sind wahr.

  • Weitere Vergleiche bestehen ebenfalls aus zwei Zeichen, das Gleichheitszeichen steht hinten: <= (kleiner oder gleich), >= (größer oder gleich) und != (ungleich).

  • Schreiben Sie im if versehentlich nur ein =, bricht make in unserer Umgebung mit einer Fehlermeldung ab und schlägt == vor.

  • Sie können Ihren Code testen, indem Sie make agree in das Terminalfenster eingeben, gefolgt von ./agree.

Warum setzen wir im Kurs trotzdem immer geschweifte Klammern, auch bei nur einer Anweisung?

Antwort

Es ist übersichtlicher und weniger fehleranfällig: Fügt man später eine zweite Anweisung hinzu, vergisst man sonst leicht die Klammern.

Was unterscheidet einen char von einem string?

Antwort

Ein char ist ein einzelnes Zeichen, ein string eine Reihe von Zeichen.

Welche Anführungszeichen verwenden Sie in C für einen char?

Antwort

Einfache Anführungszeichen, zum Beispiel 'y'.

Was ist in C der Unterschied zwischen == und =?

Antwort

== prüft auf Gleichheit, = weist einer Variablen einen Wert zu.

Was bedeutet der Operator || in C?

Antwort

„oder“.

Übung: Parsons-Problem

Eine gute Methode, um Ihr Verständnis für C-Code zu testen, ist ein Parsons-Problem (nach Dale Parsons): Code-Zeilen sind durcheinandergeraten, und Sie bringen sie in die richtige Reihenfolge.

Aufgabe: Das Programm liest eine Temperatur in °C ein und gibt aus, ob es “kalt” (unter 10°), “warm” (10–25°) oder “heiß” (über 25°) ist. Der Rahmen steht schon da:

#include <cs50.h>
#include <stdio.h>

int main(void)
{
    // hier fehlen die Zeilen
}

Bringen Sie die folgenden Zeilen in die richtige Reihenfolge und notieren Sie die Lösung als Folge von Buchstaben. Die geschweiften Klammern um jedes printf denken Sie sich dazu. Achtung: Drei Zeilen sind falsch und bleiben übrig.

A  if (temp < 10)
B  printf("warm\n");
C  else if (temp >= 25)
D  int temp = get_int("Temperatur: ");
E  if (temp <= 10)
F  printf("kalt\n");
G  else
H  string temp = get_int("Temperatur: ");
I  else if (temp > 25)
J  printf("heiß\n");

Tipp: Achten Sie auf die Grenzen der Bereiche (was ist bei genau 10°, was bei genau 25°?) und auf den Typ der Variable.

Lösung: D, A, F, I, J, G, B. Übrig bleiben E, C und H:

  • E if (temp <= 10): Bei genau 10° soll “warm” herauskommen, nicht “kalt”.
  • C else if (temp >= 25): Bei genau 25° soll “warm” herauskommen, nicht “heiß”.
  • H string temp = get_int("Temperatur: ");: get_int gibt einen int zurück, keinen string. make bricht hier ab.

Fehler an den Grenzen sind die häufigsten. Die Mitte steht im else: Was nicht kalt und nicht heiß ist, ist warm. Das ganze Programm mit den Klammern:

#include <cs50.h>
#include <stdio.h>

int main(void)
{
    int temp = get_int("Temperatur: ");
    if (temp < 10)
    {
        printf("kalt\n");
    }
    else if (temp > 25)
    {
        printf("heiß\n");
    }
    else
    {
        printf("warm\n");
    }
}

Schleifen

  • Dem Schleifen-Baustein aus Scratch können wir auch in unseren C-Programmen verwenden.

  • Wir sehen uns ein paar Beispiele aus Scratch an. Betrachten Sie den folgenden Code:

    int counter = 3;
    while (counter > 0)
    {
    	printf("meow\n");
    	counter = counter - 1;
    }
  • Beachten Sie, dass der Code der Variablen counter den Wert “3” zuweist. Dann wird in der while-Schleife “meow” ausgegeben und bei jeder Iteration der Wert “1” vom Zähler abgezogen. Sobald der Zähler nicht mehr größer als Null ist, endet die Schleife.

  • Geben Sie in Ihrem Terminal-Fenster code meow.c ein und schreiben Sie folgenden Code:

#include <stdio.h>

int main(void)
{
	printf("meow\n");
	printf("meow\n");
	printf("meow\n");
}
  • Beachten Sie, dass dies wie beabsichtigt funktioniert, aber es Potenzial für besseres Design gibt.
  • Wir können unser Programm verbessern, indem wir Ihren Code wie folgt abändern:
#include <stdio.h>

int main(void)
{
	int i = 3;
	while (i > 0)
	{
		printf("meow\n");
		i--;
	}
}
  • Beachten Sie, dass wir einen int namens i erstellen und ihm den Wert “3” zuweisen. Dann erstellen wir eine while-Schleife, die läuft, solange i > 0 erfüllt ist. Dann wird der Schleifenkörper durchlaufen. Jedes Mal wird “1” von i mit der Anweisung i-- abgezogen.

  • In ähnlicher Weise können wir es mit Hochzählen implementieren:

    #include <stdio.h>
    
    int main(void)
    {
    	int i = 1;
    	while (i <= 3)
    	{
    		printf("meow\n");
    		i++;
    	}
    }
  • Beachten Sie, dass unser Zähler i bei 1 beginnt. Jedes Mal, wenn die Schleife läuft, wird der Zähler um “1” erhöht. Sobald der Zähler größer als “3” ist, wird die Schleife angehalten.

  • In der Informatik zählen wir in der Regel von Null an. Am besten überarbeiten Sie Ihren Code daher wie folgt:

    #include <stdio.h>
    
    int main(void)
    {
    	int i = 0;
    	while (i < 3)
    	{
    		printf("meow\n");
    		i++;
    	}
    }
  • Beachten Sie, dass wir jetzt von Null an zählen.

  • Ein weiteres Werkzeug in unserem Werkzeugkasten für Schleifen ist eine for-Schleife.

  • Wir können das Design unseres Programms meow.c mit einer for-Schleife weiter verbessern. Ändern Sie Ihren Code dazu wie folgt:

    #include <stdio.h>
    
    int main(void)
    {
    	for (int i = 0; i < 3; i++)
    	{
    		printf("meow\n");
    	}
    }
  • Beachten Sie, dass die for-Schleife in ihren runden Klammern drei Teile enthält. Der erste Teil int i = 0 startet unseren Zähler bei Null. Der zweite Teil i < 3 ist die Bedingung, die überprüft wird. Schließlich weist der dritte Teil i++ die Schleife an, bei jedem Schleifendurchlauf um eins zu erhöhen.

  • Mit dem folgenden Code können wir sogar eine Endlosschleife erstellen:

#include <cs50.h>
#include <stdio.h>

int main(void)
{
	while (true)
	{
		printf("meow\n");
	}
}
  • Beachten Sie, dass true die Bedingung immer erfüllt. Daher wird der Code in der Schleife immer wieder ausgeführt. Wenn Sie dieses Programm starten, verlieren Sie die Kontrolle über Ihr Terminalfenster. Sie können das Programm abbrechen, indem Sie Control-C (oder Strg-C) auf Ihrer Tastatur drücken.

  • Später werden wir es uns noch genauer ansehen, aber eine eigene Funktion können Sie in C auch jetzt schon wie folgt erstellen:

    void meow(void)
    {
    	printf("meow\n");
    }
  • Das anfängliche void bedeutet, dass die Funktion keine Werte zurückgibt. Das (void) bedeutet, dass der Funktion keine Werte übergeben werden.

  • Diese Funktion kann in der Hauptfunktion wie folgt verwendet werden:

    #include <stdio.h>
    
    void meow(void);
    
    int main(void)
    {
    	for (int i = 0; i < 3; i++)
    	{
    		meow();
    	}
    }
    
    void meow(void)
    {
    	printf("meow\n");
    }
  • Beachten Sie, dass die Funktion “meow” mit der Anweisung “meow()” aufgerufen wird. Dies ist möglich, weil die “meow”-Funktion am Ende des Codes definiert ist und der Prototyp der Funktion am Anfang des Codes als void meow(void) angegeben ist.

  • Warum nicht einfach meow über main schreiben? Das ginge, aber main soll oben stehen, denn sie ist der Einstiegspunkt, und wer den Code liest, will sie sofort sehen. Der Compiler liest den Code allerdings von oben nach unten. Ohne Prototyp kennt er meow noch nicht, wenn er in main auf den Aufruf stößt, und meldet einen Fehler.

  • Ihre “meow”-Funktion kann weiter modifiziert werden, um Eingaben zu akzeptieren:

    #include <stdio.h>
    
    void meow(int n);
    
    int main(void)
    {
    	meow(3);
    }
    
    // Meow some number of times
    void meow(int n)
    {
    	for (int i = 0; i < n; i++)
    	{
    		printf("meow\n");
    	}
    }
  • Beachten Sie, dass der Prototyp in void meow(int n) geändert wurde, um den Compiler zu informieren, dass meow einen int als Eingabe akzeptiert.

Was bedeuten die drei Teile von for (int i = 0; i < 3; i++)?

Antwort
  • int i = 0: startet den Zähler bei 0
  • i < 3: die Bedingung, die geprüft wird
  • i++: erhöht den Zähler bei jedem Durchlauf um eins

Wie schreiben Sie in C eine Endlosschleife?

Antwort

while (true): Die Bedingung ist immer erfüllt.

Wie brechen Sie ein Programm ab, das im Terminal endlos läuft?

Antwort

Mit Strg-C (Control-C).

Was bedeutet das erste void in void meow(void)?

Antwort

Die Funktion gibt keinen Wert zurück.

Was bedeutet (void) in void meow(void)?

Antwort

Der Funktion werden keine Werte übergeben.

Warum schreibt man main meist oben in die Datei und nicht ans Ende?

Antwort

main ist der Einstiegspunkt. Wer den Code liest, will ihn sofort sehen. Funktionen, die darunter stehen, macht ein Prototyp dem Compiler bekannt.

Wozu steht void meow(void); oben in der Datei, wenn meow erst am Ende definiert wird?

Antwort

Das ist der Prototyp: Er macht die Funktion dem Compiler bekannt, sodass main sie aufrufen kann, obwohl die Definition erst am Ende steht.

Operatoren und Abstraktion

  • Sie können einen Taschenrechner in C implementieren.
  • Geben Sie in Ihrem Terminal code calculator.c ein und schreiben Sie folgenden Code:

    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
    	// Prompt user for x
    	int x = get_int("x: ");
    
    	// Prompt user for y
    	int y = get_int("y: ");
    
    	// Perform addition
    	printf("%i\n", x + y);
    }
  • Beachten Sie, wie die Funktion get_int verwendet wird, um zweimal eine Zahl vom Benutzer zu erhalten. Eine Ganzzahl wird in der int-Variablen x gespeichert, eine weitere in der int-Variablen y. Dann druckt die Funktion printf den Wert von x + y aus, und zwar an die Stelle, an der der Formatcode %i steht. Danach wird mit \n ein Zeilenumbruch ausgegeben.

  • Das + ist ein Operator. In C gibt es insbesondere folgende arithmetische Operatoren:

    • + für Addition
    • - für Subtraktion
    • * für Multiplikation
    • / für Division
    • % für Modulo (Rest nach einer Division)
  • / und % gehören oft zusammen. Angenommen, Sie schulden jemandem 97 Cent und wollen möglichst wenige Euro-Münzen herausgeben (50, 20, 10, 5, 2 und 1 Cent). 97 / 50 ergibt 1: Teilt man eine ganze Zahl durch eine ganze Zahl, fallen die Nachkommastellen weg. Also ein 50er. 97 % 50 ergibt 47: der Rest, der noch herauszugeben ist. Mit der nächsten Münze geht es genauso weiter: 47 / 20 ergibt 2, also zwei 20er, Rest 7; 7 / 10 ergibt 0, also kein 10er; ein 5er, Rest 2; ein 2er. Zusammen 5 Münzen: 50, 20, 20, 5 und 2 Cent. Genauso rechnet die Aufgabe Bargeld aus Übung 1, dort mit US-Münzen zu 25, 10, 5 und 1 Cent.

Was ergeben 47 / 20 und 47 % 20?

Antwort

47 / 20 ergibt 2 (ganze Zahl durch ganze Zahl, die Nachkommastellen fallen weg), 47 % 20 ergibt 7 (der Rest).

  • Indem wir unser zuvor erworbenes Wissen über Funktionen anwenden, können wir die Addition in eine Funktion abstrahieren. Ändern Sie Ihren Code dazu wie folgt:

    • Abstraktion ist die Kunst, unseren Code so zu vereinfachen, dass wir Probleme schrittweise in kleinere Teile zerlegen und die Details der Lösung vor uns verbergen.
    #include <cs50.h>
    #include <stdio.h>
    
    int add(int a, int b);
    
    int main(void)
    {
    	// Prompt user for x
    	int x = get_int("x: ");
    
    	// Prompt user for y
    	int y = get_int("y: ");
    
    	// Perform addition
    	int z = add(x, y);
    	printf("%i\n", z);
    }
    
    int add(int a, int b)
    {
    	int c = a + b;
    	return c;
    }
  • Beachten Sie, dass die Funktion add zwei Variablen als Eingabe benötigt. Diese Werte werden den Variablen a und b zugewiesen. Die Funktion führt dann die Addition durch und speichert das in der Variablen c. Danach gibt sie den Wert in c zurück dorthin, wo sie aufgerufen wurde.

Was berechnet der Operator % in C?

Antwort

Den Rest nach einer Division (Modulo).

Was ist Abstraktion beim Programmieren?

Antwort

Probleme schrittweise in kleinere Teile zerlegen und die Details der Lösung verbergen, etwa in einer Funktion wie add.

Was bewirkt return c; am Ende der Funktion add?

Antwort

Es gibt den Wert von c dorthin zurück, wo die Funktion aufgerufen wurde.

Scope: Variablen leben nur in ihrer Funktion

Ein wichtiges Konzept in C ist Scope (deutsch: Gültigkeitsbereich) – der Kontext, in dem Variablen existieren:

  • Jede Funktion ist ihre eigene “Box”: Die Variable x existiert nur in main, die Variable c existiert nur in add. Die Funktion add hat keine Ahnung, dass x in main existiert.

  • Werte werden kopiert, nicht geteilt: Wenn Sie add(x, y) aufrufen, werden die Werte von x und y kopiert und in den Parametern a und b gespeichert. Die Variablen x und y selbst werden nicht “gesendet”. Es sind komplett neue Variablen mit kopierten Werten.

  • Was in der Funktion passiert, bleibt in der Funktion: Die Variablen a, b und c in der Funktion add existieren nur, während die Funktion läuft. Sobald die Funktion endet und einen Wert zurückgibt, werden diese Variablen zerstört – sie existieren nicht mehr. Alles, was bleibt, ist der Rückgabewert.

  • Originalvariablen bleiben unverändert: Die Variablen x und y in main behalten ihre Werte, egal was in add passiert. Sie haben niemals mitbekommen, dass add aufgerufen wurde.

  • Genauer gilt das für jeden Block: Eine Variable lebt bis zur schließenden geschweiften Klammer des Blocks, in dem sie angelegt wurde. Das i einer for-Schleife gibt es hinter ihrer schließenden Klammer nicht mehr. Das brauchen Sie gleich bei der Eingabevalidierung.

Stellen Sie sich Funktionen wie getrennte Räume vor: Jede Funktion hat ihre eigenen Variablen und kann nicht einfach die Variablen einer anderen Funktion sehen oder ändern. Kommunikation zwischen Funktionen erfolgt nur durch Parameter (Eingabe) und Return-Werte (Ausgabe).

  • Das Design dieses Programms kann wie folgt weiter verbessert werden:

    #include <cs50.h>
    #include <stdio.h>
    
    int add(int a, int b);
    
    int main(void)
    {
      // Prompt user for x
      int x = get_int("x: ");
    
      // Prompt user for y
      int y = get_int("y: ");
    
      // Perform addition
      printf("%i\n", add(x, y));
    }
    
    int add(int a, int b)
    {
      return a + b;
    }
  • Beachten Sie, dass “c” in der Funktion “add” entfernt wurde.

  • Obwohl es sehr nützlich ist, zu einer “Add”-Funktion zu abstrahieren, ist es etwas umständlich. Sie könnten die Addition auch direkt im Aufruf von printf durchführen:

    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        // Benutzer zur Eingabe von x auffordern
        int x = get_int("x: ");
    
        // Benutzer zur Eingabe von y auffordern
        int y = get_int("y: ");
    
        // Addition durchführen
        printf("%i\n", x + y);
    }
  • Beachten Sie, dass die Addition innerhalb des Aufrufs von printf durchgeführt wird.

  • In ähnlicher Weise könnten auch andere Operationen, etwa eine Division durchgeführt werden:

    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
        // Aufforderung an den Benutzer zur Eingabe von x
        int x = get_int("x: ");
    
        // Aufforderung an den Benutzer zur Eingabe von y
        int y = get_int("y: ");
    
        // Teilen Sie x durch y
        printf("%i\n", x / y);
    }

Was bezeichnet der Begriff „Scope“ (Gültigkeitsbereich)?

Antwort

Den Kontext, in dem eine Variable existiert. In C: Variablen leben nur in ihrer Funktion.

Was passiert beim Aufruf add(x, y) mit x und y?

Antwort

Ihre Werte werden kopiert und in den Parametern a und b gespeichert. x und y selbst werden nicht „gesendet“ und bleiben unverändert.

Was geschieht mit den Variablen a, b und c der Funktion add, sobald die Funktion endet?

Antwort

Sie werden zerstört. Übrig bleibt nur der Rückgabewert.

Über welche zwei Wege kommunizieren Funktionen in C miteinander?

Antwort

Über Parameter (Eingabe) und Rückgabewerte (Ausgabe).

Linux und die Kommandozeile

  • LinuxEin Betriebssystem wie Windows oder macOS. Die CS50-Umgebung und die Prüfungslaptops laufen mit Linux. Glossar → ist ein Betriebssystem, bei dem die Kommandozeile eine wichtige Rolle spielt. Wenn wir das Terminalfenster von VS Code im Kurs benutzen, steuern wir einen Rechner, auf dem Linux läuft – entweder in der Cloud oder in einem Container, der auf Ihrem Laptop gestartet wird, wenn Sie VS Code öffnen.
  • Einige übliche Befehlszeilenprogramme, die wir verwenden können, sind:
    • cdWechselt das Verzeichnis: cd me geht in den Ordner me, cd .. eine Ebene nach oben, cd allein ins Homeverzeichnis. Glossar →, um unser aktuelles Verzeichnis (Ordner) zu wechseln
    • cp, um Dateien und VerzeichnisseAnderes Wort für Ordner. Glossar → zu kopieren
    • lsListet die Dateien und Ordner im aktuellen Verzeichnis auf. Glossar →, zum Auflisten von Dateien in einem Verzeichnis
    • mkdirLegt einen neuen Ordner an: mkdir me erzeugt den Ordner me. Glossar →, um ein Verzeichnis zu erstellen
    • mv, zum Verschieben (Umbenennen) von Dateien und Verzeichnissen
    • rmLöscht Dateien. Es gibt keinen Papierkorb; was mit rm gelöscht ist, lässt sich nicht wiederherstellen. Glossar →, zum Entfernen (Löschen) von Dateien
    • rmdir, um Verzeichnisse zu entfernen (zu löschen)
  • Das am häufigsten verwendete Programm ist ls, das alle Dateien im aktuellen Verzeichnis auflistet. Geben Sie einfach ls in das Terminalfenster ein und drücken Sie Enter. Sie werden alle Dateien im aktuellen Verzeichnis sehen.
  • Ein weiterer nützlicher Befehl ist mv, mit dem Sie eine Datei in ein anderes Verzeichnis verschieben oder umbenennen können. Zum Beispiel können Sie diesen Befehl verwenden, um Hello.c (beachten Sie das große H) in hello.c umzubenennen, indem Sie mv Hello.c hello.c eingeben.
  • Sie können auch Verzeichnisse erstellen. Sie können mkdir pset1 eingeben, um ein Verzeichnis namens pset1 zu erstellen.
  • Sie können dann cd pset1 benutzen, um von Ihrem aktuellen Verzeichnis in das neu angelegte Verzeichnis pset1 zu wechseln.
  • In welchem Verzeichnis Sie sind, sehen Sie – je nach Konfiguration – am Zeilenanfang. Wenn Sie sich unsicher sind, können Sie mit pwd (von engl. print working directory) Ihr aktuelles Verzeichnis ausgeben lassen.

Mit welchem Befehl listen Sie die Dateien im aktuellen Verzeichnis auf?

Antwort

ls.

Mit welchem Befehl wechseln Sie in ein anderes Verzeichnis?

Antwort

cd, zum Beispiel cd pset1.

Mit welchem Befehl erstellen Sie auf der Kommandozeile ein neues Verzeichnis?

Antwort

mkdir, zum Beispiel mkdir pset1.

Mit welchem Befehl verschieben oder benennen Sie auf der Kommandozeile eine Datei um?

Antwort

mv, zum Beispiel mv Hello.c hello.c.

Mit welchem Befehl kopieren Sie auf der Kommandozeile Dateien und Verzeichnisse?

Antwort

cp.

Mit welchem Befehl löschen Sie auf der Kommandozeile eine Datei?

Antwort

rm (für Verzeichnisse: rmdir).

Mit welchem Befehl lassen Sie sich auf der Kommandozeile Ihr aktuelles Verzeichnis ausgeben?

Antwort

pwd (von engl. print working directory).

Terminal-Effizienz: Die Werkzeuge der Profis

Professionelle Programmiererinnen und Programmierer verbringen viel Zeit im Terminal. Daher gibt es einige Techniken, die Ihre Arbeit deutlich effizienter machen:

  • Tab-Vervollständigung: Wenn Sie einen Dateinamen oder Befehl eingeben, drücken Sie die Tab-Taste. Das Terminal vervollständigt automatisch, was Sie schreiben wollten. Wenn es mehrere Möglichkeiten gibt, zeigt ein zweites Drücken von Tab alle Optionen an. Beispiel: Tippen Sie mv m und drücken Sie Tab – das Terminal hilft Ihnen, den Rest zu finden.

  • Befehlshistorie mit Pfeiltasten: Mit Pfeil nach oben und Pfeil nach unten können Sie durch zuvor eingegebene Befehle navigieren. Das erspart Ihnen, lange Befehle immer wieder neu einzutippen.

  • Terminal aufräumen mit clear: Der Befehl clear (oder die Tastenkombination Ctrl+L bzw. Strg+L) leert den Bildschirm und gibt Ihnen wieder Übersicht. Viele Programmierer machen das regelmäßig, um ein “aufgeräumtes Terminal” zu haben.

Diese kleinen Tricks sparen enorm viel Zeit – professionelle Entwickler tippen oft nur die Hälfte ihrer Befehle selbst, den Rest erledigen Tab-Vervollständigung und die Befehlshistorie.

Mit welchem Befehl leeren Sie den Bildschirm des Terminals?

Antwort

clear (oder die Tastenkombination Strg+L).

Mario

  • Alles, was wir heute besprochen haben, sind Bausteine für Ihre Arbeit als angehende Informatikerinnen und Informatiker.
  • Die folgenden Ausführungen sollen Ihnen helfen, sich auf die Arbeit an einer Problemstellung für diesen Kurs im Allgemeinen einzustellen: Wie geht man an ein Problem aus der Informatik heran?
  • Stellen Sie sich vor, wir wollten die Grafik des Spiels Super Mario Bros. nachbilden. Wie müsste ein Programm aussehen, das so etwas wie die vier horizontalen Fragezeichenblöcke ausgibt?

Progressives Refactoring: Code Schritt für Schritt verbessern

In den folgenden Beispielen werden wir einen wichtigen Ansatz der Softwareentwicklung kennenlernen: progressives Refactoring. Wir beginnen mit einer einfachen, aber nicht optimalen Lösung und verbessern sie Schritt für Schritt. Bei jedem Schritt fragen wir uns: Welches Problem behebt diese Änderung? Warum ist der Code jetzt besser?

Sie werden sehen, wie wir den Code mehrfach überarbeiten:

  1. Von hartkodiertem Code zu Schleifen (weniger Wiederholung)
  2. Von einfachen Schleifen zu verschachtelten Schleifen (komplexere Muster)
  3. Von “magischen Zahlen” zu Variablen (bessere Lesbarkeit)
  4. Von Variablen zu Konstanten (verhindert ungewollte Änderungen)
  5. Von festen Werten zu Benutzereingaben (Flexibilität)
  6. Von unvalidiertem Input zu Eingabeprüfung (Robustheit)
  7. Von Code-Duplikation zu eleganten Lösungen (besseres Design)

Dieser schrittweise Ansatz ist typisch für gutes Programmieren: Erst etwas zum Laufen bringen, dann systematisch verbessern.

Mario Question Marks
Mario Question Marks

  • Geben Sie im Terminalfenster code mario.c ein und programmieren Sie Folgendes:

    #include <stdio.h>
    
    int main(void)
    {
    	for (int i = 0; i < 4; i++)
    	{
    		printf("?");
    	}
    	printf("\n");
    }
  • Beachten Sie, dass hier vier Fragezeichen in einer Schleife gedruckt werden.

  • Mit der gleichen Logik können wir auch drei vertikale Blöcke erstellen.

    Mario Blocks
    Mario Blocks

  • Ändern Sie dazu Ihren Code wie folgt:

    #include <stdio.h>
    
    int main(void)
    {
    	for (int i = 0; i < 3; i++)
    	{
    		printf("#\n");
    	}
    }
  • Beachten Sie, dass drei vertikale Ziegelsteine in einer Schleife gedruckt werden.

  • Was wäre, wenn wir diese Ideen kombinieren wollten, um eine drei-mal-drei-Gruppe von Blöcken zu schaffen?

    Mario Grid
    Mario Grid

  • Wir können der obigen Logik folgen, indem wir die bisherigen Ideen kombinieren. Ändern Sie Ihren Code wie folgt:

#include <stdio.h>

int main(void)
{
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 3; j++)
		{
			printf("#");
		}
		printf("\n");
	}
}
  • Beachten Sie, dass eine Schleife innerhalb einer anderen liegt – man sagt: es sind verschachtelte Schleifen. Die erste Schleife legt fest, welche vertikale Zeile gedruckt wird. Für jede Zeile werden drei Spalten gedruckt. Nach jeder Zeile wird eine neue Zeile gedruckt.

  • Was wäre, wenn wir sicherstellen wollten, dass die Anzahl der Blöcke konstant, d. h. unveränderlich ist? Ändern Sie Ihren Code wie folgt:

    #include <stdio.h>
    
    int main(void)
    {
    	const int n = 3;
    	for (int i = 0; i < n; i++)
    	{
    		for (int j = 0; j < n; j++)
    		{
    			printf("#");
    		}
    		printf("\n");
    	}
    }
  • Beachten Sie, dass “n” jetzt eine Konstante ist. Diese Variable kann nach ihrer Initialisierung nicht mehr geändert werden.

  • Häufiger sehen Sie in C dafür eine symbolische Konstante mit #define. Sie steht oben in der Datei, unter den #include-Zeilen, ohne Gleichheitszeichen und ohne Semikolon:

    #define N 3

    Vor dem Übersetzen ersetzt der Präprozessor jedes N im Code durch 3. Im Programm schreiben Sie dann i < N statt i < 3. Solche Namen schreibt man in Großbuchstaben.

  • Mit dem, was wir schon wissen, können wir unseren Code so umbauen, dass der Benutzer nach der Größe des Rasters gefragt wird. Ändern Sie Ihren Code wie folgt:

#include <cs50.h>
#include <stdio.h>

int main(void)
{
	int n = get_int("Size: ");

	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; j++)
		{
			printf("#");
		}
		printf("\n");
	}
}
  • Beachten Sie, dass get_int verwendet wird, um den Benutzer aufzufordern.
  • Ein allgemeiner Ratschlag bei der Programmierung lautet: Vertraue nie darauf, dass sich die Benutzerinnen und Benutzer so verhalten wie du denkst (oder wie man es erwarten würde). Sie werden sich wahrscheinlich falsch verhalten, entweder unabsichtlich oder absichtlich, und zum Beispiel ungültige Werte eingeben. Wir können unser Programm vor schlechtem Verhalten schützen, indem wir überprüfen, ob die Eingaben unseren Anforderungen genügen.

Was versteht man unter progressivem Refactoring?

Antwort

Mit einer einfachen, nicht optimalen Lösung beginnen und sie Schritt für Schritt verbessern: erst zum Laufen bringen, dann systematisch verbessern.

Wie nennt man es, wenn eine Schleife innerhalb einer anderen liegt?

Antwort

Verschachtelte Schleifen.

Warum ersetzt man „magische Zahlen“ im Code durch Variablen?

Antwort

Für bessere Lesbarkeit.

Was bewirkt const in const int n = 3;?

Antwort

n ist eine Konstante: Sie kann nach der Initialisierung nicht mehr geändert werden. Das verhindert ungewollte Änderungen.

Welcher allgemeine Ratschlag gilt beim Programmieren für Eingaben der Benutzer?

Antwort

Vertraue nie darauf, dass sich Benutzer so verhalten, wie du denkst. Deshalb prüft man Eingaben.

Übung: Mario in drei Stufen

Drei Aufgaben zum selben Programm. Wählen Sie die Stufe, die Sie schaffen; wer fertig ist, nimmt die nächste.

  • Stufe 1: Ein Quadrat, dessen Größe n das Programm mit get_int einliest. Das ist das Programm oben. Tippen Sie es ab, falls Sie es noch nicht haben, und testen Sie es mit 3 und 5.

  • Stufe 2: Ein Dreieck, linksbündig: In der ersten Zeile steht ein Stein, in der zweiten stehen zwei, in der letzten n. Für die Größe 4:

    #
    ##
    ###
    ####
  • Stufe 3: Dasselbe Dreieck rechtsbündig, mit Leerzeichen vor den Steinen:

       #
      ##
     ###
    ####

    Das ist der Kern der Aufgabe Mario aus Übung 1; eine Lösung steht hier deshalb nicht. In der Übung legen Sie mario.c in einem eigenen Ordner mario an.

Lösung zu Stufe 2

Gegenüber dem Quadrat ändert sich eine einzige Stelle: Die innere Schleife läuft nicht mehr bis n, sondern bis zur Nummer der Zeile, j <= i. In der Zeile mit i gleich 0 gibt sie einen Stein aus, in der Zeile mit i gleich 1 zwei, und so weiter.

#include <cs50.h>
#include <stdio.h>

int main(void)
{
    int n = get_int("Size: ");

    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j <= i; j++)
        {
            printf("#");
        }
        printf("\n");
    }
}

Eingabevalidierung: Das “cat”-Problem

Ein interessanter Aspekt von get_int: Wenn der Benutzer einen nicht-numerischen Wert eingibt (z.B. “cat” statt einer Zahl), fragt die Funktion automatisch erneut nach, bis eine gültige Zahl eingegeben wird. Aber: Wenn der Benutzer 0 eingibt, gibt get_int diese 0 zurück – und 0 ist keine gültige Größe für unser Mario-Grid!

Das bedeutet: Wir müssen zusätzlich überprüfen, ob die Zahl sinnvoll ist. Wir wollen sicherstellen, dass n mindestens 1 ist. Daher fragen wir erneut, solange n < 1 gilt.

Ändern Sie Ihren Code wie folgt:

#include <cs50.h>
#include <stdio.h>

int main(void)
{
	int n;
	do
	{
		n = get_int("Size: ");
	}
	while (n < 1);

	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; j++)
		{
			printf("#");
		}
		printf("\n");
	}
}
  • Beachten Sie, dass der Benutzer ständig zur Eingabe der Größe aufgefordert wird, bis die Eingabe des Benutzers 1 oder größer ist. Dazu haben wir eine do-while-Schleife verwendet, bei der der Schleifenkörper erst ausgeführt wird bevor die Bedingung geprüft wird.
  • int n; steht vor der Schleife, im do-Block steht nur die Zuweisung. Legen Sie n dagegen im Block an (int n = get_int("Size: "); zwischen den geschweiften Klammern), gibt es n hinter der schließenden Klammer nicht mehr (Scope), und make bricht bei while (n < 1) ab: „use of undeclared identifier ’n’“. Beachten Sie außerdem das Semikolon hinter while (n < 1).

Was tut get_int, wenn jemand „cat“ statt einer Zahl eingibt?

Antwort

Es fragt automatisch erneut nach, bis eine gültige Zahl eingegeben wird.

Warum muss man im Mario-Programm die Eingabe von get_int zusätzlich prüfen?

Antwort

get_int gibt auch 0 zurück, und 0 ist keine gültige Größe für das Raster.

Wann prüft eine do-while-Schleife ihre Bedingung?

Antwort

Erst nachdem der Schleifenkörper ausgeführt wurde. Der Körper läuft also mindestens einmal.

Kommentare

  • Kommentare sind grundlegende Bestandteile von Computerprogrammen. Damit hinterlassen Sie sich selbst und anderen, mit denen Sie zusammenarbeiten, Erklärungen zu Ihrem Code.

  • Code, den Sie in diesem Kurs erstellen und abgeben (insbesondere im Abschlussprojekt und in der Prüfung), muss immer sinnvolle und aussagekräftige Kommentare enthalten.

  • Normalerweise besteht ein Kommentar aus einigen Wörtern oder ein paar Sätzen, die dabei helfen, zu verstehen, was in einem bestimmten Code-Block passiert. Außerdem dienen solche Kommentare als Gedächtnisstütze für Sie, wenn Sie Ihren Code später überarbeiten müssen.

  • Kommentare stehen im Code immer hinter //. Ändern Sie Ihren Code wie folgt, um Kommentare zu integrieren:

    #include <cs50.h>
    #include <stdio.h>
    
    int main(void)
    {
    	// Prompt user for positive integer
    	int n;
    	do
    	{
    		n = get_int("Size: ");
    	}
    	while (n < 1);
    
    	// Print an n-by-n grid of bricks
    	for (int i = 0; i < n; i++)
    	{
    		for (int j = 0; j < n; j++)
    		{
    			printf("#");
    		}
    		printf("\n");
    	}
    }
  • Beachten Sie, dass jeder Kommentar mit einem // beginnt.

Womit beginnt ein Kommentar in C?

Antwort

Mit //.

Wozu dienen Kommentare im Code?

Antwort

Sie erklären Ihnen selbst und anderen, was in einem Code-Block passiert, und sind eine Gedächtnisstütze, wenn Sie den Code später überarbeiten.

Fehlersuche

  • Jeder macht beim Programmieren Fehler.

  • Fehler in Programmen heißen „Bugs“ (Käfer). Das Wort war unter Ingenieuren schon lange üblich, als 1947 das Team um Grace Hopper in einem Relais des Rechners Mark II in Harvard eine echte Motte fand und ins Logbuch klebte, mit dem Vermerk „First actual case of bug being found“.

  • Betrachten Sie das folgende Bild aus der Aufgabe Mario:

    mario
    mario

  • Betrachten Sie außerdem den folgenden Code, in den absichtlich ein Fehler eingefügt wurde:

#include <stdio.h>

int main(void)
{
    for (int i = 0; i <= 3; i++)
    {
        printf("#\n");
    }
}
  • Geben Sie code buggy0.c in das Terminalfenster ein und schreiben Sie den obigen Code.

  • Wenn Sie diesen Code ausführen, erscheinen vier Steine statt der geplanten drei.

  • Wir sehen uns nun vier Methoden zum Debugging an.

  • printf ist eine sehr einfache und nützliche Methode, um Code zu debuggen. Wir könnten den Code dazu wie folgt abändern:

    #include <stdio.h>
    
    int main(void)
    {
        for (int i = 0; i <= 3; i++)
        {
            printf("i is %i\n", i);
            printf("#\n");
        }
    }
  • Wenn Sie diesen Code ausführen, werden Sie zahlreiche Ausgaben sehen, darunter i is 0, i is 1, i is 2 und i is 3. Wenn Sie das sehen, werden Sie vielleicht erkennen, dass der Code wie folgt korrigiert werden muss:

    #include <stdio.h>
    
    int main(void)
    {
        for (int i = 0; i < 3; i++)
        {
            printf("#\n");
        }
    }

    Beachten Sie, dass das “<=” durch “<” ersetzt wurde.

  • Dieser Code kann wie folgt weiter verbessert werden:

    #include <cs50.h>
    #include <stdio.h>
    
    void print_column(int height);
    
    int main(void)
    {
        int h = get_int("Height: ");
        print_column(h);
    }
    
    void print_column(int height)
    {
        for (int i = 0; i <= height; i++)
        {
            printf("#\n");
        }
    }

    Beachten Sie, dass das Kompilieren und Ausführen dieses Codes immer noch zu einem Fehler führt.

  • Um diesen Fehler zu beheben, werden wir ein neues Werkzeug einsetzen, das uns zur Verfügung steht.

  • Dieses Werkzeug zur Fehlersuche ist der sogenannte Debugger, ein Softwaretool, das verwendet wird, um Fehler im Code aufzuspüren.

  • In VS Code wird Ihnen ein vorkonfigurierter Debugger zur Verfügung gestellt. Auf Rechnern mit ARM-Prozessor, etwa Macs mit Apple-Chip, läuft er im Dev Container nur, wenn der Container in der ARM-Fassung läuft. Einen Container, der vor dem 18. Oktober 2026 eingerichtet wurde, stellen Sie dafür einmal um (Dev Container auf ARM-Rechnern umstellen).

  • Um diesen Debugger zu verwenden, setzen Sie als erstes einen Breakpoint (Haltepunkt), indem Sie links neben eine Zeile Ihres Codes klicken – direkt links neben die Zeilennummer. Wenn Sie dort klicken, wird ein roter Punkt erscheinen. Stellen Sie sich dies als ein Stoppschild vor, das den Rechner auffordert, eine Pause einzulegen, damit Sie sich ansehen können, was in diesem Teil Ihres Codes passiert.

    Haltepunkt
    Haltepunkt

  • Zweitens: Führen Sie debug50 ./buggy0 aus. Sie werden feststellen, dass, nachdem der Debugger aktiviert worden ist, eine Zeile Ihres Codes farbig markiert wird. Der Debugger hat den Rechner an dieser Codezeile angehalten. Beachten Sie in der oberen linken Ecke des Fensters, dass alle lokalen Variablen angezeigt werden, einschließlich i, das aktuell noch keinen Wert hat. Am oberen Rand des Fensters können Sie auf die mit einem Pfeil dargestellte Schaltfläche step over klicken (achten Sie auf den Tooltip beim Überfahren mit der Maus), und der Code wird weiter durchlaufen. Beachten Sie, wie sich der Wert von i ändert.

  • Dieses Werkzeug zeigt Ihnen zwar nicht, wo Ihr Fehler liegt, aber es hilft Ihnen, zu sehen, wie Ihr Code Schritt für Schritt abläuft. Sie können die Schaltfläche step into benutzen, um bei der Schritt-für-Schritt-Ausführung in eine Funktionen hineinzuschauen, anstatt diese mit step over zu überspringen.

  • Eine weitere Form der Fehlersuche wird Rubber Duck Debugging genannt. Wenn Sie Probleme mit Ihrem Code haben, überlegen Sie sich, wie Sie mit einer Gummiente laut über das Codeproblem sprechen können. Wenn Sie lieber nicht mit einer kleinen Plastikente sprechen möchten, können Sie gerne mit einem Menschen in Ihrer Nähe sprechen! Diese Person muss nicht wissen, wie man programmiert: Es ist einfach so, dass man oft merkt, wo das Problem ist, wenn man seine Gedanken in Worte fasst. Probieren Sie es aus, Rubber Duck Debugging funktioniert wirklich!

  • Schließlich gibt es die Möglichkeit, sich von KI-Werkzeugen beim Debugging helfen zu lassen. Auf der Kursseite ist das Susi: Lassen Sie sich eine Fehlermeldung erklären oder einen Hinweis geben, und beheben Sie den Fehler dann selbst.

Die vier Methoden zur Fehlersuche aus der Vorlesung: printf, Debugger, ???, KI-Werkzeuge.

Antwort

Rubber Duck Debugging.

Warum gibt for (int i = 0; i <= 3; i++) vier statt der geplanten drei Steine aus?

Antwort

Wegen <= statt <: Die Schleife läuft für i gleich 0, 1, 2 und 3.

Was ist ein Debugger?

Antwort

Ein Softwaretool, um Fehler im Code aufzuspüren. Es zeigt, wie Ihr Code Schritt für Schritt abläuft.

Wozu setzt man im Debugger einen Breakpoint (Haltepunkt)?

Antwort

Damit der Rechner an dieser Zeile anhält und Sie sich ansehen können, was dort im Code passiert.

Mit welchem Befehl starten Sie in VS Code den Debugger für das Programm buggy0?

Antwort

debug50 ./buggy0

Was tut im Debugger step into bei einem Funktionsaufruf, anders als step over?

Antwort

Es geht in die Funktion hinein und führt sie Schritt für Schritt aus. step over überspringt sie.

Warum hilft Rubber Duck Debugging?

Antwort

Wenn man seine Gedanken in Worte fasst, merkt man oft selbst, wo das Problem ist.

Typen

  • Eine der Schwächen von C ist, dass es sich von Haus aus nur in geringem Maß um die Speicherverwaltung kümmert. Zwar bietet C eine enorme Kontrolle über die Nutzung des Speichers, doch müssen Sie sich aller möglichen Fallstricke der Speicherverwaltung bewusst sein, um keine Fehler zu machen.
  • Typen sind sehr wichtig, da jeder Typ bestimmte Grenzen hat und Regeln unterworfen ist. Zum Beispiel kann man in einem int wegen der Grenzen des Speichers nur maximal 4294967296 verschiedene Werte ablegen (int-Werte belegen 32 Bit).
  • Die Anzahl der Bits begrenzt, wie hoch und niedrig wir zählen können.
  • Wenn Sie aber versuchen, mit einem int höher als 2147483647 zu zählen, kommt es zu einem Integer Overflow, bei dem ein falscher Wert (typischerweise -2147483648) in der Variablen gespeichert wird. Der int ist nämlich eine Ganzzahl mit Vorzeichen (signed) und eines der 32 Bit wird für das Vorzeichen benutzt. Daher können wir – anders als man erwarten würde – nur bis 231 – 1 zählen.
  • Zu den vier Typen vom Anfang (Variablen und Datentypen) kommen drei weitere:

    • long, Ganzzahlen mit mehr Bits (meist 64), die also viel höher zählen können als ein int. Eingabe mit get_long, Ausgabe mit %li.
    • float, eine Fließkommazahl, also eine Zahl mit Nachkommastellen. Eingabe mit get_float, Ausgabe mit %f.
    • double, eine Fließkommazahl mit mehr Bits und damit mehr genauen Stellen als ein float. Eingabe mit get_double, Ausgabe ebenfalls mit %f.
  • Auch feste Werte im Quelltext haben einen Typ. Solche Werte wie 42, 3.14, 'a' oder "Hallo" heißen Literale. Eine ganze Zahl ohne Punkt wie 3 ist ein int, eine Zahl mit Punkt wie 3.0 eine Fließkommazahl. Deshalb ergibt 4 / 3 den Wert 1 und 4 / 3.0 den Wert 1.333333 (mehr bei der Ganzzahldivision).

Wie nennt man es, wenn ein int über 2147483647 hinaus zählen soll und dabei ein falscher Wert entsteht?

Antwort

Integer Overflow.

Warum kann ein int nur bis 2 hoch 31 minus 1 zählen, obwohl er 32 Bit hat?

Antwort

Er ist vorzeichenbehaftet (signed): Eines der 32 Bit wird für das Vorzeichen benutzt.

Was unterscheidet long von int?

Antwort

Ein long hat mehr Bits und kann also höher zählen.

Ganzzahldivision: Nachkommastellen werden abgeschnitten

Ersetzen Sie im Taschenrechner x + y durch x / y und geben Sie 1 und 3 ein, erhalten Sie 0 und nicht 0,333…. Teilt man in C eine Ganzzahl durch eine Ganzzahl, ist auch das Ergebnis eine Ganzzahl: C schneidet die Nachkommastellen ab (englisch truncation). Gerundet wird nicht. 7 / 2 ergibt 3, 2 / 3 ergibt 0 und -7 / 2 ergibt -3.

Es hilft auch nicht, das Ergebnis in einer float-Variablen zu speichern: float z = x / y; ergibt 0.000000, denn die Nachkommastellen sind schon bei der Division verloren. Sie müssen die Werte vorher in float umwandeln (englisch cast): (float) x / (float) y ergibt 0.333333.

Es genügt, dass ein Operand eine Fließkommazahl ist: Dann rechnet C mit Nachkommastellen. Auch (float) x / y ergibt 0.333333. Stehen die Zahlen fest im Quelltext, schreiben Sie eine davon mit Nachkommastelle: 4 / 3 ergibt 1, 4 / 3.0 ergibt 1.333333. Denn eine Zahl ohne Punkt wie 3 ist für C ein int, eine mit Punkt wie 3.0 eine Fließkommazahl.

Was gibt printf("%i\n", 7 / 2); aus?

Antwort

3. Bei der Division zweier Ganzzahlen schneidet C die Nachkommastellen ab (Truncation). Gerundet wird nicht.

x und y sind int-Variablen mit den Werten 1 und 3. Warum ergibt float z = x / y; trotzdem 0?

Antwort

x / y ist eine Ganzzahldivision: Die Nachkommastellen sind schon abgeschnitten, bevor das Ergebnis in z landet. Abhilfe: die Werte vorher umwandeln, (float) x / (float) y.

Was gibt printf("%f\n", 4 / 3); aus, und wie ändern Sie die Rechnung, damit 1.333333 herauskommt?

Antwort

4 / 3 teilt zwei int-Werte, das Ergebnis ist 1, und %f passt nicht zu einem int: make bricht ab. Es genügt, einen Operanden als Fließkommazahl zu schreiben: 4 / 3.0 (oder 4.0 / 3) ergibt 1.333333.

Fließkomma-Ungenauigkeit: Die Messbecher-Analogie

Ein besonderes Problem bei der Programmierung betrifft Fließkommazahlen (float und double). Diese können manche Zahlen nicht präzise darstellen. Ein gutes Beispiel ist der Bruch 1/3:

Stellen Sie sich einen Messbecher vor: Er hat Markierungen für 1/4, 1/2 und 3/4. Aber wo ist die Markierung für 1/3? Sie existiert nicht! Wenn Sie 1/3 Liter abmessen sollen, müssen Sie schätzen – irgendwo zwischen der 1/4- und 1/2-Markierung.

Genauso funktionieren float-Variablen: Sie haben eine begrenzte Präzision. Sie können viele Zahlen darstellen, aber nicht alle exakt. Ein float ist wie ein Messbecher mit wenigen Markierungen, ein double hat mehr Markierungen (also bessere Genauigkeit), aber auch ein double ist nicht perfekt.

Daher sehen Sie manchmal statt 0,333333… plötzlich 0,33333334 – der Computer musste schätzen, weil die exakte Darstellung von 1/3 mit endlich vielen Bits unmöglich ist.

Deshalb vergleichen Sie Fließkommazahlen nicht mit ==. Schon 0.1 + 0.2 == 0.3 ist in C falsch, weil keine der drei Zahlen exakt dargestellt wird.

Praktischer Tipp: Verwenden Sie niemals float oder double für Geldbeträge! Nutzen Sie stattdessen Ganzzahlen (z.B. Cent-Beträge als int), um Rundungsfehler zu vermeiden.

  • Achten Sie beim Programmieren besonders auf die Arten von Variablen, die Sie verwenden, um Probleme in Ihrem Code zu vermeiden.
  • Wir haben uns in der Vorlesung einige Beispiele für Katastrophen angesehen, die durch speicherbezogene Fehler entstehen können (Jahr-2000-Problem, Jahr-2038-Problem, Pac Man Level 256, Donkey Kong Level 22, Boeing 787).

Warum zeigt ein float für 1/3 nicht exakt 0,333… an?

Antwort

Fließkommazahlen haben eine begrenzte Präzision: 1/3 lässt sich mit endlich vielen Bits nicht exakt darstellen, der Computer muss schätzen.

Welchen Typ sollten Sie für Geldbeträge verwenden?

Antwort

Ganzzahlen, etwa Cent-Beträge als int, niemals float oder double. So vermeiden Sie Rundungsfehler.

Was davon zur Basis gehört

Die Shorts zu diesem Abschnitt sagen auf ihren Seiten, welche Folien wozu gehören.

  • Basis-Code (Programmieren können):

    • Variablen, Datentypen, Operatoren, Bedingungen, Schleifen, Ausgabe mit printf (Formatcodes, Escape-Sequenzen)
    • Funktionen, Parameter, Rückgabewerte, Scope
    • Integer Overflow (erkennen und umgehen)
    • Ganzzahldivision umgehen (ein Operand als Fließkommazahl: Cast oder Literal wie 3.0), Float-Ungenauigkeit umgehen (größerer Typ; Geldbeträge in Cent als int)
    • Kommentare, Codestil (Einrückung, Klammersetzung, sprechende Namen)
    • Eingabevalidierung (Schleife bis zur gültigen Eingabe)
    • Symbolische Konstante mit #define anlegen
    • Einfache Shell-Befehle (cd, ls, cp, mv, mkdir, rm, cat, pwd)
  • Basis-Konzept (Erklären und von Hand durchspielen):

    • switch lesen, auch das Durchfallen ohne break
    • Compiler-Fehlermeldungen lesen; Syntax-, Logik- und Designfehler unterscheiden
    • Magic Numbers erkennen
  • Kür-Code (Anwenden und schreiben):

    • const-Variablen
    • Progressives Refactoring
    • Pipes, Umleitungen, Wildcards, Textfilter (sort, uniq, cut), komplexere Shell-Konstruktionen
  • Hintergrund (Einordnung):

    • Manual Pages (man)

Was die Labels bedeuten und die ganze Liste: Basis und Kür.

Zusammenfassung

In dieser Vorlesung haben Sie gesehen, wie Sie die Bausteine, die Sie in Scratch kennengelernt haben, auf die Programmiersprache C anwenden können. Sie haben gelernt:

  • wie Sie Funktionen aus Bibliotheken benutzen und Ihre eigenen Funktionen schreiben;
  • wie Sie Variablen, Bedingte Anweisungen und Schleifen verwenden;
  • wie man Abstraktion einsetzt, um seinen Code zu vereinfachen und zu verbessern;
  • wie man bei der Lösung eines Informatikproblems vorgeht;
  • wie man die Linux-Kommandozeile benutzt;
  • wie man Kommentare in seinen Code einbaut;
  • wie man Typen und Operatoren verwendet.

Außerdem haben Sie gesehen, wie Sie Fehler finden: erklären (Rubber Duck), mit printf nachsehen und mit dem Debugger anhalten.

Dies war Inf-Einf-B. Bis zum nächsten Mal!

Die Fragen zum Selbsttest helfen beim aktiven Lesen. Sie sind kein Vorgeschmack auf die Prüfung: Fragen und Antworten auswendig zu lernen reicht nicht zum Bestehen und ist vermutlich auch keine sinnvolle Vorbereitung. Was hilft: Wie Sie Inf-Einf-B bewältigen.

Diese Seite als Markdown: ansehen herunterladen