Skip to main content Link Menu Expand (external link) Document Search Copy Copied

Funktionen

Syntax

Die Syntax für die Deklaration von C-Funktionen ist identisch zu der von Methoden in Java – kein Wunder, da Java sich die Syntax hier einfach bei C abgeschaut hat.

  • Syntax ist identisch zur Methodendeklaration in Java
Syntax
RETURN_TYPE NAME(TYPE PARAM, TYPE PARAM, ...) {
    RUMPF
}
Beispiel
int add(int a, int b) {
    return a + b;
}

Hat die Funktion keinen Rückgabewert, so ist der Rückgabetyp void

Was in C natürlich fehlt ist die Angabe einer Sichtbarkeit mit public, protected und private, wie wir sie aus Java kennen. Mit dem Schlüsselwort static (siehe unten) kann man die Sichtbarkeit einer Funktion auf die aktuelle Quelltextdatei beschränken.

Deklaration vs. Definition

Java kennt keine strenge Unterscheidung zwischen der Deklaration und Definition einer Funktion. In C sind diese beiden Begriffe aber streng auseinanderzuhalten. Eine Deklaration macht die Funktion nur bekannt und kann beliebig oft erfolgen, eine Definition liefert hingegen ihren Funktionsrumpf – also ihre Implementierung – und darf nur einmal in einem Programm vorkommen.

  • Eine (Funktions-) Deklaration (aka Prototyp) macht dem Compiler bekannt
    • den Namen der Funktion
    • den Rückgabetyp
    • die Parameter
  • Eine (Funktions-) Definition liefert die Implementierung einer Funktion
  • Eine Funktion kann
    • beliebig oft deklariert
    • nur einmal definiert werden

Diese Unterscheidung wurde schon früher in der Vorlesung diskutiert.

Anders als in Java müssen in C die Funktionen vor ihrer Benutzung deklariert sein das heißt der Compiler schaut nicht weiter unten im Quelltext, ob dort noch irgendwo die Funktion vorkommt. Eine Definition liefert auch gleichzeitig eine Deklaration der Funktion.

  • Die Deklaration muss vor der Benutzung erfolgen
    • ist die Funktion vor der Benutzung definiert, muss sie nicht mehr deklariert werden
    • Deklarationen finden sich meist in .h-Dateien
    • Definitionen finden sich meist in .c-Dateien
Deklaration
int add(int a, int b);
Definition
int add(int a, int b) {
    return a + b;
}

Im folgenden Beispiel wird die add-Funktion von calc() bereits vor deren Definition verwendet. Deswegen muss eine Deklaration erfolgen. Andernfalls käme es zu einem Compiler-Fehler.

Beispiel: Deklaration vs. Definition
/* Deklaration */
int add(int a, int b);

/* Benutzung */
void calc() {
    int result;
    result = add(5, 7);
}

/* Definition nach der Benutzung */
int add(int a, int b) {
    return a + b;
}

Da die Definition einer Funktion auch gleichzeitig deren Deklaration beinhaltet, muss in diesem Beispiel keine Deklaration mehr erfolgen: add ist vor der Benutzung bereits definiert und damit deklariert.

Beispiel: Deklaration vs. Definition
/* Definition vor der Benutzung, keine Deklaration nötig */
int add(int a, int b) {
    return a + b;
}

/* Benutzung */
void calc() {
    int result;
    result = add(5, 7);
}

Aufteilung .c und .h-Datei

Wie bereits diskutiert, ist es in C üblich die Deklaration und Definition auf verschiedene Dateien aufzuteilen. Die Deklarationen findet man üblicherweise in den .h-Dateien (Header-Files) die Definitionen in den dazugehörigen .c-Dateien.

Wenn eine Funktion nur innerhalb einer Quelltextdatei benötigt wird und diese auch nicht an andere Teile des Programms exportiert werden soll, nimmt man sie nicht in die Header-Datei auf.

  • Deklarationen finden sich in .h-Dateien, Definitionen in .c-Dateien
  • Eine .c-Datei sollte ihre korrespondierende .h-Datei selbst inkludieren (verhindert Abweichungen Definition ↔ Deklaration)
greeter.h
#ifndef GREETER_H
#define GREETER_H
/*
 * Enum für die verschiedenen Tageszeiten.
 */
typedef enum { MORGEN, ABEND } TagesZeit;

/*
 * Grüßt die als n übergebene Person passend zur Tageszeit z.
 */
void greet(TagesZeit z, char* n);
#endif

Die Headerdatei enthält die Deklarationen der Funktion greet() und einer von dieser Funktion verwendeten Enumeration TagesZeit. Damit stellt sie alle Informationen zur Verfügung, die ein Nutzer der Funktion benötigt.

greeter.c
#include "greeter.h"
#include <stdio.h>

