Skript

`"
Programmieren III
(Entwurf)
WS 03/04 Prof. Dr. Sven Eric Panitz
TFH Berlin
Version Mar 17, 2004
Die vorliegende Fassung des Skriptes ist ein roher Entwurf für die Vorlesung des kommenden Semesters und wird im Laufe der Vorlesung erst seine endgültige Form finden. Kapitel können dabei umgestellt, vollkommen revidiert werden oder gar ganz wegfallen.
Dieses Skript entsteht begleitend zur Vorlesung des WS 03/04 vollkommen neu. Es stellt somit eine Mitschrift der Vorlesung dar. Naturgemäß ist es in Aufbau und Qualität nicht mit einem Buch vergleichbar. Flüchtigkeits- und Tippfehler werden sich im Eifer des Gefechtes nicht vermeiden lassen und wahrscheinlich in nicht geringem Maße auftreten. Ich bin natürlich stets dankbar, wenn ich auf solche aufmerksam gemacht werde und diese korrigieren kann.
Die Vorlesung setzt die Kerninhalte der Vorgängervorlesungen [Pan02] und [Pan03b] voraus.
Der Quelltext dieses Skripts ist eine XML-Datei, die durch eine XQuery in eine LATEX-Datei transformiert und für die schließlich eine pdf-Datei und eine postscript-Datei erzeugt wird. Der XML-Quelltext verweist direkt auf ein XSLT-Skript, das eine HTML-Darstellung erzeugt, so daß ein entsprechender Browser mit XSLT-Prozessor die XML-Datei direkt als HTML-Seite darstellen kann. Beispielprogramme werden direkt aus dem Skriptquelltext extrahiert und sind auf der Webseite herunterzuladen.

Contents

1  Die Programmiersprache C++
    1.1  Geschichtlicher Abriss
    1.2  Compiler und Werkzeuge
        1.2.1  Editoren
        1.2.2  Compiler
    1.3  Kompilieren und Ausführen
        1.3.1  Das übliche Programm: hello world
        1.3.2  Kompilieren
    1.4  Funktionen und Variablen
        1.4.1  Schreiben von Funktionen
        1.4.2  Variablen
2  Typen
    2.1  Zeiger und Referenzen
        2.1.1  Der Speicher
        2.1.2  Zeiger
        2.1.3  Funktionszeiger
        2.1.4  Synonyme Typnamen
        2.1.5  Referenzen
    2.2  Konstruierte Typen
        2.2.1  Aufzählungen
        2.2.2  Reihungen (Arrays)
        2.2.3  Produkte und Summen von Typen
3  Generische und Überladene Funktionen
    3.1  Generische Funktionen
        3.1.1  Heterogene und homogene Umsetzung
        3.1.2  Expizite Instanziierung des Typparameters
        3.1.3  Generizität für Reihungen
    3.2  Überladen von Funktionen
    3.3  Standardparameter
4  Objektorientierte Programmierung in C++
    4.1  Klassen
        4.1.1  Klassendeklaration
        4.1.2  Header-Dateien für Klassen
        4.1.3  Konstruktoren
        4.1.4  Destruktoren
        4.1.5  Konstantendeklarationen
        4.1.6  Ableiten
        4.1.7  protected
        4.1.8  Überladen von Operatoren
        4.1.9  Generische Formularklassen
        4.1.10  Closures, Funktionsobjekte und Schönfinkeleien
    4.2  Die Standard Template Library
        4.2.1  ein kleines Beispiel
        4.2.2  Konzepte
        4.2.3  Iteratoren
        4.2.4  Behälterklassen
        4.2.5  Algorithmen
    4.3  Ausnahmen
    4.4  Namensräume
5  Graphik und GUI Programmierung
    5.1  3 dimensionale Graphiken mit OpenGL
        5.1.1  Eine Zustandsmaschine
        5.1.2  Eine kleine GUI Bibliothek: GLUT
        5.1.3  Vertexes und Figuren
        5.1.4  Nah und Fern
        5.1.5  Vordefinierte Figuren
        5.1.6  Bei Licht betrachtet
        5.1.7  Transformationen
        5.1.8  GLUT Tastatureingabe
        5.1.9  Beispiel: Tux der Pinguin
A  Gtk für C++ Beispielprogramme
    A.1  Ein einfacher Zähler
        A.1.1  Die Kopfdatei
        A.1.2  Implementierung
        A.1.3  Beispielaufruf
    A.2  Einfaches Zeichnen
    A.3  Das Telespiel: Ping
B  Gesammelte Aufgaben

Einführung

Ziel der Vorlesung

Mit den Grundtechniken der Programmierung am Beispiel von Java wurde in der Vorgängervorlesungen vertraut gemacht. Jetzt ist es an der Zeit, die gelernten Konzepte in einer anderen objektorientierten Programmiersprache umzusetzen. Hierzu wird die Sprache C++ benutzt. C++ abstrahiert viel weniger als Java von der real zugrundeliegenden Maschine; auch ist der Sprachumfang von C++ wesentlich umfangreicher; eher ein Nachteil, insbesondere für Neulinge, die mit einer Vielzahl konkurrierender Konzepte in ein und derselben Sprache konfrontiert werden.
In einer zweistündigen einsemestrigen Vorlesung kann nicht die ganze Programmiersprache C++ gelehrt werden. Wir werden uns darauf konzentrieren, einige interessante Unterschiede zu Java herauszuarbeiten und die wesentlichen Konzepte zu zeigen. Mit Chance entsteht eine Einführung C++-kompakt, oder wie die Angelsachsen es gerne nennen: in a nutshell. Für Detailfragen ist jeweils ein umfassendes Lehrbuch zu konsultieren. Eine ausführliche kommentierte Literaturliste findet sich im Skript von Herrn Grude [Gru01]. Ein relativ kompaktes und preisgünstiges Lehrbuch, das mir bei der Vorbereitung geholfen hat, ist [NH02]. Eine schöne Einführung in C ohne C++ findet sich auch im zweiten Teil des Skripts Programmieren II für den Studiengang Technische Informatik von Frau Ripphausen-Lipa [RL03]. Viele C++ Kurse finden sich im Netz. Beispielhaft sei [Wie99] hier erwähnt.

Aufbau der Vorlesung

Aus Java bekannte Konstrukte wie zusammengestzte Befehle, Schleifen etc. werden nicht erklärt werden. Die Vorlesung konzentriert sich auf die Besonderheiten von C. Hierzu gehören insbesondere imperative Konzepte und Zeiger. Daher werden im ersten Kapitel C++ Konstrukte, die nicht der Objektorientierung zugehörig sind, vorgestellt. Dabei werden insbesondere auch Konstrukte genauer unter die Lupe genommen, die gleich bekannten Konzepten aus Java scheinen, aber ein anderes Verhalten an den Tag legen. Im zweiten Kapitel wird dann vorgestellt, in wieweit sich die in Java eingeübten objektorientierten Techniken in C++ umsetzen lassen. Schließlich ist ein drittes Kapitel geplant, in dem ein Beispiel für graphische und GUI Programmierung vorgestellt wird. Hier könnten GTK und/oder OpenGL zum Einsatz kommen.
Im Laufe dieses Skriptes werden wir etwas lax mit der Bezeichnung C sein. Wenn wir von C sprechen werden wir in der Regel auch C++ meinen. Nur in speziellen Fällen wird auf eine genaue Unterscheidung zwischen C und C++ Wert gelegt.

Chapter 1
Die Programmiersprache C++

1.1  Geschichtlicher Abriss

Die Programmiersprache C wurde anfang der 70er Jahre von Dennis M. Richie an den Bell Laboratories entworfen. Eine Vorläufersprache von C trug den Namen B, die im Gegensatz zu C allerdings vollkommen ungetypt war. C erfreute sich bald großer Beliebtheit als Alternative zur Programmiersprache Fortran der damals allmächtigen Firma IBM. Viele Programmierer empfanden Fortran als eine zu große Einschränkung ihrer Freiheit. C entwickelte sich als die Programmiersprache der Wahl für Unix-Programmierer. Die meisten Tools in Unix sind in C geschrieben1 und Unix selbst ist zu über 90% in C implementiert. Erst 1989 erfolgte eine Standartisierung von C durch das ANSI X3J11 Komitee.
C++ erweitert die Programmiersprache C um objektorientierte Konstrukte, wie sie zuvor z.B. in der Programmiersprache Smalltalk[Ing78] eingeführt wurden. Die Erweiterung von C ist dabei so vorgenommen worden, daß weitgehendst jedes C-Programm auch ein C++-Programm ist.
Ein wichtiges Entwurfskriterium von C ist, den Programmierer in keinster Weise einzuschränken. Man kann lapidar sagen: alles ist erlaubt, der Programmierer weiß, was er tut. Dieses geht zu Lasten der Sicherheit von Programmen. Ein C-Compiler erkennt weniger potentielle Programmierfehler als z.B. ein Javacompiler oder gar ein Haskellcompiler[Jon03]. Das hat zur Folge, daß fehlerfrei kompilierte Programme häufig unerwartete Laufzeitfehler haben. Solche Fehler können schwer zu finden sein. Um das Risiko solcher Fehler zu verringern ist es notwendig in der Programmierung wesentlich strengere Programmierrichtlinien einzuhalten als z.B. in Java, oder in Haskell, wo der Programmierer noch nicht einmal die Typen von Funktionen in einer Signatur festlegen muß. Der Compiler ist in der Lage die Signatur selbst herzuleiten. In C hingegen empfiehlt es sich, sogar im Namen einer Variablen kenntlich zu machen, um was für einen Typ es sich bei ihr handelt.
Mit der vollen Kontrollen in C erhält ein Programmierer allerdings auch die Macht, sein Programm möglichst optimiert zu schreiben. Inwiefern ein C-Programmierer heutzutage allerdings die Fähigkeiten hat, diese Macht auszuspielen gegenüber einen klug optimierenden Compiler, sei dahingestellt.
Von diesen Entwurfskriterium wurde auch nicht bei der Erweiterung von C nach C++ abgegangen.
Java im Gegensatz versucht mehr von der Maschine zu abstrahieren und ein logisch konsistentes Bild nach außen zu präsentieren. In Java ist nicht alles erlaubt, was gefällt. Allerdings steht Java syntaktisch sehr in der Tradition von C. Daher werden wir als Javaprogrammierer kaum Schwierigkeiten haben, viele der C Konstrukte zu lesen. Schleifenkonstrukte und ähnliche zusammengesetzte Befehle sehen in beiden Sprachen fast vollkommen identisch aus. Trotzdem ist, wie immer zwischen zwei äußerlich ähnlichen Sprachen, vor falschen Freunden zu warnen. Als falsche Freunde bezeichnet man Wörter, die man aus einer Sprache kennt, die aber in einer anderen Sprache eine andere Bedeutung haben. So heißt das englische eventually eben nicht eventuell sondern schließlich oder das englische pregnant hat eine Bedeutung, die selten in einem Fachtext der Informatik gebraucht wird.
Die Programmiersprache C verfolgt in vielen Fällen einen gewissen Pragmatismus. Bestimmte Konvertierungen von Typen werden unter der Hand vorgenommen, insbesondere gilt dies für Zeiger und Referenztypen. So praktisch solch ein Umgang im Alltag sein kann; so stellt dieser Pragmatismus für den Anfänger oft eine starke Hürde da. Es gibt kein einfaches mathematisches Grundkonzept für die Übersetzung und Ausführung von C Programmen. Sehr viele Ausnahmen für bestimmte Typen und Konstrukte müssen erlernt werden. In der Praxis, so hässlich dieses Konglomerat aus Pragmatismus und Ausnahmen, kleinen Tricks und Fallstricken für den Theoretiker sein mag, hat sich C durchgestzt und ist derzeit und sicherlich noch auf lange Jahre hin, als das Zentrum des Programmieruniversums anzusehen. Wohl kaum eine zweite Programmiersprache kann mit so vielen Bibliotheken und Beispielprogrammen aufwarten wie C. Didaktisch hingegen ist C für eine Einführung in die Programmierung vollkommen ungeeignet; daher fanden von jeher Programmiereinführungen auf andere Sprachen statt: Pascal, Modula zeitweise oft Algol68 und Scheme oder seit wenigen Jahren Java.

1.2  Compiler und Werkzeuge

1.2.1  Editoren

Ähnlich wie für Java gibt es natürlich auch für C eine Vielzahl mächtiger integrierter Entwicklungsumgebungen. Für den Anfänger empfiehlt es sich zunächst einmal auf diese zu verzichten und lediglich mit einem Texteditor und einem Kommandozeilenübersetzer zu beginnen. Eine Entwicklungsumgebung setzt oft viel implizites Wissen über die Sprache und ihr Ausführungs-/Übersetzungsmodell voraus.
Die meisten allgemeinen Texteditoren haben einen Modus für C Programme, so daß sie syntaktische Konstrukte erkennen und farblich markieren. Hier steht natürlich an erster Stelle der allgegenwärtige Editor emacs, der sowohl für Windows- als auch für unixartige Betriebssysteme zur Verfügung steht.

1.2.2  Compiler

Anders als in Java, wo fast ausschließlich ein Compiler benutzt wird, nämlich der Compiler javac der Firma Sun, stehen für C viele verschiedene Übersetzer zur Verfügung. Wir stellen die drei gebräuchlichsten hier vor.

gcc

gcc ist der C und C++ Compiler von Gnu. gcc steht auf Linux standardmäßig zur Verfügung und ist in der Regel auf Unixsystemen installiert. g++ ist ein Befehl, der den C Compiler mit einer Option aufruft, so daß C++ Quelldateien verstanden werden. gcc steht kostenlos zur Verfügung.
gcc ist schon längst nicht mehr allein ein C-Compiler sondern eine Sammlung von unterschiedlichen Übersetzern. So kann mit gcc neben C++ und C mitlerweile auch Java kompiliert werden. Der entsprechende Aufruf hierzu ist gcj. gcj ist nicht nur in der Lage Klassendateien für Java-Quelltext zu erzeugen, sondern auch platformabhängige Objektdateien.
cygwin   gcc steht auch dem Windowsprogrammierer zur Verfügung. Unter der Bezeichnung cygwin kann man auf einem Windowssystem alle gängigen Entwicklungstools von Linux auch auf Windows installieren. Damit läßt sich auf Windows exakt wie auf Unix arbeiten. cygwin ist kostenfrei und kann vom Netz (www.cygwin.com) heruntergeladen werden.

Borland

Die Firma Borland hat eine C++-Entwicklungsumgebung. Der Compiler wird für nichtkomerzielle Anwender von der Firma Borland frei zur Verfügung gestellt. Auch er kann vom Netz (www.borland.com/bcppbuilder/freecompiler) heruntergeladen werden.

Microsoft

Schließlich bietet Microsoft unter den Namen Visual C++ für sein Windowsbetriebssystem einen C Entwicklungsumgebung an. Zur Benutzung dessen Compilers ist allerdings ein Lizenzvertrag von Nöten.
Die Beispiele dieses Skriptes sind alle mit gcc Version 3.3.1 auf Linux übersetzt worden. Einige Programmen wurden mit gcc zusätzlich auch unter Windows getestet.

1.3  Kompilieren und Ausführen

C unterscheidet sich sowohl in der Art der Übersetzung als auch in der Ausführung vollkommen von Java.

1.3.1  Das übliche Programm: hello world

Wie in den meisten Programmierkursen, wollen wir uns auch nicht um das übliche hello world Programm drücken und geben hier die C++-Version davon an.
Hello0
#include  <iostream> 
 

int main(){
  std::cout << "Hallo Welt\n";
}

Das Programm speichern wir in einer Datei mit der Endung .cpp ab. Alternative gebräuchliche Endungen sind: .cc, .C, .cxx und .c++.
Auffällig ist, daß der Name des Programms (Hello0) im Programmtext nicht auftaucht. Anders als in Java, indem jeder Quelltextdatei eine Klasse mit dem Namen der Datei enthält. In C folgen direkt nach dem Import von Bibliotheken die Funktionen. Es muß keine Klasse wie in Java definiert werden.
Wir sehen, daß zunächst einmal die Bibliothek für Ein- und Ausgabeströme zu importieren ist: #include <iostream>. Auf derartige Zeilen, die mit den Zeichen # beginnen, werden wir noch genauer eingehen.
Die Hauptmethode läßt sich ebenso schreiben wie in Java. Sie hat allerdings den Rückgabetyp int.2 Auf das aus Java bekannte Argument (String [] args) kann in C++ verzichtet werden. Obwohl die Methode einen Rückgabetyp verlangt, braucht die Hauptmethode keinen return-Befehl.
Ein- und Ausgabe wird in C++ über Operatoren ausgedrückt. Zur Ausgabe gibt es den Operator <<. std::out ist der Ausgabestrom für die Konsole. Der Operator << hat als linken Operanden einen Ausgabestrom und als rechten Operanden ein Argument, daß auf dem Strom ausgegeben werden soll.
Stringliteralen können wie in Java durch Anführungszeichen markiert ausgedrückt werden.
Wie in Java endet auch in C++ jeder Befehl mit dem hässlichen Semikolon.
Der Operator << liefert als Ergebnis einen Ausgabestrom, so daß elegant eine Folge von Ausgaben durch eine Kette von << Anwendungen ausgedrückt werden kann.
Hello1
#include <iostream>

int main(){
  std::cout << "Hallo " << "Welt" << std::endl;
}

1.3.2  Kompilieren

Zur Übersetzung dieses einfachen Programmes ist der C-Compiler gcc mit der Quelltextdatei als Argument aufzurufen. Als zusätzliches Argument kann mit der Option -o der Name des zu erzeugenen ausführbaren Programms mit angegeben werden. Das erzeugte Programm kann anschließend direkt ausgeführt werden:
sep@swe10:~/fh/prog3/examples> g++ -o Hello1 Hello1.cpp
sep@swe10:~/fh/prog3/examples> ./Hello1
Hallo Welt
sep@swe10:~/fh/prog3/examples>

1.4  Funktionen und Variablen

Die klassischen Strukturierungsmöglichkeiten einer Programmiersprache sind Funktionen und Variablen, oder Methoden und Felder, wie wir sie in Java genannt haben. Natürlich gibt es beides auch in C.

1.4.1  Schreiben von Funktionen

In den ersten beiden Programmen haben wir schon eine Funktion geschrieben, die Funktion main. Wie zu sehen war unterscheidet sich eine Funktionsdefinition in C kaum von einer Methode in Java. Das Attribut static wie wir es aus Java für statische Methoden kennen, wird in C nicht gesetzt. In C ist der Standardfall, daß eine Funktion statisch ist. Erst in C++ können Klassen mit nicht statischen Objektmethoden definiert werden.
Wir können versuchen ein Programm mit einer zweiten Funktion in C zu schreiben:
Hello2
#include <iostream>
#include <string>

std::string getHallo(){
  return "hallo welt\n";
};

int main(){
  std::cout <<getHallo();
}

Wir definieren, die parameterlose Funktion getHallo und rufen sie in der Funktion main auf. Man beachte, daß der Typ string in C im Gegensatz zu Java klein geschrieben wird.
Von Java sind wir gewohnt, daß die Reihenfolge der Methoden in einer Klasse beliebig ist. Der Kompiler hat jeweils bereits die ganze Quelldatei studiert, bevor er die Rümpfe der Methoden übersetzt. Wir können versuchen, ob dieses für C auch gilt. Hierzu vertauschen wir die Reihenfolge der beiden Funktionen im letzten Programm.
Hello3
#include <iostream>
#include <string>

int main(){
  std::cout << getHallo();
}

std::string getHallo(){
  return "hallo welt\n";
}

Der Versuch dieses Programm zu übersetzen führt zu einem Fehler:
sep@swe10:~/fh/prog3/examples> g++ Hello3.cpp
Hello3.cpp: In function `int main(...)':
Hello3.cpp:6: implicit declaration of function `int getHallo(...)'
sep@swe10:~/fh/prog3/examples>

Offensichtlich können in C Funktionen nur benutzt werden, nachdem sie vorher im Programm definiert wurden. Diese Einschränkung hat sicherlich mit den Stand der Compilerbautechnik am Anfang der 70er Jahre zu tun. Ein Compiler, der nur einmal sequentiell das Programm von vorne nach hinten durchgeht, läßt sich leichter bauen, als ein Compiler, der mehrmals über eine Quelldatei gehen muß, um nacheinander verschiedene Informationen zu sammeln.
Wollen wir die Reihenfolge der beiden Funktionen, wie wir sie in der Datei Hello3.cpp geschrieben haben, beibehalten, so können wir das in C, indem wir die Signatur der Funktion getHallo vorwegnehmen. Dieses sieht dann ähnlich aus, wie eine Signatur in einer Javaschnittstelle. Man kann in C in ein und derselben Datei die Signatur einer Funktion deklarieren und später in der Datei diese Funktion erst implementieren.
Hello4
#include <iostream>
#include <string>

std::string getHallo();

int main(){
  std::cout << getHallo();
}

std::string getHallo(){
  return "hallo welt\n";
}

Jetzt übersetzt das Programm wieder fehlerfrei. Mit dieser Technik simuliert man gewissermaßen, was ein Javacompiler sieht, wenn er mehrfach über einen Quelltext geht. Zunächst sieht er die Signaturen aller Funktionen und erst dann betrachtet er ihre Rümpfe. Sobald die Signatur einer Funktion bekannt ist, kann die Funktion benutzt werden.

Header-Dateien

Nach dem geraden gelernten, scheint es sinnvoll in einem C Programm zunächst die Signaturen aller Funktionen zu sammeln und dann die einzelnen Funktionen zu implementieren. Die Sammlung der Signaturen ergibt eine schöne Zusammenfassung aller von einem Programm angebotener Funktionen. Es liegt nahe, diese in eine getrennte Datei auszulagern. Hierzu bedient man sich in C der sogenannten header-Dateien, die mit der Dateiendung .h abgespeichert werden.
Für unser letztes Beispiel können wir eine header-Datei schreiben, in der die Signatur der Funktion getHallo steht:
Hello5
#include <string>

std::string getHallo();

Statt jetzt in einem Programm diese Signatur am Anfang des Programms zu schreiben, sagen wir dem Compiler, er soll die header-Datei mit in das Programm einfügen. Hierzu benutzen wir das #include Konstrukt. Unser Programm sieht dann wie folgt aus:
Hello5
#include <iostream>
#include <string>

#include "Hello5.h"

int main(){
  std::cout << getHallo();
}

std::string getHallo(){
  return "hallo welt\n";
}

Wichtig ist hier zu verstehen, daß mit dem include, bevor der Compiler anfängt das Programm zu übersetzen, die Datei Hello.h komplett wörtlich an der entsprechenden Stelle eingefügt wird. Dieses Einfügen wird nicht vom eigentlichen Compiler gemacht, sondern vom einem Programm, das vor dem Compiler den Quelltext manipuliert, den sogenannten Präprozessor. Der Präprozessor liest die Zeilen des Quelltext, die mit dem Zeichen # beginnen. Diese nennt man Direktiven. Sie sind nicht Teil des eigentlichen Programms, sondern erklären dem Präprozessor, wie das eigentliche Programm zusammenzusetzen ist.

Objektdateien und Linker

Wer unsere bisherigen Programme übersetzt hat, wird festgestellt haben, daß Dateien mit der Endung .o ins Dateisystem geschrieben wurden. Diese Dateien heißen Objektdateien. Der eigentliche Compiler erzeugt nicht das ausführbare Programm, sondern Objektdateien. Die Objektdateien werden dann erst durch den sogenannten Linker zu ausführbaren Dateien zusammengefasst.
Mit der Option -c kann man den Compiler dazu bringen, nur die Objektdatei zu erzeugen und nicht anschließend noch das ausführbare Programm zu erzeugen.
Der Compiler kann Objektdateien erzeugen, in denen noch nicht jede Funktion implementiert ist. Wir können das letzte Programm schreiben, ohne die Funktion getHallo implementiert zu haben:
Hello6
#include <iostream>
#include "Hello5.h"

int main(){
  std::cout << getHallo();
}

Mit der Option -c kann der Compiler fehlerfrei die Objektdatei erzeugen.
sep@swe10:~/fh/prog3/examples> g++ -c Hello6.cpp
sep@swe10:~/fh/prog3/examples> ls -l Hello6.*
-rw-r--r--    1 sep      users          84 Sep  3 17:36 Hello6.cpp
-rw-r--r--    1 sep      users        8452 Sep  3 17:40 Hello6.o
sep@swe10:~/fh/prog3/examples>

Wollen wir hingegen ein ausführbares Programm erzeugen, so bekommen wir einen Fehler, und zwar nicht vom Compiler sondern vom Linker. Der Linker versucht jede deklarierte Funktion mit konkreten Programmcode aufzulösen. Wenn er einen solchen Code nicht findet, so meldet er einen Fehler:
sep@swe10:~/fh/prog3/examples> g++ Hello6.cpp
/tmp/ccRKWUqK.o: In function `main':
/tmp/ccRKWUqK.o(.text+0xe): undefined reference to `getHallo(void)'
collect2: ld returned 1 exit status
sep@swe10:~/fh/prog3/examples>

Um einen solchen Fehler zu produzieren, bedarf es keiner header-Datei. Wir können in einem Programm eine Funktion deklarieren ohne sie zu implementieren.
Hello7
#include <iostream>
#include <string>

std::string getHallo();

int main(){
  std::cout << getHallo();
}

Wir bekommen wieder einen Fehler des Linkers:
sep@swe10:~/fh/prog3/examples> g++ Hello7.cpp
/tmp/ccdm3ngO.o: In function `main':
/tmp/ccdm3ngO.o(.text+0xe): undefined reference to `getHallo(void)'
collect2: ld returned 1 exit status
sep@swe10:~/fh/prog3/examples>

Der Linker hat im Gegensatz zum Compiler meherere Dateien als Eingabe, die er gemeinsam verarbeitet.
Wie man sieht, sind ein paar Dinge in C zu beachten, über die man sich in Java keine Gedanken machen mußte. Das liegt primär darin begründet, daß ein C-Compiler immer nur genau eine Datei übersetzt, in der alle Information für das Programm enthalten sein müssen; und daß der Compiler nur genau einmal den Quelltext sequentiell liest.
Aufgabe 1   Übersetzen Sie die Programme aus diesem Abschnitt und lassen Sie sie laufen.
Aufgabe 2   Schreiben Sie die rekursive Funktion der Fakultät. Hierzu brauchen Sie den if-Befehl, wie Sie ihn aus Java kennen. Testen Sie die Funktion mit ein paar Werten.

1.4.2  Variablen

Variablen lassen sich in C genauso deklarieren und benutzen wie in Java. Dem Variablennamen wird bei der Deklaration der Typ vorangestellt. Das Gleichheitszeichen fungiert als der Zuweisungsoperator. Es können Variablen lokal für eine Funktion oder einen anderen Block deklariert werden, oder aber global innerhalb eines Programmes.
Vars
#include <iostream>

int i = 2;

void f(int j){
  int k = 2*j;
  i=i+k;
}

int j = 2;

int main(){
  std::cout << i <<  std::endl;
  f(15);
  std::cout << i <<  std::endl;
  f(5);
  std::cout << i <<  std::endl;
  std::cout << j <<  std::endl;
}

Mehrfach inkludieren

Genauso wie Funktionen können Variablen können auch in Headerdateien deklariert sein.
C1
int x =0;

Ebenso können Headerdateien weitere Headerdateien inkludieren:
C2
#include "C1.h"

Hier kann es zu einem Fehler kommen, wenn beide diese Headerdateien auf die eine oder andere Art in eine Datei inkludiert wird.
C3
#include "C1.h"
#include "C2.h"

int main(){};

Der Versuch dieses Programm zu übersetzen führt zu einem Fehler.
sep@linux:~/fh/prog3/examples/src> g++ -c C3.cpp
In file included from C2.h:1,
                 from C3.cpp:2:
C1.h:1: error: redefinition of `int x'
C1.h:1: error: `int x' previously defined here
sep@linux:~/fh/prog3/examples/src>


Um solche Fehler, die entstehen, wenn ein und dieselbe Datei mehrfach inkludiert wird, von vorneherein auszuschließen, bedient man sich standardmäßig einer weiteren Direktive. Mit der Direktive #define können Werte definiert werden. Mit der Direktive #ifndef kann abgefragt werden, ob ein Wert bereits definiert wurde. Nun kann man in einer Headerdatei eine für diese Datei spezifische Variable definieren. Üblicher Weise bildet man diese mit zwei Unterstrichen, gefolgt vom komplett in Großbuchstaben geschriebenen Dateinamen und der Endung _H. Den kompletten Inhalt der Headerdatei klammert man nun in einer #ifndef-Direktive. So wird der Inhalt einer Headerdatei nur dann eingefügt, wenn die spezifische Variable noch nicht definiert wurde. Also höchstens einmal.
Beherzigen wir diese Vorgehensweise, so erhalten wir statt der Datei C1.h von oben, eine entsprechende Datei, die wir zur Unterscheidung als D.h speichern.
D1
#ifndef __D1_H

#define __D1_H ;

int x =0;

#endif /* __D1_H */

Die Variable, die markiert, ob die Datei schon inkludiert wurde, heißt nach unserer entsprechenden Konvention __D1_H.
Diese Datei läßt sich jetzt woanders inkludieren.
D2
#include "D1.h"

Eine mehrfache Inklusion führt jetzt nicht mehr zu einen Fehler.
D3
#include "D1.h"
#include "D2.h"

int main(){};

Das Programm läßt sich jetzt fehlerfrei übersetzen:
sep@linux:~/fh/prog3/examples/src> g++ -o D3 D3.cpp
sep@linux:~/fh/prog3/examples/src> ./D3

In größeren C-Projekten kann man diese Vorgehensweise standardmäßig angewendet sehen.

Chapter 2
Typen

2.1  Zeiger und Referenzen

In Java gab es im Prinzip nur zwei unterschiedliche Arten von Typen: Objekte und primitive Typen3. Damit schottet uns die Sprache Java von dem eigentlichen technischen Speichermodell eines von Neumann Rechners ab. Um die Unterschiede der verschiedenen Typen in C++ verstehen zu können, müssen wir uns ein wenig konkreter mit dem Speichermodell eines Rechners beschäftigen.

2.1.1  Der Speicher

Der Hauptspeicher eines Computers besteht aus einzelnen durchnummerierten Speicherzellen. Im Prinzip stehen in jeder Speicherzelle nur Zahlen. Als was diese Zahlen interpretiert werden, hängt von dem Programm ab, das die Zahl in der Speicherzelle benutzt. Dabei kann die Zahl interpretiert werden als:
Die letzten beiden Interpretationen der Zahl in einer Speicherzelle schlagen sich nicht auf die Programmiersprache Java durch. Es gibt keine Datentypen, die die Adresse einer Speicherzelle darstellen und es gibt keinen Datentyp, der für den Code einer Methode steht. In C findet sich dieses wieder.
Folgende kleine Tabelle illustriert den Speicher. Wir sehen die Speicherzellen 1017 bis 1026 und die angedeutete Bedeutung des Inhalts dieser Speicherzellen.
...  ...  
101742
10181025
1019pop
1020load r1
102215
1023x
10241019
1025A
102614
......

2.1.2  Zeiger

Deklaration von Zeigertypen

In C können Variablen angelegt werden, deren Inhalt die Adresse einer Speicherzelle ist. Der Typ einer solchen Variablen endet mit einem Sternzeichen *. Diesem vorangestellt ist Typ der Daten, die in dieser Speicherzelle liegen. So bezeichent der Typ int* die Adresse einer Speicherzelle, in der der Wert einer ganzen Zahl steht; im Gegensatz zum Typ int, der direkt eine ganze Zahl darstellt.

Erzeugen von Zeigern

Eine Variable mit der Deklaration int i; entspricht einer Speicherzelle für eine ganze Zahl. Mit dem Zeichen & als einstelligen Operator kann in C für eine Variable die Adresse dieser Speicherzelle erhalten werden. Der Operator & dient also dazu Zeigerwerte zu erzeugen.
Beispiel:
Wir deklarieren drei Variablen: eine vom Typ int, einen Zeiger auf eine Variable vom Typ int und einen Zeiger auf eine Variable, die wieder ein Zeiger ist. Für alle drei Variablen werden die Werte ausgegeben.
Zeiger1
#include <iostream>

int main(){
  int i = 5;
  int* pI = &i;
  int** ppI = &pI;
  
  std::cout << i 
    << ", " << pI 
    <<", "  << ppI 
    << std::endl;
}

Das Programm kann z.B.  folgende Ausgabe haben:
sep@swe10:~/fh/prog3/examples> ./Zeiger1
5, 0xbffff1b8, 0xbffff1b4
sep@swe10:~/fh/prog3/examples>

Es empfiehlt sich, Variablen, die Zeiger sind, auch im Variablennamen als solche zu kennzeichnen. Eine gebräuchliche Methode ist, sie mit einem p für pointer beginnen zu lassen.

Zeiger auflösen

Der Wert eines Zeigers selbst, wie das obige Beispiel gezeigt hat, ist recht uninteressant. Es handelt sich um die Nummer einer Speicheradresse. Diese hat wenig mit der eigentlichen Programmlogik zu tun. Ein Zeiger ist in der Regel dafür interessant, um ihm dazu zu benutzen, wieder auf den Inhalt der Speicherzelle, auf die der Zeiger zeigt, zuzugreifen. Eine Zeigervariable sozusagen zu benutzen, um mal in die Zelle, dessen Adresse der Zeiger darstellt, hineinzuschauen. Hierzu stellt C das Malzeichen * als einstelligen Operator zur Verfügung. Er ist sozusagen der inverse Operator zum Operator &. Er überführt einen Zeiger auf eine Variable, wieder in den Wert der Variablen.
Beispiel:
Mit dem Operator * läßt sich wieder der Wert der Speicherzelle, auf den ein Zeiger weist, erhalten.
Zeiger2
#include <iostream>

int main(){
  int i = 5;
  int *pI = &i;
  int **ppI = &pI;
  
  std::cout << i 
    << ", " << pI 
    << ", " << *pI 
    <<", "  << ppI 
    << ", " << *ppI 
    << ", " << **ppI
    << std::endl;
}

Wir bekommen z.B. folgende Ausgabe:
sep@swe10:~/fh/prog3/examples> ./Zeiger2
5, 0xbffff1b8, 5, 0xbffff1b4, 0xbffff1b8, 5
sep@swe10:~/fh/prog3/examples>


Zuweisungen auf Speicherzellen

Der Operator * kann auch in Ausdrücken auf der linken Seite des Zuweisungsoperators benutzt werden.
Beispiel:
Wir verändern eine Variable einmal dirket und zweimal über Zeiger auf diese Variable.
Zeiger3
#include <iostream>

int main(){
  int i = 5;
  int *pI = &i;
  int **ppI = &pI;
  std::cout << i << ", " << *pI << ", " << **ppI << std::endl;

  i = 10;
  std::cout << i << ", " << *pI << ", " << **ppI << std::endl;

  *pI = 20;  
  std::cout << i << ", " << *pI << ", " << **ppI << std::endl;

  **ppI = 42;  
  std::cout << i << ", " << *pI << ", " << **ppI << std::endl;
}

Das Programm erzeugt folgende Ausgabe. Die Änderung der Variablen wird durch jeden Zeigerzugriff auf sie sichtbar.
sep@swe10:~/fh/prog3/examples> ./Zeiger3
5, 5, 5
10, 10, 10
20, 20, 20
42, 42, 42
sep@swe10:~/fh/prog3/examples>

Rechnen mit Zeigern

Zeiger sind technisch nur die Adressen bestimmter Speicherzellen, also nichts weiteres als Zahlen. Und mit Zahlen können wir rechnen. So können tatsächlich arithmetische Operationen auf Zeigern durchgeführt werden.
Beispiel:
Wir erzeugen mehrere Zeiger auf ganze Zahlen und betrachten die Nachbarschaft eines dieser Zeiger.
Zeiger4
#include <iostream>

int main(){
  int i = 5;
  int j = 42;
  int *pI = &i;
  int *pJ = &j;
  
  int *pK;

  std::cout << i 
    << ", " << pI 
    << ", " << pI+1 
    <<", "  << *(pI+1) 
    << ", " << pI-1 
    <<", "  << *(pI-1) 
    << ", " << pJ 
    << ", " << *pJ 
    << std::endl;

  *pK = 12;
    std::cout << pK <<", " << *pK << std::endl; 
}

Dieses Programm erzeugt z.B. folgende interessante Ausgabe:
sep@swe10:~/fh/prog3/examples> ./Zeiger4
5, 0xbffff1b8, 0xbffff1bc, -1073745416, 0xbffff1b4, 42, 0xbffff1b4, 42
0xbffff1c8, 12
sep@swe10:~/fh/prog3/examples>

Das Ergebnis einer Zeigerarithmetik hängt sehr davon ab, wie die einzelnen Variablen im Speicher abgelegt werden. In der Regel wird man die Finger von ihr lassen und nur ganz bewußt auf sie zurückgreifen, wenn man genau weiß, was man tut, um eventuell Optimierungen durchführen zu können.

Zeiger als Parameter

Wenn man in Java und in C einer Funktion einen primitiven Typ als Parameter übergibt, sind innerhalb des Methodenrumpf gemachte Zuweisungen außerhalb der Funktion nicht sichtbar:
Beispiel:
Folgendes Javaprogramm gibt die 0 und nicht die 42 aus.
JIntAssign
class JIntAssign {
  static void f(int x){x=42;}

  public static void main(String [] _){
    int y = 0;
    f(y);
    System.out.println(y);
  }
}

Entsprechend funktioniert auch das analoge C Programm:
CIntAssign
#include <iostream>

void f(int x){x=42;}

int main(){
  int y = 0;
  f(y);
  std::cout << y << std::endl;
}

Auch hier ist das Ergebnis 0.
Der Grund für dieses Verhalten ist, daß der Wert der ganzen Zahl, an die Funktion f übergeben wird, und nicht die Adresse, an der diese Zahl steht.
Mit Zeigern kann man in C die Adresse, in der eine Zahl steht, als Parameter übergeben:
CPIntAssign
#include <iostream>

  void f(int* pX){*pX=42;}

  int main(){
    int y = 0;
    int *pY = &y;
    f(pY);
    std::cout << y << std::endl;
  }

Dadurch, daß wir einen Zeiger auf eine Speicherzelle übergeben, können wir diesen Zeiger benutzen, um die Speicherzelle zu verändern. Jetzt ist das Ergebnis die 42. Die Variable y wird effektiv geändert.
In Java haben wir keine Zeiger zur Verfügung. Wollen wir den Wert einer ganzen Zahl, der als Parameter einer Methode übergeben wird, effektiv in dieser Methode ändern, so geht das nur, wenn diese ganze Zahl das Feld eines Objektes ist, daß der Methode übergeben wird.
Beispiel:
Eine typische Klasse, die nur Objekte für eine ganze Zahl erzeugt:
IntBox
class IntBox{
  int value;
  public IntBox(int v){value=v;}
}

Objekte dieser Klasse können Methoden übergeben werden. Dann kann das Objekt als Referenz auf seine Felder benutzt werden, so daß die Felder in diesem Objekt effektiv verändert werden können.
JIntBoxAssign
class JIntBoxAssign {
  static void f(IntBox xBox){xBox.value=42;}
  public static void main(String [] _){
    IntBox yBox = new IntBox(0);
    f(yBox);
    System.out.println(yBox.value);
  }
}

Die Ausgabe dieses Programms ist 42.
Aufgabe 3  
Schreiben Sie jetzt eine Version der Fakultätsfunktion, in der ein Zeiger auf die Variable, in der das Ergebnis akkumuliert wird, als zweites Argument übergeben wird:
FakAcc
void fak(int x,int* result);

Schreiben Sie Testfälle für diese Funktion.

Dynamisch Adresse anfordern

new Operation  
Bisher haben wir Zeiger immer über den Operator & erzeugt, d.h. wir haben für eine bestehende Variable uns die Adresse geben lassen. Variablen werden ein für allemal fest im Programmtext definiert. Ihre Anzahl ist also bereits statisch im Programmtext festgelegt. C erlaubt eine dynamische Anforderung von Speicherzellen. Hierzu gibt es den Operator new.4 Ihm folgt der Name eines Typs. Er erzeugt einen neuen Zeiger auf eine Speicherzelle bestimmten Typs.
new bewirkt, daß vom Betriebssystem eine neue Speicherzelle bereitgestellt wird. In dieser Speicherzelle können dann Werte gespeichert werden.
Beispiel:
new ist gefährlich, denn jedesmal, wenn der Operator new ausgeführt wird, bekommen wir eine frische Speicherzelle für die Programmausführung zugewiesen. Wenn wir das wiederholt machen, so werden wir immer mehr Speicher vom Betriebssystem anfordern. Irgendwann wird das Betriebssystem keinen Speicher mehr zur Verfügung stellen können. Folgendes Programm durchläuft eine Schleife und fordert jedesmal eine neue Speicherzelle an.
News
#include <iostream>

int main(){
    while (true){
	int *pX = new int;
        std::cout << pX << std::endl;
    }
}

Startet man dieses Programm und betrachtet parallel (z.B. mit dem Befehl top auf Unix oder im Task Manager von Windows) die Systemresourcen, so wird man sehen, daß der vom Programm News benutzte Bereich des Hauptspeichers immer mehr anwächst, bis das Programm den ganzen Hauptspeicher belegt und das Betriebssystem lahmlegt.
Solche Situationen, in denen ein Programm immer mehr Speicher anfordert, ohne diesen wirklich zu benötigen, nennt man auf Englisch space leak.
delete Operation  
Mit new läßt sich in C eine frische Speicherzelle vom Betriebssystem anfordern. Damit das nicht unkontrolliert passiert, stellt C einen dualen Operator zur Verfügung, der Speicherzellen wieder dem Betriebssystem zurück gibt. Dieser Operator heißt delete. Der delete-Operator wird auf Zeigerausdrücke angewendet.
Beispiel:
Das Speicherloch des letzten Beispiels läßt sich schließen, indem der Zeiger nach seiner Ausgabe immer wieder gelöscht, d.h. dem Betriebssystem zurückgegeben wird.
Deleted
#include <iostream>

int main(){
    while (true){
	int *pX = new int;
        std::cout << pX << std::endl;
        delete pX;
    }
}

Startet man dieses Programm, so stellt man fest, daß es einen konstanten Bedarf an Speicherkapazität benötigt. Zusätzlich kann man beobachten, daß die Speicheradresse stets die gleiche ist, also wiederverwendet wird. Das Betriebssystem gibt uns gerade wieder die Adresse, die wir eben wieder freigegeben haben.
In Java kennen wir keine direkte dynamische Speicherverwaltung über Zeiger. Dort wird jede Speicherverwaltung automatisch von der Javalaufzeitumgebung durchgeführt. Wenn neue Objekte erzeugt werden, wird neuer Speicher angefordert; Speicher, der nicht mehr benötigt wird, wird von Zeit zu Zeit wieder freigegeben.
Die new-Operation war gefährlich, weil wir Speicherlöcher bekommen konnten. Auch die delete-Operation ist gefährlich. Wenn wir Speicher zu früh freigeben, obwohl er noch benötigt wird, so können wir auch Laufzeitfehler bekommen.
Beispiel:
Wir erzeugen mit new einen Zeiger. Diesen Zeiger kopieren wir in einen zweiten Zeiger, über den schließlich die Speicherzelle wieder freigegeben wird. Jetzt haben wir einen Zeiger pI, dessen Zieladresse vom Programm neu verwendet werden kann.
WrongDelete
#include <iostream>

void f(int* pI){
  int* pX = pI;
  delete pX;
}

int main(){
  int *pI = new int;
  *pI = 42;
  std::cout << *pI << std::endl;
  f(pI);
  int *pK = new int;
  *pK = 16;
  std::cout << *pI <<  std::endl;
}

Und tatsächlich: führen wir dieses Programm aus, so steht plötzlich in der Zieladresse des Zeigers pI die Zahl 16, obwohl wir diese hier nie hineinschreiben wollten.
sep@linux:~/fh/prog3/examples/src> g++ -o WrongDelete WrongDelete.cpp
sep@linux:~/fh/prog3/examples/src> ./WrongDelete
42
16
sep@linux:~/fh/prog3/examples/src>

Wie man sieht, kann die dynamische Speicherverwaltung bei sorglosem Umgang zu vielfältigen, schwer zu verstehenden Fehlern führen.

Zeiger auf lokale Variablen

Wir können nicht darauf vertrauen, daß alle Zeiger, die auf eine Speicherzelle zeigen, immer noch in eine benutzte Speicherzelle zeigen. Es kann passieren, daß wir einen Zeiger haben, die Speicherzelle, auf die dieser zeigt, aber schon längst nicht mehr für eine Variable benutzt wird. Dieses ist insbesondere der Fall, wenn wir einen Zeiger auf eine lokale Variable einer Funktion haben. Sobald die Funktion verlassen wird, existieren die lokalen Variablen der Funktion nicht mehr. Haben wir danach noch Zeiger auf diese Variablen, do zeigen diese in Speicherbereiche, die wieder freigegeben wurden.
Beispiel:
Betrachten wir folgendes Programm. Die Funktion refToSquare hat als Rückgabewert einen Zeiger auf ihre lokale Variable j.
LocalVarPointer
#include <iostream>

int*  refToSquare(int i){
  int j = i*i;
  return &j;
}

int main(){
  int *pJ = refToSquare(5);
  int k = 42;
  std::cout << k <<std::endl;
  std::cout << *pJ <<std::endl;
}

Der Übersetzer warnt uns davor, diesen Zeiger nach ausserhalb der Funktion zurückzugeben:
sep@linux:~/fh/prog3/examples/src> c++ -o LocalVarPointer LocalVarPointer.cpp
LocalVarPointer.cpp: In function `int* refToSquare(int)':
LocalVarPointer.cpp:4: Warnung: address of local variable `j' returned
sep@linux:~/fh/prog3/examples/src>

Und tatsächlich, scheint er Recht mit seiner Warnung zu haben, denn das Programm liefert einen überraschenden undefinierten Wert während der Ausführung:
sep@linux:~/fh/prog3/examples/src> ./LocalVarPointer
42
-1073745324
sep@linux:~/fh/prog3/examples/src>

2.1.3  Funktionszeiger

Alles wird irgendwo im Hauptspeicher des Rechners abgelegt. Zeiger sind bestimmte Stellen im Speicher. Da auch die Funktionen, also der Programmcode im Speicher abgelegt sind, lassen sich entsprechend Zeiger auf Funktionen definieren. Die Syntax, um aus einem Funktionsnamen eine Zeiger auf diese Funktion zu erzeugen, ist der bereits bekannte Operator &.
Beispiel:
Wir definieren eine einfache Funktion, die 5 auf eine Zahl addiert. Wir greifen auf diese Funktion über ihren Adresszeiger in der Anwendung zu.
Function
#include <iostream>

int add5(int x){return x+5;}

int main(){
  int (*f) (int) = &add5;
  int i = (*f)(4);

  std::cout << i << std::endl;
}

Im obigen Beispiel haben wir Gebrauch von den beiden Operatoren * und & zum Referenzieren und Dereferenzieren der Methode add5 gemacht. Wir brauchen dieses nicht einmal. Das Programm läßt sich ebenso ohne diese Operatoren schreiben:
Function2
#include <iostream>

int add5(int x){return x+5;}

int main(){
  int (*f) (int) = add5;
  int i = f(4);

  std::cout << i << std::endl;
}

Auch dieses Programm übersetzt fehlerfrei und liefert dasselbe Ergebnis wie sein Vorgänger. Der C-Übersetzer verhält sich wieder pragmatisch und wandelt den Funktionsnamen add5 automatisch in einen Zeiger auf den Funktionscode um; und ebenso wird die Zeigervariable f implizit dereferenziert.
Zeigerarithmetik hat ihre Grenzen. Für Funktionszeiger kann keine Arithmetik angewendet werden. Foldendes Programm, in dem man annehmen könnte, daß der Aufruf der Methode eval inmitten der Methode f springt, wird vom Übersetzer zurückgewiesen:
WrongPointerArith
#include <iostream>

int f(){
    return 1+1+1+1+1+1+1+1+1+1+1+1;
}

int eval(int (*f)()){
    return f();
}

int main(){
  std::cout << eval(*f + 8) << std::endl;
}

Der durchaus verständliche Fehlertext lautet:
sep@linux:~/fh/prog3/examples/src> g++ WrongPointerArith.cpp
WrongPointerArith.cpp: In function `int main()':
WrongPointerArith.cpp:12: error: pointer to a function used in arithmetic
sep@linux:~/fh/prog3/examples/src>

