C/C++ Workshop - Teil 9
Inhalt:

1.) Was sind Funktionen
2.) Entwurf 1: while-Schleifen
3.) Entwurf 2: for-Schleifen
4.) Entwurf 3: Funktionen aufsplitten
5.) Entwurf 4: eine Klasse erstellen (Inline-Implementierung)
6.) Entwurf 5: Deklaration und Implementierung der Klasse trennen
7.) Entwurf 6: Klasse auf *.h und *.cpp Dateien verteilen

Autor:
Thomas Gern

Was sind Funktionen?
Funktionen sind ausgelagerte Programmteile die nur in bestimmten Stellen im Quellcode vom Programmierer aufgerufen werden und einen Wert zurückliefern. Beispiel kann man mit der Funktion
int addieren(int a, int b) { return (a + b); }
zwei ganze Zahlen (Integer) addieren und bekommt das Ergebnis der Addition zurückgeliefert. Funktionen die keine Werte zurückliefern werden als Unterprogramme verwendet. In Pascal bzw. Delphi spricht man hier von Prozeduren.
In diesem Kapitel wollen wir ein einfaches Beispiel behandeln und es immer weiter ausbauen.

Aufgabe:
Ein Programm entwerfen, das eine "Pyramide" folgender Art auf den Bildschirm zeichnet:

## Bitte Kopf um 90 Grad nach rechts kippen ##

*
* *
* * *
* * * *
* * * * *
* * * *
* * *
* *
*
## kippen_ende ##

Erster Entwurf: mit while-Schleifen

#include <iostream>

//------------------------------
void zeichnePyramide(int anzahl)
//------------------------------
{
    int i=1;
    while (i<anzahl)
    {
        for (int j=0; j<i; j++) cout << "* ";
        cout << endl;
        i++;
    }
    // i=anzahl;
    while (i>0)
    {
        for (int j=0; j<i; j++) cout << "* ";
        cout << endl;
        i--;
    }
}

//--------
int main()
//--------
{
    zeichnePyramide(5);
    return 0;
}

Im Hauptprogramm wird die Funktion

void zeichnePyramide(int anzahl)
als Funktion ohne Rückgabeparameter aufgerufen.
Die Funktion erledigt alle Berechnungen und Bildschirmausgaben, so dass wir nur die Funktion mit einem Parameter aufrufen müssen. Für die Berechnungen und Darstellung verwenden wir while-Schleifen.

Zweiter Entwurf: mit for-Schleifen

#include <iostream>

//------------------------------
void zeichnePyramide(int anzahl)
//------------------------------
{
    for (int i=0; i<anzahl; i++)
    {
        for (int j=0; j<i; j++) cout << "* ";
        cout << endl;
    }
    for (int i=anzahl; i>0; i--)
    {
        for (int j=0; j<i; j++) cout << "* ";
        cout << endl;
    }
}

//--------
int main()
//--------
{
    zeichnePyramide(5);
    return 0;
}

Im Unterschied zum vorherigen Beispiel haben wir hier die while-Schleifen durch for-Schleifen ersetzt.

int wert=10;

while (wert > 0)                     for (int wert=10;wert > 0; wert--)
{                                    {
    ...                                  ...
    wert--;                          }
}

Dritter Entwurf: Funktionen aufsplitten

#include <iostream>

//----------------------------
void zeichneSymbol(int anzahl)
//----------------------------
{
    for (int i=0; i<anzahl; i++) cout << "* ";
    cout << endl;
}

//------------------------------
void zeichnePyramide(int anzahl)
//------------------------------
{
    for (int i=0; i<anzahl; i++) zeichneSymbol(i);
    for (int i=anzahl; i>0; i--) zeichneSymbol(i);
}

//--------
int main()
//--------
{
    zeichnePyramide(5);
    return 0;
}

Die Funktion

void zeichnePyramide(int anzahl)
hat in den letzten beiden Beispielen die komplette Arbeit alleine erledigt. In diesem Beispiel habe wir die Funktion in ihre beiden Aufgaben zerlegt. Die im Hauptprogramm aufgerufene Funktion
zeichnePyramide(5);
berechnet nur noch wie viele Zeichen pro Zeile dargestellt werden müssen. Zum Zeichnen dieser Zeichen verwenden wir die Funktion
void zeichneSymbol(int anzahl)

Vierter Entwurf: Ein Klasse "Pyramide" erstellen

