Programmierkurs/Programmierkurs.org

13 KiB

Programmierkurs

Lernziele

Algorithmisches Denken

Der wichtigste Teil beim Programmieren lernen ist definitiv die bestimmte Denkweise, die es benötigt: Ein Compiler hat keinen gesunden Menschenverstand. Ein Programm macht immer nur exakt das, was du ihm sagst. Ein ganz wichtiger Aspekt beim Programmieren lernen ist es, gut darin zu werden, solche Dinge tun zu können. Algorithmisches denken besteht im Grunde darin, eine komplexe Aufgabe in kleinere, Idiotensichere Arbeitsschritte zu zerlegen. Dafür können wir uns ein paar Werkezeuge zu Nutze machen:

  • Dekomposition: Du hast eine sehr vielteilige Problemstellung, und weißt nicht, wie du sie am besten angehen sollst? Zerleg sie in kleinere Teile, mit denen du besser umgehen kannst und löse statt dem ganzen Problem auf einmal mehrere einfache Probleme.

    • Beispiel: Lineares Gleichungssystem lösen. Das ist eine ziemlich komplexe Aufgabe, aber eine, die sich sehr gut algorithmisch lösen lässt. Der Aufbau lautet normalerweise grob: Matrix auf Zeilenstufen Form bringen -> Rücksubstitution -> Gleichungen mit jeweils einer unbekannten lösen. Dafür brauchst du aber einzele Unterwerkzeuge, zum Beispiel: Zwei Zeilen addieren. Eine Zeile mit einem Skalar multiplizieren. Mithilfe von Dekomposition schreibst du dann einfach zuerst eine Funktion, die eine Zeile mit einem Skalar multipliziert, eine Funktion, die zwei Zahlen addiert usw. Das machst du dann so oft, bis du auf Zeilenstufenform bist. So zerlegst du die große Aufgabe in vergleichsweise einfache Teilaufgaben zerlegst, die einfach zu programmieren sind, wie das addieren von zwei Zeilen. Das Motto hier ist auch "Divide-And-Conquer".
  • Abstraktion: Im Grunde das Gegenteil von Dekomposition. Wenn dir zu viele Details um die Ohren fliegen, kannst du mithilfe von Abstraktion das ganze Problem einmal von außen betrachten, damit du nicht den Faden verlierst, was du grade eigentlich machen willst.

    • Beispiel: Entwicklung eines Filesystems. Hierbei kann es schnell passieren, dass du irgendwann keine Ahnung mehr hast, was deine 200 Zeilen lange Datenstruktur eigentlich macht. Und wozu du sie brauchst. Setze es in den Gesamtkontext: Eventuell dient deine Datenstruktur eingentlich nur dazu, Metadaten für Dateien anzuzeigen. Wenn du dann in einem anderen Teil des Dateisystems unterwegs bist, brauchst du nur wissen, dass dir diese Struktur Metadaten anzeigt. Nicht was für interne Enums verwendet werden, um Zugriffsrechte verwalten. Bei der Betrachtung von außen reicht es dir schon zu wissen, dass du diese Information daher bekommen kannst. Nicht wie.
  • Mustererkennung: Oft bekommst du bei der Softwareentwicklung Problemstellungen, die anderen Problemen ähneln, die du schon an einer andren Stelle des Programms gelohnt hast. Dann kannst du deine Lösung entweder weiterentwickeln oder wiederverwenden

    • Beispiel: Lineares Gleichungssystem lösen. Manchmal ist es bei der Lösung von einem LGS nötig, eine Zeile von einer anderen zu subtrahieren. Mithilfe der Mustererkennung sehen wir: Subtrahieren ist eigentlich das gleiche wie addieren, nur dass wir erst alle Vorzeichen umdrehen. Und wie drehen wir vorzeichen um? Wir multiplizieren mit dem Skalar -1. So wurde unser neues Problem, Zeilen substrahieren, Mithilfe von Mustererkennung und Dekomposition vereinfacht: Statt eine Subtraktionsmethode schreiben zu müssen, benutzen wir einfach die Methoden, die wir sowieso schon fürs addieren und das multiplizieren mit einem Skalar geschrieben haben, und ersparen uns so Zeit und Arbeit.
  • Pseudocode: Wenn du dir erstmal keine Gedanken machen möchtest, wo du welche Semikolons und Klammern setzen willst, und auch erstmal ohne jeden einzelnen Datentyp über das Problem nachdenken willst, ist Pseudocode hilfreich. Du schreibst das Programm quasi in natürlicher Sprache, die Code Strukturen hat. Dann musst du das danach nur noch in Lauffähigen Code übersetzen. Im Sinne dieses Skripts werde ich statt richtigem Pseudocodes einfaches Python schreiben, da dir Struktur sehr ähnlich zu klassischem Pseudocode ist. Im Grunde kannst du bei Pseudocode aber eine beliebe Syntax wählen. Viele Leute schreiben auch gerne Pseudocode, der in seiner Struktur eher Java oder C ähnelt.

    • Kurz dazu: Manchmal, wenn das auch schon zu komplex ist, kannst du den Code auch erstmal nur in natürlicher Sprache beschreiben, dann in Pseudocode umwandeln, und dann in einer richtigen Programmiersprache schreiben.
  • Rubber-Duck-Debugging: Eigentlich eine Debugging Technik, wenn irgendwas in deine Code nicht richtig funktioniert, aber du dir nicht bist, warum. Der Name kommt daher, das sich Programmiere dafür wirklich quietscheentchen auf den Tisch gestellt haben, um diese Technik zu benutzen. Ich gehe manchmal aber auch einfach zu meinem Hund und Texte sie dann voll. Das geht auch. Die Technik ist sehr nützlich, wenn du Schwierigkeiten hast, durch deine eigene Logik einen durchblick zu bekommen, und du nicht verstehst warum das was du machst nicht funktioniert. Im Grudne erklärst du deiner Quietscheente im Detail was dein Code machen soll, und wie du es machst. Oft fällt dir schon dabei auf, was genau daran logisch nicht richtig ist. Kann theoretisch auch angewendet werden, um überhaupt zu verstehen, ob man mit seiner Lösung auf dem richtigen Weg ist.

