Teil 1: Grundlagen


Funktionen

Aufbau einer Funktion

Eine Funktion fasst eine oder mehrere Anweisungen unter einem eindeutigen Namen zusammen. Diese Anweisungen können dann durch Angabe des Funktionsnamens von (fast) beliebiger Stelle im Programm beliebig oft ausgeführt werden.

Eine Funktion besteht aus vier Bestandteilen: dem Returntyp RTYP, dem Funktionsname FNAME, den Parametern PARAMETER und dem Funktionscode.

1. Form:

RTYP FNAME ([PARAMETER])
{
    Anweisungen der Funktion
    [return RETURNWERT;]
}

Bei dieser Form der Funktion wird für alle Parameter und für den Returnwert explizit der Datentyp vorgegeben.

2. Form:

auto FNAME ([PARAMETER]) ->RTYP
{
    Anweisungen der Funktion
    [return RETURNWERT;]
}

Diese Form wird immer dann eingesetzt, wenn der Returntyp der Funktion vom Datentyp eines Parameters abhängt. In der Regel wird der Returntyp mittels decltype() vom Datentyp eines Parameters abgeleitet.

3. Form:

auto FNAME ([PARAMETER])
{
    Anweisungen der Funktion
    return RETURNWERT;
}

Bei diesem Funktionstyp wird der Returntyp der Funktion durch implizites Auswerten des Datentyps des RETURNWERT bestimmt.

Die Angaben in eckigen Klammer sind optional. So kann z.B. eine Funktion ohne jegliche Parameter aufgerufen werden.

Bevor eine Funktion aufgerufen werden kann, muss sie entweder vorher definiert sein (sprich: der Funktionscode muss bekannt sein) oder aber deklariert sein. Bei der Funktionsdeklaration wird lediglich der Funktionskopf, bestehen aus dem Returntyp, dem Funktionsnamen und den optionalen Parametern, angegeben.

Um eine Funktion aufzurufen, wird an der Stelle im Programm, an der die Funktion ausgeführt werden soll, der Funktionsnamen gefolgt von einer Klammer ( ) mit den optionalen Argumenten und einem abschließenden Semikolon angegeben.

Funktion ohne Rückgabewert und ohne Parameter

Bei diesem Funktionstyp ist nur die erste Form der Funktion zulässig und als Returntyp ist void einzusetzen.

#include <iostream>
using std::cout;
using std::endl;

// Funktionsdeklaration, nur notwendig wenn
// Funktionsdefinition nach Funktionsauf folgt
void PrintHeader();

// Funktionsdefinition
void PrintFooter()
{
    cout << "Fusszeile\n";
}

// main()
int main()
{
    PrintHeader();    // Funktionsaufrufe
    cout << "Text\n";
    PrintFooter();
}

// Funktionsdefinition
void PrintHeader()
{
    cout << "Kopfzeile\n";
}

Funktion mit Rückgabewert und ohne Parameter

Bei diesem Funktionstyp sind alle drei Formen erlaubt, obgleich die zweite Form hier wenig Sinn macht. Zu beachten Sie, dass hier Rückgabewert und nicht Rückgabewerte steht. Eine Funktion kann ein Datum zurückliefern.

Die Rückgabe des Datums erfolgt mit der return-Anweisung, wobei nach dem Schlüsselwort return das zurückzugebende Datum angegeben wird. Der Datentyp des zurückgegebenen Datums sollte dabei mit dem Returntyp der Funktion übereinstimmen. Stimmen die Datentypen nicht überein, versucht der Compiler das zurückzugebende Datum in den Returntyp der Funktion zu konvertieren.

#include <iostream>
#include <cstdlib> // Fuer rand() und srand() Funktion
#include <ctime>   // Fuer time()
using std::cout;
using std::endl;

// Funktionsdefinition, 1. Form
// Explizite Angabe des Returntyps
int GetRandom1()
{
    // Zufallszahlengenerator initialisieren
    auto uhrzeit = time(nullptr);
    srand(static_cast<unsigned int>(uhrzeit));
    // Zufallszahl auslesen
    auto var = std::rand();
    // Zufallszahl zurueckgeben
    return var;
}

// Funktionsdefinition, 3. Form
// Returntyp wir durch return-Anweisung bestimmt
auto GetRandom2()
{
    // Zufallszahl zurueckgeben
    return rand();
}

// main()
int main()
{
    cout << "1. Zufallszahl: " << GetRandom1() << endl;
    cout << "2. Zufallszahl: " << GetRandom2() << endl;
}

Funktion ohne Rückgabewert und mit Parameter

