C++: Strukturierte Programmierung mit Funktionen

Strukturierte Programmierung hei├čt vor allem, dass man wiederkehrende Quelltexte zu Unterprogrammen (Funktionen) zusammenfasst. Dieser Abschnitt zeigt, wie Funktionen in C++ realisiert werden (genauer: wie sie definiert und aufgerufen werden). Die Quelltexte lassen sich besser strukturieren, wenn man die Definition einer Funktion in Deklaration und Implementierung aufteilt. Weiter wird das ├ťberladen von Funktionen besprochen.
Abgestimmt in Hilfreich
Noch keine Kommentare

Einordnung des Artikels

Einf├╝hrung

Bisher wurden Algorithmen im Sinne der prozeduralen Programmierung geschrieben: man schreibt eine Folge von Anweisungen, die von oben nach unten ausgef├╝hrt werden, wobei die Strukturelemente Bedingungspr├╝fung, Alternative und Schleifen f├╝r einen verwickelten Ablauf sorgen k├Ânnen.

Es sollte klar sein, dass die prozedurale Programmierung bei kleinen Programmen (solange sie etwa nur einige Bildschirme f├╝llen) sehr effektiv eingesetzt werden kann. Bei gr├Â├čeren Vorhaben st├Â├čt diese Art der Programmierung schnell an ihre Grenzen:

  • die Programme werden un├╝bersichtlich (und zwar ├╝berproportional zu ihrer Gr├Â├če),
  • semantische Fehler sind schwer auffindbar,
  • manche Operationen k├Ânnen Nebeneffekte produzieren, die man bei ├änderungen nicht beachtet,
  • soll das Programm abge├Ąndert werden, m├╝ssen alle betroffenen Stellen aufgesucht werden (vergisst man eine, produziert man neue Fehler),
  • soll das Programm erweitert werden, ist es meist einfacher von Neuem zu beginnen.

Mit dem Grundsatz teile und herrsche lassen sich diese Nachteile zwar nicht vollst├Ąndig vermeiden, man kann aber bei deutlich komplexeren Programmen den ├ťberblick behalten. Mit teile und herrsche ist gemeint, dass man abgegrenzte Teile des Algorithmus in Funktionen auslagert. Dies setzt allerdings voraus, dass diese Funktionen m├Âglichst keine Abh├Ąngigkeit zum restlichen Algorithmus besitzen, so dass sie in verschiedenen Situationen eingesetzt werden k├Ânnen. Man spricht dann nicht mehr von prozeduraler Programmierung sondern von strukturierter Programmierung. Klug definierte Funktionen vermeiden Wiederholungen im Quelltext, k├Ânnen wiederverwedet werden und f├╝hren somit zu einer Modularisierung des Programmes.

Mit den mathematischen Funktionen aus der Standard-Bibliothek cmath haben sie Paradebeispiele f├╝r solche Funktionen kennengelernt. Dieser Abschnitt soll in diese Technik einf├╝hren, Sie werden lernen:

  • wie man Funktionen selbst definiert und aufruft,
  • wie man einer Funktion Parameter ├╝bergibt und einen R├╝ckgabewert erzeugt,
  • wie man Deklaration und Implementierung einer Funktionen trennt.

Tip:

Jede Funktion sollte eine und nur eine, klar definierte Aufgabe erf├╝llen.

Man kann die Sichtweise auch umkehren:

Beim Entwickeln eines Programmes muss man sich immer ├╝berlegen, welche Aufgaben mehrfach vorkommen und sinnvoll in Funktionen verlegt werden k├Ânnen.

Trennung von Deklaration und Implementierung einer Funktion

Die folgenden Programmzeilen berechnen aus zwei gegebenen Gleitkommazahlen deren Mittelwert:

double x;
double y;
// x und y werden irgendwie gesetzt

double mean = (x + y) / 2;

Wird diese Mittelwert-Berechnung ├Âfters verwendet, bietet es sich an, f├╝r diese Aufgabe eine Funktion zu definieren; sie soll den Namen mean() erhalten.

