Weitere Konzepte der strukturierten Programmierung in C++: Einsatz von Vektoren, Gültigkeitsbereiche

Um Beispiele zur strukturierten Programmierung zu zeigen, werden einige Funktionen vorgestellt, die mit den Klassen vector und array aus der Standard-Bibliothek arbeiten. Weiter wird anhand der Beispiele das Konzept des Gültigkeitsbereiches näher erläutert, insbesondere werden Eigenschaften und Einsatz-Möglichkeiten von lokalen Variablen, globalen Variablen und lokalen static-Variablen diskutiert.

Einordnung des Artikels

Im vorhergehenden Kapitel C++: Strukturierte Programmierung mit Funktionen wurde ein kleines Statistik-Projekt vorbereitet. Mit diesem wird hier weitergearbeitet und daran werden die neuen Konzepte erläutert.

Einige Erweiterungen des Statistik-Projektes: Einsatz von Vektoren

Bisher wurde in Statistik.h nur eine globale Funktion angeboten, nämlich

double mean(double x1, double x2);

Wünschenswert ist selbstverständlich eine Funktion, die ein Tupel von Zahlen beliebiger Länge als Eingabewert besitzt und dafür den Mittelwert berechnet. Durch das Überladen von Funktionen könnte man Versionen mit drei, vier, ... Argumenten anbieten — auch das ist nicht zufriedenstellend. Wie kann man der Funktion mean() eine Liste von Zahlen übergeben?

In der Standard-Bibliothek gibt es dazu die Klassen array und vector mit folgenden Eigenschaften:

  1. Sowohl ein array-Objekt als auch ein vector-Objekt ist ein Tupel von n gleichartigen Elementen. Dabei heißt gleichartig, dass alle Elemente identischen Datentyp besitzen müssen; und dieser Datentyp muss beim Erzeugen des Objektes angegeben werden.
  2. Beim Erzeugen eines array-Objektes muss die Anzahl n der Elemente festgelegt werden und kann später nicht mehr verändert werden.
  3. Die Klasse vector besitzt nahezu identische Eigenschaften wie array, allerdings kann die Anzahl der Elemente während der Ausführung des Programms dynamisch angepasst werden.

Das heißt aber, dass die Klasse array nur dann eingesetzt werden kann, wenn man zum Beispiel den Mittelwert immer für eine Zahlenfolge konstanter Länge berechnen möchte, denn der Funktionsprototyp:

// Berechnet den Mittelwert eines Arrays (Zufallsfolge randomSequence = rs der Länge NUMBER_OF_TRIALS)
double mean(array<int, NUMBER_OF_TRIALS> rs);

muss die Länge des Arrays enthalten, hier die Konstante NUMBER_OF_TRIALS , die schon vor dem Aufruf von mean() gesetzt werden muss.

Dagegen lässt sich die gewünschte Funktion mit vector realisieren:

// Berechnet den Mittelwert eines Vektors (Zufallsfolge randomSequence = rs) beliebiger Länge
double mean(vector<int> rs);

Im Körper der Funktion mean() kann die Länge des Vektors rs abgefragt werden und dann (mit Hilfe einer Schleife) der Mittelwert der Zahlenfolge berechnet werden. Jetzt kann also eine Zahlenfolge beliebiger Länge an die mean()-Funktion übergeben werden.

Um die neue Mittelwert-Berechnung zu testen, wird eine weitere Funktion angeboten, die eine Zufallsfolge erzeugt (an deren Quelltext werden Sie feststellen, dass die Folge keineswegs zufällig ist — zum Test der mean()-Funktion ist sie ausreichend).

Die folgenden Quelltexte zeigen wieder die drei Dateien:

Statistik.h             // Deklarationen
Statistik.cpp           // Implementierungen
main.cpp                // Test der Funktionen
// Statistik.h
// Deklarationen der statistischen Funktionen

#ifndef STATISTIK_H_INCLUDED
#define STATISTIK_H_INCLUDED

#include <vector>

double mean(double x1, double x2);

// Berechnet den Mittelwert eines Vektors (Zufallsfolge randomSequence = rs)
double mean(std::vector<int> rs);