#include <iostream>

//------------
class Pyramide
//------------
{
    public:
        Pyramide():_anzahl(1) {}
        Pyramide(int i):_anzahl(i) {}

        //-------------
        void zeichnen()
        //-------------
        {
            for (int i=0; i<_anzahl; i++) zeichneSymbol(i);
            for (int i=_anzahl; i>0; i--) zeichneSymbol(i);
        }

    private:
        // Zählvariable, wieviele Symbole gezeichnet werden müssen
        int _anzahl;

        //----------------------------
        void zeichneSymbol(int anzahl)
        //----------------------------
        {
            for (int i=0; i<anzahl; i++) cout << "* ";
            cout << endl;
        }
};

//--------
int main()
//--------
{
    Pyramide MeinePyramidenVariable(5);
    MeinePyramidenVariable.zeichnen();
    return 0;
}

Jetzt haben wir die ganze Pyramiden-Funktionen in einer Klasse gekapselt. Alles was nach dem Bezeichner "public:" geschrieben wird, ist von ausserhalb der Klasse aufrufbar. Konstruktoren und Destruktoren sind Funktionen ohne Rückgabeparameter (auch nicht void !) und haben den gleichen Namen wie die Klasse in der sie stehen. Zur Unterscheidung zwischen Konstruktor und Destruktor beginnen die Destruktoren mit einer Tilde:

class FooBar {
public:
    FooBar() { ... }    // Konstruktor
    ~FooBar() { ... }   // Destruktor
}
Ein Konstruktor wird immer beim Erzeugen einer neuen Instanz aufgerufen und werden hier dazu benutzt um Werte vorzubelegen. Destruktoren werden dann aufgerufen, wenn unsere Variable vom Typ Pyramide nicht mehr benötigt wird (hier: Programmende). Jede Klasse besitzt einen Konstruktor und Destruktor, der Speicher vorbelegt bzw. wieder freigiebt. In unserem Beispiel benötigen wir keinen Destruktor.
Pyramide() : _anzahl(1) {}
Pyramide(int i) : _anzahl(i) {}
Dies sind die beiden in der Klasse Pyramide verwendeten Konstruktoren. Wird eine Variable mit
Pyramide MeineVariable;
erzeugt, wird der erste Konstruktor aufgerufen und die private-Variable _anzahl wird mit dem Wert 1 vorbelegt. Wird dagegen eine Variable mit
Pyramide NochEineVariable(10);
erzeugt, wird automatisch unser zweiter Konstruktor aufgerufen, der _anzahl mit einer Zahl vorbelegt (hier: 10).

Prinzipieller Aufbau einer Klasse:

class KlassenName {
    public:
        KlassenName() {}; // Konstruktor
        ~KlassenName() {}; // Destruktor
        ...

    protected:
        ...

    private:
        ...
}
Bei Klassen gibt es drei Bereiche (public, protected und private) in denen Funktionen und Variablen definiert werden können:
public:       Alle hier definierten Variablen und Funktionen sind von ausserhalb
              der Klasse nutzbar und direkt aufrufbar.

private:      Der Bereich private kennzeichnet Funktionen und Variablen die nur
              der Klasse selbst zur Verfügung stehen. Abgeleitete Klassen können hierauf
              nicht zugreifen.

protected:    Protected ist eine "Aufweichung" von private und erlaubt auch abgeleiteten
              Klassen den Zugriff auf hier definierte Variablen und Funktionen.
              Ansonsten wie private.

Fünfter Entwurf: Klasse enthält nur die Funktionsdeklarationen

#include <iostream>

//------------
class Pyramide
//------------
{
    public:
        Pyramide();
        Pyramide(int i);
        void zeichnen();

    private:
        int _anzahl;
        void zeichneSymbol(int anzahl);
};

//------------------
Pyramide::Pyramide():_anzahl(1) {}
//------------------

//------------------------
Pyramide::Pyramide(int i):_anzahl(i) {}
//------------------------

//-----------------------
void Pyramide::zeichnen()
//-----------------------
{
    for (int i=0; i<_anzahl; i++) zeichneSymbol(i);
    for (int i=_anzahl; i>0; i--) zeichneSymbol(i);
}

//--------------------------------------
void Pyramide::zeichneSymbol(int anzahl)
//--------------------------------------
{
    for (int i=0; i<anzahl; i++) cout << "* ";
    cout << endl;
}

