I-PROGR2-HOME

  1. Einführung

  2. Pumpen-Beispiel in UML

  3. Von UML nach C++

  4. Pumpen-Beispiel in C++

  5. Testfragen und Übungen


I-PROGR2 SS03 - PROGRAMMIEREN2 - Vorlesung mit Übung
Von UML zu C++; Klassen in C++

 Achtung : Skript gibt den mündlichen Vortrag nicht vollständig wieder !!!
 Skript ist abgeschlossen !!
                         

AUTHOR: Gerd Döben-Henisch
DATE OF FIRST GENERATION: March-31, 2003
DATE OF LAST CHANGE: June-16, 2003
EMAIL: Gerd Döben-Henisch



1. Einführung


In der vorausgeheden VL wurde die Grundidee des Klassenkonzeptes als einer abstrakten Beschreibung einer potentiell unendlich grossen Menge möglicher konkreter Objekte eingeführt. Zugleich wurde die Entscheidung gefällt, die Repräsentationen von Klassen, ihren Objekten und deren Interaktionen primär mit den Mitteln von UML darzustellen und diese abstrakten Konzepte dann mit den Mitteln von C++ zu implementieren. Auf den ersten Blick mag diese Entscheidung umständlich erscheinen, im weiteren Verlauf wird man aber --hoffentlich-- sehen, dass die Einbeziehung der UML-Darstellungsmittel einen Zuwachs an konzeptioneller Klarheit bringen wird. Schon das heutige kleine Beispiel wird zeigen, dass ohne die Einbeziehung von UMl schon elementare Sachverhalte kaum sichtbar werden.

Es sei aber hier ausdrücklich darauf hingewiessen, dass bei den UML-Darstellungen in der Regel Vereinfachungen vorgenommen werden, da der primäre Zweck hier kein Kurs in UML ist (dazu sei auf den Standard direkt verwiessen), sondern eine Einführung in die objektorientierte Programmierung, für die wir UML soweit einbeziehen, als es uns dabei hilft.


START



2. Pumpen-Beispiel in UML


Wir beginnen die Überlegungen mit einen Ausschnitt aus unserem Synapsenproblem, nämlich der Punpe als Teil einer postsynaptischen Membran. Wir übersetzen dieses Problem in eine onbjektorientierte Struktur mittels UML. Anschliessend wird die UML-Darstellung in eine C++-Darstellung konvertiert.

Das auf die Ionenpumpe reduzierte Problem kann man vereinfachend so umschreiben: im Rahmen des synaptischen Spaltes ('synaptic gap') liegt für eine bestimmte Substanz eine bestimmte Konzentration 'sextern' vor, die sich im Vergleich zur Konzentration im Innern der postsynaptischen Membran (sintern) praktisch nicht ändert. Wohl kann sich die interne Konzentration in Abhänigkeit von verschiedenen Ereignissen ändern (Wertebereich [0,n] mmol). Für eine Ionenpumpe wird angenommen, dass sie einen Rezeptor hat, der auf einen bestimmten Schwellwert ('threshold', theta) eingestellt ist. Wird dieser Schwellwert in Abhängigkeit von der Pumprichtung ('direction', dir = in bzw. 0 oder dir = out bzw. 1) unterschritten bzw. überschritten, dann wird die Pumpe eingeschaltet und sie pumpt entsprechend ihrer Kapazität ('capacity', cap) eine bestimmte Menge entweder 'hinein' oder 'hinaus'.

Sicher sind bei dieser Problemstellungen verschiedene Analysen möglich. Die folgende Lösung wurde im Hinblick auf die weitere Fortsetzung gewählt und um besondere Eigenschaften von Klassen/ Objekten sichtbar zu machen.



umlint

Use Case für Pumpenbeispiel



Das Diagramm zeigt einen sogenannten Use Case: ein User in Interaktion mit den im Problem identifizierten Klassen.