// Erzeugt Zufallsfolge mit int-Zahlen von min bis max der Länge number
std::vector<int> createRandomSequence(int min, int max, int number);

#endif // STATISTIK_H_INCLUDED
// Statistik.cpp
// Implementierungen der statistischen Funktionen

#include "Statistik.h"

#include <iostream>
#include <vector>

using namespace std;

double mean(double x1, double x2)
{
    return (x1 + x2) / 2;
}

// Berechnet den Mittelwert eines Vektors (Zufallsfolge randomSequence)
double mean(vector<int> randomSequence)
{
    int number = randomSequence.size();
    int summe {0};
    for (int value : randomSequence)
    {
        summe += value;
    }
    return static_cast<double> (summe) / number;
}

// Erzeugt Zufallsfolge mit int-Zahlen von min bis max der Länge number
vector<int> createRandomSequence(int min, int max, int number)
{
    int diff = max - min + 1;

    vector<int> randomSequence(number);

    for (int i = 0; i < number; i++)
    {
        randomSequence[i] = (i % diff) + min;       // This is a FAKE! TODO: echte Zufallsfolge erzeugen!
        cout << randomSequence[i] << endl;
    }

    return randomSequence;
}
// main.cpp

#include "Statistik.h"

#include <iostream>
using namespace std;

int main() 
{
    vector<int> rs = createRandomSequence(1, 6, 20);        // 20x Würfeln
    for (int randomElement : rs)
    {
        cout << randomElement;              //  12345612345612345612
    }
    cout << "\nMittelwert: " << mean(rs) << endl;
    return 0;
}

Zur Erklärung::

1. Aktionen in der main()-Methode in main.cpp:

A) Da man auf die Statistik-Funktionen zugreifen möchte, muss Statistik.h inkludiert werden (Zeile 3; in Statistik.h sind die Deklarationen der Funktionen).

B) In Zeile 10 wird zunächst ein Vektor namens rs (kurz für randomSequence) angelegt mittels:

vector<int> rs = createRandomSequence(1, 6, 20);

Das Objekt rs ist vom Datentyp vector und jede Komponente des Vektors muss vom Datentyp int sein. Das eigentliche Erzeugen des Objektes geschieht dann in der Funktion createRandomSequence(). Diese Funktion wird so aufgerufen, dass eine Zahlenfolge mit Einträgen zwischen 1 und 6 erzeugt werden (Würfel), wobei die Länge der Zahlenfolge 20 sein soll.

C) In der for-Schleife (Zeile 11 bis 14) wird die Zahlenfolge ausgegeben; man sieht, dass es keine Zufallsfolge ist, da in der Funktion createRandomSequence() kein echter Zufallsgenerator eingebaut wurde.

Die for-Anweisung sieht völlig anders aus als gewohnt. Man nennt sie eine verkürzte for-Anweisung, die für Container-Klassen wie array und vector verwendet werden kann.

Die for-Schleife ist glechwertig zu folgender Schleife, die mit Hilfe des Index auf die Komponenten des Vektors zugreift:

for (int i = 0; i < rs.size(); i++)
{
    cout << rs[i];          //  12345612345612345612
}

Hier wird mit rs.size() festgestellt, wieviele Komponenten der Vektor hat (nach obigem Aufruf von createRandomSequence() sind es 20). Die Komponenten des Vektors sind indiziert mit 0, 1, 2,..., 19 (nicht mit 1, 2,..., 20). Der Zugriff auf eine Komponente des Vektors erfolgt dann mit

rs[i]

Damit sollte die verkürzte for-Schleife verständlich sein: Man definiert ein geeignetes Objekt (hier randomElement), das einer Komponente des Vektors entspricht; der Datentyp muss int sein, da der Vektor mit

vector<int> rs

deklariert wurde. Der Operator : sorgt dann dafür, dass dieses Objekt randomElement über die Komponenten des Vektors rs iteriert.

2. Deklarationen in Statistik.h:

3. Implementierungen in Statistik.cpp:

Es gibt natürlich auch eine einfachere Art, wie ein Vektor erzeugt werden kann, sie ist hier allerdings nicht zu gebrauchen:

