← Startseite
🧩

Vorlesung 8

Funktionen
Code modular gestalten — eigene Funktionen schreiben

Grundlagen der Programmierung
Prof. Dr. Alexandra Mikityuk
HTW Berlin

Funktionen Parameter return

Lernziele

  • Verstehen, warum man eigene Funktionen schreibt
  • Die vier Teile einer Funktion kennen: Typ, Name, Parameter, Body
  • Funktionen mit und ohne Parameter schreiben
  • Funktionen mit Rückgabewert schreiben — und wann void reicht
  • Verstehen, was (void) bei Parametern bedeutet
  • Prototypen nutzen — und wissen, wo welche Funktion in der Datei steht

Warum eigene Funktionen?

Stellt euch ein Programm vor, das dreimal einen Notendurchschnitt rechnet — für drei verschiedene Klassen.

😤 Ohne Funktion

// Klasse A — 8 Zeilen
summe = 0;
for (i=0; i<n; i++) summe += a[i];
schnitt = summe / n;
...

// Klasse B — 8 Zeilen kopiert
// Klasse C — 8 Zeilen kopiert

3× kopiert → 3× Bug-Risiko. Beim Korrigieren: alle 3 anpassen.

Mit Funktion

// Einmal definiert:
double durchschnitt(int a[], int n) {
    // ... Logik ...
}

// Dreimal aufgerufen:
schnittA = durchschnitt(klasseA, 30);
schnittB = durchschnitt(klasseB, 28);
Drei Gründe für Funktionen: Wiederverwendung (1× schreiben, n× nutzen) · Klarheit (Code liest sich wie ein Inhaltsverzeichnis) · Testen (eine Funktion isoliert prüfen statt 500 Zeilen).

Was ist eine Funktion eigentlich?

Eine Mini-Maschine mit klarer Aufgabe — Input rein, etwas passiert (oder ein Wert kommt raus).

Eingabe → FUNKTION → Ausgabe

📐 Mathematik

f(x) = x²

Eingabe x = 3 → Ausgabe 9.

💻 C

int quadrat(int x) {
    return x * x;
}

quadrat(3) → 9.

Übrigens: main und printf sind auch Funktionen — die habt ihr schon die ganze Zeit benutzt. Heute lernt ihr, eure eigenen zu bauen.

Die vier Teile einer Funktion

int  addiere  (int a, int b)  {
 ↑           ↑          ↑                 
  Typ   Name   Parameter
    return a + b;
}                                ← ④ Body

Rückgabetyp

Was die Funktion zurückgibt: int, double, char — oder void für „nichts".

Name

Wie die Funktion aufgerufen wird. Verb ist gut: berechne_, drucke_.

Parameter

Eingaben: Liste von Typ + Name, durch Kommas getrennt.

④ Body: der Code zwischen den geschweiften Klammern. Hier passiert die Arbeit.

Erste Funktion — ganz einfach

// Definition
void begruessung(void) {
    printf("Hallo Welt!\n");
    printf("Willkommen!\n");
}

// Aufruf
int main(void) {
    begruessung();        // Klammern, kein Argument
    begruessung();        // nochmal — gleicher Code, kein Copy-Paste
    return 0;
}
Was bedeutet void in dieser Zeile?
  • Das erste void = „gibt nichts zurück"
  • Das zweite (void) = „erwartet nichts"
Die Funktion macht einfach was (druckt) — kein Rein, kein Raus.

Moment — warum (void) und nicht einfach ()?

In C sind int main() und int main(void) nicht das Gleiche!

😕 int main()

Der Compiler liest: „die Parameter-Liste ist nicht spezifiziert".

Er weiß nicht, ob die Funktion 0 oder 10 Parameter hat — und prüft beim Aufruf gar nicht.

int main(void)

Der Compiler liest: „diese Funktion nimmt KEINE Parameter entgegen".

Klare Aussage. Wenn jemand trotzdem Argumente übergibt → Compiler-Fehler.

Der Unterschied in Aktion:
void hallo();          // Prototyp ohne (void)
void hallo_v(void);    // Prototyp mit (void)