Aufgabe 4   (2 Punkte) Schreiben Sie eine Funktion, die mit einer Intervallschachtellung für eine stetige Funktion eine Nullstelle approximiert.
Nullstellen
float nullstelle(float (*f) (float),float mi,float ma);

Testen Sie Ihre Implementierung mit folgendem Programm:
TesteNullstellen
#include "Nullstellen.h"
#include <iostream>

float f1(float x){return 2*x*x*x+5;}
float f2(float x){return 2*x +1 + x*x*x/4 ;}
float f3(float x){return 10*x+5;}
float f4(float x){return 0;}
float f5(float x){return x*x;}

void teste(float (*f)(float)){
  const float fn = nullstelle(f,-10,11);
  std::cout << "n = "<<fn<< ", f(n) = " << f(fn) << std::endl;
}

int main(){
  teste(f1);
  teste(f2);
  teste(f3);
  teste(f4);
  teste(f5);
}

2.1.4  Synonyme Typnamen

Wie wir im letztem Abschnitt gesehen haben, können Typnamen recht kompliziert werden. Eine Funktion, die einen float in einen float Wert überführt, hat bereits den Typ: float (*f)(float). Wie sieht dann erst der Typ aus, der eine solche Funktion als Eingabe hat und eine andere Ausgabe erzeugt? Typen könnten theoretisch immer länger werden. Um noch griffig zu erkennen, um was es sich bei dem Typ handeln soll, kann man Typen einen Namen geben. Hierzu gibt es in C das typedef Konstrukt. Dem Schlüsselwort typedef folgt der eigentliche Typ und dem dann ein neuer frei zu wählender Bezeichner für das Typsynonym.
Beispiel:
Ein einfaches Typsynonym für Zahlen:
Zahl
#include <iostream>

typedef int zahl;

zahl f(zahl x){return 2*x;}

int main(){
  std::cout << f(21) << std::endl;
}

Für Typen, die in ihrer Syntax eigentlich einen Variablenbezeichner klammern, steht an der Stelle des Variablenbezeichners im typedef-Konstrukt der Name des Typsynonyms.
Beispiel:
Für führen ein Synonym für Funktionen auf float-Werten ein:
FloatFunction
#include <iostream>

typedef float zahl;
typedef zahl (*zahlenFunktion)(zahl);

zahl verdoppel(zahl x){return 2*x;}

zahl applyTwice(zahlenFunktion f, zahl x){
  return f(f(x));
}

int main(){
  std::cout << applyTwice(verdoppel,10.5) << std::endl; 
}

Drei Vorteile ergeben sich durch das typedefKonstrukt:
Die Definition eines Typsynonyms wird sinnvoller Weise in einer Header-Datei vorgenommen.
Ein schönes Beispiel für einen sprechenden Typnamen findet sich in der Blibliothek cstddef. Dort wird ein Typsynonym size_t definiert, um ihn für Reihungen als Typ für den Index zu benutzen.

2.1.5  Referenzen

Zeiger sind ein sehr mächtiges aber auch sehr gefährliches Programmkonstrukt. Leicht kann man sich bei Zeigern vertun, und plötzlich in undefinierte Speicherbereiche hineinschauen. Zeiger sind also etwas, von dem man lieber die Finger lassen würde. Ein C Programm kommt aber kaum ohne Zeiger aus. Um dynamisch wachsende Strukturen, wie z.B. Listen zu modellieren, braucht man Zeiger. Um nicht Werte sondern Referenzen an Funktionen zu übergeben, benötigt man Zeiger.
Daher hat man sich in C++ entschlossen einen weiteren Typ einzuführen, der die Vorteile von Zeigern hat, aber weniger gefährlich in seiner Anwendung ist, den Referenztyp.

Deklaration von Referenztypen

Für einen Typ, der kein Referenztyp ist, wird der Referenztyp gebildet, indem dem Typnamen das Ampersandzeichen & nachgestellt wird. Achtung, in diesen Fall ist das Ampersandzeichen kein Operator, sondern ein Bestandteil des Typnamens5. So gibt es z.B. für den Typ int den Referenztyp int & oder für den Typ string den Referenztyp string &. Es gibt keine Referenztypen von Referenztypen: int && ist nicht erlaubt. Anders als bei Zeigern, bei denen Zeiger auf Zeigervariablen erlaubt waren.

Initialisierung

Eine Referenzvariable benötigt bei der Deklaration einen initialen Wert. Andernfalls kommt es zu einen Übersetzungsfehler:
NoInitRef
int &i;

Der Übersetzer gibt folgende Fehlermeldung:
sep@linux:~/fh/prog3/examples/src> g++ -c NoInitRef.cpp
NoInitRef.cpp:1: error: `i' declared as reference but not initialized
sep@linux:~/fh/prog3/examples/src>

Referenzvariablen lassen sich mit Variablen des Typs, auf den referenziert werden soll, initialisieren:
InitRef
int i;
int &j=i;

Referenzvariablen lassen sich nicht mit Konstanten oder dem Ergebnis eines beliebigen Ausdrucks initialisieren:
WrongInitRef
int i;
int &j1=i+2;
int &j2=2;

Auch das führt zu Übersetzungsfehlern:
sep@linux:~/fh/prog3/examples/src> g++ -c WrongInitRef.cpp
WrongInitRef.cpp:2: error: could not convert `(i + 2)' to `int&'
WrongInitRef.cpp:3: error: could not convert `2' to `int&'
sep@linux:~/fh/prog3/examples/src>

Benutzung

Ist eine Referenzvariable einmal initialisiert, so kann sie als Synonym für die Variable auf die die Referenz geht, betrachtet werden. Jede Änderung der Referenzvariablen ist in der Originalvariablen sichtbar und umgekehrt.
Im folgenden Programm kann man sehen, daß Modifizierungen an der Originalvariablen über die Referenzvariablen sichtbar werden.
ModifyOrg
#include <iostream>
int  main(){
    int i=42;
    int &j=i;
    std::cout << j << std::endl;
    i=i+1;
    std::cout << j << std::endl;
    i=i-1;
    std::cout << j << std::endl;
    int k=j;
    std::cout << k << std::endl;
    i=i+1;
    std::cout << k << std::endl;
}

Das Programm hat folgende Ausgabe:
sep@linux:~/fh/prog3/examples/src> ./ModifyOrg
42
43
42
42
42
sep@linux:~/fh/prog3/examples/src>

Wie man sieht, wird anders als bei Zeigern, für die Referenzvariablen nicht eine Speicheradresse ausgegeben, sondern der Wert, der in der Originalvariablen gespeichert ist. Eine Referenzvariable braucht nicht wie ein Zeiger dereferenziert zu werden. Dieses wird bereits automatisch gemacht. Referenzen werden automatisch dereferenziert, wenn der Kontext den Originaltyp erwartet. Daher kann auch eine Referenzvariable wieder direkt einer Variablen des Originaltyps zugewiesen werden. So wird in der Zuweisung der Referenz j auf die Variable k des Originaltyps int oben, der in der referenzierten Variablen gespeicherte Wert in k gespeichert. Ändert sich dieser, so ist das in k nicht sichtbar.
Genauso findet eine automatische Dereferenzierung statt, wenn einer Referenzvariablen ein neuer Wert des Originaltyps zugewiesen wird:
ModifyRef
#include <iostream>

int main(){
  int i = 21;
  int &j=i;
  
  j=2*j;
  std::cout << i << ", "<< j << std::endl;
}

Das Programm führt zu folgender Ausgabe:
sep@linux:~/fh/prog3/examples/bin> ./ModifyRef
42, 42
sep@linux:~/fh/prog3/examples/bin>

Die Änderung an der Referenzvariabel wird auch in der Originalvariabel sichtbar.
Besonders vertrackt wird es, wenn wir einer Referenzvariablen eine andere Referenzvariable zuweisen.
RefToRef
#include <iostream>

int main(){
  int i1 = 21;
  int &j1=i1;

  int i2 = 42;
  int &j2=i2;  
  std::cout << i2 <<", "<< j2 << std::endl;

  j2 = j1;
  std::cout << i2 <<", "<< j2 << std::endl;

  j2 = 15;
  std::cout << i1 <<", "<< j1 << std::endl;
  std::cout << i2 <<", "<< j2 << std::endl;
}

Die erzeugte Ausgabe ist:
sep@linux:~/fh/prog3/examples/src> ./RefToRef
42, 42
21, 21
21, 21
15, 15
sep@linux:~/fh/prog3/examples/src>

Obwohl wir eine Zuweisung j2=j1 haben, sind beides weiterhin Referenzen auf unterschiedliche Variablen! Die Zeile bedeutet nicht, daß die Referenz j2 jetzt die gleiche Referenz wie j1 ist; sondern daß lediglich der mit der Referenz j2 gefundene Wert in die durch j1 referenzierte Variable abzuspeichern ist. In dieser Zeile werden beide Referenzvariablen also implizit dereferenziert. Eine einmal für eine Variable initialisierte Referenz läßt sich also nicht auf eine andere Variable umbiegen.

Referenzen als Funktionsparameter

Im Prinzip gilt für Funktionsparameter von einem Referenztyp dasselbe wie für Variablen von einem Referenztyp. Der Funktion wird keine Kopie der Daten übergeben, sondern eine Referenz auf diese Daten:
RefArg
#include <iostream>

void f(int &x){
  x=x+1;
}

int main(){
  int i = 41;
  f(i);
  std::cout << i << std::endl;
}

Die erwartete Ausgabe ist:
sep@linux:~/fh/prog3/examples/bin> ./RefArg
42
sep@linux:~/fh/prog3/examples/bin>

Es gelten dieselben Einschränkungen, wie wir sie schon für die Zuweisung auf Referenzvariablen kennengelernt haben. Es dürfen Referenzfunktionsparameter nicht mit beliebigen Ausdrücken aufgerufen werden:
WrongCall
#include <iostream>

void f(int &x){}

int main(){
  int i = 41;
  f(i+1);
  f(1);
}

Wir bekommen wieder die schon bekannten Fehlermeldungen:
sep@linux:~/fh/prog3/examples/src> g++ -c WrongCall.cpp
WrongCall.cpp: In function `int main()':
WrongCall.cpp:8: error: could not convert `(i + 1)' to `int&'
WrongCall.cpp:3: error: in passing argument 1 of `void f(int&)'
WrongCall.cpp:9: error: could not convert `1' to `int&'
WrongCall.cpp:3: error: in passing argument 1 of `void f(int&)'
sep@linux:~/fh/prog3/examples/src>

2.2  Konstruierte Typen

2.2.1  Aufzählungen

C ermöglicht es mit Aufzählungen, eine Menge möglicher Werte zu definieren. Diese Menge ist geordnet. Technisch handelt es sich lediglich um eine Untermenge der ganzen Zahlen. Auf Aufzählungstypen können Vergleichsoperationen angewendet werden. Arithmetische Operationen liefern ganze Zahlen als Ergebnis. Zahlen können durch eine Typzusicherung in Aufzählungen umgewandelt werden.
Beispiel:
Wir definieren ein Aufzählung von 6 Farben und benutzen diese.
Farben
#include <iostream>

enum farben 
  {rot, gruen, gelb, blau, magenta, umbra};

int main() {
  enum farben f = gelb;

  std::cout << f <<std::endl;
  if (f== gelb) std::cout << "gelbe Farbe" <<std::endl;
  if (f== umbra) std::cout << "Farbe umbra" <<std::endl;
  if (f> rot) std::cout << "Farbe größer Rot" <<std::endl;
  f=(farben)(f+1);
  std::cout << f << std::endl;
  if (f== blau) std::cout << "blaue Farbe" <<std::endl;
  if (f> gelb) std::cout  << "Farbe größer Gelb" <<std::endl;
  f=gruen;
  std::cout << f << std::endl;

  f=(farben)(10);
  std::cout << "die Farbe 10: " << f << std::endl;
}

Das Programm erzeugt folgende Ausgabe:
sep@linux:~/fh/prog3/examples/bin> ./Farben
2
gelbe Farbe
Farbe größer Rot
3
blaue Farbe
Farbe größer Gelb
1
die Farbe 10: 10
sep@linux:~/fh/prog3/examples/bin>

Wie man als erstes sieht, werden die Werte der Aufzählung wie Zahlen behandelt. Die Werte der Aufzählung beginnen bei 0. Die Farbe gelb entspricht im Beispiel also der Zahl 2. Sobald auf farben gerechnet wurde, wird eine Typzusicherung nötig.
Auch der bool'sche Datentyp der Wahrheitswerte, wird intern in C++ wie eine Aufzählung behandelt. Dieses wird besonders deutlich, wenn wir bool'sche Werte ausgeben. Es zeigt sich auch, daß zusammengesetzte Befehle mit Bedingungen Zahlen als Werte für die bool'sche Bedingung zulassen
ShowBool
#include <iostream>

int main(){
  std::cout << true << std::endl;
  std::cout << false << std::endl;

  if (10) std::cout << "10 ist wahr" << std::endl;
  if (0) ; else std::cout << "0 ist falsch" << std::endl;
}

Wir können uns entsprechend also einen eigenen Typ für Wahrheitswerte definieren:
Wahrheit
#include <iostream>

enum wahrheit {falsch, wahr};

int main(){
    if (wahr) std::cout << "wahr" << std::endl;

    wahrheit w = (wahrheit)(falsch||wahr);
    if (w) std::cout << "wahr" << std::endl;

    w = (wahrheit)(falsch&&wahr);   
    if (w) std::cout << "wahr" << std::endl;

    w=(wahrheit)(1==1);
    if (w) std::cout << "wahr" << std::endl;
}


Wie man sieht, zwingt uns C immerhin dazu, eine Typzusicherung von bool'schen Werten auf unseren Aufzählungstyp zu machen.

2.2.2  Reihungen (Arrays)

Wir kennen Reihungen bereits aus Java. In Java waren Reihungen kaum etwas anderes als eine weitere Klasse, für die es eine besondere Syntax gab. Daher haben Reihungen in der Javavorlesung nur ein sehr wenig Raum eingenommen. Es ist dort für Reihungen nicht viel Neues zu den Javakonzepten zu lernen.
In C begegnen uns Reihungen als alte Freunde wieder; aber wir sollten einen genauen Blick auf sie werfen. Sie werden sich als falsche Freunde erweisen. An ihnen ist gut die Entwurfsphilosophie von C im Gegensatz zu Java zu erkennen.
Die Syntax für Reihungen entspricht bis auf einige Ausnahmen der aus Java bekannten. Der Reihungstyp besteht zuerst aus dem Elementtyp, dann dem Variablennamen, der schließlich von einem eckigen Klammernpaar gefolgt wird. Reihungen können mit in geschweiften Klammern eingeschlossenen Elementauflistungen initialisiert werden. Zugriff auf Reihungselemente erfolgt durch den in einem eckigen Klammerpaar eingeschlossenen Index. Der Reihungsindex startet von 0.
FirstArray
#include <iostream>

int main(){
  int xs [] = {1,2,3};
  std::cout << xs[1];
}

Die aus Java bekannte Syntax, in die Typbezeichnung einer Reihung keine Klammer um einen Variablennamen bildet, ist in C leider nicht erlaubt.
WrongArray
int main(){
  int [] xs = {1,2,3}; // Java Syntax
}

Das macht die Typsprache von C in vielen Fällen sehr unübersichtlich.

Größendeklaration

Ein auffälliger Unterschied zu Java ist, daß bei der Deklaration einer Reihungsvariablen die Anzahl der Elemente bekannt sein muß:
UnknownSize
int main(){
  int xs [];
}

Die Übersetzung dieses Programms führt zu einem Fehler:
sep@linux:~/fh/prog3/examples/src> g++ UnknownSize.cpp
UnknownSize.cpp: In function `int main()':
UnknownSize.cpp:2: error: storage size of `xs' isn't known
sep@linux:~/fh/prog3/examples/src>

Der C-Übersetzer muß immer in der Lage sein, die Anzahl der Elemente einer Reihung abzuleiten. Der Grund dafür ist, daß sobald eine Variable deklariert wird, der C Compiler den Speicherplatz für die entsprechende Variable anfordern muß. In C bedeutet das bei einer Reihung, daß der Speicher für eine komplette Reihung angefordert wird und nicht ein Zeiger auf die Variable. Hierfür muß die Anzahl der Elemente bekannt sein.
KnownSize
#include <iostream>

int main(){
  int xs [15];
  std::cout << xs[1];
}

Das folgende Javaprogramm würde bei einer direkten Übertragung nach C bereits mehrere Fehler werfen:
ArrayTest
class ArrayTest {

    public static void main(String [] _){
	int [] ys = {1,2,3};
	int [] zs;
    }
}

Reihungen scheinen wirklich falsche Freunde zu sein.
Die Elementanzahl für eine Reihung muß nicht zur Übersetzungszeit bekannt sein, sondern kann dynamisch errechnet werden und z.B. als Parameter einer Funktion übergeben werden:
LocalArray
#include <iostream>

void f(int l){
  int xs [l];
  for (int i= 0; i<l;i=i+1){
    xs[i]=i;
    std::cout << xs[i] << ",";
  }
  std::cout << std::endl;
}

int main(){
  f(7);
  f(2);
}

Wie wir uns in der Ausgabe überzeugen können, werden in diesem Programm in der Funktion f nacheinander zwei Reihungen unterschiedlicher Länge erzeugt.6
sep@linux:~/fh/prog3/examples/src> ./LocalArray
0,1,2,3,4,5,6,
0,1,
sep@linux:~/fh/prog3/examples/src>

Die Größe einer Reihung

In Java enthalten Reihungen ein Feld, in dem die Anzahl der Elemente der Reihung gespeichert ist. Eine Reihung kann wie ein Objekt behandelt werden. Das Reihungsobjekt kann einfach nach diesem Längenfeld gefragt werden:
ArrayLength
class ArrayLength {

  public static void main(String [] _){
    int xs [] = {1,2,3};
    System.out.println(xs.length);
  }
}

In C geht dieses nicht. Zwar ist C recht pienzig, wenn wir Reihungen deklarieren und verlangt, daß die Elementeanzahl ableitbar ist, hingegen kann diese Information von einer Reihung nicht direkt abgefragt werden. Reihungen sind hier lediglich ein zusammenhängender Block von Speicherzellen. Es ist keine zusätzliche Information zu diesem Block gespeichert. Daher muß der Programmierer selber Sorge dafür tragen, die Elementanzahl einer Reihung in einer zusätzlichen Variablen zu speichern.
In C++ gibt es einen Trick, die Anzahl der Elemente zu erfragen: Es gibt in C++ die Funktion sizeOf mit der für eine Variable die Anzahl der von ihr belegten Speicherzellen erfragt werden kann. Da in C jedes Reihungselement in einer Reihung vom gleichen Typ ist, läßt sich aus der Größe eines beliebigen Elements und der Gesamtgröße der Reihung, die Länge einer Reihung berechnen.
CArrayLength
#include <iostream>
#include <cstddef>


int main(){
  int xs [] = {1,2,3};
  double ys [] = {1,2,3};

  size_t xsSize   = sizeof(xs);
  size_t xsLength = xsSize/sizeof(xs[0]);

  size_t ysSize   = sizeof(ys);
  size_t ysLength = ysSize/sizeof(ys[0]);

  std::cout << "xsSize:   " << xsSize   << std::endl;
  std::cout << "xsLength: " << xsLength << std::endl;
  std::cout << "ysSize:   " << ysSize   << std::endl;
  std::cout << "ysLength: " << ysLength << std::endl;
}

Starten wir dieses Programm, so sehen wir, daß tatsächlich die korrekte Länge für beide Reihungen berechnet wird. Die Elemente benötigen unterschiedlich viel Speicherplatz.
sep@linux:~/fh/prog3/examples/bin> ./CArrayLength
xsSize:   12
xsLength: 3
ysSize:   24
ysLength: 3
sep@linux:~/fh/prog3/examples/bin>


Zaunpfahlprobleme

Will man einen Zaun von 10 Meter Länge bauen und alle zwei Meter einen Zaunpfahl setzen, so benötigt man 6 und nicht 5 Pfähle. Es passiert einem leicht, sich hier um eins zu versehen. Diese typische Fehler um genau eins in einem Index, passiert bei der Programmierung von Reihungen gerne. Schuld daran dürfte sicher auch sein, daß der erste Index die 0 ist.7 Daran sind wir aus Java bereits gewöhnt. Java wirft uns eine entsprechende Ausnahme, wenn wir mit einem zu großen Index in eine Reihung greifen:
JWrongIndex
class WrongIndex {
  public static void main(String [] _){
    String [] xs = {"hallo", "was", "kommt", "jetzt"};
    for (int i=0;i<=xs.length;i++){
      System.out.println(xs[i]);
    }
  }
}

Das Programm bricht mit einer Ausnahmemeldung ab:
sep@linux:~/fh/prog3/examples/classes> java WrongIndex
hallo
was
kommt
jetzt
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4
        at WrongIndex.main(JWrongIndex.java:5)
sep@linux:~/fh/prog3/examples/classes>

In C passiert etwas unerwartetes:
CWrongIndex
#include <iostream>

int main(){
  std::string xs []  = {"hallo", "was", "kommt", "jetzt"};
  for (int i=0;i<=4;i++){
    std::cout <<xs[i]<< std::endl;
  }

  int ys []  = {1,2,3,4};
  for (int i=0;i<=4;i++){
    std::cout <<ys[i]<< std::endl;
  }
}

Das Programm bricht nicht ab und unterrichtet uns nicht darüber, daß wir einen zu hohen Index benutzen:
sep@linux:~/fh/prog3/examples/bin> ./CWrongIndex
hallo
was
kommt
jetzt

1
2
3
4
1074641520
sep@linux:~/fh/prog3/examples/bin>

Stillschweigend greift das Programm in die nach der Reihung liegenden Speicherbereiche. Dort werden irgendwelche Werte gefunden, mit denen dann weitergerechnet wird. Dieses ist natürlich eine gefährliche Quelle für Fehler. Zusätzlich kann es sogar zu einer Sicherheitslücke werden, wenn es gelingt über einen falschen Reihungsindex auf private Daten, die im Speicher abgelegt sind, zuzugreifen.
Hier zeigt sich ganz deutlich das Entwurfskriterium von C. Es werden keine Checks während der Ausführung durchgeführt, es sei denn der Programmierer hat diese explizit vorgeschrieben. In Java steht die Robustheit und Sicherheit an oberster Stelle des Sprachentwurfs.

Reihungszuweisung

In Java haben wir uns nicht viel Gedaken um Reihungen gemacht. Wir haben Reihungen wie jeden anderen Objektypen auch behandelt. Unter anderen konnten wir Reihungsvariablen einander zuweisen und erhielten dadurch mehrere Variablen, die auf dieselbe Reihung zeigten.
JAssignAndModify
class JAssignAndModify{
   public static void main(String [] _){
     int [] xs = {1,2,3};
     int [] ys = xs;
     ys[1]=42;
     System.out.println(xs[1]);
  }
}

Der Grund dafür, daß dieses in Java funktioniert, liegt in der Tatsache, daß Reihungen wie andere Objekttypen auch, Referenzen auf die entsprechenden Typen sind. Das ist in C nicht der Fall. Betrachten wir jetzt das entsprechende Programm in C:
IllegalAssign
#include <iostream>
int main(){
  int xs [] = {1,2,3};
  int ys [3];
  ys = xs;
  ys[1]=42;
  std::cout <<xs[1]<<std::endl;
}

Bereits in der Kompilierung erhalten wir einen Fehler:
sep@linux:~/fh/prog3/examples/src> g++ IllegalAssign.cpp
IllegalAssign.cpp: In function `int main()':
IllegalAssign.cpp:5: error: ISO C++ forbids assignment of arrays
sep@linux:~/fh/prog3/examples/src>

Wollen wir das aus Java bekannte Verhalten simulieren, so müssen wir wieder auf Zeiger zurückgreifen. Wir benötigen einen Zeiger auf eine Reihung. Leider ist die Syntax hierfür in C etwas umständlich. Dieses liegt an der klammernden Schreibweise für Reihungstypen, die eine Variable umschließt. Der Typ läßt sich so nicht in C ohne einen Variablennamen notieren. Ein Zeiger auf eine Reihung von drei ganzzahligen Elementen schreibt sich als:
int (* pXs)[3].8
Hier nun das obige Javabeispiel über Zeiger in C realisiert:
CAssignAndModify
#include <iostream>
int main(){
  int xs [3] = {1,2,3};
  int (* pXs)[3]  = &xs;
  int (* pYs)[3]  = pXs;

  (*pYs)[1] =42;

  std::cout << xs[1] << std::endl;
}

Die Ausgabe ist wieder die beliebte Antwort 42.

Reihungen von Zeigern

Im letzten Abschnitt haben wir Zeiger auf Reihungen realisiert. Jetzt realisieren wir eine Reihung von Zeigern. Da Zeiger ebenso Typen sind, wie alle anderen Typen auch, lassen sich von Zeigern auch Reihungen bilden:
ArrayOfPointers
#include <iostream>

int main(){
  int *xs [] = {new int, new int, new int};
  for (int i = 0;i<3;i=i+1){
    *xs[i] = i;
  }

  for (int i = 0;i<3;i=i+1){
      std::cout << xs[i] << " " <<*xs[i] << std::endl;
  }
}

Das Programm erzeugt folgende Ausgabe:
sep@linux:~/fh/prog3/examples/src> g++ -o ArrayOfPointers ArrayOfPointers.cpp
sep@linux:~/fh/prog3/examples/src> ./ArrayOfPointers
0x8049cd8 0
0x8049ce8 1
0x8049cf8 2
sep@linux:~/fh/prog3/examples/src>

Aufgabe 5  
Implementieren sie die Funktion compose entsprechend folgender Header-Datei:
Compose
typedef int (* intfunction)(int);

int compose(intfunction fs [],int length,int x);

Sie soll eine Komposition aller Funktionen in der Reihung fs auf den Wert x anwenden.
Es soll dabei gelten:
compose({f1,... fn},n,x) = fn(fn-1(...(f1(x))...))
Testen Sie Ihre Lösung mit folgender Testdatei:
ComposeTest
#include <iostream>
#include "Compose.h"

int f1(int x){return x*x;}
int f2(int x){return x;}
int f3(int x){return x+2;}
int f4(int x){return x*4;}
int f5(int x){return -x*x*x;}

int main(){
    intfunction fs [] = {f1,f2,f3,f4,f5};
    std::cout << compose(fs,5,2)<<std::endl;
}

Dynamische Reihungen

Wir haben das Schlüsselwort new kennengelernt, um einen Zeiger auf neu angeforderten Speicherplatz zum Speichern von Daten eines bestimmten Typs zu bekommen. Mit dem delete-Konstrukt konnte derart angeforderter Speicherplatz auch wieder freigegeben werden. Diese Konstrukte können ebenso auch für Reihungen angewendet werden. Auch hierbei muß gewährleistet sein, daß zur Ausführung des new Ausdrucks die Größe des benötigten Speicherplatzes bekannt ist, also insbesondere die Elementanzahl der Reihung.
Ebenso wie bei skalaren Typen erhalten wir durch den new Operator einen Zeiger auf den neu angeforderten Speicherbereich.
Beispiel:
Folgendes Programm erzeugt dynamisch nacheinander meherere Reihungen, benutzt diese kurz und gibt anschließend den durch die Reihung belegten Speicherbereich wieder frei:
NewArray
#include <iostream>