vector<int> randomSequence = {1, 2, 3, 4, 5, 6, 1};

Die Initialisierungsliste wird verwendet, um einen Vektor mit sieben Komponenten zu erzeugen; Die Länge muss jetzt nicht ausdrücklich angegeben werden, da sie vom Compiler aus der Initialisierungsliste ablesbar ist.

Aufgabe:

In der main()-Methode wird die Zufallsfolge ausgegeben (Zeile 11 bis 14).

Schreiben Sie dafür geeignete Funktionen:

Rufen Sie diese Funktionen aus der main()-Methode auf.

Gültigkeitsbereich (scope)

Früher wurden bereits Gültigkeitsbereiche von Variablen besprochen (siehe Der Gültigkeitsbereich einer Variable (''scope'') im Kapitel Elementare Syntax: Schleifen). Diese Diskussion kann nun erweitert werden.

Variable in der Parameter-Liste einer Funktion: lokale Variable

Oben wurde schon gesagt, dass die Variablen in einer Parameter-Liste in der Deklaration nicht aufgeführt werden müssen (lediglich ihre Datentypen). Dagegen werden sie in der Implementierung benötigt. Zur Veranschaulichung diene das bereits bekannte Beispiel einer Implementierung von mean():

// Berechnet den Mittelwert eines Vektors (Zufallsfolge randomSequence)
double mean(vector<int> randomSequence)
{
    int number = randomSequence.size();
    int summe {0};
    for (int value : randomSequence)
    {
        summe += value;
    }
    return static_cast<double> (summe) / number;
}

Der Name randomSequence in der Parameter-Liste ist frei wählbar; er soll auf die Bedeutung der Variable hinweisen.

Auf die Variable randomSequence kann im gesamten Körper der Funktion mean() zugegriffen werden; man bezeichnet sie als lokale Variable behandelt. Ihr Gültigkeitsbereich ist lokal auf den Funktions-Körper beschränkt.

Wenn es eine andere Variable mit gleichem Namen gibt (zum Beispiel eine globale Variable, siehe folgender Abschnitt), so wird die andere Variable im Gültigkeitsbereich der lokalen Variable verdeckt. Für den Compiler ist also immer eindeutig definiert, welche Variable gerade mit ihrem Namen angesprochen wird, für den Menschen kann die doppelte Vergabe von Variablen-Namen schnell zu Fehlern führen.

Lokale Variablen im Körper einer Funktion

Oben wurde zum Beispiel die Funktion createRandomSequence() betrachtet:

// Erzeugt Zufallsfolge mit int-Zahlen von min bis max der Länge number
vector<int> createRandomSequence(int min, int max, int number)
{
    int diff = max - min + 1;

    vector<int> randomSequence(number);

    for (int i = 0; i < number; i++)
    {
        randomSequence[i] = (i % diff) + min;       // This is a FAKE! TODO: echte Zufallsfolge erzeugen!
        cout << randomSequence[i] << endl;
    }

    return randomSequence;
}

Die Variable diff ist lokal im Körper der Funktion createRandomSequence() definiert; ihr Gültigkeitsbereich ist also auf diesen Funktionskörper beschränkt. Anders formuliert: es ist nicht möglich, auf die Variable von außerhalb zuzugreifen.

Wenn die Abarbeitung des Funktionskörpers beendet ist (nach dem return-statement) und die Programm-Ausführung dorthin zurückkehrt, von wo aus die Funktion aufgerufen wurde, existiert die Variable diff nicht mehr.

Globale Variable

Im Abschnitt Trennung von Deklaration und Implementierung einer Funktion im Kapitel Strukturierte Programmierung mit Funktionen wurde anhand der Funktion mean() gezeigt, wie man sinnvoll Deklaration und Implementierung trennt, wenn beide in einer Datei enthalten sind. Diese Beispiel soll nun abgeändert werden durch eine Vereinfachung der Ausgabe des Mittelwertes. Dazu wird eine globale Variable eingeführt, die einen string (Zeichenkette) enthält. Weiter wird eine Funktion eingeführt, die die Ausgabe erleichtert.

#include <iostream>
using namespace std;