hallo(42);              // ✗ Compiler sagt NICHTS — gefährlich!
hallo_v(42);            // ✓ Compiler-Fehler: zu viele Argumente

(void) ist die saubere Form

Alle Funktionen ohne Parameter werden konsequent mit (void) geschrieben.

// ✓ alle drei Funktionen: kein Parameter → (void)

void trennlinie(void) {
    printf("-------------------\n");
}

int lies_zahl(void) {
    int n;
    scanf("%d", &n);
    return n;
}

int main(void) {     // auch main bekommt (void)
    trennlinie();
    int x = lies_zahl();
    printf("Du gabst %d ein\n", x);
    return 0;
}
Faustregel: kein Parameter → immer (void). So denken auch professionelle C-Codebases (Linux-Kernel, glibc) — das ist nicht Geschmackssache.

Funktion mit einem Parameter

Parameter sind Eingabe-Variablen — sie machen die Funktion flexibel.

// Begrüßung — jetzt mit Name als Parameter
void begruesse(char name[]) {
    printf("Hallo, %s!\n", name);
}

int main(void) {
    begruesse("Anna");     // → Hallo, Anna!
    begruesse("Bob");      // → Hallo, Bob!
    begruesse("Carla");    // → Hallo, Carla!
    return 0;
}
Parameter vs. Argument:
  • Parameter = die Variable in der Funktions-Definition (char name[])
  • Argument = der konkrete Wert beim Aufruf ("Anna")

Mehrere Parameter

void drucke_rechteck(int breite, int hoehe) {
    for (int z = 0; z < hoehe; z++) {
        for (int s = 0; s < breite; s++) {
            printf("*");
        }
        printf("\n");
    }
}

int main(void) {
    drucke_rechteck(5, 3);   // 5 breit, 3 hoch
    printf("---\n");
    drucke_rechteck(8, 2);
    return 0;
}
Reihenfolge zählt! drucke_rechteck(5, 3) heißt breite=5, hoehe=3 — nicht umgekehrt. C ordnet die Argumente positionell zu.

Beispiel — ein Sterne-Dreieck malen

void drucke_dreieck(int hoehe) {
    for (int i = 1; i <= hoehe; i++) {
        for (int j = 0; j < i; j++) {
            printf("* ");
        }
        printf("\n");
    }
}

int main(void) {
    drucke_dreieck(5);
    return 0;
}
Ausgabe:
*
* *
* * *
* * * *
* * * * *
Was hat sich geändert? Eine Schleife von VL 6, jetzt eingepackt in eine Funktion. Wer drucke_dreieck(10) ruft, bekommt ein größeres Dreieck — derselbe Code.

Rückgabewert — return

Funktionen können etwas zurückgeben — wie eine Mathe-Funktion: Input rein, Wert raus.

// Rückgabetyp ist int — die Funktion liefert eine Ganzzahl
int quadrat(int x) {
    return x * x;          // gibt den Wert zurück
}

int main(void) {
    int ergebnis = quadrat(5);    // ergebnis ist jetzt 25
    printf("5 hoch 2 = %d\n", ergebnis);

    printf("7 hoch 2 = %d\n", quadrat(7));  // direkt in printf
    return 0;
}
Wichtig: return beendet die Funktion sofort. Was nach return kommt, läuft nicht mehr.

Drei Mini-Funktionen mit Rückgabewert

// Doppelt so groß
int doppelt(int x) {
    return x * 2;
}

// Maximum von zwei Zahlen
int max(int a, int b) {
    if (a > b) return a;
    return b;
}

// Celsius zu Fahrenheit
double celsius_zu_f(double c) {
    return c * 9.0 / 5.0 + 32.0;
}

int main(void) {
    printf("Doppelt von 7: %d\n", doppelt(7));         // → 14
    printf("Max(8, 3):     %d\n", max(8, 3));            // → 8
    printf("20°C in F:     %.1f\n", celsius_zu_f(20));   // → 68.0
    return 0;
}
Beobachtung: jede Funktion hat eine Aufgabe. Das ist die Single Responsibility-Regel — eine Funktion, eine Sache.

void vs. Rückgabetyp — wann was?

📤 void — tut etwas

Ausgabe machen, Stern drucken, etwas zeichnen.

