Übungsbesprechung von Kapitel2

siehe loesungen2.html

Programmieren mit c++, Kapitel 3

Grafische Darstellungen

Bisher haben wir uns immer auf einfache Text-Ein- und Ausgaben beschränkt. Um die Resultate von irgendwelchen Berechnungen als Bilder darzustellen, oder eine Grafische Benutzerschnittstelle zu verwenden muss man im allgemeinen auf spezielle Funktionen des Betriebssystem zurückgreifen. Um sich nicht zu sehr mit Betriebssystem-Dingen auseinanderzusetzen, und vor allem um unabhängig vom gerade verwendeten System zu programmieren, gibt es diverse Packete die entsprechende C-Programme oder C++-Klassen zur Verfügung stellen. Ich werde hier xtekplot1 verwenden, da ich das selbst geschrieben habe und somit am besten kenne. Wer will kann natürlich auch ein anderes Packet (z.B. QT) verwenden und die folgenden Programmteile entsprechend anpassen.

Wer die Details mit xtekplot1 (noch) nicht so genau wissen will kann das Minmalbeispiel hier runterladen, den folgenden Abschnitt überspringen und direkt bei Übung 8 weitermachen.
(Entpacken unter Linux: tar zxvf name.tar.gz
andere Unixe: gunzip name.tar.gz; tar -xvf name.tar)

Hier ein minimales Beispiel um xtekplot1 zu verwenden:

//grafikstart.cc
#define VERSION "Version 0.0"
#include <stdio.h>
#include <stdlib.h>
#ifndef GCC_VERSION
#define GCC_VERSION (__GNUC__ * 1000 + __GNUC_MINOR__)
#endif
#if GCC_VERSION>=3000
#include <iostream>
using namespace std;
#else
#include <stream.h>
#endif
#ifdef AMIGA
#include <amitekplot1.h>
#else
#include <xtekplot1.h>
#endif

const int XMAX=640,YMAX=512,TIEFE=4;
static int breite,hoehe,tiefe,visklasse; //Globale Variablen
static double xmin= -0.1, ymin=0.0, xmax=1.1, ymax=1.0;
static int exitflag=0;
void menu_exit() {exitflag=1;}

void grafik_zeichnen() {plot(0.,0.,PENUP); plot(1.,1.,PENDOWN);}

main(int argc,char *argv[])
{
 //tek_setdebug(1);//test
 if(argc>1 && *argv[1]=='?')
    {printf("programmname  %s\n",VERSION); exit(0);}
 getmaxsize(&breite,&hoehe,&tiefe,&visklasse);
 if(breite>XMAX) breite=XMAX;  if(hoehe>YMAX) hoehe=YMAX;
 if(tiefe>TIEFE) tiefe=TIEFE;
 setsize(breite,hoehe,tiefe);
 setmenu(1,"File");
 setmenu(1,"Exit",&menu_exit);
 inital(xmin,ymin,xmax,ymax); /* Grafikfenster oeffnen */
 grafik_zeichnen();
 term_refresh();
 while(exitflag==0 && waitmenu(1)==0)
        ;// auf Benutzereingaben warten
 inital_new();
 term_exit();
 return 0;
}// ende von main
Wir wollen dieses also für die folgenden Beispiele als Stammprogramm verwenden.

Funktionsweise des Stammprogramms

Die Teile mit GCC_VERSION stellen sicher dass es auf dem neuen GCC, wie auch auf älteren Versionen läuft.
Der Teil mit den Globalen Variablen ist nicht ganz so elegant wie man sich ein C++ Programm wünschen würde, ist aber einfacher so. (Nach dem neuen C++ Standard soll man static ganz vermeiden und statt dessen namespace verwenden, würde dann aber mit älteren Compilern nicht mehr gehen)
Mit der xtekplot1-Funktion getmaxsize() erhalten wir die maximal mögliche Grösse eines Fensters, mit setsize() stellen wir ein welche Grösse wir verwenden wollen. Die setmenu()-Aufrufe bereiten das Fenstermenu vor. Mit inital() wird schliesslich das Fenster geöffnet und das Benutzerkoordinatensystem definiert. So können wir also z.B. mit plot() hineinzeichnen ohne selbst in Bildschirmkoordinaten umrechnen zu müssen. Mit term_refresh() wird das bisher gezeichnete in einem Puffer gesichert, so dass z.B. nach Verdeckung des Fensters die Zeichnung automatisch wiederhergestellt wird. Wenn wir erneut zeichnen wollen, müssen wir inital_new() aufrufen (man könnte dabei auch noch ein neues Benutzerkoordinatensystem definieren).
In der Warteschlaufe wird auf Menu-Aufrufe durch den Benutzer gewartet. Der Parameter 1 in waitmenu() stellt sicher dass beim Warten keine Rechenzeit verbrauchen wird.
Mit dem Aufruf von term_exit() schliessen wir das Fenster wieder.