const string MW = "Mittelwert: ";                           // Globale Variable; Konstante

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

// Ausgabe eines bereits berechneten Mittelwertes
void printMean(double meanValue);                           // Deklaration der Funktion printMean()

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

    double meanValue = mean(x, y);                          // Aufruf der Funktion mean() mit zwei Variablen
    cout << MW << meanValue << endl;                        // MW kann in main() verwendet werden

    meanValue = mean(1, 7);                                 // Aufruf der Funktion mean() mit zwei Zahlen
    printMean(meanValue);
    return 0;
}

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

void printMean(double meanValue)
{
    cout << MW << meanValue << endl;                        // MW kann in printMean() verwendet werden
}

Zur Erklärung:

  1. Die Variable meanValue erscheint im Programm insgesamt zweimal: sie ist jeweils eine lokale Variable, die einmal innerhalb von main() definiert wird und einmal als Parameter der Funktion printMean() vorkommt. Es kann zu keiner Verwechslung kommen.
  2. Die Variable MW, eine string-Konstante (Zeile 4), wird außerhalb von allen Funktionen definiert und ist somit eine globale Variable, die überall verwendet werden kann: sowohl in main() (Zeile 18) als auch in printMean() (Zeile 33).

Dass die globale Variable MW eine Konstante ist, hat nichts mit ihrem Gültigkeitsbereich zu tun. Man kann das Beispiel auch mit einer echten Variable realisieren, wie das folgende Beispiel zeigt:

#include <iostream>
using namespace std;

string mw;                      // Globale Variable; keine Initialisierung: NICHT zu empfehlen

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

// Ausgabe eines bereits berechneten Mittelwertes
void printMean(double meanValue);                           // Deklaration der Funktion printMean()

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

    double meanValue = mean(x, y);                          // Aufruf der Funktion mean() mit zwei Variablen
    mw = "Mittelwert in main(): ";                          // mw kann in main() gesetzt werden
    cout << mw << meanValue << endl;                        

    meanValue = mean(1, 7);                                 // Aufruf der Funktion mean() mit zwei Zahlen
    printMean(meanValue);
    return 0;
}

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

void printMean(double meanValue)
{
    mw = "Mittelwert in printMean(): ";
    cout << mw << meanValue << endl;                        // mw kann in printMean() gesetzt werden
}

Jetzt ist die globale string-Variable mw nicht als Konstante definiert (Zeile 4) und kann später gesetzt werden. Dies geschieht sowohl innerhalb von main() (Zeile 18) als auch innerhalb von printMean() (Zeile 34).

Lokale static-Variable

Zwischen globalen und lokalen Variablen gibt es noch ein zunächst merkwürdig anmutendes Konstrukt: lokale static-Variable, die im Körper einer Funktion definiert werden.

Eine lokale static-Variable kann zum Beispiel eingesetzt werden, um mitzuzählen, wie oft eine Funktion aufgerufen wurde wie das folgende Beispiel zeigt; es werden wieder die Mittelwerte mit mean() berechnet. Neu ist, dass innerhalb von mean() eine Zählvariable counter definiert ist. Der Einfachheit halber sind mean() und main() jetzt wieder in einer Datei.

#include <iostream>
using namespace std;

// mean berechnet den Mittelwert zweier Zahlen x1 und x2
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()
{
    static int counter = 1;                 // lokale static-Variable

    double mean = (x1 + x2) / 2;
    
    cout << "Anzahl der Aufrufe von mean: " << counter << endl;
    counter++;                              // Hochzählen bei jedem Aufruf von mean()
    
    return mean;
}

In diesem Beispiel ist alles bekannt, neu sind lediglich die Zeilen 22, 26 und 27. In main() wird zweimal mean() aufgerufen, was mit Hilfe der Variable counter gezählt wird. Die Konsolen-Ausgabe lautet:

Anzahl der Aufrufe von mean: 1
Mittelwert: 5
Anzahl der Aufrufe von mean: 2
Mittelwert: 4

Dass die Variable counter zu Beginn der Ausführung von createRandomSequence() auf 1 gesetzt wird, geschieht nur beim ersten Aufruf, bei späteren Aufrufen wird dies ignoriert.