Diese Art der Darstellung ergibt sich aus der Tatsache, dass Klassen 'in sich geschlossene' Gebilde sind, von denen gegenüber der Ausenwelt, also gegenüber einem potentiellen User, nur das sichtbar ist, was öffentlich ('public', '+') ist (Idee des 'Information Hiding').

In unserem Beispiel wurde angenommen, dass sich das Problem in drei Klassen zerlegen lässt:

Es stellt sich jetzt die Frage, wer ist eigentlich der User? Wir stellen die Beantwortung dieser Frage für einen Moment zurück. Stattdessen wollen wir der Frage nachgehen, wie denn generell die Interaktion zwischen unseren Klassen aussehen muss.



activity

Sequenzdiagramm zur Repräsentation von Interaktion



Das Sequenzdiagramm setzt voraus, dass es konkrete Objekte der zuvor definierten Klassen gibt, nämlich das Objekt g1 der Klasse gap, usw.

Zu jedem Objekt gehört eine 'Lebenslinie' als Zeitachse (von oben nach unten). Bevor der eigentliche Lauf ('Run') losgeht, müssen bei allen beteiligten Objekten die notwendigen Anfangswerte gesetzt werden (Initialisierung, 'Initialize'). Ist diese Phase abgeschlossen, beginnt das eigentliche Geschehen als 'Run'. Dieser Run ist repetitiv: Ist das Ende der Zeitpfeile erreicht, beginnt der Run wieder von vorne.

Die Grundstruktur stellt sich wie folgt dar: Zuerst wird das ps1-Objekt nach dem aktuellen Konzentrationswert si gefragt. Dann wird das Pumpenobjekt p1 aufgerufen und die Funktion p1:activity(si) gestartet. Diese prüft, ob gepumpt werden muss. Wenn ja, dann wird --in Abhängigkeit von der eingestellten Richtung-- entweder von aussen nach innen oder von innen nach aussen gepumpt. Für jeden Pumpvorgang müssen also die entsprechenden Funktionen des Objektes g1 und ps1 aktiviert werden.

An dieser Stelle der UML-orientierten Analyse soll abgebrochen werden und geschaut werden, wie sich diese bislang erarbeiteten Strukturen nach UML übersetzen lassen.


START



3. Von UML nach C++


Für eine Übersetzung der UML-Klassen in die entsprechende C++-Klassen genügt es, zu wissen, wie man die Attribute und Operationen samt der 'Sichtbarkeit' (privat, öffentlich...) übersetzt.



class

Von UML nach C++



Aus dem Schaubild kann man entnehmen, dass dem Rechteck, das in UML eine Klasse repräsentiert, in C++ das struct-Gebilde 'class NAME { ... }; entspricht.

Die UML-Attribute werden innerhalb einer C++-Klasse als Elemente dargestellt, die wie Variablen deklariert werden.

Die UML-Operationen werden in einer C++-Klasse ebenfalls als Elemente aufgeführt, die wie Funktionen (und zwar normalerweise) nur die Prototypen) deklariert werden.

Die UML-Sichtbarkeitsindikatoren '-', '#' sowie '+' werden in C++ entsprechend durch die Schlüsselwörter 'private:', 'protected:' sowie 'public:' dargestellt.

Während die Anordnung der Attribute und Operationen in einer UML-Klasse streng geregelt ist, kann man in einer C++-Klasse die Elemente (inklusive der Sichtbarkeitsindikatoren) beliebig anordnen und mischen.

Eine einfache C++Klasse zeigt das nachfolgende Code-Fragment:




//--------------------------
//
// dummy.h
//
//--------------------------

class DUMMY {

  // Variables

 private:

  int x;

 public:

  float size;

  // Member-Functions

 public:

  dummy();

  float show_size();

 private:

  void change(int);

  ~dummy();


};