Für die folgenden Programme werden wir dieses Stammprogramm fast unverändert übernehmen. Es wird dann einfach das Unterprogramm grafik_zeichnen() durch die wesentlichen Teile unseres Programms ersetzt, zusätzliche Menupunkte definiert und vielleicht in der Warteschlaufe noch was eingefügt.

Das makefile zu unserm Stammprogramm sieht so aus:

C=gcc -c -I$h -O3
L=c++ $h/xtekplot1.o -lm -lX11 -L/usr/X11R6/lib -O3
## Amiga:
#C=gcc -c -Ih: -DAMIGA -O3
#L=gcc -lm h:amitekplot1.o h:amigalib.a h:atan2.o -O3

all: grafikstart
grafikstart: grafikstart.o 
	$L grafikstart.o -o grafikstart
grafikstart.o: grafikstart.cc
	$C grafikstart.cc

clean:
	rm -f *.o
Die Option -O3 veranlasst den Compiler zum optimieren, kann also auch weggelassen werden. Wenn der Debugger gdb benutzt werden soll, sollte -O3 durch -ggdb ersetzt werden.
$h sollte auf das entsprechende Verzeichnis von xtekplot1 zeigen. Unter Unix (Linux) macht man dazu in der kshell (bash) export h="/pfad/h" oder mit der cshell setenv h /pfad/h, auf Amiga assign h: sys:pfad/h. Auf dem pcihenri ist das schon für alle Benutzer vordefiniert.
Die Option -DAMIGA bewirkt ein #define AMIGA womit wir im Programm mit #ifdef testen können wenn der Amiga was spezielles benötigt.

Übung 8: Bringe das Stammprogramm auf deinem System zum laufen.

Konstruktor, Destruktor, Operatoren und Freunde

Um die Variablen einer Klasse zu initialisieren braucht man eine Funktion die beim Erzeugen eines Objekts dieser Klasse (man spricht auch von einer Instanz der Klasse) aufgerufen wird. Diese Funktion nennt man einen Konstruktor und wird als typlose Funktion mit dem selben Namen wie die Klasse erstellt. Man kann auch mehrere Konstruktoren mit unterschiedlicher Anzahl oder Typen von Parametern definieren.
Unser Primzahlenbeispiel würde dann etwa so aussehen:
class Primzahl
{
 int p;
public:
 Primzahl() {p=2;}
 Primzahl(int zahl) {p=zahl;}
 int lesen() {return p;}
 int next();
 int vorherige();
};
Wie schon angedeutet können wir dann in unserm Beispielprogramm mit "Primzahl prim(1000);" die Variable direkt initialisieren und somit auf prim.setzen(1000) verzichten.

Ein neues Beispiel:

class Vector
{
 double ko[2]; //ko[0]=x-Koordinate  ko[1]=y-Koordinate
public:
 Vector() {}
 Vector(double x,double y) {ko[0]=x; ko[1]=y;}
 double x() {return ko[0];}
 double y() {return ko[1];}
};
Für Vectoren beliebiger Grösse könnte es etwa so aussehen:
class Vector
{
 double *ko; //ko[0]=x-Koordinate  ko[1]=y-Koordinate
 int max;
public:
 Vector() {ko=new double[max=2];}
 Vector(int n) {ko=new double[max=n];}
 Vector(double x,double y) {ko=new double[max=2]; ko[0]=x; ko[1]=y;}
};
Beim Beenden eines Objekts sollte man den mit new reservierten Speicher wieder freigeben. Dies kann man mit einem Destruktor realisieren . Einen Destruktor definiert man wieder als eine Typlose Funktion, wobei der Name ~Klassenname sein muss, in unserm Beispiel also:
 ~Vector() {delete[] ko;}
Wenn wir mit unsern selbst definierten Klassen gleich wie mit gewöhnlichen Zahlen rechnen wollen, brauchen wir Operatoren die damit umgehen können.
Beispiel für den auf 2 Koordinaten beschränkten Vector:
 double& operator[](int i) {return ko[i];}
 double operator=(Vector v) {ko[0]=v.ko[0]; ko[1]=v.ko[1];}
 double operator+=(Vector v) {ko[0] += v.ko[0];  v.ko[1] += v.ko[1];}