void drucke_zeile(void) {
    printf("---\n");
}

Kein return nötig.

📥 Rückgabetyp — berechnet etwas

Einen Wert berechnen, den jemand anderes weiterverarbeitet.

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

return mit Wert ist Pflicht.

Faustregel: braucht der Aufrufer einen Wert? → Rückgabetyp. Soll die Funktion einen Effekt haben (drucken)? → void.

Wie funktioniert ein Funktionsaufruf?

Bei jedem Aufruf bekommt die Funktion ihre eigenen lokalen Variablen in einem neuen „Stockwerk" — dem Stack-Frame.

int quadrat(int x) {
    return x * x;
}

int main(void) {
    int a = 5;
    int b = quadrat(a);
                          // (1) a wird gelesen, in x kopiert
                          // (2) quadrat-Frame: x=5, rechnet 25
                          // (3) return 25 → b wird 25
                          // (4) quadrat-Frame wird weggeräumt
}
Bildlich: main ruft quadrat — und wartet, bis quadrat fertig ist. Dann macht main mit dem Ergebnis weiter.

Parameter sind Kopien — Pass by Value

Wenn die Funktion einen Parameter ändert, ändert sich der Original-Wert außerhalb nicht.

void verdopple_versuch(int x) {
    x = x * 2;          // ändert nur die Kopie x
    printf("In Funktion: x = %d\n", x);
}

int main(void) {
    int a = 5;
    verdopple_versuch(a);    // Ausgabe: In Funktion: x = 10
    printf("Nach Aufruf: a = %d\n", a);
                              // Ausgabe: Nach Aufruf: a = 5  ← unverändert!
    return 0;
}
Warum 5 und nicht 10? Beim Aufruf wird der Wert von a in x kopiert. x ist eine eigene Variable in der Funktion. a bleibt unangetastet.
Konsequenz: wer ein Ergebnis nach draußen will → return nutzen, nicht den Parameter ändern.

Lokale Variablen — was in der Funktion bleibt

Variablen innerhalb der Funktion existieren nur dort.

int summe_bis(int n) {
    int summe = 0;        // lokal! nur hier sichtbar
    for (int i = 1; i <= n; i++) {
        summe += i;
    }
    return summe;
}

int main(void) {
    printf("%d\n", summe_bis(10));   // → 55
    // printf("%d\n", summe);   ← FEHLER! summe ist nur in summe_bis sichtbar
}
Warum ist das gut? Jede Funktion hat ihre eigene kleine Welt. Eine andere Funktion könnte auch eine Variable summe haben — die zwei stören sich nicht. Das macht Programme stabil.

Funktionsprototypen — dem Compiler Bescheid sagen

C liest die Datei von oben nach unten. Wenn main oben eine Funktion ruft, die erst unten definiert ist → Fehler.

😱 Ohne Prototyp

int main(void) {
    printf("%d", quadrat(3));
    // ✗ Compiler: kenne ich nicht!
}

int quadrat(int x) {
    return x * x;
}

Mit Prototyp

// Prototyp — Signatur ohne Body
int quadrat(int x);

int main(void) {
    printf("%d", quadrat(3));  // ✓
}

int quadrat(int x) {
    return x * x;
}
Prototyp = Versprechen. „Die Funktion existiert mit dieser Signatur — der Body kommt später." Compiler ist zufrieden.

Wo platziere ich Funktionen im Code?

Die typische Datei-Struktur:

#include <stdio.h>

// 1. Prototypen ganz oben
int quadrat(int x);
int max(int a, int b);

// 2. main als zweites — das Leit-Programm
int main(void) {
    printf("%d\n", quadrat(5));
    printf("%d\n", max(3, 7));
    return 0;
}

// 3. Funktions-Definitionen unten
int quadrat(int x) { return x * x; }
int max(int a, int b) { return a > b ? a : b; }
Wie ein Buch: Inhaltsverzeichnis oben (Prototypen) · der Plot in der Mitte (main) · Details unten (Definitionen).

Beispiel — Funktionen arbeiten zusammen

#include <stdio.h>

// Prototypen
int  lies_zahl(void);
int  ist_gerade(int n);
void drucke_ergebnis(int n, int gerade);

