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