int main(){
  for (int i = 10; i<20;i=i+1){
    int *xs = new int[i];
    for (int j = 0; j<i;j=j+1){
      xs[j]=j;
      std::cout << xs[j] << ",";      
    }
    std::cout << std::endl;;      
    delete xs;
  }
}

Und tatsächlich können wir uns in der Programmausgabe davon überzeugen, Reihungen verschiedener Länge erzeugt zu haben:
sep@linux:~/fh/prog3/examples/src> ./NewArray
0,1,2,3,4,5,6,7,8,9,
0,1,2,3,4,5,6,7,8,9,10,
0,1,2,3,4,5,6,7,8,9,10,11,
0,1,2,3,4,5,6,7,8,9,10,11,12,
0,1,2,3,4,5,6,7,8,9,10,11,12,13,
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,
sep@linux:~/fh/prog3/examples/src>

Reihungen als Parameter

Wir können Funktionen schreiben, die Reihungen als Parameter übergeben bekommen. Diese Reihungen lassen sich im Funktionsrumpf ganz normal als Reihungen behandeln:
AddInts
#include <iostream>

int add(int xs [],int argc){
  int result=0;
  for (int i=0;i<argc;i++){
    result=result+xs[i];
  }
  return result;
}

int main(){
  int xs [] = {1,2,3,4};
  std::cout << add(xs,4) << std::endl;
}

Wir können jetzt einmal in diesem Programm einen Fehler einbauen, um zu sehen, als was für einen Typ der Compiler unseren Reihungsparameter betrachtet:
WrongArrayParameter
#include <iostream>

int add(int xs [],int argc){
  int result=0;
  for (int i=0;i<argc;i++){
    result=result+xs[i];
  }
  return result;
}

int main(){
    int xs [] = {1,2,3,4};
    std::cout << add(xs) << std::endl;
}

Auf diese Weise erzwingen wir eine Fehlermeldung des Compilers, in der die erwarteten Parametertypen der Funktion add angegeben werden. Der Kompilierversuch führt zu folgender Fehlermeldung:
sep@linux:~/fh/prog3/examples/src> g++ -c WrongArrayParameter.cpp
WrongArrayParameter.cpp: In function `int main()':
WrongArrayParameter.cpp:3: error: too few arguments to function `int add(int*,
   int)'
WrongArrayParameter.cpp:13: error: at this point in file
sep@linux:~/fh/prog3/examples/src>

Der Übersetzer ist der Meinung, der erste Parameter der Funktion add ist ein Zeiger auf eine ganze Zahl. Der Übersetzer macht aus einem Reihungsparameter nicht nur implizit einen Zeiger, sondern einen Zeiger auf das erste Element des Reihungsparameters. Die Schreibweise der eckigen Klammern für Reihungen, läßt sich somit als eine versteckte Zeigerarithmetik interpretieren. xs[i] bedeutet dann: *(xs+i).
Tatsächlich können wir ohne die Syntax der eckigen Klammern mit Reihungen arbeiten. Obiges Programm läßt sich auch wie folgt formulieren:
NoBrackets
#include <iostream>

int add(int xs [],int argc){
  int result=0;
  for (int i=0;i<argc;i++){
    result=result + *(xs+i);
  }
  return result;
}

int main(){
  int xs [] = {1,2,3,4};
  std::cout << add(xs,4) << std::endl;
}

Auch diese Version hat als Ergebnis die 10.

Reihungen als Rückgabewert

Wir sind aus Java gewohnt, Reihungen mehr oder weniger ohne Ausnahme wie alle anderen Typen auch zu benutzen; sie als Parameter zu übergeben, oder sie als Ergebnis einer Funktion zurückzugeben. Im letzem Abschnitt haben wir bereits gesehen, daß bei der Parameterübergabe von Reihungen diese implizit als Zeiger auf das erste Element behandelt werden. Gleiches gilt auch für die Rückgabewerte von Funktionen. Dadurch lassen sich einige Probleme einhandeln. Versuchen wir einmal eine einfache Funktion zu schreiben, die eine neue Reihungsvariable anlegt und diese als Ergebnis zurückgibt:
WrongArrayReturn
#include <iostream>

int*  newArray(int length){
  int result [length];
  return result;
}

int main(){
   int* xs = newArray(5);

   for (int i=0;i<5;i++){
     std::cout << xs[i];
   }
   std::cout << std::endl;
}

Die Übersetzung dieser Klasse glückt zwar, aber der Übersetzer gibt uns eine ernst zu nehmende Warnung:
WrongArrayReturn.cpp: In function `int* newArray(int)':
WrongArrayReturn.cpp:5: Warnung: address of local variable `result' returned
sep@linux:~/fh/prog3/examples/src>

Die Warnung ist insofern ernst zu nehmen, als daß wir tatsächlich ein undefiniertes Laufzeitverhalten bekommen:
sep@linux:~/fh/prog3/examples/src> ./WrongArrayReturn
1-1073745324-10737454321074404108134519424
sep@linux:~/fh/prog3/examples/src>

Den Grund hierfür kennen wir bereits aus unseren Kapitel über Zeiger. Implizit behandelt der Übersetzer die Zeile return result als eine Rückgabe des Zeigers auf die lokale Variable result. Eine lokale Variable innerhalb einer Funktion, wird im Speicher nach Beendigung der Funktion wieder freigegeben. Zeigen wir noch auf diese Variable, so werden wir an ihrer Speicherzelle undefinierte Werte sehen.
Die einzige vernünftigen Möglichkeiten der Rückgabe von Reihungen in C sind die Rückgabe eines als Parameter überreichten Zeiger auf eine Reihung, oder aber der Zeiger auf eine dynamisch neu erzeugte Reihung:
ReturnArray
#include <iostream>

int*  newArray(int length){
    return new int[length];
}

int main(){
   int* xs = newArray(5);

   for (int i=0;i<5;i++){
     std::cout << xs[i];
   }
   std::cout << std::endl;
}

Dieses Programm übersetzt ohne Warnung und liefert die erwartete Ausgabe.
sep@linux:~/fh/prog3/examples/src> g++ -o ReturnArray ReturnArray.cpp
sep@linux:~/fh/prog3/examples/src> ./ReturnArray
00000
sep@linux:~/fh/prog3/examples/src>

Tatsächlich findet man es relativ selten in C Programmen, daß eine Funktion eine Reihung als Rückgabewert hat. Zumeist bekommen Funktionen Reihungen als Parameter übergeben und manipulieren diese.

C Strings als Reihungen von Zeichen

In der Programmiersprache C also ohne dem ++ angehängt, wurden Strings als Reihungen von Zeichen betrachtet. Dieses hat große Auswirkungen auf das Arbeiten mit Strings und unterscheidet sich vollkommen von der Arbeit mit der Klasse String in Java.
Eine Stringliteral kann tatsächlich als Initialisierung einer Reihung von Elementen des Typs char benutzt werden.
Beispiel:
Folgendes Programm legt eine Zeichenkette als Reihung an und betrachtet diese einmal über die Reihungsvariable und einmal über einen Zeiger auf das erste Element.
CString
#include <iostream>

int main(){
  char xs [] = "hallo";
  char *ys = xs;
  ys[1] = 'e'; 
  std::cout << xs << std::endl;
  std::cout << ys << std::endl;
  xs[5] = '!';
  std::cout << xs << std::endl;
}

Die Zeichenkette besteht aus 5 Buchstaben, also ist ihr höchster Index die 4. Wenn wir am Index 5 ein neues Zeichen anspeichern, dann bekommen wir eine unerwartete Ausgabe, nämlich viele nach dem Ausrufezeichen nachfolgende Zeichen:
sep@linux:~/fh/prog3/examples/src> g++ -o CString CString.cpp
sep@linux:~/fh/prog3/examples/src> ./CString
hello
hello
hello!%/1?%/1#@ @@8%/1(@
sep@linux:~/fh/prog3/examples/src>

Intern hat jeder C-String ein Element mehr als Zeichen. In diesem letzten Element ist mit einer 0 markiert, daß der String jetzt zu Ende ist. Zeichenketten werden also intern durch ein Nullelement beendet. Dieses ist unabhängig von der eigentlichen technischen Länge der Reihung. Im obigen Beispiel haben wir diese Endemarkierung gelöscht, indem wir sie mit einem Ausrufezeichen überschrieben haben. Von da an betrachtete C alle nachfolgenden Werte im Speicher als Zeichen, die zu dem String gehören. Bei der Ausgabe werden alle diese Zeichen mit ausgegeben, obwohl die Reihung schon längst beendet ist.
Initialisiert man eine Reihung von Zeichen mit der Mengenklammeraufzählung ist der beendende Nullwert unbedingt zu berücksichtigen.
CString2
#include <iostream>

int main(){
  char hello[] = 
   { 'H', 'e', 'l', 'l', 'o', '\0' };
  std::cout << hello << std::endl;
}

Wird die Endemarkierung bei der Initialisierung vergessen, so blickt C unkontrolliert weiter in den Speicher und betrachtet alles nachfolgende als Zeichen der Zeichenkette, bis zufällig ein Nullwert kommt. Das folgende Programm erzeugt auch eine indeterministische Ausgabe:
CString3
#include <iostream>

int main(){
  char hello[] = 
   { 'H', 'e', 'l', 'l', 'o'};
  std::cout << hello << std::endl;
}

Für C ist eine Zeichenkette immer genau dann beendet, wenn der abschließende Nullwert auftritt. Dieses gilt auch, wenn er inmitten der Reihung auftritt.
Im folgenden Programm ist nach der Zuweisung des Nullwerts für das Element am Index 6 für C der String nach dem Wort hello beendet, obwohl die Reihung länger ist und noch weitere sinnvolle Zeichen in der Reihung gespeichert sind.
CString4
#include <iostream>

int main(){
  char hello [] = "hello world!";
  hello[5] ='\0';

  std::cout << hello << std::endl;
}

In traditioneller C-Programmierung gibt es typischer Weise Reihungen einer maximalen Länge, in denen unterschiedlich lange Strings gespeichert sind. Für das Programm bedeutet das, daß die Stringwerte an diesen Stellen eine bestimmte Maximallänge nicht überschreiten dürfen. Daher kommt in vielen älteren Programmen wahrscheinlich die Restriktion, daß bestimmte Namen, Pfade oder Optionen nur eine bestimmte Länge haben dürfen.
Programmieren von Strings in C ist ein mühsames Geschäft, was nur mit einer Vielzahl von bereitgestellten Bibliotheksfunktionen zu bewerkstelligen ist. In C++ steht eine umfangreiche Stringbibliothek in Form einer Stringklasse wie in Java zur Verfügung.

Kommandozeilenparameter

Auch für die Hauptmethode hat es Auswirkungen, daß C Reihungen ihre Elementanzahl nicht kennen. Unsere bisherigen Hauptmethoden hatten keine Argumente. Man kann zur Kommandozeilenübergabe in C aber auch eine Hauptmethode schreiben, der in einer Reihung die Kommandozeilenoptionen übergeben werden. Hierzu braucht man dann aber auch ein weiteres Argument, das die Länge der übergebenen Reihung mit angibt.
CommandLineOption
#include <iostream>

int main(int argc, char * argv []){  
  for (int i=0;i<argc;i++){
    std::string arg = argv[i];
    std::cout << arg << std::endl;
  }
}

Wie man an dem folgenden Testlauf sieht, wird als erstes Reihungselement der Name des Programms übergeben. Dieses war in Java nicht der Fall.
sep@linux:~/fh/prog3/examples/bin> ./CommandLineOption hallo C -v gruss
./CommandLineOption
hallo
C
-v
gruss
sep@linux:~/fh/prog3/examples/bin>

Gleichheit auf Reihungen

Da Reihungen Blöcke innerhalb des Speichers sind, ließe sich in C technisch eine Gleichheit auf Reihungen einfach realisieren. Für zwei Reihungen brauchen nur paarweise der Inhalt der Speicherzellen verglichen werden. C benutzt zum Arrayvergleich als Identitätsoperator ebenso wie Java die Adressidentität. Ebenso wie in Java, ist eine inhaltliche Gleichheit auszuprogrammieren:
CEQArray
#include <iostream>

bool arrayEq(int xs [],int xsLength, int ys [],int ysLength){
  if (xsLength!=ysLength) return false;
  for (int i=0;i<xsLength;i++){
    if (xs[i]!=ys[i]) return false;
  }
  return true;
}

std::string printBool(bool b){
  if (b) return "true"; else return "false";
}

int main(){
  int xs [] = {1,2,3,4};
  int ys [] = {1,2,3,4};
  int zs [] = {0,2,3,4};
  
  std::cout << printBool(xs==ys) << std::endl;
  std::cout << printBool(xs==zs) << std::endl;
  std::cout << printBool(arrayEq(xs,4,ys,4)) << std::endl;
  std::cout << printBool(arrayEq(xs,4,zs,4)) << std::endl;
}

Wir erhalten folgende Ausgabe:
sep@linux:~/fh/prog3/examples/bin> ./CEQArray
false
false
true
false
sep@linux:~/fh/prog3/examples/bin>

Mehrdimensionale Reihungen

In Java und in C  
In Java haben wir ganz generisch Reihungen von Reihungen definiert und benutzt. Hierzu brauchten wir nichts weiter zu beachten. Eine Reihung war dort ein ganz ordinärer Typ, mit dem dieselbe Konstrukte wie mit jedem anderen Typ auch gebaut werden konnten. Eine Mehrdimensionale Reihung war schlicht eine Reihung, deren Elemente wieder Reihungen waren. In C spielt eine mehrdimensionale Reihung eine kleine Sonderrolle. Die Syntax ist zunächst ähnlich der Syntax in Java:
JTwoDimArray
class JTwoDimArray{
  public static void main(String [] _){
   int xys [][] = new int [5][12];
   for (int x=0;x<5;x=x+1){
     for (int y=0;y<12;y=y+1){
       xys[x][y]=x+y;
     }
   }

   for (int x=0;x<5;x=x+1){
    for (int y=0;y<12;y=y+1){
     System.out.println(xys[x][y]);
    }
   }
  }
}



Und das entsprechende Programm in C:
TwoDimArray
#include <iostream>

int main(){
  int xys [5][12];
  for (int x=0;x<5;x=x+1){
    for (int y=0;y<12;y=y+1){
       xys[x][y]=x+y;
    }
  }

  for (int x=0;x<5;x=x+1){
    for (int y=0;y<12;y=y+1){
      std::cout << xys[x][y]<< std::endl;
     }
  }
}

Initialisierung durch Aufzählung  
In C können auch mehrdimensionale Reihungen durch die Schreibweise der Mengenklammern direkt durch eine Aufzählung der Elemente initialisiert werden. Allerdings ist hierbei zu beachten, daß C verlangt im Typnamen bereits die Länge der inneren Reihungen zu kennen. Deshalb führt das folgende Programm zu Fehlern in der Übersetzung:
WrongTwoDimArray
int main(){
  int xys [][]
    = {{11,12,13,14,15}
      ,{21,22,23,24,25}
      ,{31,32,33,34,35}
      ,{41,42,43,44,45}
    };
}

Der Gnu-Compiler erzeugt z.B. die folgende Fehlermeldung:
sep@linux:~/fh/prog3/examples/src> g++ WrongTwoDimArray.cpp
WrongTwoDimArray.cpp: In function `int main()':
WrongTwoDimArray.cpp:3: error: declaration of `xys' as multidimensional array
   must have bounds for all dimensions except the first
WrongTwoDimArray.cpp:7: error: assignment (not initialization) in declaration
sep@linux:~/fh/prog3/examples/src>


Geben wir hingegen in der Typdeklaration die Elementanzahl der inneren Reihungen an, so übersetzt dieses Programm.
TwoDimArray2
#include <iostream>

int main(){
  int xys [][5]
    = {{11,12,13,14,15}
      ,{21,22,23,24,25}
      ,{31,32,33,34,35}
      ,{41,42,43,44,45}
    };

  for (int x=0;x<4;x=x+1){
    for (int y=0;y<5;y=y+1){
      std::cout << xys[x][y]<< std::endl;
     }
  }
}

C verlangt eine feste Länge für die inneren Reihungen. Das war in Java nicht der Fall.
Beispiel:
Folgendes Javaprogramm erzeugt eine zweidimensionale Reihung, deren erste innere Reihung drei und deren zweite innere Reihung 4 Elemente hat. Eine solche mehrdimensionale Reihung ist in C nicht möglich.
DifferentLengths
class DifferentDims {  
  public static void main(String [] _){
    int [] xs = {1,2,3};
    int [] ys = {4,5,6,7};
    int [][] xys = {xs,ys};

    for (int x=0;x<2;x=x+1){
      for (int y=0;y<xys[x].length;y=y+1){
        System.out.println(xys[x][y]);
      }
    }
  }
}


Zeigerzugriff  
In Java hat der Zugriff auf die inneren Elemente einer mehrdimensionalen Reihung eine Reihung als Ergebnis gegeben. Wir können jetzt einmal versuchen, was C uns liefert, wenn wir nur einen einfachen Index benutzen:
TwoDimArray3
#include <iostream>

int main(){
  int xys [][5]
    = {{11,12,13,14,15}
      ,{21,22,23,24,25}
      ,{31,32,33,34,35}
      ,{41,42,43,44,45}
    };

  for (int x=0;x<4;x=x+1){
      std::cout << xys[x]<< std::endl;
  }
}

Das Programm hat folgende Ausgabe:
sep@linux:~/fh/prog3/examples/src> ./TwoDimArray3
0xbffff1b0
0xbffff1c4
0xbffff1d8
0xbffff1ec
sep@linux:~/fh/prog3/examples/src>

Wir bekommen wieder Speicheradressen, nämlich die Speicheradressen für jeweils das erste Element der inneren Reihung. Alle Elemente einer mehrdimensionalen Reihung liegen anscheinend hintereinander in einem zusammenhängenden Speicherbereich. Wir können daher uns auch der Zeigerarithmetrik bedienen, um mit einer einfachen und nicht mit zwei geschachtelten Schleifen, auf alle Elemente einer mehrdimensionalen Reihung zuzugreifen:
OneLoopOnly
#include <iostream>

int main(){
  int xys [][5]
    = {{11,12,13,14,15}
      ,{21,22,23,24,25}
      ,{31,32,33,34,35}
      ,{41,42,43,44,45}
    };
  int *pXys = &xys[0][0];

  for (int x=0;x<20;x=x+1){
      std::cout << *(pXys+x)<< std::endl;
  }
}

Warnung vor einer Notation  
Eine kleine Warnung noch. In C gibt es die Möglichkeit innerhalb eines eckigen Klammernpaares durch Komma getrennt zwei Indizes anzugeben. Dieses liegt an einem anderen Konstrukt in C. Durch Komma getrennte Ausdrücke haben den Wert des letzten Ausdrucks in dieser Liste. Im folgenden Programm ist dieses daran zu erkennen, daß die Ausgabe Adressen sind, also nur ein Index ausgewertet wurde:
NotTheIndexYouProbablyMean
#include <iostream>

int main(){
  int xys [][5]
    = {{11,12,13,14,15}
      ,{21,22,23,24,25}
      ,{31,32,33,34,35}
      ,{41,42,43,44,45}
    };

  for (int x=0;x<4;x=x+1){
    for (int y=0;y<5;y=y+1){
      std::cout << (x,y) << std::endl;
      std::cout << xys[x,y]<< std::endl;
     }
  }
}

Die Ausgabe des Programms ist z.B.:
sep@linux:~/fh/prog3/examples/src> ./NotTheIndexYouProbablyMean
0
0xbffff180
1
0xbffff194
2
0xbffff1a8
3
0xbffff1bc
4
0xbffff1d0
0
0xbffff180
1
0xbffff194
2
0xbffff1a8
3
0xbffff1bc
4
0xbffff1d0
0
0xbffff180
1
0xbffff194
2
0xbffff1a8
3
0xbffff1bc
4
0xbffff1d0
0
0xbffff180
1
0xbffff194
2
0xbffff1a8
3
0xbffff1bc
4
0xbffff1d0
sep@linux:~/fh/prog3/examples/src>

Der Ausdruck in den eckigen Indexklammern wird immer nur zum Wert von y ausgewertet.

Funktionen höherer Ordnung für Reihungen

Wir haben bereits gesehen, daß Funktionen über Funktionszeigern an andere Funktionen als Parameter übergeben werden können. Im Zusammenhang mit Reihungen eröffnet dieses ein mächtiges Programmierprinzip. Fast jede Funktion, die sich mit Reihungen beschäftigt, wird einmal jedes Element der Reihung durchgehen und dazu eine for-Schleife benutzen. Wie wäre es denn, wenn man dieses Prinzip verallgemeinert und eine allgemeine Funktion schreibt, die einmal jedes Element einer Reihung betrachtet und etwas mit ihm macht. Was mit jedem Element zu tun ist, wird dieser Funktion als Funktionszeiger übergeben.
Funktionen, die als Parameter Funktionen übergeben bekommen, werden auch als Funktionen höherer Ordnung bezeichnet.
Beispiel:
Wir schreiben für Reihungen ganzer Zahlen die Funktion map, die jedes Element einer Reihung mit einer übergebenen Funktion umwandelt:
IntMap
#include <iostream>

void map(int xs [],int l,int (*f) (int)){
  for (int i=0;i<l;i++){
    xs[i]=f(xs[i]);
  }
}

Wir stellen drei Funktionen, die eine ganze Zahl als Parameter und Ergebnis haben zur Verfügung:
IntMap
int add5(int x){return x+5;}
int square(int x){return x*x;}
int doubleX(int x){return 2*x;}

Jetzt können wir die Funktion map verwenden, um für jedes Element einer Reihung, eine derartige Funktion anzuwenden. Zur Ausgabe von Reihungen schreiben wir eine kleine Hilfsfunktion:
IntMap
void printArray(int xs [],int l){
  std::cout<<"{";
  for (int i=0;i<l-1;i++){
    std::cout << xs[i]<<", ";
  }
  std::cout << xs[l-1] << "}"<<std::endl;
}

int main(){
  int xs [] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
  map(xs,15,add5);
  printArray(xs,15);
  map(xs,15,square);
  printArray(xs,15);
  map(xs,15,doubleX);
  printArray(xs,15);
}