void greet(TagesZeit zeit, char* name) {
    printf("%s %s", zeit == MORGEN ? "Guten Morgen" : "Guten Abend", name);
}
user.c
#include "greeter.h"
int main() {
    greet(ABEND, "Thomas");
}

In der dazugehörigen .c-Datei finden sich die Implementierung der Funktion (ihre Definition). Der Verwender der Funktionalität user.c inkludiert die Header-Datei und kennt somit die Deklaration der Funktion greet(). Deren Definition ist unwichtig und wird vom Linker später hinzugefügt.

Pass-by-Value

Beim Aufruf einer Funktion in C werden die Parameter als Pass-By-Value übergeben. Damit entspricht das Verhalten von C exakt dem von Java: Alle Werte werden beim Funktionsaufruf kopiert. Möchte man stattdessen die Inhalte der Variablen in der Funktion verändern, also ein Pass-By-Reference durchführen, muss man dies mit Pointern simulieren. Java verhält sich hier genauso, nur dass Objekte als Referenzen übergeben werden, die beim Funktionsaufruf kopiert werden.

  • Alle Funktionsparameter werden per Pass-By-Value übergeben
  • Mit Pointern kann man aber jederzeit Pass-By-Reference simulieren
Pass-By-Value
void swap(int a, int b) {
    int tmp = a;
    a = b;
    b = tmp;
}

int main() {
    int a = 42, b = 23;
    swap(a, b);
    printf("a=%d, b=%d", a, b); /* a=42, b=23 */
}

Die swap-Funktion funktioniert nicht, weil die Integer-Werte a und b innerhalb der Funktion Kopien der Werte von main sind.

Will man, dass die Funktion die Werte vertauschen kann, muss man Pointer verwenden.