Auffällig könnten in diesem Code-Fragment jene Element-Funktionen sein, die den gleichen Namen haben wie die Klasse. Jene Funktionen mit gleichen Namen wie die Klasse und ohne vorausgehende Tilde '~' das sind die sogenannten Konstruktoren. Jene mit einer vorausgehenden Tilde, das sind die Dekonstruktoren. Konstruktoren und Dekonstruktoren sind in UML nicht notwendig (trotzdem natürlich darstellbar). Konstruktoren und Dekonstruktoren bilden einen deutlichen UNterschied von C++ zu UML und dokumentieren, dass C++ nicht nur eine abstrakte objektorientierte Darstellungsweise bietet, sondern darüberhinaus ja auch eine mögliche konkrete Implementierung. Man könnte auch sagen, dass die Konstruktoren und Dekonstruktoren gleichsam eine Brücke schlagen zwischen den abstrakten Klassenkonzepten und konkreten, lauffähigen Objekten als Instanzen der Klassen. Ein Konstruktor ist nämlich genau jene Funktion, die aktiviert wird, wenn in einem Programm eine konkrete Instanz einer abstrakten Klasse --auch Objekt genannt-- gebildet wird. Zu jeder konkreten Instanz gehört neben dem Namen (:= Objektnamen) und dem Typ (:= hier eine Klasse!) normalerweise auch die Reservierung von Speicherplatz und die Belegung mit konkreten Werten. Diese 'Arbeiten' werden bei Initialisierung eines konkreten Objektes von der Konstruktorfunktion geleistet. Der Dekonstruktor ist dann --wie der Name schon nahelegt, für das 'Abräumen' bzw. das 'Aufräumen' eines zuvor eingeführten Objektes zuständig. Soll ein Objekt wieder gelöscht werden, dann ist es Aufgabe des Dekonstruktors, alle zuvor reservierten Speicherplätze, aktivierten Funktionen und Filedeskriptoren wieder zu löschen.

In den meisten Fällen benötigt eine Anwendung --wie auch schon unser einfaches Beispiel-- mehr als nur eine Klasse. Um hier den Überblick zu halten, empfiehlt es sich, die verschiedenen Code-fragmente in einer klaren Anordnung abzulegen. Wie schon in C, wo in der Regel zwischen dem Hauptprogeamm mit main(), verschiedenen Headerdateien (.h) mit den Funktionsprototypen sowie den Implementierungen der Funktionen in .c-Dateien unterschieden wurde, wird im allgemeinen auch in C++ eine ähnliche Vorgehensweise ('policy') befolgt (siehe Schaubild):



class2

Verteilung des Codes auf verschiedene Dateien



Jede Klasse hat normalerweise ihre eigene Headerdatei (.h oder .hpp) mit der Klassendefinition und Funktionsprototypen und zusätzlich eine Implementierungsdatei gleichen Namens, nur mit der Endung .cc oder .cpp. Ausserdem gibt es auch hier eine Hauptdatei mit der Funktion main(), in der die Instanzen der Klassen erzeugt und benutzt werden.

An dieser Stelle wird deutlich, dass der User in C++ eine Funktion ist, nämlich die main-Fuktion, in der Objekte der Klassen erzeugt und nach Bedarf über ihre öffentlichen Element-Funktionen benutzt werden.

Nach dieser Identifizierung des Users unseres Use Cases als das Programm mit der main-Funktion --das wir deshalb auch usage.cpp nennen-- kann man noch zwei weitere wichtige Einsichten festhalten:

.
START



4. Pumpen-Beispiel in C++


Der folgende Sourcekode ist nicht vollständig bzgl. der geplanten Funktionalität der Klassen, ist aber in einem Zustand, dass er sich kompilieren lässt.

