Notes 4.5
Sie können diese Seite ausdrucken oder mit einem PDF-Drucker in ein PDF umwandeln, um Ihre eigenen Notizen hinzuzufügen.
Willkommen zur Fortsetzung!
- Letzte Woche haben wir Pointer eingeführt – Variablen, die Speicheradressen enthalten.
- Heute vertiefen wir dieses Wissen. Wir schauen uns noch einmal genauer an, wie man dynamisch Speicher anfordert und wieder freigibt (malloc und free).
- Außerdem schauen wir uns an, wie Funktionsaufrufe im Speicher organisiert werden (Stack) und wie man mit Dateien arbeitet (File I/O).
Übungsprobleme: Pointer-Wiederholung
In der Vorlesung haben wir drei Übungsprobleme bearbeitet, um das Pointer-Wissen von letzter Woche zu aktivieren.
Kurze Pointer-Syntax-Wiederholung
- Adressoperator
&: Gibt die Speicheradresse einer Variable zurück - Dereferenzierungsoperator
*: Greift auf den Wert an einer Speicheradresse zu - Pointer-Deklaration:
int *p = &n;— p speichert die Adresse von n - Dereferenzierung:
*pgibt den Wert an der Adresse zurück (oder setzt ihn) - Array-Pointer-Äquivalenz:
s[i]ist identisch mit*(s + i) - Strings: Ein
char*zeigt auf das erste Zeichen;\0markiert das Ende
malloc und dynamische Speicherverwaltung
Früher haben wir Arrays immer mit fester Größe deklariert, die zur Compile-Zeit bekannt sein muss. Mit malloc können wir Speicher zur Laufzeit anfordern – das nennt man dynamische Speicherallokation.
Wir beginnen mit einer neuen Datei testme.c:
#include <stdio.h>
int main(void)
{
int numbers[4];
}Das ist ein statisches Array – die Größe 4 ist fest im Code. Jetzt ändern wir es zu dynamischer Speicherallokation:
#include <stdio.h>
int main(void)
{
int *numbers = malloc(4 * sizeof(int));
}Wenn wir das kompilieren, bekommen wir einen Fehler. Wir haben vergessen, die Standard-Library einzubinden:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *numbers = malloc(4 * sizeof(int));
}Jetzt weisen wir den Elementen Werte zu und geben sie aus:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *numbers = malloc(4 * sizeof(int));
numbers[0] = 1;
numbers[1] = 2;
numbers[2] = 3;
for (int i = 0; i < 4; i++)
{
printf("%i ", numbers[i]);
}
printf("\n");
}Ausgabe: 1 2 3 0
Moment – woher kommt die 0? Wir haben numbers[3] nie initialisiert! Der von malloc zurückgegebene Speicher enthält Garbage-Werte – was auch immer vorher an dieser Stelle im Speicher stand. In diesem Fall war es zufällig 0, aber das kann jeder beliebige Wert sein – darauf darf man sich nie verlassen!
Array-Notation vs. Zugriff mit Pointer-Arithmetik
Eine weitere Erkenntnis: Die Array-Notation numbers[i], die wir zuerst kennengelernt haben, ist nichts anderes als ein Dereferenzieren eines Pointers – sie sieht einfacher und übersichtlicher aus. Man sagt auch numbers[i] ist syntaktischer Zucker (syntactic sugar) für *(numbers + i). Wir können das direkt zeigen:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *numbers = malloc(4 * sizeof(int));
numbers[0] = 1;
numbers[1] = 2;
numbers[2] = 3;
for (int i = 0; i < 4; i++)
{
printf("%i ", *(numbers + i));
}
printf("\n");
}Die Ausgabe ist identisch! Der Compiler weiß, dass numbers ein int* ist, also bedeutet numbers + 1 in Wirklichkeit “ausgehend von der Adresse von numbers gehe um 4 Bytes” weiter (die Größe eines int).
malloc(size)reserviert zur LaufzeitsizeBytes auf dem Heap und gibt einen Pointer zurücksizeof(typ)gibt die Größe eines Datentyps in Bytes zurück- Garbage-Werte:
mallocinitialisiert den Speicher nicht! - Array ≡ Pointer:
a[i]ist identisch mit*(a + i)
Strings, Pointer und Segmentation Faults
Was passiert, wenn wir zwei Pointer auf denselben String zeigen lassen? Wir programmieren:
#include <stdio.h>
int main(void)
{
char *s = "Hi";
char *t;
t = s;
printf("%s %s\n", s, t);
}Ausgabe: Hi Hi
Das ist zu erwarten – t zeigt auf dieselbe Speicheradresse wie s. Beide Pointer verweisen auf dasselbe String-Literal.
Was passiert wohl, wenn wir das zweite Zeichen in s ändern nachdem wir seinen Wert in t kopiert haben? Ändert sich dann auch t (wir erwarten: nein, um Strings zu kopieren braucht man bekanntlich strcpy). Probieren wir es also aus:
#include <stdio.h>
int main(void)
{
char *s = "Hi";
char *t;
t = s;
s[1] = 'o'; // Aus "Hi" soll "Ho" werden
printf("%s %s\n", s, t);
}Kompiliert problemlos, aber bei der Ausführung:
Segmentation faultWarum? "Hi" ist ein String-Literal (ein fester Wert). Initialisiert man einen char-Pointer mit einem String-Literal, legt der Compiler die Bytes des Literals in einem schreibgeschützten Speicherbereich ab. Der Pointer s (der wie jede lokale Variable auf dem Stack gespeichert wird – später dazu mehr!) zeigt dann auf diesen schreibgeschützten Speicherbereich. Dort dürfen wir nicht schreiben und beim Versuch, es doch zu tun, stürzt das Programm ab.
Die Lösung: Stack-Array statt Pointer
Wenn wir den String verändern wollen, müssen wir ihn auf dem Stack anlegen – das geht mit malloc nicht (malloc liefert immer Pointer auf Speicher im Heap, wo viel mehr Platz ist als auf dem Stack):
#include <stdio.h>
int main(void)
{
char s1[4] = "Hi!"; // Array auf dem Stack - veränderbar!
char *s2 = "Hi!"; // Pointer auf Literal - nur lesen!
printf("%s %s\n", s1, s2);
}Ausgabe: Hi! Hi!
Beide sehen gleich aus, aber intern sind sie unterschiedlich. Wir können das mit %p sehen:
#include <stdio.h>
int main(void)
{
char s1[4] = "Hi!";
char *s2 = "Hi!";
printf("%s %s\n", s1, s2);
printf("%p %p\n", s1, s2);
}Die Adressen sind völlig unterschiedlich! s1 liegt auf dem Stack, s2 zeigt in den Nur-Lese-Bereich.
Jetzt können wir s1 verändern:
#include <stdio.h>
int main(void)
{
char s1[4] = "Hi!";
char *s2 = "Hi!";
s1[1] = 'o'; // OK - s1 liegt auf dem Stack
// s2[1] = 'o'; // CRASH - s2 in read-only memory
printf("%s %s\n", s1, s2);
}Ausgabe: Ho! Hi!
strcpy – Strings kopieren
Wir wollen wieder einmal einen String kopieren. Dazu nutzen wir strcpy aus <string.h> – also programmieren wir schnell:
#include <stdio.h>
#include <string.h>
int main(void)
{
char s1[4] = "Hi!";
char *t;
strcpy(t, s1);
printf("%s\n", t);
}Segmentation Fault! Warum? Wir haben vergessen, Speicher zu reservieren. Der Pointer t ist nicht initialisiert, er enthält Garbage Values – also das, was auch immer im Speicher steht, an dessen Stelle der Compiler diesen Pointer (auf dem Stack) speichert. Der Wert im Speicher wird von strcpy dann als Adresse verwendet, an die s1 kopiert werden soll. Beim Versuch an der Adresse, die in t steht, in den Speicher zu schreiben, fällt dem Betriebssystem der Fehler auf und es lässt unser Programm abstürzen.
Wir müssen zuerst Speicher reservieren:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char s1[4] = "Hi!";
char t[4]; // Speicher auf dem Stack
strcpy(t, s1);
printf("%s %s\n", s1, t);
}Ausgabe: Hi! Hi!
Jetzt haben wir zwei unabhängige Kopien – t ändern ändert s1 nicht.
- String-Literal (
char *s = "Hi"): Nur lesbar, liegt im geschützten Speicher - Stack-Array (
char s[4] = "Hi"): Veränderbar, liegt auf dem Stack - Segmentation Fault: Betriebssystem hat Versuch erkannt, in geschützten Speicher zu schreiben
strcpy(dest, src): Kopiert String vonsrcnachdest– Ziel muss genug Platz haben!
Stack vs. Heap
Wir haben gesehen, dass wir mit strcpy Strings kopieren können. Aber wohin kopieren wir? Es gibt zwei Möglichkeiten: Stack oder Heap.
Heap-Allokation mit malloc
Wenn wir zur Laufzeit Speicher brauchen, verwenden wir malloc:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char s1[10] = "Hi!";
char *t = malloc(strlen(s1));
strcpy(t, s1);
printf("%s\n", t);
}Das Programm wird meistens laufen, aber es hat einen Bug! strlen("Hi!") gibt 3 zurück, aber der String braucht aber 4 Bytes (H, i, !, \0). Die korrigierte Version:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char s1[10] = "Hi!";
char *t = malloc(strlen(s1) + 1); // +1 für Null-Terminator!
strcpy(t, s1);
printf("%s\n", t);
}malloc kann fehlschlagen
Was passiert, wenn kein Speicher mehr verfügbar ist? malloc gibt einen besonderen Wert zurück: NULL. Daher muss man immer auf NULL prüfen, wenn man malloc verwendet.:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char s1[10] = "Hi!";
char *t = malloc(strlen(s1) + 1);
if (t == NULL)
{
return 1; // Fehler - kein Speicher!
}
strcpy(t, s1);
printf("%s\n", t);
}Stack vs. Heap im Vergleich
Hier ein Programm, das alle vier Varianten zeigt:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char s1[4] = "Hi!"; // Stack-Array
char *s2 = "Hi!"; // Pointer auf Literal (read-only)
char *u = malloc(strlen(s1) + 1); // Heap
char t[4]; // Stack-Array
strcpy(t, s1);
strcpy(u, s1);
s1[1] = 'o';
printf("%s %s %s %s\n", s1, s2, t, u);
printf("s1=%p s2=%p t=%p u=%p\n", s1, s2, t, u);
}Ausgabe:
Ho! Hi! Hi! Hi!
s1=0x7fff... s2=0x55... t=0x7fff... u=0x55...Beachten Sie die Adressen:
s1undthaben ähnliche Adressen (beide auf dem Stack, beginnen mit0x7fff...)s2zeigt in den schreibgeschützten Datenbereich (wo String-Literale liegen)uzeigt in den Heap (vonmallocallokiert)- Beide (
s2undu) liegen bei niedrigeren Adressen als der Stack (beginnen mit0x55...)
Das Speicherlayout
Der Speicher eines Programms ist in Bereiche aufgeteilt:
┌─────────────────┐ niedrige Adressen
│ Machine Code │ ← der kompilierte Code
├─────────────────┤
│ Globals │ ← globale Variablen (außerhalb von Funktionen)
├─────────────────┤
│ Heap │ ← wächst zu größeren Adressen ↓
│ ↓ │
│ │
│ ↑ │
│ Stack │ ← wächst zu kleineren Adressen ↑
└─────────────────┘ hohe Adressen- Stack: Lokale Variablen, Funktionsparameter, Rücksprungadressen (erklären wir später). Automatisch verwaltet.
- Heap: Dynamisch allokierter Speicher (
malloc). Muss manuell freigegeben werden (free). - Globals: Variablen, die außerhalb von Funktionen deklariert werden (z.B.
int WIDTH = 256;in unserem BMP-Programm am Ende der heutigen Vorlesung). Sie existieren während der gesamten Programmlaufzeit und sind von jeder Funktion aus erreichbar. - Machine Code: Das kompilierte Programm selbst.
Bonus: Speicherbereiche mit GDB inspizieren (nicht klausurrelevant)
In der Pause haben wir mit dem Debugger GDB experimentiert, um zu sehen, wie das Programm tatsächlich im Speicher liegt. GDB (GNU Debugger) ist ein mächtiges Werkzeug zum Analysieren von Programmen – die Details sind nicht klausurrelevant, aber helfen beim Verständnis des Speicherlayouts.
Zunächst ein Testprogramm, das drei verschiedene Speicherbereiche zeigt:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char s1[4] = "Hi!"; // liegt auf dem Stack
char *s2 = "Hi"; // zeigt auf String-Literal (schreibgeschützt)
char *s3 = malloc(3); // liegt auf dem Heap
printf("%p\n%p\n%p", s1, s2, s3);
}Ausgabe:
0x7fff397fb64c ← s1 (Stack)
0x5e02afb66008 ← s2 (String-Literal, read-only)
0x5e02cd9be2a0 ← s3 (Heap, von malloc)Mit gdb ./testme, dann break main, run und info proc mappings sehen wir alle Speicherbereiche:
(gdb) info proc mappings
Mapped address spaces:
Start Addr End Addr Perms objfile
0x5f951484b000 0x5f951484c000 r--p /workspaces/.../testme
0x5f951484c000 0x5f951484d000 r-xp /workspaces/.../testme ← Code
0x5f951484d000 0x5f951484e000 r--p /workspaces/.../testme ← Literale!
0x5f951484f000 0x5f9514850000 rw-p /workspaces/.../testme
...
0x726da498b000 0x726da49b3000 r--p /usr/lib/.../libc.so.6
0x726da49b3000 0x726da4b3b000 r-xp /usr/lib/.../libc.so.6 ← printf etc.
...
0x726da4b90000 0x726da4b9d000 rw-p ← Heap-Bereich (anonym)
...
0x7ffe6b40e000 0x7ffe6b42f000 rw-p [stack]Die Perms-Spalte (Permissions) zeigt die Berechtigungen:
r= read (lesen erlaubt)w= write (schreiben erlaubt)x= execute (ausführen erlaubt)p= private (privat für diesen Prozess)
Wo liegt was?
| Variable | Adresse (ca.) | Bereich | Perms | Bedeutung |
|---|---|---|---|---|
s1 | 0x7fff... | [stack] | rw-p | Stack: lesbar, schreibbar |
s2 | 0x5e02... (niedrig) | testme | r--p | Read-only! Literale |
s3 | 0x5e02... (Heap) | anonym | rw-p | Heap: lesbar, schreibbar |
Wichtige Erkenntnisse:
- String-Literale (
"Hi") liegen imr--p-Bereich des Programms – nur lesbar, daher der Segmentation Fault beim Schreibversuch! - Der Heap (malloc) ist ein anonymer
rw-p-Bereich – lesbar und schreibbar - Der Stack ist
rw-p, aber nicht ausführbar (xfehlt) – das schützt vor Buffer-Overflow-Attacken - Nur der Code-Bereich hat
x(execute) – das Betriebssystem verhindert, dass andere Bereiche als Code ausgeführt werden
Speicherverwaltungsfunktionen
| Funktion | Beschreibung |
|---|---|
malloc(size) | Reserviert size Bytes (nicht initialisiert) |
free(ptr) | Gibt Speicher wieder frei |
Weitere Funktionen (nicht relevant in Inf-Einf-B): calloc (Speicher wird reserviert und mit Nullen initialisiert), realloc (Größe eines bereits reservierten Speicherbereichs ändern)
- Stack: Schnell, automatisch, begrenzte Größe
- Heap: Flexibel, manuell, größerer Speicher verfügbar
strlen(s) + 1: Immer den Null-Terminator berücksichtigen!free(ptr): Heap-Speicher muss explizit freigegeben werden
Funktionen und der Stack
Um zu verstehen, wie der Stack bei Funktionsaufrufen funktioniert, bauen wir ein Programm mit mehreren Funktionen, die jeweils eine lokale Variable verwenden:
#include <stdio.h>
void f(void)
{
char s2[10] = "Hello";
printf("%p %s (in f)\n", s2, s2);
}
int main(void)
{
char s1[10] = "Hi";
printf("%p %s (in main)\n", s1, s1);
f();
}Ausgabe:
0x7ffffffc4eb6 Hi (in main)
0x7ffffffc4e96 Hello (in f)Die Adresse in f ist kleiner als in main! Man sagt: Der Stack “wächst nach unten”, also zu kleineren Adressen, wenn eine Funktion aufgerufen wird.
Fügen wir eine dritte Funktion hinzu, die von der zweiten aufgerufen wird:
#include <stdio.h>
void g(void)
{
char s3[10] = "World";
printf("%p %s (in g)\n", s3, s3);
}
void f(void)
{
char s2[10] = "Hello";
printf("%p %s (in f)\n", s2, s2);
g(); // f ruft g auf!
}
int main(void)
{
char s1[10] = "Hi";
printf("%p %s (in main)\n", s1, s1);
f();
}Ausgabe:
0x7ffffffc4eb6 Hi (in main)
0x7ffffffc4e96 Hello (in f)
0x7ffffffc4e76 World (in g)In g hat der String eine noch kleinere Adresse – wie erwartet.
Jede Funktion bekommt ihren eigenen Bereich auf dem Stack – einen Stack-Frame. Die Adressen werden immer kleiner, je tiefer wir in die Funktionsaufrufe gehen.
Stack-Wachstum bei verschachtelten Aufrufen
Schauen wir uns die zeitliche Abfolge an. Der Stack wächst zu niedrigeren Adressen (“nach oben” im Diagramm):
(1) in main() (2) in f() (3) in g()
┌────────────────┐
│ g(): "World" │
│ 0x...4e76 │
┌────────────────┐ ├────────────────┤
│ f(): "Hello" │ │ f(): "Hello" │
│ 0x...4e96 │ │ 0x...4e96 │
┌────────────────┐ ├────────────────┤ ├────────────────┤
│ main(): "Hi" │ │ main(): "Hi" │ │ main(): "Hi" │
│ 0x...4eb6 │ │ 0x...4eb6 │ │ 0x...4eb6 │
└────────────────┘ └────────────────┘ └────────────────┘In Schritt (3) sind alle drei Stack-Frames gleichzeitig aktiv: main() hat f() aufgerufen, und f() hat g() aufgerufen.
Was passiert beim Zurückkehren?
(4) nach g() (5) nach f()
zurück in f() zurück in main()
┌────────────────┐
│ f(): "Hello" │
│ 0x...4e96 │
├────────────────┤ ┌────────────────┐
│ main(): "Hi" │ │ main(): "Hi" │
│ 0x...4eb6 │ │ 0x...4eb6 │
└────────────────┘ └────────────────┘Wenn g() zurückkehrt (Schritt 4), wird sein Stack-Frame “freigegeben” – er wird nicht aktiv überschrieben, aber der Compiler weiß, dass dieser Speicherbereich nun wieder verfügbar ist. Wenn dann f() zurückkehrt (Schritt 5), wird auch dessen Frame freigegeben.
Stack-Frame-Wiederverwendung
Um das “Freigeben” besser zu verstehen, ändern wir das Programm: Statt dass f() die Funktion g() aufruft, ruft nun main() beide Funktionen nacheinander auf:
#include <stdio.h>
void g(void)
{
char s3[10] = "World";
printf("%p %s (in g)\n", s3, s3);
}
void f(void)
{
char s2[10] = "Hello";
printf("%p %s (in f)\n", s2, s2);
// g() wird NICHT mehr von hier aufgerufen!
}
int main(void)
{
char s1[10] = "Hi";
printf("%p %s (in main)\n", s1, s1);
f(); // f() wird aufgerufen und kehrt zurück
g(); // DANN wird g() aufgerufen
}Ausgabe:
0x7ffffffc4eb6 Hi (in main)
0x7ffffffc4e96 Hello (in f)
0x7ffffffc4e96 World (in g)Die lokalen Variablen von f() und g() haben dieselbe Adresse! Das zeigt: Nachdem f() zurückgekehrt ist, wurde sein Stack-Frame freigegeben. Als g() dann aufgerufen wurde, hat es denselben Speicherbereich wiederverwendet.
So sieht die zeitliche Abfolge aus:
(1) vor f() (2) während f() (3) nach f() (4) während g()
vor g()
┌────────────────┐ ┌────────────────┐
│ f(): "Hello" │ │ g(): "World" │
│ 0x...4e96 │ │ 0x...4e96 │
────────────────── ├────────────────┤ ────────────────── ├────────────────┤
┌────────────────┐ │ main(): "Hi" │ ┌────────────────┐ │ main(): "Hi" │
│ main(): "Hi" │ │ 0x...4eb6 │ │ main(): "Hi" │ │ 0x...4eb6 │
│ 0x...4eb6 │ └────────────────┘ │ 0x...4eb6 │ └────────────────┘
└────────────────┘ └────────────────┘In Schritt (2) liegt f() “über” main() auf dem Stack. In Schritt (3) ist f() zurückgekehrt – sein Frame ist freigegeben, aber die Bytes stehen noch im Speicher (sie werden nicht aktiv gelöscht). In Schritt (4) wird g() aufgerufen und bekommt exakt denselben Speicherbereich zugewiesen, den zuvor f() hatte.
Das ist auch der Grund, warum lokale Variablen “Garbage Values” enthalten können: Wenn wir eine Variable nicht initialisieren, enthält sie noch die Werte, die eine frühere Funktion dort hinterlassen hat!
Was speichert ein Stack-Frame?
Jeder Stack-Frame enthält u.a.:
- Lokale Variablen der Funktion
- Parameter, die der Funktion übergeben wurden (falls vorhanden)
- Rücksprungadresse (wo geht es im Programm weiter, wenn die Funktion endet oder
returnaufgerufen wird)
┌─────────────────────────┐
│ Rücksprungadresse │
│ Parameter │
│ Lokale Variablen │
│ ... │
└─────────────────────────┘- Stack-Frame: Jede Funktion bekommt einen eigenen Speicherbereich auf dem Stack
- Wachstumsrichtung: Stack wächst zu kleineren Adressen (nach oben im Diagramm)
- Automatische Verwaltung: Stack-Frames werden vom Compiler automatisch erstellt und freigegeben
- Begrenzte Größe: Zu viele ineinander geschachtelte Funktionsaufrufe (z.B. durch Rekursion) führen zu Stack Overflow
Speichergrenzen
Wir würden gerne einmal den Fall sehen, dass malloc NULL zurückgibt, weil es keinen Speicher mehr reservieren konnte. Können wir das provozieren, indem wir sehr viel Speicher anfordern?
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
// 1024^3 = 1 Gigabyte (GB)
char *p = malloc(1024 * 1024 * 1024);
if (p == NULL)
{
printf("Kein Speicher!\n");
return 1;
}
printf("Speicher erhalten: %p\n", p);
}Überraschenderweise funktioniert das oft, sogar wenn im Rechner weniger als 1 GB Speicher eingebaut wäre! Warum? Moderne Betriebssysteme verwenden Memory Overcommit – sie versprechen Speicher, der vielleicht gar nicht physisch existiert. Erst wenn man den Speicher tatsächlich benutzt, wird es problematisch.
Achtung: Integer Overflow
Moderne Computer haben oft 16, 32 oder sogar 64 GB RAM. Angenommen, wir wollen 10 GB auf einmal reservieren:
char *p = malloc(10 * 1000000000); // 10 GB?
Das funktioniert nicht wie erwartet! Das Problem: Zahlenliterale wie 1000000000 werden vom Compiler als int interpretiert. Ein int ist typischerweise 32 Bit und kann maximal die Zahl 2 Milliarden (231 – 1 = 2.147.483.647) darstellen. Wieso 231 – 1 und nicht 232 – 1? Weil int ein vorzeichenbehafteter Datentyp ist (signed); ein Bit von den 32 Bit wird benötigt, um das Vorzeichen zu speichern; der Wertebereich geht von –2.147.483.648 bis +2.147.483.647 geht).
Was passiert bei 10 * 1000000000? Das Ergebnis (10 Milliarden) passt nicht in einen int – es kommt zu einem Integer Overflow. Der Compiler könnte warnen, aber im schlimmsten Fall kompiliert das Programm und verhält sich zur Laufzeit unerwartet.
size_t (nicht klausurrelevant)
Die Lösung ist der Datentyp size_t – ein unsigned Integer, der groß genug für jede Speichergröße ist:
// FALSCH: Literale werden als int interpretiert (32-bit)
char *p = malloc(10 * 1000000000); // Overflow vor dem malloc-Aufruf!
// RICHTIG: Expliziter Cast auf size_t (portabel auf allen Plattformen)
size_t size = (size_t)10 * 1000000000;
char *p = malloc(size);
// Alternativ mit ULL-Suffix (unsigned long long, mind. 64-bit):
char *p = malloc(10ULL * 1000000000ULL);Der Cast (size_t) ist die sicherste Variante, weil size_t auf jeder Plattform die richtige Größe für Speicheradressen hat. Das ULL-Suffix steht für “unsigned long long” und ist garantiert mindestens 64 Bit groß – im Gegensatz zu UL (“unsigned long”), das auf Windows auch im 64-bit-Modus nur 32 Bit sein kann.
scanf und Buffer Overflow
Wir haben in der letzten Vorlesung scanf gesehen. Damit kann man Werte von der Standard-Eingabe einlesen. scanf erwartet einen Format-String wie printf und die Adresse im Speicher, an die es den eingelesenen Wert speichern soll. Bevor man scanf aufruft, muss man also den benötigten Speicher auf dem Stack oder Heap reservieren.
Eine der gefährlichsten Funktionen in C ist scanf mit %s.
Schauen wir uns erst einmal an, wie man mit scanf eine Zahl einliest (so ähnlich wie get_int aus cs50.h):
#include <stdio.h>
int main(void)
{
int n;
scanf("%d", &n); // Liest eine Zahl - sicher!
printf("n = %d\n", n);
}Hier kann nichts Schlimmes passieren: Mit %d liest scanf genau einen int (4 Bytes), egal wie viele Ziffern man eingibt. Die Zahl wird einfach in den Integer konvertiert.
Gefährlich: scanf mit %s
Das Problem ist %s – hier liest scanf beliebig viele Zeichen – eben bis die Eingabe im Terminal zu Ende ist:
#include <stdio.h>
int main(void)
{
char s1[10] = "Hello";
printf("%s\n", s1);
}Ausgabe: Hello – soweit alles gut.
Jetzt fügen wir scanf hinzu:
#include <stdio.h>
int main(void)
{
char s1[10] = "Hello";
printf("s1 vorher: %s\n", s1);
scanf("%s", s1);
printf("s1 nachher: %s\n", s1);
}Wenn wir “123” eingeben:
s1 vorher: Hello
123
s1 nachher: 123Das sieht wie erwartet aus: scanf hat die drei Zeichen “123” eingelesen und noch einen NUL-Terminator (\0) dahinter in den Buffer s1 geschrieben. printf hört das Lesen dort auf.
Nach dem NUL-Zeichen steht in s1 natürlich noch das o von “Hello” im Speicher – scanf hat es nicht überschrieben. Diesen Teil des Strings “sieht” printf aber nicht, weil es beim ersten NUL-Zeichen ab der Startadresse des Strings zu lesen aufhört.
Aber was passiert, wenn wir mehr als 9 Zeichen eingeben?
s1 vorher: Hello
AAAAAAAAAAAAAAAAAAAAAAAAAA (26 A's eingegeben)
s1 nachher: AAAAAAAAAAAAAAAAAAAAAAAAAASieht harmlos aus, aber: Das Programm hat über die Grenzen des Arrays hinaus geschrieben! Wir hatten nur 10 Bytes allokiert (für maximal 9 Zeichen zzgl. NUL-Terminator) Die zusätzlichen Zeichen überschreiben andere Variablen auf dem Stack – oder noch schlimmer, die Rücksprungadresse.
Wenn wir Glück haben, können wir das Überschreiben “fremden” Speichers demonstrieren:
#include <stdio.h>
int main(void)
{
char s1[10] = "Hello";
int wichtige_variable = 42;
printf("Wichtig: %d\n", wichtige_variable);
scanf("%s", s1); // Wenn > 9 Zeichen eingegeben werden...
printf("Wichtig: %d\n", wichtige_variable);
}Führen Sie das Programm aus geben Sie 16 Zeichen ein und prüfen Sie, ob printf zwei Mal “42” ausgibt. Wenn beim zweiten printf etwas anderes als “42” ausgegeben wird, hat Ihre Eingabe den Inhalt von wichtige_variable überschrieben! Garantiert ist das nicht, da die Reihenfolge, in der lokale Variablen auf dem Stack liegen, vom Compiler festgelegt wird. Es ist nicht garantiert, dass wichtige_variable “hinter” s1 liegt und überschrieben werden kann, wenn man über s1 hinausschreibt.
Wenn Sie deutlich mehr Zeichen eingeben und von scanf in den Speicher schreiben lassen, etwa 50 Zeichen, wird Ihr Programm abstürzen. Warum?
Bonus: Überschriebene Rücksprungadresse (nicht klausurrelevant)
Der Stack enthält nicht nur lokale Variablen, sondern auch die Rücksprungadresse – die Stelle im Code, zu der das Programm nach Ende der Funktion zurückkehren soll:
┌─────────────────────────┐
│ s1[0] ... s1[9] │ ← unser Buffer (10 Bytes)
├─────────────────────────┤
│ wichtige_variable │ ← vielleicht hier, vielleicht auch vor s1
├─────────────────────────┤
│ Rücksprungadresse │ ← Problem, wenn die überschrieben wird!
├─────────────────────────┤
│ weiterer Stackframe │
└─────────────────────────┘Wenn wir genug Zeichen eingeben, überschreiben wir irgendwann die Rücksprungadresse. Dann “weiß” das Programm nicht mehr, wohin es zurückkehren soll, wenn die Funktion fertig ist:
- Bester Fall: Das Programm crasht mit “Segmentation Fault”
- Schlimmster Fall: Ein Angreifer überschreibt die Rücksprungadresse mit einer Adresse seiner Wahl und führt eigenen Code aus – eine sogenannte Buffer-Overflow-Attacke
Die sichere Alternative: fgets
#include <stdio.h>
int main(void)
{
char s1[10];
fgets(s1, 10, stdin); // Liest maximal 9 Zeichen + \0
printf("Eingabe: %s\n", s1);
}fgets nimmt die Buffergröße als Parameter und liest nie mehr Zeichen als angegeben.
Was ist stdin? Das ist ein vordefinierter FILE-Pointer (mehr dazu im nächsten Abschnitt), der die Standard-Eingabe repräsentiert – also das, was über die Tastatur (bzw. das Terminal) eingegeben wird. Analog gibt es stdout (Standard-Ausgabe, wohin printf schreibt) und stderr (Standard-Fehlerausgabe). Diese drei “Dateien” sind automatisch geöffnet, wenn ein C-Programm startet.
scanf("%d", &n): Sicher – liest genau einen Integerscanf("%s", buf): Kennt keine Größenbeschränkung – gefährlich!- Buffer Overflow: Schreiben über die Grenzen eines Arrays hinaus, kann Rücksprungadresse überschreiben
- Undefiniertes Verhalten: Programm kann crashen, kann “funktionieren”, kann Sicherheitslücken öffnen
fgets(buf, size, stdin): Sichere Alternative mit expliziter Größenangabe
Datei-Eingabe und -Ausgabe (File I/O)
Dateien sind eine wichtige Möglichkeit, Daten dauerhaft zu speichern. In C arbeiten wir mit FILE-Pointern.
Eine Datei zum Schreiben öffnen
#include <stdio.h>
int main(void)
{
FILE *file = fopen("output.txt", "w");
}fopen gibt einen Pointer auf eine FILE-Struktur zurück – oder NULL, wenn das Öffnen fehlschlägt:
#include <stdio.h>
int main(void)
{
FILE *file = fopen("output.txt", "w");
if (file == NULL)
{
printf("Fehler beim Öffnen!\n");
return 1;
}
}Alternative Schreibweise: Da NULL dem Wert 0 entspricht und 0 in C als false gilt, sind diese beiden Prüfungen äquivalent:
if (file == NULL) { ... } // Explizit
if (!file) { ... } // Kürzer, weil NULL = 0 = false
Die kürzere Variante sieht man in der Praxis häufig. Wichtig ist nur, dass man überhaupt prüft!
In eine Datei schreiben
Mit fprintf schreiben wir formatiert in eine Datei – genau wie printf, nur mit einem FILE* als erstem Argument:
#include <stdio.h>
int main(void)
{
FILE *file = fopen("phonebook.csv", "w");
if (file == NULL)
{
printf("Fehler beim Öffnen!\n");
return 1;
}
fprintf(file, "Name,Nummer\n");
fprintf(file, "Alice,0123456789\n");
fprintf(file, "Bob,9876543210\n");
fclose(file); // Datei schließen nicht vergessen!
}Nach der Ausführung enthält phonebook.csv:
Name,Nummer
Alice,0123456789
Bob,9876543210Dateimodi
| Modus | Bedeutung |
|---|---|
"r" | Lesen (Datei muss existieren) |
"w" | Schreiben (erstellt/überschreibt Datei) |
"a" | Anhängen (fügt am Ende hinzu) |
"rb" | Binär lesen |
"wb" | Binär schreiben |
Anmerkung zu Binärmodus: Auf Linux/macOS (und im CS50-Codespace) macht das b keinen Unterschied – Dateien werden immer byteweise gelesen/geschrieben. Auf Windows hingegen übersetzt der Textmodus Zeilenumbrüche (\n ↔ \r\n), was bei Binärdateien (Bilder, Audio, etc.) zu Problemen führt. Wenn Sie Programme schreiben, die auch auf Windows laufen sollen, sollte man bei Binärdateien immer "rb"/"wb" verwenden.
Aus einer Datei lesen
Für textbasiertes Lesen verwenden wir fgets. Zunächst lesen wir eine einzelne Zeile:
#include <stdio.h>
int main(void)
{
FILE *file = fopen("phonebook.csv", "r");
if (file == NULL)
{
printf("Datei nicht gefunden!\n");
return 1;
}
char line[100];
fgets(line, 100, file); // Liest eine Zeile (max. 99 Zeichen + \0)
printf("Erste Zeile: %s", line);
fclose(file);
}Wichtig: fgets liest bis zu n-1 Zeichen, aber hört früher auf, wenn ein Zeilenumbruch (\n) oder das Dateiende erreicht wird. Der Zeilenumbruch wird dabei mit in den Buffer übernommen! Das macht fgets ideal zum zeilenweisen Lesen von Textdateien oder Benutzereingaben von stdin.
Das funktioniert – aber was, wenn wir die ganze Datei lesen wollen? Wir könnten einen riesigen Buffer anlegen und hoffen, dass die Datei hineinpasst. Aber das ist unpraktisch: Wir wissen oft nicht im Voraus, wie groß die Datei ist, und ein Buffer von mehreren Megabytes wäre Speicherverschwendung.
Die Lösung: Wir lesen in einer Schleife, Zeile für Zeile. Dafür nutzen wir den Rückgabewert von fgets:
- Bei Erfolg gibt
fgetsden Pointer auf den Buffer zurück (alsoline) - Am Dateiende (oder bei Fehler) gibt
fgetsNULLzurück
Das passt perfekt zu einer while-Schleife:
#include <stdio.h>
int main(void)
{
FILE *file = fopen("phonebook.csv", "r");
if (file == NULL)
{
printf("Datei nicht gefunden!\n");
return 1;
}
char line[100];
while (fgets(line, 100, file) != NULL)
{
printf("Zeile: %s", line);
}
// Schleife endet automatisch, wenn fgets NULL zurückgibt (Dateiende)
fclose(file);
}Die Schleife läuft, solange fgets erfolgreich eine Zeile liest. Sobald das Dateiende erreicht ist, gibt fgets NULL zurück, die Bedingung ist falsch, und die Schleife endet.
Binäre Daten mit fread/fwrite
Für binäre Dateien (wie Bilder) verwenden wir fread und fwrite. Im Gegensatz zu fgets ignoriert fread Zeilenumbrüche komplett – es liest einfach die angegebene Anzahl Bytes, egal welche Werte diese haben.
Die Signatur von fread:
size_t fread(void *buffer, size_t size, size_t count, FILE *datei);
// wohin? wie groß wie viele? woher?
// ist ein Elemente
// Element?
Zunächst lesen wir einmalig 10 Bytes:
#include <stdio.h>
int main(void)
{
FILE *file = fopen("daten.bin", "rb");
if (file == NULL)
{
return 1;
}
char buffer[10];
int gelesen = fread(buffer, 1, 10, file); // 10 Elemente à 1 Byte
printf("Gelesen: %d Bytes\n", gelesen);
fclose(file);
}Auch hier stellt sich die Frage: Was, wenn die Datei größer ist als unser Buffer? Wir müssten die gesamte Dateigröße kennen und entsprechend viel Speicher reservieren – bei großen Dateien unpraktisch.
Die Lösung ist wieder eine Schleife. Der Rückgabewert von fread hilft uns dabei:
freadgibt die Anzahl der erfolgreich gelesenen Elemente zurück- Am Dateiende gibt es weniger zurück als angefordert (oder 0)
#include <stdio.h>
int main(void)
{
FILE *file = fopen("daten.bin", "rb");
if (file == NULL)
{
return 1;
}
char c;
while (fread(&c, 1, 1, file) == 1) // Solange genau 1 Byte gelesen wird
{
printf("%02x ", (unsigned char)c); // Byte als Hex ausgeben
}
// Schleife endet, wenn fread 0 zurückgibt (Dateiende)
fclose(file);
}Format-String %02x: Das x steht für Hexadezimal (Basis 16), die 2 bedeutet “mindestens 2 Stellen”, und die 0 davor bedeutet “mit führenden Nullen auffüllen”. So wird das Byte 0x0A als 0a ausgegeben statt nur a. Das ist praktisch, um Binärdaten übersichtlich darzustellen – jedes Byte nimmt genau 2 Zeichen ein.
Die Bedingung fread(...) == 1 prüft: “Wurde genau 1 Element gelesen?” Wenn das Dateiende erreicht ist, gibt fread 0 zurück, die Bedingung ist falsch, und die Schleife endet.
Für größere Blöcke (effizienter als Byte für Byte):
char buffer[512];
size_t gelesen;
while ((gelesen = fread(buffer, 1, 512, file)) > 0)
{
// gelesen enthält die tatsächliche Anzahl gelesener Bytes
// (kann beim letzten Block < 512 sein)
}Vorsicht: fread schreibt kein Null-Byte!
Ein häufiger Fehler beim Lesen von Textdateien mit fread: Wir wollen den gelesenen Inhalt mit printf("%s", buffer) ausgeben. Hier ein naiver Versuch:
#include <stdio.h>
int main(void)
{
FILE *file = fopen("phonebook.csv", "r");
if (file == NULL) return 1;
char buffer[10];
while (fread(buffer, 1, 9, file) > 0)
{
printf("%s\n", buffer); // PROBLEM!
}
fclose(file);
}Das Problem: Angenommen, die Datei enthält 25 Zeichen. Im ersten Durchlauf liest fread 9 Bytes in den Buffer. Im zweiten Durchlauf wieder 9 Bytes. Im dritten Durchlauf sind nur noch 7 Bytes übrig – fread liest diese 7 Bytes, aber die letzten 2 Bytes im Buffer enthalten noch den alten Inhalt vom vorherigen Durchlauf!
Noch schlimmer: fread schreibt kein \0 ans Ende. printf("%s", ...) liest also weiter, bis es zufällig auf ein Null-Byte trifft – möglicherweise über den Buffer hinaus (undefiniertes Verhalten!).
Die Lösung: Nach jedem fread manuell ein Null-Byte setzen. Aber dafür müssen wir wissen, wie viele Bytes gelesen wurden – wir brauchen also den Rückgabewert von fread innerhalb der Schleife.
Bisher haben wir in while(...) immer nur boolesche Ausdrücke gesehen, z.B. while (i < 10). Jetzt brauchen wir ein neues Muster: Zuweisung und Vergleich kombiniert:
while ((gelesen = fread(buffer, 1, 9, file)) > 0)Was passiert hier?
fread(...)wird aufgerufen und gibt die Anzahl gelesener Bytes zurück- Dieser Rückgabewert wird in
gelesengespeichert (Zuweisung mit=) - Dann wird
gelesen > 0geprüft (boolescher Vergleich) - Wenn
> 0, läuft die Schleife; wenn0, endet sie
Wichtig: Die doppelten Klammern um die Zuweisung! Ohne sie:
while (gelesen = fread(...) > 0) // FALSCH!
Hier würde zuerst fread(...) > 0 ausgewertet (ergibt true oder false, also 1 oder 0), und dann würde gelesen = 1 oder gelesen = 0 zugewiesen – nicht das, was wir wollen!
Außerdem gibt der Compiler ohne die Extra-Klammern eine Warnung aus: Er vermutet, dass wir versehentlich = statt == geschrieben haben. Die Klammern signalisieren: “Ja, ich meine wirklich eine Zuweisung.”
Kurzform ohne > 0: In vielen Programmen sieht man auch:
while ((gelesen = fread(buffer, 1, 9, file)))Das funktioniert, weil in C der Wert 0 als false gilt und jeder andere Wert als true. Wenn fread also 0 zurückgibt (Dateiende), ist die Bedingung falsch und die Schleife endet. Die explizite Variante mit > 0 ist aber lesbarer.
#include <stdio.h>
int main(void)
{
FILE *file = fopen("phonebook.csv", "r");
if (file == NULL) return 1;
char buffer[10];
int gelesen;
while ((gelesen = fread(buffer, 1, 9, file)) > 0)
{
buffer[gelesen] = '\0'; // Manuell terminieren!
printf("Gelesen: %d Bytes\n", gelesen);
printf("%s\n", buffer);
}
fclose(file);
}Jetzt wird nach jedem Lesevorgang das Null-Byte an die richtige Stelle gesetzt – auch wenn im letzten Durchlauf weniger gelesen wurde.
Merke:
fgetsfügt automatisch ein\0ein (deshalb für Textdateien oft praktischer)freadist “dumm” – es kopiert nur Bytes, ohne sich um Strings zu kümmern- Bei
freadmit Textausgabe: immer manuell null-terminieren!
Was ist eigentlich ein FILE*? (nicht klausurrelevant)
FILE ist kein eingebauter Datentyp wie int oder char – es ist ein mit typedef definierter Struct-Typ. Was steht da drin? Das ist absichtlich vor uns verborgen (opaque type), aber konzeptionell enthält diese Struktur Informationen, die das Betriebssystem braucht:
- Welche Datei ist geöffnet (eine Art Referenznummer)
- An welcher Position in der Datei befinden wir uns gerade
- Puffer für effizienteres Lesen/Schreiben
- Fehlerstatus
Wenn wir fopen aufrufen, passiert im Betriebssystem einiges: Es prüft, ob die Datei existiert, ob wir Zugriffsrechte haben, und merkt sich in einer internen Tabelle, dass unser Programm diese Datei geöffnet hat. Der FILE*-Pointer, den wir zurückbekommen, ist unser “Ticket” für alle weiteren Operationen mit dieser Datei.
Ein Gedankenexperiment: Wir könnten theoretisch den Speicher, auf den FILE* zeigt, Byte für Byte mit einem char* auslesen – schließlich ist alles im Speicher nur eine Folge von Bytes:
// SINNLOS, ABER MÖGLICH: FILE-Struktur byteweise auslesen
#include <stdio.h>
int main(void)
{
FILE *file = fopen("test.txt", "w");
if (file == NULL) return 1;
// Hässlicher Cast: FILE* → char*
char *raw = (char *)file;
// sizeof(FILE) gibt die Größe der Struktur in Bytes
printf("FILE struct ist %lu Bytes groß\n", sizeof(FILE));
// Jeden Byte der FILE-Struktur als Hex ausgeben
for (size_t i = 0; i < sizeof(FILE); i++)
{
printf("%02x ", (unsigned char)raw[i]);
if ((i + 1) % 16 == 0) printf("\n");
}
fclose(file);
}Ausgabe (systemabhängig, bei mir ~216 Bytes Datenmüll):
FILE struct ist 216 Bytes groß
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
...Das funktioniert – aber was sehen wir? Nur Bytes ohne erkennbare Bedeutung. Die interne Struktur ist systemabhängig, und wir sollen sie nicht direkt manipulieren. Stattdessen nutzen wir die bereitgestellten Funktionen (fread, fwrite, fclose, …), die wissen, wie man mit dieser Struktur umgeht.
FILE *: Pointer auf eine Datei-Struktur (vontypedefdefiniert)fopen(name, mode): Öffnet Datei, gibtNULLbei Fehler zurückfprintf(file, format, ...): Wieprintf, aber in eine Dateifread(buf, size, count, file): Liest binäre Datenfwrite(buf, size, count, file): Schreibt binäre Datenfclose(file): Schließt die Datei – immer aufrufen!
Bonus: BMP-Bild generieren
Mit unserem Wissen über File I/O können wir ein BMP-Bild komplett selbst erstellen – generative Computerkunst!
Hinweis: Das genaue BMP-Datenformat (Header-Struktur, Byte-Reihenfolge, Padding) ist nicht klausurrelevant. Es geht darum zu sehen, wie die File-I/O-Funktionen (fopen, fwrite, fclose) zusammenwirken, um eine echte Binärdatei zu erzeugen. Der Code enthält außerdem Funktionspointer – ein fortgeschrittenes Konzept, das wir hier nur kurz streifen (ebenfalls nicht klausurrelevant).
Spielen erlaubt! Ändern Sie die aufgerufene Muster-Funktion am Beginn der main-Funktion, schreiben Sie Ihre eigene Muster-Funkton und experimentieren Sie mit Farben und Formeln. Öffnen Sie die erzeugte kunst.bmp-Datei nach jedem Aufruf in einem Bildbetrachter oder einfach in VS Code durch Doppelklick oder Eingabe von code kunst.bmp und schauen Sie, was passiert!
Das folgende Programm kunst.c erzeugt ein 256×256 Pixel großes Bild mit verschiedenen Mustern:
#include <stdio.h>
#include <math.h> // Für sin(), cos()
// Kompilieren mit "make kunst" oder
// "clang -o kunst kunst.c -lm" ("m" ist die Math-Library)
// Bildgröße (globale Variable, damit alle Funktionen darauf zugreifen können)
int WIDTH = 256;
int HEIGHT = 256;
// ============================================================
// MUSTER-FUNKTIONEN
// Jede Funktion berechnet RGB für Position (x, y)
// Parameter: x, y, und Pointer auf r, g, b
// ============================================================
// Muster 1: Farbverlauf
void muster_farbverlauf(int x, int y, unsigned char *r, unsigned char *g, unsigned char *b)
{
*r = x; // Rot nimmt nach rechts zu
*g = y; // Grün nimmt nach oben zu
*b = 128; // Blau konstant
}
// Muster 2: Schachbrett
void muster_schachbrett(int x, int y, unsigned char *r, unsigned char *g, unsigned char *b)
{
int feldgroesse = 32;
if ((x / feldgroesse + y / feldgroesse) % 2 == 0)
{
*r = 255; *g = 255; *b = 255; // Weiß
}
else
{
*r = 0; *g = 0; *b = 0; // Schwarz
}
}
// Muster 3: XOR-Fraktal (Sierpinski-Dreieck!)
void muster_xor_fraktal(int x, int y, unsigned char *r, unsigned char *g, unsigned char *b)
{
int xor_wert = x ^ y;
*r = xor_wert;
*g = xor_wert * 2;
*b = 255 - xor_wert;
}
// Muster 4: Konzentrische Kreise
void muster_kreise(int x, int y, unsigned char *r, unsigned char *g, unsigned char *b)
{
int dx = x - WIDTH / 2;
int dy = y - HEIGHT / 2;
int distanz = dx * dx + dy * dy; // Quadrat der Entfernung vom Zentrum
*r = (distanz / 100) % 256;
*g = (distanz / 50) % 256;
*b = (distanz / 25) % 256;
}
// Muster 5: Diagonale Streifen
void muster_streifen(int x, int y, unsigned char *r, unsigned char *g, unsigned char *b)
{
int streifen = (x + y) / 16;
if (streifen % 2 == 0)
{
*r = 255; *g = 100; *b = 50; // Orange
}
else
{
*r = 50; *g = 100; *b = 255; // Blau
}
}
// Muster 6: Plasma-Effekt (vereinfacht)
void muster_plasma(int x, int y, unsigned char *r, unsigned char *g, unsigned char *b)
{
// Einfache Wellen-Kombination
int wert1 = (x * x + y * y) / 100;
int wert2 = x * y / 64;
int wert3 = (x - y) * (x - y) / 100;
*r = (wert1 + wert2) % 256;
*g = (wert2 + wert3) % 256;
*b = (wert1 + wert3) % 256;
}
// Muster 7: Multiplikationsmuster
void muster_multiplikation(int x, int y, unsigned char *r, unsigned char *g, unsigned char *b)
{
*r = (x * y) % 256;
*g = (x * y / 2) % 256;
*b = (x * y / 4) % 256;
}
// Muster 8: Raster mit Farbverlauf
void muster_raster(int x, int y, unsigned char *r, unsigned char *g, unsigned char *b)
{
// Dünne Linien alle 16 Pixel
if (x % 16 == 0 || y % 16 == 0)
{
*r = 255; *g = 255; *b = 255; // Weiße Linien
}
else
{
*r = x; *g = 0; *b = y; // Farbverlauf dazwischen
}
}
// Muster 9: Wellen mit sin/cos
void muster_wellen(int x, int y, unsigned char *r, unsigned char *g, unsigned char *b)
{
// sin() erwartet Radiant, gibt Werte von -1 bis 1 zurück
// Wir skalieren auf 0-255
double welle1 = sin(x / 16.0) * 127 + 128;
double welle2 = cos(y / 16.0) * 127 + 128;
double welle3 = sin((x + y) / 20.0) * 127 + 128;
*r = (unsigned char) welle1;
*g = (unsigned char) welle2;
*b = (unsigned char) welle3;
}
// Muster 10: Interferenz (überlappende Wellen)
void muster_interferenz(int x, int y, unsigned char *r, unsigned char *g, unsigned char *b)
{
// Zwei Wellenzentren
double dx1 = x - WIDTH / 4;
double dy1 = y - HEIGHT / 4;
double dist1 = sqrt(dx1 * dx1 + dy1 * dy1);
double dx2 = x - 3 * WIDTH / 4;
double dy2 = y - 3 * HEIGHT / 4;
double dist2 = sqrt(dx2 * dx2 + dy2 * dy2);
// Überlagerung der Wellen
double wert = sin(dist1 / 8.0) + sin(dist2 / 8.0);
unsigned char farbe = (unsigned char)((wert + 2) * 63); // Skalieren auf 0-255
*r = farbe;
*g = 255 - farbe;
*b = 128;
}
// Muster 11: Spirale
void muster_spirale(int x, int y, unsigned char *r, unsigned char *g, unsigned char *b)
{
double dx = x - WIDTH / 2;
double dy = y - HEIGHT / 2;
double winkel = atan2(dy, dx); // Winkel in Radiant (-π bis π)
double distanz = sqrt(dx * dx + dy * dy);
// Spirale: Farbe hängt von Winkel + Distanz ab
double wert = sin(winkel * 5 + distanz / 10.0) * 127 + 128;
*r = (unsigned char) wert;
*g = (unsigned char) (255 - wert);
*b = (unsigned char) ((winkel + 3.14159) / 6.28318 * 255);
}
// ============================================================
// EIGENES MUSTER HIER EINFÜGEN!
// ============================================================
void muster_eigenes(int x, int y, unsigned char *r, unsigned char *g, unsigned char *b)
{
// TODO: Eigene Formel hier!
*r = 128;
*g = 128;
*b = 128;
}
// ============================================================
// FUNKTIONSPOINTER-ARRAY
// Pointer können auch auf Funktionen zeigen!
// ============================================================
// Typ-Definition: Ein Pointer auf eine Muster-Funktion
typedef void (*MusterFunktion)(int, int, unsigned char*, unsigned char*, unsigned char*);
// Array aller Muster-Funktionen (Index = Muster-Nummer)
MusterFunktion alle_muster[] = {
muster_eigenes, // 0
muster_farbverlauf, // 1
muster_schachbrett, // 2
muster_xor_fraktal, // 3
muster_kreise, // 4
muster_streifen, // 5
muster_plasma, // 6
muster_multiplikation,// 7
muster_raster, // 8
muster_wellen, // 9
muster_interferenz, // 10
muster_spirale // 11
};
int ANZAHL_MUSTER = 12;
// ============================================================
// HAUPTPROGRAMM
// ============================================================
int main(void)
{
// *** HIER MUSTER AUSWÄHLEN (0-11) ***
int muster = 1;
// Sicherheitscheck
if (muster < 0 || muster >= ANZAHL_MUSTER)
{
muster = 0;
}
// Datei öffnen
FILE *file = fopen("kunst.bmp", "wb");
if (file == NULL)
{
printf("Fehler beim Öffnen!\n");
return 1;
}
// === BMP Header schreiben (54 Bytes) ===
fprintf(file, "BM");
int filesize = 54 + WIDTH * HEIGHT * 3;
fwrite(&filesize, 4, 1, file);
int zero = 0;
fwrite(&zero, 4, 1, file);
int offset = 54;
fwrite(&offset, 4, 1, file);
int headersize = 40;
fwrite(&headersize, 4, 1, file);
fwrite(&WIDTH, 4, 1, file);
fwrite(&HEIGHT, 4, 1, file);
short planes = 1;
fwrite(&planes, 2, 1, file);
short bitcount = 24;
fwrite(&bitcount, 2, 1, file);
for (int i = 0; i < 6; i++)
{
fwrite(&zero, 4, 1, file);
}
// === Pixel schreiben ===
for (int y = 0; y < HEIGHT; y++)
{
for (int x = 0; x < WIDTH; x++)
{
unsigned char r, g, b;
// Muster-Funktion über Array aufrufen - eine Zeile!
alle_muster[muster](x, y, &r, &g, &b);
// BMP speichert BGR (nicht RGB!)
fputc(b, file);
fputc(g, file);
fputc(r, file);
}
}
fclose(file);
printf("kunst.bmp mit Muster %i erstellt!\n", muster);
return 0;
}Was sind Funktionspointer? (nicht klausurrelevant)
Im Code oben taucht eine ungewöhnliche Zeile auf:
typedef void (*MusterFunktion)(int, int, unsigned char*, unsigned char*, unsigned char*);Das definiert einen neuen Typ namens MusterFunktion. Eine Variable dieses Typs kann die Adresse einer Funktion speichern – genau wie ein int* die Adresse eines int speichert.
Warum ist das nützlich? Wir können ein Array von Funktionen anlegen:
MusterFunktion alle_muster[] = { muster_farbverlauf, muster_schachbrett, ... };Und dann eine Funktion über ihren Index aufrufen:
alle_muster[muster](x, y, &r, &g, &b); // Ruft die muster-te Funktion auf
So können wir zur Laufzeit entscheiden, welche Funktion ausgeführt wird – ohne lange if-else- oder switch-Ketten. Das ist ein mächtiges Konzept, das wir in Inf-Einf-B aber nicht mehr benötigen.
So fügen Sie ein eigenes Muster hinzu:
- Kopieren Sie eine bestehende Muster-Funktion
- Benennen Sie sie um (z.B.
muster_meinname) - Ändern Sie die Berechnung von
*r,*g,*b - Fügen Sie den Funktionsnamen zum Array
alle_muster[]hinzu - Erhöhen Sie
ANZAHL_MUSTERum 1 - Kompilieren und testen!
Ideen zum Experimentieren:
*r = (x * x + y * y) / 200— Radiale Helligkeit*g = abs(x - y)— Diagonale (braucht#include <stdlib.h>)*b = (int)(sin(x / 10.0) * sin(y / 10.0) * 127) + 128— Gitter aus Wellen- Kombinieren Sie mehrere Muster!
Zusammenfassung
In diesen zwei Vorlesungen haben Sie etwas über Pointer gelernt, mit denen Sie auf Daten an bestimmten Speicherplätzen zugreifen und diese manipulieren können. Behandelt haben wir insbesondere…
Woche 1:
- Pixelbilder und RGB
- Hexadezimalzahlen
- Speicheradressen
- Pointer-Grundlagen (
&und*) - Strings als
char* - Pointer-Arithmetik
- String-Vergleiche (
strcmp) - malloc und free (dynamische Speicherverwaltung)
- strcpy (Strings kopieren)
- Stack vs. Heap
- Valgrind (Speicherfehler finden)
- Garbage-Werte
- scanf
- Datei-E/A (fopen, fprintf, fread, fwrite, fclose)
Bis zum nächsten Mal!