Pass-By-Value mit Pointer
void swap(int* a, int* b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main() {
    int a = 42, b = 23;
    swap(&a, &b);
    printf("a=%d, b=%d", a, b); /* a=23, b=42 */
}

static

Das Schlüsselwort static ist für Java-Programmierer besonders verwirrend, weil es in C eine völlig andere Bedeutung hat: Während es in Java dafür sorgt, dass Variablen und Methoden an der Klasse hängen und nicht mehr am Objekt, schränkt des in C bei globalen Variablen und Funktionen deren Sichtbarkeit ein.

  • Werden Funktionen oder globale Variablen als static gekennzeichnet, dann sind sie außerhalb der Datei nicht sichtbar (→ protected in Java)
static char gruss[] = "Guten Tag";
static void gruesse() {
    printf("%s\n", gruss);
}

Deklariert man in C eine lokale Variabel als der static, dann handelt es sich gar nicht um eine lokale Variable, sondern um eine globale Variable, deren Sichtbarkeit auf diese Funktion beschränkt ist. Wie alle globalen Variablen wird sie nicht auf dem Stack, sondern im Daten-Segment alloziert. Die Initialisierung der Variabel in der Funktion wird nur ein einziges Mal, beim ersten Aufruf durchgeführt.

  • Lokale Variablen mit static werden nicht auf dem Stack alloziert, sondern im Daten-Segment und werden nur einmal initialisiert
#include <stdio.h>

void counter() {
    static int c = 1;
    printf("%d\n", c++);
}

int main() {
    counter(); /* 1 */
    counter(); /* 2 */
    counter(); /* 3 */
}

In dem Beispiel sieht man deutlich, dass die Variable c über die Funktionsaufrufe hinweg ihren Wert behält. Beim ersten Aufruf von counter() wird sie mit dem Wert 1 initialisiert, bei allen weiteren Aufrufen findet Initialisierung aber nicht mehr statt.

Schlüsselwort const

Ein Schlüsselwort in C, das es in Java überhaupt nicht gibt, ist const. In Java hat man darauf verzichtet, weil man es als zu komplex angesehen hat und sich stattdessen für final entschieden, das eine andere Semantik als const hat. Mit const kann man in C kennzeichnen, dass eine Variabel nicht verändert werden darf.

Relativ kompliziert wird die Verwendung von const, wenn es sich um Pointer handelt: Das const kann sich entweder auf den Pointer beziehen oder aber auf das Objekt auf das der Pointer zeigt oder sogar auf beides.

  • Mit const kann deklariert werden, dass ein Argument nicht geändert wird
  • Bei Funktionen meist nur sinnvoll für Pointer (wg. Pass-by-Value)
  • Bei Pointern muss man unterscheiden
    • Pointer auf einen konstanten Wert: Pointer kann geändert werden Wert nicht
      const int *ptr oder int const *ptr
    • Konstanter Pointer auf einen Wert: Pointer kann nicht geändert werden, Wert jedoch schon
      int *const ptr
    • Konstanter Pointer auf einen konstanten Wert: Weder Pointer noch Wert können geändert werden
      const int *const ptr

Bei Funktionsparametern ist const im Allgemeinen nur bei Pointern sinnvoll, weil die Parameter ohnehin by-value übergeben werden und const hier wenig bringt.

Konstanter Wert
int i = 10, j = 20;
const int *ptr = &i; /* Pointer auf konstanten Wert */

ptr = &j; /* Ok */
*ptr = 100; /* Fehler */
Konstanter Pointer
int i = 10, j = 20;
int *const ptr = &i; /* Konstanter Pointer */

ptr = &j; /* Fehler */
*ptr = 100; /* Ok */
Konstanter Pointer auf konstanten Wert
int i = 10, j = 20;
const int *const ptr = &i; /* Konstanter Pointer */

ptr = &j; /* Fehler */
*ptr = 100; /* Fehler */

Schlüsselwort extern

Bei der Deklaration von Funktionen wird vom Compiler das Schlüsselwort extern automatisch hinzugefügt, da aus dem fehlenden Funktionsrumpf klar wird, dass es sich „nur“ um eine Deklaration handelt.

Bei Variablen gibt es keine Möglichkeit, ohne zusätzliches Schlüsselwort, die Deklaration von der Definition zu unterscheiden. Deswegen muss hier bei der Deklaration extern eingesetzt werden.

  • Mit extern kann man Variablen deklarieren, die an anderer Stelle (anderer Datei) deklariert werden
  • extern-Deklaration führt keine Initialisierung durch
file1.c
extern char buffer[]; /* kein Speicher reservieren */

int main() {
    printf("%s\n", buffer);
}
file2.c
char buffer[] = "Hallo Welt!"; /* Speicher reservieren */

Eine Variable muss – genauso wie eine Funktion – vor ihrer Benutzung deklariert werden. Wenn eine Variable, die in einer anderen Datei (Compilationseinheit) definiert wurde, benutzt werden soll, muss man sie entsprechend deklarieren. Durch das Schlüsselwort extern vor der Variable wird angezeigt, dass kein Speicher reserviert werden soll, da dies an anderer Stelle bei der Definition erfolgt.

In diesem Beispiel wird buffer in file1.c nur deklariert und in file2.c dann definiert. Der Linker kümmert sich am Ende darum, dass die main-Funktion auf die richtige Variable im Speicher zugreift.

Funktionspointer

C hat – anders als Java – von Anfang an Funktionen als „first class citizens“ betrachtet, sodass man Funktionen wie jedes andere Datenobjekt auch verwenden kann. Das Mittel dazu sind Funktionspointer, d. h. Pointer, die auf Funktionen zeigen. Diese Pointer können wie alle anderen Pointer auch übergeben und zugewiesen werden. Pointer-Arithmetik scheidet aus naheliegenden Gründen aus.

Leider ist die Syntax für die Deklaration von Variablen, die Funktionspointer enthalten können etwas sehr kompliziert ausgefallen, sodass es sogar Webseiten gibt, die die Syntax übersetzen.

Funktionen können über Funktionspointer wie Daten übergeben und verwaltet werden

  • int func(): Funktion, die einen int zurückgibt
  • int *func(): Funktion, die einen Pointer auf einen int zurückgibt
  • int (*func)(): Pointer auf eine Funktion, die einen int zurückgibt
  • int *(*func)(): Pointer auf eine Funktion, die einen Pointer auf einen int zurückgibt
  • int *(*func)(char*): Pointer auf eine Funktion, die einen Pointer auf einen int zurückgibt und einen Pointer auf char als Parameter hat

Über den Aufrufoperator () kann die Funktion, auf die der Funktionspointer zeigt, dann aufgerufen werden.

Beispiel: Funktionspointer
int rechne(int (*func)(int a, int b), int a, int b) {
    return (*func)(a, b);
}

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int main() {
    int r;

    r = rechne(add, 2, 17); /* 19 */
    printf("%d\n", r);

    r = rechne(sub, 2, 17); /* -15 */
    printf("%d\n", r);
}

Das Beispiel verwendet Funktionspointer, um der Funktion rechne() zur Laufzeit eine beliebige Implementierung einer arithmetischen Operation zu übergeben.

Hier einmal dasselbe Beispiel in Java unter Verwendung von Lambdas:

import java.util.function.IntBinaryOperator;

public class Rechner {

    public static int rechne(IntBinaryOperator func, int a, int b) {
        return func.applyAsInt(a, b);
    }

    public static void main(String[] args) {
        IntBinaryOperator sub = (a, b) -> a - b;
        IntBinaryOperator add = (a, b) -> a + b;

        int r;

        r = rechne(add, 2, 17);
        System.out.println(r); /* 19 */


        r = rechne(sub, 2, 17);
        System.out.println(r); /* -15 */
    }
}

Copyright © 2022 Thomas Smits