🏠 Startseite

📚 Definitionen & Begriffe

Fortgeschrittene Algorithmen und Programmierung in C
HTW Berlin • WiSe 2025/26

Inhaltsverzeichnis

1. Erweiterte Konzepte

Konzepte

Type Casting

Type Casting ist die explizite Umwandlung eines Datentyps in einen anderen. Dies ist notwendig, wenn verschiedene Datentypen in Berechnungen gemischt werden.

Arten:

  • Implizites Casting: Automatisch vom Compiler durchgeführt
  • Explizites Casting: Manuell durch den Programmierer
int x = 10;
int y = 3;
double ergebnis = (double)x / y; // 3.333...
// Ohne Cast: int z = x / y; // 3

char c = 'A';
int ascii = (int)c; // 65
Konzepte

Konstanten (const)

Das Schlüsselwort const markiert eine Variable als unveränderlich. Der Wert kann nach der Initialisierung nicht mehr geändert werden.

Vorteile:

  • Schutz vor unbeabsichtigten Änderungen
  • Bessere Lesbarkeit und Dokumentation
  • Optimierungsmöglichkeiten für den Compiler
const double PI = 3.14159265359;
const int MAX_SIZE = 100;

// PI = 3.14; // Fehler: const kann nicht geändert werden
Konzepte