├ähnlich wie Variablen deklariert und initialisiert werden, wird eine Funktion deklariert und implementiert. Die Deklaration ben├Âtigt der Compiler, um die Aufrufe der Funktion auf Korrektheit zu pr├╝fen. Die Implementierung wird dann erst bei der Ausf├╝hrung des Programmes ben├Âtigt, wenn die Funktion tats├Ąchlich aufgerufen wird und die Berechnungen ausgef├╝hrt werden m├╝ssen.

Die Deklaration kann sogar weggelassen werden, da der Compiler auch die Implementierung auswerten kann; ihre Anwesenheit sorgt aber f├╝r eine bessere Lesbarkeit des Quelltextes. L├Ąsst man die Deklaration weg, muss die Implementierung vor dem Aufruf der Funktion stehen. ├ťblich ist es die Deklaration m├Âglichst weit oben im Programm zu plazieren (vor main() und die Implementierung nach main()). Zudem sollte die Deklaration mit einer kurzen Beschreibung der Aufgabe der Funktion versehen werden.

Diese Anordnung ist naheliegend: dem Leser des Programms sollte die Deklaration der Funktion ausreichen, um sie sinnvoll einzusetzen; die Implementierung wird er nur in Zweifelsf├Ąllen aufsuchen.

Hier das Beispiel, in dem die Mittelwert-Berechnung mit Hilfe von mean() ausgef├╝hrt wird. Das Beispiel enth├Ąlt:

  • die Deklaration der Funktion mean() zu Beginn der Datei (nach den includes und dergleichen und vor main(),
  • die main()-Methode, in der die Funktion mean() zweimal aufgerufen wird,
  • die Implementierung der Funktion mean() am Ende der Datei (nach main()).
#include <iostream>
using namespace std;

// mean() berechnet den Mittelwert zweier Zahlen x1 und x2                // Kommentar zur Beschreibung der Aufgabe der Funktion mean()
double mean(double x1, double x2);                                      // Deklaration der Funktion mean()

int main()
{
    double x {3};
    double y {7};

    double meanValue = mean(x, y);                          // Aufruf der Funktion mean() mit zwei Variablen
    cout << "Mittelwert: " << meanValue << endl;

    meanValue = mean(1, 7);                                 // Aufruf der Funktion mean() mit zwei Zahlen
    cout << "Mittelwert: " << meanValue << endl;
    return 0;
}

double mean(double x1, double x2)                           // Implementierung der Funktion mean()
{
    double mean = (x1 + x2) / 2;
    return mean;
}

Zur Erkl├Ąrung:

  1. Die Deklaration (Zeile 5) besteht aus drei Teilen:
    • Der Name der Funktion wird vereinbart (hier mean()).
    • Die Parameter-Liste muss angegeben werden, also die Eingabewerte der Funktion: hier sind es zwei Gleitkommazahlen vom Datentyp double. Die Namen der Parameter sind unerheblich und k├Ânnen sogar weggelassen werden. Nicht weggelassen werden d├╝rfen die Datentypen der Eingabewerte. Oftmals k├Ânnen die Namen der Parameter aber sinnvoll gew├Ąhlt werden, da sie auf deren Bedeutung hinweisen. Hier wurden sie x1 und x2 genannt, was darauf hindeuten soll, dass sie austauschbar sind (ohne den berechneten Wert zu ver├Ąndern). Die Eintr├Ąge der Parameter-Liste sind durch Kommas getrennt.
    • Der Datentyp des R├╝ckgabewertes (dieser steht vor dem Namen der Funktion): Die Funktion mean() berechnet einen Wert vom Datentyp double. Das hei├čt beim Aufruf der Funktion mean() entsteht ein Zahlenwert vom Datentyp double, der in einen Befehl eingebaut werden kann (siehe Aufruf der Funktion).
  2. Die Implementierung der Funktion mean() (in den Zeilen 20 bis 24) muss nat├╝rlich in der ersten Zeile mit der Deklaration ├╝bereinstimmen (Parameter-Liste und Datentyp des R├╝ckgabewertes). In den geschweiften Klammern stehen dann die Anweisungen, die zur Berechnung des R├╝ckgabewertes f├╝hren. Die Implementierung endet mit dem return-statement (Zeile 23). Die Namen der Parameter sind zwar frei w├Ąhlbar, d├╝rfen in der Implementierung aber nicht weggelassen werden, da mit ihnen im K├Ârper der Funktion Berechnungen ausgef├╝hrt werden.
  3. Der Aufruf der Funktion mean() (in den Zeilen 12 und 15) erfolgt einmal mit Variablen und einmal mit Zahlen; bei diese Berechnungen sind nat├╝rlich die Datentypen der Deklaration von mean() zu beachten (beziehungsweise die Typumwandlungen). Der Aufruf von mean() erzeugt stets eine Gleitkommazahl (double), die hier in einer Zuweisung verwendet wird.

Tip:

Ordnen Sie Kommentar (zur Beschreibung der Aufgabe einer Funktion), Deklaration und Implementierung immer so an wie im Beispiel oben.

Spezialf├Ąlle

In der strukturierten Programmierung hat man anfangs streng zwischen Prozedur und Funktion unterschieden (siehe Prozeduren und Funktionen in der ├ťbersicht ├╝ber Programmiersprachen), heute gibt man diese Trennung meist auf. Die folgenden Spezialf├Ąlle sollte man aber kennen.

R├╝ckgabetyp void

Eine Funktion kann auch keinen R├╝ckgabewert berechnen und stattdessen nur Aktionen ausf├╝hren (etwa Konsolen-Ausgaben). Meistens brauchen diese Funktionen kein return-statement ÔÇö sie k├Ânnen sogar mehrere haben wie die folgenden Beispiele zeigen.

Beispiele:

1. Die Funktion printFactors(int n) soll die Teiler der Zahl n ausgeben:

#include <iostream>
using namespace std;

// Ausgabe der Teiler einer nat├╝rlichen Zahl
void printFactors(int n);

int main()
{
    printFactors(27);           // 1 3 9 27
    return 0;
}

void printFactors(int n)
{
    cout << 1 << endl;
    for (int i {2}; i < 1 + n / 2; i++)
    {
        if (n % i == 0)
            cout << i << endl;
    }
    cout << n << endl;
}

Die Funktion printFactors(int n) kommt ohne return-statement aus und liefert dennoch f├╝r n > 2 die richtigen Ausgaben. Ausnahme ist der Aufruf

printFactors(1);

Jetzt wird der Teiler 1 zweimal ausgegeben.

Und noch schlimmer:

printFactors(0);

liefert die Ausgabe 1 und 0. Um dies zu vermeiden, wird die Funktion mit mehreren return-statements implementiert.

2. Neue Version von printFactors(int n) (hier ist nur ihre Implementierung gezeigt, die Deklaration bleibt unver├Ąndert):

void printFactors(int n)
{
    if (n == 0)
        return;
    if (n == 1)
    {
        cout << 1 << endl;
        return;
    }
    cout << 1 << endl;
    for (int i {2}; i < 1 + n / 2; i++)
    {
        if (n % i == 0)
            cout << i << endl;
    }
    cout << n << endl;
}

Der Quelltext ist zwar deutlich aufgebl├Ąht, liefert aber bei den Spezialf├Ąllen n = 0 und n = 1 die richtigen Ausgaben. Die return-statements sorgen daf├╝r, dass die Funktion vorzeitig verlassen wird.

Funktionen ohne Parameter

Eine Funktion muss keine Parameter besitzen. Man darf in diesem Fall aber die runden Klammern nicht weglassen. So wie bisher die main()-Methode eingesetzt wurde, war sie genau dieser Spezialfall. Ein anderes Beispiel:

#include <iostream>
using namespace std;

// life, the universe and everything
int getAnswer();

int main()
    {
        cout << getAnswer() << endl;            // 42
        return 0;
    }
    
int getAnswer()
{
    // millions of calculations
    return 42;
}

Funktionen ├╝berladen

Das obige Beispiel der Mittelwert-Berechnung mit mean() wirft sofort Fragen auf:

  • Wenn man eine Mittelwert-Berechnung mit drei Eingabewerten durchf├╝hren m├Âchte, ist dann der Name mean() bereits vergeben oder kann er nochmals verwendet werden? Ja!
  • Kann man Funktionen mit identischen Eingabewerten aber unterschiedlichen Datentypen des R├╝ckgabewertes unter dem gleichen Namen anbieten? Nein!

Es ist erlaubt, eine Funktion zu ├╝berladen, damit ist gemeint, dass sie identischen Namen aber eine andere Parameter-Liste besitzt. Das folgende Beispiel definiert zus├Ątzlich die Funktion mean() f├╝r drei Eingabewerte:

#include <iostream>
using namespace std;

// mean berechnet den Mittelwert zweier Zahlen x1 und x2
double mean(double x1, double x2);                                          // Deklaration der Funktion mean() mit zwei Eingabewerten

// mean berechnet den Mittelwert dreier Zahlen x1, x2 und x3
double mean(double x1, double x2, double x3);                               // Deklaration der Funktion mean() mit drei Eingabewerten

int main()
{
    double x {3};
    double y {7};
    double z {11};

    double meanValue = mean(x, y);                              // Aufruf der Funktion mean() mit zwei Variablen
    cout << "Mittelwert: " << meanValue << endl;                // 5

    double meanValue = mean(x, y, z);                           // Aufruf der Funktion mean() mit drei Variablen
    cout << "Mittelwert: " << meanValue << endl;                // 7
    
    return 0;
}

double mean(double x1, double x2)                               // Implementierung der Funktion mean() mit zwei Eingabewerten
{
    double mean = (x1 + x2) / 2;
    return mean;
}

double mean(double x1, double x2, double x3)                    // Implementierung der Funktion mean() mit drei Eingabewerten
{
    double mean = (x1 + x2 + x3) / 3;
    return mean;
}

Der R├╝ckgabetyp darf nicht ├╝berladen werden. Das folgende Beispiel f├╝hrt ÔÇö schon durch die Deklaration ÔÇö zu einem Compiler-Fehler:

double mean(double x1, double x2);

int mean(double x1, double x2);         // Compiler-Fehler

Tip:

Setzen Sie das ├ťberladen von Funktionen nur ein, wenn die Funktionen identische Aufgaben erf├╝llen.

Deklaration, Prototyp und Signatur einer Funktion

Oben wurde zwischen Deklaration und Implementierung einer Funktion unterschieden:

  • Die Deklaration zeigt:
    • den Namen der Funktion,
    • die Parameter-Liste (genauer: deren Datentypen, die Namen der Parameter k├Ânnen weggelassen werden) und
    • den Typ des R├╝ckgabewertes.
  • Die Implementierung muss die Deklaration wiederholen (und zus├Ątzlich den Parametern willk├╝rliche Namen geben, da sie in der Implementierung verarbeitet werden).

Die Deklaration wird oft auch als Prototyp einer Funktion bezeichnet.

Deklaration und Implementierung zusammen nennt man die Definition der Funktion.

Bei ├╝berladenen Funktionen unterscheiden sich nur die Parameter-Listen, Name und Typ des R├╝ckgabewertes sind identisch. Man nennt Name und Parameter-Liste einer Funktion ihre Signatur.

Globale Funktionen

Am Beispiel der Funktion mean() wurde im vorhergehenden Abschnitt gezeigt, wie Deklaration und Implementierung getrennt werden. Der Nachteil dieser L├Âsung ist allerdings, dass beide in derselben Datei waren wie die main()-Methode, die dann mean() aufgerufen hat. W├╝nschenswert ist stattdessen eine L├Âsung wie sie zum Beispiel bei den Funktionen von cmath angeboten wird: Diese sind als globale Funktionen definiert und k├Ânnen von jedem beliebigen Programm aus aufgerufen werden.

Das Beispiel mit mean() soll jetzt ÔÇö bei identischer Deklaration und Implementierung ÔÇö so aufgebaut werden, dass mean() als globale Funktion aufgerufen werden kann. Um die Trennung zwischen Deklaration und Implementierung beizubehalten, werden zwei Dateien angelegt:

Statistik.h             // enth├Ąlt die Deklaration von mean()
Statistik.cpp           // enth├Ąlt die Implementierung von mean()

Der Name Statistik f├╝r die Dateien ist wieder frei w├Ąhlbar ÔÇö er soll darauf hindeuten, dass die Dateien erweitert werden und sp├Ąter mehrere statistische Funktionen anbieten. Es folgen zuerst die beiden Dateien f├╝r die globale Funktion mean() und anschlie├čend die Datei

main.cpp            // Test des Aufrufs von mean()

von der aus die Funktion mean() aufgerufen wird.

// Statistik.h          
// Deklaration von mean()

#ifndef STATISTICS_H_INCLUDED
#define STATISTICS_H_INCLUDED

double mean(double x1, double x2);

#endif // STATISTICS_H_INCLUDED
// Statistik.cpp            
// Implementierung von mean()

#include "Statistik.h"

double mean(double x1, double x2)
{
    return (x1 + x2) / 2;
}
// main.cpp
// Test zum Aufruf der globalen Funktion mean()

#include "Statistik.h"          // erm├Âglicht den Aufruf von mean, die in Statistik.h deklariert ist

#include <iostream>
using namespace std;

int main() {

    double meanValue = mean(3.5, 5.5);
    cout << meanValue << endl;          // 4.5
    return 0;
}

Zur Erkl├Ąrung:

1. Die Datei Statistik.h:

Zum Anlegen der Datei w├Ąhlt man in Code::Blocks

File -> New... -> C/C++ header

und w├Ąhlt als Datei-Endung .h. Damit werden Header-Dateien bezeichnet.

Neu sind die drei Pr├Ąprozessor-Direktiven (Zeile 4, 5 und 9), die von Code::Blocks angelegt werden. Ihre Bedeutung ist folgende: Die Datei Statistik.h muss von mehreren anderen Dateien inkludiert werden; bei komplexen Projekten kann es dann leicht passieren, dass eine Datei mehrfach inkludiert wird ÔÇö genau das verhindern die drei Direktiven, man nennt sie daher auch include guards.

Mit diesem Wissen sollten die Namen der Direktiven selbsterkl├Ąrend sein: Sie definieren ein Symbol STATISTICS_H_INCLUDED f├╝r den Fall, dass es noch nicht definiert ist und so weiter. Der Name STATISTICS_H_INCLUDED ist beliebig w├Ąhlbar, darf nur nicht in einer anderen Header-Datei verwendet werden ÔÇö sonst sind die include guards wirkungslos. Inkludiert wird dann alles, was in die Pr├Ąprozessor-Direktiven define und endif eingeschlossen ist:

#define STATISTICS_H_INCLUDED

// alles hier wird inkludiert

#endif // STATISTICS_H_INCLUDED

2. Die Datei Statistik.cpp:

Sie enth├Ąlt die Implementierung der Funktion mean(). Achten Sie darauf, dass die Datei tats├Ąchlich kompiliert wird und die zugeh├Ârige Objekt-Datei

Statistik.o

in Ihrem Projekt-Ordner enthalten ist. Wenn sp├Ąter das Kompilieren von main.cpp zu einem Fehler f├╝hrt und die Fehlermeldung besagt, dass mean() nicht definiert ist, kann es daran liegen, dass Statistik.cpp nicht kompiliert wurde.

Da jetzt Deklaration und Implementierung in getrennten Dateien enthalten sind, muss der include-Befehl

#include "Statistik.h"

enthalten sein. Beachten Sie, dass die Datei anders angegeben wird als dies bei includes aus der Standard-Bibliothek der Fall ist:

  • die Datei-Endung muss angegeben werden,
  • anstelle der spitzen Klammern werden Anf├╝hrungsstriche gesetzt.

Ein umgekehrter include-Befehl ist nicht n├Âtig: die Header-Datei muss nicht ihre Implementierung inkludieren (wenn Sie bedenken, wie der Compiler arbeitet, sollte dies klar sein!).

3. Die Datei main.cpp:

  • In der main()-Methode wird die globale Funktion mean() aufgerufen (Zeile 11) ÔÇö dieser Schritt ist bereits bekannt.
  • Damit der Aufruf erfolgen kann, muss auch hier die Datei Statistik.h inkludiert werden (Zeile 4) und Statistik.o muss beim build-Prozess erzeugt worden sein.

Aufgabe:

Legen Sie ein neues Projekt mit den drei besprochenen Dateien an. Verwenden Sie dazu am Besten

File -> New -> Project -> Empty project

├ťberpr├╝fen Sie, ob die Objekt-Datei Statistik.o erzeugt wurde.

Sp├Ąter wird dieses Projekt um einige statistische Funktionen erweitert.