Es gibt noch viele weitere Tools, die wir verwenden können, aber das hier sind einige der vielseitigsten. Im Laufe dieses Plans werden wir sie auch anwenden um Probleme zu lösen.

Einige Algorithmen, die wir lernen werden

  • Einfache Sortieralgorithmen. Bubble sort und selection sort. Außerdem besprechen wir am Ende einmal Quicksort ganz oberflächlich. Eine richtige Implementierung ist aber extrem anspruchsvoll und in Sprachen wie C überhaupt nicht mehr sinnvoll.
  • Numerische Algorithmen. Zum Beispiel zum berechnen der Fibonacci Reihe. Wir schauen uns auch das Kinderspiel "Fizzbuzz" an, und schreiben eine simple Implementierung.
  • Einfache Spiele. Zum Beispiel ein TicTacToe Spiel. Einmal in der Mensch-Gegen-Mensch Variante, einmal gegen einen Computer, der zufällige Züge macht.
  • … Und viele weitere, um Konzepte Hands-on zu lernen!

Grundlegende Datenstrukturen

Wir wissen ja bereits, dass ein Array in C eine festgelegte maximale Größe besitzt. Wie kann es dann sein, dass es in Python Listen gibt, die von außen genauso funktionieren wie arrays, aber keine begrenzte Länge haben und wo immer wieder neue Sachen dazugetan, oder alte Sachen entfernt werden? Hierzu schauen wir uns Linked Lists (verkettete Listen) und Doubly-Linked-Lists (Doppelt verkettete Listen) an, und implementieren solche Strukturen in C und java. Manchmal würden wir auch lieber mit strukturierteren Daten arbeiten, zum Beispiel einem Datentypen, wo du über einen Kategorienamen auf die Daten zugreifen kannst, z. B. so: {"username": "BladeOfENAT", "Alter": 21, "Premium": true}. Das nennt man ein Dictionary und wir werden auch ein solches implementieren, und uns anschauen, wie man damit in Java (und python in den "Pseudo"code beispielen auch) arbeiten kann. Am Ende besprechen wir auch die Baumdatenstruktur und überlegen, was für Operationen man darauf anwenden könnte.

Programmieren

Natürlich darf das Programmieren bei einem Programmierkurs nicht fehlen. Wir werden im Detail die Syntax von Java und C besprechen, und uns auch anschauen, wie wir Pseudocode als Werkzeug benutzen können, um uns zu helfen, Vorlagen für den späteren Code zu schreiben und so den Prozess vereinfachen. Wir werden insgesamt 2 Projekte schreiben. Eins iterativ während des lernens um Dinge direkt üben zu können, eins danach, als Abschlussprojekt. Das erste ist eine To-Do Liste (iteratives Projekt im Kurs) und ein Textbasiertes Spiel (Abschlussprojekt)

Programmieren

Was sind compiler, wie compile ich Code und was genau macht das eigentlich alles?

Bevor wir mit dem Programmieren beginnen, brauchen wir noch eine Möglichkeit, diesen auch ausführbar zu machen. Dafür benutzt man einen Compiler. Im Hintergrund sind dann auch noch Dinge wie ein Linker beteiligt, aber da machen wir uns später Gedanken drum. Wir werden im Grunde mit 2 Compilern arbeiten:

  • gcc (der GNU C Compiler, aus GCC, der GNU Compiler Collection, für C Programme)
  • Java SDK (Java Software development kit. Java braucht mehr als nur einen einfachen compiler, deswegen ist hier das ganze kit mit dabei.)

Installation

GCC