int main(void) {
    int n = lies_zahl();
    int g = ist_gerade(n);
    drucke_ergebnis(n, g);
    return 0;
}

int lies_zahl(void) {
    int x;
    printf("Gib eine Zahl: ");
    scanf("%d", &x);
    return x;
}

int ist_gerade(int n) {
    return n % 2 == 0;     // 1 wenn gerade, 0 wenn ungerade
}

void drucke_ergebnis(int n, int gerade) {
    if (gerade) printf("%d ist gerade\n", n);
    else        printf("%d ist ungerade\n", n);
}
Beobachtung: main ist 5 Zeilen — liest wie ein Inhaltsverzeichnis. Die Details verstecken sich in den drei Hilfsfunktionen. Genau so soll guter Code aussehen.

Best Practice — gute Funktionsnamen

Gut

  • berechne_durchschnitt
  • drucke_dreieck
  • ist_primzahl
  • lies_zahl

Der Name sagt sofort, was die Funktion tut.

Schlecht

  • do_it
  • f1, test, temp
  • process_stuff
  • go

Drei Wochen später versteht niemand mehr, was gemeint war — du selbst auch nicht.

Faustregel: Name = Verb + Substantiv (was tut sie? mit was?). drucke_dreieck ist klar — dreieck oder d nicht.

Best Practice — eine Funktion, eine Aufgabe

Klein & klar

  • Eine Bildschirmseite, höchstens
  • Macht eine Sache
  • Wenig Parameter (3-4 ist viel)
  • Klarer Name passt zur Aufgabe

Groß & chaotisch

  • 200 Zeilen Code
  • Macht 10 Dinge — „und" im Namen
  • 15 Parameter
  • Kommentare wie „TODO: aufräumen"
Faustregel: wenn du eine Funktion in einem Satz beschreiben kannst — gut. Wenn du „und" brauchst („liest Eingabe und rechnet und druckt") — wahrscheinlich zwei Funktionen.

⚠️ Häufige Fehler

1️⃣ Prototyp vergessen

Funktion oben aufgerufen, unten definiert, kein Prototyp → Compiler-Warnung oder -Fehler.

2️⃣ return vergessen

Funktion mit Rückgabetyp ohne return → undefiniertes Verhalten.

3️⃣ Falsche Reihenfolge der Argumente

drucke_rechteck(3, 5) statt (5, 3) → Ergebnis falsch.

4️⃣ Lokale Variable außerhalb

In main auf eine Variable zugreifen wollen, die nur in einer anderen Funktion existiert → Fehler.

5️⃣ Parameter ändern wollen

Erwarten, dass die Änderung in der Funktion außen sichtbar wird → ist sie nicht (Pass by Value).

6️⃣ () statt (void)

Beim Prototyp leere Klammern statt (void) → Compiler prüft nicht.

🤔 Mini-Quiz

Was gibt der folgende Code aus?

void plus_eins(int x) {
    x = x + 1;
}

int main(void) {
    int a = 10;
    plus_eins(a);
    printf("%d\n", a);
    return 0;
}
A) 11
B) 10
C) 0
D) Compiler-Fehler
Warum 10? Parameter sind Kopien. plus_eins erhöht seine eigene Kopie x auf 11 — aber a in main bleibt 10. Wer wirklich erhöhen will → Rückgabewert nutzen.

Zusammenfassung

  • Funktionen kapseln Logik — 1× schreiben, n× nutzen. Vier Teile: Typ · Name · Parameter · Body.
  • void = „nichts zurück" (Effekt). Sonst Rückgabetyp + return.
  • (void) als Parameterliste = „keine Parameter" — sauberer als leere ().
  • Parameter sind Kopien (Pass by Value) — Änderungen bleiben in der Funktion.
  • Prototypen oben in der Datei, main in der Mitte, Definitionen unten.
  • Funktionen klein und fokussiert halten — ein Verb + Substantiv im Namen.

Vielen Dank!

Fragen?

Prof. Dr. Alexandra Mikityuk

HTW Berlin · Büro Raum 308

Tel +49 30 5019-2664

Nächste Woche: switch/case + Algorithmen I

1 / …