Auch bei diesem Funktionstyp ist nur die erste Form zulässig und als Returntyp ist ebenfalls void einzusetzen.

Bei der Deklaration der Funktion müssen zumindest die Datentypen der Parameter angegebenen werden. Die Angabe von Parameternamen ist optional, sollten jedoch der besseren Lesbarkeit wegen immer vorhanden sein. Die einzelnen Parameter werden durch Komma getrennt aufgelisten.

Bei der Definition einer Funktion ist der Parametername zwingend vorgeschrieben. Über diesen Namen wird auf die beim Aufruf der Funktion angegebenen Argumente zugegriffen.

Damit das Ganze nicht zu einfach wird, gibt es prinzipiell zwei Arten von Parametern: called-by-value Parameter und Referenzparameter.

called-by-value Parameter

Bei einem Parameter vom Typ call-by-value erhält die aufgerufene Funktion eine Kopie des beim Funktionsaufruf angegebenen Datums. Daraus folgt, dass Änderungen des Parameterwerts zwar innerhalb der Funktion erlaubt sind, diese sich aber nur auf die Kopie auswirken.

Referenzparameter

Ein Referenzparameter ermöglicht einer Funktion, Änderungen an einem übergebenen Datum dauerhaft vorzunehmen. D.h., die Funktion erhält hierbei keine Kopie sondern eine Referenz (Verweis) auf das übergebene Datum.

Ein Referenzparameter wird dadurch gekennzeichnet, dass nach dem Datentyp des Parameters der Referenzoperator & folgt.

#include <iostream>
using std::cout;
using std::endl;

// Funktionsdefinition
// Funktion erhaelt Kopie des uebergebenen Arguments
void PrintHeader(auto page)
{
    cout << "Seite " << page << endl;
    // Dies hat keine Auswirkung auf den uebergebenen Wert
    page = 0;
}

// Funktionsdefinition
// Funktion erhaelt Referenzparameter, d.h. Verweise
// auf die uebergebenen Argumente
void Swap(int& val1, int& val2)
{
    // Inhalte der Parameter tauschen
    auto temp = val1;
    val1 = val2;
    val2 = temp;
}

// main()
int main()
{
    // Seitenueberschrift ausgeben
    for (auto index = 1; index < 4; index++)
        PrintHeader(index);

    // Zwei Variablen definieren und initialisieren
    int var1 = 10, var2 = 20;
    cout << "var1: " << var1 << ", var2: " << var2 << endl;
    // Inhalt der Variablen tauschen
    Swap(var1, var2);
    cout << "Nach Tauschen der Variablen:\n";
    cout << "var1: " << var1 << ", var2: " << var2 << endl;
}

Konstante Referenzparameter

In vielen Fällen ist es durchaus sinnvoll, Daten per Referenz zu übergeben damit kein Kopiervorgang der Daten notwendig wird, aber innerhalb der Funktion keine Änderung dieser Daten zuzulassen. Um Referenzparameter als nicht veränderbar innerhalb einer Funktion zu kennzeichnen, wird dem Datentyp des Referenzparameters das Schlüsselwort const vorangestellt. Damit ist es dann auch möglich, Referenzen auf Literale an eine Funktion zu übergeben.

#include <iostream>
#include <string>
using std::cout;
using std::endl;
using namespace std::string_literals;

// Funktionsdefinition
// Erhaelt den auszugebenden Text als
// konstanten string-Referenzparameter
void PrintString(const std::string& text)
{
    cout << "Ausgabe: " << text << endl;
}

// main()
int main()
{
    // string definieren / initialisieren
    std::string text1{ "Text1"s };
    // string ausgeben
    PrintString(text1);
    // string-Literal ausgeben
    PrintString("Text2"s);
}

Felder als Parameter

Um ein eindimensionales Feld an eine Funktion zu übergeben, wird dessen Anfangsadresse übergeben. Wie im Kapitel über Felder erwähnt, ist der Name eines eindimensionalen Feldes gleichbedeutend mit einem Zeiger auf den Beginn des Feldes.

Der Zugriff auf die Feldelemente kann innerhalb der Funktion entweder durch Dereferenzierung des übergebenen Zeigers oder aber indiziert erfolgen.

#include <iostream>
#include <string>
using std::cout;
using std::endl;
using namespace std::string_literals;