Man beachte dass der Rückgabewert vom operator[] eine Referenz auf double ist, damit es wie gewohnt auch auf der linken Seite einer Zuweisung (L-Value) verwendet werden kann (also z.B. v[1]=y).
Um zwei Vectoren zu addieren ohne einen davon zu verändern brauchen wir eine Funktion operator+ die selbst nicht in der Klasse sein muss, aber trotzdem Zugriff auf den geschützten Bereich haben soll. Dazu definieren wir eine befreundete Funktion:
public:
...
friend Vector operator+(Vector,Vector);
};
Vector operator+(Vector v1,Vector v2)
{
 Vector v;
 v.ko[0]=v1.ko[0]+v2.ko[0];
 v.ko[1]=v1.ko[1]+v2.ko[1];
 return v;
}

Abgeleitete Klassen

Wir wollen jetzt mal ein Dreieck zeichnen, unter Verwendung einer Klasse die so erweiterbar ist, dass wir später damit auch Figuren mit mehr Ecken und Kanten zeichnen können. Die entsprechenden Klassen könnten also etwa so aussehen:
class Kante
{
 int k[2];//Indexe auf die Ecken
public:
 void set(int a,int b) {k[0]=a; k[1]=b;}
 int a() {return k[0];}
 int b() {return k[1];}
};
class Figur
{
 int maxn,neck,nkant;
 Vector *eck;
 Kante *kant;
public:
 Figur(int n);
 int getn() {return neck;}
 int ecke(double x,double y) {eck[neck]=Vector(x,y); return neck++;}
 int kante(int a,int b) {kant[nkant].set(a,b); return nkant++;}
 void zeichnen(int farbe=1);
 Vector& operator[](int i) {return eck[i];}
};
Figur::Figur(int n)
{
 int n2=n*n/2;
 eck=new Vector[maxn=n];
 kant=new Kante[n2];
 neck=0; nkant=0;
}

void linie(Vector a,Vector b)
{
 plot(a[0],a[1],PENUP); plot(b[0],b[1],PENDOWN);
}
void Figur::zeichnen(int farbe)
{
 for(int i=0;i<nkant;i++)
   {int a=kant[i].a(), b=kant[i].b();
    linie(eck[a],eck[b]);
   }
}
static Figur dreieck(3);
Jetzt sollten wir noch die Koordinaten der Ecken unseres Dreiecks setzen und die Kanten definieren. Wir könnten dies natürlich im Hauptprogramm etwa so machen:
 dreieck.ecke(0,0); dreieck.ecke(1,0); dreieck.ecke(0,1);
 dreieck.kante(0,1); dreieck.kante(1,2); dreieck.kante(2,0);
Eleganter wäre es aber wenn wir das im Konstruktor unserer Klasse machen könnten. Wir brauchen also eine Klasse Dreieck die mit der Klasse Figur identisch ist, aber dazu noch einen Konstruktor hat, dem wir die Koordinaten eines Dreiecks mitgeben können. Das kann mit einer Abgeleiteten Klasse realisiert werden:
class Dreieck:public Figur
{
public:
 Dreieck(double,double,double,double,double,double);
};
Dreieck::Dreieck(double x1,double y1,
		 double x2,double y2,
		 double x3,double y3) : Figur(3)
{
 ecke(x1,y1); ecke(x2,y2); ecke(x3,y3);
 kante(0,1); kante(1,2); kante(2,0);
}
static Dreieck dreieck(0,0, 1,0, 0,1);
Mit :public wird also die Oberklasse (auch Basisklasse genannt) zur Klasse Dreieck definiert. Oder anders ausgedrückt: Dreieck ist eine von Figur abgeleitete Klasse.
Beim Konstruktor bewirkt :Figur(3) den entsprechenden Aufruf des Konstruktors der Oberklasse. Die Funktionen der abgeleiteten Klasse können auch auf geschützte Objekte der Basisklasse zugreifen.

Da sollte ich noch nachtragen dass es in Klassen ausser public: noch folgende Schlüsselworte gibt:
private: ist alles was vor public: kommt,
protected: kann verwendet werden wenn abgeleitete Klassen auch Zugriff haben sollen.

Um nicht alles abtippen zu müssen kann man hier das Dreieckbeispiel runterladen.

Übung 9: Vervollständige das Dreieckbeispiel so dass mehrere farbige Dreiecke gezeichnet werden. Oder vielleicht auch noch 4-Ecke, 5-Ecke ...


Last update: 30-März-2007