Ab diesem Beispiel wird auch Gebrauch gemacht von dem Programm 'make' (für Details siehe 'man make' oder 'info make' unter Linux). Ab einer bestimmten Anzahl von beteiligten Dateien ist dieses Werkzeugt fast unverzichtbar. Man listet in einer Datei mit Namen 'Makefile' alle Dateien auf, die zum Projekt gehören sowie ihre Abhängigkeiten untereinander. Nach jeder Änderung einer einzelnen --oder einiger weniger-- Datei(en) genügt es dann, in dem Ordner, in dem das Makefile und die beteiligten Dateien abgelegt sind, 'make' auf der Kommandozeile einzugeben. Das Programm make identifiziert dann mit Hilfe der Makefile-Datei, welche Dateien überhaupt im Projekt vorkommen und überprüft automatisch, welche Dateien seit dem letzten Mal tatsächlich verändert wurden. Es werden dann nur diejenigen Dateien neu kompiliert, die geändert wurden.


 

objects = usage.o gapsrc.o  psmsrc.o pumpsrc.o

usage: $(objects)
	g++ -o usage $(objects)

usage.o: usage.cpp
	g++ -c usage.cpp

gapsrc.o: gapsrc.cpp gap.hpp
	g++ -c gapsrc.cpp

psmsrc.o: psmsrc.cpp psm.hpp
	g++ -c psmsrc.cpp

pumpsrc.o: pumpsrc.cpp pump.hpp
	g++ -c pumpsrc.cpp


clean:
	rm usage $(objects)



In der nachfolgenden Datei usage.cpp mit dem Hauptprogramm main() wurde das Aktivitätsprogramm noch nicht vollständig implementiert; auch sind die nachfolgenden Klassen noch 'unvollständig'. Man kann aber zu Beginn deutlich die drei Konstruktoren gap::gap g1(100); psm::psm psm1(120); pump::pump pmp1(80,50,0); erkennen, für jedes instantieerte Objekt einer. Jeder Konstruktor ist so gebaut, dass er die notwendigen Initialisierungswerte für die Objekte zu Beginn übernehmen kann.



  //-----------------------
//
// usage.cpp
//
// Compilation: make
// Usage: usage
//
// author: gerd d-h
// first: april-2, 2003
//-----------------------



#include "gap.hpp"
#include "psm.hpp"
#include "pump.hpp"

#include <iostream>

using namespace std;

int main(){

  int si;
  int c;

  gap::gap g1(100);         // Init with sextern
  psm::psm psm1(120);        // Init with sintern
  pump::pump pmp1(80,50,0); //Init with Theta, cap, dir=0 := 'in'


  cout << "Nur ein Test" << endl;
  cout << "Ausgabe von gap = " << g1.getsubst(20) << endl;
  cout << "TEST = " << psm1.getsubst(20) << endl;
  cout << "TEST = " << psm1.putsubst(30) << endl;
  cout << "TEST = " << psm1.getconc() << endl;

  si = psm1.getconc();

  cout << "PMP-Out = "<< pmp1.activity(si) << endl;


}





//-----------------------------
//
// gap.hpp
//
//------------------------------

#ifndef GAP_HPP
#define GAP_HPP

#include <iostream>


class gap {

 private:

  int sextern;

 public:

  gap(int);

  int getsubst(int);

};


#endif



Am Beispiel des Konstruktors 'gap(int)' der Klase gap kann man sehen, wie der Mechanismus der Initialisierung von Werten realisiert ist. Zwischen Funktionskopf gap::gap(int se) und Funktionsrumpf {...} gibt es eine Liste, eingeleitet mit einem Doppelpunkt ':', gefolgt von Variablennamen der Klasse gap --hier nur einer-- mit runden Klammern (). Die Werte in den runden Klammern übergeben Initialisierungswerte an diese Variablen. sextern(se) besagt dann, dass das Argument der Funktion gap::gap(int se) an die Variable sextern überwiesen werden soll. Da es sich bei der Funktion gap::gap(int se) um einen Konstruktor handelt, der zu Beginn eines Programms aufgerufen wird, und bei diesem aufruf ein Argumentwert übergeben werden kann, kann man auf diese Weise beim Aufruf den Variablen einer Klasse Werte zuweisen.






#include <iostream>
#include "gap.hpp"

using namespace std;

gap::gap(int se)
  : sextern(se)  //gap mit se initialisieren
{
  //Keine weiteren Anweisungen
}