Makros (#define)

Makros werden durch den Präprozessor vor der Kompilierung durch ihren Wert ersetzt. Sie definieren symbolische Konstanten oder Code-Fragmente.

Unterschied zu const:

  • Makros haben keinen Typ (reine Textersetzung)
  • Werden zur Compile-Zeit ersetzt
  • Können auch Ausdrücke und Funktionen sein
#define PI 3.14159
#define MAX(a,b) ((a) > (b) ? (a) : (b))
#define QUADRAT(x) ((x) * (x))

int kreis = PI * r * r; // Wird zu: 3.14159 * r * r
int groesser = MAX(5, 8); // Gibt 8
Konzepte

Präprozessor

Der Präprozessor ist ein Programm, das vor dem eigentlichen Compiler läuft und Anweisungen ausführt, die mit # beginnen.

Wichtige Präprozessor-Direktiven:

  • #include: Bindet Header-Dateien ein
  • #define: Definiert Makros
  • #ifdef / #ifndef: Bedingte Kompilierung
  • #pragma: Compiler-spezifische Anweisungen
#include <stdio.h>
#define DEBUG 1

#ifdef DEBUG
    printf("Debug-Modus aktiv");
#endif

2. Erweiterte Operatoren

Operatoren

Bitweise Operatoren

Bitweise Operatoren arbeiten auf Bit-Ebene und manipulieren einzelne Bits von Ganzzahlen.

Übersicht:

  • & (AND): Bit ist 1, wenn beide Bits 1 sind
  • | (OR): Bit ist 1, wenn mindestens ein Bit 1 ist
  • ^ (XOR): Bit ist 1, wenn genau ein Bit 1 ist
  • ~ (NOT): Invertiert alle Bits
  • << (Left Shift): Verschiebt Bits nach links
  • >> (Right Shift): Verschiebt Bits nach rechts
int a = 5; // 0101 in binär
int b = 3; // 0011 in binär

int and = a & b; // 0001 = 1
int or = a | b; // 0111 = 7
int xor = a ^ b; // 0110 = 6
int not = ~a; // 1010 = -6 (Zweierkomplement)
int left = a << 1; // 1010 = 10 (Multiplikation mit 2)
int right = a >> 1;// 0010 = 2 (Division durch 2)
Operatoren

Compound Assignment Operatoren

Compound Assignment Operatoren kombinieren eine Operation mit einer Zuweisung in einem Schritt.

Alle Compound Operatoren:

  • += Addition: x += 5 ist gleich x = x + 5
  • -= Subtraktion: x -= 3 ist gleich x = x - 3
  • *= Multiplikation: x *= 2 ist gleich x = x * 2
  • /= Division: x /= 4 ist gleich x = x / 4
  • %= Modulo: x %= 3 ist gleich x = x % 3
  • &=, |=, ^=, <<=, >>= für bitweise Operationen
int x = 10;
x += 5; // x ist jetzt 15
x -= 3; // x ist jetzt 12
x *= 2; // x ist jetzt 24
x /= 4; // x ist jetzt 6
x %= 4; // x ist jetzt 2
Operatoren

Ternärer Operator (?:)

Der ternäre Operator ist eine kompakte Form einer if-else-Anweisung. Er hat drei Operanden.

Syntax: Bedingung ? Wert_wenn_wahr : Wert_wenn_falsch

  • Nützlich für einfache Verzweigungen
  • Kann in Zuweisungen verwendet werden
  • Sollte nicht verschachtelt werden (schlechte Lesbarkeit)
int alter = 20;
char* status = (alter >= 18) ? "Volljährig" : "Minderjährig";

int max = (a > b) ? a : b; // Größerer Wert

// Statt:
int max;
if (a > b) {
    max = a;
} else {
    max = b;
}

3. Erweiterte Kontrollstrukturen

Kontrollfluss

break

Das Schlüsselwort break beendet eine Schleife oder einen switch-case-Block sofort und springt zum nächsten Befehl nach der Struktur.

Verwendung:

  • Vorzeitiges Beenden von Schleifen
  • Beenden einzelner case-Blöcke in switch
  • Beendet nur die innerste Schleife bei verschachtelten Schleifen
// Suche nach Element
for(int i = 0; i < 100; i++) {
    if(array[i] == gesuchterWert) {
        printf("Gefunden bei Index %d", i);
        break; // Schleife beenden
    }
}
Kontrollfluss

continue

Das Schlüsselwort continue überspringt den Rest des aktuellen Schleifendurchlaufs und springt zur nächsten Iteration.

Verwendung:

  • Überspringen bestimmter Werte
  • Vermeidung tief verschachtelter if-Strukturen
  • Filtert Elemente in Schleifen
// Nur ungerade Zahlen ausgeben
for(int i = 0; i < 10; i++) {
    if(i % 2 == 0) {
        continue; // Gerade Zahlen überspringen
    }
    printf("%d ", i); // Gibt nur 1, 3, 5, 7, 9 aus
}
Kontrollfluss

goto

Das Schlüsselwort goto springt zu einem markierten Label im Code. Wird meist vermieden, da es zu "Spaghetti-Code" führen kann.

Sinnvolle Verwendung:

  • Fehlerbehandlung (Sprung zu Cleanup-Code)
  • Ausbruch aus mehrfach verschachtelten Schleifen
  • In allen anderen Fällen: Besser vermeiden!
for(int i = 0; i < 10; i++) {
    for(int j = 0; j < 10; j++) {
        if(fehler) {
            goto error_handler;
        }
    }
}

error_handler:
    printf("Fehler aufgetreten");
    // Cleanup-Code
Kontrollfluss

Nested Loops (Verschachtelte Schleifen)

Verschachtelte Schleifen sind Schleifen innerhalb von Schleifen. Häufig verwendet für mehrdimensionale Datenstrukturen.

Komplexität: Bei n verschachtelten Schleifen mit je m Durchläufen: O(m^n)

// Multiplikationstabelle
for(int i = 1; i <= 10; i++) {
    for(int j = 1; j <= 10; j++) {
        printf("%4d", i * j);
    }
    printf("\n");
}

// 2D-Array durchlaufen
int matrix[3][3];
for(int zeile = 0; zeile < 3; zeile++) {
    for(int spalte = 0; spalte < 3; spalte++) {
        printf("%d ", matrix[zeile][spalte]);
    }
}
Kontrollfluss

Switch-case

Die switch-case-Anweisung ermöglicht eine Mehrfachverzweigung basierend auf dem Wert einer Variablen. Effizienter als mehrere if-else bei vielen Fällen.

Wichtige Punkte:

  • Nur für ganzzahlige Werte und char
  • Jeder case benötigt meist ein break
  • default-Fall ist optional, aber empfohlen
  • Fall-through: Ohne break werden folgende cases auch ausgeführt
char note = 'B';
switch(note) {
    case 'A':
        printf("Sehr gut!");
        break;
    case 'B':
        printf("Gut!");
        break;
    case 'C':
        printf("Befriedigend");
        break;
    default:
        printf("Unbekannte Note");
}

4. Zeiger (Pointers)

Zeiger

Pointer (Zeiger)

Ein Pointer ist eine Variable, die eine Speicheradresse als Wert speichert. Zeiger ermöglichen direkten Zugriff auf den Speicher.

Vorteile:

  • Effiziente Übergabe großer Datenstrukturen
  • Dynamische Speicherverwaltung
  • Manipulation von Arrays und Strings
  • Implementierung komplexer Datenstrukturen
int zahl = 42;
int *ptr = &zahl; // ptr zeigt auf zahl

printf("Wert: %d\n", *ptr); // 42
printf("Adresse: %p\n", ptr); // z.B. 0x7fff5fbff5ac
printf("Adresse: %p\n", &zahl); // Gleiche Adresse
Zeiger

Adressoperator (&)

Der Adressoperator & gibt die Speicheradresse einer Variablen zurück.

Verwendung: Adressen an Pointer zuweisen, Call-by-Reference

int x = 10;
int *ptr = &x; // ptr erhält Adresse von x

printf("x liegt bei: %p", &x);

// Bei Funktionen (Call-by-Reference)
void aendere(int *wert) {
    *wert = 100;
}
aendere(&x); // Übergibt Adresse von x
Zeiger

Dereferenzierung (*)

Der Dereferenzierungsoperator * greift auf den Wert zu, auf den ein Pointer zeigt.

Achtung: * hat zwei Bedeutungen - Deklaration und Dereferenzierung!

int zahl = 42;
int *ptr = &zahl; // * bei Deklaration

printf("%d", *ptr); // * zum Zugriff (Dereferenzierung): 42
*ptr = 100; // Ändert zahl auf 100

// Jetzt ist zahl = 100
Zeiger

NULL-Pointer

Ein NULL-Pointer ist ein Pointer, der auf keine gültige Speicheradresse zeigt. Er wird verwendet, um nicht initialisierte Pointer zu kennzeichnen.

Wichtig:

  • Immer Pointer mit NULL initialisieren, wenn keine Adresse vorhanden
  • Vor Dereferenzierung auf NULL prüfen
  • Verhindert unvorhersehbares Verhalten
int *ptr = NULL; // Pointer zeigt auf nichts

// Sicherer Zugriff:
if(ptr != NULL) {
    printf("%d", *ptr);
} else {
    printf("Pointer ist NULL!");
}

// Nach free() sollte Pointer auf NULL gesetzt werden
free(ptr);
ptr = NULL;
Zeiger

Call-by-Reference

Bei Call-by-Reference wird die Adresse einer Variablen an eine Funktion übergeben. Die Funktion kann so das Original verändern.

Vorteile:

  • Funktionen können mehrere Werte "zurückgeben"
  • Effizient bei großen Datenstrukturen
  • Direktes Ändern von Originalwerten
void tausche(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int x = 5, y = 10;
tausche(&x, &y); // x = 10, y = 5

// Mehrere "Rückgabewerte"
void berechne(int a, int b, int *summe, int *produkt) {
    *summe = a + b;
    *produkt = a * b;
}
Zeiger

Pointer-Arithmetik

Pointer-Arithmetik ermöglicht das Rechnen mit Adressen. Addition/Subtraktion verschiebt den Pointer um Vielfache der Typgröße.

Operationen:

  • ptr + n: Verschiebt um n Elemente vorwärts
  • ptr - n: Verschiebt um n Elemente rückwärts
  • ptr1 - ptr2: Anzahl Elemente zwischen Pointern
  • ptr++, ptr--: Nächstes/vorheriges Element
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr; // Zeigt auf arr[0]

printf("%d", *ptr); // 10
ptr++; // Zeigt auf arr[1]
printf("%d", *ptr); // 20
printf("%d", *(ptr+2)); // 40 (arr[3])

// Array durchlaufen
for(int *p = arr; p < arr + 5; p++) {
    printf("%d ", *p);
}
Zeiger

Zeiger auf Arrays

Der Name eines Arrays ist gleichzeitig ein Pointer auf das erste Element. Arrays und Pointer sind eng verwandt.

Wichtig: arr[i] ist äquivalent zu *(arr + i)

int zahlen[5] = {1, 2, 3, 4, 5};
int *ptr = zahlen; // oder &zahlen[0]

// Drei gleichwertige Zugriffe:
printf("%d", zahlen[2]); // 3
printf("%d", *(zahlen + 2)); // 3
printf("%d", ptr[2]); // 3

// Funktion mit Array-Parameter
void ausgabe(int *arr, int size) {
    for(int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
}
Zeiger

Zeiger auf Zeiger (Pointer to Pointer)

Ein Pointer auf einen Pointer speichert die Adresse eines anderen Pointers. Wird mit ** deklariert.

Verwendung:

  • 2D-Arrays und Matrizen
  • Ändern von Pointern in Funktionen
  • Dynamische Arrays von Strings
int wert = 42;
int *ptr = &wert;
int **ptr_ptr = &ptr; // Zeiger auf Zeiger

printf("%d", **ptr_ptr); // 42

// 2D-Array als Pointer auf Pointer
int **matrix = malloc(3 * sizeof(int*));
for(int i = 0; i < 3; i++) {
    matrix[i] = malloc(4 * sizeof(int));
}

// Zugriff: matrix[i][j]

5. Arrays & Strings (Erweitert)

Arrays

Mehrdimensionale Arrays

Mehrdimensionale Arrays sind Arrays von Arrays. Am häufigsten sind 2D-Arrays (Matrizen).

Speicherung: Row-major order (zeilenweise im Speicher)

// 2D-Array (3 Zeilen, 4 Spalten)
int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

// Zugriff
int wert = matrix[1][2]; // 7

// Durchlaufen
for(int i = 0; i < 3; i++) {
    for(int j = 0; j < 4; j++) {
        printf("%d ", matrix[i][j]);
    }
    printf("\n");
}
Strings

String-Funktionen (string.h)

Die Standard-Bibliothek string.h bietet viele Funktionen zur String-Manipulation.

Wichtige Funktionen:

  • strlen(s): Länge des Strings (ohne '\0')
  • strcpy(dest, src): Kopiert String (unsicher!)
  • strcat(dest, src): Hängt String an
  • strcmp(s1, s2): Vergleicht Strings (0 = gleich)
  • strncpy(dest, src, n): Kopiert max. n Zeichen (sicher)
  • strchr(s, c): Sucht Zeichen in String
  • strstr(s1, s2): Sucht Substring
char str1[50] = "Hallo";
char str2[50] = "Welt";

int laenge = strlen(str1); // 5
strcat(str1, " "); // "Hallo "
strcat(str1, str2); // "Hallo Welt"

if(strcmp(str1, str2) == 0) {
    printf("Gleich");
}

// Sicheres Kopieren
strncpy(str1, "Test", 49);
str1[49] = '\0'; // Sicherstellen
Strings

Character Arrays

Character Arrays sind Arrays vom Typ char. Strings sind spezielle Character Arrays mit '\0' am Ende.

Unterschied:

  • Character Array: Kann beliebige Zeichen enthalten
  • String: Muss mit '\0' enden
// Character Array (kein String)
char chars[5] = {'H', 'a', 'l', 'l', 'o'};

// String (mit '\0')
char string[6] = {'H', 'a', 'l', 'l', 'o', '\0'};
// Oder einfacher:
char string2[] = "Hallo"; // '\0' automatisch

// Länge
sizeof(chars); // 5
sizeof(string); // 6
strlen(string); // 5 (zählt '\0' nicht)
Strings

String-Literale

String-Literale sind Text in Anführungszeichen. Sie sind konstant und im Read-Only-Speicher gespeichert.

Wichtig: String-Literale sollten nicht verändert werden!

// String-Literal (read-only)
char *str1 = "Hallo"; // Pointer auf konstanten String
// str1[0] = 'h'; // Fehler! Nicht veränderbar

// Kopierbarer String
char str2[] = "Hallo"; // Array-Kopie
str2[0] = 'h'; // OK: "hallo"

// Richtig:
const char *konstant = "Hallo"; // Markiert als const

6. Funktionen (Erweitert)

Funktionen

Rekursion

Rekursion ist, wenn eine Funktion sich selbst aufruft. Jede Rekursion braucht eine Abbruchbedingung.

Bestandteile:

  • Basisfall: Abbruchbedingung ohne weiteren Aufruf
  • Rekursiver Fall: Funktion ruft sich selbst auf
  • Fortschritt: Problem wird kleiner
// Fakultät rekursiv
int fakultaet(int n) {
    // Basisfall
    if(n <= 1) return 1;
    // Rekursiver Fall
    return n * fakultaet(n - 1);
}

// fakultaet(5) = 5 * 4 * 3 * 2 * 1 = 120

// Fibonacci
int fib(int n) {
    if(n <= 1) return n;
    return fib(n-1) + fib(n-2);
}
Funktionen

Function Pointers (Funktionszeiger)

Function Pointers sind Zeiger auf Funktionen. Sie ermöglichen das Übergeben von Funktionen als Parameter.

Verwendung:

  • Callback-Funktionen
  • Generische Algorithmen (z.B. qsort)
  • Event-Handler
// Funktionen
int addiere(int a, int b) { return a + b; }
int multipliziere(int a, int b) { return a * b; }

// Function Pointer
int (*operation)(int, int);

operation = addiere;
printf("%d", operation(3, 4)); // 7

operation = multipliziere;
printf("%d", operation(3, 4)); // 12

// Als Parameter
int berechne(int a, int b, int (*func)(int, int)) {
    return func(a, b);
}
Funktionen

Inline Functions

Inline-Funktionen werden mit dem Schlüsselwort inline deklariert. Der Compiler versucht, den Funktionscode direkt einzufügen statt einen Aufruf zu machen.

Vorteile:

  • Schneller (kein Function-Call-Overhead)
  • Für kleine, häufig aufgerufene Funktionen

Nachteile:

  • Größerer Code bei großen Funktionen
  • Nur eine Empfehlung an den Compiler
// Inline-Funktion
inline int max(int a, int b) {
    return (a > b) ? a : b;
}

int ergebnis = max(5, 8); // Wird zu: (5 > 8) ? 5 : 8
Funktionen

Static Functions

Static Functions sind Funktionen, die nur innerhalb ihrer Quelldatei sichtbar sind. Sie haben "internal linkage".

Vorteile:

  • Verhindert Namenskonflikte
  • Bessere Modularisierung
  • Private Hilfsfunktionen
// In helper.c
static int berechneIntern(int x) {
    return x * 2;
}

// Nur in helper.c nutzbar
int oeffentlicheFunktion(int x) {
    return berechneIntern(x) + 5;
}

// In main.c
// berechneIntern(5); // Fehler: nicht sichtbar!
oeffentlicheFunktion(5); // OK
Funktionen

Header Files (.h)

Header Files enthalten Funktionsdeklarationen, Makros, Typdefinitionen und Konstanten. Sie werden mit #include eingebunden.

Struktur:

  • Funktionsprototypen
  • Makros und Konstanten
  • Strukturdefinitionen
  • Include Guards (#ifndef)
// math_utils.h
#ifndef MATH_UTILS_H // Include Guard
#define MATH_UTILS_H

#define PI 3.14159

// Funktionsprototypen
int addiere(int a, int b);
double kreisflaeche(double radius);

#endif

// math_utils.c
#include "math_utils.h"

int addiere(int a, int b) {
    return a + b;
}

// main.c
#include "math_utils.h"

7. Speicherverwaltung (Erweitert)

Speicher

Stack vs Heap

Der Stack und Heap sind zwei verschiedene Speicherbereiche mit unterschiedlichen Eigenschaften.

Stack:

  • Automatische Verwaltung (LIFO)
  • Schnell, aber begrenzt
  • Lokale Variablen, Parameter
  • Wird automatisch freigegeben

Heap:

  • Manuelle Verwaltung (malloc/free)
  • Langsamer, aber groß
  • Dynamische Allokation
  • Muss manuell freigegeben werden
// Stack
int x = 10; // Automatisch auf Stack
int arr[100]; // Stack

// Heap
int *ptr = malloc(100 * sizeof(int)); // Heap
// ... Verwendung ...
free(ptr); // Manuell freigeben
Speicher

Scope (Gültigkeitsbereich)

Der Scope definiert, wo eine Variable sichtbar und zugreifbar ist.

Typen:

  • Local: Nur im Block/Funktion sichtbar
  • Global: Im gesamten Programm sichtbar
  • Static (lokal): Behält Wert zwischen Aufrufen
  • Static (global): Nur in aktueller Datei sichtbar
int global = 100; // Globaler Scope

void funktion() {
    int lokal = 5; // Lokaler Scope
    static int zaehler = 0; // Behält Wert
    zaehler++;
}

// lokal existiert hier nicht!
// global ist überall verfügbar
Speicher

Lifetime (Lebensdauer)

Die Lifetime beschreibt, wie lange eine Variable im Speicher existiert.

Typen:

  • Automatic: Existiert nur während Blockausführung
  • Static: Existiert während gesamter Programmlaufzeit
  • Dynamic: Von malloc bis free
void beispiel() {
    int auto_var = 5; // Automatic: stirbt bei }
    static int static_var = 5; // Static: lebt weiter
    int *dyn = malloc(sizeof(int)); // Dynamic
    free(dyn); // Beendet Lifetime
}
Speicher

Memory Layout (Speicherlayout)

Ein Programm ist im Speicher in verschiedene Segmente unterteilt.

Segmente (von oben nach unten):

  • Stack: Lokale Variablen, Parameter (wächst nach unten)
  • Heap: Dynamischer Speicher (wächst nach oben)
  • BSS: Uninitialisierte globale Variablen
  • Data: Initialisierte globale Variablen
  • Text: Programmcode (read-only)
// Text Segment
int addiere(int a, int b) { return a + b; }

// Data Segment
int global_init = 42;

// BSS Segment
int global_uninit;

int main() {
    int stack_var = 10; // Stack
    int *heap_var = malloc(sizeof(int)); // Heap
    free(heap_var);
}
Speicher

Dangling Pointer

Ein Dangling Pointer ist ein Pointer, der auf einen ungültigen Speicherbereich zeigt. Häufige Fehlerquelle!

Ursachen:

  • Zugriff nach free()
  • Rückgabe von Adressen lokaler Variablen
  • Verwendung nach Scope-Ende
// Problem 1: Nach free()
int *ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr);
// *ptr = 10; // FEHLER: Dangling Pointer!
ptr = NULL; // Lösung: Auf NULL setzen

// Problem 2: Lokale Variable
int* fehler() {
    int lokal = 5;
    return &lokal; // FEHLER: lokal stirbt!
}

// Richtig:
int* richtig() {
    int *p = malloc(sizeof(int));
    *p = 5;
    return p; // OK (aber free nicht vergessen!)
}

8. Strukturen & Datentypen

Strukturen

struct (Struktur)

Eine Struktur ist ein zusammengesetzter Datentyp, der mehrere Variablen unterschiedlicher Typen unter einem Namen gruppiert.

Vorteile:

  • Zusammenfassung zusammengehöriger Daten
  • Bessere Code-Organisation
  • Grundlage für objektorientiertes Design
// Definition
struct Person {
    char name[50];
    int alter;
    double groesse;
};

// Verwendung
struct Person p1;
strcpy(p1.name, "Anna");
p1.alter = 25;
p1.groesse = 1.70;

// Initialisierung
struct Person p2 = {"Bob", 30, 1.85};
Strukturen

typedef

typedef erstellt einen Alias (alternativen Namen) für einen bestehenden Datentyp. Macht Code lesbarer.

Verwendung: Oft mit struct kombiniert

// Ohne typedef
struct Person {
    char name[50];
    int alter;
};
struct Person p1; // "struct" nötig

// Mit typedef
typedef struct {
    char name[50];
    int alter;
} Person;

Person p2; // Kürzer!

// Andere Beispiele
typedef unsigned long ulong;
typedef int* IntPtr;
Strukturen

enum (Aufzählung)

Ein enum definiert einen Datentyp mit benannten ganzzahligen Konstanten. Macht Code lesbarer als rohe Zahlen.

Standardwerte: Beginnen bei 0, automatisch hochgezählt

// Definition
enum Wochentag {
    MONTAG, // 0
    DIENSTAG, // 1
    MITTWOCH, // 2
    DONNERSTAG, // 3
    FREITAG // 4
};

enum Wochentag heute = MITTWOCH;

// Eigene Werte
enum Status {
    FEHLER = -1,
    OK = 0,
    WARNUNG = 1
};

// Mit typedef
typedef enum { ROT, GELB, GRUEN } Ampel;
Strukturen

union

Eine union ist wie eine struct, aber alle Members teilen sich denselben Speicherplatz. Nur ein Member ist gleichzeitig gültig.

Größe: So groß wie das größte Member

union Daten {
    int i;
    float f;
    char str[20];
};

union Daten d;
d.i = 42; // Jetzt ist i gültig
printf("%d", d.i); // 42

d.f = 3.14; // Jetzt ist f gültig, i ungültig!
printf("%f", d.f); // 3.14
// printf("%d", d.i); // FEHLER: i nicht mehr gültig!

// Größe = sizeof(größtes Member) = 20 Bytes
Strukturen

Nested Structures (Verschachtelte Strukturen)

Strukturen können andere Strukturen als Members enthalten.

typedef struct {
    int tag;
    int monat;
    int jahr;
} Datum;

typedef struct {
    char name[50];
    Datum geburtsdatum; // Verschachtelt
    int alter;
} Person;

// Verwendung
Person p;
strcpy(p.name, "Anna");
p.geburtsdatum.tag = 15;
p.geburtsdatum.monat = 3;
p.geburtsdatum.jahr = 1998;
p.alter = 27;
Strukturen

Pointer zu Strukturen

Pointer auf Strukturen verwenden den Pfeil-Operator (->) statt dem Punkt-Operator.

Vorteil: Effiziente Übergabe an Funktionen

typedef struct {
    char name[50];
    int alter;
} Person;

Person p1 = {"Anna", 25};
Person *ptr = &p1;

// Zwei gleichwertige Zugriffe:
printf("%s", (*ptr).name); // Mit Dereferenzierung
printf("%s", ptr->name); // Mit Pfeil-Operator

// In Funktionen
void ausgabe(Person *p) {
    printf("%s ist %d", p->name, p->alter);
}

ausgabe(&p1);

9. Dateioperationen

Dateien

fopen

fopen() öffnet eine Datei und gibt einen FILE-Pointer zurück. Erste Funktion für Dateioperationen.

Modi:

  • "r": Lesen (Datei muss existieren)
  • "w": Schreiben (löscht existierende Datei)
  • "a": Anhängen (am Ende hinzufügen)
  • "r+": Lesen und Schreiben
  • "rb", "wb": Binärmodus (+ "b")
FILE *datei = fopen("test.txt", "r");
if(datei == NULL) {
    printf("Fehler beim Öffnen!");
    return -1;
}

// ... Dateioperationen ...

fclose(datei); // Immer schließen!
Dateien

fclose

fclose() schließt eine geöffnete Datei. Wichtig: Puffert werden geleert und Ressourcen freigegeben.

FILE *datei = fopen("test.txt", "r");
// ... Operationen ...
fclose(datei); // Datei schließen
datei = NULL; // Empfohlen
Dateien

fread & fwrite

Binäre Lese- und Schreibfunktionen für Rohdaten.

Syntax: fread(buffer, size, count, file)

  • buffer: Ziel/Quelle der Daten
  • size: Größe eines Elements
  • count: Anzahl der Elemente
  • file: FILE-Pointer
// Schreiben
int zahlen[5] = {1, 2, 3, 4, 5};
FILE *f = fopen("data.bin", "wb");
fwrite(zahlen, sizeof(int), 5, f);
fclose(f);

// Lesen
int gelesen[5];
f = fopen("data.bin", "rb");
fread(gelesen, sizeof(int), 5, f);
fclose(f);
Dateien

fprintf & fscanf

Formatierte Ausgabe/Eingabe in/aus Dateien. Wie printf/scanf, aber für Dateien.

// Schreiben
FILE *f = fopen("output.txt", "w");
fprintf(f, "Name: %s\n", "Anna");
fprintf(f, "Alter: %d\n", 25);
fclose(f);

// Lesen
char name[50];
int alter;
f = fopen("output.txt", "r");
fscanf(f, "Name: %s\n", name);
fscanf(f, "Alter: %d\n", &alter);
fclose(f);
Dateien

fgets & fputs

Zeilenweises Lesen und Schreiben von Strings.

fgets: Liest bis zu n-1 Zeichen oder bis Newline

// Schreiben
FILE *f = fopen("text.txt", "w");
fputs("Erste Zeile\n", f);
fputs("Zweite Zeile\n", f);
fclose(f);

// Lesen
char zeile[100];
f = fopen("text.txt", "r");
while(fgets(zeile, 100, f) != NULL) {
    printf("%s", zeile);
}
fclose(f);
Dateien

Binary vs Text Mode

Dateien können im Text- oder Binärmodus geöffnet werden.

Text Mode:

  • Für lesbare Textdateien
  • Newline-Konvertierung (OS-abhängig)
  • Modi: "r", "w", "a"

Binary Mode:

  • Für Rohdaten (Bilder, Videos, etc.)
  • Keine Konvertierung, Byte-für-Byte
  • Modi: "rb", "wb", "ab"
// Text Mode
FILE *txt = fopen("text.txt", "r");
fprintf(txt, "Text: %d", 42);

// Binary Mode
FILE *bin = fopen("data.bin", "rb");
int zahl;
fread(&zahl, sizeof(int), 1, bin);

// Wichtig: Strukturen nur im Binary Mode!

10. Git & Versionskontrolle (Erweitert)

Git

Merge

Merge kombiniert Änderungen aus verschiedenen Branches. Es gibt verschiedene Merge-Strategien.

Arten:

  • Fast-Forward: Einfaches Vorspulen ohne Merge-Commit
  • 3-Way-Merge: Erstellt Merge-Commit mit zwei Parents
  • Recursive: Standard-Strategie für komplexe Merges
git checkout main
git merge feature-branch

// Fast-Forward erzwingen
git merge --ff-only feature

// Merge-Commit erzwingen
git merge --no-ff feature
Git

Conflict Resolution (Konfliktlösung)

Konflikte entstehen, wenn dieselben Zeilen in verschiedenen Branches unterschiedlich geändert wurden.

Vorgehen:

  • Git markiert Konflikte mit <<<, ===, >>>
  • Manuell entscheiden, welche Version behalten
  • Marker entfernen
  • git add und git commit
// Konflikt-Marker in Datei:
<<<<<<< HEAD
int x = 10;
=======
int x = 20;
>>>>>>> feature-branch

// Nach Lösung:
int x = 15; // Kompromiss oder eine Version

git add datei.c
git commit -m "Konflikt gelöst"
Git

Branching Strategies

Branching Strategies definieren, wie Branches in einem Team verwendet werden.

Beliebte Strategien:

  • Git Flow: main, develop, feature/*, release/*, hotfix/*
  • GitHub Flow: Nur main + Feature-Branches
  • Trunk-Based: Direkt auf main, kurze Feature-Branches
// Git Flow
git checkout -b develop
git checkout -b feature/neue-funktion
// ... Entwicklung ...
git checkout develop
git merge feature/neue-funktion

git checkout -b release/1.0
// ... Tests ...
git checkout main
git merge release/1.0
git tag v1.0
Git

Rebase

Rebase verschiebt Commits auf eine neue Basis. Linearisiert die History.

Unterschied zu Merge:

  • Merge: Erstellt Merge-Commit, behält Branches
  • Rebase: Schreibt History um, lineare Historie

Warnung: Niemals öffentliche Commits rebasen!

// Feature-Branch auf aktuellen main aktualisieren
git checkout feature
git rebase main

// Bei Konflikten:
// 1. Konflikt lösen
git add datei.c
git rebase --continue

// Oder abbrechen:
git rebase --abort

// Interaktives Rebase (Commits bearbeiten)
git rebase -i HEAD~3
Git

Cherry-pick

Cherry-pick kopiert einzelne Commits von einem Branch zu einem anderen.

Verwendung:

  • Spezifische Fixes übertragen
  • Einzelne Features aus Branch holen
  • Hotfixes in mehrere Branches
// Commit-Hash finden
git log feature-branch

// Commit in aktuellen Branch kopieren
git cherry-pick a1b2c3d

// Mehrere Commits
git cherry-pick a1b2c3d e4f5g6h

// Range
git cherry-pick main~4..main~2
Git

Stash

Stash speichert temporär uncommittete Änderungen, um den Working Directory sauber zu machen.

Verwendung:

  • Branch wechseln ohne Commit
  • Änderungen temporär parken
  • Sauberen Zustand wiederherstellen
// Änderungen stashen
git stash
git stash save "WIP: Feature X"

// Liste anzeigen
git stash list

// Wiederherstellen
git stash pop // Letzter stash + entfernen
git stash apply // Letzter stash, behalten
git stash apply stash@{2} // Spezifischer stash

// Löschen
git stash drop stash@{0}
git stash clear // Alle löschen

📌 Wichtige Hinweise

📚 Viel Erfolg beim Lernen!

HTW Berlin • Fortgeschrittene Algorithmen und Programmierung • WiSe 2025/26