//--------
int main()
//--------
{
    Pyramide MeinePyramidenVariable(5);
    MeinePyramidenVariable.zeichnen();
    return 0;
}

Im Gegensatz zu Entwurf 4 haben wir jetzt die Implementierung der Funktionen ausserhalb der Klasse vorgenommen. Die Klasse selbst enthält nur noch die Funktionsdeklarationen. Da wir die Funktionen jetzt nicht mehr innerhalb der Klasse definieren, muss bei jeder Funktions-Implementierung der Klassenname angegeben werden:

//------------
class Pyramide
//------------
{
    public:
        ...
        void zeichnen();

    private:
        ...
}

//-----------------------
void Pyramide::zeichnen()
//-----------------------
{
    ...
}
Als weiteres kann man die Klassendeklaration sammt zugehöriger Funktionsimplementierung
  • in eine eigene Datei schreiben (z.B. Pyramide.cls)
  • in jeweils eine eigene Datei schreiben (Pyramide.h für die Klassendeklaration und Pyramide.cpp für die Funktionsimplementierungen)
In jedem Fall muß man im Hauptprogramm die entsprechende Datei dann einbinden. Dies geschieht mit der Anweisung
#include "Pyramide.cls"
bzw.
#include "Pyramide.h"
Sechster Entwurf: pyramide.h

#ifndef _pyramide_h
#define _pyramide_h

//------------
class Pyramide
//------------
{
    public:
        Pyramide();
        Pyramide(int i);
        void zeichnen();

    private:
        int _anzahl;
        void zeichneSymbol(int anzahl);
};
#endif

Sechster Entwurf: pyramide.cpp

#ifndef _pyramide_h
#include "pyramide.h"
#endif

#include <iostream>

//------------------
Pyramide::Pyramide():_anzahl(1) {}
//------------------

//------------------------
Pyramide::Pyramide(int i):_anzahl(i) {}
//------------------------

//-----------------------
void Pyramide::zeichnen()
//-----------------------
{
    for (int i=0; i<_anzahl; i++) zeichneSymbol(i);
    for (int i=_anzahl; i>0; i--) zeichneSymbol(i);
}

//--------------------------------------
void Pyramide::zeichneSymbol(int anzahl)
//--------------------------------------
{
    for (int i=0; i<anzahl; i++) cout << "* ";
    cout << endl;
}

Mit den farblich hervorgehobenen Quelltextzeilen wird ein mehrfaches includieren verhindert:

/* pyramide.h */
#ifndef _pyramide_h
#define _pyramide_h
    ...
#endif


/* pyramide.cpp */
#ifndef _pyramide_h
#include "pyramide.h"
#endif
    ...
Üblicherweise verwendet man den Klassennamen, eingekleidet in Underscore: _klassenname_h um die Mehrfachincludierung zu vermeiden. Unser Hauptprogramm reduziert sich dann auf folgendes:

Sechster Entwurf: Hauptprogramm

#include "pyramide.h"

//--------
int main()
//--------
{
    Pyramide MeinePyramidenVariable(5);
    MeinePyramidenVariable.zeichnen();
    return 0;
}

Kommandoaufruf zum Compilieren


Für die Beispiele 1..5
c++ main.cpp -o main.exe

Für Beispiel 6
c++ pyramide.cpp main.cpp -o main.exe

alternativ kann man sich auch ein Makefile basteln:

Makefile


# Makefile
# Erstellt am 03.09.1999
#-------
VERSION  = 1.0
COMPILER = c++
LINKER   = c++
PROJECT  = test.exe
SRCFILES = main.cpp
OBJFILES = $(SRCFILES:.cpp=.o)
INCLUDES = -I.
DEFINES  =
CFLAGS   = -ansi -malign-double -mcpu=i686 -Os -Wall
LFLAGS   = -s
#-------
.cpp.o:
	$(COMPILER) $(DEFINES) $(CFLAGS) $(INCLUDES) -o $@ -c $<

ALL: $(OBJFILES)
	$(LINKER) $(LFLAGS) $(OBJFILES) -o $(PROJECT)

clean:
	rm *.o

distclean: clean
	rm *.exe

Feedback ist wichtig für die Verbesserung des Service
Letzte Änderung: Thomas Gern
Datum: 17. September 1999 - pro-Linux.de; (c) Pro-Linux