int gap::getsubst(int c){


  cout << "Aktuelle externe Konzentration = " << sextern << endl;
  return c;

}




//-----------------------------
//
// psm.hpp
//
//------------------------------

#ifndef PSM_HPP
#define PSM_HPP


class psm {

 private:

  int sintern;

 public:

  psm(int);

  int getsubst(int);
  int putsubst(int);
  int getconc();

};


#endif





#include <iostream>
#include "psm.hpp"

using namespace std;

psm::psm(int si)
  : sintern(si)  //psm mit si initialisieren
{
  //Keine weiteren Anweisungen
}


int psm::putsubst(int c){


  cout << "PSM: Receiving substance = " << c << endl;
  sintern+= c;
  return sintern; // Give new sintern back

}

int psm::getsubst(int c){


  cout << "PSM: Transmitting Substance = " << c << endl;
  if ((sintern - c) <= 0) sintern=0;
  else sintern-=c;
  return sintern;  // Give new sintern back

}

int psm::getconc(){ //Get Concentration


  cout << "PSM: Aktuelle interne Konzentration = " << sintern << endl;
  return sintern;

}




//-----------------------------
//
// pump.hpp
//
//------------------------------

#ifndef PUMP_HPP
#define PUMP_HPP


class pump {

 private:

  int theta;   // Threshold for activation
  int cap;     // Pumping capacity
  int dir;     // Direction of pumping


 public:

  pump(int,int,int); //theta, cap, dir

  int activity(int); //Depending from theta and si the pump will become active


};


#endif





#include "pump.hpp"
#include <iostream>

using namespace std;


  pump::pump(int t,int c,int d)
  : theta(t), cap(c), dir(d)
{
  //Keine weiteren Anweisungen
}

 int pump::activity(int si) {

   if (dir == 0) {
     cout << "PMP: Case dir = 0 "<< endl;
     if (si < theta){ //Pumping from extern to intern
       return cap;
     }
     else {return 0;}

   }
   else {
     cout << "PMP: Case dir = 1 "<< endl;
     if (si > theta) {cout << "PMP: Has to pump outwards" << endl;}
     else {cout << "PMP: Nothing to do" << endl; }
   }
 }


START



5. Fragen und Aufgaben


  1. Was wird in einem Use Case dargestellt?


  2. Wie können verschiedene Klassen miteinander kommunizieren?


  3. Welches sind die drei wichtigen Abteilungen in einer UML-Klassendarstellung?


  4. Welchen Zweck erfüllt ein UML-Aktivitätsdiagramm?


  5. Was sind die wichtigsten Elemente eines UML-Aktivitätsdiagramms?


  6. Beschreiben Sie, wie sie die Elemente einer UML-Klasse in eine C++-Klasse übersetzen.


  7. Geben sie ein Beispiel für eine einfache C++-Klasse.


  8. Was sind Konstruktoren und Dekonstruktoren; beschreiben Sie ihre Aufgabe.


  9. Wie ordnet man normalerweise die Elemente eines Projektes mit vielen C++-Dateien an?


  10. In welcher Weise kann das Werkzeug 'make' Sie bei einem Programmierprojekt unterstützen?


  11. Beschreiben Sie den Zusammenhang zwischen einem UML-Aktivitätsdiagramm und jenem Programm in C++, in dem die Objekte der Klassen erzeugt werden.


  12. Beschreiben Sie den Mechanismus, wodurch ein Konstruktor bestimmte Variablen einer Klasse initialisieren kann.


  13. Welche zusätzlichen Möglichkeiten bietet ein Konstruktor sonst noch?


  14. Vervollständigen Sie die Klassendefinitionen des Pumpen-Beispiels


  15. Implementieren Sie das UML-Aktivitätsdiagramm vollständig in C++.


  16. Erarbeiten Sie sich das Kanal-Beispiel vollständig in UML und setzen es vollstndig in C++ um.



START