GCC auf Windows ist gelinde gesagt extrem nervig. Die eifnachste Methode wäre zweifelsfrei, auf einer kleinen Platte Linux zu installieren. Dort ist GCC meist vorinstalliert, ansonsten reicht ein einziger Befehl und du hast alles. Für Windows sollte dieses (zugegebermaßen ziemlich alte) Tutorial aber auch funktionieren: https://www.youtube.com/watch?v=x-2ZCkS3OHY&list=PLNmsVeXQZj7q4shI4L__SRpetWff9BjLZ. Der Text Editor ist bei C mehr oder weniger egal, du solltest halt etwas benutzen wie NotePad++ (sehr minimalistisch) oder Visual Studio Code (beliebt für große Softwareprojekte). VSCode kannst du wahrscheinlich auch so konfigurieren, das da ein C Compiler mit eingebaut wird (z. B. Clang oder GCC).

Java SDK

Der Mit Abstand beste Weg in Java zu programmieren ist es, einfach IntelliJ IDEA zu benutzen. Das Programm hat einen eigenen Code Checker, Java Compiler, Java Laufzeit und Text Editor in einem (und wird genauso auch in der Prüfung benutzt).

Textausgabe

Jetzt können wir endlichen anfangen zu Programmieren. Und es gibt kaum etwas einfacheres als Textausgabe. Einmal zwei Beispiele (Erst C, dann java). Was genau die einzelnen Code Segmente

#include <stdio.h>

int main() {
    printf("Hello World!\n");
    return 0;
}

Was genau die include Anweisung und die Funktionsdefinition so machen, besprechen wir später.

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

Alternativ könnte statt System.out.println auch so etwas wie System.out.print, System.out.printf oder weitere verwenden. println ist im Grunde das gleiche wie print, nur mit einem %n am Ende, und printf entspricht dem printf aus C, aber das gehen wir im Detail gleich auch noch drauf ein.

Grundlegende Datentypen

Computer arbeiten mit Werten in Binärdarstellung. Deswegen muss es beim ausführen des Programms Weisen geben, wie die Unterschiede zwischen einer positiven Zahl, negativen Zahl, ganzen Zahl, Kommazahl, text, etc. dargestellt werden kann. Dafür gibt es die primitiven Datentypen bzw. Die Wertdatentypen. Im Speicher werden die alle gleich abgelegt, deswegen muss der Compiler und später das Betriebssystem anhand von uns bereitgestellter Informationen entscheiden, wie es die Daten interpretieren soll. Hier ein Liste der wichtigsten Datentypen:

  • int (integer): Ganze Zahl. In C und Java normalerweise 32bit, gibt's aber auch in 16bit Variante (short) bzw. 64bit Variante (long).
  • float: Kommazahl. Sollte eigentlich nie benutzt werden! Die Genauigkeit von 32bit floats (Standard) ist sehr gering und führt oft zu Rundungsfehlern. Besser: double. Quasi der Gleiche Datentyp, aber mit 64 bit und also Doppelter (double) Genauigkeit.
  • char: Im Computer eigentlich nichts weiter als eine 8Bit Zahl. Wird vom Computer entweder als Buchstabe oder Zahl interpretiert.
  • String: In C nur eine Kette von Chars, die mit dem besonderen bit '\0' (Nullbit) beendet wird. In Java und fast allen anderen modernen Programmiersprachen ein eigener Datentyp.
  • bool: manchmal auch boolean (je nach Sprache, in C ist es bool, in java boolean). In C musst du dafür bool.h includieren. Beinhaltet nur ein paar typedefs. In Java ein eigener Datentyp. Allgemein gilt: true = 1 und false = 0. Primitiver Wert, der dafür benutzt wird anzugeben, ob etwas wahr oder falsch ist. Extrem nützlich.

In diesen Datentypen können wir quasi alles speichern was wir wollen. Hier ein Beispiel, erst in C, dann in Java.

#include <stdio.h>
#include <bool.h>

int main() {
    int beine = 4;
    int besonderes_bein = 1;
    char spezies = "Pferd";
    bool error_status = false; // char error_status = 0

    printf("Das %s, hat %d Beine\n", spezies, beine + besonderes_bein); // STDOUT: Das Pferd hat 5 Beine
    return error_status; // return 0
}

Die Kommentare dienen dazu, zu verdeutlichen, was genau sich hinter den typedefs und Variable verbirgt. Unsere Java version sieht etwas anders aus:

public class Beispiel {
    public static void main(String[] args) {
        int beine = 4;
        int besonderes_bein = 1;
        String spezies = "Pferd";
        boolean das_hier_ist_nur_ein_beispiel = true; // Diese Variable ist hier völlig sinnlos. Dient nur dazu,die Sytax zu demonstrieren
        printf("Das %s, hat %d Beine%n", spezies, beine + besonderes_bein); // STDOUT: Das Pferd hat 5 Beine
    }
}