// Ausgabe eines Feldes
// pArray: Zeiger auf int-Feld
// size: Anzahl Feldelemente
void PrintArray(int *pArray, unsigned int size)
{
    cout << "Feldinhalt:\n";
    // Gesamtes Feld indiziert ausgeben
    for (auto index = 0U; index < size; index++)
        cout << pArray[index] << ',';
    // 1. und letztes Element ausgeben per Derefenzierung
    cout << "\n1. Element: " << *pArray << endl;
    cout << "Letztes Element: " << *(pArray + size - 1);
}

// main()
int main()
{
    // Auszugebendes Feld und Feldgroesse berechnen
    int feld[]{ 1,2,3,4 };
    constexpr unsigned int SIZE = sizeof(feld) / sizeof(feld[0]);
    // Feld ausgeben
    PrintArray(feld, SIZE);
}

Funktion mit Rückgabewert und mit Parameter

Hier sind alle 3 Funktionsformen erlaubt und auch sinnvoll.

Zusätzlich zum vorherigen Funktionstyp kann nun ein Datum zurückgegeben werden. Die Rückgabe des Datums erfolgt wiederum mit der return-Anweisung, wobei nach dem Schlüsselwort return das zurückzugebende Datum angegeben wird.

#include <iostream>
using std::cout;
using std::endl;

// 1. Form einer Funktion
// Groesstes Feldelement suchen
int Max(int* pArray, unsigned int size)
{
    // 1. Wert im Feld ist groesstes Element
    auto maxValue = pArray[0];
    // Feld ab 2. Element durchlaufen
    for (unsigned int index=1; index<size; index++)
    {
        // Wenn aktuelles Feldelement groesser ist
        if (pArray[index] > maxValue)
            maxValue = pArray[index];
    }
    // Maximalen Wert zurueckgeben
    return maxValue;
}

// 2. Form einer Funktion
// Kleinestes Feldelement suchen
// Ablauf identisch mit Max() Funktion
auto Min(int* pArray, unsigned int size) -> decltype(pArray[0])
{
    auto minValue = pArray[0];
    for (unsigned int index = 1; index < size; index++)
    {
        if (pArray[index] < minValue)
            minValue = pArray[index];
    }
    return minValue;
}

// 3. Form einer Funktion
// Mittelwert der Feldelemente berechnen
auto Average(int* pArray, unsigned int size)
{
    // Alle Elemente im Feld aufsummieren
    auto sum = 0;
    for (unsigned int index = 0; index < size; index++)
        sum += pArray[index];
    // Mittelwert als Gleitkommawert zurueckgeben
    return (float)sum / (float)size;
}

// main()
int main()
{
    // Auszugebendes Feld und Feldgroesse berechnen
    int feld[]{ 2,1,4,3 };
    constexpr unsigned int SIZE = sizeof(feld) / sizeof(feld[0]);
    // Max/Min/Mittel im Feld berechnen
    cout << "Maximalwert: " << Max(feld, SIZE) << endl;
    cout << "Minmalwert : " << Min(feld, SIZE) << endl;
    cout << "Mittelwert : " << Average(feld, SIZE) << endl;
}

constexpr Funktion

Um eine Funktion als constexpr Funktion zu deklarieren bzw. zu definieren, wird vor dem Returntyp das Schlüsselwort constexpr gestellt. Das Besondere an einer constexpr Funktion ist, dass der Compiler beim Aufruf der Funktion versucht das Ergebnis der Funktion zu berechnen. Damit der Compiler dies kann, muss jeder Parameter ein zur Compilezeit berechenbarer Literaltyp sein und der Returnwert muss ebenfalls ein Literaltyp sein.

#include <iostream>
using std::cout;
using std::endl;

// Berechnung der Fakultaet
// Dies ist eine rekursive Funktion, dh. sie ruft sich
// solange selbst wieder auf, bis der Parameter val
// den Wert 1 annimmt
constexpr unsigned long Fakul(unsigned long val)
{
    if (val <= 1)
        return 1;
    else
        return val * Fakul(val - 1);
}

// Umrechung von Grad in Bogenmass
// 360 Grad = 2 PI
const double PI = 3.1416; // PI ungefaehr
constexpr double Deg2Rad(double deg)
{
    return 2. * PI / 360. * deg;
}

// main()int main()
{
    // Fakultaeten berechnen
    // Das Argument muss ein Literal sein!
    cout << "F(2): " << Fakul(2UL) << endl;
    cout << "F(5): " << Fakul(5UL) << endl;
    // Umrechnung Grad in Bogenmass
    cout << "90 Grad = " << Deg2Rad(90) << endl;
    cout << "360 Grad = " << Deg2Rad(360) << endl;
}



Copyright © cpp-tutor.de
Impressum &Datenschutz