Und hier die Ausgabe des Programms:
sep@linux:~/fh/prog3/examples/src> ./IntMap
{6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
{36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400}
{72, 98, 128, 162, 200, 242, 288, 338, 392, 450, 512, 578, 648, 722, 800}
sep@linux:~/fh/prog3/examples/src>

Zunächst wird auf jedes Element der Reihung 5 aufaddiert. Dann werden die Elemente quadriert und schließlich verdoppelt.
Aufgabe 6  
Sie sollen in dieser Aufgabe die Sortiermethode des Bubble-Sort, wie sie im ersten Semester vorgestellt wurde, für Reihungen von ganzen Zahlen umsetzen:
Aufgabe 7   (2 Punkte) Schreiben Sie eine Funktion fold mit folgender Signatur:
int fold(int xs [],int length,int (*foldOp)(int,int),int startV)
Die Spezifikation der Funktion sei durch folgende Gleichung gegeben:
fold(xs,l,op,st) = op(... op(op(op(st,xs[0]),xs[1]),xs[2])...,xs[l-1])
Oder bei Infixschreibweise der Funktion op gleichbedeutend über folgende Gleichung:
fold({x0,x1,,...,xl-1},l,op,st) = st op x0 op x1 op x2 op ... op xl-1
Testen Sie Ihre Methode fold mit folgenden Programm:
FoldTest
#include <iostream>

int add(int x,int y){return x+y;}
int mult(int x,int y){return x*y;}

int fold(int xs [],int length,int (*foldOp)(int,int),int startV);

int main(){
  int xs [] = {1,2,3,4,5};
  std::cout << fold(xs,5,add,0)<< std::endl;
  std::cout << fold(xs,5,mult,1)<< std::endl;
}

2.2.3  Produkte und Summen von Typen

Betrachten wir einmal Typen als die Menge aller Werte die von diesem Typ sind. So können wir aus zwei Typen einen neuen Typ konstruieren. In der theoretischen Informatik unterscheidet man hierbei zwei Konstruktionsprinzipien: Produkt und Summe.
Als das Produkt zweier Typen T1 und T2 bezeichnet man den Typ, der aus Paaren von Werten der beiden Grundtypen besteht. Die Bezeichnung Produkt ist in diesem Fall gerechtfertigt: habe T1 n Elemente und T2 m Elemente; der Produkttyp T1ÄT2 hat dann n*m Elemente.
Als Summe zweier Typen T1 und T2 bezeichnet man den Typ, dessen Elemente entweder einen Wert des einen oder des anderen Typs hat. Die Bezeichnung Summe ist in diesem Fall gerechtfertigt: habe T1 n Elemente und T2 m Elemente; der Summentyp T1ÅT2 hat dann n+m Elemente.
In Java haben wir ohne viel darüber nachzudenken Produkt und Summentypen gebildet. Produkttypen ergeben sich aus den Feldern einer Klasse. Folgende Klasse bildet den Produkttyp für String und int.
StringIntProduct
class StringIntProduct{
  String typ1;
  int    typ2;
  public StringIntProduct(String e1, int e2){
    typ1=e1;
    typ2=e2;
  }
}

Summentypen können in Java über Vererbung gebildet werden. Bilden wir als Beispiel die Summe von int und String. Hierzu definieren wir eine Klasse, die die Summe ausdrücken soll:
StringIntSum
class StringIntSum {}

Als Unterklassen dieser Summenklasse definieren wir jetzt jeweils eine Klasse für die beiden summierten Typen.
StringTyp
class StringTyp extends StringIntSum {
  StringTyp(String v){value=v;}
  String value;
}

IntTyp
class IntTyp extends StringIntSum {
  IntTyp(int v){value=v;}
  int value;
}

In C gibt es zwei explizite Typkonstrukte für Summen und Produkte.

Typprodukt mit Strukturen

Strukturen können als eine sehr rudimentäre Vorstufe von Klassen in C betrachtet werden. In anderen Programmiersprachen spricht man auch gerne von records. In C++ sind Strukturen eigentlich nicht mehr notwendig. Dort werden sie vollständig von Klassen subsumiert.
Strukturen werden ähnlich wie Klassen definiert: dem Schlüsselwort struct folgt der Name den man der Struktur geben will, anschließend folgt in geschweiften Klammern eingeschlossen eine Attributliste, entsprechend den Feldern einer Klasse. Ein Analogon zu Methoden in Klassen gibt es für Strukturen nicht. Ebenso keine Vererbung zwischen Strukturen. Elemente eines Strukturtyps werden durch die Aufzählung der konkreten Werte für die einzelnen Attribute der Struktur erzeugt. Diese Aufzählung steht in geschweiften Klammern und ist durch Kommas getrennt. Auf die einzelnen Attribute einer bestehenden Struktur kann durch die für Klassen bekannte Punktnotation zugegriffen werden.
Beispiel:
Im folgenden Beispiel definieren wir eine Struktur zur Speicherung von Personendaten. Eine Instanz wird gebildet, auf der mehrere Test zur internen Speicherablage von Strukturen gemacht werden.
PersonStruktur
#include <string>
#include <iostream>

struct Person 
  {std::string name
  ;std::string vorname
  ;std::string strasse
  ;std::string stadt
  ;int plz
  ;int gebjahr
  ;int gebmonat
  ;int gebtag
  ;};

int main(){
  struct Person p1
    = {"Zabel", "Walther", "Hauptstrasse", "Ludwigshafen"
      , 65345, 1962, 3, 26};

  std::cout << p1.stadt << std::endl;

  std::cout << sizeof(p1) << std::endl;
  std::cout
   << &p1.name    << std::endl << &p1.vorname << std::endl
   << &p1.strasse << std::endl << &p1.stadt   << std::endl
   << &p1.plz     << std::endl << &p1.gebjahr << std::endl
   << &p1.gebmonat<< std::endl << &p1.gebtag  << std::endl;
}

Der Programmausgabe kann man entnehmen, daß der Übersetzer die Attribute einer Struktur direkt hintereinander im Speicher ablegt.
sep@linux:~/fh/prog3/examples/src> ./PersonStruktur
Ludwigshafen
32
0xbffff140
0xbffff144
0xbffff148
0xbffff14c
0xbffff150
0xbffff154
0xbffff158
0xbffff15c
sep@linux:~/fh/prog3/examples/src>


Zuweisung von Strukturen  
Für Reihungen war es nicht möglich eine Zuweisung zwischen zwei Reihungsvariablen zu machen. Der Übersetzer hat dieses bereits zurückgewiesen. Strukturvariablen können einander zugewiesen werden. Allerdings werden wir hier ein anderes Verhalten beobachten, als wir es mit unseren Javahintergrund vermutet hätten.
Beispiel:
Wir definieren eine einfache Struktur, erzeugen eine Instanz und weisen diese einer zweiten Strukturvariablen zu:
AssignStruct
#include <string>
#include <iostream>

struct S {std::string n1; std::string n2;};

int main(){
  struct S s1 = {"hallo","welt"};
  struct S s2 = s1;

  std::cout << s1.n1 << " " << s1.n2 << std::endl;
  std::cout << s2.n1 << " " << s2.n2 << std::endl;

  s2.n2 ="berlin";

  std::cout << s1.n1 << " " << s1.n2 << std::endl;
  std::cout << s2.n1 << " " << s2.n2 << std::endl;
}

An der Ausgabe können wir erkennen, daß die beiden Variablen s1 und s2 sich nicht die gleichen Daten teilen. Eine Änderung in der einen Struktur bewirkt keine Änderung in der zweiten Struktur.
sep@linux:~/fh/prog3/examples/src> c++ -o AssignStruct AssignStruct.cpp
sep@linux:~/fh/prog3/examples/src> ./AssignStruct
hallo welt
hallo welt
hallo welt
hallo berlin
sep@linux:~/fh/prog3/examples/src>

Der Grund hierfür ist, daß die Zuweisung auf Strukturen eine Kopie der Struktur auf der rechten Seite bewirkt. Wir bekommen ein Verhalten, wie wenn wir in Java explizit die Methode clone auf das Objekt der rechten Seite angewendet hätten.
Strukturen als Parameter  
Die Parameterübergabe an eine Funktion ist eine Art Zuweisung. Dem Funktionsparameter wird der Wert zugewiesen. Und auch hier gilt für Strukturen dasselbe wie für die Zuweisung: der Funktion wird eine Kopie der Instanz der Struktur übergeben.
Beispiel:
Schreiben wir für eine einfache Struktur für Personen eine Funktion, die den Namen ändern soll. Diese funktioniert nicht, wenn wir die Struktur übergeben, sondern nur wenn wir einen Zeiger auf die Struktur übergeben.
PassStruct
#include <string>
#include <iostream>

struct Person 
  {std::string name
  ;std::string vorname
  ;};

void doNotChangeName(struct Person p,std::string n){
    p.name = n;
}

void changeNameWithPointer(struct Person * p,std::string n){
    (*p).name = n;
}

int main(){
  struct Person p1
    = {"Brecht", "Bertholt"};
  doNotChangeName(p1,"Schmidt");
  std::cout << p1.name << std::endl;

  changeNameWithPointer(&p1,"Schmidt");
  std::cout << p1.name << std::endl;
}

Die Ausgabe bestätigt uns, daß tatsächlich bei der Übergabe einer Struktur an eine Funktion, die Struktur kopiert wird.
sep@linux:~/fh/prog3/examples/src> g++ -o PassStruct PassStruct.cpp
sep@linux:~/fh/prog3/examples/src> ./PassStruct
Brecht
Schmidt
sep@linux:~/fh/prog3/examples/src>

Strukturen in Strukturen  
Die Attribute einer Struktur können auch wieder Strukturen sein.
StructOfStruct
#include <string>
#include <iostream>
 
struct S1 {std::string n;std::string n2;std::string n3;};

struct S2 {std::string n;struct S1 s;};

int main (){
  S1 s1 = {"hello","wie","geht's"};
  S2 s2 = {"welt",s1};
  std::cout << s2.s.n << " " << s2.n << std::endl;

  std::cout << sizeof(s1) << std::endl;
  std::cout << sizeof(s2) << std::endl;
}


Wie man sieht, können verschiedene Strukturen gleiche Attributnahmen haben.
Der Ausgabe dieses Programms kann man entnehmen, daß bei einer Struktur tatsächlich die Attribute direkt hintereinander gespeichert werden:
sep@linux:~/fh/prog3/examples/src> ./StructOfStruct
hello welt
12
16
sep@linux:~/fh/prog3/examples/src>

In der Struktur s2 wird kein Zeiger auf die Instanz der Struktur s1 abgelegt, sondern die komplette Instanz der Struktur s2.
Rekursive Strukturen  
Wir haben uns im ersten Semester ausgiebig mit Listen als Beispiel einer rekursiven Datenstruktur beschäftigt. Mit Strukturen haben wir auch in C eine Möglichkeit bekommen, rekursive Datentypen zu definieren. Hierzu muß es lediglich innerhalb der Struktur ein Attribut geben, daß einen Zeiger auf eine Instanz von eben jener Struktur enthält. Damit können wir unsere Listenspezifikation aus dem ersten Semester in C++-Strukturen umsetzen. Unsere Spezifikation bestand aus 2 Konstruktoren Cons und Empty, den Selektoren head und tail, sowie einer Testfunktion isEmpty.
In C können wir eine entsprechende Struktur definieren. In einer Kopfdatei spezifizieren wir die für Listen relevanten Funktionen, die zwei Konstruktoren, die zwei Selektoren, die Testfunktion isEmpty und eine weitere Beispielfunktion auf Listen: length. Damit präsentieren sich Listen auch in C fast so, wie wir es bereits aus Java gewohnt sind.
IntList
struct IntList {int head;IntList *tail;bool isEmpty;};
  
IntList *Cons(int hd,IntList* tl);
IntList *Empty();

int head(IntList *dieses);
IntList *tail(IntList *dieses);
bool isEmpty(IntList *dieses);

int length(IntList * dieses);

Der große Unterschied zu der Implementierung über Klassen ist, daß alle Selektorfunktionen und Testfunktionen die Liste als Parameter benötigen. In der entsprechenden Javaklasse ist dieser Parameter implizit vorhanden durch das Objekt, auf das die Methode aufgerufen wird. Dort konnte dieser eigentlich implizite Parameter über das Schlüsselwort this angesprochen werden. In C Strukturen müssen wir diesen impliziten this-Parameter explizit machen und der Funktion übergeben.
In der Implementierungsdatei setzen wir unsere Spezifikation um. In den beiden Konstruktoren ist zunächst mit dem new-Operator Speicher für die zu konstruierende Liste anzufordern. Die Attribute sind entsprechend zuzuweisen und die neu erzeugte Instanz der Struktur zurückzugeben. Die Selektoren und die Testfunktion greifen jeweils nur auf eines der Attribute zu, die Funktion length ist identisch implementiert, wie wir es aus dem ersten Semester kennen.
IntList
#include "IntList.h"

IntList *Cons(int hd,IntList* tl){
  IntList * result = new IntList;
  (*result).head=hd;
  (*result).tail=tl;
  (*result).isEmpty=false;
  return result;
}

IntList *Empty(){
  IntList * result = new IntList;
  (*result).isEmpty=true;
  return result;
}

int head(IntList *dieses){
  return (*dieses).head;
}
IntList *tail(IntList *dieses){
  return (*dieses).tail;
}
bool isEmpty(IntList *dieses){
  return (*dieses).isEmpty;
}

int length(IntList * dieses){
  if (isEmpty(dieses)) return 0;
  return 1+length((*dieses).tail);
}

Zum Testen erzeugen wir eine kleine Liste und geben die Länge ihrer Restliste aus.
TestIntList
#include <iostream>
#include "IntList.h"

int main(){
  IntList *xs = Cons(1,Cons(2,Cons(3,Cons(4,Empty()))));
  std::cout << length(tail(xs))<<std::endl;
}

sep@linux:~/fh/prog3/examples/src> g++ -c IntList.cpp
sep@linux:~/fh/prog3/examples/src> g++ -c TestIntList.cpp
sep@linux:~/fh/prog3/examples/src> g++ -o TestIntList IntList.o TestIntList.o
sep@linux:~/fh/prog3/examples/src> ./TestIntList
3
sep@linux:~/fh/prog3/examples/src>

Benutzen wir die obige Listenimplementierung aus C genauso wie wir es in Java gewohnt waren, so stoßen wir irgendwann auf ein ernstes Speicherproblem. Jeder Aufruf einer der beiden Konstruktorfunktionen Cons und Empty fordert neuen Speicherplatz an. Wenn wir Listen in unserem Programm erzeugt haben, die nicht mehr benutzt werden, für die es sogar keine Möglichkeit mehr gibt, sie zu benutzen, weil keine Variable, oder auch keine Verzeigerung mehr auf ihren Speicherbereich zeigt, so werden diese trotzdem weiterhin im Speicher stehen. In Java wurden solche Daten irgendwann automatisch aufgeräumt. Javas virtuelle Maschine hat eine automatische Speicherverwaltung, die nicht mehr benutze Datenübereste im Speicher aufräumt. C hat dieses nicht. In C müssen wir solche Speicherbereiche, wie wir bereits gesehen haben, explizit mit dem Operator delete wieder freigeben.
Und jetzt einmal ganz in C  
Die Listenimplementierung im letzten Abschnitt benutzt zwar keine objektorientierten Eigenschaften von C++, benutzt aber eine Vielzahl bequemer Notationen und Konstrukte aus C++, die in C noch mühsam von Hand kodiert werden müssen. Wie wir bereits wissen, gibt es keine bool'schen Werten in C. Wir müssen uns hierzu eine eigene Aufzählung definieren. Desweiteren existiert nicht der bequeme Operator << zur Ausgabe auf Ströme in C. Bei einer genauen Betrachtung des obigen C++ Programms, kann man sehen, daß wir oft auf das Schlüsselwort struct beim Typ IntList verzichtet haben. Der C++-Übersetzer hat aus dem Typnamen allein abgeleitet, daß es sich um eine Struktur handelt. In C geht dieses nicht.
Diese kleinen Unterschiede spiegeln sich schon in der Kopfdatei der C Implementierung wieder:
CIntList
enum bool {false,true};

struct IntList{
  int head;
  struct IntList* tail;
  enum bool empty;
};
  
struct IntList *Cons(int hd,struct IntList* tl);
struct IntList *Empty();

int head(struct IntList *dieses);
struct IntList *tail(struct IntList *dieses);
enum bool isEmpty(struct IntList *dieses);

int length(struct IntList* dieses);

Aber zu allervorderst gibt es keinen Operator new in C. In C wird Speicher angefordert, indem man lediglich spezifiziert, wieviel Speicherplatz man benötigt, für das, was zu speichern ist. Hierzu gibt es in C die Funktion void* malloc(size_t n);. Ihr ist als Argument mitzugeben, wieviel Speicher angefordert wird. Ihr Ergebnis ist ein Zeiger auf den neuen Speicherbereich. Der Typ für die Daten, auf die der Zeiger zeigt, ist void. Es ist ja noch nicht angegeben worden, was für Daten dort zu speichern sind. Durch eine Typzusicherung mit der gleichen Notation wie aus Java bekannt, werden diese Daten dann erst als Daten eines bestimmten Typs interpretiert. In unserer C Implementierung werden entsprechend die new-Ausdrücke durch Aufrufe an malloc ersetzt. Damit sieht die C Implementierung wie folgt aus.
CIntList
#include "CIntList.h"
  
struct IntList *Cons(int hd,struct IntList* tl){
  struct IntList * result
     = (struct IntList*) malloc(sizeof(struct IntList));
  (*result).head=hd;
  (*result).tail=tl;
  (*result).empty=false;
  return result;
}

struct IntList *Empty(){
  struct IntList * result
    = (struct IntList*) malloc(sizeof(struct IntList));
  (*result).empty=true;
  return result;
}

int head(struct IntList *dieses){
  return (*dieses).head;
}
struct IntList *tail(struct IntList *dieses){
  return (*dieses).tail;
}

enum bool isEmpty(struct IntList *dieses){
  return (*dieses).empty;
}

int length(struct IntList* dieses){
  if (isEmpty(dieses)) return 0;
  return 1+length((*dieses).tail);
}

Zum Testen ist jetzt schließlich lediglich zu beachten, daß statt des C++ Operators << ein Funktionsaufruf auf eine Standardausgabefunktion aus C zu erfolgen hat:
TestCIntList
#include "CIntList.h"

int main(){
  struct IntList *xs
    = Cons(1,Cons(2,Cons(3,Cons(4,Empty()))));
  printf("%i\n",length(tail(xs)));
}

Der Test liefert wieder das zu erwartende Ergebnis:
sep@linux:~/fh/prog3/examples/src> gcc -c TestCIntList.c
sep@linux:~/fh/prog3/examples/src> gcc -c CIntList.c
sep@linux:~/fh/prog3/examples/src> gcc -o TestCIntList TestCIntList.o CIntList.o
sep@linux:~/fh/prog3/examples/src> ./TestCIntList
3
sep@linux:~/fh/prog3/examples/src>

Aufgabe 8   Implementieren Sie für unsere in C++ implementierten Listenstruktur die folgenden Funktionen (nach der Spezifikation aus dem ersten Semester):
Die Pfeilnotation  
Im letzten Beispiel haben wir oft mit Zeigern auf Strukturen zu tun gehabt. Um auf die Attribute dieser Strukturen zuzugreifen muß erst der Zeiger aufgelöst werden, um dann schließlich mit der Punktnotation auf die referenzierte Struktur zuzugreifen. Wir erhalten dann einen Ausdruck wie z.B.: (*dieses).head;. Ausdrücke dieser Art kommen sehr häufig beim Arbeiten mit C Strukturen und auch mit C++ Klassen vor. Daher gibt es eine abkürzende Schreibweise hierfür: den Operator ->, der einen Pfeil darstellen soll. Es läßt sich also dieses->head statt (*dieses).head; schreiben.
Beispiel:
Ein kleines C Beispiel zum Gebrauch der Pfeilnotation.
Arrow
struct Person 
  {char* name
  ;char* vorname
  ;};

void setName(struct Person* p,char* newName){
  p->name=newName;
}

int main(){
  struct Person p1 = {"Hotzenplotz", "Walther"};
  struct Person * pP1 = &p1;
  printf("%s\n",p1.name);
  printf("%s\n",(*pP1).name);
  printf("%s\n",pP1->name);
  setName(pP1,"Zabel");
  printf("%s\n",p1.name);
  printf("%s\n",(*pP1).name);
  printf("%s\n",pP1->name);
}

Die drei Arten auf den Namen der Struktur zuzugreifen, ergeben jeweils dasselbe Ergebnis.
Objektorientierung mit Strukturen simulieren  
Die ursprünglichen C Strukturen sind bereits sehr nah an der objektorientierten Programmierung. Drei entscheidene Eigenschaften der Objektorientierung fehlen:
In diesem Abschnitt wollen wir einmal versuchen, ob die Sprache C nicht schon mächtig genug ist, um diese drei Konzepte zu implementieren; und wir werden sehen, daß mit einigen Klimmzügen dieses tatsächlich möglich ist.
Zunächst einmal gibt es ein ganz einfachen Trick, wie in Strukturen Methoden integriert werden können. Hierzu kann eine Methode außerhalb der Struktur definiert werden, um dann in der Struktur selbst einen Funktionszeiger auf diese Funktion gespeichert werden. Damit wir in den Objektmethoden auf die Felder des Objektes zugreifen können, muß die Funktion einen Zeiger auf dieses Objekt zur Verfügung haben, den sogenannten this-Zeiger.
Versuchen wir mit diesen Mitteln eine Pseudoklasse für Personen in C zu definieren. Hierzu definieren wir eine Struktur mit den entsprechenden Feldern, und einen Feld für den Funktionszeiger auf die Objektmethode getFullName.
CPerson
// pseudo class for persons
struct CPerson 
  {char* name
  ;char* vorname
  ;char* strasse
  ;char* stadt
  ;int plz
  ;char* (*getFullNameFunction)(struct CPerson*)
  ;};

//pseudo instance method for some this object 
char* getFullName(struct CPerson * this);

//constructor for CPerson
struct CPerson* newPerson 
  (char* n, char* v,char* str,char* st,int p);

Als nächstes versuchen wir eine Unterklasse der Pseudoklasse CPerson zu definieren. Diese soll alle Eigenschaften von CPerson haben, und zusätzlich noch eine Matrikelnummer. Jetzt können wir uns die Tatsache zu Nutze machen, daß die einzelnen Attribute einer Struktur direkt hintereinander im Speicher abgelegt werden können. Es reicht somit aus, ein Attribut für eine Instanz der Pseudosuperklasse vorzusehen und danach die zusätzlichen Attribute zu definieren.
CPerson
// pseudo subclass of CPerson for students
struct CStudent 
  {struct CPerson super
  ;int matrNr;
  };

//constructor for CStudent
struct CStudent* newStudent
  (char* n, char* v,char* str,char* st,int p,int ma);

Soweit die Schnittstellenbeschreibung der beiden gewünschten Pseudoklassen. Bevor wir diese als objektorientierte Bibliothek implementieren, können wir zunächst ein einfaches Testbeispiel für diese Kopfdatei schreiben. Wir wollen in der Lage sein, Personen und Studenten zu erzeugen. Studenten sollen in Variablen für Felder abgelegt werden können. Der Aufruf der Funktion getFullName soll über late-binding jeweils die entsprechende Objektmethode für CPerson bzw. CStudent ausführen.
TestCPerson
#include "CPerson.h"

int main(){
  //create a person object
  struct CPerson* p1
   = newPerson("Schmidt","Harald","studio449","koeln",54566);

  //create a student object, but store it in a Person variable
  struct CPerson* p2 
   = newStudent("Andrak","Manuel","studio449","koeln",54566
               ,565456);
 
  //print the full names of the two person objects
  printf("die erste Person ist: %s\n",getFullName(p1));
  printf("der zweite Person ist: %s\n",getFullName(p2));

  //make a cast of the second person to student
  printf("mit Matrikel-Nr.: %i\n",((struct CStudent*)p2)->matrNr);
}

Soweit schon einmal der Testfall für unsere kleine Bibliothek. Es gilt jetzt die beschriebenen Schnittstellen so zu implementieren, daß wir verschiedene Methoden getFullName für Personen und Studenten erhalten und über späte Bindung auf die für das ursprünglich erzeugte Objekt geschriebene Methode zugegriffen wird. Zunächst einmal schreiben wir die zwei Methoden, die für Personen bzw.  Studenten den vollen Namen errechnen. Da wir uns auf einfaches C beschränken wollen, haben wir keine Klasse zur Darstellung von Zeichenketten sondern nur Reihungen von Zeichen und müssen Funktionen für diese aus den Standardbibliotheken benutzen:
CPerson
#include "CPerson.h"
#include <string.h>
#include <stdlib.h>

char* getFullPersonName(struct CPerson* this){
  size_t nameLength = strlen (this->name);
  size_t vornameLength = strlen (this->vorname);
  char* result
   = malloc((nameLength + vornameLength+3) * sizeof(char));
  char* end = result; 
  end=(char*)stpcpy(end,this->name);  
  end=(char*)stpcpy(end,", ");  
  end=(char*)stpcpy(end,this->vorname);  
  return result;
}

char* getFullStudentName(struct CPerson* this){
  size_t nameLength = strlen (this->name);
  size_t vornameLength = strlen (this->vorname);
  char* result
   = malloc((nameLength + vornameLength+3) * sizeof(char));
  char* end = result; 
  end=(char*)stpcpy(end,this->vorname);  
  end=(char*)stpcpy(end," ");  
  end=(char*)stpcpy(end,this->name);  
  return result;
}

Es fehlen uns schließlich nur noch die Implementierungen der Konstruktormethoden. Diese splitten wir in zwei Teile, einen erzeugenen Teil und einen Initialisierungsteil. Hier zunächst der Initialisierungsteil für Personen. Einem Personenobjekt werden die einzelnen Werte für seine Felder zugewiesen:
CPerson
void initPerson(struct CPerson* result
               ,char* n, char* v,char* str,char* st,int p){
  result->name = n;
  result->vorname = v;
  result->strasse = str;
  result->stadt = st;
  result->plz = p;
  result->getFullNameFunction = &getFullPersonName;
}

Insbesondere sei bemerkt, daß die Funktion zur Berrechnung eines vollen Namens mit der entsprechenden Funktion für Personen initialisiert wird.
Der eigentliche Konstruktor für Personen, fordert über die C Funktion malloc genügend Speicherplatz für ein Personenobjekt an und führt die Initialisierung in diesem neuen Speicherbereich durch:
CPerson
struct CPerson* newPerson
   (char* n, char* v,char* str,char* st,int p){
  struct CPerson* result 
    = (struct CPerson*) malloc(sizeof(struct CPerson));
  initPerson(result, n,  v, str, st, p);
  return result;
}

Der Konstruktor für Studenten fordert zunächst den notwendigen Speicher für einen Studenten an, initialisiert dann mit den Initialisator der Superklasse den Personenteil für Studenten. Dieses entspricht durchaus den Aufruf des Superkonstruktors, wie wir ihn aus Java kennen. Schließlich wird der Funktionszeiger überschrieben mit der für Studenten spezifischen Funktion, und zu guter letzt wird das neue Feld der Matrikelnummer initialisiert.
CPerson
struct CStudent* newStudent
   (char* n, char* v,char* str,char* st,int p,int ma){
  struct CStudent* result 
    = (struct CStudent*) malloc(sizeof(struct CStudent));
  initPerson(&(result->super), n,  v, str, st, p);

  (result->super).getFullNameFunction = &getFullStudentName;

  result->matrNr = ma;

  return result;
}

Wollte man noch eine Unterklasse von CStudent schreiben, so wäre es vorteilhaft gewesen, auch für Studenten schon zwischen Konstruktorfunktion und Initialisator zu unterscheiden.
Schließlich können wir noch eine Funktion vorsehen, die für eine Person oder eine beliebige Unterklasse davon, die Objektfunktion getFullName aufruft.
CPerson
char* getFullName(struct CPerson * this){    
  return (*(this->getFullNameFunction))(this);
};

Und tatsächlich, unser Testprogramm zeigt, daß wir es geschafft haben, späte Bindung in C zu implementieren:
sep@linux:~/fh/prog3/examples/src> gcc -o TestCPerson TestCPerson.c CPerson.c
TestCPerson.c: In function `main':
TestCPerson.c:11: Warnung: initialization from incompatible pointer type
sep@linux:~/fh/prog3/examples/src> ./TestCPerson
die erste Person ist: Schmidt, Harald
der zweite Person ist: Manuel Andrak
mit Matrikel-Nr.: 565456
sep@linux:~/fh/prog3/examples/src>

Wir bekommen lediglich eine Warnung bei der Zuweisung eines Studenten auf eine Variable für Personenreferenzen. Von dieser Warnung lassen wir uns aber in diesem Fall nicht stören.

Typsumme mit Unions

Im letzen Abschnitt haben wir mittels Strukturen Produkttypen zwischen mehreren Typen definiert. Die zweite aus der theoretischen Informatik bekannte Typbildung ist die Summe von Typen. Auch hierfür stellt C ein eigenes Konstrukt zur Verfügung: die Vereinigung von Typen, die union.
Syntaktisch sieht eine Typvereinigung einer Struktur sehr ähnlich. Lediglich das Schlüsselwort struct ist durch das Schlüsselwort union zu ersetzen. Somit läßt sich eine Vereinigung der Typen int und char* definieren als:
union stringOrInt {int i;char *s;};.
Daten des Typs union stringOrInt sind nun entweder eine Zahl, die über das Attribut i angesprochen werden kann, oder der Zeiger auf das erstes Element einer Zeichenkette, der über das Attribut s angesprochen werden kann.
Beispiel:
Im folgenden kleinem Programm wird eine Typsumme definiert und beispielhaft in einer Hauptmethode benutzt.
Union1
#include <iostream>

union stringOrInt {int i;char *s;};

int main(){
  union stringOrInt sOrI;
  sOrI.s="hallo da draussen";
  std::cout << sOrI.s << std::endl;

  sOrI.i=42;
  std::cout << sOrI.i << std::endl;
}

Wie man der Ausgabe entnehmen kann, wird in der Variablen des Typs stringOrInt entweder ein int oder eine Zeichenkettenreferenz gespeichert:
sep@linux:~/fh/prog3/examples/src> g++ -o Union1 Union1.cpp
sep@linux:~/fh/prog3/examples/src> ./Union1
hallo da draussen
42
sep@linux:~/fh/prog3/examples/src>

Im letzten Beispiel haben wir die Typsumme gutartig benutzt. Die Typsumme drückt aus, daß die Daten entweder von einem oder vom anderen Typ sind. Im Beispiel haben wir uns gemerkt, welchen Typ wir als letztes in der entsprechenden Variablen gespeichert haben und nur auf diesen Typ zugegriffen. C bewahrt uns aber nicht davor, dieses immer korrekt zu tun.
Beispiel:
In diesem Beispiel benutzen wir den selben Summentypen wie im vorhergehenden Beispiel. Jetzt interpretieren wir aber jeweils die Daten auch als die andere Typalternative.
Union2
#include <iostream>

union stringOrInt {int i;char *s;};

int main(){
  union stringOrInt sOrI;
  sOrI.s="hallo da draussen";
  std::cout << sOrI.s << std::endl;
  std::cout << sOrI.i << std::endl;

  sOrI.i=42;
  std::cout << sOrI.i << std::endl;
  std::cout << sOrI.s << std::endl;
}

Wenn wir jetzt einen Adresse auf einen Zeichenkette in der Variablen sOrI gespeichert haben und auf die Variable mit ihrem int-Attribut i zugreifen, so bekommen wir die gespeicherte Adresse als Zahl geliefert.
Wenn wir umgekehrt eine Zahl gespeichert haben und diese nun als Adresse einer Zeichenkette lesen wollen, so kommt es in der Regel zu einem Fehler, weil wir höchstwahrscheinlich versuchen eine Adresse im Speicher zu lesen, auf die wir nicht zugreifen können.
sep@linux:~/fh/prog3/examples/src> ./Union2
hallo da draussen
134514920
42
Speicherzugriffsfehler
sep@linux:~/fh/prog3/examples/src>

Um Fehler, wie im letzem Beispiel zu vermeiden, ist es ratsam Strukturen und Typsummen geschachtelt zu verwenden. Hierzu kapselt man die Typsumme in eine Struktur. In dieser Struktur gibt ein zweites Attribut an, um welchen Typ es sich gerade in der Typsumme handelt. Für das Attribut, daß den Typ kennzeichnet, bietet sich an, einen Aufzählungstypen zu verwenden.
Beispiel:
In diesem Beispiel definieren wir eine Typsumme, die in einer Struktur eingebettet ist. Dazu definieren wir drei Typen: eine Aufzählung, eine Typsumme und eine Struktur.
Union3
#include <iostream>

enum TypeMarker {INT,STRING};
union stringOrInt {int i;char * s;};

struct strOrInt {TypeMarker type;union stringOrInt v;};

Jetzt können wir uns zwei Funktionen schreiben, die jeweils eine der beiden Varianten unseres Summentypens konstruieren:
Union3
struct strOrInt forInt(int i){
  strOrInt result;
  result.type=INT;
  result.v.i=i;
  return result;
}

struct strOrInt forString(char * s){
  strOrInt result;
  result.type=STRING;
  result.v.s=s;
  return result;
}

Schließlich ein Beispiel für eine Funktion, die den neuen Typen benutzt. Hierzu ist zunächst zu fragen, von was für einen Typ denn die gespeichert sind, um dann entsprechend mit den Daten zu verfahren:
Union3
char * toString(struct strOrInt si){
  char* result;
  if (si.type==INT){
    sprintf(result,"%d",si.v.i);
  }else if (si.type==STRING){
    result=si.v.s;
  }
  return result;
}

Abschließend ein kleiner Test in einer Hauptmethode:
Union3
int main(){
  strOrInt test = forString("hallo da draussen");
  std::cout << toString(test)<<std::endl;
  test = forInt(42);
  std::cout << toString(test)<<std::endl;
}

In der Ausgabe können wir uns davon überzeugen, daß tatsächlich immer die Daten der Typsumme korrekt interpretiert werden:
sep@linux:~/fh/prog3/examples/src> g++ -o Union3 Union3.cpp
sep@linux:~/fh/prog3/examples/src> ./Union3
hallo da draussen
42
sep@linux:~/fh/prog3/examples/src>

Schließlich ist noch interessant, wieviel Speicher C für eine Typsumme reserviert. Auch dieses können wir experimentel erfragen:
Union4
#include <iostream>

union U1 {char a1 [15]; char a2 [200];};
union U2 {int a1; char a2 [16];};

int main(){
  U1 u1;
  U2 u2;
  std::cout << sizeof(u1) << std::endl;
  std::cout << sizeof(u2) << std::endl;
 }

An der Ausgabe ist zu erkennen, daß der maximal notwendige Speicher für einen der Typen in der Summe angefordert wird.
sep@linux:~/fh/prog3/examples/src> ./Union4
200
16
sep@linux:~/fh/prog3/examples/src>

Chapter 3
Generische und Überladene Funktionen

3.1  Generische Funktionen

Die Idee einer generischen Funktion ist, eine Funktionsdefinition zu schreiben, die für mehrere verschiedene Parametertypen funktioniert. In funktionalen Sprachen werden generische Funktionen auch als polymorphe Funktionen bezeichnet; dieser Begriff ist in der objektorientierten Programmierung aber anderweitig belegt. Oft wird auch der Begriff der parameterisierten Typen verwendet.
Java kannte bis zur Version 1.5 keine Möglichkeit der generischen Programmierung. Mitlerweile ist der Spezifikationsprozess der Javagemeinde abgeschlossen, so daß ab Version 1.5 auch in Java generisch programmiert werden kann[BCK+01]. In C++ kann generisch über sogenannte Formulare englisch templates programmiert werden.9 In Vergleich zu Javas generischen Funktionen sind die C++-Formulare etwas primitiver technisch umgesetzt.
Die Grundidee ist, eine Funktion zu schreiben, die für mehrere Typen gleich funktioniert. Eine typische und einfache derartige Funktion ist eine trace-Funktion, die ihr Argument unverändert als Ergebnis wieder ausgibt, mathematisch also die Identität darstellt. Als Seiteneffekt gibt die Funktion das Argument auf den Bildschirm aus. Diese Funktion läßt sich generisch schreiben. Hierzu definiert man vor der Funktionssignatur, daß ein Typ variabel gehalten wird. Es wird eine Typvariabel eingeführt. Dazu bedient man sich des Schlüsselworts template und setzt in spitze Klammern den Namen der eingeführte Typvariablen. Dieser ist das Schlüsselwort typename voranzustellen. Für die generische Funktion trace ergibt sich folgende Signatur.
Trace
template <typename a>
a trace(a x);

Man kann Typvariablen als allquantifiziert ansehen. Dann sagt obige Signatur aus: für alle Typen a, funktioniert die Funktion trace, indem sie ein Parameter dieses Typs nimmt, und diesen Typ auch als Ergebnis hat.
Implementieren wir nun die Funktion. Hierzu schreiben wir wieder die entsprechende Signatur mit der Variablen. Der Parameter soll auf der Konsole aufgegeben werden und wieder zurückgegeben werden.
Trace
#include <iostream>
#include "Trace.h"

template <typename a>
a trace(a x){
  std::cout << ">>>trace: " << x << std::endl;
  return x;
}

Jetzt können wir die Funktion für einfache Beispiele einmal ausprobieren:
Trace
int main(){
  int x = 5 + trace(21*2);
  char *str = "hallo";
  trace(str);
  x = trace(6 + trace(18*2));
}

Wir rufen die Funktion mehrmals für ganze Zahlen und einmal für Strings auf.
sep@linux:~/fh/prog3/examples/src> g++ -o Trace Trace.cpp
sep@linux:~/fh/prog3/examples/src> ./Trace
>>>trace: 42
>>>trace: hallo
>>>trace: 36
>>>trace: 42
sep@linux:~/fh/prog3/examples/src>

Wir haben oben behauptet, daß die Typvariabel a für die Funktion trace als allquantifiziert betrachtet werden kann. In Javas generischen Typen wäre dieses auch der Fall, in C kann man dieses nicht sagen. Für welche Typen die Formularmethode trace anwendbar ist, geht aus der Signatur nicht hervor. Dieses wird von C erst dann geprüft wenn tatsächlich die Funktion aufgerufen wird. Dann wird das Formular genommen und die Typvariabel ersetzt durch den Typ, auf den die Funktion angewendet wird. Das Formular wird benutzt, der konkrete Typ eingestzt und dann diese Instanz des Formulars vom Compiler auf Typkorrektheit geprüft.
Unsere Funktion trace kann nur übersetzt werden für Typen, auf denen der Ausgabeoperator << definiert ist. Dieses ist für ganze Zahlen und Strings der Fall. Versuchen wir jetzt einmal die Funktion für eine Struktur aufzurufen. Die Struktur kann nicht mit dem Operator << ausgegeben werden und daher kann auch die Funktion trace nicht für diese Struktur aufgerufen werden. Versuchen wir dieses:
TraceWrongUse
#include <iostream>
#include "Trace.h"

template <typename a>
a trace(a x){
  std::cout <<">>>trace: "<< x << std::endl;
  return x;
}

struct S {int x;char* s;};

int main(){
  S u = {42,"hallo"};
  u = trace(u);
}

Wir erhalten folgende wortreiche Fehlermeldung (von der wir hier nur den Anfang wiedergeben):
sep@linux:~/fh/prog3/examples/src> g++ -c TraceWrongUse.cpp
TraceWrongUse.cpp: In function `a trace(a) [with a = S]':
TraceWrongUse.cpp:13:   instantiated from here
TraceWrongUse.cpp:6: error: no match for 'operator<<' in '
   std::operator<<(std::basic_ostream<char, _Traits>&, const char*) [with
   _Traits = std::char_traits<char>]((&std::cout), ">>>trace: ") << x'
/usr/include/g++/bits/ostream.tcc:63: error: candidates are:
   std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT,
   _Traits>::operator<<(std::basic_ostream<_CharT,
   _Traits>&(*)(std::basic_ostream<_CharT, _Traits>&)) [with _CharT = char,
   _Traits = std::char_traits<char>]
/usr/include/g++/bits/ostream.tcc:85: error:
   std::basic_ostream<_CharT, _Traits>& std::basic_ostream<_CharT,
   _Traits>::operator<<(std::basic_ios<_CharT,
   _Traits>&(*)(std::basic_ios<_
         .
         .
         .

sep@linux:~/fh/prog3/examples/src>

So wortreich die Fehlermeldung ist, so gibt sie uns doch in den ersten paar Zeilen sehr gut Auskunft darüber, was das Problem ist. Wir instanziieren die Typvariable in der Methode trace mit der Struktur S. Dann muß der Operator für den Typ S angewendet werden, wofür dieser aber nicht definiert ist.
In C++ kann man aus der Signatur einer generischen Funktion nicht erkennen, für welche Typen die Funktion aufgerufen werden kann. In Javas generischen Typen wird dieses möglich sein.
Aufgabe 9   Schreiben Sie eine Funktion zur Funktionakomposition mit folgender Signatur:
template <typename a, typename b, typename c>
c composition(c (* f2)(b),b (* f1)(a),a x);

Sie sei spezifiziert durch die Gelichung: composition(f2,f1,x)=f2(f1(x)). Testen Sie Ihre Implementierung für verschiedene Typen.

3.1.1  Heterogene und homogene Umsetzung

Wir haben bereits gesehen, daß die Signatur einer generischen Funktion allein nicht ausreicht, um zu bestimmen, auf welche Parametertypen die Funktion anwendbar ist. Das ist ein neues Phänomen, welches uns bisher noch nicht untergekommen ist und offenbahrt eine große Schwäche der Umsetzung der Genrizität in C++. Diese Eigenschaft hat ein weitreichende Konsequenz für die Arbeit mit Kopfdateien. Versuchen wir jetzt einmal unser Beispiel der Funktion trace in der gleichen Weise mit einer Kopfdatei zu bearbeiten, wie wir es bisher gemacht haben. Wir schreiben also eine Kopfdatei mit lediglich der Signatur:
T
template <typename a>
a trace(a x);

Und die entsprechende Implementierungsdatei:
T
#include <iostream>
#include "T.h"

template <typename a>
a trace(a x){
  std::cout << ">>>trace: " << x << std::endl;
  return x;
}

Schließlich ein Programm, daß die Funktion trace benutzen will und daher die Kopfdatei T.h einbindet:
UseT
#include "T.h"

int main(){
  trace(2*21);
}

Wir übersetzen diese zwei Programme und erzeugen für sie die Objektdateien, was fehlerfrei gelingt:
ep@linux:~/fh/prog3/examples> cd src
sep@linux:~/fh/prog3/examples/src> g++ -c T.cpp
sep@linux:~/fh/prog3/examples/src> g++ -c UseT.cpp
sep@linux:~/fh/prog3/examples/src>

Wollen wir die beiden Objektdateien nun allerdings zusammenbinden, dann bekommen wir eine Fehlermeldung:
sep@linux:~/fh/prog3/examples/src> g++ -o UseT UseT.o T.o
UseT.o(.text+0x16): In function `main':
: undefined reference to `int trace<int>(int)'
collect2: ld returned 1 exit status
sep@linux:~/fh/prog3/examples/src>

Es gibt offensichtlich keinen Code für die Anwendung der generischen Funktion trace auf ganze Zahlen. Hieraus können wir schließen, daß der C++-Übersetzer nicht einen Funktionscode für alle generischen Anwendungen einer generischen Funktion hat, sondern für jede Anwendung auf einen speziellen Typ, besonderen Code erzeugt. Für das Programm T.cpp wurde kein Code für die Anwendung der Funktion trace auf ganze Zahlen erzeugt, weil dieser Code nicht im Programm T.cpp benötigt wird. Das Formular wurde quasi nicht für den Typ int ausgefüllt. Etwas anderes wäre es gewesen, wenn in der Datei T.cpp eine Anwendung von trace auf einen Ausdruck des Typs int enthalten gewesen wär. Dann wäre auch in der Objektdatei T.o Code für eine Anwendung auf int zu finden gewesen.
Die Umsetzung generischer Funktionen, in der für jede Anwendung der Funktion auf einen bestimmten Typen eigener Code generiert wird, das Formular für einen bestimmten Typen ausgefüllt wird, nennt man die heterogene Umsetzung. Im Gegensatz dazu steht die homogene Umsetzung, wie sie in Java 1.5 realisiert ist. Hier findet sich in der Klassendatei nur ein Code für eine generische Funktion. Als Konsequenz werden intern in Java die generischen Typen als vom Typ Object betrachtet, und nach jeder Anwendung einer generischen Methode wird eine Typzusicherung nötig.
Wie können wir aber aus dem Dilemma mit den Kopfdateien für generische Funktionen herauskommen. Da C++ zum Ausfüllen des Formulares für die Anwendung einer generischen Funktion den Funktionsrumpf benötigt, bleibt uns nichts anderes übrig, als auch den Funktionsrumpf in die Kopfdatei mit aufzunehmen.

3.1.2  Expizite Instanziierung des Typparameters

Wir haben oben die generischen Funktionen aufgerufen, ohne explizit zu sagen, für welchen Typ wir die Funktion in diesem Fall aufzurufen wünschen. Der C++ Übersetzer hat diese Information inferiert, indem er sich die Typen der Parameter angeschaut hat. In manchen Fällen ist diese Inferenz nicht genau genug. Dann haben wir die Möglichkeit explizit anzugeben, für was für einen Typ wir eine Instanz der generischen Funktion benötigen. Diese explizite Typangabe für den Typparameter wird dabei in spitzen Klammern dem Funktionsnamen nachgestellt.
Beispiel:
In diesem Beispiel rufen wir zweimal die Funktion trace mit einer Zahlenkonstante auf. Einmal geben wir explizit an, daß es sich bei der der Konstante um eine int-Zahl handeln soll, einmal , daß es sich um eine Fließkommazahl handeln soll.
ExplicitTypeParameter
#include "T.cpp"

int main(){
  trace<float>(2*21656567);
  trace<int>(2*21656567);
}

Die zwei Aufrufe der Funktion trace führen entsprechend zu zwei verschieden formatierten Ausgaben.
sep@linux:~/fh/prog3/examples/src> ./ExplicitTypeParameter
>>>trace: 4.33131e+07
>>>trace: 43313134
sep@linux:~/fh/prog3/examples/src>

3.1.3  Generizität für Reihungen

Besonders attraktiv sind generische Funktionen für Reihungen und wie wir später auch sehen werden für Sammlungsklassen. Man denke z.B. daran, daß, um die Länge einer Liste zu berechnen, der Typ der Listenelemente vollkommen gleichgültig ist.
Wir haben im Kapitel über Reihungen eine Funktion map geschrieben mit folgender Signatur:
void map(int xs [],int l,int (*f) (int));
Hier wurde auf jedes Reihungselement eine Funktion angewendet. Die Funktion map war so aber nur für Reihungen, deren Elemente ganze Zahlen sind, definiert. Mit generischen Fuktionen können wir diese Funktion veralgemeinern. Hierzu führen wir zwei Typvariablen ein: eine erste die den Elementtyp der ursprünglichen Reihung angibt, den zweiten, der den Elementtyp der Ergebnisreihung angibt. Damit lät sich eine generische Funkton map auf Reihungen wie folgt definieren:
GenMap
#include <iostream>

template <typename a, typename b>
void map(a xs [],b ys [],int l,b (*f) (a)){
  for (int i=0;i<l;i++){
    ys[i]=f(xs[i]);
  }
}

Die an map übergebene Funktion, die in der ursprünglichen Funktion map den Typ int (*f) (int) hatte, ist nun in ihrer Typisierung variabel gehalten. Sie hat einen Typ a als Parametertyp und einen beliebigen Typ b als Ergebnistyp. Jetzt übergeben wir zwei Reihungen: eine Ursprungsreihung von Elementen des variabel gehaltenen Typs a und eine Ergebnisreihung von dem ebenfalls variabel gehaltenen Typ b. Die Funktion map wendet ihren Funktionsparameter f auf die Elemente der Ursprungsreihung an und speichert die Ergebnisse in der Ergebnisreihung ab.
Sofern es sich bei den Elementtypen einer Reihung um Typen handelt, für die der Operator << definiert ist, läßt sich auch eine generische Ausgabefunktion für Reihungen schreiben:
GenMap
template <typename a>
void printArray(a xs [],int l){
  std::cout<<"{";
  for (int i=0;i<l-1;i++){
    std::cout << xs[i]<<", ";
  }
  std::cout << xs[l-1] <<"}"<<std::endl;
}

Nun können wir die beiden generischen Reihungsfunktionen einmal testen. Hierzu schreiben wir drei kleine Funktionen, die wir der Funktion map übergeben werden:
GenMap
int add5(int x){return x+5;}
char charAt2(std::string xs){return xs[2];}
std::string doubleString(std::string ys){return ys+ys;}

Und tatsächlich läßt sich die Funktion map für ganz unterschiedliche Typen anwenden:
GenMap
int main(){
  int xs [] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
  int ys [15];
  map(xs,ys,15,&add5);
  printArray(ys,15);

  std::string us  []= {"hallo","jetzt","wirds","cool"};
  char vs [4];
  map(us,vs,4,&charAt2);
  printArray(vs,4);

  std::string ws [4];
  map(us,ws,4,&doubleString);
  printArray(ws,4);
}

Hier die Ausgabe dieses Tests:
sep@linux:~/fh/prog3/examples/bin> ./GenMap
{6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
{l, t, r, o}
{hallohallo, jetztjetzt, wirdswirds, coolcool}
sep@linux:~/fh/prog3/examples/bin>

Aufgabe 10   Schreiben Sie jetzt eine generische Fuktion fold auf Reihungen und testen Sie diese auf verschiedenen Typen aus.
Aufgabe 11   Verallgemeinern Sie jetzt Ihre Funktion bubbleSortBy zu einer generischen Funktion, mit der sie Reihungen beliebiger Typen sortieren können.

3.2  Überladen von Funktionen

Im letzen Abschnitt haben wir eine Funktionsdefinition geschrieben, die als Schablone für beliebig viele Typen stehen konnte. Jetzt gehen wir gerade den umgekehrten Weg. Wir werden Funktionen überladen, so daß wir mehrere Funktionsdefinitionen für ein und dieselbe Funktion erhalten. Dieses Prinzip kennen wir bereits aus Java.
Beispiel:
Jetzt schreiben wir die trace Funktion nicht mit Hilfe einer Funktionsschablone, sondern durch Überladung.
OverloadedTrace
#include <string>
#include <iostream>

struct Person 
  {std::string name
  ;std::string vorname
  ;};

Person trace(Person& p){
  std::cout << p.vorname << " " << p.name<< std::endl;
  return p;
}

int trace (int i){
  std::cout << i << std::endl;return i;
}

float trace (float i){
  std::cout << i << std::endl;return i;
}

std::string trace (std::string i){
  std::cout << i << std::endl;return i;
}

int main(){
  Person p1 = {"Zabel", "Walther"};
  Person p2 = trace(p1);
  trace (456456456);
  trace (425456455.0f);
  std::string s1 = "hallo";
  std::string s2 = trace (s1);
}

Das Programm führt zu der erwarteten Ausgabe:
sep@linux:~/fh/prog3/examples/src> ./OverloadedTrace
Walther Zabel
456456456
4.25456e+08
hallo
sep@linux:~/fh/prog3/examples/src>

3.3  Standardparameter

In C ist es möglich, in der Funktionssignatur bestimmte Parameter mit einem Standardwert zu belegen. Dann kann die Funktion auch ohne Übergabe dieses Parameters aufgerufen werden. Der in dem Aufruf fehlende Parameter wird dann durch den Standardwert der Signatur ersetzt.
Beispiel:
Im folgenden definieren wir eine Funktion, die zwei ganze Zahlen als Parameter erhält. Der zweite Parameter hat einen Standardwert von 40. Bei Aufruf der Funktion mit nur einem Parameter wird für den Parameter y der Standardwert benutzt.
DefaultParameter
#include <iostream>

int f(int x,int y=40){return x+y;}

int main(){
    std::cout << "f(1,2) = "<< f(1,2) << std::endl;
    std::cout << "f(2)   = "<< f(2) << std::endl;
}

An der Ausgabe können wir uns davon überzeugen, daß für das fehlende y der Wert 40 benutzt wird.
sep@linux:~/fh/prog3/examples/src> g++ -o DefaultParameter DefaultParameter.cpp
sep@linux:~/fh/prog3/examples/src> ./DefaultParameter
f(1,2) = 3
f(2)   = 42
sep@linux:~/fh/prog3/examples/src>

Bei der Benutzung von Standardwerten für Parameter gibt es eine Einschränkung in C. Sobald es für einen Parameter einen Standardwert gibt, muß es auch einen Standardwert für alle folgenden Parameter geben. Es geht also nicht, daß für den n-ten Parameter ein Standardwert definiert wurde, nicht aber für den m-ten Parameter mit n < m.
Standardwerte sind eine bequeme Notation, können aber leicht mit überladenen Funktionen simuliert werden:
Beispiel:
Jetzt simulieren wir, so wie wir es bereits aus Java gewohnt sind, Standardparameter durch eine überladene Funktionsdefinition.
OverloadedInsteadOfDefaults
#include <iostream>

int f(int x,int y){return x+y;}
int f(int x){return f(x,40);}

int main(){
    std::cout << "f(1,2) = "<< f(1,2) << std::endl;
    std::cout << "f(2)   = "<< f(2) << std::endl;
}

Das Programm funktioniert entsprechende dem vorherigen Beispiel:
sep@linux:~> g++ -o OverloadedInsteadOfDefaults OverloadedInsteadOfDefaults.cpp
sep@linux:~> ./OverloadedInsteadOfDefaults
f(1,2) = 3
f(2)   = 42
sep@linux:~>

Chapter 4
Objektorientierte Programmierung in C++

Eine der wichtigsten Erweiterungen von C nach C++ ist die Einführung der objektorientierten Programmierung. Hierzu wurden in C++ Strukturen erweitert, so daß mit ihnen folgende aus Java bereits bekannte Konzepte ausgedrückt werden können:
Verwirrend für den Javaprogrammierer mag zunächst sein, daß es keine allgemeine Oberklasse Object gibt. Auch das aus Java bekannte Konzept von Schnitstellen gibt es in C++ nicht. Hingegen kann eine Klasse mehrere Oberklassen haben.

4.1  Klassen

Klassen sind eine natürliche Erweiterung von Strukturen. Die Syntax ist entsprechend dieselbe. Lediglich statt des Schlüsselworts struct ist das Schlüsselwort class zu benutzen.

4.1.1  Klassendeklaration

In einer Klasse gibt es wie in Java Felder und Methode. Anders als in Java sind in C++ standardmäßig alle Eigenschaften einer Klasse als private deklariert, d.h.  sie dürfen nur innerhalb der Klasse benutzt werden. Mit dem Schlüsselwort public gefolgt von einem Doppelpunkt werden alle folgenden Eigenschaften der Klasse als öffentlich für jeden auf den Objekten zugreifenden Benutzer deklariert. Dieses kann mit dem Schlüsselwort private gefolgt von einem Doppelpunkt für weitere Eigenschaften wieder zurückgesetzt werden.
Wie wir für Strukturen bereits gesehen haben, bekommen wir auch für Klassen nicht automatisch wie in Java Referenzen auf Objekte, sondern die Objekte direkt im Speicher. Die Zuweisung bewirkt auch für Objekte daher eine Kopierung des Objekts. Die Initialisierung von Objekten kann wie bei Strukturen auch durch eine Aufzählung der Felder in geschweiften Klammern erfolgen.
Beispiel:
Unsere erste C++ Klasse zur Darstellung von Personen:
P1
#include <iostream>

class Person {
 public:
  std::string nachname;
  std::string vorname;
  std::string getFullName(){
    return vorname+" "+nachname;
  }
};

int main(){
  Person p1;
  p1.nachname="Zabel";
  p1.vorname="Walther";
  std::cout << p1.getFullName() << std::endl;
  Person p2 = p1;
  std::cout << p2.getFullName() << std::endl;
  p2.nachname = "Hotzenplotz";
  std::cout << p1.getFullName() << std::endl;
  std::cout << p2.getFullName() << std::endl;
  Person p3 = {"Shakespeare","William"};
  std::cout << p3.getFullName() << std::endl;
}

An der Ausgabe kann man wieder deutlich erkennen, daß eine Zuweisung ein Objekt kopiert.
sep@linux:~/fh/prog3/examples/src> ./P1
Walther Zabel
Walther Zabel
Walther Zabel
Walther Hotzenplotz
William Shakespeare
sep@linux:~/fh/prog3/examples/src>

4.1.2  Header-Dateien für Klassen

In der Kopfdatei zu einer Bibliothek schreibt man in der Regel die Klasse mit allen ihren Eigenschaften, läßt jedoch für jede Methode den Methodenrumpf fort. Für die Klasse Person erhalten wir so die folgende Kopfdatei.
P2
#include <string>

class Person {

public:
  std::string nachname;
  std::string vorname;
  std::string getFullName();
};

In der eigentlichen Bibliotheksdatei, braucht die Klassendefinition nun nicht mehr wiederholt zu werden. Sie kann ja inkludiert werden. Die fehlenden Funktionsimplementierungen können jetzt getätigt werden. Hierzu ist den Methoden jeweils anzuzeigen, zu welcher Klasse sie gehören. Dieses geschieht durch das Voranstellen des Klassennamens vor den Funktionsnamen. Als Seperator wird ein zweifacher Doppelpunkt benutzt. Für die Klasse Person erhalten wir die folgende Implementierungsdatei:
P2
#include "P2.h"

std::string Person::getFullName(){return vorname+" "+nachname;}

Jetzt kann eine Klasse benutzt werden, indem man lediglich die Kopfdatei inkludiert:
TestP2
#include "P2.h"
#include <iostream>

int main(){
  Person p1;
  p1.nachname="Zabel";
  p1.vorname="Walther";
  std::cout << p1.getFullName() << std::endl;
  Person p2 = p1;
  std::cout << p2.getFullName() << std::endl;
  p2.nachname = "Hotzenplotz";
  std::cout << p1.getFullName() << std::endl;
  std::cout << p2.getFullName() << std::endl;
  Person p3 = {"Shakespeare","William"};
  std::cout << p3.getFullName() << std::endl;
}

Das Programm verhält sich wieder wie die vorherige Version ohne Kopfdatei:
sep@linux:~/fh/prog3/examples/src> g++ -c TesteP2.cpp
sep@linux:~/fh/prog3/examples/src> g++ -c P2.cpp
sep@linux:~/fh/prog3/examples/src> g++ -c TestP2.cpp 
sep@linux:~/fh/prog3/examples/src> g++ -o TestP2 P2.o TestP2.o
sep@linux:~/fh/prog3/examples/src> ./TestP2
Walther Zabel
Walther Zabel
Walther Zabel
Walther Hotzenplotz
William Shakespeare
sep@linux:~/fh/prog3/examples/src>

4.1.3  Konstruktoren

Aus Java kennen wir Konstruktoren. Auch in C++-Klasse können Konstruktoren definiert werden. Syntaktisch wird dieses ebenso wie in Java als Methoden ohne Rückgabetyp mit dem Namen der Klasse notiert. Ebenso wie in Java gibt es in C++ auch den this-Zeiger, der als Zeiger auf das Objekt dient, für das eine Methode oder ein Konstruktor aufgerufen wurde. Da this eine Zeigervariable ist, wird für sie nicht mit dem Punktoperator sondern mit dem Pfeiloperator auf die einzelnen Eigenschaften zugegriffen.
Beispiel:
Wir können jetzt eine Klasse Person schreiben, die so wie wir es aus Java gewohnt sind, einem Konstruktor hat, in dem alle relevanten Felder der Klasse explizit gesetzt werden:
Person
#include <string>
#include <iostream>

class Person {

  private:
   std::string nachname;
   std::string vorname;
   std::string strasse;
   int hausnummer;
   int plz;
   std::string ort;

  public:
    Person(std::string nachname ,std::string vorname
          ,std::string strasse  ,int hausnummer
          ,int plz              ,std::string ort);

    std::string getFullName();
    std::string getAddress();
    void setStrasse(std::string strasse);
};

Person
#include <string>
#include <iostream>
#include "Person.h"

Person::Person(std::string nachname,std::string vorname
              ,std::string strasse,int hausnummer
              ,int plz,std::string ort){
  this->nachname=nachname;
  this->vorname=vorname;
  this->strasse=strasse;
  this->hausnummer=hausnummer;
  this->plz=plz;
  this->ort=ort;
}

std::string Person::getFullName(){
  return vorname+" "+nachname;
}

std::string Person::getAddress(){
  char hausnr[20];
  sprintf(hausnr,"%d",hausnummer);

  char plznr[20];
  sprintf(plznr,"%d",plz);

  return strasse+" "+hausnr +"\n"+" "+plznr+ort;
}

void Person::setStrasse(std::string strasse){
  this->strasse = strasse;
}

Bei der Benutzung der Klasse Person ist jetzt zu beachten, daß sobald eine Variable als vom Typ Person deklariert wird, C++ einen Konstruktor von dieser Klasse aufrufen will. Anders als in Java, wo Variablen nur Referenzen auf die entsprechenden Objekte dargestellt haben, gilt in C++, daß Variablen direkt die Daten bezeichnen. Daher führt eine Variablendeklaration direkt zum Aufruf eines Konstruktors. Wenn ein Konstruktor mit Parametern benutzt werden soll, so sind die konkreten Parameter in runden Klammern dem Variablennamen direkt folgend anzugeben.
TestePerson1
#include "Person.h"
#include <iostream>

int main(){
  Person p1("Schmidt","Helmut","Brüderstrasse"
           ,82,20121,"Hamburg");
  Person p2("Kohl","Helmut","Saumagenstrasse"
           ,98,60121,"Ludwigshafen");

  std::cout << p1.getFullName() << std::endl
            << p1.getAddress()  << std::endl;
  std::cout << p2.getFullName() << std::endl 
            << p2.getAddress()  << std::endl;

  p1.setStrasse("Sonnenstrasse");
  std::cout << p1.getFullName() << std::endl 
            << p1.getAddress()  << std::endl;
}

Hieraus folgt insbesondere, daß, sobald es keinen Konstruktor ohne Parameter gibt, keine Variable ohne die Angabe der Parameter für den Konstruktor mehr deklariert werden kann. C++ will nämlich in diesem Fall den leeren Konstruktor benutzen, den es nicht gibt.
Folgendes Programm gibt daher einen Übersetzungsfehler.
PersonKonstruktorError
#include "Person.h"

int main(){
  Person p;
}

Der C-Compiler beschwert sich über einen fehlenden Konstruktor:
sep@linux:~/fh/prog3/examples/src> g++ PersonKonstruktorError.cpp
PersonKonstruktorError.cpp: In function `int main()':
PersonKonstruktorError.cpp:4: error: no matching function for call to `Person::
   Person()'
Person.cpp:4: error: candidates are: Person::Person(const Person&)
Person.cpp:21: error:                 Person::Person(std::basic_string<char,
   std::char_traits<char>, std::allocator<char> >, std::basic_string<char,
   std::char_traits<char>, std::allocator<char> >, std::basic_string<char,
   std::char_traits<char>, std::allocator<char> >, int, int,
   std::basic_string<char, std::char_traits<char>, std::allocator<char> >)
sep@linux:~/fh/prog3/examples/src>

Konstruktion mit new

Wir kennen schon den Operator new zum Erzeugen von Zeigern auf Daten eines bestimmten Typs. In Zusammenhang mit Klassen bekommt dieser Operator die aus Java bekannte Semantik. Dem Operator new folgt der Name des Typs, für den ein neuer Speicherbereich reserviert werden soll; in unserem Fall jetzt ist dies die Klasse. Dem Klassennamen folgen die konkreten Parametern für einen der Konstruktoren. Das Ergebnis ist ein Zeiger auf ein neues Objekt der Klasse. Dieses wird auch deutlich im Typ. Eine Variable die mit dem Operator new für die Klasse Person erzeugt wird, ist nicht vom Typ Person sondern Person*.
Beispiel:
Jetzt benutzen wir die Klasse Person, indem wir die Objekte über den Operator new erzeugen. Wir speichern das Ergebnis in Variablen des Zeigertyps Person*. Zum Benutzen der Eigenschaften dieses Zeigertyps müssen wir den Pfeiloperator bedienen und nicht wie zuvor den Punktoperator.
TestePerson2
#include "Person.h"
#include <iostream>

int main(){
  Person* p1 = new Person("Schmidt","Helmut","Brüderstrasse"
                         ,82,20121,"Hamburg");
  Person* p2 = new Person("Kohl","Helmut","Saumagenstrasse"
                         ,98,60121,"Ludwigshafen");

  std::cout << p1->getFullName() << std::endl
            << p1->getAddress()  << std::endl;
  std::cout << p2->getFullName() << std::endl 
            << p2->getAddress()  << std::endl;

  p1->setStrasse("Sonnenstrasse");
  std::cout << p1->getFullName() << std::endl 
            << p1->getAddress() << std::endl;
}

Syntaktischer Zucker für Konstruktoren

Typischer Weise bekommen Konstruktoren Werte für die Felder einer Klasse. In der Regel werden im Konstruktor die übergebenen Werte den Feldern zugewiesen. Wir erhalten Zeilen der Form:
this->nachname=nachname;
In C++ gibt es eine Notation, die speziell für solche Fälle vorgesehen ist. Vor dem Rumpf des Konstruktors können diese Zuweisungen in der Form: feldName(wert) erfolgen. Man spricht auch von der Initialisierung der Felder. Die Feldinitialisierungen stehen zwischen Parameterliste des Konstruktors und dem Konstruktorrumpf. Sie werden per Doppelpunkt von der Parameterliste abgetrennt und untereinander durch Komma getrennt.
Beispiel:
Den Konstruktor der Klasse Person hätten wir demnach auch wie folgt schreiben können:
PersonAlternativ
#include <string>
#include "Person.h"

Person::Person(std::string nachname,std::string vorname
              ,std::string strasse,int hausnummer
              ,int plz,std::string ort):
   nachname(nachname),vorname(vorname),strasse(strasse)
  ,hausnummer(hausnummer),plz(plz),ort(ort){}

4.1.4  Destruktoren

Im Zusammenhang mit der dynamischen Speicherverwaltung haben wir den zum Operator new dualen Operator delete kennengelernt. Ein Pendant zu delete gab es in Java nicht. Die virtuelle Maschine gibt nicht mehr benutzte Speicherzellen automatisch wieder frei. In C++ muß der Programmierer die dynamische Speicherverwaltung selbst vornehmen. Als Faustregel ist zu merken, daß für jedes new im Programmdurchlauf irgendwann einmal ein delete ausgeführt werden muß.
Betrachten wir einmal genau die dynamische Speicherverwaltung für ein Programm. Hierzu bedienen wir uns der einfachen verketteten Listenimplementierung, wie wir sie für Java implementiert hatten. Listen seien wieder durch die zwei Konstruktoren, den zwei Selektoren und einer Testmethode spezifiziert. Zusätzlich wollen wir eine Funktion implementieren, die zwei Listen konkateniert. Um ein zu dem entsprechenden Javaprogramm analoge Funktionalität zu haben, müssen wir explizit mit Zeigern auf die Restliste arbeiten.
StringList
#include <string>

class StringList {
  private:
    bool empty;
    std::string hd;
    StringList*  tl;

  public:
    StringList();
    StringList(std::string hd,StringList* tl);

    bool isEmpty();
    std::string head();
    StringList* tail();

    StringList* concat(StringList* other);
};

Die Funktionen lassen sich relativ einfach implementieren.
StringList
#include <string>
#include <iostream>
#include "StringList.h"

StringList::StringList():empty(true){};
StringList::StringList(std::string hd,StringList* tl):
   empty(false),hd(hd),tl(tl){} 
    
bool StringList::isEmpty(){return empty;}
std::string StringList::head(){return hd;}
StringList* StringList::tail(){return tl;}

StringList* StringList::concat(StringList* other){
  if (isEmpty()) return other;
  return new StringList(head(),tail()->concat(other));
}

Folgendes kleines Testprogramm wollen wir in seiner Ausführung genau betrachten. Wir erzeugen zwei Listen, die in lokalen Variablen gespeichert werden. Dann konkatenieren wir diese beiden Listen und speichern die Ergenisliste in einer weiteren lokalen Variablen. Anschließend rufen wir den Operator delete für die erste Liste auf und weisen allen lokalen Variablen das Ergebnis der Konkatenation zu.
ConcatTest
#include <iostream>
#include "StringList.h"

int main(){
  StringList* xs = new StringList("friends"
                  ,new StringList("romans"
                  ,new StringList("contrymen"
                  ,new StringList())));

  StringList* ys = new StringList("lend"
                  ,new StringList("me"
                  ,new StringList("your"
                  ,new StringList("ears"
                  ,new StringList()))));
  
  StringList* zs = xs->concat(ys);

  delete xs;
  ys=zs;
  xs=zs;
}

Betrachten wir in einer vereinfachten Darstellung, wie der Speicher aussieht, nachdem Zeile 16 des Programms ausgeführt wurde. Der Speicher teilt sich in zwei Bereiche, den sogenannten Keller (stack), auf dem lokale Variablen und Parameterwerte abgespeichert werden und dem heap zur Speicherung dynamisch erzeugter Daten. Für die lokalen Variablen der Methode main ist Speicher auf dem stack angelegt worden. In diesen Speicherzellen, stehen für die drei lokalen Zeigervariablen xs, ys und zs Adressen aus dem Speicher. Der stack sieht also in etwa wie folgt aus:
zs22
ys13
xs1
Diese Adressen wurden durch die Ausführung des Operators new während der Laufzeit des Programms errechnet. Insgesamt wurde 12 mal während des Programmdurchlaufs der Operator new ausgeführt. Viermal beim Erzeugen der Liste xs, fünfmal beim Erzeugen der Liste ys und schließlich dreimal bei der Ausführung der Funktion concat. (concat erzeugt für jeden nichtleeren Listenknoten des this-Objekts einen neuen Listenknoten.) Jedesmal wurde Speicher mit new angefordert, um ein Objekt der Klasse StringList abzuspeichern. In unserer vereinfachten Form gehen wir davon aus, daß für jedes Feld der Klasse StringList eine Adresse zum Speichern benutzt wird. Ein new für StringList reserviert in unserer Darstellung einen Block von drei Speicherzellen. Insgesamt werden also 12*3 Speicherzellen im heap reserviert. In der ersten Zelle dieser Tripel steht der bool'sche Wert, der angibt, ob es sich um eine leere Liste handelt, in der zweiten Zelle der Kopf des Listenknotens und in der dritten Zelle die Adresse der Schwanzliste. Man beachte, daß auch für leere Listen die Zellen für Kopf und Schwanz existieren, auch wenn dort undefinierte Werte drinstehen.
Die folgende Tabelle gibt in diesem vereinfachten Modell ein Abbild des heap nach Ausführung der Listenkonkatenation:
1false
2``friends''
34
4false
5``romans''
67
7false
8countrymen
910
10true
11
12
13false
14``lend''
1516
16false
17``me''
1819
19false
20``your''
2122
22false
23``ears''
2425
25true
26
27
28false
29``friends''
3031
31false
32``romans''
3334
34false
35``countrymen''
3613
36 Speicherzellen wurden reserviert. Auf die Speicherzelle 13 wird zweimal verwiesen. Einmal von Zelle 36 aus und einmal vom stack aus. In Zeile 28 des Programms fordern wir jetzt C++ auf, den Speicher auf dem die Variable xs zeigt im Speicher wieder frei zu geben. C++ weiß, daß es sich um einen Zeiger auf ein Objekt des Typs StringList handelt und solche Objekte drei Speicherzellen belegen. Daher gibt C++ die Speicherzellen 1, 2 und 3 wieder frei. Die Speicherzellen 4 bis 12, die auch zur Liste xs gehören bleiben hingegen erhalten. Der delete-Operator löscht zunächst nur genau das eine Objekt auf dem gezeigt wird. Es werden in diesem Objekt durch Zeigervariablen weiter referenzierte Objekte nicht mitgelöscht. Führen wir schließlich noch die letzten zwei Befehle des Programms aus, so ergibt sich auf dem stack folgende Situation:
zs28
ys28
xs28
Im heap existieren jetzt mit den Zellen 4 bis 12 Speicherzellen, die nirgends mehr referenziert werden. Wir haben ein Speicherleck. Es wird an Speicher festgehalten, auf den nicht mehr zugegriffen werden kann. Wir hätten statt nur den Knoten, auf den xs zeigt, zu löschen, auch jeweils die Schwanzlisten für alle Knoten die über xs zu erreichen sind, löschen müssen. Statt delete xs hätte man also eine Funktion benötigt, die rekursiv alle Schwanzknoten löscht, bevor der Knoten selbst gelöscht wird:
void deleteList(StringList* xs){
  if (!xs->isEmpty()){
    deleteList(xs->tail());
  }
  delete xs;
}

Genau hierfür gibt es in C++ ein Konstrukt, den Destruktor. Ein Destruktor hat keinen Parameter. Der Destruktor enthält den Code, der für ein Objekt ausgeführt wird, wenn für einen Zeiger auf dieses Objekt der Operator delete aufgerufen wird, bevor das Objekt selbst aus dem Speicher entfernt wird. Der Destruktor wird in C++ durch den Klassennamen mit vorangestellter Tilde ~ bezeichnet. Ebenso wie bei Konstruktoren wird, wenn kein Destruktor im Programm definiert wird, von C++ ein leerer Destruktor generiert.
Beispiel:
Jetzt ergänzen wir die Klasse StringList um eine Destruktor, der dafür sorgt, daß mit delete nicht nur der ersten Knoten einer Liste aus dem Speicher entfernt wird, sondern die komplette Liste.
StringListDestructed
#include <string>
#include <iostream>

class StringList {
  private:
    bool empty;
    std::string hd;
    StringList*  tl;

  public:
    ~StringList(){
      if (!isEmpty()) delete tl;
    }

    StringList(){
      empty= true; 
    }

    StringList(std::string hd,StringList* tl){
      empty= false; 
      this->hd=hd;
      this->tl=tl;
    }

    bool isEmpty(){return empty;}
    std::string head(){return hd;}
    StringList* tail(){return tl;}

    StringList* concat(StringList* other){
      if (isEmpty()) return other;
      return new StringList(head(),tail()->concat(other));
    }
};

int main(){
  StringList* xs = new StringList("friends"
                  ,new StringList("romans"
                  ,new StringList("contrymen"
                  ,new StringList())));

  StringList* ys = new StringList("lend"
                  ,new StringList("me"
                  ,new StringList("your"
                  ,new StringList("ears"
                  ,new StringList()))));
  
  StringList* zs = xs->concat(ys);

  delete xs;
  ys=zs;
  xs=zs;
}

Was soll ich löschen?

Die delete-Operation kann schwierig sein. Wann genau darf ein Objekt aus dem Speicher gelöscht werden. Man mag geneigt sein im obigen Beispiel auch einen Aufruf delete ys einzufügen. Dann würden mittels unserers Destruktors auch die Speicherzellen 13 bis 27 gelöscht werden. Das wäre nicht gut, denn ausgehend über die Speicherzelle 28 für die Liste zs kann man eine Zeigerkette bilden, die Schließlich in Adressfeld 36 auf die Adresse 13 verweist. Wenn wir mit delete ys diesen Speicherbereich wieder freigegeben hätten, dann würden wir jetzt auf undefinierte Werte stoßen. Der Grund dafür ist, daß wir in der Funktion concat keine Kopie der zweiten Liste erzeugen, sondern ein zweites Mal auf den Speicherbereich dieser Liste verweisen. Da dieses in der Funktion concat passiert, können wir das nicht unbedingt wissen, wenn die Funktion concat nicht ganz genau darüber dokumentiert ist. Für die Programmierung und Entwicklung vobn Bibliotheken in C++ ist daher ganz wichtig, nur in Ausnahmefällen und gut dokumentiert Zeiger auf bestimmte Speicherbereiche zu kopieren, so daß über mehrere Zeiger auf den gleichen Bereich verwiesen wird. Ansonsten ist die explizite Speicherverwaltung entsprechend schwierig und schwer zu handhaben.

Wie macht Java das?

In Java haben wir uns nie Gedanken darüber machen müssen, ob in Speicher noch irgendwelcher Datenreste von Objekten liegen, die wir nicht mehr benötigen. Java hat eine eigene Müllentsorgung, die solche nicht mehr benötigten Objekte aus dem Speicher löschen. Wie macht Java das?
Bleiben wir bei unseren obigen Beispiel. Wie wir uns überzeugt haben können die Speicherzellen 1 bis 12 nach Abarbeitung der Methode nicht mehr erreicht werden. Turnusmäßig, wenn der Speicherbereich im heap knapp wird, startet Java die Müllentsorgung. Hierzu folgt Java allen Zeigern, die auf dem stack liegen und führt Buch darüber, welche Adressen im heap damit erreicht werden. In unserem Beispiel wird vom stack aus nur noch die Speicherzelle 28 erreicht. Hier liegt ein Objekt, das in Zelle 30 auf die Zelle 31 verweist. Auch dieses vermerkt sich Java. Java bildet den transitiven Abschluß der Zeigerketten und erhält damit die Menge aller noch erreichbaren Speicherzellen. Diese Speicherzellen kopiert sich Java in einen neuen frischen Speicherbereich. Die Zeigervariablen müssen dabei auf die Werte in den neuen Speicherbereich umdefiniert werden. Ist dieser Prozess abgeschlossen, so hat Java die noch benötigt Daten aus dem Speicher herausgezogen. Alles andere ist Müll gewesen. Java gibt komplett den alten heap zur Neuverwendung wieder frei. Das Bild der Müllentsorgung ist nach diesem Prinzip etwas ungenau. Java schmeißt nicht den Müll weg, sondern rettet die noch benötigten Daten und schmeißt dann alles weg.
Sprachen mit automatischer Speicherverwaltung gehen ursprünglich auf List zurück. C hat diese automatische Speicherverwaltung nicht, es gibt aber C Umgebungen, die eine solche automatische Verwaltung anbieten. Hier muß ein C Programmierer sich darauf beschränken keine Zeigerarithmetik zu verwenden, sprich Speicheradressen auf die zugegriffen wird, erst wieder zu berechnen. Alle noch benötigten Speicheradressen müssen explizit im Speicher liegen.

4.1.5  Konstantendeklarationen

Ähnlich wie in Java Variablen und Parameter als mit dem Schlüsselwort final als konstant deklariert werden können, lassen sich auch in C++ Werte als unveränderbar deklarieren. Hierzu dient in C das Schlüsselwort const ;

Konstante Objektmethoden

In C++ Klassen können Methoden als konstant in Bezug auf das this-Objekt deklariert werden. Damit wird gekennzeichnet, daß über den Aufruf der Methode für ein Objekt, dieses Objekt seine Werte nicht ändert. Hierfür wird das Schlüsselwort const der Parameterliste nachgestellt.
ConstThis
#include <iostream>

class Const {
public:
  int i;

  void printValue() const {
    std::cout << i << std::endl;
  }
};

int main(){
  Const test;
  test.i=42;
  test.printValue();
}

Im obigen Programm konnte die Methode printValue als konstant deklariert werden. Sie gibt lediglich den Wert eines Feldes der Klasse auf der Konsole aus. Das Objekt selbst wird nicht verändert.
Das folgende Programm fügt der Klasse eine zweite Methode zu. Auch diese wird als konstant deklariert, allerdings zu unrecht, denn sie setzt den Wert eines Feldes des Objekts neu.
ConstThisError1
#include <iostream>

class Const {
public:
  int i;
  void setValue(int j) const {
    i=j;
  }

  void printValue() const {
    setValue(42);
    std::cout << i << std::endl;
  }
};

Der C++-Übersetzer weist dieses Programm zu Recht zurück und gibt die folgende Fehlermeldung:
sep@linux:~/fh/prog3/examples/src> g++ ConstThisError1.cpp
ConstThisError1.cpp: In member function `void Const::setValue(int) const':
ConstThisError1.cpp:7: error: assignment of data-member `Const::i' in read-only
   structure
sep@linux:~/fh/prog3/examples/src>

Der Übersetzer führt sehr genau Buch darüber, daß nicht über Umwege durch Aufruf nicht konstanter Methoden innerhalb einer konstanten Methode, das Objekt geändert wird. Auch fogendes Programm führt zu einen Übersetzungsfehler.
ConstThisError2
#include <iostream>

class Const {
public:
  int i;
  void setValue(int j) {
    i=j;
  }

  void printValue() const {
    setValue(42);
    std::cout << i << std::endl;
  }
};

Jetzt beschwert sich der Übersetzer darüber, daß durch den Aufruf nicht konstanten Methode setValue innerhalb der als konstant deklarierten Methode printValue zum modifizieren des Objektes führen könnte.
sep@linux:~/fh/prog3/examples/src> g++ ConstThisError2.cpp
ConstThisError2.cpp: In member function `void Const::printValue() const':
ConstThisError2.cpp:11: error: passing `const Const' as `this' argument of `
   void Const::setValue(int)' discards qualifiers
sep@linux:~/fh/prog3/examples/src>

konstante Parameter

Auch Parameter von Funktionen können in C als konstant deklariert werden. Hierzu ist das Attribut const dem Parameternamen voranzustellen. Wenn wir in einem Programm Variablen inklusive des this-Zeigers auf die ein oder andere Weise als konstant deklariert haben, dürfen wir die Referenzen nur an Funktionen übergeben, wenn der entsprechende Parameter auch als konstant deklariert wurde.
In der folgenden Klasse ist der Parameter der Funktion doNotModify als konstant deklariert. Daher darf diese Funktion mit dem this-Zeiger in der konstanten Methode printValue aufgerufen werden.
ConstParam
#include <iostream>
class Const {
  public:
    int i;
    void Const::printValue() const;
};

void doNotModify(const Const* c){
  std::cout << "in doNotModify" << std::endl;
} 

void Const::printValue() const {
  doNotModify(this);
  std::cout << i << std::endl;
}

int main(){
  Const c;
  c.i=42;
  c.printValue();
}

Wenn hingegen der Parameter der Funiktion doNotModify nicht als konstant deklariert wäre, bekämen wir einen Übersetzungsfehler.
ConstParamError
#include <iostream>
class Const {
public:
  int i;
  void Const::printValue() const;
};

void doNotModify(Const* c){
  std::cout << "in doNotModify" << std::endl;
} 

void Const::printValue() const {
  doNotModify(this);
  std::cout << i << std::endl;
}

Wieder beschwert sich der Übersetzer zu Recht darüber, daß das als konstant deklarierte eventuell verändert werden könnte.
sep@linux:~/fh/prog3/examples/src> g++ ConstParamError1.cpp
ConstParamError1.cpp: In member function `void Const::printValue() const':
ConstParamError1.cpp:13: error: invalid conversion from `const Const* const' to
   `Const*'
sep@linux:~/fh/prog3/examples/src>

Das gilt natürlich nicht nur für den this-Zeiger, sondern für beliebig als nicht modifizierbar deklarierte Referenzen. Auch folgendes Programm wird vom Übersetzer zurückgewiesen.
ConstParamError2
#include <iostream>
class Const {
public:
  int i;
  void Const::printValue() const;
};

void doModify(Const* c){
  std::cout << "in doModify" << std::endl;
} 

int main(){
  const Const c();
  doModify(&c);
}

Wir erhalten die nun bereits bekannte Fehlermeldung:
sep@linux:~/fh/prog3/examples/src> g++ ConstParamError2.cpp
ConstParamError2.cpp: In function `int main()':
ConstParamError2.cpp:14: error: cannot convert `const Const (*)()' to `Const*'
   for argument `1' to `void doModify(Const*)'
sep@linux:~/fh/prog3/examples/src>

Wie man sieht, geht das Konzept zur Deklaration von konstanten Werten um einiges über das hinaus, was in Java über das Attribut final ausgedrückt werden kann. Stellen wir einmal gegenüber, inwieweit in Java das Attribut final und inwieweit in C++ das Attribut const Objekte vor Modifikationen schützt.
Zunächst testen wir dieses anhang einer einfachen Containerklasse in Java, die wir der Kürze halber mit Javas generischen Typen, die es seit Java 1.5 gibt, ausdrücken:
Box
public class Box <elementType> {
  public elementType contents;
  public  Box(elementType c) {contents = c;}
  
  public static void main(String [] _){
    final Box<Box<String>> bbs
	= new Box<Box<String>>(new Box<String>("hallo")); 
    
    //allowed
    bbs.contents.contents = "welt";

    //allowed
    bbs.contents = new Box<String>("world");

    //not allowed
    //bbs = new Box<Box<String>>(new Box<String>("hello")); 
  }
}

Der Javaübersetzer verhindert nur, daß einer als final attributierten Variablen ein neues Objekt zugewisen wird. Das über die Variable gespeicherte Objekt kann beliebig verändert werden
Jetzt versuchen wir dasselbe in C++. Da wir noch nicht generische Klassen in C++ kennengelernt haben, benötigen wir zwei Klassen Box. Eine um einen String zu kapseln, und eine um eine Box für einen String zu kapseln:
BoxBox
#include <string>

class BoxString{
  public:
    std::string contents;
    BoxString(std::string c) {contents = c;}
};

class BoxBoxString{
  public:
    BoxString* contents;
    BoxBoxString(BoxString* c) {contents = c;}
};

Als Test legen wir jetzt ein Objekt der Klasse BoxBoxString an und speichern einen Zeiger darauf in einer als konstant deklarierten Zeigervariablen und direkt in einer Variablen:
BoxBox
int main(){
  const BoxBoxString* pbbs
   = new BoxBoxString(new BoxString("hallo")); 
  const BoxBoxString bbs = *pbbs;

Jetzt versuchen wir über diese beiden Variablen drei verschiedenen Modifikationen. Einmal für den Inhalt der inneren Box, dann die Zuweisung eines neuen Inhalts und schließlich die Zuweisung auf die Variable selbst:
BoxBox
  //allowed
  pbbs->contents->contents = "welt";

  //not allowed
  //pbbs->contents = new BoxString("world");

  //allowed
  pbbs = new BoxBoxString(new BoxString("hello"));

Für den Zugriff über die Zeigervariable wird unterbunden, einem Feld des referenzierten Objekts einen neuen Wert zuzuweisen. Hingegen wird erlaubt in einem Feld des Objekts referenzierte Objekte zu verändern. Die Konstanzeigenschaft setzt sich also nicht transitiv über alle per Zeiger erreichbare Objekte fort. Vielleicht überraschend, daß der Variablen selbst ein neuer Zeiger zugewiesen werden darf.
Jetzt können wir noch die drei obigen Tests für den direkten Zugriff auf das Objekt durchführen.
BoxBox
  //allowed
  bbs.contents->contents = "welt";

  //not allowed
  //bbs.contents = new BoxString("world");

  //not allowed
  //bbs = *pbbs;
}

Zusätzlich wird verboten, daß der Variablen selbst ein neuer Wert zugewiesen wird.
Prinzipiell ist es zu empfehlen, möglichst wann immer möglich das Attribut const in C zu setzen.

4.1.6  Ableiten

Ebenso wie Java kennt C++ das Ableiten von Klassen. Es können Unterklassen zu einer Klasse definiert werden, die diese Unterklasse um weitere Eigenschaften erweitern oder bestehende Eigenschaften überschreiben. Statt des aus Java bekannten Schlüsselworts extends wird die Oberklasse zu einer Klasse mit einem Doppelpunkt vom Klassennamen abgetrennt. Wenn alle Eigenschaften der Oberklasse ohne weitere Einschränkungen benutzt werden sollen, so ist die Oberklasse als public geerbt mit anzugeben. Eine Objekt der Unterklasse ist damit auch ein Objekt der Oberklasse:
FirstSubClass
#include <string>
#include <iostream>

class Upper{};       

class Lower: public Upper{};

int main(){
  Upper* up = new Lower();
}

Für Konstruktoren gilt wieder dasselbe wie in Java. Bevor im Konstruktor der Unterklasse weitere für das Objekt der Unterklasse notwendige Initialisierungen bewerkstelligt werden können, ist zunächst einmal über einen Konstruktor der Oberklasse das Objekt vollständig zu initialisieren.
Hierzu gibt es nicht wie in Java das Schlüsselwort super sondern der Superkonstruktoraufruf wird vor dem eigentlichen Rumpf des Konstruktors mit dem Namen der Superklasse notiert.
Student
#include "Person.h"
#include <iostream>

class Student: public Person {

  private:
   int matrNr;

  public:
    Student(std::string nachname ,std::string vorname
          ,std::string strasse  ,int hausnummer
          ,int plz              ,std::string ort
          ,int mNr)
     :Person(nachname,vorname,strasse,hausnummer,plz,ort){
      this->matrNr = mNr;      
    }
};

int main(){
  Person* pP
   = new Student("Andrak","Manuel","schmidtstr"
                ,449,54654,"Koeln",723454);

  std::cout << pP->getFullName() << std::endl;
}

Anders als in Java braucht die Ableitungshierarchie in C++ nicht streng hierarchisch zu sein; eine Klasse kann mehrere Oberklassen haben. Die verschiedene Oberklassen einer Klasse werden durch Komma getrennt angegeben. Es kann dabei zu einen neuen Problem kommen: wie ist zu verfahren, wenn eine Klasse von zwei Oberklassen ein und dieselbe Methode erben. Welche der beiden Methoden ist im Zweifelsfall auszuführen? Wie wir es aus Java kennen, können wir Methoden in einer Unterklasse überschreiben (eng. override). Allerdings kennt C++ zwei Arten überschriebener Methoden:
Machen wir zunächst einen einfachen Test der Überschreibung einer Methode. Wir definieren uns eine Ober- und eine davon abgelittene Unterklasse. In der Unterklasse wird die Methode getDescription überschrieben.
UpperLowerTest1
#include <string>
#include <iostream>

  class Upper{
    public:
      std::string getDescription(){return "Obere Klasse";}
  };       

  class Lower: public Upper{
    public:
      std::string getDescription(){return "Untere Klasse";}
  };

In einem Test legen wir je ein Objekt dieser Klasse an und betrachten dieses einmal über eine Zeigervariable und einmal direkt. Die Variablen sind jeweils vom Typ der Oberklasse.
UpperLowerTest1
int main(){
  Upper* upP = new Upper();
  Upper* loP = new Lower();

  Upper up = *upP;
  Upper lo = *loP;

  std::cout << "upP->getDecription()" 
            << upP->getDescription() << std::endl;
  std::cout << "loP->getDecription()" 
            << loP->getDescription() << std::endl;

  std::cout << "up.getDecription()" 
            << up.getDescription() << std::endl;
  std::cout << "lo.getDecription()" 
            << lo.getDescription() << std::endl;
}

Wir betrachten das Ergebnis der Methode getDescription für alle die vier definierten Variablen. Als Ergebnis wird immer die Methode der Oberklasse ausgeführt.
sep@linux:~/fh/prog3/examples/src> ./UpperLowerTest
upP->getDecription()Obere Klasse
loP->getDecription()Obere Klasse
up.getDecription()Obere Klasse
lo.getDecription()Obere Klasse
sep@linux:~/fh/prog3/examples/src>

Wir haben also keine späte Bindung beobachten können. Obwohl in den Variablen loP und lo auf ein Objekt zugegriffen wird, daß einmal von der Klasse Lower erzeugt wurde, wird doch jeweils die Methode der Klasse Upper ausgeführt.
Jetzt machen wir einmal denselben Test, ändern aber nur ein Wort: wir stellen in der Klasse Upper der Methode getDescription das Attribut virtual voran:
UpperLowerTest2
#include <string>
#include <iostream>

  class Upper{
    public:
      virtual std::string getDescription(){return "Obere Klasse";}
  };       

  class Lower : public Upper{
    public:
     std::string getDescription(){return "Untere Klasse";}
  };

Nun lassen wir ein und denselben Test noch einmal laufen:
UpperLowerTest2
#include <string>

int main(){
  Upper* upP = new Upper();
  Upper* loP = new Lower();

  Upper up = *upP;
  Upper lo = *loP;

  std::cout << "upP->getDecription()" 
            << upP->getDescription() << std::endl;
  std::cout << "loP->getDecription()" 
            << loP->getDescription() << std::endl;

  std::cout << "up.getDecription()" 
            << up.getDescription() << std::endl;
  std::cout << "lo.getDecription()" 
            << lo.getDescription() << std::endl;
}

Jetzt stellen wir fest, daß der Zugriff auf das für die Klasse Lower erzeugte Objekt per Zeigervariable, zur späten Bindung der Methode getDescription führt:
sep@linux:~/fh/prog3/examples/src> ./UpperLowerTest2
upP->getDecription()Obere Klasse
loP->getDecription()Untere Klasse
up.getDecription()Obere Klasse
lo.getDecription()Obere Klasse
sep@linux:~/fh/prog3/examples/src>

Zu beachten ist, daß dieses nur über die Zeigervariable funktioniert. Der Aufruf der Methode getDescription über die Variable lo führt zur Ausführung der entsprechenden Methode aus der Klasse Upper.
Virtueller Destruktor  
Destruktoren sollen dafür sorgen, daß ein Objekt regelgerecht aus dem Speicher wieder entfernt wird. Hier ist späte Bindung wünschenswert. Es soll nicht davon abhängen, ob wir über eine Zeigervariable einer Oberklasse auf ein Objekt zugreifen, bei einem delete ist das Objekt der Unterklasse vollständig zu entfernen. Daher sind Destruktoren in der Regel virtuelle Methoden. Mindestens dann, wenn es mindestens eine virtuelle Methode in einer Klasse gibt, ist der Destruktor auch als virtuell zu deklarieren.

4.1.7  protected

Auch C++ kennt noch ein drittes Sichtbarkeitsattribut mit dem gleichen Namen wie in Java, nämlich protected. Mit protected wird Unterklassen einer Klasse erlaubt auf eine Eigenschaft zuzugreifen.

4.1.8  Überladen von Operatoren

C++ erlaubt es, alle in C++ bekannten Operatoren für jede beliebige Klasse zu überladen. Das gilt für die gängigen Operatoren wie +, -, *, / aber auch tatsächlich für jeden irgendwie in C++ legalen Operator, inklusive solcher Dinge wie der Funktionsanwendung, ausgedrückt durch das runde Klammernpaar.
Zum Überladen eines Operators Å ist in einer Klasse eine Funktion operatorÅ zu schreiben.
Beispiel:
Wir definieren eine Klasse zur Darstellung komplexer Zahlen. Auf komplexen Zahlen definieren wir Addition, Multiplikation und die Norm, die durch Funktionsanwendungsklammern ausgedrückt werden soll.
Complex
#include <string>
#include <iostream>

class Complex {
  public:
    float im;
    float re;

    Complex(float re,float im);

    Complex operator+(const Complex& other)const;
    Complex operator*(const Complex& other)const;
    float operator()()const;

    std::string toString()const;
};

Die Operatoren lassen sich wie reguläre Funktionen implementieren:
Complex
#include "Complex.h"
#include <sstream>

Complex::Complex(float re,float im){
  this->re=re;this->im=im;
}

Complex Complex::operator+(const Complex& other)const{
  return Complex(re+other.re,im+other.im);
}

Complex Complex::operator*(const Complex& other)const{
  return 
   Complex(re*other.re-im*other.im,re*other.im+im*other.re);
}

float Complex::operator()()const{
  return re*re+im*im;
}
std::string Complex::toString()const{
  std::string result="";
  std::stringstream ss;
  ss << re;
  ss >> result;
  result=result+"+";
  ss << im;
  ss >> result;
  result=result+"i";
  return result;
}

Jetzt können wir mit komplexen Zahlen ebenso rechnen, wie mit eingebauten Zahlentypen:
TestComplex
#include "Complex.cpp"    
  
int main(){
  Complex c1(31,40);
  Complex c2(0.5,2);
  std::cout << (c1+c2).toString() << std::endl;
  std::cout << (c1*c2).toString() << std::endl;
  std::cout << c2() << std::endl;
}

Nicht nur auf klassischen Zahlentypen können wir Operatoren definieren. Auch für unsere immer mal wieder beliebte Listenklasse, können wir Operatoren definieren. Zur Konkatenation von Listen ist z.B. der Operator + eine natürliche Wahl, so daß wir folgende Kopfdatei für Listen erhalten:
StringLi
#include <string>

class StringLi {
  private:
    std::string hd;
    const StringLi* tl;
    const bool empty;

  public: 
    StringLi(std::string hd,const StringLi* tl);
    StringLi();
    virtual ~StringLi();

    bool isEmpty()const;
    std::string head()const;
    const StringLi* tail()const;
 
    StringLi* clone()const;

    const StringLi* operator+(const StringLi* other) const;

    std::string toString()const;
};

Im folgenden die naheliegende Implementierung dieser Listen.
StringLi
#include <string>
#include "StringLi.h"

StringLi::StringLi(std::string h,const StringLi* t):empty(false){
  this->hd=h;
  this->tl=t;
}

StringLi::StringLi():empty(true){}

StringLi::~StringLi(){
  if (!isEmpty()) delete tl;
}

bool StringLi::isEmpty()const{return empty;}
std::string StringLi::head()const{return hd;}    
const StringLi* StringLi::tail()const{return tl;}
 
StringLi* StringLi::clone()const{
  if (isEmpty()) return new StringLi();
  return new StringLi(head(),tail()->clone());
}

const StringLi* StringLi::operator+(const StringLi* other) const{
  if (isEmpty()) return other->clone();
  return new StringLi(head(),(*tail())+other);
}

std::string StringLi::toString()const{
  if (isEmpty()) return "()";
  std::string result="["+head();
  for (const StringLi* it=this->tail()
      ; !it->isEmpty();it=it->tail()){
    result=result+","+it->head();
  }
  return result+"]";
}

Listen lassen sich jetzt durch die Addition konkatenieren:
StringLiTest
#include <iostream>
#include "StringLi.h"

int main(){
  const StringLi* xs
    = new StringLi("friends",
      new StringLi("romans",
      new StringLi("contrymen",
      new StringLi())));


  const StringLi* ys 
   = new StringLi("lend",
     new StringLi("me",
     new StringLi("your",
     new StringLi("ears",
     new StringLi()))));

  const StringLi* zs = *xs+ys;
  delete xs;
  delete ys;

  std::cout << zs->toString() << std::endl;
}

Wie man der Ausgabe entnehmen kann, führt der Operator + tatsächlich zur Konkatenation.
sep@linux:~/fh/prog3/examples/src> g++ -o StringLiTest StringLi.cpp StringLiTest.cpp
sep@linux:~/fh/prog3/examples/src> ./StringLiTest
[friends,romans,contrymen,lend,me,your,ears]
sep@linux:~/fh/prog3/examples/src>

Eine vollständige Tabelle aller in C++ überladbaren Operatoren findet sich in Abbildung 4.1.
+-*/%^&
|~!=<>+=
-=*=/*%=^=&=|=
<<>><<=>>===!=<=
>=&&||++--->*,
->[]()newdeletenew[]delete[]
#1C++ Operatoren, die überladen werden können.
Aufgabe 12   (3 Punkte) Ergänzen Sie die obige Listenklasse um weitere Listenoperatoren.
Testen Sie ihre Implementierung an einigen Beispielen.

4.1.9  Generische Formularklassen

Wir haben im Zusammenhang mit dem Attribut const bereits ein Beispiel dafür gesehen, daß wir zweimal fast dieselbe Klasse geschrieben haben. Lediglich in einem bestimmten Typnamen haben sich die beiden Klassen unterschieden. Einmal hatten wir eine Klasse zum kapseln eines Stringwertes und einmal eine analoge Klasse zum Speichern eines Objektes der ersten Klasse erhalten. Wir werden in der Programmierung häufig in die Situation kommen, daß sich zwei Klassen lediglich im Typ eines ihrer Felder unterscheiden. Um verdoppelten Code zu verhindern, bietet C++ an, auch Klassen generisch zu schreiben.
Betrachten wir einmal zwei Klassen, die einfache Container sind. Einmal einen Container für die Stringwerte:
BoxString
#include <string>

class BoxString{
  public:
    std::string contents;
    BoxString(std::string c) {contents = c;}
};

Zum anderen eine analoge Klasse hierzu, zum Speichern von ganzen Zahlen:
BoxInt
class BoxInt{
  public:
    int contents;
    BoxInt(int c) {contents = c;}
};

Über den Formularmechanismus von C++ können wir statt dieser beiden Klassen eine einzige Formularklasse, eine generische Klasse schreiben. Hierzu ersetzen wir den konkreten Typ der variabel gehalten werden soll durch eine frei zu wählende Typvariable und deklarieren vor der Klasse, daß es sich um eine Formularklasse mit genau dieser Typvariablen für die Klasse handeln soll:
Box
#include <string>

template <class elementType>
class Box{
  public:
    elementType contents;
    Box(elementType c) {contents = c;}
};

Diese generische Klasse kann jetzt jeweils für konkrete Typen instanziiert werden. Hierzu dient die von den generischen Methoden bereits bekannte Notation mit den spitzen Klammern. Dem Typ Box ist stets mit anzugeben, mit welchem konkreten Typ für die Typvariable er benutzt werden soll. So gibt es jetzt den Typ Box<std::string> ebenso wie den Typ Box<int> und sogar den in sich verschachtelten Typ Box<Box<std::string>*>:
TestGenBox
#include <string>
#include <iostream>
#include "Box.h"

int main(){
  const Box<std::string>* bs = new Box<std::string>("hallo"); 
  const Box<int>* bi = new Box<int>(42); 
  const Box<Box<std::string>*>* bbs
    = new Box<Box<std::string>*>(new Box<std::string>("hallo")); 

  std::string s = bs -> contents;
  int i = bi -> contents;
  std::string s2= bbs -> contents->contents;

  std::cout << bs -> contents << std::endl;
  std::cout << bi -> contents << std::endl;
  std::cout << bbs -> contents->contents << std::endl;
}

Generische Typen sind ein zur Objektorientierung vollkommen orthogonales Konzept; es verträgt sich vollkommen mit den objektorientierten Konzepten, wie der Vererbung. Es ist möglich Unterklassen generischer Klassen zu bilden. Diese Unterklassen können selbst generisch sein oder auch nicht.
Wir können z.B.  die obige Klasse Box erweitern, so daß wir in der Lage sind Paare von Objekten zu speichern:
Pair
#include <string>
#include "Box.h"

template <class fstType,class sndType>
class Pair: public Box<fstType>{
  public:
    sndType snd;
    Pair(fstType c,sndType snd):Box<fstType>(c),snd(snd) {}

    std::string toString(){
	return "("+contents+","+snd+")"; 
    }
};

Diese Klasse läßt sich ebenso wie die Klasse Box für konkrete Typen für die zwei Typvariablen instanziieren.
TestPair
#include "Pair.h"
#include <iostream>
#include <string>

int main(){
  Pair<std::string,int> p("hallo",42);
  std::cout << p.contents << std::endl;
  std::cout << p.snd << std::endl;
}

Wir können z.B.  eine Paarklasse definieren, die nur noch eine Typvariable hat.
UniPair
#include "Pair.h"

template <class fstType>
class UniPair: public Pair<fstType,fstType>{
  public:
    UniPair(fstType c,fstType snd):Pair<fstType,fstType>(c,snd) {}
    void swap(){fstType tmp=contents;contents=snd;snd=tmp;}
};

Jetzt muß für diese Klasse im konkreten Fall auch nur noch ein Typparameter instanziiert werden:
TestUniPair
#include "UniPair.h"
#include <iostream>
#include <string>

int main(){
  UniPair<std::string> p("welt","hallo");
  p.swap();
  std::cout << p.contents << std::endl;
  std::cout << p.snd << std::endl;
}

Generische Listen

Eine Paradeanwendung für generische Typen sind natürlich sämtliche Sammlungsklassen. Anstatt jetzt einmal Listen für Zeichenketten und einmal Listen für ganze Zahlen zu programmieren, können wir eine Schablone schreiben, in der der Elementtyp der Listenelemente variabel gehalten wird.
Im folgenden also eine weitere Version underer Listenklasse. Dieses Mal als generische Klasse. Die Typvariable haben wir mit dem Symbol A bezeichnet.
Zunächst sehen wir zwei Hilfsfunktionen vor, die uns helfen werden, die Methoden contains und toString möglichst generisch zu formulieren:
Li
#include <string>
#include <sstream>

template <typename X>
bool equals(X x,X y){return x==y;}

std::string callToString(std::string x ){return x;}
std::string callToString(int x ){

  std::stringstream ss;
  std::string result;

  ss << x;
  ss >> result;

  return result;
}

template <typename X>
std::string callToString(X* x ){return x->toString();}

Jetzt können wir mit der eigentlichen Klasse beginnen. Wir haben zunächst die gängigen Definitionen für Konstruktoren, Selektoren und Testmethode:
Li
template <class A>
class Li {
  private:
    A hd;
    const Li<A>* tl;
    const bool empty;

  public: 
    Li(A hd,const Li<A>* tl):empty(false){
      this->hd=hd;
      this->tl=tl;
    }

    Li():empty(true){}


    bool isEmpty()const{return empty;}
    A head()const{return hd;}    
    const Li<A>* tail()const{return tl;}

Im Destruktor wird die Listenstruktur aus dem Speicher geräumt. Achtung, die Elemente werden nicht gelöscht. Dieses ist extern vorzunehmen.
Li
    virtual ~Li(){
      if (!isEmpty()) delete tl;
    }

Es folgen ein paar gängige Methoden, die wir schon mehrfach implementiert haben.
Li
    Li<A>* clone()const{
      if (isEmpty()) return new Li<A>();
      return new Li<A>(head(),tail()->clone());
    }

    const Li<A>* operator+(const Li<A>* other) const{
      if (isEmpty()) return other->clone();
      return new Li<A>(head(),(*tail())+other);
    }

    int size() const {
      if (isEmpty()) return 0;
      return 1+tail()->size();
    }

Die Methode contains, die testen soll, ob ein Element in der Liste enthalten ist, sehen wir in zwei Versionen vor. Der einen Versionen kann eine Gleichheitsfunktion für die Elemente mitgegeben werden, die andere benutzt hierfür den Operator ==.
Li
    bool contains(A el, bool (*eq)(A,A))const{
      if (isEmpty())return false;
      if (eq(head(),el)) return true;
      return tail()->contains(el,eq);
    }

    bool contains(A el)const{
      return contains(el,equals);
    }

Als nächstes folgt eine typische Methode höherer Ordnung. Es wird eine neue Liste erzeugt, indem auf jedes Listenelement eine Funktion angewendet wird. Hier kommt eine weitere Typvariable ins Spiel. Wir lassen in dieser Methode offen, welchen Typ die Elemente der Ergebnisliste haben soll.
Li
    template <typename B>
    Li<B>* map(B(*f)(A))const{
      if (isEmpty()) return new Li<B>();
      return new Li<B>(f(head()),tail()->map(f));
    }

Und schließlich die Methode, um eine Stringdarstellung der Liste zu erhalten. Diese Methode erwartet vom Elementtyp A, daß auf disem eine Funktion callToString definiert ist. Diese haben wir oben für Zeichenketten und für Zeiger auf Klassen, die eine Methode toString haben, definiert.
Li
    std::string toString(){
      if (isEmpty()) return "[]";
      std::string result="["+(callToString(this->head()));
      for (const Li<A>* it=this->tail()
          ; !it->isEmpty();it=it->tail()){
        result=result+","+(callToString(it->head()));
      }
      return result+"]";
    }
};

Zeit einen Test für die generischen Listen zu schreiben. Wir legen Listen von Zeichenketten, aber auch Listen von Paaren an. Einige Methoden werden ausprobiert, insbesondere auch die Methode map. Auch die Methode toString läßt sich für die verschiedenen Elementtypen ausführen.
TestLi
#include "Li.h"
#include "UniPair.h"
#include <iostream>
#include <string>

int callSize(std::string s){return s.size();}

std::string getContents(UniPair<std::string>* p){
 return p->contents;
}

int main(){
  Li<std::string>* xs
    = new Li<std::string>("Schimmelpfennig",
      new Li<std::string>("Moliere",
      new Li<std::string>("Shakespeare",
      new Li<std::string>("Bernhard",
      new Li<std::string>("Brecht",
      new Li<std::string>("Salhi",
      new Li<std::string>("Achternbusch",
      new Li<std::string>("Horvath",
      new Li<std::string>("Sophokles",
      new Li<std::string>("Calderon",
      new Li<std::string>()))))))))));

  std::cout << xs->toString() << std::endl; 
  std::cout << xs->contains("Brecht") << std::endl; 
  std::cout << xs->contains("Zabel") << std::endl; 

  Li<int>* is = xs->map(callSize);
  delete xs;

  std::cout << is->toString() << std::endl; 
  delete is;

  Li<UniPair<std::string>* >* ys =
  new Li<UniPair<std::string>* >(new UniPair<std::string>("A","1")
 ,new Li<UniPair<std::string>* >(new UniPair<std::string>("B","2")
 ,new Li<UniPair<std::string>* >(new UniPair<std::string>("C","3")
 ,new Li<UniPair<std::string>* >())));
 
  std::cout << ys->toString() << std::endl; 

  xs=ys->map(getContents);
  std::cout << xs->toString() << std::endl; 
}

Der Test führt zu folgenden Ausgaben:
sep@linux:~/fh/prog3/examples/src> ./TestLi
[Schimmelpfennig,Moliere,Shakespeare,Bernhard,Brecht,Salhi,Achternbusch,Horvath,Sophokles,Calderon]
1
0
[15,7,11,8,6,5,12,7,9,8]
[(A,1),(B,2),(C,3)]
[A,B,C]
sep@linux:~/fh/prog3/examples/src>

Aufgabe 13   (Alternative zur letzten Punkteaufgabe 3 Punkte) In dieser Aufgabe ist eine Klassen für HashMengen zu schreiben:
Aufgabe 14   Schreiben Sie jetzt analog zu Ihrer Lösung aus der letzten Aufgabe, eine generische Klasse HashMap, die eine endliche Abbildung von Schlüsseln auf Werte realisiert. Benutzen Sie hierzu auch die bereits geschrieben Klasse Pair, um die Schlüssel-Wert-Paare in Listen zu speichern.

4.1.10  Closures, Funktionsobjekte und Schönfinkeleien

In diesem Kapitel sollen ein Paar Konzepte aus der funktionalen Programmierung in C++ vorgestellt werden. Wir werden dabei nicht mehr allein auf C Funktionszeigern zurückgreifen, sondern Klassen schreiben, die Funktionsobjekte repräsentieren.

Closures

Zunächst wollen wir eine Klasse vorstellen, die lediglich einen Wert speichern kann; wir erhalten eine Klasse entsprechend der generischen Klasse Box aus dem vorangegangenen Abschnitt. Wie sehen also ein Feld vor, in dem ein beliebiger Wert abgespeichert werden kann. Zum Initialsieren sei ein Konstruktor vorhanden. Ein leerer Konstruktor ermöglicht auch uninitialisierte Werte zu haben:
Closure
template <class A>
class Closure {

  private:
    A value;

  public: 
    Closure <A> (A a) {
      this->value=a; 
    };

    Closure <A>(){}

Auf den abgespeicherten Wert soll über eine get-Methode zugegriffen werden. Statt aber eine Methode getValue vorzusehen, überladen wir den Operator der Funktionsanwendung. Dieses ist der Operator bestehend aus den rundem Klammerpaar.
Closure
    virtual  A operator()() { 
      return value;
    };
};

Damit können wir ein Closure-Objekt wie eine Funktion ohne Parameter benutzen, die den einmal abgespeicherten Wert als Rückgabe hat:
TestSimpleClosure
#include "Closure.h"
#include <iostream>

int main(){
   Closure<int> f1 = Closure<int>(42);
   std::cout << f1() << std::endl;   
}

Mit der Klasse Closure lassen sich also einfache konstante Funktionen beschreiben. Nun wollen wir dieses Konzept erweitern:
Ein closure soll eine noch nicht ausgerechnete Funktionsanwendung bezeichnen. Hierzu betrachten wir zunächst erst einmal Funktionen mit genau einem Argument. Solche Funktionen haben einen Parametertyp und einen Rückgabetyp. Diese beiden Typen lassen wir generisch und beschreiben sie durch eine Typvariable aT für Argumenttyp und rT für Rückgabetyp.
ApplyFun
#include "Closure.h"
#include <iostream>

template <class aT,class rT> 
class ApplyFun : public Closure<rT> {

Ein closure wird durch eine Funktion und den Wert, auf den diese Funktion angewendet werden soll ausgedrückt:
ApplyFun
  private:
    rT (*f)(aT); 
    aT arg;

Zusätzlich sehen wir Felder vor, in denen, sobald es berechnet wurde, das Ergebnis der Funktionsanwendung gespeichert werden kann und die Information, ob das Ergebnis bereits berechnet wurde:
ApplyFun
    rT result;
    bool resultIsEvaluated;

Der Konstruktor initialisiert die beiden Felder der Funktion und des Arguments. Für ein neu erzeugtes Objekt wird das Ergebnis der Funktionsanjwendung noch nicht ausgerechnet:
ApplyFun
  public: 
    ApplyFun <aT,rT> (rT(*f)(aT),aT a) {
      this->f=f; 
      this->arg=a;
      resultIsEvaluated=false;
    };

    ApplyFun <aT,rT> () {resultIsEvaluated=false;}

Entscheidend ist nun, wie der Operator der Funktionsanwedung überschrieben wird: hier soll nun endlich die Funktions auf ihr Argument angewendet werden. Wurde das Ergebnis bereits ausgewertet, so findet es sich im Feld result, ansonsten ist es erst zu berechnen und für eventuelle spätere Aufrufe im Feld result zu speichern.
ApplyFun
    virtual  rT operator()() { 
      if (!resultIsEvaluated){
        resultIsEvaluated = true;
        result=f(arg);
      }
      return result;
    };
};

Mit der Klasse ApplyFun haben wir zwei Dinge erreicht:
Zeit nun endlich einmal mit Objekten der Klasse ApplyFun zu arbeiten:
TestClosure
#include "ApplyFun.h"
#include "Li.h"
#include <iostream>
#include <string>

Hierzu definieren wir uns zunächst ein paar sehr einfache Funktionen, die wir in closures verpacken wollen:
TestClosure
int add1(int x){std::cout << "add1" << std::endl;return x+1;}
int add2(int x){std::cout << "add2" << std::endl;return x+2;}
int add3(int x){std::cout << "add3" << std::endl;return x+3;}
int add4(int x){std::cout << "add4" << std::endl;return x+4;}
int getSize(std::string xs){return xs.length();}

Wir können ein paar closures anlegen:
TestClosure
int main(){
  Closure<int>* c1 = new ApplyFun<int,int>(add1,41);
  Closure<int>* c2 = new ApplyFun<int,int>(add2,40);
  Closure<int>* c3 = new ApplyFun<int,int>(add3,39);
  Closure<int>* c4 = new ApplyFun<int,int>(add4,38);

Erst durch expliziten Aufruf des Funktionsklammeroperators führt dazu, daß tatsächlich eine Funktion auf ihr Argument angewendet wird:
TestClosure
  std::cout << (*c2)() << std::endl;  

Wir können jetzt z.B. eine Liste von closures anlegen.
TestClosure
  Li<Closure<int>* >* xs = 
      new Li<Closure<int>* >(c1,
      new Li<Closure<int>* >(c2,
      new Li<Closure<int>* >(c3,
      new Li<Closure<int>* >(c4,
      new Li<Closure<int>* >()))));

Berechnen wir die Länge einer solchen Liste von closures, so werden die einzelnen Werte der Listenelemente nicht ausgerechnet, sie werden dazu ja nicht benötigt:
TestClosure
  std::cout << xs->size() << std::endl;

Erst wenn wir explizit ein Element der Liste berechnet haben wollen, wird dessen Berechnungen angestoßen.
TestClosure
  std::cout << (*(xs->tail()->tail()->head()))() << std::endl;  
  delete xs;

Abschließend ein Beispiel für ein closure, der auf Strings arbeitet:
TestClosure
  Closure<int>*  c5
    = new ApplyFun<std::string,int>(getSize,"witzelbritz");
  std::cout << (*c5)() << std::endl;  
}

Das Programm hat folgende leicht nachzuvollziehende Ausgabe:
sep@linux:~/fh/prog3/examples/src> ./TestClosure
add2
42
4
add3
42
11
sep@linux:~/fh/prog3/examples/src>

In den Programmiersprachen Miranda[Tur85], Clean (www.cs.kun.nl/~clean) und Haskell[Jon03] sind closures zum Standardprinzip erhoben worden. Für jede Funktionsanwendung wird zunächst ein nicht ausgewerteter closure im Speicher angelegt. Erst wenn durch eine if- oder case-Bedingung das Ergebnis der Berechnung benötigt wird, stößt die Laufzeitumgebung die Berechnung des Ergebnisses der Funktionsanwendung an. Solche Programmiersprachen werden als lazy bezeichnet.
Unendliche Listen  
Über noch nicht ausgewertete Funktionsanwendungen lassen sich jetzt potentiell unendliche Listen beschreiben. Hierzu sehen wir eine Listenklasse vor, die nicht die Schwanzliste in einem Feld gespeichert hat, sondern einen closure, in dem lediglich gespeichert ist, wie der Rest Liste zu berechnen ist, falls denn einmal die Methode tail aufgerufen wird.
Wir können also unsere Listenklasse so umändern, daß im Feld tl nun ein closure gespeichert ist und die Methode tail zur Auswertung dieser führt. Ansonsten läßt sich unsere bisherige Listenimplementierung übernehmen:
LazyList
#include "ApplyFun.h"
#include <iostream>

template <class A>
class LazyList {
  private:
    A hd;

Statt also direkt des Schwanz der Liste zu speichern, speichern wir intern einen closure für diesen Schwanz:
LazyList
    Closure<LazyList<A>*>* tl;
    const bool empty;

Wir sehen sowohl einen Cons-Konstruktor für eine direkt übergebene Schwanzliste, als auch für eine closure, der die Schwanzliste berechnet vor:
LazyList
  public: 
    LazyList(A hd, Closure<LazyList<A>*>* tl):empty(false){
      this->hd=hd;
      this->tl=tl;
    }

    LazyList(A hd,LazyList<A>* t):empty(false){
      this->hd=hd;
      this->tl= new Closure<LazyList<A> >(t);
    }

Der Konstruktor für leer Listen fehlt natürlich nicht:
LazyList
    LazyList():empty(true){}

Die Methoden head und isEmpty funktionieren, wie bereits bekannt:
LazyList
    bool isEmpty()const{return empty;}
    A head()const{return hd;}

Die Methode tail sorgt jetzt dafür, daß die Schwanzliste endlich ausgerechnet wird:
LazyList
    LazyList<A>* tail(){
      return (*tl)();
    }
};

Jetzt können wir z.B. die Liste aller natürlichen Zahlen definieren. Trotzdem läuft uns der Speicher nicht voll, weil wir immer nur die nächste Restliste erzeugen, wenn die Methode tail aufgerufen wird. Hierzu schreiben wir eine Methode from, die einen Listenknoten von Lazylist erzeugt, und für den tail einen closure erzeugt:
TestLazyList
#include "LazyList.h"
#include <iostream>

LazyList<int> *  from(int fr){
  Closure<LazyList<int>* >* tl 
   = new ApplyFun<int,LazyList<int>*>(from,fr+1);
  return new LazyList<int>(fr,tl);
}

Folgender kleiner Test illustriert, die Benutzung der verzögerten Listen:
TestLazyList
int main(){
  LazyList<int>* xs = from(2);  
  for (int i=0;i<200000;i=i+1){
    LazyList<int>* ys = xs->tail();  
    std::cout << xs->head() << std::endl;
    delete xs;
    xs = ys;
  }
}

Wenn wir dieses Programm starten bekommen wir nacheinander die natürlichen Zahlen ausgegeben. Das Programm hat einen konstanten Speicherbedarf.
Unsere unendlichen Listen realisieren ein Konzept, das uns schon lange bekannt ist: Iteratoren. Die Methode isEmpty entspricht der negierten Methode hasNext. Über die Methode tail erhalten wir in dieser Analogie einen neuen Iterator, dessen nächster Wert über die Methode head erfragt werden kann. Ebenso wie bei Iteratoren in der Regel nicht alle Werte, die der Iterator enthält, komplett zu einer Zeit im Speicher liegen, werden von Objekten der Klasse LazyList auch die Elemente erst nach Bedarf im Speicher angelegt. Und natürlich können Iteratoren auch unendlich viele Werte liefern. Hierzu haben wir im ersten Semester bereits ein Beispiel gesehen, als wir die obiger Liste analoge Klasse From programmiert haben.
Aufgabe 15  
Schreiben Sie weitere Funktionen, die Objekte der Klasse LazyList erzeugen und testen Sie diese:

Funktionsobjekte

Die Objekte der Klasse Closure des letzten Abschnitts haben konstante Funktionen ohne Parameter repräsentiert. In diesen Abschnitt wollen wir echte Funktionsobjekte modellieren, die Funktionen mit einem Parameter darstellen. Hierzu kapseln wir einfach einen Funktionszeiger in einem Objekt und überladen den Funktionsanwendungsoperator für diese Klasse, so daß er direkt zur Funktionsanwendung führt. Wir erhalten die folgende einfache Klasse:
Lambda
template <class aT, class rT> 
class Lambda { 
  private:
    rT (*f)(aT); 

  public: 
    Lambda <aT,rT> (rT(*_f)(aT)){
      this->f=_f; 
    }; 

    Lambda <aT,rT> (){}

    virtual  rT operator()(aT arg) { 
      f(arg);
    };
};

Ein Objekt des Typs Lambda<argType,resultType> kann nun ebenso benutzt werden wie ein Funktionszeiger des Typs: resultType (* f)(argType).
TestLambda
#include "Lambda.h"
#include <iostream>

int add5(int x) {return x+5;}

int main(){
  Lambda<int,int> f1(add5);
  std::cout << add5(37) << std::endl;  
  std::cout << f1(37)   << std::endl;  
}

Currying

Im letzten Abschnitt haben wir eine Klasse geschrieben, die Funktionen darstellen kann, indem ein Funktionszeiger einfach gekapselt wird. Wir können jetzt Unterklassen der Klasse Lambda definieren, die uns erlauben auf verschiedene Weise Funktionsobjekte zu erzeugen.
Wir wollen eine Klasse schreiben, mit der sich Funktionsobjekte nach dem Prinzip des sogenannten Currying erzeugen lassen. Diesen Prinzip ist nicht nach einem indischen Reisgericht sondern nach dem Mathematiker Haskell B. Curry(1900-1982) benannt10.
Die Grundidee des Currying ist, eine Funktion nur partiell auf nicht alle Parameter anzuwenden. Eine Funktion f mit zwei Argumenten, also mit dem Typ: (a,b)®c kann betrachtet werden als eine Funktion mit nur einem Parameter, deren Ergebnis eine Funktion mit einem zweiten Parameter ist, also mit dem Typ: a® (b® c). Unter dieser Betrachtungsweise läßt sich eine Funktion f zunächst auf das erste Argument anwenden und dann das Ergebnis dieer Anwendung auf das zweite Argument. Also statt f(x,y) kann die zweistellige Funktion betrachtet werden als: (f(x))(y). Somit lassen sich unter der Voraussetzung, daß Funktionen Daten sind, die das Ergebnis einer anderen Funktion sein können, alle Funktionen als einstellige Funktionen modellieren.
Jetzt wollen wir in C++ eine Funktionsklasse schreiben, die für eine zweistellige Funktion und einen Wert für den ersten Paramter eine Funktion, die noch den zweiten Parameter erwartet, schreiben. Hierzu leiten wir eine generische Klasse Curry von unserer Funktionsklasse Lamda ab.
Curry
#include <iostream>
#include "Lambda.h"

template <class a,class b,class c>
class Curry : public Lambda<a,c>{

Ein Curry-Objekt soll für eine zweistellige Funktion und einen Wert für den ersten Parameter eine Funktion erzeugen, die den zweiten Parameter erwartet. Hierzu sehen wir Felder für Funktion und ersten Parameter vor und initialisieren diese im Konstruktor:
Curry
  public:
    b i;
    a (*f)(b x,c y);

    Curry(a(*f)(b x, c y),b i):i(i),f(f){}

Im Funktionsanwenungsoperator wird schließlich die ursprünglich zweistellige Funktion auf die endlich vorliegenden zwei Parameter angewendet:
Curry
    virtual a operator()(c y){ return f(i,y);}
};

Zeit für einen kleinen Test. Wir benutzen eine zweistellige Additionsfunktion. Aus dieser können wir neue einstellige Funktionen über Currying erzeugen, die auf einen Wert einen festen Zahlenwert draufaddieren:
Curry
int add(int x,int y){return x+y;}

int main(){
  Lambda<int,int>* add5 = new Curry<int,int,int>(add,5);
  int i = (*add5)(37);
  std::cout << i << std::endl;
}

Das Prinzip des Currying kan ein nützliches Entwurfsmuster sein, wenn die Daten der Argumente für eine Funktionsanwednung nicht zu einem Zeitpunkt gleichzeitig vorliegen. Dann läßt sich die Funktion schon einmal auf die zunächst bereits vorliegenden Daten anwenden und das entstehende Funktionsobjekt für die weiteren Anwendungen speichern. Insbesondere in der Programmierung von Graphiken kann es eine solche Situation geben. Angenommen es gibt eine Funktion zum zeichnen von Vierecken mit 5 Argumenten:
drawRect(Color c,Width w,Height h,Pos xPos,Pos yPos)
Mittels Currying ließen sich jetzt neue Funktionen definieren, eine in der die Farbe bereits gesetzt ist, eine in der die Form auf Quadrate einer bestimmten Größe festgelegt ist:
drawRedRect = curry(drawRect,red);
drawRedSquare = curry(drawRedRect,3,3);

Die so neu gebauten Funktionsobjekte lassen sich nun benutzen, um an unterschiedlichen Positionen rote Quadrate zu zeichnen:
drawRedSquare(42,80);
drawRedSquare(17,4);
drawRedSquare(08,15);
drawRedSquare(47,11);

4.2  Die Standard Template Library

Die wichtigste Bibliothek in C++ stellt die standard template library (STL) dar. In ihr finden sich Klassen für Sammlungen, aber auch eine Klasse für Strings, ähnlich wie wir sie aus Java kennen, und Klassen, die sich mit Funktionsobjekten beschäftigen. Die STL benutzt exessiv Klassenschablonen, auch für Klassen, bei denen man das nicht gleich erwarten würde, wie z.B. string. In der STL werden fast alle in den vorangegangenen Abschnitten vorgestellten Konzepte benutzt, wie z.B. generische Klassen und Funktionen, überladene Operatoren, Funktionen höherer Ordnung, verallgemeinerte Zeigertypen und verallgemeinerte Funktionstypen. Das Bestreben, die in der Bibliothek umgesetzte Funktionaliltät möglichst allgemein sowohl für generische Sammlungsklassen als auch für Reihungen anzubieten, führt dazu, daß die Methoden in der STL nicht Objektmethoden sind, sondern globale Funktionen, die in gleicher Weise für Reihungen als auch auf Sammlungen funktionieren. Daher war es auch sinnvoll, den Funktionen nicht allgemein Sammlungen bzw. Reihungen als Parameter zu übergeben, sondern ein verallgemeinertes Konzept von Zeigern auf das erste und das letzte Element der Sammlung, für die eine Funktion auszuführen ist.

4.2.1  ein kleines Beispiel

Als kleines Beispiel, an dem fast alle wichtigen Konzepte der STL zu erkunden ist, wollen wir eine Sammlung von Zahlen nehmen, der wir eine einstellige Funktion übergeben. Die Funktion soll auf jedes Element der Sammlung angewendet werden. Hierzu stellt die STL die Funktion for_each zur Verfügung. Sie hat drei Parameter, den Anfang und das Ende des Bereichs, auf dem die im dritten Parameter übergebene Funktion angewendet werden soll.

Anwendung auf Reihungen

Zunächst betrachten wir, wie die Funktion for_each für eine Reihung angewendet werden kann. Wir schreiben eine Funktion, die ganze Zahlen verdoppelt. In der Hauptmethode legen wir eine Reihung von drei Elementen an und wenden die Funktion durch einen Aufruf von for_each auf jedes Element der Reihung an.
ForEachArray
#include <algorithm>
#include <iostream>

void doppel(int& x){x=2*x;}

int main(){
  int  ar[3] = {17,28,21};
  std::for_each(ar,ar+3,doppel);
   
  for (int i= 0;i<3;i=i+1){
    std::cout << ar[i] << std::endl;
  }
}

Anwendung auf Vektoren

Jetzt machen wir dasselbe Spiel nicht für eine Reihung sondern für eine Sammlung der STL-Klasse vector.
ForEachVector
#include <vector>
#include <algorithm>
#include <iostream>

void doppel(int& x){x=2*x;}

int main(){
  std::vector<int> v = std::vector<int>(3);
  v[0] = 17; v[1]=28; v[2] = 21;
  std::for_each(v.begin(),v.begin()+3,doppel);
   
  for (int i= 0;i<3;i=i+1){
      std::cout << v[i] << std::endl;
  }
}

Wie man sieht, ist die Klasse vector generisch über ihren Elementtyp11. Der Operator [] ist überladen, so daß er ebenso wie bei Reihungen benutzt werden kann. Lediglich das Argument, das den Anfang des Elementbereichs bezeichnen, für den for_each anzuwenden ist, wird über die Objektmethode begin() der Klasse vector bestimmt. Ansonsten sieht das Programm fast identisch zu der Version Auf Reihungen aus.
Nach diesem kleinen motivierenden Beispiel betrachten wir in den folgenden Abschnitten die in der STL realisierten Konzepte im Einzelnen.

4.2.2  Konzepte

Eine Schwäche der C++ Schablonen haben wir schon angesprochen. Es gibt keine Möglichkeit in C++ auszudrücken, welche Typen für die Typvariablen eingesetzt werden können. Dieses kann nicht in der Signatur oder in irgendeiner Weise in C++ ausgedrückt werden. Hier ist man auf Versuch und Irrtum angewiesen. Erst wenn man versucht, eine Schablone mit einem bestimmten Typ zu benutzen, kann es zu einem Fehler kommen, wenn sich herausstellt, daß dieser Typ nicht für die Typvariable eingesetzt werden konnte. Ein solches Beispiel haben wir in dem Programm TraceWrongUse.cpp bereits kennengelernt.
Den Entwicklern der STL war dieses Problem der C++ Formularklasse natürlich bewußt. Sie haben daher großen Wert darauf gelegt, möglichst genau in der Dokumentation zu beschreiben, mit was für Typen eine Typvariable instanziiert werden darf. Hierfür haben die STL Entwickler sogenannte Konzepte (concepts) eingeführt. Konzepte existieren nur in der STL-Dokumentation und sind kein Bestandteil von C++.
Als Beispiel betrachten wir jetzt einmal die Signatur der oben benutzten Funktion for_each:
template <class InputIterator, class UnaryFunction>
UnaryFunction for_each
 (InputIterator first, InputIterator last, UnaryFunction f);

Wie man sieht, ist die Funktion über zwei Typen generisch geschrieben. Für die zwei generisch gelassene Typen werden die Typvariablen UnaryFuntion und InputIterator benutzt. Betrachtet man die STL Dokumentation, so stellt man fest, daß für die Typvariablennamen UnaryFunction und InputIterator je eine eigene Dokumentationsseite existiert. In dieser Seite ist beschrieben, was ein Typ, der für die Variable eingesetzt werden kann, alles für Funktionalität vorweisen können muß. Dieses wird in der STL Dokumentation als Konzept bezeichnet. So sind wir nicht überrascht, daß für das Konzept UnaryFunction verlangt wird, daß Typen dieses Konzepts den einstelligen Funktionsanwendungsoperator besitzen.
In Java würde man Konzepte durch Schnittstellen beschreiben.

4.2.3  Iteratoren

Eines der wichtigsten Konzepte der STL sind Iteratoren. Iteratoren in der STL sind kaum zu vergleichen mit den über eine Schnittstelle definierten Iteratoren aus Java. In der STL stehen Operatoren vielmehr ein verallgemeinertes Prinzip von Zeigern auf Elementen einer Sammlung dar.
Auch Iteratoren sind in der STL als Konzept spezifiziert. Ein Iterator ist ein Typ, für den bestimmte Funktionen und Operationen zur Verfügung stehen.

Triviale Iteratorn

Das einfachste Konzept eines Iterators, dem alle anderen Iteratoren zu Grunde liegen, ist der trivial iterator.
Für alle Typen X des Konzepts trivial iterator wird verlangt:

InputIterator

Das erste Konzept eines Iterators ist uns in der Funktion for_each begegnet. Dabei handelte es sich um einen einfachen Operator, der zu den im Konzept des trivialen Iterators noch eine weitere Operation zuläßt, nämlich das Erhöhen des Iterators, so das er auf ein Nachfolgeelement zeigt. Dieses drückt sich dadurch aus, daß erwartet wird, daß der Operator ++ für Typen dieses Konzepts existiert. Es soll also möglich sein, den Iterator um eins zu erhöhen, so daß seine Dereferenzierung auf ein nächstes Element zeigt.
Wie man sieht, erfüllen Zeiger allgemein die Anforderungen eines InpuIterators. Über die Zeigerarithmetik können wir jeweils auf das nächste im Speicher anbgelegte Element gleichen Typs zugreifen.
Wenn wir zusätzlich noch eine Gleichheit auf InputIteratoren voraussetzen, so haben wir alles, um mit zwei Iteratoren, die Anfang und Ende eines zu iterierenden Elementbereichs bezeichnen, über alle Elemente dieses Bereichs einmal von vorne bis hinten durchzuiterieren:
Beispiel:
Wir schreiben eine generische Funktion, die ermöglicht das größte Element eines Iterationsbereichs zu bestimmen.
InputIterator
#include <iostream>
#include <string>

template <typename InputIterator,typename Element>
Element* maximum(InputIterator begin,InputIterator end){
  Element* result = begin; 
  for (InputIterator it = begin+1;it<end;it++){
    if ((*result) < (*it)){ result=it;}
  } 
  return result;
}

int main(){
  int  ar[3] = {17,28,21};
  std::cout << * (maximum<int*,int>(ar,ar+3)) << std::endl;

  std::string  sr[3] = {"shakespeare","calderon","moliere"};
  std::cout << *(maximum<std::string*,std::string>(sr,sr+3)) 
            << std::endl;
}

Eine Schwäche der Modellierung in der STL läßt sich erkennen: der Elementtyp spielt für einen Iterator keine Rolle. Es wird nicht ausgedrückt, daß die Elemente auf die ein Iterator zeigt von einen bestimmten Typ sein sollen. Dieses wird erst bei der Anwendung der generischen Funktion auf bestimmte Werte Typen geprüft.

4.2.4  Behälterklassen

Die STL stellt primär Behälterklasssen zur Verfügung. Hierzu gehören sowohl listenartige Sequenzen, als auch Mengen und Abbildungen und schließlich auch Strings.

Sequenzen

Sequenzen sind Behältertypen, die die klassischen Listenfunktionalität zur Verfügung stellen:
Als Implementierung für Sequenzen stehen in der STL die folgenden fünf Klassen zur Verfügung:
Beispiel:
Folgendes kleine Programm zeigt den einfachen Umgang mit Sequenzklassen. Man beachte, daß die eigentlichen Typen der Iteratoren oft unbekannt sind. Wir können über diese abstrahieren, indem wir eine generische Funktion für die Iteration schreiben, die wir ohne konkrete Angabe der Typparameter aufrufen.
VectorTest
#include <vector>
#include <list>
#include <string>
#include <iostream>

template <typename Iterator>
void doPrint(Iterator begin,Iterator end){
  std::cout << "[";
  for (Iterator it = begin;it!=end;){
    std::cout << *it;  
    it++;    
    if (it != end) std::cout << ",";
  }
  std::cout << "]"<<std::endl;
}

int main (){
  std::string words [] ={ "the","world","is","my","oyster"};
  std::vector<std::string> sv;
  std::list<std::string> sl;
  std::cout << sv.capacity() << std::endl;

  for (int i=0;i<5;i++){
    sv.insert(sv.end(),words[i]);
    sl.insert(sl.end(),words[i]);
  }
  std::cout << sv.capacity() << std::endl;

  doPrint(sv.begin(),sv.end());
  sl.reverse();
  doPrint(sl.begin(),sl.end());
}

Mengen und Abbildungen

Die zweite große Gruppe von Behäterklassen in der STL bilden die Abbildungen und Mengen, wobei eine Abbildung als eine Menge von Paaren dargestellt wird. Hierzu steht in ähnlicher Weise, wie wir es schon mehrfach implementiert haben die generische Klasse pair zur Verfügung.
Es stehen jeweils vier Klassen für Mengen und Abbildungen zur Verfügung. Für jede dieser Klassen gibt es jeweils zwei Versionen: einmal werden die Elemente in sortierter Reihenfolge gespeichert, einmal über einen Hashcode. Ersteres ist sinnvoll, wenn die Elemente oft sortiert benötigt werden, letzteres, wenn eine schnelle Suche erforderlich ist. Die vier Klassen sind:
Entsprechend gibt es die gleichen Klassen mit dem Haschprinzip unter den Namen: hash_set, hash_map, hash_multiset und hash_multimap.
Beispiel:
Folgendes Programm zeigt einen rudimentären Umgang mit Abbildungen. Eine Abbildung von Namen nach Adressen wird definiert. Interessant ist die Überladung des Operators [] für Abbildungen.
MapTest
#include <map>
#include <string>
#include <iostream>

class Address {

  public:
    std::string strasse;
    int hausnummer;
    Address(){}
    Address(std::string strasse,int hasunummer)
        :strasse(strasse),hausnummer(hausnummer){}
};
      

int main(){
  std::map<std::string, Address,std::less<std::string> > 
    addressBook;
  addressBook.insert(
   std::pair<std::string,Address>
    ("Hoffmann",Address("Gendarmenmarkt",42)));
  addressBook.insert(
   std::pair<std::string,Address>
    ("Schmidt",Address("Bertholt-Brecht-Platz",1)));

  std::cout << addressBook["Hoffmann"].strasse << std::endl;

  addressBook["Tierse"] = Address("Pariser Platz",1);

  std::cout << addressBook["Tierse"].strasse << std::endl;
  std::cout << addressBook["Andrak"].strasse << std::endl;
}

Man beachte, daß die Klasse map über drei Typen parameterisiert ist:

Arbeiten mit String

Auch die Klasse string enthält die wesentlichen Eigenschaften einer Behälterklasse. Ein String kann als eine Art Vektor mit Zeichen verstanden werden, also ähnlich dem Typ vector<char>. Entsprechend gibt es für einen String auch die Iteratoren, die Anfang und Ende eines Strings bezeichnen. Da die meisten Algorithmen auf Iteratoren definiert sind, lassen sich diese auch für Strings aufrufen.
Beispiel:
Folgendes kleine Programm demonstriert einen sehr rudimentären Umgang mit Strings.
Pallindrom
#include <string>
#include <iostream>

using namespace std;

template <typename t>
bool isPallindrome(t w){
  t w2 = w;
  reverse(w2.begin(),w2.end());
  return w==w2;
}

int main(){
  cout << isPallindrome<string>("rentner")  << endl;
  cout << isPallindrome<string>("pensioner")<< endl;
  cout << isPallindrome<string>("koblbok")  << endl;
  cout << isPallindrome<string>("alexander")<< endl;
  cout << isPallindrome<string>("stets")    << endl;
  cout << isPallindrome<string>
                   ("ein neger mit gazellezag tim regen nie")
            << endl;
}

4.2.5  Algorithmen

Wir haben bereits gesehen, daß die STL nicht durchgängig objektorientiert entworfen wurde. Nur Basisfunktionalität ist in den einzelnen Behäterklassen implementiert. Darüberhinaus stehen Funktionen zur Verfügung, die verschiedenste Algorithmen auf Behälterklassen realisieren. Die Algorithmen bekommen jeweils den Start- und Enditerator des Abschnitts eines Behäterobjekts als Parameter übergeben.
Ein paar Beispiele dieser Algorithmen haben wir bereits kennengelernt, wie z.B. reverse, der die Reihenfolge in einer Sequenz umdreht. Eine weitere Funktion auf Behäterklassen war die Funktion for_each. Diese Funktion ist eine Funktion höherer Ordnung, indem sie nämlich eine weitere Funktion als Argument hat, die auf jedes Element angewendet werden soll.
Die STL stellt in ihrem Entwurf eine interessante Symbiose von Konzepten der objektorientierten Programmierung und Konzepten der funktionalen Programmierung dar. Viele Algorithmen für Behäterobjekte sind wie for_each höherer Ordnung, indem Sie eine Funktion als Parameter übergeben bekommen.

Funktionsobjekte

Da viele Algorithmen der STL höherer Ordnung sind, werden Funktionsobjekte oft benötigt. In der STL sind einige einfache oft benötigte Funktionsobjekte bereits vordefiniert. So gibt es z.B. für die arithmetischen Operatoren Klassen, die Funktionsobjekte der entsprechenden Operation darstellen. Die entsprechenden Klassen können mit der Unterbibliothek functional inkludiert werden.
Wir treffen in der STL einige Bekannte aus den letzten Abschnitten. So gibt es das Funktionsobjekt compose1 (allerdings bisher nur in der Erweiterung der STL von silicon graphics (www.sgi.com) ), das die Komposition zweier Funktionen darstellt. Auch kann über eine Funktion bind1st currying praktiziert werden.
Beispiel:
Folgendes Programm demonstriert Currying in der STL mit der Funktion bind1st.
STLCurry
#include <functional>
#include <iostream>

int main(){
  std::cout <<bind1st(std::plus<int>(),17)(25) <<std::endl;
}

Beispiel:
Folgendes Programm demonstriert die Benutzung der Klasse plus, die ein Funktionsobjekt für die Addition darstellt in Zusammenhang mit einer Funktion höherer Ordnung. Mit der STL-Funktion transform werden die Elemente zweier Reihungen paarweise addiert.
TransformTest
#include <algorithm>
#include <functional>
#include <iostream>

using namespace std;

void print (int a){cout << a <<",";}

int main(){
  int xs [] = {1,2,3,4,5};
  int ys [] = {6,7,8,9,10};
  int zs [5];
  transform(xs,xs+5,ys,zs,plus<int>());
  for_each(zs,zs+5,print);
  cout << endl;
}

Beispiel:
Selbstredend bietet die STL auch Funktionen zum Sortieren von Behälterobjekten an. Die Sortiereigenschaft kann dabei als Funktionsobjekt übergeben werden. Im folgenden Programm wird nach verschiedenen Eigenschaften sortiert.
SortTest
#include <vector>
#include <list>
#include <string>
#include <iostream>

Zunächst definieren wir eine Klasse, für die kleiner Relation, die sich nur auf die Länge von Strings bezieht.
SortTest
class ShorterString{
  public:
    bool operator()(std::string s1,std::string s2){
      return s1.length()<s2.length();
    }
};

Die nächste Kleinerrelation vergleicht zwei Strings rückwärts gelesen in der lexikographischen Ordnung:
SortTest
bool reverseOrder(std::string s1,std::string s2){
  reverse(s1.begin(),s1.end());
  reverse(s2.begin(),s2.end());
  return s1<s2;
}

Mit folgenden Hilfsmethoden werden wir die Behälterobjekte auf dem Bildschirm ausgeben:
SortTest
void print(std::string p){
  std::cout << p << ","; 
}

template <typename Iterator>
void printSequence(Iterator begin,Iterator end){
  if (begin==end) {
    std::cout << "[]" << std::endl;return;
  }
  std::cout << "["; 
  for_each(begin,end-1,print);
  std::cout << *(end-1) << "]"<<std::endl; 
}

Eine Reihung von Wörtern wird sortiert. Erst nach der Länge der Wörter, dann nach den rückwärts gelesenen Wörtern (sich reimende Wörter stehen so direkt untereinander) und schließlich nach der Standardordnung, der lexikographischen Ordnung:
SortTest
int main (){
  int l = 8;
  std::string words []
   ={"rasiert","schweinelende","passiert","legende"
    ,"schwestern","verlustiert","gestern","stern"};

  std::sort(words,words+l,ShorterString());
  printSequence(words,words+l);

  std::sort(words,words+l,reverseOrder);
  printSequence(words,words+l);

  std::sort(words,words+l);
  printSequence(words,words+l);
}

Und tatsächlich hat das Programm die erwartete Ausgabe:
sep@linux:~/fh/prog3/examples/src> g++ -o SortTest SortTest.cpp
sep@linux:~/fh/prog3/examples/src> ./SortTest
[stern,rasiert,legende,gestern,passiert,schwestern,verlustiert,schweinelende]
[legende,schweinelende,stern,gestern,schwestern,rasiert,passiert,verlustiert]
[gestern,legende,passiert,rasiert,schweinelende,schwestern,stern,verlustiert]
sep@linux:~/fh/prog3/examples/src>

Aufgabe 16   (2 Punkte) Sie haben bereits zweimal eine Funktion fold implementiert. Einmal auf Reihungen und einmal auf unseren selbstgeschriebenen einfach verketteten Listen. Jetzt sollen Sie die Funktion noch einmal allgemeiner schreiben.

4.3  Ausnahmen

Das Konzept der Ausnahmen, die geworfen und gefangen werden können, kennt C++ in ähnlicher Weise, wie wir es in Java kennengelernt haben. Es gibt den throw-Befehl und die try und catch Blöcke. Hauptunterschiede sind:
Beispiel:
ExceptionTest
#include <string>
#include <iostream>

int fac(int x){
  if (x<0) {
    std::string msg = "negative Zahl in Fakultaet";
    throw msg;
  } 
  if (x==0) return 1;
  return x*fac(x-1);
}


int main(){
  try{
    std::cout << fac(5) << std::endl;
    std::cout << fac(-5) << std::endl;
  }catch (std::string s){
    std::cout << s << std::endl;
  }
}

4.4  Namensräume

Die Namensräume in C++ entsprechen vom Konzept her den Paketen in Java. Unterschiede sind, daß nicht wie in Java sich die Paketstruktur in der Ordnerstruktur auf dem Dateisystem wiederspiegelt und daß es kein Sichtbarkeitsattribut, das sich auf Pakete bezieht, gibt.
Namensräume können hierarchisch sein, wie es in Java Unterpakete gibt, so gibt es also auch Unternamensräume.
Eigene Namensräume entsprechend der package-Klausel in Java werden in C++ über das Schlüsselwort namespace vereinbart. Es folgt in geschweiften Klammern ein Block, dessen Definitionen in dem gerade definierten Namensraum liegen.
Die Qualifizierung eines Bezeichners über seinen Namensraum wird in C++ mit einem zweifachen Doppelpunkt vorgenommen (in Java war dies einfach durch einen Punkt.).
Zur unqualifizierten Benutzung von Bezeichnern aus einem Namensraum kann die Klausel using namespace benutzt werden. Dieses entspricht dem import in Java. Die using namespace Deklaration gilt ähnlich wie Variablendeklarationen nur in dem Block, in dem sie getätigt wird, also nicht außerhalb des Blockes.
Beispiel:
Im folgenden Programm können einige der Konzepte der Namensräume in C++ nachvollzogen werden:
Zunächst definieren wir drei Namensräume, in denen sich jeweils eine Funktion drucke befindet. Einer der Namensräume ist ein Unternamensraum.
NamespaceTest
#include <iostream>

using namespace std;

namespace paket1 {
  void drucke() { cout << "paket1" << endl;
  }
}

namespace paket2 {
  void drucke() { cout << "paket2" << endl;
  }
  namespace paket3{
    void drucke() { cout << "paket3" << endl;
    }
  }
}

In der Hauptmethode gibt es Tests mit drei using Blöcken. Entsprechend bezieht sich der unqualifizierte Name stets auf eine andere Funktion drucke.
NamespaceTest
int main(){
  {
    using namespace paket2::paket3;
    paket2::drucke();    
    drucke();    
    paket1::drucke();
  }    
  {
    using namespace paket2;
    drucke();    
    paket3::drucke();    
    paket1::drucke();    
  }
  {
    using namespace paket1;
    paket2::drucke();    
    paket2::paket3::drucke();    
    drucke();
  }    
}

Chapter 5
Graphik und GUI Programmierung

5.1  3 dimensionale Graphiken mit OpenGL

Die umfangreichste Standardwerk zu OpenGL ist das sogenannte redbook[WBN+97]. OpenGL kommt mit zwei zusätzlichen Bibliotheken: Eine Bibliothek nützlicher graphischer Objekte mit Namen GLU [CFH+98] und eine systemunabhängige GUI Bibliothek mit Namen GLUT [Kil96]. Ein kleines Tutorial für die Portierung von OpenGL für Haskell habe ich aufs Netz gestellt [Pan03a].

5.1.1  Eine Zustandsmaschine

5.1.2  Eine kleine GUI Bibliothek: GLUT

In der Bibliothek OpenGL existieren keine Funktionen zum Programmieren graphischer Benutzeroberflächen. Um aber eine minimale Benutzeroberfläche schreiben zu können, existiert die Bibliothek GLUT. Sie ist für kleine bis maximal mittelgroße Programme geeignet.
Ein GLUT-Programm hat im Wesentlichen immer die gleiche Struktur. Das GLUT System wird initialisiert, ein Basismodus für dieses System wird initialisiert, ein Fenster deklariert, diesem Fenster eine OpenGL Zechenfunktion übergeben und schließlich das Gesamtsystem gestartet.
In der Zeichenfunktion finden sich beliebig viele OpenGL-Befehle und sie wird in der Regel mit dem Befehl flush() bzw. bei doppel gepufferten Modus mit glutSwapBuffers() beendet.
Beispiel:
In diesem kleinen Beispiel wird ein Fenster geöffnet, dessen Zeichenfunktion lediglich den Fensterhintergrund mit einer definierten Löschfarbe löscht.
HelloWindow
#include <GL/glut.h>

void display(void){
  glClearColor(0.0, 0.0, 0.0, 0.0);
  glClear(GL_COLOR_BUFFER_BIT);
  glFlush();
}

int main (int argc, char **argv){
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_RGB);
  glutCreateWindow("Hello Window");
  glutDisplayFunc(display);
  glutMainLoop();
}

Glut bietet über das Öffnen von Fenster eine ganze Reihe weiterer Bibliotheksfunktionen an. Insbesondere auch die Möglichkeit auf Tastatureingaben zu reagieren.

5.1.3  Vertexes und Figuren

Die Basiseinheit in OpenGL sind Punkte in einem 3-dimensionalen Raum, sogenannte Vertexes. Ein Vertex kann über den Funktionsaufruf glVertex3f definiert werden. In diesem Namen zeigt der Präfix gl an, daß es sich um eine OpenGL-Funktion handelt, die 3, daß es drei Koordinaten gibt und daß f, daß diese Koordinaten als float Zahl übergeben werden. Die einzelnen Vertexes können dann zu unterschiedlichen vorgegebenen Figuren verbunden werden.
Wir definieren eine kleine Bibliothek, die es uns ermöglicht ein paar vorgegebene Vertexes als unterschiedliche Figuren in einen Fenster zu zeichenen. Hierzu definieren wir eine Funktion moin, die als zusätzlichen Parameter eine Figur enthält12.
SomePoints
#include <GL/glut.h>
void moin(int argc, char **argv,int shape);

In der Implementierungsdatei sehen wir eine Reihe von Vertexes vor. In einer Variablen wird gespeichert, zu was für eine Figur diese Punkte verbunden werden sollen:
SomePoints
#include "SomePoints.h"
int shape=GL_POINTS;

void points(){
   glVertex3f(0.2,-0.4,0);
   glVertex3f(0.46,-0.26,0);
   glVertex3f(0.6,0,0);
   glVertex3f(0.6,0.2,0);
   glVertex3f(0.46,0.46,0);
   glVertex3f(0.2,0.6,0);
   glVertex3f(0.0,0.6,0);
   glVertex3f(-0.26,0.46,0);
   glVertex3f(-0.4,0.2,0);
   glVertex3f(-0.4,0,0);
   glVertex3f(-0.26,-0.26,0);
   glVertex3f(0,-0.4,0);
}

Die eigentliche Zeichenfunktion kappselt diese Punkte mit den Befehlen glBegin und glEnd. Der Funktion glBegin wird dabei als Parameter mitgegeben, zu was für einer Figur die Punkte verbunden werden sollen.
SomePoints
void pointsAsShape(){
  glClear(GL_COLOR_BUFFER_BIT );
  glColor3f (0.0, 0.0, 0.0); 
  glBegin(shape); 
    points();
  glEnd();
  glFlush();
}

Und schließlich noch die Pseudohauptmethode, die noch zusätzlich die zu erzeugende Figur als Parameter enthält
SomePoints
void moin(int argc, char **argv,int s){
  shape=s;
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_RGB);
  glutCreateWindow("Hello");
  glClearColor(1.0, 1.0, 1.0, 1.0);
  glutDisplayFunc(pointsAsShape);
  glutMainLoop();
}

Diese Bibliothek können wir benutzen, um die verschiedenen in OpenGL bekannten Figuren zu zeichnen. OpenGL kennt 10 Arten von Figuren, die aus Vertexes gebildet werden können:
Mit Hilfe unserer Beispielpunkte aus SomePoints lassen sich als einfache Einzeiler Beispiele für alle 10 Figuren schreiben:
HelloPoints
#include "SomePoints.h"
int main (int argc, char **argv){moin(argc,argv,GL_POINTS);}

HelloLines
#include "SomePoints.h"
int main (int argc, char **argv){moin(argc,argv,GL_LINES);}

HelloLineStrip
#include "SomePoints.h"
int main (int argc, char **argv){moin(argc,argv,GL_LINE_STRIP);}

HelloLineLoop
#include "SomePoints.h"
int main (int argc, char **argv){moin(argc,argv,GL_LINE_LOOP);}

HelloTriangles
#include "SomePoints.h"
int main (int argc, char **argv){moin(argc,argv,GL_TRIANGLES);}

HelloTriangleStrip
#include "SomePoints.h"
int main (int argc, char **as){moin(argc,as,GL_TRIANGLE_STRIP);}

HelloTriangleFan
#include "SomePoints.h"
int main (int argc, char **argv){moin(argc,argv,GL_TRIANGLE_FAN);}

HelloQuads
#include "SomePoints.h"
int main (int argc, char **argv){moin(argc,argv,GL_QUADS);}

HelloQuadStrip
#include "SomePoints.h"
int main (int argc, char **argv){moin(argc,argv,GL_QUAD_STRIP);}

HelloPolygon
#include "SomePoints.h"
int main (int argc, char **argv){moin(argc,argv,GL_POLYGON);}

Die durch diese Programme erzeugten Fenster sind in Abbildung zu bewundern.
images/HelloPoints.epsf.gif images/HelloLines.epsf.gif images/HelloLineStrip.epsf.gif images/HelloLineLoop.epsf.gif images/HelloTriangles.epsf.gif images/HelloTriangleStrip.epsf.gif images/HelloTriangleFan.epsf.gif images/HelloQuads.epsf.gif images/HelloQuadStrip.epsf.gif images/HelloPolygon.epsf.gif
Figure 5.1: Alle 10 möglichen Figuren.
In OpenGL gibt es keine Figuren mit runden Kanten oder Flächen. Diese müssen über Approximation von vielen kleinen geraden Flächen gebildet werden.
Beispiel:
Das folgende Programm zeichnet einen Funktionsgraphen:
Graph
#include <GL/glut.h>

float (*f) (float);
float square(float x) {return 3*x*x*x;};

void display(void){
  glClearColor(0.0, 0.0, 0.0, 0.0);
  glClear(GL_COLOR_BUFFER_BIT);

  const int steps =100;
  const float step = 2/(float)100;
  glBegin(GL_LINE_STRIP);
  for (int i = 0; i < steps; i=i+1){
    const float x = -1+i*step;
    const float y = f(x);
    glVertex2f(x,y);
  }
  glEnd();
  glFlush();
}

int main (int argc, char **argv){
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_RGB);
  glutCreateWindow("Function Graph Window");
  f = square;
  glutDisplayFunc(display);
  glutMainLoop();
}

images/Graph.epsf.gif
Figure 5.2: Der Graph einer Funktion.
Aufgabe 17   (1 Punkt) Schreiben Sie eine OpenGL-Funktion
void circle(int radius),
mit der Sie einen Kreis zeichnen können. Benutzen Sie hierzu die Figur GL_POLYGON.

5.1.4  Nah und Fern

5.1.5  Vordefinierte Figuren

Im letzen Abschnitt war zu sehen, daß OpenGL nur 10 primitive Arten kennt, wie Figuren gebaut werden können. Kompliziere Figuren, insbesondere runde Flächen sind approximativ aus diesen Grundfiguren zusammenzusetzen. In der Bibliothek GLUT sind eine Reihe von komplexeren Figuren bereits vordefiniert.
Alle diese dreidimesionalen Figuren gibt es einmal als solide geschlossene Form und einmal durch ein Netz dargestellt. Entsprechend gibt es jeweils zwei Funktionen, z.B.  glutWireCube(GLdouble size) und glutSolidCube(GLdouble size) für Würfel.
Beispiel:
In diesem Beispiel wird eine Kugel definiert.
HelloSphere
#include <GL/glut.h>

void display(void){
  glClear(GL_COLOR_BUFFER_BIT );
  glColor3f (1.0, 0.0, 0.0); 
  glutSolidSphere(0.8,10,10);
  glFlush();
}

int main (int argc, char **argv){
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_RGB);
  glutCreateWindow("Hello Cube");
  glClearColor(1.0, 1.0, 1.0, 1.0);
  glutDisplayFunc(display);
  glutMainLoop();
}

Das Ergebnis einer solchen dreidimensionalen Figur ist auf dem Bildschirm zunächst recht enttäuschend. Man sieht nur einen Kreis.

5.1.6  Bei Licht betrachtet

Um für dreidimensionale Figuren auch einen dreidimensionalen optischen Effekt zu erzeugen, braucht es Licht und Schatten auf einem Körper. OpenGL bietet die Möglichkeit, Lichtquellem zu definieren und für die Flächen einer Figur zu definieren, wie ihr Material auf unterschiedlichen Lichteinfluß reagieren.
Das Lichtmodell von OpenGL ist relativ komplex. Es gibt verschiedene Arten von Lichtquellen (ambient, diffuse, specular), die unterschiedlich stark gesetzt werden können. In diesem Kurzkapitel begnügen wir uns mit dem Beispiel der GLUT-Körper bei Licht betrachtet.
Um für alle 18 vorgefertigten Figuren aus GLUT ein Beispielprogramm zu bekommen, schreiben wir wieder einer einfache Bibliothek, in der eine Lichtquelle definiert wird:
RenderShape
#include <GL/glut.h>
int moin (int argc, char **argv,void(*display)(),char* title);

RenderShape
#include "RenderShape.h"

void (*shapeFun)();

void display(){
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  shapeFun();
  glFlush();
} 

int moin (int argc, char **argv,void(*shape)(),char* title){
  shapeFun=shape;
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_RGB|GLUT_DEPTH);

  glutCreateWindow(title);

  GLfloat mat_specular[] = { 1.0, 1.0, 1.0, 1.0 }; 
  GLfloat mat_shininess[] = { 50.0 }; 
  GLfloat light_position[] = { 1.0, 1.0, -2.5, 0.0 }; 

  glShadeModel (GL_SMOOTH); 

  glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular); 
  glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess); 
  glLightfv(GL_LIGHT0, GL_POSITION, light_position); 


  glEnable(GL_LIGHTING); 
  glEnable(GL_LIGHT0); 
  glEnable(GL_DEPTH_TEST);

  glClearColor(1.0, 0,0,0);
  glutDisplayFunc(display);
  glutMainLoop();
}

Und jetzt lassen sich auf einfache Weise Tests für alle Figuren erzeugen:
WireSphere
#include "RenderShape.h"
void f(){glutWireSphere(1,10,10); }
int main (int argc,char **argv){moin(argc,argv,f,"WireSphere");}

SolidSphere
#include "RenderShape.h"
void f(){glutSolidSphere(1,10,10); }
int main (int argc,char **argv){moin(argc,argv,f,"SolidSphere");}

WireCube
#include "RenderShape.h"
void f(){glutWireCube(1); }
int main (int argc,char **argv){moin(argc,argv,f,"WireCube");}

SolidCube
#include "RenderShape.h"
void f(){glutSolidCube(1); }
int main (int argc,char **argv){moin(argc,argv,f,"SolidCube");}

WireTorus
#include "RenderShape.h"
void f(){glutWireTorus(0.4,0.6,10,30); }
int main (int argc,char **argv){moin(argc,argv,f,"WireTorus");}

SolidTorus
#include "RenderShape.h"
void f(){glutSolidTorus(0.4,0.6,10,30); }
int main (int argc,char **argv){moin(argc,argv,f,"SolidTorus");}

WireIcosahedron
#include "RenderShape.h"
void f(){glutWireIcosahedron(); }
int main (int ac,char **av){moin(ac,av,f,"WireIcosahedron");}

SolidIcosahedron
#include "RenderShape.h"
void f(){glutSolidIcosahedron(); }
int main (int ac,char **av){moin(ac,av,f,"SolidIcosahedron");}

WireOctahedron
#include "RenderShape.h"
void f(){glutWireOctahedron(); }
int main (int ac,char **av){moin(ac,av,f,"WireOctahedron");}

SolidOctahedron
#include "RenderShape.h"
void f(){glutSolidOctahedron(); }
int main (int ac,char **av){moin(ac,av,f,"SolidOctahedron");}

WireTetrahedron
#include "RenderShape.h"
void f(){glutWireTetrahedron(); }
int main (int ac,char **av){moin(ac,av,f,"WireTetrahedron");}

SolidTetrahedron
#include "RenderShape.h"
void f(){glutSolidTetrahedron(); }
int main (int ac, char **av){moin(ac,av,f,"SolidTetrahedron");}

WireDodecahedron
#include "RenderShape.h"
void f(){glutWireDodecahedron(); }
int main (int ac,char **av){moin(ac,av,f,"WireDodecahedron");}

SolidDodecahedron
#include "RenderShape.h"
void f(){glutSolidDodecahedron(); }
int main (int ac,char **av){moin(ac,av,f,"SolidDodecahedron");}

WireCone
#include "RenderShape.h"
void f(){glutWireCone(0.3,0.8,20,25); }
int main (int ac,char **av){moin(ac,av,f,"WireCone");}

SolidCone
#include "RenderShape.h"
void f(){glutSolidCone(0.3,0.8,20,25); }
int main (int ac,char **av){moin(ac,av,f,"SolidCone");}

WireTeapot
#include "RenderShape.h"
void f(){glutWireTeapot(0.6); }
int main (int ac,char **av){moin(ac,av,f,"WireTeapot");}

SolidTeapot
#include "RenderShape.h"
void f(){glutSolidTeapot(0.6);}
int main (int ac,char **av){moin(ac,av,f,"SolidTeapot");}

Die Ergebnisse dieser Programme befinden sich in den Abbildungen 5.3 bzw. 5.4.
images/SolidSphere.epsf.gif images/SolidCube.epsf.gif images/SolidTorus.epsf.gif images/SolidIcosahedron.epsf.gif images/SolidOctahedron.epsf.gif images/SolidTetrahedron.epsf.gif images/SolidDodecahedron.epsf.gif images/SolidCone.epsf.gif images/SolidTeapot.epsf.gif
Figure 5.3: Alle 9 Glut Figuren mit Oberfläche.
images/WireSphere.epsf.gif images/WireCube.epsf.gif images/WireTorus.epsf.gif images/WireIcosahedron.epsf.gif images/WireOctahedron.epsf.gif images/WireTetrahedron.epsf.gif images/WireDodecahedron.epsf.gif images/WireCone.epsf.gif images/WireTeapot.epsf.gif
Figure 5.4: Alle 9 Glut Figuren als Netzwerk.

5.1.7  Transformationen

5.1.8  GLUT Tastatureingabe

Beispiel:
CubeWorld
#include <GL/glut.h>

void display(void);
void keyboard(unsigned char, int, int);
void resize(int, int);
void drawcube(int, int, void (*color) ());

int main (int argc, char **argv){
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
  glutInitWindowSize(600, 600);
  glutInitWindowPosition(40, 40);
  glutCreateWindow("The Cube World");
  glClearColor(0.0, 0.0, 0.0, 0.0);
  glEnable(GL_DEPTH_TEST);
  glutDisplayFunc(display);
  glutKeyboardFunc(keyboard);
  glutReshapeFunc(resize);  
  glutMainLoop();
}

void red()  {glColor3f(1.0,0.0,0.0);}
void blue() {glColor3f(0.0,1.0,0.0);}
void green(){glColor3f(0.0,0.0,1.0);}
void grey() {glColor3f(0.8, 0.8, 0.8);}

void drawcube(int x_offset, int z_offset, void (*color)()){
  color();
  glPushMatrix();
    glTranslatef(x_offset,0.0,z_offset);
    glutSolidCube(10);
  glPopMatrix();
}

void display(void){
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  /* draw the floor */
  glPushMatrix();
    glTranslatef(0,-5,20);
    glScalef(30,0.1,30);
    drawcube(0, 0, grey);
  glPopMatrix();

  /* draw 12 cubes with different colors */
  drawcube(75, 57, red);
  drawcube(-65, -12, green);
  drawcube(50, -50, red);
  drawcube(-56, 17, blue);
  drawcube(67, 12, green);
  drawcube(-87, 32, red);
  drawcube(-26, 75, blue);
  drawcube(57, 82, green);
  drawcube(-3, 12, red);
  drawcube(46, 35, blue);
  drawcube(37, -2, green);

  glutSwapBuffers();
}

void keyboard(unsigned char key, int _, int __){
  switch (key){
    case 'a': case 'A': glTranslatef(5.0, 0.0, 0.0); break;
    case 'd': case 'D': glTranslatef(-5.0, 0.0, 0.0);break;
    case 'w': case 'W': glTranslatef(0.0, 0.0, 5.0); break;
    case 's': case 'S': glTranslatef(0.0, 0.0, -5.0);break;
  }
  display();
}

void resize(int width, int height){
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(45.0, width / height, 1.0, 400.0);
  glTranslatef(0.0, -5.0, -150.0);
  glMatrixMode(GL_MODELVIEW);
}

Die Klötzchenwelt ist in Abbildung 5.5 zu bewundern.
images/CubeWorld.epsf.gif
Figure 5.5: Dreidimensionale Klötzchenwelt.

5.1.9  Beispiel: Tux der Pinguin

In einem abschließenden Beispiel benutzen wir alle oben vorgestellten OpenGL Konstrukte, um das Linuxmaskottchen Tux zu zeichnen.
Tux
#include <GL/glut.h>
#include <math.h>

float zPos = -1;

void display();
void keyboard(unsigned char, int, int);
void resize(int, int);

int main (int argc, char **argv){
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
  glutCreateWindow("Tux");
  glClearColor(1.0, 0.0, 0.0, 0.0);

  glEnable(GL_LIGHTING); 
  glEnable(GL_NORMALIZE);
  glEnable(GL_DEPTH_TEST);

  GLfloat lights[] = { 1, 1, 1, 1 }; 
  GLfloat light_position[] = { 1, 0.4, 0.8, 1 }; 
  glLightfv(GL_LIGHT0, GL_POSITION, light_position); 

  glLightfv(GL_LIGHT0, GL_AMBIENT, lights); 
  glLightfv(GL_LIGHT0, GL_DIFFUSE, lights); 
  glLightfv(GL_LIGHT0, GL_SPECULAR, lights);
  glEnable(GL_LIGHT0); 

  glutDisplayFunc(display);
  glutKeyboardFunc(keyboard);
  glutReshapeFunc(resize);  
  glutMainLoop();
}

void keyboard(unsigned char key, int _, int __){
  switch (key){
    case '+': zPos=zPos+0.1;break;
    case '-': zPos=zPos-0.1;break;
  }
  display();
}

void resize(int w, int h){
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  float near   = 0.001;
  float    far    = 40;
  float    fov    = 90;
  float    ang    = (fov*3.14)/(360);
  float    top    = near / ( cos(ang) / sin(ang) );
  float    aspect = w/h;
  float    right = top*aspect;
  glFrustum(-right, right, -top, top, near, far);
  glMatrixMode(GL_MODELVIEW);
}

void sphere(float r, float xs, float ys,float  zs){
  glScalef(xs, ys, zs);
  glutSolidSphere(r,100,100);
}

void rotateZ(float a){glRotatef(a,0,0,1);}
void rotateY(float a){glRotatef(a,0,1,0);}
void rotateX(float a){glRotatef(a,1,0,0);}

void crMat(float rd,float gd,float bd
          ,float rs,float gs,float bs,float exp){
  float diffuse [] ={rd, gd, bd,  1.0};
  glMaterialfv(GL_FRONT,GL_DIFFUSE, diffuse);
  glMaterialfv(GL_FRONT,GL_AMBIENT, diffuse);
  float specular [] ={rs, gs, bs,  1.0};
  glMaterialfv(GL_FRONT,GL_SPECULAR, specular);
  float shine [] ={exp};
  glMaterialfv(GL_FRONT,GL_SHININESS,shine);
}

void whitePenguin() {
  crMat(0.58, 0.58, 0.58, 0.2, 0.2, 0.2, 50.0);
}
void blackPenguin() {crMat(0.1, 0.1, 0.1,0.5, 0.5, 0.5, 20.0);}
void beakColour()   {crMat(0.64, 0.54, 0.06,0.4, 0.4, 0.4, 5);}
void nostrilColour(){
  crMat(0.48039, 0.318627, 0.033725,0.0,0.0,0.0, 1);
}
void irisColour()   {crMat(0.01, 0.01, 0.01,0.4, 0.4, 0.4, 90.0);}

void makeBody(){
  glPushMatrix();
    blackPenguin();
    sphere(1, 0.95, 1.0, 0.8);
  glPopMatrix();
  glPushMatrix();
    whitePenguin();
    glTranslatef( 0, 0, 0.17);
    sphere(1, 0.8, 0.9, 0.7);
  glPopMatrix();
}

void createTorso(){
  glPushMatrix();
    glScalef( 0.9, 0.9, 0.9);
    makeBody();
  glPopMatrix();
}


void leftHand(){
  glPushMatrix();
    glTranslatef( -0.24, 0, 0);
    rotateZ(20);
    rotateX(90);
    blackPenguin();
    sphere( 0.5, 0.12, 0.05, 0.12);
  glPopMatrix();
}


void leftForeArm(){
  glPushMatrix();
    glTranslatef(-0.23, 0, 0);
    rotateZ( 20);
    rotateX( 90);
    leftHand();
    blackPenguin();
    sphere( 0.66, 0.3, 0.07, 0.15);
  glPopMatrix();
}

void rightForeArm(){leftForeArm();}

void leftArm(){
  glPushMatrix();
    rotateY(180);
    glTranslatef( -0.56, 0.3, 0);
    rotateZ( 45);
    rotateX( 90);
    leftForeArm();
    blackPenguin();
    sphere( 0.66, 0.34, 0.1, 0.2);
  glPopMatrix();
}

void rightArm(){
  glPushMatrix();
    glTranslatef(-0.56, 0.3, 0);
    rotateZ( 45);
    rotateX(-90);
    rightForeArm();
    blackPenguin();
    sphere( 0.66, 0.34, 0.1, 0.2);
  glPopMatrix();
}

void createShoulders(){
  glPushMatrix();
    glTranslatef( 0, 0.4, 0.05);
    leftArm();
    rightArm();
    glScalef( 0.72, 0.72, 0.72);
    makeBody();
  glPopMatrix();
}


void createBeak(){
  glPushMatrix();
    glTranslatef( 0,-0.205, 0.3);
    rotateX(10);
    beakColour();
    sphere( 0.8, 0.23, 0.12, 0.4);
  glPopMatrix();
  glPushMatrix();
    beakColour();
    glTranslatef( 0, -0.23, 0.3);
    rotateX( 10);
    sphere( 0.66, 0.21, 0.17, 0.38);
  glPopMatrix();
}


void leftIris(){
  glPushMatrix();
    glTranslatef( 0.12,-0.045, 0.4);
    rotateY( 18);
    rotateZ( 5);
    rotateX( 5);
    irisColour();
    sphere( 0.66, 0.055, 0.07, 0.03);
  glPopMatrix();
}


void rightIris(){
  glPushMatrix();
    glTranslatef( -0.12,-0.045, 0.4);
    rotateY( -18);
    rotateZ( -5);
    rotateX( 5);
    irisColour();
    sphere( 0.66, 0.055, 0.07, 0.03);
  glPopMatrix();
}

void leftEye(){
  glPushMatrix();
    glTranslatef( 0.13,-0.03, 0.38);
    rotateY( 18);
    rotateZ( 5);
    rotateX( 5);
    whitePenguin();
    sphere( 0.66, 0.1, 0.13, 0.03);
  glPopMatrix();
}

void rightEye(){
  glPushMatrix();
    glTranslatef( -0.13,-0.03, 0.38);
    rotateY( -18);
    rotateZ( -5);
    rotateX( 5);
    whitePenguin();
    sphere( 0.66, 0.1, 0.13, 0.03);
  glPopMatrix();
}

void createEyes(){
  leftEye();
  leftIris();
  rightEye();
  rightIris();
}

void createHead(){
  glPushMatrix();
    glTranslatef( 0, 0.3, 0.07);
    createBeak();
    createEyes();
    rotateY( 90);
    blackPenguin();
    sphere( 1, 0.42, 0.5, 0.42);
  glPopMatrix();
}

void createNeck(){
  glPushMatrix();
    glTranslatef( 0, 0.9, 0.07);
    createHead();
    rotateY(90);
    blackPenguin();
    sphere( 0.8, 0.45, 0.5, 0.45);
    glTranslatef( 0, -0.08, 0.35);
    whitePenguin();
    sphere( 0.66, 0.8, 0.9, 0.7);
  glPopMatrix();
}

void footBase(){
  glPushMatrix();
    sphere( 0.66, 0.25, 0.08, 0.18);
  glPopMatrix();
}


void toe1(){
  glPushMatrix();
    glTranslatef(-0.07, 0, 0.1);
    rotateY( 30);
    sphere(0.66, 0.27, 0.07, 0.11);
  glPopMatrix();
}

void toe2(){
  glPushMatrix();
    glTranslatef(-0.07, 0, -0.1);
    rotateY (-30);
    sphere( 0.66,  0.27, 0.07, 0.11);
  glPopMatrix();
}

void toe3(){
  glPushMatrix();
    glTranslatef(-0.08, 0, 0);
    sphere( 0.66,  0.27, 0.07, 0.10);
  glPopMatrix();
}

void foot(){
  glPushMatrix();
    glScalef(  1.1, 1.0, 1.3); 
    beakColour();
    footBase();
    toe1();
    toe2();
    toe3();
  glPopMatrix();
}

void rightFoot(){
  glPushMatrix();
    glTranslatef( 0,-0.09, 0);
    rotateY( 180);
    foot();
  glPopMatrix();
}

void leftFoot(){
  glPushMatrix();
    glTranslatef( 0,-0.09, 0);
    rotateY( 100);
    foot();
  glPopMatrix();
}


void leftCalf(){
  glPushMatrix();
    glTranslatef( 0,-0.21, 0);
    rotateY( 90);
    leftFoot();
    beakColour();
    sphere( 0.5, 0.06, 0.18, 0.06);
  glPopMatrix();
}

void rightCalf(){
  glPushMatrix();
    glTranslatef( 0,-0.21, 0);
    rightFoot();
    beakColour();
    sphere( 0.5, 0.06, 0.18, 0.06);
  glPopMatrix();
}

void hipBall(){
  glPushMatrix();
    blackPenguin();
    sphere( 0.5, 0.09, 0.18, 0.09);
  glPopMatrix();
}

void leftTigh(){
  glPushMatrix();
    rotateY( 180);
    glTranslatef(-0.28,-0.8, 0);
    rotateY( 110);
    hipBall();
    leftCalf();

    rotateY (-110);
    glTranslatef( 0,-0.1, 0);
    beakColour();
    sphere( 0.5, 0.07, 0.3, 0.07);
  glPopMatrix();
}

void rightTigh(){
  glPushMatrix();
    glTranslatef(-0.28,-0.8, 0);
    rotateY( -110);
    hipBall();
    rightCalf();

    glTranslatef( 0,-0.1, 0);
    beakColour();
    sphere( 0.5, 0.07, 0.3, 0.07);
  glPopMatrix();
}

void createTail(){
  glPushMatrix();
    glTranslatef( 0,-0.4,-0.5);
    rotateX (-60);
    glTranslatef( 0, 0.15, 0);
    blackPenguin();
    sphere( 0.5, 0.2, 0.3, 0.1);
  glPopMatrix();
}

void tux(){
  glPushMatrix();
    glScalef( 0.35, 0.35, 0.35);
    rotateY (-180);
    createTorso();
    createShoulders();
    createNeck();
    leftTigh();
    rightTigh();
    createTail();
  glPopMatrix();
}

void display(){
  glLoadIdentity();
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  gluLookAt(0,0,zPos,0,0,0,0,1,0);
  tux();
  glutSwapBuffers();
}

Der so gezeichnete kleine Kerl ist in Abbildung 5.6 zu sehen.
images/Tux.epsf.gif
Figure 5.6: Tux der Pinguin.

Appendix A
Gtk für C++ Beispielprogramme

A.1  Ein einfacher Zähler

Die wahrscheinlichst einfachste interaktive GUI-Anwendung stellt ein Zähler da. Es wird in einem Fenster in einemn Label eine Zahl dargestellt, die sich über einen Knopf erhöhen läßt. Wir sehen eine Klasse für diese GUI-Anwendung vor.

A.1.1  Die Kopfdatei

In der Kopfdatei werden die Eigenscvhaften der Klasse festgelegt. Sie erweitert die Fensterklasse aus GTK, enthält den aktuellen Zählerstand in einem int Feld und drei graphisches Unterkomponenten. In der Methode on_button_clicked() wird das Verhalten auf die Aktion eines Knopfdrucks spezifiziert.
Counter
#include <gtkmm/button.h>
#include <gtkmm/window.h>
#include <gtkmm/label.h>
#include <gtkmm/box.h>

class Counter : public Gtk::Window{

public:
  Counter();
  virtual ~Counter();

protected:
  virtual void on_button_clicked();

  Gtk::Button button;
  Gtk::Label  label;
  Gtk::HBox  box;
  int counter;
};

A.1.2  Implementierung

In der Implementierungsdatei werden die graphischen Elemente initialisiert. Über die Methode signal_clicked() kann der Knopfkomponente die entsprechende Funktion für die Knopfdruckaktion zugewiesen werden.
Counter
#include "Counter.h"
#include <iostream>
#include <sstream>
#include <string>

Counter::Counter()
:  button("Drück Mich")
  ,label("0"){
  counter=0;
  set_border_width(10);

  button
    .signal_clicked()
    .connect(SigC::slot
              (*this, &Counter::on_button_clicked));

  box.add(button);
  box.add(label);
  add(box);

  button.show();
  label.show();
  box.show();
}

Counter::~Counter(){}

void Counter::on_button_clicked(){
  std::stringstream ss;
  std::string zahl;
  counter=counter+1;
  ss << counter;
  ss >> zahl;
  label.set_text(zahl);
}

A.1.3  Beispielaufruf

Zur Benutzung dieser GUI-Komponente ist lediglich die GTK zu starten und das Objekt der Methode run von GTK übergeben werden.
testcounter
#include <gtkmm/main.h>
#include "Counter.h"

int main (int argc, char *args[]){
  Gtk::Main kit(argc,args);

  Counter counter;
  Gtk::Main::run(counter); 
}

images/Counter.epsf.gif
Figure A.1: Counter in Aktion.

A.2  Einfaches Zeichnen

Strichkreis
#include <gtkmm/drawingarea.h>
#include <gdkmm/colormap.h>
#include <gdkmm/window.h>
#include <gtkmm/box.h>
#include <gtkmm/window.h>
#include <gtkmm/main.h>

#include <cmath>

const double DEG2RAD = 3.1415928 / 180.0;

class StrichKreis : public Gtk::DrawingArea
{
public:
  StrichKreis(int r);
  ~StrichKreis();

protected:
  virtual void on_realize();
  virtual bool on_expose_event(GdkEventExpose* event);

  Glib::RefPtr<Gdk::GC> gc_;
  Gdk::Color black_, white_;
};

StrichKreis::StrichKreis(int r){
  Glib::RefPtr<Gdk::Colormap> colormap = get_default_colormap();
  black_ = Gdk::Color("black");
  white_ = Gdk::Color("white");
  
  colormap->alloc_color(black_);
  colormap->alloc_color(white_);
  set_size_request(r, r);
}

StrichKreis::~StrichKreis(){}

void StrichKreis::on_realize(){
  // We need to call the base on_realize()
  Gtk::DrawingArea::on_realize();

  // Now we can allocate any additional resources we need
  Glib::RefPtr<Gdk::Window> window = get_window();
  gc_ = Gdk::GC::create(window);
  window->set_background(black_);
}

bool StrichKreis::on_expose_event(GdkEventExpose*){
  Glib::RefPtr<Gdk::Window> window = get_window();

  // window geometry: x, y, width, height, depth
  int winx, winy, winw, winh, wind;
  window->get_geometry(winx, winy, winw, winh, wind);

  window->clear();
  int radius = winw/2;
  if (winh<winw) radius=winh/2;

  gc_->set_foreground(white_);

  for (int i = 0; i<=360; i=i+1){  
      int x1,y1,x2,y2;
      x1=winw/2;
      y1=winh/2;
      x2 = x1 + (int) (radius * cos(DEG2RAD * i));
      y2 = y1 + (int) (radius * sin(DEG2RAD * i));
      window->draw_line(gc_, x1,y1,x2,y2 );
  } 

  return true;
}


int main(int argc, char** argv){
  Gtk::Main main_instance (argc, argv);
  Gtk::Window* window = new Gtk::Window();
  Gtk::HBox* m_box0 = new Gtk::HBox(false,0);

  StrichKreis* kreis1 = new StrichKreis(400);
  StrichKreis* kreis2 = new StrichKreis(180);
  StrichKreis* kreis3 = new StrichKreis(50);
  StrichKreis* kreis4 = new StrichKreis(100);
  StrichKreis* kreis5 = new StrichKreis(20);

  m_box0->pack_start(*kreis1, Gtk::PACK_EXPAND_WIDGET, 0);
  m_box0->pack_start(*kreis2, Gtk::PACK_EXPAND_WIDGET, 0);
  m_box0->pack_start(*kreis3, Gtk::PACK_EXPAND_WIDGET, 0);
  m_box0->pack_start(*kreis4, Gtk::PACK_EXPAND_WIDGET, 0);
  m_box0->pack_start(*kreis5, Gtk::PACK_EXPAND_WIDGET, 0);

  window->add(*m_box0);
  window->show_all();

  Gtk::Main::run(*window);
  return 0;
}

images/Strichkreis.epsf.gif
Figure A.2: Strichkreis in Aktion.

A.3  Das Telespiel: Ping

Movable
#include <gtkmm/window.h>
#ifndef __MOVABLE_H

#define __MOVABLE_H ;

class Movable {
  int _xPos;
  int _yPos;
  int _xDir;
  int _yDir;

  int _width;
  int _height;

public:
  Movable();
  Movable(const int xPosVal,const int yPosVal);
  virtual ~Movable();

  void setXPos(const int val);
  void setYPos(const int val);
  void setXDir(const int val);
  void setYDir(const int val);
  void setWidth(const int val);
  void setHeight(const int val);

  int getXPos(){return _xPos;}
  int getYPos(){return _yPos;}
  int getXDir(){return _xDir;}
  int getYDir(){return _yDir;}
  int getWidth(){return _width;}
  int getHeight(){return _height;}

  bool touches(Movable &other);

  virtual void move(const int maxX,const int maxY);
  virtual void paint(Glib::RefPtr<Gdk::Window> window
                    ,Glib::RefPtr<Gdk::GC> gc_);

private:
  bool touchesHorizontal(Movable &other);
  bool touchesVertical(Movable &other);
};

#endif /* __MOVABLE_H */

Movable

#include "Movable.h"


Movable::Movable(){
  _xPos=0;
  _yPos=0;
  _xDir=1;
  _yDir=1;

  _width=10;
  _height=10;
}

Movable::Movable(const int xPosVal,const int yPosVal){
  _xPos=xPosVal;
  _yPos=yPosVal;
  _xDir=10;
  _yDir=10;

  _width=10;
  _height=10;
}

Movable::~Movable(){}

void Movable::setXPos(const int val){_xPos=val;}
void Movable::setYPos(const int val){_yPos=val;}
void Movable::setXDir(const int val){_xDir=val;}
void Movable::setYDir(const int val){_yDir=val;}
void Movable::setWidth(const int val){_width=val;}
void Movable::setHeight(const int val){_height=val;}

void Movable::move(const int maxX,const int maxY){ 
  _xPos = _xPos + _xDir;
  _yPos = _yPos + _yDir;
}

void Movable::paint
    (Glib::RefPtr<Gdk::Window> window,Glib::RefPtr<Gdk::GC> gc_){
}

bool Movable::touches(Movable &other){
    return (   touchesHorizontal(other) 
            && touchesVertical(other));
  }

bool Movable::touchesHorizontal(Movable &other){
    const int otherRight = other._xPos+other._width;
    const int thisRight  = _xPos+_width;

    return (_xPos<=other._xPos  && other._xPos<=thisRight)
        || (_xPos<=otherRight   && otherRight<=thisRight)
        || (_xPos>=other._xPos  && otherRight>=_xPos);

}

bool Movable::touchesVertical(Movable &other){
    const int otherUpper = other._yPos+other._height;
    const int thisUpper  = _yPos+_height;

    return   (_yPos<=other._yPos    && other._yPos<=thisUpper)
          || (_yPos<=otherUpper     && otherUpper<=thisUpper)
          || (_yPos>=other._yPos    && otherUpper>=thisUpper);
}

Ball
#include "Movable.h"

class Ball  : public Movable{
public:
  Ball();
  ~Ball();

  virtual void move(const int maxX,const int maxY);
  virtual void paint(Glib::RefPtr<Gdk::Window> window
                    ,Glib::RefPtr<Gdk::GC> gc_);
};

Ball
#include <gdkmm/window.h>
#include <gtkmm/window.h>
#include "Ball.h" 

Ball::Ball(){}
Ball::~Ball(){}

void Ball::move(const int maxX,const int maxY){
  Movable::move(maxX,maxY);

  if (getXPos()>=maxX+80) setXPos(30);     
  if (getXPos()<=-80) setXPos(maxX-40);

  if (getYPos()>=maxY-getHeight() || getYPos()<=0)
    setYDir(-getYDir());
}

void Ball::paint(Glib::RefPtr<Gdk::Window> window
                ,Glib::RefPtr<Gdk::GC> gc_){
  window->draw_arc(gc_, 1,getXPos(),getYPos()
                  ,getWidth(),getHeight(),0,360*64);
}

Paddle
#include "Movable.h"

class Paddle  : public Movable{
public:
  Paddle(const int xPosVal,const int boardHeight);
  ~Paddle();

  virtual void move(const int maxX,const int maxY);
  virtual void paint(Glib::RefPtr<Gdk::Window> window
                    ,Glib::RefPtr<Gdk::GC> gc_);

  static const int yMoveVal =11;
};

Paddle
#include <gdkmm/window.h>
#include <gtkmm/window.h>
#include "Paddle.h" 

Paddle::Paddle(const int xPosVal,const int boardHeight) 
  :Movable(xPosVal,boardHeight-50){
  setWidth(10);
  setHeight(50);
  setXDir(0);
  setYDir(0);
}

Paddle::~Paddle(){}

void Paddle::move(const int maxX,const int maxY)
{ Movable::move(maxX,maxY);
  if (getYPos()>=maxY-getHeight()) setYPos(maxY-getHeight());

  if (getYPos()<0) setYPos(0);
}

void Paddle::paint(Glib::RefPtr<Gdk::Window> window
                  ,Glib::RefPtr<Gdk::GC> gc_){
  window->draw_rectangle(gc_,1,getXPos()
                        ,getYPos(),getWidth(),getHeight());
}

GameCanvas
#include <gtkmm/drawingarea.h>
#include <gdkmm/window.h>
#include <gtkmm/window.h>

class GameCanvas : public Gtk::DrawingArea{
public:
  GameCanvas();
  virtual ~GameCanvas();
  bool timer_callback();

  virtual void moveObjects(const int maxX,const int maxY);
  virtual void paintObjects(Glib::RefPtr<Gdk::Window> window
                           ,Glib::RefPtr<Gdk::GC> gc_);
  virtual void doChecks();


  //Overridden default signal handlers:
  virtual void on_realize();
  virtual bool on_expose_event(GdkEventExpose* event);

  Glib::RefPtr<Gdk::GC> gc_;
  Gdk::Color black_, white_;
};

GameCanvas
#include <gtkmm/drawingarea.h>
#include <gdkmm/colormap.h>
#include <gdkmm/window.h>
#include <gtkmm/window.h>

#include "GameCanvas.h"


GameCanvas::GameCanvas(){
  Glib::RefPtr<Gdk::Colormap> colormap = get_default_colormap();
  black_ = Gdk::Color("black");
  white_ = Gdk::Color("white");
  
  colormap->alloc_color(black_);
  colormap->alloc_color(white_);
  set_size_request(400, 200);

  Glib::signal_timeout()
   .connect(SigC::slot(*this,&GameCanvas::timer_callback), 3);

  add_events(Gdk::EXPOSURE_MASK);
}

GameCanvas::~GameCanvas(){}

void GameCanvas::on_realize(){
  // We need to call the base on_realize()
  Gtk::DrawingArea::on_realize();

  // Now we can allocate any additional resources we need
  Glib::RefPtr<Gdk::Window> window = get_window();
  gc_ = Gdk::GC::create(window);
  window->set_background(black_);
}

bool GameCanvas::on_expose_event(GdkEventExpose*){
  // we need a ref to the gdkmm window
  Glib::RefPtr<Gdk::Window> window = get_window();

  window->clear();

  gc_->set_foreground(white_);

  paintObjects(window,gc_);
  return true;
}

bool GameCanvas::timer_callback(){
  int winx, winy, winw, winh, wind;
  Glib::RefPtr<Gdk::Window> window = get_window();
  window->get_geometry(winx, winy, winw, winh, wind);

  moveObjects(winw,winh);
  doChecks();

  queue_draw();

  // never disconnect timer handler
  return 1;
}

void GameCanvas::moveObjects(const int maxX,const int maxY){}

void GameCanvas::paintObjects(Glib::RefPtr<Gdk::Window> window
                             ,Glib::RefPtr<Gdk::GC> gc_){}

void GameCanvas::doChecks(){}

Ping
#include <gtkmm/drawingarea.h>
#include <gdkmm/colormap.h>
#include <gdkmm/window.h>
#include <gtkmm/box.h>
#include <gtkmm/main.h>
#include <iostream>
#include "Ball.h"
#include "Paddle.h"
#include "GameCanvas.h"

using namespace std;

class Ping : public GameCanvas{
public:
  Ping();
  ~Ping();
  bool timer_callback();

  int pointsLeftPlayer;
  int pointsRightPlayer;
  bool newBall;

  Ball ball;
  Paddle leftPaddel;
  Paddle rightPaddel;

  virtual void moveObjects(const int maxX,const int maxY);
  virtual void paintObjects(Glib::RefPtr<Gdk::Window> window
                           ,Glib::RefPtr<Gdk::GC> gc_);

  virtual void doChecks();

  virtual bool on_key_press_event(GdkEventKey* event);
  virtual bool on_key_release_event(GdkEventKey* event);
};

Ping::Ping():GameCanvas(),leftPaddel(10,200),rightPaddel(380,200){
  set_flags(Gtk::CAN_FOCUS);
  pointsLeftPlayer = 0;
  pointsRightPlayer = 0;
}


Ping::~Ping(){}

void Ping::paintObjects(Glib::RefPtr<Gdk::Window> window
                       ,Glib::RefPtr<Gdk::GC> gc_){
  ball.paint(window,gc_);
  leftPaddel.paint(window,gc_);
  rightPaddel.paint(window,gc_);
}

void Ping::moveObjects(const int maxX,const int maxY){ 
  this->ball.move(maxX,maxY);
  this->leftPaddel.move(maxX,maxY); 
  this->rightPaddel.move(maxX,maxY); 
}

void Ping::doChecks(){
  if (ball.touches(leftPaddel) || ball.touches(rightPaddel))
    ball.setXDir(-ball.getXDir()); 

  if (ball.getXPos()<0&& newBall){ 
    pointsRightPlayer++;newBall=false;
  }else if (ball.getXPos()>400&&newBall){
    pointsLeftPlayer++;newBall=false;
  }else if (!newBall && ball.getXPos()>40 
                     && ball.getXPos()<80) {
    newBall=true;
  }
}

bool Ping::on_key_press_event(GdkEventKey* event){
  if(event->keyval == 'l')
    rightPaddel.setYDir(- Paddle::yMoveVal);
  else if(event->keyval == 'a')
    leftPaddel.setYDir(- Paddle::yMoveVal);
}
 
bool Ping::on_key_release_event(GdkEventKey* event){
  if(event->keyval == 'l')
    rightPaddel.setYDir(Paddle::yMoveVal);
  else if(event->keyval == 'a')
    leftPaddel.setYDir(Paddle::yMoveVal);
  else cout << event->keyval << endl;
}

int main(int argc, char** argv){
  Gtk::Main main_instance (argc, argv);
  Gtk::Window* window = new Gtk::Window();
  Gtk::HBox m_box0(false,0);

  Ping* p = new Ping();
  m_box0.pack_start(*p, Gtk::PACK_EXPAND_WIDGET, 0);
  window->add(m_box0);
  window->show_all();

  Gtk::Main::run(*window);
  delete p;
  delete window;
  return 0;
}

Appendix B
Gesammelte Aufgaben

Aufgabe 0   Übersetzen Sie die Programme aus diesem Abschnitt und lassen Sie sie laufen.
Aufgabe 1   Schreiben Sie die rekursive Funktion der Fakultät. Hierzu brauchen Sie den if-Befehl, wie Sie ihn aus Java kennen. Testen Sie die Funktion mit ein paar Werten.
Aufgabe 2  
Schreiben Sie jetzt eine Version der Fakultätsfunktion, in der ein Zeiger auf die Variable, in der das Ergebnis akkumuliert wird, als zweites Argument übergeben wird:
FakAcc
void fak(int x,int* result);

Schreiben Sie Testfälle für diese Funktion.
Aufgabe 3   (2 Punkte) Schreiben Sie eine Funktion, die mit einer Intervallschachtellung für eine stetige Funktion eine Nullstelle approximiert.
Nullstellen
float nullstelle(float (*f) (float),float mi,float ma);

Testen Sie Ihre Implementierung mit folgendem Programm:
TesteNullstellen
#include "Nullstellen.h"
#include <iostream>

float f1(float x){return 2*x*x*x+5;}
float f2(float x){return 2*x +1 + x*x*x/4 ;}
float f3(float x){return 10*x+5;}
float f4(float x){return 0;}
float f5(float x){return x*x;}

void teste(float (*f)(float)){
  const float fn = nullstelle(f,-10,11);
  std::cout << "n = "<<fn<< ", f(n) = " << f(fn) << std::endl;
}

int main(){
  teste(f1);
  teste(f2);
  teste(f3);
  teste(f4);
  teste(f5);
}

Aufgabe 4  
Implementieren sie die Funktion compose entsprechend folgender Header-Datei:
Compose
typedef int (* intfunction)(int);

int compose(intfunction fs [],int length,int x);

Sie soll eine Komposition aller Funktionen in der Reihung fs auf den Wert x anwenden.
Es soll dabei gelten:
compose({f1,... fn},n,x) = fn(fn-1(...(f1(x))...))
Testen Sie Ihre Lösung mit folgender Testdatei:
ComposeTest
#include <iostream>
#include "Compose.h"

int f1(int x){return x*x;}
int f2(int x){return x;}
int f3(int x){return x+2;}
int f4(int x){return x*4;}
int f5(int x){return -x*x*x;}

int main(){
    intfunction fs [] = {f1,f2,f3,f4,f5};
    std::cout << compose(fs,5,2)<<std::endl;
}

Aufgabe 5  
Sie sollen in dieser Aufgabe die Sortiermethode des Bubble-Sort, wie sie im ersten Semester vorgestellt wurde, für Reihungen von ganzen Zahlen umsetzen:
Aufgabe 6   (2 Punkte) Schreiben Sie eine Funktion fold mit folgender Signatur:
int fold(int xs [],int length,int (*foldOp)(int,int),int startV)
Die Spezifikation der Funktion sei durch folgende Gleichung gegeben:
fold(xs,l,op,st) = op(... op(op(op(st,xs[0]),xs[1]),xs[2])...,xs[l-1])
Oder bei Infixschreibweise der Funktion op gleichbedeutend über folgende Gleichung:
fold({x0,x1,,...,xl-1},l,op,st) = st op x0 op x1 op x2 op ... op xl-1
Testen Sie Ihre Methode fold mit folgenden Programm:
FoldTest
#include <iostream>

int add(int x,int y){return x+y;}
int mult(int x,int y){return x*y;}

int fold(int xs [],int length,int (*foldOp)(int,int),int startV);

int main(){
  int xs [] = {1,2,3,4,5};
  std::cout << fold(xs,5,add,0)<< std::endl;
  std::cout << fold(xs,5,mult,1)<< std::endl;
}

Aufgabe 7   Implementieren Sie für unsere in C++ implementierten Listenstruktur die folgenden Funktionen (nach der Spezifikation aus dem ersten Semester):
Aufgabe 8   Schreiben Sie eine Funktion zur Funktionakomposition mit folgender Signatur:
template <typename a, typename b, typename c>
c composition(c (* f2)(b),b (* f1)(a),a x);

Sie sei spezifiziert durch die Gelichung: composition(f2,f1,x)=f2(f1(x)). Testen Sie Ihre Implementierung für verschiedene Typen.
Aufgabe 9   Schreiben Sie jetzt eine generische Fuktion fold auf Reihungen und testen Sie diese auf verschiedenen Typen aus.
Aufgabe 10   Verallgemeinern Sie jetzt Ihre Funktion bubbleSortBy zu einer generischen Funktion, mit der sie Reihungen beliebiger Typen sortieren können.
Aufgabe 11   (3 Punkte) Ergänzen Sie die obige Listenklasse um weitere Listenoperatoren.
Testen Sie ihre Implementierung an einigen Beispielen.
Aufgabe 12   (Alternative zur letzten Punkteaufgabe 3 Punkte) In dieser Aufgabe ist eine Klassen für HashMengen zu schreiben:
Aufgabe 13   Schreiben Sie jetzt analog zu Ihrer Lösung aus der letzten Aufgabe, eine generische Klasse HashMap, die eine endliche Abbildung von Schlüsseln auf Werte realisiert. Benutzen Sie hierzu auch die bereits geschrieben Klasse Pair, um die Schlüssel-Wert-Paare in Listen zu speichern.
Aufgabe 14  
Schreiben Sie weitere Funktionen, die Objekte der Klasse LazyList erzeugen und testen Sie diese:
Aufgabe 15   (2 Punkte) Sie haben bereits zweimal eine Funktion fold implementiert. Einmal auf Reihungen und einmal auf unseren selbstgeschriebenen einfach verketteten Listen. Jetzt sollen Sie die Funktion noch einmal allgemeiner schreiben.
Aufgabe 16   (1 Punkt) Schreiben Sie eine OpenGL-Funktion
void circle(int radius),
mit der Sie einen Kreis zeichnen können. Benutzen Sie hierzu die Figur GL_POLYGON.

Index

Klassenverzeichnis

List of Figures

    4.1  C++ Operatoren, die überladen werden können.
    5.1  Alle 10 möglichen Figuren.
    5.2  Der Graph einer Funktion.
    5.3  Alle 9 Glut Figuren mit Oberfläche.
    5.4  Alle 9 Glut Figuren als Netzwerk.
    5.5  Dreidimensionale Klötzchenwelt.
    5.6  Tux der Pinguin.
    A.1  Counter in Aktion.
    A.2  Strichkreis in Aktion.

Bibliography

[BCK+01]
G. Bracha, N. Cohen, Chr. Kemper, St. Marx, S. E. Panitz, D. Stoutamire, K. Thorup, and Ph. Wadler. Adding generics to the java programming language: Participant draft specification. Java Community Process 14, 4 2001.
[CFH+98]
Norman Chin, Chris Frazier, Paul Ho, Zicheng Lui, and Kevin P. Smith. The OpenGL Graphics System Utility Library.
ftp://ftp.sgi.com/opengl/doc/opengl1.2/glu1.3.ps, 1998.
[Gru01]
Ulrich Grude. C++ für Java-Programmierer.
www.tfh-berlin.de/~grude/SkriptCPPSS01.pdf, 2001. Skript, TFH Berlin.
[Ing78]
Daniel H. H. Ingalls. Smalltalk-76 programming system. In Proc. 5th Annual ACM Symposium on Principles of Programming Languages, 1978.
[Kil96]
Mark J. Kilgard. The OpenGL Utility Toolkit (GLUT).
http://www.opengl.org/developers/documentation/glut/glut-3.spec.ps, 1996.
[NH02]
Alexander Niemann and Stefan Heitsiek. Das Einsteigerseminar: C++ Objektorientierte Programmierung. verlag moderne industrie Buch AG & Co KG, Bonn, 2002.
[Pan02]
Sven Eric Panitz. Programmieren I.
www.panitz.name/prog1/index.html, 2002. Skript zur Vorlesung, TFH Berlin.
[Pan03a]
Sven Eric Panitz. HOpenGL - 3D Graphics with Haskell, A small Tutorial.
www.panitz.name/hopengl/index.html, 2003. Online Script, TFH Berlin.
[Pan03b]
Sven Eric Panitz. Programmieren II.
www.panitz.name/prog2/index.html, 2003. Skript zur Vorlesung, TFH Berlin.
[RL03]
Heike Ripphausen-Lipa. Programmieren II (TI).
www.tfh-berlin.de/~ripphaus/Prg2/Prg2.htm, 2003. Skript, TFH Berlin.
[Tur85]
D. A. Turner. Miranda: A non-strict functional language with polymorphic types. In Functional Programming Languages and Computer Architecture, number 201 in Lecture Notes in Computer Science, pages 1-16. Springer, 1985.
[WBN+97]
Mason Woo, OpenGL Architecture Review Board, Jackie Neider, Tom Davis, and Dave Shreiner. OpenGL Programming Guide 3rd Edition. Addison-Wesley, Reading, MA, 1997.
[Wie99]
Greg Wiegand. Teach Yourself C++ in 21 Days. www.mcp.com/sams, 1999.

Footnotes:

1Eine interessante Ausnahme ist der Editor emacs, der in Lisp geschrieben ist.
2Manche Compiler lassen für die Hauptmethode auch den Rückgabetyp void zu. So z.B. auch gcc in Version 2.95.3.
3Es gab auch noch Reihungen, die wir aber weitgehendst wie Objekte mit einer zusätzlichen Syntax betrachtet haben.
4new ist also ein falscher Freund, denn hier hat er kaum etwas mit dem entsprechenden Operator in Java zu tun.
5Ebenso wie das Zeichen * sowohl der Dereferenzierungsoperator für Zeiger als auch Bestandteil des Typnamens für Zeiger war.
6Angeblich sollen bestimmte Versionen des Microsoft C++ Compilers derartige Programme nicht übersetzen.
7Europäische Häuser starten bei der Stockwerknummerierung mit einem E für Erdgeschoss. Amerikanische Häuser numerieren das Ergeschoss bereits mit 1. Auch hier kommt es gerne zum Zaunpfahlproblem.
8In C Fehlermeldungen und Textbüchern werden solche Typen ohne den Variablennamen geschrieben, also als int (*)[3]. Dieses ist in C Quelltexten so nicht erlaubt.
9Auf deutsch findet sich auch auf die Bezeichnung: Schablone.
10Obwohl die Idee hierfür wohl ursprünglich von Moses Schönfinkel stammt, aber Schönfinkeling wäre wohl eine wenig attraktive Bezeichnung gewesen.
11So wie ja auch Reihungen über den Elementtyp generisch sind.
12moin nehme ich als Norddeutscher gerne für Funktionen, die fast die Methode main sind, lediglich noch mehr Parameter haben.
13Entspricht in meiner installierten OpenGL Bibliothek nicht der Dokumentation.?



File translated from TEX by TTH, version 3.20.
On 17 Mar 2004, 14:30.