Wissenswertes zu C++ in alphabetischer Ordnung

Geschwurbel von Daniel Schwamm (13.02.1995-08.03.1995)

Inhalt

1. ABLEITUNGEN

Die Basisklasse(n) einer Klasse werden in der sog. BASISLISTE angeführt und oft mit den (Zugriffs-)Spezifizieren "virtual", "public", "protected" und "private" garniert. Unten werden kurz die Unterschiede zwischen einer public- und einer virtual public-Ableitung verdeutlicht:

struct X{};        X   X   struct X{};                  X
struct Y:X{};   => |   |   struct Y:virtual X{};  ==>  / \
struct Z:X{};      Y   Z   struct Z:virtual Y{};       Y Z
struct A:Y,Z{};     \ /    struct A:Y,Z{};             \ /

Eine Klasse Y, die private abgeleitet wird von der Basisklasse X, kann auf alle nicht-privaten Datenelemente und Elementfunktionen von X zugreifen, aber nur innerhalb ihres Geltungsbereichs. Ein Zugriff selbst auf public-Elemente von X ist über ein Objekt von Y und dem Punkt- bzw. Pfeiloperator nicht möglich. protected-Elemente werden bei der private-Ableitung nur eine Stufe tief vererbt und sind dann private; eine weitere Vererbung ist im Gegensatz zur public-Ableitung nicht mehr möglich!

struct X{int i;};
struct Y:private X{};
void main(){
  Y y;
  cout << y.i;    // FEHLER!
}

Der Zugriffsspezifizierer protected macht die public-Elemente der Basisklasse zu protected-Elementen der abgeleiteten Klasse. private- und protected-Elemente behalten ihren Status.

Eine Klasse Y, die public abgeleitet wird von der Basisklasse X, kann auf alle nicht-privaten Datenelemente und Elementfunktionen von X zugreifen. Im Gegensatz zur private-Ableitung kann hier mittels des Pfeil- bzw. Punktoperators und einem Objekt von Y auf public-Elemente von X zugegriffen werden. Obiges Beispiel würde also problemlos funktionieren. Zu beachten ist aber, dass die protected-Elemente weiterhin geschützt sind. Sie können jedoch im Gegensatz zur private-Ableitung problemlos weitervererbt werden.

Eine Klasse Y, die von der Klasse X private abgeleitet ist, verbirgt vor dem Anwender alle Elemente der Basisklasse, d.h. auch die public-Elemente. Über einen Trick kann der Anwender diese aber dennoch ansprechen: Er nimmt einen Cast von Y* auf X* vor, dann kann er über das X*-Objekt auf die public-Elemente von X zugreifen.

Da bei einer public-Ableitung die Schnittstelle der Basisklasse vollständig weitervererbt wird, besitzen die abgeleiteten Klassen neben ihren eigenen Eigenschaften auch die der Basisklasse(n). Die abgeleiteten Klassen-Objekte sind also auch immer Objekte der Basisklassen. Dies entspricht in der Modellierung der IS_A-Struktur.

Die private-Ableitung schränkt die Fähigkeiten der abgeleiteten Objekte wesentlich ein, sodass diese keine IS_A-Struktur präsentieren können. Die abgeleitete Klasse ist keine Basisklasse, benutzt diese aber. Man kann hier von einer USES_A-Struktur sprechen.

2. ABSTRAKTE KLASSE

Eine Klasse mit nur EINER rein virtuellen Funktion ist bereits abstrakt. Von solch einer Klasse können keine Objekte erzeugt werden, unabhängig davon, ob es noch irgendwelche Datenelemente und nicht-virtuelle oder nicht-rein-virtuelle Elementfunktionen gibt. Übrigens: Nur virtuelle Elementfunktionen können mit dem Zusatz "=0" deklariert werden!

Rein abstrakte Funktionsdeklarationen können ausserhalb der Klassendeklaration sehr wohl eine Default-Implementierung erhalten, die von abgeleiteten Klassen über den Geltungsbereich-Operator :: (auch Qualifizierer genannt) angesprochen werden kann. Vorteil: Im Gegensatz zu einer normalen virtuellen Funktion MÜSSEN solche Funktionen von abgeleiteten Klassen überschrieben werden (auch wenn sie dabei die Basisklassen-Version benutzen können), da sie sonst selbst auch abstrakt wären.

Eine Klasse, die von einer abstrakten Klasse abgeleitet wird, muss ALLE rein virtuellen Elementfunktionen definieren, falls sie selbst nicht abstrakt sein möchte. "virtual" braucht nur dann vor die Elementfunktionen geschrieben zu werden, falls die Klasse weiteren Ableitungen zugänglich ist.

class A{virtual void f()=0;void g(){}};
void A::f(){}    // Definition erlaubt!
class B:A{};
void main(){
  B b;           // Fehler, denn B ist eine abstrakte Klasse!
}

Von abstrakten Klassen sind keine Objekte erzeugbar, daher dürfen sie auch nicht als formale Argumente oder Rückgabewerte verwendet werden (der Compiler würde protestieren). Möglich ist aber die Verwendung von Referenzen und Pointern auf abstrakte Klassen, da diese auf abgeleitete Objekte verweisen können.

3. AKTION

Eine Aktion ist eine Reaktion, die ein Objekt nach einer Zustandsänderung hervorbringt. Sie kann eine Zeitdauer besitzen oder zeitlos sein. Im ersteren Fall wirkt sie nur auf das eigene Objekt zurück (=Methodenaufruf, der etwas berechnet), im zweiten Fall kann sie ein Ereignis für ein anderes Objekt darstellen (=Nachrichtenverbindung zu einem anderen Objekt, sprich: Aufruf einer Methode eines anderen Objekts). Aktionen sind also entweder mit einem Zustand assoziiert (sofortige Aktion auf eigenes Objekt) oder mit einem Zustandsübergang (Methodenaufruf eines anderen Objekts). Eine Aktion eines Objekts ist z.B. visualisiert als ausgehender Pfeil in einem Ereignisfolge-Diagramm, während eingehende Pfeil Ereignisse darstellen, die u.U. durch Aktionen anderer Objekte hervorgerufen werden.

4. AKTIVITÄTSDIAGRAMM

Darstellungsmethode für den Zuständen-Ablauf mehrerer Task-Objekte. Legende: ---=running, ...=suspended, nichts=finished.

Task X|---...---.......-----
Task Y|------......------...
-----------------------------> Zeit-/Ereignisachse

Über dieses Diagramm kann ein Programmierer versuchen, die Aktivitäten der einzelnen Threads mittels sleep()-Aufrufe zu koordinieren. Dies ist jedoch ausgesprochen mühsam. Besser ist, dass ein Prozess sich Schlafen legen kann und einem anderen Task über wait_for_exit() (nach Schader wäre der Aufruf terminationAwaited() besser) mitteilt, dass dieser in nach Abschluss seiner Arbeit zu wecken hat.

Wenn Threads von der Idee her unabhängig sein sollen, wozu ist dann eine Serialisierung überhaupt notwendig? Aufgrund von begrenzten Ressourcen, wie z.B. Diskettenlaufwerke oder auch das cout-Objekt. Die Koordination kann über sleep() oder wait_for_exit() erfolgen, aber auch über Prioritäten und Task-Scheduler erfolgen. Am sichersten jedoch ist die Verwendung von Semaphore, so existiert bei Borland eine Nachtwächter-Klasse namens TMutex. Solche Semaphore können auch verhindern, dass sich Duplikate eines Objektes in verschiedene CPUs laden, sodass es bei der Ausgabe zu Inkonsistenzen kommen kann.

Die TMutex-Klasse führt als Attribut eine Mutex-ID und geht eine Beziehung mit einer Lock-Klasse ein. Nur wenn ein Task den Konstruktor der Lockklasse erfolgreich aufrufen kann, kann er auf die geschützte Resource zugreifen. Kritisch anzumerken ist, dass keine explizite Freigabe des Lock-Objekts möglich ist, z.B. Unlock(), sondern dass der Destruktor-Aufruf am Ende eine Block-Geltungsbereiches abgewartet werden muss.

5. ANTWORTMENGE

Die Antwortmenge ist eine Metrik, die besonders auch für objektorientierte SW geeignet ist. Aufsummiert werden hier alle Methoden einer Klasse, sowie alle klassenfremden Methoden, die innerhalb der eigenen Methoden aufgerufen werden. Dies gibt die Komplexität einer Klasse wieder.

6. ANWEISUNG

Eine Anweisung (Statement) wird vollständig abgearbeitet und alle Seiteneffekte dabei vermerkt. Erst dann wird die nächste Anweisung abgearbeitet. So wird z.B. garantiert, dass eine Postfix-Dekrement-Operation in einer Anweisung in der nächsten Anweisung bereits vollständig durchgeführt ist. C++ wird daher auch Seiteneffekt-Programmierung genannt (jeder Funktionsaufruf gilt bereits als Seiteneffekt). Ein einzelnes ";" ist eine vollständige Anweisung!

Anweisungen steuern die Verarbeitung und den Kontrollfluss von Programmen. Funktionsrümpfe, Deklarationen, Ausdrücke mit abschliessendem ";" und Blöcke stellen Anweisungen dar. Jede Anweisung kann mehrfach mit Labels markiert werden.

7. APPLICATION FRAME

Anwendungsrahmen dienen im Wesentlichen der Erzeugung von Human Interface Clients (HIC) für Applikationen, denen sie damit ein typisches Look&Feel aufzwingen. Im Gegensatz zu Toolkits können mit ihnen aber auch Objekte für die Applikation erzeugt werden, so enthält die OWL z.B. auch Container-Klassen mit impliziter Sortierung der Elemente.

8. ASSOZIATIVITÄT

Operatoren können links- oder rechts-assoziativ sein. Links-assoziative Operatoren sind i.d.R. zweistellige Operatoren, die von links nach rechts ausgewertet werden (Ausnahme: Zuweisungsoperator), d.h. es gilt: a/b/c <==> (a/b)/c oder a[1][2] <==> (a[1])[2]. Die einstelligen Operatoren dagegen sind meist rechts-assoziativ, d.h. sei werden von rechts nach links ausgewertet.

9. ATTRIBUT

Im OOA-Modell können atomare Attribute verwendet werden. Das sind Attribute, die mehrere Attribute umfassen können, wie z.B. das Attribut "Anschrift". Im OOD-Modell müssen diese atomaren Attribute dann wieder Normalisiert werden. Auch abgeleitete Attribute und die Surrogate, von denen im OOA-Modell abstrahiert werden kann, sind im OOD-Modell explizit zu benennen.

Attribute, die in abstrakten Klassen auftauchen und in den abgeleiteten Klassen, zeigen dem OOP-Entwicklern an, dass diese Attribute in der abstrakten Klasse rein virtuell zu deklarieren sind.

Bei Attributen, die in der OOA ermittelt werden, sind folgende SONDERFÄLLE zu beachten: Ein Attribut, das nicht zu allen Objekten einer Klasse passt, verlangt u.U. eine Neustrukturierung der Klasse; eine Klasse, die nur ein Attribut führt, muss darauf geprüft werden, ob sie nicht als Attribut einer anderen Klasse genügt; gleiche Attribute sollten nicht in mehreren Klassen Verwendung finden; ableitbare Informationen sollten nicht in eigenen Attributen untergebracht werden (erst in der OOD); lässt sich für eine Klasse kein Attribut finden, muss die Notwendigkeit der Klasse überprüft werden.

10. AUFRUFOPERATOR

Der Aufrufoperator kann benutzt werden, um einem Datentyp den Charakter einer Funktion zu geben, die einen frei definierbaren Funktionswert zurückliefert. So lässt sich z.B. eine Polynomklasse denken, deren Objekt p für ein int-Argument x einen double-Funktionswert y=p(x) zurückliefert.

double Polynom::operator()(int x){
  double px=a[n];
  for(int i=n-1;i>=0;i--)
    px=px*x+a[i];
  return px;
}
void main(){
  double a[]={1.1,2.2,3.3};
  Polynom p(2,a);
  cout << p(6) << endl;
}

11. AUFZÄHLUNGSTYP

Enumeratoren-Konstanten können mit gleichen Werten initialisiert werden. Doch die Sinnhaftigkeit eines solchen Vorgehens kann bezweifelt werden. Insbesondere beim Casten gibt es dann durch Mehrdeutigkeiten Probleme, z.B.:

enum Moral{gut=1,schlecht=1};
void main(){
Moral m1=1;// Fehler, weil 1 falscher Typ
Moral m2=(Moral)1;// Fehler, weil mehrdeutig
}

Jede Enumerator-Deklaration ist IMMER auch Deklaration! Ihre Aufgabe ist in erster Linie, die Lesbarkeit von Programmen zu erhöhen oder als Konstanten in Klassen zu dienen.

12. AUSDRUCK

Ausdrücke (Expressions) sind kleinere Einheiten in C++-Programmen als Anweisungen, d.h. eine Anweisung kann einen Ausdruck oder mehrere Ausdrücke enthalten, nicht aber umgekehrt.(?) Bei der Ermittlung des Werts eines Ausdrucks sind Seiteneffekte möglich, d.h. Operanden können sich durch Operatoren ändern. Bei der Auswertung eines Ausdrucks ist die Assoziativität und die Priorität der Operatoren zu beachten.

13. AUSNAHME-HANDLING

Was ist der Sinn der Ausnahmebehandlung? Es gilt, dass ein Programmierer einer Bibliothek oft nicht in der Lage ist, Fehler in intelligenter Weise abzufangen, da seine Programme anwendungsunabhängig sind. Der Benutzer einer Bibliothek dagegen weiss sehr wohl, wie er mit Fehlern in seinem Programm verfahren sollte.

Als Anwendungsbeispiel einer Fehlerbehandlung sei folgendes Programm aufgeführt, welches einen Indexüberlauf abfängt:

struct Vector{
  int *p; int size;
  class Überlauf{int ix;Überlauf(int i):ix(i){}}
  int& operator[](int i){
    if(i>=0 && i<size)return p[i];
    throw Überlauf(i);
  }
}
void main(){
  Vector v(3);int i;
  try{char *s;gets(s);i=atoi(s);cout<<v[i]<<endl;}
  catch(Vector::Überlauf u){
   cout << u.i << "=falscher Index";
  }
  catch(...) ...
  // wenn Fehler nicht abgefangen wird, dann Terminierung.

In Fehlerklassen sind auch Templates, Enumeratoren und Ableitungen erlaubt. Enumeratoren können z.B. dazu verwendet werden, innerhalb einer catch-Anweisung eine weitere Selektion mittels switch-Anweisungen durchzuführen.

In neueren Compilern ist die Ausnahmebehandlung integriert. So wirft der new-Handler per Default bereits bei Misserfolg einen Fehler aus, der abgefangen werden muss, damit das Programm nicht terminiert. Dadurch ändern sich bestehende Programme, da bisher nur eine Null zurückgeliefert wurde, was unbehandelt folgenlos blieb. Um die Anwender nicht zu umständlichen Umschreibearbeiten mit try-Blöcken zu zwingen, gestattet Borland mittels "new_handler(0);" die Deaktivierung des Fehlerauswurfs.

Die Funktion exit() ruft noch den Destruktor von automatischen Objekten auf, die Funktion abort() tut nicht einmal das. Will man auch alle Heap-Objekte zerstören, so wird eine Ausnahme-Behandlung unumgänglich.

14. BASEBALL-MODELL

Das Baseball-Modell von Coad und Nicola betont das frühe Verfügbarmachen von Analyse- und Designergebnissen. Nach ein wenig Analyse, wird ein wenig Design betrieben, dann ein wenig Implementierung, dann beginnt man wieder von vorne, wobei das Ergebnis immer komplexer wird. Wichtig dabei ist, dass die Trennung zwischen den Designkomponenten nicht verwischt wird, evtl. sogar von verschiedenen Teams realisiert wird. Durch die Prototyping-Philosophie kann der Benutzer bereits sehr früh in die Entwicklung von Systemen mit einbezogen werden.

15. BEZIEHUNG

Beziehungen werden nicht in der Strukturschicht, sondern in der Service- oder Attributschicht des OOA-Modells modelliert. Werden sie in der Attributschicht gebildet, heissen sie Instanzverbindungen, die durch Linien mit Kardinalitäten ins Modell einzuzeichnen sind. Werden sie in der Serviceschicht gebildet, heissen sie Nachrichtenverbindungen, die als Pfeile einzuzeichnen sind (und die evtl. den Namen der aufzurufenden Methode tragen).

Instanzverbindungen sind immer dann nötig, wenn ein Objekt zur Ausführung seiner Aufgabe ein anderes Objekt benötigt. Nachrichten-Verbindungen sind immer dann nötig, wenn ein Objekt zur Ausführung seiner Aufgabe die Methoden eines anderen Objekts benötigt.

Insbesondere wenn Viele-zu-Viele-Instanzverbindungen in einem OOA-Modell identifiziert werden können, sollte man sich überlegen, ob zwischen diesen Verbindungen nicht noch eine weitere Klasse einzufügen ist (z.B. eine Ereignisklasse, die zu erinnernde Attribute führt).

Bei Objektbeziehungen der OOA sind folgende SONDERFÄLLE zu beachten: Bei (*:n):(*,m)-Beziehungen sollte evtl. eine Zwischenklasse eingefügt werden, die beziehungsrelevante Informationen festhält; auch bei einfachen Beziehungen sollten in einer der beteiligten Klassen Attribute geschaffen werden, die beziehungsrelevante Informationen führen; geht eine Klasse nur eine Beziehung mit sich selbst ein, kann zur näheren Beschreibung der Beziehungsart eine Zwischenklasse eingefügt werden; zwischen Klassen können mehrere verschiedenartige Beziehungen bestehen - sie alle sind zu modellieren (und evtl. durch Attribute oder Zwischenklassen näher zu beschreiben); u.U. genügt es, wenn ein Objekt die Beziehung verwaltet, weswegen nur eine Kardinalitätsangabe nötig ist.

16. BINDUNG

C++ unterscheidet zwei Arten von Bindung:

  1. Interne Bindung: Variablen/Funktionen mit interner Bindung gelten nur innerhalb der Datei, in der sie deklariert sind. Andere Programmdateien können mit ihren Namen nichts anfangen (auch nicht über extern-Deklarationen). Die lokalen und Funktionsgeltungsbereiche (aber nicht Funktionsnamen!) sind immer intern gebunden! Auch inline-, const-ohne-extern- und static-Variable des globalen Geltungsbereichs sind intern gebunden, ebenso typedefs, enums, Templates, sowie zuletzt Klassen mit nur inline-Funktionen und ohne static-Objekte, die nicht in Deklarationen mit externer Bindung auftauchen. Beispiel: "const int i=2;" bedeutet, dass die Konstante "i" nur in einer Programmdatei erkannt wird. Besonderheit: static bindet Typen immer intern, ausser bei Klassenelementen; die werden seltsamerweise extern!
  2. Externe Bindung: Variablen/Funktionen mit externer Bindung können in jeder Programmdatei verwendet werden, sofern ihre Namen nicht durch gleich lautende Variablen verdeckt werden. Nötig ist dazu jedoch eine extern-Deklaration (im Falle von Klassen der komplette Deklarationsrumpf ohne extern). Per Default sind globale Variable und Funktionen extern gebunden. Nicht-inline-Elementfunktionen und nicht-static-Klassenelemente sind zwangsweise extern gebunden, ebenso globale Variablen mit den Spezifizierer "extern". Mittels const, typedef und enum werden globale Variablen "interniert", da für sie keine adressierbaren Symbole existieren, sondern diese direkt in den Code eingetragen werden.

Ausschlaggebend für die Bindung ist nicht die Deklaration, sondern die Definition. So werden Klassen erst durch eine Definition ausserhalb der Klassendeklaration extern. Da auch static-Klassenelemente und friend-Funktionen ausserhalb des Deklarationsrumpfes definiert werden, machen sie Klassen extern. Und ebenso wirken inline-Klassendefinition ausserhalb des Deklarationsrumpfes.

17. BOOL

Neuer Datentyp von C++, der bisher aber nur vom g++ direkt unterstützt wird. Werte: TRUE=1, FALSE=0.

18. CLUSTER-MODELL

Das CM von Meyer betrachtet nicht die Entwicklung von Modellen für bestimmte Problembereiche, sondern vielmehr die Entwicklung von Clustern. Das sind logisch zusammengehörende Klassen. In einem dreistufigen Prozess (SPEC, DESIMPL, VALGEN) können in zeitlicher Überschneidung mehrere Cluster parallel und unabhängig voneinander entworfen werden. Ein besonders bzgl. von Reuse-Pattern-Entwicklung interessanter Ansatz.

19. CONST

Das Schlüsselwort const kann vor oder hinter einem Datentyp stehen. Einmal wird dadurch der Variableninhalt, einmal der Zeiger auf die Variable zur Konstanten gemacht. Zwischen diesen abgeleiteten Datentypen ist keine Standardkonversion möglich!

const int i=2;
int const *j=&i;        // ERROR
int const *k=(int*)i;   // OK

Wird eine Referenz eines konstanten Objekt an eine Funktion übergeben, und diese Referenz dann verändert, so erzeugt der Compiler mit Warnung eine temporäre Kopie der Referenz. Diese Kopie ist änderbar, jedoch wird der call-by-reference-Mechanismus ausgeschaltet, d.h. bei Rückkehr aus der Funktion hat sich die Referenz nicht geändert.

void f(int &i){i++;}    // i ist temporäre Kopie von ci, trotz
void main(              // call-by-reference mittels &-Operator
  const int ci=2;
  f(ci);
};

In Elementfunktionen, die mit const deklariert wurden, können durchaus Variablen verändert werden, ohne dass sich der Compiler beschwert. Diese Variablen müssen aber lokal sein. Datenelemente der Klasse können nur dann verändert werden, wenn sie mit "mutable" deklariert werden. Alternativ dazu kann aber auch eine Kopie des aktuellen Objekts angelegt werden, welches lokalen Geltungsbereich besitzt und damit frei veränderlich ist.

struct X {
  int i;
  int f() const {
    X *THIS=(X*)this;    // const MUSS mit (X*) weg-gecastet werden!
    THIS->i++;           // ist jetzt erlaubt, weil THIS lokal ist.
    return THIS->i;      // auch in this wurde i mitverändert!
  }
};

Es können konstante Objekte erzeugt werden mittels "const X x;". Zu beachten ist jedoch, dass solche Objekte keine Funktionen aufrufen können, die nicht const spezifiziert wurden (ausser sie sind inline definiert und ändern nichts am Objekt). Die static-Elemente sind weiterhin änderbar, da sie objektunabhängig sind. Der this-Zeiger ist bei nicht-const-Funktionen vom Typ X*const, bei const-Funktionen vom Typ const X*const. Es existiert keine implizite Konversion von X*const nach const X*const, weswegen der const-Spezifizierer als Unterscheidungskriterium für überladene Funktionen genügt. Und ausserdem können aus const-Funktionen keine nicht-const-Funktionen aufgerufen werden!

Elementfunktionen können const sein. Wird in einer solchen Funktion das this-Objekt verändert, bricht der Compiler ab. Dennoch lässt sich durch einen Trick die Veränderung des this-Objekts erreichen, indem einfach eine Kopie von this erzeugt wird:

 class X{
  int i;
  void f()const{
   i=5;   // Fehler 
   this->i=5;  // Fehler
   X *This=(X*)this; // Referenzkopie erstellen
   X->i=5M;  // OK: this-Objekt wurde verändert
  }
 };

20. CONTAINER-KLASSE

Containerklassen sich auf drei Arten in C++ realisieren:

  1. void-Pointer-Version: Es wird eine Listenstruktur erzeugt, die void*-Objekte aufnehmen kann. Vorteil ist die einfache Implementierung und Geschwindigkeit eines solchen Codes. Nachteilig ist jedoch, dass gemischte Objekte in der Liste ablegbar sind, ohne spezielles Typ-Attribut aber beim Zugriff nicht auf den ursprünglichen Typ zurück gecastet werden kann, da man ihn nicht kennt. In Zukunft soll es jedoch dazu eine Funktion "typeid(TYP)" geben.
  2. Basisklassen-Version: Die Basisklasse vererbt an die abgeleitete Klassen, die die Container-Elemente führen, seine virtuelle public-Schnittstelle (mit Default-Implementationen). Dieses Vorgehen ist übersichtlich und objektorientiert, kann bei vielen verschiedenen Elementtypen aber rasch zu einer "Klassen-Explosion" führen. "fat-interfaces" sind übrigens zu vermeiden. Nur die virtuellen inline-Funktionen werden im Headerfile dargestellt.
  3. Template-Version: Ein und derselben Container-Klasse wird beim Aufruf der Elementtyp mit übergeben, wodurch der Compiler selbstständig eine spezielle Version der Container-Klasse erzeugt. So kommt es zwar immer noch zu einer Klassen-Explosion, jedoch muss sich dazu kein Programmierer bemühen. Der Programmcode kann dadurch erheblich verlängert werden, der Code ist in den Headern offen gelegt, die hohe Flexibilität und 100% Typ-Sicherheit ist jedoch ein grosser Pluspunkt.

Es ist zu beachten, dass die in der Container-Liste geführten Elemente meistens nicht von der Existenz des Containers abhängen sollten, d.h. dass eine Löschung eines Objekts aus dem Container nur den Pointer bzw. die Referenz darauf löscht, nicht aber das Objekt selbst zerstört! Werden jedoch wie etwa bei der Template-Implementation jeweils Kopien der Objekte angelegt, sind diese selbstredend zu zerstören. Ob man sich für eine Pointer-Container oder Kopien-Container entscheidet, ist designabhängig. Ausbaufähige Idee: Template- und void*-Container mischen, dann erhält man im Idealfall Typ-Sicherheit ohne Code-Duplizierung.

Bei void*-Container findet keine Typ-Prüfung statt, bei Object-Ansatz-Container erst zur Laufzeit, und bei Templates während der Übersetzung. Typ-Kontrolle kann durch static-Variablen, String-Vergleiche der Klassennamen, sicherere benutzerdefinierte Casts, Indizierung und Enumeratoren nachträglich realisiert werden. Eine Typ-Prüfung zur Laufzeit ist für C++ derzeit in Arbeit. Beim Object-Ansatz müssen auch für eingebaute Typen wie int eigenen Klassen abgeleitet werden. Templates lassen den Code aufgedeckt und machen Programme länger; aber auch sie erlauben Vererbung, sodass verschiedene Objekte in ein Container ablegbar sind! Da Templates auf virtual verzichten können, sind sie schneller, da sie Elementfunktionen inline deklarieren können. Zu beachten ist, dass für Sortierfunktionen u.ä. die entsprechenden Operatoren überschrieben werden.

21. COPY-KONSTRUKTOR

Bei Vererbungsstrukturen ist zu beachten, dass der Copy-Konstruktor auch die Teile der Basisklasse zu konstruieren, sprich: initialisieren hat. I.d.R. geschieht dies in der Initialisiererliste durch Aufruf des Copy-Konstruktors der Basisklasse(n). Dasselbe gilt in ähnlicher Weise auch für den Zuweisungsoperator (hier ist ein A::operator=(a) nötig).

Copy-Konstruktoren sind Konversionsfunktionen. Sie werden bei Initialisierungen (über "=" oder "()"), nicht aber Zuweisungen aufgerufen. Wichtig sind sie, wenn Objekte mit Zeigern auf den Heap dupliziert werden sollen. Mittels Copy-Konstruktor lassen sich neue Speicherbereiche im Heap anlegen und initialisieren (Deep Copy), statt einfach einen Zeiger auf die alte Resource umzubiegen, sodass Änderungen an einem Objekt Änderungen am kopierten Objekt mit sich bringen..

Copy-Konstruktoren dürfen als formales Argument kein Objektwert führen, da dies eine Endlosrekursion bewirken würde. Auch Pointer sind verboten! Als Argumente zulässig sind jedoch Referenzen auf (const-)Objekte.

Um den Compiler am Erzeugen eines Standard-Copy-Konstruktors zu hindern, muss ein eigener Copy-Konstruktor angelegt werden. Befindet sich dieser im private-Teil, so kann er vom Anwender nicht aufgerufen werden. Diese Eigenschaft vererbt sich auch weiter, d.h. existiert auch nur eine Basisklasse mit private-Copy-Konstruktor, erzeugt der Compiler für die abgeleiteten Klassen keinen Standard-Copy-Konstruktor (da diese ja den Copy-Konstruktor der Basisklasse aufrufen können müssen).

22. COUPLING

Das Coupling gibt die äussere Verflechtung einer Klasse wieder, d.h. der Grad, mit dem sie mit anderen Klassen Beziehungen eingeht. Dazu gehören Objektbeziehungen, Nachrichten-Verbindungen und Aggregationsstrukturen (aber nicht Vererbung). Das Coupling sollte so gering wie möglich gehalten werden, um Domino-Effekte bei Klassenänderungen zu minimieren.

23. DEFAULT

static-Element-Member können als Zuweisungswerte an Argumente in Standard-Konstruktoren verwendet werden!

struct X{
  static int Anzahl;
  X(int i=Anzahl){/*...*/}; // OK
};

24. DEFINE

Die Präprozessor-Anweisungen eignen sich auch zum Definieren von regelrechten Funktionen (allerdings nur void-Funktionen). So kann z.B. eine Variable wegen der Überladbarkeit von cout von BELIEBIGEM Typ sein und dennoch folgendermassen ausgegeben werden:

#define F(x) cout << x << endl;
void main(){
  F(2);
  F(2.2);
  F("Text");
}

25. DEKLARATION

Enumeratoren zählt man zu den Deklarationen, da über sie NEÜ Namen mit NEÜR Semantik im Programm erzeugt werden. Dagegen gelten typedef-Anweisungen nicht als Deklarationen, da bei ihnen nur alte Namen neü Namen erhalten, ohne dass sie ihre Semantik verändern.

Deklarationen sind Anweisungen (und nicht nur Ausdrücke). Deklarationen sind dann Definitionen, wenn der Compiler die Grösse der Deklaration erkennen kann, wenn z.B. in Klassendeklarationen keine dynamischen Felder enthalten sind. Es gilt:

 class IntVek1; // nur Deklaration
 class IntVek2{ // Deklaration UND Definition, da
  int i;        // Compiler bereits Objekte von IntVek
  IntVek2();    // erzeugen kann.
 }; 
 class IntVek3{ // reine Definition (Aber: Definitionen
  int i;        // sind immer auch Deklarationen)
  IntVek(){i=0;}
 };

Im Beispiel "class X{void f();};" gilt void als Deklarationsspezifizierer und f() als Funktionsdeklarator. Eine Klasse ist erst dann DEFINIERT, wenn ALLE Elementfunktionen und static-Datenelemente definiert sind,. Nicht-static-Datenelemente können erst in Konstruktoren definiert werden, da sie objektabhängig sind, d.h. "class X{double d=1.2;};" ist ein Fehler.

26. DESTRUKTOR

Da Destruktoren keine formalen Argumente führen dürfen, können sie nicht überladen werden. Aus diesem Grund besitzt jede Klasse entweder keinen oder genau einen Destruktor. Der kann auf drei Arten aufgerufen werden: Beim Programmende in globalen Objekten oder static-lokalen Objekten, beim Verlassen eines Blocks in lokalen Objekten, oder explizit beim Löschen von Objekten mittels "delete". Ein Standard-Destruktor zerstört nur die einfachen Datenelemente, löscht aber keine verpointerten Strukturen im Heap. Destruktoren sind objektabhängig und zerstören Objekte, daher dürfen sie weder static noch const deklariert sein. Im Gegensatz zu Konstruktoren können sie jedoch virtual sein, was den Vorteil hat, dass abgeleitete Objekte nicht explizit die Destruktoren ihrer Teilobjekte aufrufen müssen.

27. DIENST

Das OOA-Modell identifiziert Dienste in der Service-Schicht. Unterschieden werden dabei die einfachen Dienste wie erzeugen(), verbinden(), ändern() und löschen(), die nicht explizit zu benennen sind, und komplexe Dienste. Typische Vertreter der komplexen Dienste sind etwa berechne()-Funktionen, die sich auf die eigenen Datenelemente beschränken, und überwache()-Funktionen, die i.d.R. Methoden wie initialisiere() und beende() enthalten und mit externen Objekten eine Verbindung eingehen.

28. EINGEBETTETE KLASSE

Eine in eine andere Klassendeklaration eingefügte Klassendeklaration bezeichnet man als eingebettete Klasse. Der eingebetteten Klasse entstehen dadurch so gut wie keine Zugriffsvorteile auf die umgebende Klasse (das Gleiche gilt auch umgekehrt) - es muss immer vollständig (über Objekte) quantifiziert werden. Daher ist es meist besser, auf eingebettete Klassen zu verzichten und sie ausserhalb zu deklarieren. Eine eingebettete Klasse Y in der Klasse X kann übrigens auch über "X::Y y;" Objekte erzeugen, ohne dass es Objekte von X geben muss! Externe Funktionsdefinitionen erfolgen dann in diesem Fall vollständig qualifiziert über "void X::Y::f(){...};".

Eingebettete Klassen sind nicht etwa im globalen Namensraum deklariert, sondern im Geltungsbereich der umgebenden Klasse. Sie ist lokal zur umgebenden Klasse. Dies bemerkt man schon dadurch, dass nur über verkettete Pfeil-/Punktoperatoren auf eine eingebettete Klasse zugegriffen werden kann. Vorteil: Weniger globale Namen.

29. ELEMENTFUNKTION

Über nur einen Aufruf können mehrere Elementfunktionen aufgerufen werden, wenn diese einen Zeiger auf ihr Objekt zurückliefern.

struct X{
  X f(){cout << 1; return *this;}
  X g(int i){cout << i; return *this}
};
void main(){
  X x;
  x.f().g(2).f().g(3).g(4);      // OK!
}

30. ELEMENT-POINTER

In C++ lassen sich Datentypen ableiten, die Pointer auf Datenelemente bzw. Elementfunktionen darstellen:

struct X{
  int i;
  int f(double &d){return d*i;}
  static int is;
  static int fs(double &d){return d*is;}
};
int X::is=1;
void main(){
  int X::*dp=&X::i;
  int (X::*fp)(double&)=&X::f;    // Funktionen sind einzuklammern
  int *dsp=&X::is;              // static-Version mit normalem Pointer!
  int (*fsp)(double&)=&X::f2;
  X x;
  x.*dp=2;
  cout << (x.*fp)(3.3);           // Funktionen sind einzuklammern
  cout << *dsp;
  cout << (*fsp)(3.3);
}

Zeiger auf Elementfunktionen werden i.d.R. durch virtuelle Funktionen ersetzt. Die Deklaration eines Element-Pointers legt nur den Typ fest, nicht auf welches Element er letztlich zeigen wird. Das verleiht ihnen eine gewisse Flexibilität beim Einsatz. Auf static-Elementfunktionen können keine Element-Pointer gebogen werden, da Element-Pointer immer implizit einen this-Zeiger mitführen. Daher lassen sich auch keine globalen Funktionen mit Element-Pointern verbinden. Normale Pointer lassen sich dagegen sehr wohl auf static-Elemente setzen.

31. ENTROPIE

Die Programmentropie ist das Mass dafür, inwieweit ein Programm durch Modifikationen in "Unordnung" geraten ist.

32. ENUM

Enumerationen sind immer Definitionen, nie nur Deklarationen. Über sie werden neue Namen in ein Programm eingeführt, die einen konstanten Wert führen. Dieser Wert wird allerdings nicht in einer bestimmten Speicherzelle gespeichert. Der Compiler setzt den Wert vielmehr direkt in den Code ein. Daher gelten enums (wie typedefs) als bindungslos (tatsächlich sind sie aber intern gebunden).

Achtung: Befinden sich im Namensraum von enum-Definitionen Variablen mit gleichem Namen, so werden stets die enums verdeckt (evtl. mit Warnung vom Compiler)!

33. EREIGNIS

Ein Ereignis wird durch ein Objekt oder ein externes System (z.B. Datenquelle/-senke) ausgelöst. Es ist ZEITLOS, weswegen mehrere Ereignisse gleichzeitig in einem System auftreten können. Bestimmte Ereignisse bewirken bei bestimmten Objekten einen genau definierten Zustandsübergang, d.h. sie überführen ein Objekt von einem Zustand in einen neuen Zustand, i.d.R. indem sie eine entsprechende Methode des Objekts aufrufen (da nur diese auf die Attribute zugreifen dürfen). Es gilt also, dass Ereignisse immer einen Sender und meistens einen Empfänger haben. Sie können einem Empfänger Informationen zutragen. Implizit führen sie z.B. immer das Attribut des Zeitpunkts. Ereignisse können untereinander abhängig oder auch unabhängig sein. Ein Objekt kann ein Ereignis regelrecht erwarten.

34. EXTERN

Eine blockfreie Anweisung (die also globalen Geltungsbereich besitzt, d.h. dem Geltungsbereich Datei) der Form "int i;" ist nicht nur Deklaration, sondern auch Definition der Variable i. Zudem wird dieselbe auch noch mit dem Wert 0 initialisiert! Anders sieht es aus, wenn ein "extern" vorgeschaltet wird: "extern int i;" ist nur Deklaration, keine Definition! Dagegen ist "extern int i=2;" wieder eine Definition, die nur einmal im Programm vorgenommen werden kann und muss (da sich sonst der Linker über die unbekannte Variable i beschwert). Ist doch simpel, oder?

35. EXTERNE DATENHALTUNG

In der Daten-Management-Komponente muss entscheiden werden, ob Klassen und Objekte persistent (räumlich UND zeitlich dauerhaft) oder transient zu konzipieren sind. DM wird nötig, wenn sehr grosse Objekte anfallen, wenn Objekte mehrfach genutzt werden (Data Sharing), wenn sie als Kommunikationsmedium dienen, und wenn nur wenige Objekte zu einem Zeitpunkt relevant sind (sodass das Halten der restlichen Objekte Platzverschwendung darstellt).

ASPEKTE der externen Datenhaltung sind die Geschwindigkeit, die deutlich abnimmt, die Fragen des Speicherformats und der Dateninterpretation, die Zugriffssprachen, und die Konsistenz der Objekte. All diese Aspekte werden von RDBS berücksichtigt, die aber durch ihre mengenorientierte embedded SQL nicht besonders objektorientiert sin. Die Zukunft von OODBS ist dagegen sehr unsicher, denn es ist wahrscheinlicher, dass die Objekte sich in einem LAN irgendwann selbst koordinieren können. Dies verlangt jedoch neue HW und neue BS.

Bevor Objekte in DBS abgebildet werden können, benötigt man zunächst ein in sich konsistentes konzeptionelles Modell. Dies kann ein OOA-Modell oder ein ERM sein, wichtig ist, dass dabei die statischen (Gültigkeitsbedingungen an Objekte) und dynamischen (Einschränkungen der Operationen) Integritätsbedingungen definiert werden, und dass die Objekte, ihre Beziehungen und ihre Methoden beschrieben werden. Nachteilig an Relationen ist, dass Objekte mit Beziehungsattributen "verschmutzt" werden, und dass bei Erweiterungen neue Beziehungstabellen zu entwickeln sind, was die Flexibilität einschränkt.

Realisiert wird die externe Datenhaltung i.d.R. durch DBS, auch wenn hier mit Impedance Mismatch (Reibungsverluste) beim Zugriff und Speichern gerechnet werden muss. Fragen: Wie die Operationen auf die Objekte speichern, wie die Beziehungen und Strukturen ablegen, welches Datenmodell anwenden, welche Zugriffssprache (SQL) verwenden, wie die Objektidentität sicherstellen, wie verschiedene Versionen verwalten? Zur Beantwortung dieser Fragen sind Zugriffs- und Benutzerprofile zu erstellen, (objektorientierte) DBS (z.B. ObjectStore) oder gar BS (NeXTStep) zu begutachten, Nebenläufigkeit-Methoden zu studieren, usw.

Wir unterscheiden objektabhängige und klassenabhängige Persistenz. Bei beiden Persistenz-Typen müssen transiente bzw. transient gedachte Objekte den Funktionsoverhead der persistenten Objekte mitführen. Heute wird der Einfachheit halber i.d.R. nur eine klassenabhängige Persistenz geboten. Surrogate für die Objektidentität können hier durch Object-Server bereitgestellt werden. Objekte können sich selbst speichern (objektorientiert gedacht), sich selbst speichern lassen (nicht mehr so stark objektorientiert), oder sie werden gespeichert (kaum noch objektorientiert). Letztere Methode wird über einen Object-Server erreicht, der friend der persistenten Klassen sein muss, um auf deren innere Struktur zugreifen zu können.

Speichermöglichkeiten von persistenten Objekten mit Vererbungsstrukturen. Beispiel:

       /Student\
 Person         HiWi
       \Angest /
  1. Repeat Class Model: Jede Basisklasse wiederholt sich in den abgeleiteten Klassen. Die Objekte werden in allen Klassen abgespeichert, in denen sie vorkommen.
  2. Universal Class Modell: Es wird eine Universal-Klassen-Tabelle aus allen Attributen aller Klassen gebildet.
  3. Leaf Overlap Model: Wie bei Repeat Class Model, aber die Objekte werden nur in ihrer "Blatt"-Klasse abgelegt.
  4. Split Instance Model: Die Instanzen werden aufgesplittet, sodass jede Klasse nur die eigenen Attribute führt. Dadurch verteilt sich ein Objekt zwangsläufig auf alle Klassen, in denen es vorkommt.

Speicherung von Beziehungen und Aggregations-Strukturen:

  1. 1:1-Beziehungen (Fahrzeug-Lenkrad): Eine Klasse führt als Attribut die Schlüsselattribute der Beziehungsklasse.
     Fahrzeug: #FID Hubraum LID Lenkrad: #LID Form
                 1  1100    2             1   rund
                 2  2000    1             2   oval 
    
  2. 1:n-Beziehungen (Liste-Element): Wie die 1:1-Beziehung, nur dass Objekte, die Mehrfach-Beziehungen eingehen, auch mehrfach in der Relation gebildet werden.
     Liste: #LID Titel EID Element: #EID Inhalt
              1  Gemüse 1             1   Bohnen
              1  Gemüse 2             2   Karotten
              2  Obst   3            3   Äpfel 
    
  3. m:n-Beziehung (Fahrzeug-Strecke): Hierfür wird eine eigene Relation gebildet, die aus den Schlüsselattributen beider Klassen besteht. Zu beachten ist, dass nirgendwo Nullwerte geführt werden dürfen (da sich diese keinen Objekten eindeutig zuweisen lassen).
     Fahrzeug: #FID Hubraum Strecke: #LID Start
                 1  1100           1  Mannheim
                 2  2000           2  Hannover
    
     Fahrzeug-Strecke: #FID #LID
                         1    1
                         1    2
                         2    1
                         2    2
    

Statt in RDBS können Objekte auch in normalen Textdateien gespeichert werden, die z.B. eine "getaggte" Strukturnotation verwenden (ähnlich ASN.1). Auch die Auflösung von Objekten in binäre Relationen ist denkbar; dadurch geht zwar jegliche Strukturinformation verloren und man erhält eine Unzahl von Relationen, doch sofern die Objekte ihren eigenen Aufbau kennen, können sie alle Informationen selbst zusammensuchen. Auch bei Benutzung eines Object-Servers ist es nicht ungewöhnlich, wenn Objekte sich über istream- oder ostream-Objekte selbst laden/speichern, während die Surrogateerzeugung (z.B. über eine static-Variable) dem Server obliegt. Die Persistenz kann in den Programmen durch simple "#ifndef"-Direktiven an und aus gestellt werden.

36. FELDTYP

Es gibt keine Felder der Grösse Null, keine Felder mit Referenzen, keine Felder mit void-Pointern und keine Felder mit Funktionen (aber Funktionspointern!). Achtung: C++ prüft keine Feldergrenzen. Der Bezeichner eines Feldes ist nicht modifizierbar, d.h. direkte Zuweisungen der Form "char ch[2]; ch="a";" sind nicht möglich, im Gegensatz zu "char *cp; cp="a"". An Pointer können auch Felder zugewiesen werden, aber an Felder keine Pointer (Feldbezeichner sind konstant)! Zeiger sind verbiegbar, Felder nicht.

Wann immer der Compiler auf einen Feldbezeichner mit Indexoperator stösst, wandelt er diesen sofort in einen Pointer auf das erste bzw. angegebene Element um. Seltsam ist, dass Grössenargumente als Funktionsargumente NICHT konstant sein müssen!

 void f(int size){char ch[size];} // OK

In DOS und WINDOWS sind Felder auf ein einzelnes 64 kB-Segment beschränkt. Unter UNIX können sie erheblich grösser sein.

Indizierte Zeiger sind Zeiger, die mit dem Indexoperator versehen wurden, um die gewöhnungsbedürftige Zeigerarithmetik zu umgehen. Im Gegensatz zu Feldbezeichnern sind indizierte Zeiger jedoch jederzeit änderbar. Für eine komplette Simulation von Feldern durch indizierte Zeiger sollten diese const deklariert werden. Damit erhält man die wohl einzig sinnvolle Verwendungsmöglichkeit von const-Pointern! Aber: Indizierte Zeiger zeigen stets in den Heap, Felder auf den Stack.

Kritik: Die Grösse, die konstant sein muss, eines Feldes ist nicht überall feststellbar - z.B. innerhalb von Funktionen, bei denen das Feld als Argument übergeben wurde (da es Compiler implizit in Pointer gewandelt hat). Zuweisungen an Feldbezeichner sind nicht möglich im Gegensatz zu Pointern, da Feldern der Zuweisungsoperator fehlt.

x[2][2]={1,2,3} bewirkt x[0][0]=1, x[0][1]=2, x[1][0]=3. Es wird also immer von hinten nach vorne aufgefüllt. Nur die Angaben für die erste Dimension kann entfallen, damit der Compiler die Offsets berechnen kann. Auch bei Deklarationen ohne Definition kann die Grösse weggelassen werden. Operatoren können mit Feldern nichts anfangen (ausser "sizeof" und "&"), sie werden daher stets in Pointer umgewandelt: "a[0]" wird sofort zu "*ap", und "a[3]" sofort zu "*(a+3)". Da [] links-assoziativ ist, gilt: a[1][1]=x; <==> (a[1])[2]=x;

Ein Pointer "fp" auf ein Feld "int f[5][3]" ist folgendermassen zu erstellen: "int (*fp)[3];", d.h. der zweite (und dritte ...) Index ist mit anzugeben. Grund: Der Compiler muss für die Zeigerarithmetik die Offsets kennen. Dies erlaubt die Zugriffsformen auf das Feld[2][2]: "fp[2][2]=1; (*fp)[2][2]=1; (*(fp+2))[2]=1; (*(*(fp+2))+2)=1;"

37. FRIEND

Welchen Sinn könnten es haben, wenn eine Basisklasse friend ihrer abgeleiteten Klasse ist? Im unten stehenden Beispiel kann nur A ein Objekt von B erzeugen, da es als einzige Klasse Freund von B ist. Das macht A in gewisser Weise zu einer abstrakten Objektklasse. Dies alleine wäre auch durch eine protected-Ableitung ohne friend zu erreichen gewesen. Aber dann könnten auch von A abgeleitete Objekte auf A zugreifen. Dies geht bei friends nicht, da diese nicht transitiv sind.

class B{
  friend class A;
  B();
  void f();
};
class A:private B{f();}; // OK
class C:public B {f();}; // FEHLER

Warum verstossen friends nur bedingt gegen das Data-Hiding? Sie werden vom Programmierer der Klasse vergeben (also nicht vom späteren Anwender). Sie sind nicht transitiv, wirken also nur eine Ebene weit (von der mit friend versehenen Klassen abgeleitete Klassen haben nichts davon!).

friends gehören NICHT zum Geltungsbereich der Klasse, weswegen sie auch einen Objektverweis als Argument übergeben bekommen müssen, bzw. ein Objekt im Rumpf deklarieren müssen. Auf einen solchen Objektverweis kann nur verzichtet werden, wenn ausschliesslich auf static-Member zugegriffen werden soll.

Wenn die Wahl zwischen friends und Elementfunktion besteht, so sollte man sich i.d.R. für Elementfunktionen entscheiden, insbesondere wenn Manipulationen am Objekt vorgenommen werden. Dadurch werden auch die globalen Namen klein gehalten. Ausserdem müssen virtuelle Funktionen, Konstruktoren und Destruktoren Elementfunktionen sein. Für friend-Funktionen spricht eigentlich nur z.T. die "natürlicherer" Schreibweise: Statt x.f() schreibt man dann f(x).

Können friend-Funktionen virtuell sein? Wenn sie Elementfunktionen sind und in der zugehörigen Klasse als virtuell deklariert sind, sind sie virtuell. Normale Funktionen können nicht virtuell deklariert sein. Jedoch können an sie Objekt-Referenzen übergeben werden, die dann innerhalb der Funktion den virtual-Mechanismus benutzen. Das macht sie zu quasi-virtuelle friend-Funktionen.

38. FUNKTION

Funktionen sind abgeleitete Datentypen. Ihre formalen Argumente und Rückgabewerte werden über Initialisierungen realisiert, nicht einfache Zuweisungen. Ihr Geltungsbereich ist lokal, d.h. sie sind nur temporär existent. Häufig sind die Compiler intelligent genug, um zu erkennen, ob es genügt, temporäre Variablen nur in der CPU zu erzeugen und für die Dauer ihrer Existenz zu speichern.

Aktuelle Argumente, die hart im Codesegment stehen, z.B. "void f("t")", besitzen keine Adressen, die der Compiler zum Zeitpunkt der Kompilierung ermitteln könnte. Wird nun aber als formales Argument eine Referenz gefordert, z.B. "void f(char& c)", hilft sich der Compiler dadurch, dass er im Stack eine temporäre Variable für "t" erzeugt.

Die formalen Argumente werden beim Funktionsaufruf mit den aktuellen Argumenten INITIALISIERT, wobei es also zu (Standard-)Konversionen kommen kann. Ausserdem können Werte an const-Variablen zugewiesen werden! Auch der Rückgabewert wird initialisiert; die Funktion ist dabei nur als lvalue zu verstehen, wenn eine Referenz zurückgeliefert wird. Wenn eine Funktion nie aufgerufen wird und auch ihre Adresse nirgendwo zu berechnen ist, so muss sie nicht definiert werden!

39. FUNKTIONSWERT

Bei der Rückgabe von Funktionswerten ist eine Kopie meistens unvermeidbar. Gibt man nur eine Referenz auf das lokale Objekt zurück, liefert man Unsinn zurück, da das Objekt mit Verlassen des Funktionsrumpfes zerstört wird. Erzeugt man auf dem Heap ein Objekt, so kann man eine Referenz darauf zurückgegeben. Allerdings besitzt das Objekt dann keinen Namen mehr, kann also nicht mehr gelöscht werden! Aus diesem Grund gibt man am ehesten Pointer auf Heap-Objekte oder Kopien von Objekten zurück.(?)

40. FUNKTIONSZEIGER

Im Gegensatz zu den meisten anderen Sprache lassen sich in C++ sogar Zeiger auf Funktionen realisieren, d.h. Zeiger erzeugen, die in das Codesegment zeigen. Aufgrund der Standardkonversion lassen sich Funktionszeiger auf verschiedene Arten initialisieren und benutzen:

int f(int &i){return i;}
void main(){
  int (*zf)(int&); // oder typedef (*ZF)(int&); ZF zf;
  zf=&f;           // Initialisierung. Kürzer: zf=f;
  (*zf)(2);            // Aufruf. Kürzer: zf(); aber NICHT: *zf()
}

Der Spezifizierer "typedef" kann verwendet werden, um Funktionszeiger mit einfacheren Namen zu versehen. Diese Datentypen können auch als Klassennamen in Klassen-Template-Funktionen verwendet werden (es können die Funktionszeiger aber auch direkt innerhalb der eckigen Klammern angegeben werden).

void f(){cout << "hello";}
template<class A>class AA{
  A fz;
  void f(){fz();}
};
void main(){
  typedef void (*ZF)();
  AA<ZF>a;                // Aufruf über typedef-Datentyp
  a.fz=fz;
  a.f();
  AA<void(*)()>b;         // ebenfalls korrekt.
}

Vor dem Aufruf müssen Funktionspointer normalerweise mittels "*" dereferenziert werden. Zusätzlich ist ihr Namen zu umklammern, da "()" eine höhere Priorität besitzt als "*". Es gilt aber auch die einfacherer Schreibweise wie folgt:

 double f(int &i,char *cp){/*...*/}
 void main(){
  double (*FP)(int&,char*)=f;
  (*FP)(1,"tst1"); // formal korrekt
  FP(2,"tst2");  // auch OK
 }

Für Funktionspointer existieren keine Standardkonversionen,. ausser die nach void*. Konversionen müssen hier also benutzerdefiniert werden.

41. GELTUNGSBEREICH

C++ kennt vier verschiedene Geltungsbereiche, die das statische System betreffen, und abhängig sind vom Ort der Deklaration:

  1. Lokal: Alle in einem Block eingeschlossenen Deklarationen, die nicht Klassen- oder Funktionsgeltungsbereich besitzen. Auch Funktionsargumente gehören zu diesem Geltungsbereich.
  2. Global (Datei): Alle in keinen Block eingeschlossenen Deklarationen. Ihre Anzahl sollte aufgrund der Gefahr von Ambiguitäten mit anderen Programmdateien gering gehalten werden. Sinnvoll sind hier v.a. typedefs und Funktionen.
  3. Funktion: Ausschliesslich Label-Deklarationen für z.B. goto-Sprunganweisungen innerhalb der aktuellen Funktion.
  4. Klasse: Alle Deklarationen innerhalb eines Klassen- bzw. Struktur-Deklarationsrumpfes. Der Zugriff darauf erfolgt indirekt über Elementfunktionen und/oder direkt über den Punkt-, Pfeil- oder Geltungsbereichs-Operator.

In Zukunft sollen sich noch sogenannte Name-Spaces einrichten lassen. Damit lassen sich über vollständige Qualifizierungen eindeutige globale Variablen in sichererer Weise als bisher realisieren. Beispiel:

 namespace X{void f(const int&);};
 void main(){X::f(2);};

42. GOTO

Die Sprunganweisung goto springt zu Marken mit dem Geltungsbereich Funktion, d.h. man kann mit ihr nur innerhalb der Klassendefinition herumspringen. Zu beachten ist, dass die Marke vor der Sprunganweisung deklariert wird.

43. HIC

Zum Designen der Benutzerschnittstelle sollten Benutzer-Profile erstellt werden. Vor älteren Anwendern muss z.B. die Komplexität verborgen werden (alte Sekretärin vor MS WORD!). Ein Trennung der Benutzergruppen in Anfänger, Fortgeschrittene und Experten ist meist sinnvoll (eine HIC für nur eine Benutzergruppe zu schreiben ist einfach, aber eine für alle schwierig). Bestimmte Konventionen sind einzuhalten, z.B. das Datumsformat. Die Sprache sollte aus dem Problembereich stammen (kein EDV-Jargon). (Kontextsensitive) Hilfssysteme erleichtern den Umgang mit dem Programm. Fehler sollten dokumentiert werden (keine Nummern wie bei OS/2!). Ideal sind Human Interface Components, die in den Sitzungen lernen, sich auf ihre Anwender anzupassen (bieten z.B. auf Knopfdruck das Laden bestimmter Files an). Wichtig ist die Feedback-Gabe; der Anwender erfährt so gleich, ob seine Operation erfolgreich war.

Folgende Dialogformen werden unterschieden:

  1. benutzergeführte Dialoge: Der Anwender bestimmt frei, was er als nächstes tun will. Z.B. bei Kommandosprachen gegeben.
  2. systemgeführte Dialoge: Eine begrenzte, kontextsensitive Auswahl wird angezeigt. Ist bei Menüs und Eingabemasken gegeben.
  3. gemischte Dialoge: Der Anwender hat eine begrenzte Auswahl von Möglichkeiten, nachdem er sich durch Mausklick für ein bestimmtes Objekt entschieden hat. Z.B. bei WINDOWS gegeben. Der Erfolg, z.B. Löschen eines Icons, wird direkt angezeigt, d.h. Feedback ist unmittelbar gegeben.

Es gibt 8 goldene Regeln für das Dialogdesign:

  1. In Menüs Operationen logisch gruppieren.
  2. Hot Keys für Profis.
  3. Konsistenter Aufbau und Dokumentation.
  4. Kontrolle der Benutzer-Inputs. Keine Gedächtnisüberforderung.
  5. Fehler dokumentieren, nicht nur nummerieren. UNDO-Option.
  6. Feedback-Gabe des Systems.
  7. Menütiefe und -breite beschränkt.
  8. Hilfssystem (kontextsensitiv).

44. HILFSSYSTEM

Hilfssysteme gelten als Extra, wenn sie nicht direkt in das Programm designt wurden und selbstständig aufgerufen werden müssen. Ansonsten spricht man von integrierten Hilfssystemen, die i.d.R. kontextsensitiv auf Fehler und Hilfsanfragen reagieren. Bei Extra-Hilfssystemen gibt es welche, bei denen man sich über Indizes zur gesuchten Information navigieren muss. Es gibt auch welche, die Suchfunktionen anbieten, die gezieltes Anfragen gestatten. Zuletzt gibt es auch welche, die aus dem Programm gestartet werden können und dann kontextsensitiv Hilfen aufweisen - und diese Alternative ist sicher die beste.

45. HÜLL-KLASSE

Wrapper-Klassen werden um externe Systeme gelegt, um so deren Funktionalität (und nur ihre Funktionalität!) im eigenen System verfügbar zu machen. Beispiel: Generierung einer Sensor-Klasse, die einen physikalischen Sensor simuliert.

46. IFIP-Modell

Eine HIC-Architektur, bestehend aus 4 Teilen. Die E/A-Einheit (das, was man sieht, z.B. Pulldowns) wird getrennt betrachtet von der Werkzeug-Einheit und der Dialog-Einheit (Regelwerk, Fehler-Handling) auf dem Rechner und der Organisationseinheit (Umsetzung des Problembereichs; Integration der Werkzeuge). Jede Einheit kann für sich extra entwickelt und bewertet werden. Vorläufer des Seeheim-Modells. Wie dieses ist das IFIP-Modell nur ein Beschreibungsmodell, kein Implementationsmodell wie der MVC.

47. IMPLIB

Neues Konzept bei Borland C++: Statt dass Objekte aus Bibliotheken in statischer Weise als Ganzes in EXE-Programme gelinkt werden, erkennt der Linker bei Verwendung der IMPLIB die für das Programm relevanten Teile und linkt nur diese zum EXE-Programm, welches dadurch deutlich kürzer wird. Wird z.B. eine Elementfunktionen im Programm nie aufgerufen, so muss auch ihre Definition nicht in den Codeteil kopiert werden. Der Compiler betrachtet nur die aktuelle Programmdatei, während der Linker das ganze Programm sieht; er bestimmt, ob für eine Variable einmal oder mehrfach Speicherplatz zu allokieren ist.

48. INDEXOPERATOR

Welchen Vorteil bietet das Überladen des Indexoperators? Es erlaubt, auch Nicht-Integers als Indizes zu übergeben. So könnte z.B. ein double als Index benutzt werden, deren Integer-Teil und Nachkomma-Teil verschiedene Auswahloptionen bieten könnten. Auch mehrstellige Indizes sind auf diese Weise möglich. Der Rückgabewert ist beliebig.

Da an Indizes von Objekten i.d.R. auch Zuweisungen möglich sein sollten, muss deren Funktionsrumpf einen this-Zeiger zurückliefern. Da ein solcher Funktionsrumpf nicht const deklariert sein kann, bringt ein Aufruf der Form "cout << a[4];" Probleme mit sich, da "cout" ein const-Objekt voraussetzt. Dies lässt sich umgehen, indem der Indexoperator noch einmal überladen wird: Er behält seinen Rumpf bei, aber die Operatorfunktion wird const deklariert ("int X::operator[](double d) const;").

49. INITIALISIERUNG

Initialisierungen sind nicht mit Zuweisungen identisch, da mit einer Initialisierung die Allokation von Speicherplatz einhergeht, während eine Zuweisung einer Variable nur den Wert einer vorher allokierten Speicherzelle zuweist. Zuweisungen an const-Variablen sind nicht möglich, aber Initialisierungen im Moment ihrer Definition. Der vielleicht wesentlichste Unterschied ist aber, dass Initialisierungen den Konstruktor aufrufen, während Zuweisungen mit dem Zuweisungsoperator arbeiten!

Bei der Übergabe von Argumenten an Funktionen findet eine Initialisierung statt und nicht nur eine Zuweisung! D.h. es werden Variablen zur Aufnahme der aktuellen Argumente des Aufrufs erzeugt und deren Werte in diese kopiert. Der Geltungsbereich der formalen Argumente ist dann lokal.

Eine Klasse, die keine static-Datenelemente, keine Basisklassen, keinen Konstruktor und keine virtuellen Elementfunktionen enthält und die vollständig public spezifiziert ist, kann über eine Initialisierungsliste initialisiert werden: "struct X{int i;}; X x[]={{"1"},{"2"}};".

Initialisierer-Listen bewirken nicht, dass die Objekte in der dort angegebenen Reihenfolge initialisiert werden. Nur die Reihenfolge der Deklarationen ist ausschlaggebend. D.h. man muss darauf achten, dass in einer Klasse die Datenelement-Deklarationen vertauscht werden, damit die Initialisiererliste korrekt abgearbeitet wird (weil z.B. die Initialisierung einer Variable die Initialisierung einer anderen Variable voraussetzt).

50. INFORMATION HIDING

Nur die Elementfunktionen einer Klasse haben unbeschränkten Zugriff auf Klassenelemente, während andere Zugriffsmöglichkeiten durch Zugriffsrechte näher spezifiziert werden können. Der Anwender einer Klasse sieht nur das von der Klasse, was er auch sehen soll.

51. INFORMATIONSFLUSS

Der Informationsfluss ist eine Metrik, die sich auch für objektorientierte Programme eignet. Unterschieden wird hier ein globaler Informationsfluss von A nach B (Objekt A ändert z.B. C, auf das B zugreift) und ein lokaler Informationsfluss von A nach B (A benutzt Methoden von B, A greift über C auf B zu). Informationsflüsse, die in eine Klasse eingehen, heissen fan-in, Informationen, die nach aussen getragen werden, fan-out. Fan-in und fan-out geben die Komplexität einer Klasse wieder. Es kann eine Trennung zwischen statischer und dynamischer Sicht stattfinden.

52. INLINE

Dieser Spezifizierer vor (Element-)Funktionen bewirkt, dass der Funktionsrumpf an jede Stelle eines Aufrufs der Funktion kopiert wird. Intelligente Compiler erkennen sogar statische Wertzuweisungen und setzten diese direkt ein. Sowie im Programm irgendwo die Adresse einer inline-Funktion verrechnet wird, schaltet der Compiler die inline-Funktion aus (an der internen Bindung ändert sich dadurch jedoch nichts).

inline bewirkt in jedem Fall, dass die Funktion interne Bindung erhält - auch wenn die Funktion letztlich nicht wirklich inline vom Compiler realisiert wird. Grund: Es existiert kein Coderumpf mehr im Codeteil, dessen Adresse nach aussen exportierbar wäre. Das bedeutet auch, dass der inline-Funktionscode bei mehreren Programmdateien MEHRFACH erzeugt wird, wenn inline nicht aktiviert wird.

inline-Definitionen, die nicht in Headern untergebracht sind, müssen in allen Programmdateien 100%ig EXAKT wiederholt werden. Diese Kontrolle obliegt alleine dem User, da durch die durchgehenden inline-Definitionen eine Klasse interne Bindung erhält, der Compiler und Linker also diese Klasse jeweils nur isoliert für eine Programmdatei betrachten können.

53. I/O-OPERATOREN

C++ verfügt über keinen eingebauten I/O-Operatoren, sondern diese werden über eine Bibliothek nachgeladen. Als Ausgabe- und Eingabe-Stream kann jeweils nur eine Instanz "cout" und "cin" erzeugt werden, d.h. wenn mehrere Tasks (oder Threads) Ausgaben tätigen, sollte das "cout"-Objekt zuvor über Semaphore allokiert werden müssen.

Ist die Wahl ">>" und "<<" für die Eingabe- und Ausgabeoperatoren klug gewesen? Ja, denn sie haben eine niedrige Priorität (dies erlaubt Ausgaben von Objekten ohne Klammersetzung), sie sind mnemonisch (ihre Funktion ist bereits visuell verständlich), sie sind symmetrisch (Eingabe genau umgedreht zur Ausgabe), und sie sind links-assoziativ (bei cout << a << b; wird a vor b ausgegeben).

54. ITERATION

Hierzu gehören while-, do- und for-Anweisungen.

55. KLASSEN-TEMPLATES

Parametrisierten Klassen können in den eckigen Klammern Klassenschablonen zugewiesen werden, die NIE innerhalb der Klassendeklaration bzw. Klassendefinition auftauchen. Dies ist bei Template-Funktionen NICHT möglich.

Die Klassenschablonen, die an eine Klasse übergeben werden, können dazu benutzt werden, um als Klassennamen für Template-Klassen-Deklarationen zu dienen.

Die Übergabe von Template-Klassen als Klassennamen geht nicht direkt, da sich der Compiler über die doppelten eckigen Klammern beschwert. Dieses Problem lässt sich aber über eine typedef-Definition umgehen.

template<class A>class AA{A a;};
template<class B>class BB{B b;};
void main(){
  typedef AA<int> CC;
  BB<CC>bb;               // OK
  BB<AA<int>>bb;    // Fehler
}

Möchte man eine NORMALE Klasse von einem Klassen-Template ableiten, so muss im Klassenkopf der Versionstyp des Klassen-Templates durch Angabe des gewünschten Datentyps angegeben werden. Ist die abgeleitete Klasse jedoch ebenfalls parametrisiert, so kann die Typ-Angabe auf den ersten Aufruf der abgeleiteten Klasse verlagert werden, sofern im Klassenkopf die Klassenschablone an die Basisklasse weitergegeben wird.

template<class A>class AA{A a;};
template<class A>class BB:AA<A>{A a;};
void main(){BB<int>bb;};

Im Gegensatz zu Template-Funktionen können bei Klassen-Templates Konstruktoren mit Default-Argumenten korrekt arbeiten. Voraussetzung ist jedoch, dass der Default-Parameter sich IMPLIZIT in den gewählten Klassennamen konvertieren lässt. Da dies bei benutzerdefinierten Datentypen praktisch nie der Fall ist, ist von Standard-Konstruktoren mit Default-Parametern bei Klassen-Templates generell abzuraten. Übriges: Funktioniert die Konversion nicht implizit, beschwert sich der Compiler über das Problem, auch wenn der Default-Parameter niemals aktiviert wird!

Werden Elementfunktionen von Klassen-Templates nicht im Deklarationsrumpf definiert, so muss ihren Definitionen das Schlüsselwort "template" vorangestellt werden. Der Name der Klassenschablone ist dabei unabhängig von der Klassenschablone der Klassen-Deklaration. Diese "outline"- Definitionen können auch dazu benutzt werden, um für bestimmte Klassennamen spezielle Versionen der Elementfunktion zu erzeugen (dies entspricht dem Überladen bei nicht-parametrisierten Klassen). Zu beachten ist, dass dabei auf "template" zu verzichten ist!

template<class A>class AA{A a;int b;A f(AA*);};
template<class B>B f(AA<B> *aa){cout << aa->a;return aa->a;}
int f(AA<int>*aa){cout << aa->b;return aa->b;}
void main(){
  AA<double>a;
  a.f(&a);      // Ausgabe von a.a
  AA<int>b;
  b.f(&b);      // Ausgabe von b.b
  a.f(&b);      // FEHLER, weil keine Konvertierung möglich ist.
}

An Klassentemplates können in den eckigen Klammern auch konstante Grössen mitgeteilt werden. Beim Aufruf müssen diese jedoch als konstante Typen übergeben werden.

 template<class T,int size>struct X{int size;T *inhalt;};
 void main(){X<char,5>x;}

Die Verwendung von ">" bei Template-Deklarationen ist nicht immer glücklich gewählt. Grund: ">>" wird als Input-Operator gewertet, und muss mit Whitespace getrennt werden. Beispiel:

 template<class T>class X{int f(T*);};
 template<class T>class Y{int g(T*);};
 void main(){
  X<int>x;  // OK
  Y<X<int>>y1; // Fehler
  Y<X<int> >y2; // OK!
 }

Eingebettete Klassen-Templates sind verboten, also: "template<class T>class X{template<class T>class Y{};};" ist ein Fehler. Aber natürlich dürfen Klassen-Templates eingebettete normale Klassen führen.

56. KOHÄSION

Die Kohäsion gibt wieder, inwieweit eine Klasse nach innen wirkt, wobei hier auch ganze Vererbungsstrukturen zu der Klasse gehörend empfunden werden. Die Kohäsion ist möglichst ausgeprägt zu designen, d.h. Methoden sollen eigene Methoden verwenden, Klassen auf eigene Methoden oder vererbte Methoden zurückgreifen. Grund: Information Hiding. Problematisch ist hier die selektive Vererbung anzusehen, da sie nicht echt "is_a"-Strukturen modelliert.

57. KOMMA-OPERATOR

Über Komma-Operatoren lassen sich mehrteilige Anweisungen in einer Zeile realisieren. So liefer z.B. folgendes Programm eine "5" zurück: "int t; cout << (t=3,t+2) << endl;".

58. KONSTANTE

Zeiger auf Konstante Objekte können verändert werden, so lange nicht schreibend auf das referenzierte Objekt zugegriffen wird.

 void f(const char *p){cout << p++ << endl;} // OK

Neben symbolischen Konstanten, die durch den Spezifizierer const deklariert werden, gibt es Literalkonstanten, die sich durch ihre Schreibweise als Konstanten auszeichnen, und zwar derart, dass der Compiler i.d.R. selbstständig ihren Typ erkennen kann. Zu beachten ist, dass der Typ solcher Literalkonstanten maschinenabhängig ist: So hat "66000" auf den PC den Typ "long int", während auf UNIX-Kisten nur ein "int" herausspringt. Der Typ kann aber auch durch eine Typ-Endung spezifiziert werden: "L "steht für "long", "U" für "unsigned". Der Wert kann dezimal (erste Ziffer!=0), hexadezimal (1.Ziffer=0, 2.Ziffer=x) oder oktal (1.Ziffer=0) dargestellt werden. Es gilt, dass Gleitpunktzahlen ohne Typ-Endung immer vom Typ "double" sind; durch abschliessendes "F" werden sie zu "float", durch "L" zu "long double".

59. KONSTRUKTOR

Ein Objekt einer abgeleiteten Klasse kann erzeugt werden, auch wenn der Konstruktor der abgeleiteten Klasse nicht explizit den Konstruktor der Basisklasse(n) aufruft. Dies funktioniert aber nur, sofern die Basisklasse über einen Default-Konstruktor verfügt, denn nur diesen kann der Compiler für die abgeleitete Klasse implizit erzeugen.

Ein Konstruktor mit dynamischer Speicherallokation lässt sich elegant folgendermassen realisieren:

struct X{
  int size;
  char Inhalt;
  X(int i){inhalt=new char[size=i];}
  ~X(){delete[]Inhalt;}
};

Im Konstruktor-Kopf - in der Initialisiererliste - wird initialisiert, im Konstruktor-Rumpf wird zugewiesen. Bei der Initialisierung wird anders als bei der Zuweisung einer Variable nicht nur ein Wert zugewiesen, sondern auch entsprechender Speicherplatz für sie allokiert. Bei dynamischer Allokation findet die Initialisierung oft auch innerhalb des Funktionsrumpfes statt. Übrigens: Initialisiert werden können nur Variablen der aktuellen Klasse und der DIREKTEN Basisklasse.

struct X{
  int i;
  int &ri;
  const char *str;
  char *fp;
  X(int a, int b, char *txt):ri(b), str(txt){
    i=a;
    fp=new char[i];
  }
};

Die Aufrufreihenfolge von Konstruktor und Destruktor verdeutlicht folgendes Beispiel:

struct X{X(){cout << "x" << endl;}~X(){cout << "-x" << endl;}};
struct A{A(){cout << "a" << endl;}~A(){cout << "-a" << endl;}};
struct B:A{B(){cout << "b" << endl;}~B(){cout << "-b" << endl;}};
struct C:B{X x;C(){cout << "c" << endl;}~C(){cout << "-c" << endl;}};
void main(){
  C c;        // Ausgabe: a,b,x,c,-c,-x,-b,-a
}

60. KONTROLLFLUSS

Hierzu gehören if- und switch-Anweisungen.

61. KONVERSION

Um benutzerdefinierte Datentypen konvertieren zu können, müssen i.d.R. die zugehörigen Operatoren überladen werden. Im folgenden Beispiel wird X in einen Integerwert konvertiert:

struct X{
  int i;
  operator int(){return i;}      // KEIN Rückgabewert vorne angegeben!
};
void main(){
  X x;
  cout << (int)x << int(x) << x; // liefert immer x.i
}

Nicht jede Konversion lässt sich über den Konstruktor realisieren, da dieser nur Objekte des eigenen Typs zurückliefern kann. Konversionsfunktionen liefern dagegen den Typ zurück, den sie überladen (es kann kein Funktionswert explizit angegeben werden).

Eine Konversion von einen Pointer auf einen anderen Pointer funktioniert nur dann vollständig, wenn der neue Pointer mindestens genauso viele Bits aufnehmen kann wie der alte Pointer. Konversionen kommen (implizit) zum Einsatz bei Initialisierungen, z.B. bei der Übergabe lokaler Argumente an formale Argumente einer Funktion und bei der Rückgabe von Funktionswerten, und bei Zuweisungen.

Bei der Standardkonversion wird eine Zuweisung den Typ zugewiesen erhalten, der am längsten ist. Dass heisst, "long int + int" liefert "long int" als Ergebnis. Das ist sehr praktisch und effizient.Leider wird NICHT von "long int" nach "unsigned long int" und nach "long double" konvertiert.

Es kann auf 3 Arten explizit konvertiert werden:

  1. Cast-Version: double d=(double)2;
  2. Funktionsversion: double d=double(2);
  3. Spezifizierer-Versionen:
    • double d=static_cast<double>2; Achtung: Liefert nur bei Referenzen Lvalues!
    • const_cast um const für den aktuellen Aufruf zu entfernen. Sinnvoll z.B. um const-Heap-Objekte zu löschen!
    • reinterpret_cast um C++-Typ-Prüfung auszuschalten!
    • dynamic_cast für typsichere Laufzeitprüfung (alle anderen Casts sind statisch!). Auch RTTT genannt. Liefert eine Null zurück, wenn der Cast misslingt.

Der Ausdruck "4u + 2.0l" hat den Typ "long double"!

62. LAUFZEIT-OPTIMIERUNG

Gehört zum Design der Problembereichskomponente und stärker noch zur OOP. Folgende Massnahmen lassen sich hier treffen:

  • const: Das "Verconsten" von Programmen erleichtert und vertieft wesentlich die statische Fehlersuche.
  • register: Optional versucht der Compiler (temporäre) Variablen in die schnellen Register abzulegen, z.B. Schleifen-Indizes. Insbesondere RISC-Rechner haben über 100 Register.
  • inline: Optional fügt der Compiler den Funktionsrumpf an die Stelle des Aufrufs ein. Dadurch spart man sich den Aufwand der Stack-Ablegung der Argumente. Der Code verlängert sich durch die Redundanz jedoch u.U. erheblich. Vorsicht auch bei mehreren Programmdateien, denn die explizite externe Einbindung von inline-Code führt zur Duplizierung desselben, auch wenn der Compiler aufgrund der Grösse des Codes die normale Funktionsaufruf-Version beibehält!
  • abgeleitete Attribute:
    • - "Cache": Speichervariablen einrichten, die zu setzende Werte wie Grössen eines Feldes enthalten, damit dieser Wert nicht jedes Mal berechnet werden muss. Nachteil: Wartung aufwendig.
    • 20:80-Regel: Nur 20 des Codes werden zu 80% der Laufzeit benötigt. In erster Linie ist dieser Codeteil zu optimieren (und verifizieren).
    • virtuelle Funktionen gezielt einsetzen. Die Vererbung der Schnittstelle und evtl. (Default-)Implementierungen kann bei der statischen Fehlersuche hilfreich sein.
    • statt Indexoperatoren Zeigerarithmetik verwenden.
    • Die neuartigen BOOL-Variablen kennen nur zwei Werte und werden sehr schnell abgearbeitet.
    • Auch der Spezifizierer "mutable" ist brandneu. Dadurch lassen sich Variablen erzeugen, die innerhalb von const-Elementfunktionen veränderbar sind!
    • Container-Klassen: Man hat hier in der Implementierung die designabhängige Alternativen zwischen Typ-Sicherheit, Codeduplizierung und Programmieraufwand.
    • Referenzen-Zählen: Über diese Methode lässt sich Speicherplatz sehr effizient nutzen. Statt mehrfacher Allokation und Freigabe wird einmal allokiert und mehrfach belegt und erst am Ende freigegeben.
    • Klassen für systemnahe Aufgaben: Speziell auf die HW ausgerichtete Klassen, die evtl. sogar in Assembler codiert sind, können die Geschwindigkeit eines Systems wesentlich erhöhen. Das geht auf Kosten der Portabilität (!!) und sollte ähnlich wie Treiber ordnungsgemäss verkapselt werden. Auch Vereinfachungen wie C-Funktionen in eine C++-Klasse packen, gehört hier dazu (Beispiel DIR-Klasse).
    • Verwendung von friends: Die friends brechen nur beschränkt die Kapselung auf, erlauben aber u.U. sehr effizienten Zugriff auf Datenkonstrukte, sowie designspezifisches Verhalten von Funktionen.

63. LISTEN-IMPLEMENTIERUNG

Für die Grösse einer Liste sollte ein Datenelement gewartet werden. Vorstellbar wäre darüber hinaus ein Flag der Form "grösseOK", dass bei jeder Listenänderung auf "FALSE" gesetzt wird.

Wird die Elementfunktion "getSize()" aufgerufen, kann über das Flag geprüft werden, ob die Liste durchzuzählen ist oder ob direkt "anzahl" zurückgegeben werden kann.

Deklariert man "anzahl" und "grösseOK" als "mutable int", dann können diese Werte auch in const-Elementfunktionen verändert werden. Designtechnisch kann man so zwischen semantischem und bit-weisem "Consten" Unterscheidungen treffen. Achtung: Nicht jeder Compiler beherrscht die mutable-Deklarationen. Über "#ifndef"-Anweisungen muss also evtl. der this-Pointer kopiert und über "This->grösseOK=1" verändert werden, ohne die const-Elementfunktion zu verletzen.

64. LOC

Die Lines Of Code als Metrik zu benutzen, ist zu kurz gedacht. Ihr Einsatz in Formeln wie "Produktivität=LOC/(Aufwand*Zeit)" erscheint aber durchaus sinnvoll zu sein. Geklärt werden muss, was als Zeilen gilt: Auch Kommentare, logische oder "physikalische" Screen-Zeilen? Zu beachten ist, dass das LOC-Mass im hohen Masse Sprachabhängig ist (z.B. Assembler < C), und keine Hilfe für Reuse von Spezifikationen und Code darstellt.

65. LVALUE

Ein Lvalue ist ein Ausdruck, der auf eine Funktion oder ein Objekt verweist, d.h. auf eine bestimmte Speicherregion. Z.B. ist der Name eines Objekts auf der linken Seite einer Zuweisung ein Lvalue.

Lvalues können im Gegensatz zu Rvalüs veränderbar sein. Verschiedene Operatoren verlangen daher die Benutzung von Lvalues, ansonsten beschwert sich der Compiler. Lvalues wie const-Objekte oder Funktionspointer sind nicht veränderbar. Ein Funktionsaufruf ist nur dann ein Lvalue, wenn als Funktionswert eine Referenz auf ein Objekt zurückgeliefert wird.

Alle Präfix-Operatoren liefer Lvalues zurück, weshalb möglich ist: "int i; ------i;". Die Postfix-Operationen liefern dagegen einen Rvalue zurück. Erst nach dem Aufruf wird per Seiteneffekt der Objektwert inkrementiert/dekrementiert. Daher ist nicht möglich: "int i; i----;".

66. MATCHING

Das überladen von Funktionsnamen ist die eine Form des Polymorphismus, die es vermutlich nur in C++ bzw. C gibt. Welcher Funktionsprototyp letztlich mit welchem Aufruf aktiviert wird, legt der Compiler STATISCH fest, d.h. zur Kompilierungszeit. Dabei gilt das Motto: ÜBERSETZUNGSFEHLER SIND BESSER ALS LAUFZEITFEHLER! Um die beste Funktion für einen Aufruf zu ermitteln, geht der Compiler folgendermassen vor:

1.Schritt: Der Compiler sucht nach exakter Übereinstimmung der aktuellen Argumente mit den formalen Argumenten. U.U. kommt es auch zu TRIVIALEN Konversionen, wie z.B. von "int" nach "int&", da sich hier am Bitmuster nichts ändert (int bleibt int, wird nicht zu void, char o.ä.).

2.Schritt: Auch wenn 1.Schritt erfolgreich war, sucht der Compiler nach Mehrdeutigkeiten, um sie bitterlich beklagen zu können. Ohne den Verlust von Genauigkeit nimmt er eine TYP-ANGLEICHUNG der aktuellen und formalen Argumente vor, so z.B. von "char" nach "int" (weil char<int).

3.Schritt: Der Compiler schuftet weiter. Jetzt betreibt er die eingebaute STANDARDKONVERSION, die auch mit Informations-Verlusten leben kann. Aus "long int" wird ruck-zuck "int", aus "double" wird "int" usw. Besonders wird die "0" behandelt, die nun auch als Null-Pointer interpretiert wird. Und jeder Pointer wird auf den void-Pointer konvertiert!

4.Schritt: Der Compiler berücksichtigt auch die Arbeit der Programmierer und nimmt eine BENUTZERDEFINIERTE Konversion der Argumente vor. Im einfachsten Falle ruft er dazu Konstruktoren mit EINEM Argument auf.

5.Schritt: Zu guter Letzt sieht sich der Compiler noch nach UNSPEZIFIZIERTEN Argumenten um, d.h. um Funktionsprototypen mit "..." als formale Argumente. Der 5.Schritt gilt immer als schlechteste Alternative, d.h. wenn 1-4 ausreicht, wird 5 nicht beachtet!

Etwas kompliziert wird der Matching-Prozess noch durch das Vorhandensein MEHRERER Argumente. Hierbei verfährt der Compiler nach der Strategie, dass die Funktion genommen wird, die in jedem Punkt mindesten gleich gut wie jede andere Funktion ist, in wenigsten einem Punkt jedoch besser ist. Übrigens: Der FUNKTIONSWERT spielt beim Matching überhaupt keine Rolle!

67. METRIK

Bei der Analyse von Metriken wird folgendermassen vorgegangen:

  1. Definition der Metrik: Formel mit Erklärung der Komponenten.
  2. Grundlegende Annahmen: Erläuterung, über was das Mass Aussagen macht. Anschliessend eine Behauptung der Form: Je mehr dieses Mass vorliegt, desto mehr/weniger ... Besonders auf Komplexität, Cohesion und Coupling eingehen. Hinweis auf dynamische Aspekte geben. Die fixen Bestandteile der Formel sind zu begründen.
  3. Ziele bei Einsatz: Wiederholung der Behauptung von (2), darüber hinaus aber auch weitere Behauptungen ableiten, in der Form: Das Mass erlaubt Rückschlüsse über ... und ..., dies hat Auswirkungen auf ... und ... Eingehen auf den Standpunkt der Programmierer. Aufdeckung von Schwachstellen in Analyse/Design als besonderer Vorteil.
  4. Richtwert/Idealwert: Wenn Richtwert okay, dann Verbindlichkeit oder Faustwert-Charakter (Indikator-Wirkung) herausarbeiten. Fallunterscheidung, wenn möglich. Wenn Mass schwierig quantifizierbar ist, dann Hinweis auf fehlende Untersuchungen oder generelle Unmöglichkeit, sowie der Hinweis, dass das Mass im relativen Vergleich eingesetzt werden kann, dann aber eine Abweichung kontextspezifisch zu interpretieren ist. Daraus resultierende Probleme für Empfehlungen wie "So viel wie nötig, so wenig wie möglich" andeuten.
  5. Anwendbarkeit: Hinweis auf alle Formelteile, die nicht eindeutig sind, Fragen dazu, um sie näher einzugrenzen. Kosten-Nutzen-Relation beachten. Auf Abweichungen von der OOE-Philosophie besonders hinweisen. Liegen solche vor, kann es kein alleiniges Mass sein.
  6. Einsetzbarkeit im SW-Lebenszyklus: Frühesten Einsatzpunkt unter welchen Bedingungen erläutern. Es gilt, dass Analyse und Designphase als früh gelten. Beachtung von statischen (Klassen-) und dynamischen (Objekt-)Versionen. Wenn erst in OOP-Phase erzeugbar, dann Hinweis auf evtl. Sprachabhängigkeit. Abschliessend den einfachsten Fall - z.B. die direkte Ableitung aus dem Analysemodell - beschreiben (aber auch dadurch anfallenden Beschränkungen). Wenn erst später Einsatz möglich ist, die Idee von Schätzwerten und Erfahrungswerten analysieren.
  7. Anmerkungen/Erweiterungen: Plausibel machen der Behauptungen aus (2) und (3), aber Hinweis auf fehlende Sektoren. Daraus neue Synthese bilden (i.d.R. Kombination der Masse, Normierung der Komponenten, sinnvolle Vereinfachungen). Unplausible Formelbestandteile kritisieren. Bemerkung der Form "auch wenn Idealwert erreicht, dann ..." machen. Gegenüberstellung von Kosten und Nutzen, v.a. auch unter dem Gesichtspunkt der Erweiterung.

Bei der Berechnung von Metriken muss extra darauf hingewiesen werden, wenn in der Formel benutzte Komponenten aus dem Analysemodell nicht ersichtlich sind. Kandidaten hierzu sind z.B. einfache Methoden oder fehlende Nachrichtenverbindungen. Werden abgeleitete Klassen geprüft, sind für sie auch die höher stehenden Klassen einzubeziehen (z.B. bezüglich der Methodenanzahl oder Nachrichtenverbindungen)! Übrigens:Metriken sind v.a. nur dann sinnvoll, wenn sie in CASE-Tools (z.B. MAOOAM) direkt eingebaut sind! Dadurch können Qualitätsnormen eingehalten werden, die erst ein SW-Engineering ermöglichen.

Sicher sinnvolles Ziel: Übertragung des Function-Point-Verfahrens und des COCOMO-Kostenschätzung-Modells auf OOE! Bei ersterem werden die Funktionen gezählt und ihre Komplexität geschätzt, sowie mit ihrer Wichtigkeit gewichtet. Diese Werte können über firmeninterne Erfahrung in Personen-Monate umgerechnet werden. COCOMO berücksichtigt dagegen, dass verschieden ausgebildete Teams verschiedene Kosten bei der SW-Entwicklung verursachen (Schätzwerte), und zudem zwischen Basic-, Intermediate- und Detailed-COCOMO unterschieden.

68. METRIK-KLASSIFIKATION

Die verschiedenen Metriken lassen sich - wenn auch nicht immer eindeutig - folgendermassen klassifizieren: Zunächst sind Metriken als Qualitätsmasse nur ein Bestandteil der Bewertung von SW-Engineering-Prozessen. Man unterscheidet hier Produkt-, Prozess- und Ressourcen-Qualitätsmasse. Unser Interesse gilt in erster Linie Produktmassen, die sich in objektorientierte und nicht-objektorientierte Produktmasse trennen lassen. Die ooPQM betrachten entweder Klassen, Objekte oder ganze Systeme. Sie werden in strukturelle und interne Masse getrennt: Erstere betrachten die Beziehungen, Nachrichtenverbindungen und Aggregationsstrukturen zwischen Klassen, das Zweite das Information Hiding und die Vererbung einzelner Klassen. Die Qualitätsmasse lassen sich auch weiter trennen in direkte und indirekte Masse, wobei für uns nur indirekte Masse (wie Informationsfluss) relevant sind, die sich aus direkten Massen (wie die LOC) zusammensetzen. Die Qualitätsmasse lassen sich zuletzt in bewertende und vorhersagende Masse unterteilen. Erstere Masse betrachten den konkreten Fall, während das Zweite auch auf zukünftige Aspekte (wie Reuse) eingehen.

69. MUTEX

Mutex ist ein Semaphor, der knappen Ressourcen wie "cout" vor Mehrfachzugriff schützt. Threads können ein Mutex-Objekt mit eingebetteter Lock-Klasse erhalten. Sowie ein Thread ein Lock-Objekt erzeugt hat, hat es exklusiven Zugriff. Nach der Zerstörung von Lock ist Mutex wieder frei. Beispiel:

 struct MeinTask:public Thread{
  void run(){
   for(int i=0; i<5;i++){
    {
     Mutex::Lock l(mtx); // Blockiere, bis Lock erzeugt
     cout << ID << endl;
    } // zerstöre Lock
    sleep(1);
   }
  }
 };
 Mutex mtx;
 void main(){
  MeinThread t1,t2;
  t1.start(); t2.start();  // starte Tasks
  t1.terminationAwaited(); t2.terminationAwaited();
 }

70. MVC

Die Model-View-Controller-Architektur schreibt vor, wie ereignisgesteuerte Programme erstellt werden sollten, wodurch Programme mit einer interaktiven Oberfläche versehen werden. View und Controller bilden jeweils ein Paar, das auf bestimmte Model-Komponenten abgestimmt ist. Die Model-Komponente muss daher nichts über die Datenrepräsentation oder Eingabeverarbeitung wissen, sondern kann sich voll auf die Problemlösung konzentrieren.

* Problematisch am MVC ist, dass für jede Model-Komponente spezielle View-Controller-Paare aus den Standardtypen abzuleiten sind. Dies führt rasch zu einer "Explosion" von abgeleiteten Klassen. Stark abgemildert wurde dieses Problem durch die Einführung von Template-Parametrisierung. Dadurch bedingt muss aber die Model-Komponente nun wissen, mit welchen Datentypen sie bei welchen Eingaben zu reagieren hat, d.h. die Trennung zwischen Modell und Oberfläche wird zwangsläufig etwas verwässert.

Die HIC-Architektur MVC erleichtert das Reuse, erlaubt er doch die Modellierung von View- und Controller-Komponenten unabhängig von einer Model-Komponente. Das erlaubt die Entwicklung von Oberflächen-Bibliotheken. Ein Model verfügt i.d.R. über viele Views. Views und Controller gehen meist eine 1:1-Beziehung ein - tatsächlich ist die Controller-Komponente oft implizit in die Views eingearbeitet, verfügt daher nicht immer über eigene Klassen (die konzeptionelle Trennung rührt eigentlich nur aus SMALLTALK her). Oft stellt der Controller nicht mehr dar, als eine Ewigschleife, die alle Events (z.B. Drücken von F1) abfängt, und darüber die Views und evtl. das Model informiert.

Als Werkzeuge für das Oberflächen-Design kommen infrage:

  • Tools, die aus OOA-Modell automatisch Oberfläche generieren. So werden abstrakte Klassen z.B. zu Menütiteln, die abgeleiteten Klassen zu Menüpunkten, Aggregationsklassen zu Fenster, die Zerlegungsklassen zu Unterfenstern, usw.
  • 4GL
  • Application Frames: Bibliotheken mit View- und Controller-Klassen, die ein typisches Look&Feel der HIC erzwingen. Im Gegensatz zu UIMS können damit neben der Oberfläche aber durchaus auch Applikationen programmiert werden. Beispiele: OWL von Borland für WINDOWS; XVT für virtuelle APIs, wodurch es z.Z. für 7 BS die gleichen Oberflächen anbieten kann.
  • UIMS (Toolkit): Arbeiten interpretativ, d.h. entwickeln zur Laufzeit zu Application die passende Oberfläche nach dem Generator-Ansatz oder Editor-Ansatz (?). Die gegebenen Möglichkeiten sind aber schlecht erweiterbar, was die Anwendbarkeit von UIMS stark beschränkt (werden am Lehrstuhl nicht mehr eingesetzt). Beispiel: DevGuide von SUN für OpenWindows.

Nach Rumbaugh sollte jedes System nur die unbedingt nötige Komplexität nach aussen zeigen. Ein Interface hierfür ist zu erstellen, welches eine möglichst von der Anwendung unabhängige Oberfläche kontaktiert, die aus mehreren Einzelteilen besteht. Das Modell kennt nur die semantische Information, zu dessen Entwicklung jedoch der Problembereich bekannt sein muss. Danach kann ermittelt werden, was mittels Views anzuzeigen ist; falls vorhanden, können alte Klassen wiederverwendet werden. Zuletzt muss designed werden, wie Views und Modell miteinander interagieren. Die Controller überführen dazu die Benutzereingaben in passende Operationen. Jede Schicht kann also ziemlich für sich alleine entwickelt werden, auch wenn die Reihenfolge Modell - View - Controller einzuhalten ist. Es kommt zu keinen Zirkulationen. Am besten wird eine abstrakte Klasse für alle Modell-Objekte geschaffen, an die die Views und Controller Nachrichten versenden. Widgets sind Werkzeuge aus Application Frames, die frei verwendbare View-Controller-Einheiten darstellen.

MCV ist ein Implementationsmodell (Gegensatz zu Seeheim und IFIP), d.h. es werden hier auch programmtechnische Aspekte beschrieben. Neben einer Trennung von Oberfläche und Anwendung findet daher auch eine Trennung der Oberfläche in Interaktionsteil und Präsentationsteil statt. Das erlaubt eine effektivere Modularisierung bei der Programmierung.

71. OOA

Die OOA nach Coad und Yourdon ist ein geordneter Prozess, der aus folgenden Schritten besteht:

  • Erstellung statisches Modell: Hier werden die Schichten Klassen&Objekte, Subjekte, Attribute, Methoden und Strukturen aus der Problemspezifikation erstellt. Dadurch erhält man alle statisch relevanten Informationen bzgl. der Systemverantwortlichkeit.
  • Erstellung dynamisches Modell: Hier wird durch Szenarien, Ereignisfolge-Diagrammen, Objektfluss-Diagrammen (OFDs) und Zustands-Diagrammen der zeitliche Ablauf von Methodenaufrufen des Systems beschrieben und geprüft. OFDs entsprechen in etwa einer SA ohne Data Dictionary, die i.d.R. nur 3 Ebenen tief sind.
  • Erstellung funktionales Modell: Mittels Struktogrammen und strukturierter Sprache (Pseudocode) werden die Algorithmen der Methoden des Systems wiedergegeben.

72. OOA-MCK-TRANSFORMATION

Moderne Ansätze sehen vor, aus dem statischen Analyse-/Designmodell automatisch MCK-Prototypen zu generieren. Folgende Richtlinien scheinen dabei sinnvoll zu sein:

  • Klassen: Klassen => Menütitel+ 2 Fenster (Einzelobjekt-Fenster, List-Fenster); Klassenname => Fenstertitel; Attribute => I/O-Felder, Methoden => Buttons für Aktionen.
  • Vererbungsstrukturen: Basisklasse => Menütitel, wenn nicht abstrakt, dann auch erster Menüpunkt; abgeleitete Klassen => Menüpunkte (bzw. Untermenüpunkte); in den zugehörigen Fenstern werden nach Möglichkeit die vererbten Eigenschaften mit angezeigt; wenn mehrstufig, dann Fenster IN Fenster IN Fenster; wenn multiple, dann Fenster NEBEN Fenster IN Fenster.
  • Aggregation/Beziehungen: Aggregation => Menüpunkt + Fenster; Teil => 2 Fenster (Einzelteil, List-Fenster); wenn Strukturbeziehung optional, dann auch Teil als Menüpunkt aufnehmen; zwischen den Fenstern von Objekten, die eine Beziehung eingehen, kann direkt gewechselt werden; existenzielle Aggregationsstrukturen bei der Generierung neuer Teilfenster zu beachten.
  • Subjekte: Subjektname => Menütitel im Hauptprogramm; durch Aufruf der Menupunkte gelangt man in Unterprogramme mit Menütiteln, die aus den dort vorhandenen Objekten bestehen.

73. OPEN-CLOSED-PRINZIP

Das OCP ist für Klassenbibliotheken einzuhalten. Es bedeutet, dass Klassen offen für Erweiterungen über Vererbungen sein sollen, aber geschlossen gegenüber Änderungen ihres Innenlebens. Erreicht wird dies durch niedriges Coupling und hohe Kohäsion. Dies garantiert eine hohe Wiederverwendbarkeit der Klassen.

74. PATTER

Immer wiederkehrende Klassen(-Gruppen) in Problembereichen werden als Pattern bezeichnet. Solche Klassen können z.B. sein: Person, Rolle, usw. Sie sind allgemeingültig (nicht nur für OOA und OOD, sondern auch für OOP) und daher effektiv für Black Box Reuse einsetzbar. Meist bestehen sie aus einer Vererbungsstruktur bzw. Verbund von 2 bis 5 Klassen (aber auch einfache Beziehungen sind möglich). Sie müssen unabhängig von Anwendungen generiert werden. Meist verfügen sie über eine eigene Speicherverwaltung mittels new und delete. Besonderheiten wie Referenzenzählen oder Umschlag-für-Briefe-Funktionen können sie besonders wertvoll machen.

Typische von Schader aufgeführte Patterns sind:

  • Gruppierung-Mitglied: Die Gruppe führt die Gesamtfunktionalität, z.B. "koordinieren", während die Mitglieder individuelle Fähigkeiten besitzen. Dasselbe gilt für die Status.
  • Gegenstand-Beschreibung: Besitzen viele Klassenobjekte die gleichen Attributeausprägungen, so können diese in einem Beschreibungsklasse ausgelagert werden. Das spart Speicherplatz.
  • Instrument-Ereignisregistrierung: Erzeugen Objekte einen Output, der über längere Zeit relevant ist, so kann dieser in einem Ereignisnotierungsobjekt gesichert werden.
  • Objekt-Beziehungsbeschreibung-Objekt: Zur näheren Beschreibung von Beziehungen zwischen Objekten, lassen sich Beschreibungsobjekte einführen.
  • Darsteller-Rollen: Personen verfügen über Grundattribute, darüber hinaus aber auch besondere Fähigkeiten, die sie nur in bestimmten Rollen ausleben können. Es ist sinnvoll, diese Rollen extra zu modellieren.

75. PERSISTENZ

Mit der Persistenz von Objekten ist in erster Linie ein Identitätsproblem verbunden. Auch nach längerer Speicherzeit muss sichergestellt sein, dass jedes Objekt eines Systems über sein - für den Programmierer transparentes - Surrogat eindeutig identifiziert werden kann.

Implementierung: Meist wird eine spezielle Klasse PERSISTENZ mit den Elementfunktionen save()/load() gebildet, die als Basisklasse für alle persistenten Klassen herangezogen werden kann. In EIFFEL z.B. lässt sich dies leicht - ohne jede Codeänderung - durch die Klasse HERE realisieren, von der jede normale Klasse sowieso automatisch abgeleitet ist. Ein anderer Weg der Implementierung ist die Schaffung eines Servers/Containers, der Objekte laden und speichern kann. Dies ermöglicht insbesondere eine objektabhängige Persistenz, d.h. Objekte einer Klasse können auch transient bleiben. Zu klären ist weiterhin, ob Objekte ihre Zustandsänderungen sofort auf Platte "durchschreiben" sollten, und ob neben einem atomaren Zugriff (dazu wird die ganze vernetzte Struktur in den Hauptspeicher geladen) auch ein Zugriff auf Interobjekte möglich sein sollte. Für beide Ideen gilt, dass ihre Nutzen nicht den dazu nötigen Aufwand rechtfertigen.

Persistenz verlangt eine eindeutige räumliche und zeitliche Identität der Objekte. Dazu müssen sie unabhängig von ihrem Zustand, d.h. Attributen, sein. Die Objekte müssen dazu u.a. mit verschiedenen Versionen ihrer selbst umgehen können.Die Integrität ist durch das Information Hiding relativ leicht zu realisieren. Durch Persistenz sollte sich an vorhandenen Systemen nicht viel ändern, d.h. die Objekte verhalten sich wie bisher, und jedes Objekt kann unabhängig von seinem Typ persistent sein. Man unterscheidet weiterhin die Persistenz durch Erreichbarkeit (die ganze vernetzte Struktur ist persistent) von der einfacheren expliziten Persistenz (nur Vererbungsstruktur ist persistent).

76. PFEILOPERATOR

Der Pfeiloperator "x->i", der einen Zugriff auf das Klassenelemente i des Objekts x erlaubt, ist nur eine Kurzschreibweise von "(*x).i".

77. POLYMORPHISMUS

Um über einen Basiszeiger eine Funktion der abgeleiteten Klasse aufzurufen, obwohl die Basisklasse eine gleichnamige Funktion definiert, muss diese Elementfunktion in der Basisklasse "virtual" sein!

struct A{void f(){}};
struct B:A{void f(){}};
void main(){
  B b;
  A *ap=&b;
  ap->f();      // Aufruf von A::f, nicht wie gewollt von B::f!
}

78. PREPROZESSOR

Der C-Präprozessor erlaubt die Einfügung von Zeichen (#include), eine bedingte Übersetzung des Codes (#ifnotdef) und die Benutzung von parametrisierten (!) Makros (#define).

79. PROBLEMBEREICHSKOMPONENTE

Erweiterung des statischen Analysemodells um abstrakte Klassen, abgeleitete Datentypen, aufgebrochene Attribute plus deren näheren Beschreibung, Aufnahme von Surrogaten, Änderungen der Kardinalitäten, usw. Die Coad/Yourdon-Notation erlaubt leider nicht alle OOP-spezifischen Besonderheiten darzustellen, z.B. virtuelle Basisklassen, selektive Vererbung, protected-Ableitungen, usw. Ein weitere Schritt ist die Untersuchung von Bibliotheken, ob diese nicht bereits passende (oder anpassbare) Objekte enthalten. Im Gegensatz zur Analyse geht das Design also stärker Bottom-up als Top-down vor (die OOP selbst funktioniert nach dem Gegenstrom-Prinzip). Um Objekte zu finden, muss eine solche Bibliothek Kriterien wie einheitliche Notation, Angebot von Suchfunktionen, Klassifizierungen, Analyse-/Design-Ergebnisse, usw. erfüllen. Hier sind dann jedoch zusätzlich Aspekte der Fehleranfälligkeit bzw. Fehlerhaftung und des Copyrights zu berücksichtigen. Beispiele: tools.h++, OWL, NIHCL, MACL (Mannheim Class Library), XVT.

80. PROGRAMMIERUNG

Man unterscheidet zwischen Programmieren im Kleinen und Programmierung im Grossen. Ersteres umschreibt die direkte Umsetzung von Spezifikationen in den Programmcode. Als Hilfsmittel ist hier nur eine Programmierumgebung (Editor, Compiler) bekannt. Doch erst beim Programmieren im Grossen, welches alle Phasen des SW-Lebenszyklus umfassen, kann die OOE ihr volles Potenzial ausfahren. De Marco behauptet: "Nur was sich messen lässt, ist kontrollierbar." Verlangt ist daher ein Clean Room Engineering, d.h. das fehlerlose Entwickeln von Anfang an, möglichst als Reengineering (Reuse) verstanden. Neuartige wissensbasierte CASE-Tools helfen bei der Wahl zwischen den verschiedenen OOP, OOD-Diagrammen und der Kostenabschätzung. Lower-/Upper-CASE-Tools unterstützen Programmierung im (Kleinen)/Grossen, doch nur die seltenen Integrated-CASE-Tools unterstützen auch die Programmierung im Vielfachen: Sie bringen eine komplette SW-ENTWICKLUNGSUMGEBUNG mit sich.

Die strukturierte Programmierung basiert auf den Prinzipien Sequenz, Iteration und Selektion, sowie auf der (einspurigen) EVA-Methode. Als Hilfsmittel sind v.a. Pseudocodes und Struktogramme zu nennen. Eine Erweiterung stellt das modulare Programmieren dar, welches bereits mit Import/Export von Schnittstellen und Data Hiding operiert. Bis zur objektorientierten Programmierung mit Vererbung, Polymorphismus und Information Hiding ist es da nicht mehr weit.

81. PROZESSORGANISATION

Folgende Stufen in der Prozessorganisation lassen sich identifiziert:

  1. Anfangsniveau: Jeder arbeitet individuell für sich. 85% der US-Unternehmen halten sich an dieser rückständigen Prozessorganisation.
  2. Wiederholbar: Es existiert eine informelle Organisationsnorm, die vorschreibt, wie es innerhalb der Firma gemacht werden sollte. Immerhin 12% der Unternehmen haben solche Normen eingeführt.
  3. Gemanagt: Die interne Prozessnorm wird von speziell abgestellten Managern auf Einhaltung überwacht. Nur 3% der Unternehmen haben den Sinn solcher Formalisierungsmassnahmen erkannt.
  4. Messbar: Die interne Firmennorm wird nicht nur von speziellen Managern geleitet, sondern ihre Ergebnisse werden einer laufenden Prüfung unterzogen, um so eine ständige Vergleichsbasis zu erhalten. Ebenso wie Niveau 5 ist dieses Niveau der Prozessorganisation so gut wie noch nicht implementiert worden.
  5. Optimiert: Die Ergebnisse, die die laufende Kontrolle der Prozesse liefert, werden benutzt, um rückkoppelnd Verbesserungen entwickeln zu können. So bildet sich mit der Zeit ein optimierter Ablauf der Arbeitsprozesse heraus.

82. REFERENZ

Um Pointer an Referenz-Variablen zuzuweisen, müssen diese zuvor über den Inhalts-Operator * dereferenziert werden. So kann man z.B. einer Referenz-Variablen die dynamische Speicheradresse einer Integer-Variablen zuweisen.

int &i=*new int; // new int=Typ int*, daher Dereferenzierung nötig
i=2;
cout << i;

Liefert eine Funktion eine Referenz auf ein Objekt zurück, kann der Funktion über den Zuweisungsoperator = ein Wert zugewiesen werden, der letztlich dem zurückgegebenem Objekt zugewiesen wird.

int& MAX(int &a,int &b){
  return (a>b)?a:b;
}
void main(){
  int a=2, b=3;
  MAX(a,b)=4;
  cout << b;      // liefert 4 zurück!
}

Der Typ void& ist nicht erlaubt! Eine Referenz ist in ihrer Ausprägung objektabhängig. Ebenso verboten sind Konstrukte wie TYP&* oder TYP&&. Erlaubt sind dagegen Referenzen auf Pointer von void-Objekten, also void*&!

Referenzen sind eine Art const-Pointer. Und ebenso wie diese können sie nur genau einmal initialisiert werden! Zuweisungen an Referenzvariablen bewirken keine weitere Initialisierung mehr, sondern eine Änderung des Objekts, auf den die Referenz verweist! Ihr Einsatzgebiet ist damit stark eingeschränkt, was Referenzen zu einer Art Bürger 2.Klasse macht.

Referenzen MÜSSEN initialisiert werden (wie auch konstante Zeiger). Als einziger Unterschied zu const-Pointern gilt, dass Referenzen direkt auf ihren referenzierten Inhalt verweisen, während const-Pointer dazu mittels * dereferenzieren müssen. Es gilt:

 int i=1; int &ri=i; ri+=21; // i wird um 21 erhöht.

T& ist IDENTISCH mit T& const! Mittels const T& können Rvalues, mittels T& aber nur Lvalues mit berechenbarer Adresse referenziert werden (in Blöcken erzeugt der Compiler für Literals eine temporäre Variable, sodass "int &i=1;" möglich ist, nicht jedoch im globalen Geltungsbereich, da sie hier fest im Code stehen und somit keine berechenbare Adresse besitzen). Die Adresse der Referenz kann niemals herausbekommen werden! Aus diesem Grund kann es auch keine Pointer oder Referenzen auf Referenzen geben, ebenso wenig Felder mit Referenzen. Auch über new können keine Referenzen erzeugt werden. Eine void&-Deklaration ist nicht zulässig.

Referenzen als formale Argumente simulieren call-by-values, d.h. die übergebenen Objekte können verändert werden (nur der Referenzzeiger ist const, nicht der Inhalt, auf den er zeigt), und grosse Objekte können so sehr schnell übergeben werden.

Warum ist "int &i=1;" ein Fehler? Weil "1" konstant ist, int& aber ein Lvalue erwartet. Okay ist dagegen "const int &i=1;".

Auf Referenzen existieren kaum Operatoren (nur sizeof und &). Alle Operatoren operieren auf die Objekte, die Referenzen referenzieren. Es gilt: "double &rd=d <==> double &const rd=d". Referenzen müssen mit einem Lvalue initialisiert werden, weswegen "int &ri=i++" ein Fehler ist. Werden Literale referenziert, erzeugt der Compiler temporäre Kopien der Literale, die er referenziert. So gilt: const char &rch="1"; <==> char tmp="1"; const char &rch=tmp;

83. REFERENZENZÄHLEN

Ein Objekt führt ein Attribut, welches zählt, wie oft auf das Objekt verwiesen wird. Ist der Wert Null, kann das Objekt gefahrlos gelöscht bzw. durch ein anderes Objekt überschrieben werden. Auf diese Art kommt es nie zu referenziellen Inkonsistenz, einmal allokierter Speicherplatz kann aber mehrfach belegt werden.

Mögliches Einsatzgebiet: Vektorklasse. Es ist nicht nötig, für jeden Vektor ein eigenes Objekt zu erzeugen, wenn Vektoren mit gleichen Werten anfallen. Statt durch den Copy-Konstruktor eine Kopie eines referenzierten Vektors anzulegen, wird nur der Referenzzähler des Vektors erhöht und seine Adresse zurückgeliefert. Aber Achtung: Eine Änderung an diesem Vektor bewirkt natürlich eine Änderung an "allen" Vektoren (den Programmen erscheint es ja so, als hätten sie jeweils eigene Vektoren erzeugt). Es empfiehlt sich daher, im Konstruktor zwischen Lesevektoren und Schreibvektoren zu unterscheiden; nur das Zweite erzeugen echte Kopien.

84. REUSE

Früher betraf das Reuse nur das Reuse von SW-ICs, die aus der SW-Krise helfen sollten. Heute versteht man darunter auch viel umfassender die Wiederverwendung von Analyse-, Design- und Dokumentationsergebnissen. Deshalb muss auch zwischen Bausteinkatalogen und Bausteinbibliotheken unterschieden werden: Nur letztere beinhalten neben der Spezifikation auch den Quellcode. Ziel des Reuse ist in erster Linie die Steigerung von Produktivität und Qualität, ausserdem erlaubt es die Verfolgung eines Bottom-up-Ansatzes! Beim Black Box Reuse können alten Klassen direkt eingebunden werden, während White Box Reuse Änderungen und Erweiterungen an der Klasse verlangen.

Warum hat sich das Reuse noch nicht allgemein durchgesetzt?

  • nicht-technische Problemfelder
  • psychologische Barrieren: Not invented here-Syndrome. Misstrauen gegenüber Entwicklung anderer. Suchaufwand für Reuse-Klasse ohne Findungsgarantie.
  • wirtschaftliche Barrieren: Mehraufwand ist teuer.
  • juristische Barrieren: Haftungsregeln, Urheberrecht.
  • organisatorische Barrieren: Wer verantwortlich? Wie Reuse organisieren? Ist ein Belohnungssystem einzurichten?
  • technische Problemfelder: Wie erkennt man SW-ICs? Wo bindet man das Reuse im SW-Entwicklungsprozess ein? Wie klassifiziert, spezifiziert, dokumentiert und findet man SW-ICs? Wie sichert man ihre hohe Qualität?

Minderung der nicht-technischen Problemfelder ist möglich durch ein Management, das Measurements (Kontrollsysteme) und Incentives (Belohnungssysteme) mithilfe von Metriken etabliert. Neben der Initialisierung wird die Population und Qualität der SW-ICs bewertet. Die Benutzer von SW-ICs müssen ebenfalls belohnt werden. Das Management verteilt in einem gross angelegten Reuse-Programm die Verantwortung auf Erzeuger, Administratoren, Gutachter und Benutzergruppen von/für SW-ICs, legt Analyse- und Entwurfswerkzeuge fest, und kümmert sich um die Schulung. Als Metriken kommen infrage: Shipped Source Instructions (LOC ohne Kommentare), Reused Source Instructions, Reuse Frequency (Nutzung meiner ICs durch andere), Part Value (Nutzung meiner Zeilen durch andere), Reuse Percentage (Grad der Wiederverwendung).

Die technischen Problemfelder wird durch allgemeine Zugänglichkeit der Reuse-Optionen gemildert. Folgende Alternativen stellen sich hier zur Verfügung: Funktionsbibliotheken (z.B. SPSS), SW-Schablonen, Generatoren (z.B. Ressourcen-Compiler von Borland C++), abstrakte Datentypen, Objektorientierung (Klassen, Vererbung, Polymorphismus, generische Klassen, Standardisierung durch OMG) und zuletzt ganzen Anwendungsarchitekturen (ähnlich SW-Schablone, nur dass mit Objekten aufgefüllt wird; -: Jeweils nur für bestimmte Problembereiche geeignet). Die Objektorientierung erscheint hier als der geeignetste Weg, um Black Box Reuse, White Box Reuse und Reuse for Design verwirklichen zu können. Doch zunächst müssen die allgemein verwendbare SW-ICs eines Problembereichs gefunden werden. Dies geschieht über eine Domain Analysis (horizontal genannt, wenn für mehrere Problemfelder, sonst vertikal), die mithilfe von Problembereichs-EXPERTEN durchgeführt wird. Dazu wird ein Projekt geplant, Daten gesammelt, analysiert, klassifiziert und evaluiert. Wichtig ist, dass die Qualitätskriterien Korrektheit, Allgemeinheit, Portabilität, Erweiterbarkeit, Integrierbarkeit und Selektierbarkeit beachtet werden. Klassifizieren kann man die ICs disjunkt-hierarchisch (was aber trotz evtl. Querverweise und XPS-Hilfe eine umständliche Navigations-Suche abverlangt) oder mittels Facetten, die durch Terme beschrieben werden, die die Such-Deskriptoren vorgeben. Die Facette "Qualität" setzt sich z.B. aus den Termen "gut", "sehr gut" und "ISO9000-zertifiziert" zusammen. So etwas lässt sich über RDBS ähnlich wie IRS realisieren. Ein Konzeptgraph, der Ähnlichkeiten zwischen den Klassen festhält, kann bei interaktiven Fragesystemen helfen. Der Verwendungskontext ist durch die Beziehungen, Aggregationen (Zusammenfassungen), Nachrichtenverbindungen, Vererbung und Versionsverweise gegeben. CASE-Tools können bei der Generierung von SW-ICs helfen, Konzepte wie RPC, CORBA, DCE usw. bei der Verteilung im Netz. Das solche (verteilten) Facetten-Bibliotheken keine Zukunftsmusik mehr sind, beweist IBM mit den Produkten ReDiscovery (nur KATALOG für OS/2 mit AUTOMATISCHER Beschreibungsgenerierung) und Corporate Reuse Environment (realisiert als DB2-Datenbank).

85. SCHIZOPHRENES OBJEKT

Bei einer Vererbung der Schnittstelle MIT Implementation, kann es durch Überschreiben von Funktionen zu schizophrenen Objekten kommen. Über einen "basierten" Zeiger gelingt es dann nämlich nicht mehr, die Funktion der abgeleiteten Klasse aufzurufen, da der bitterböse Compiler sich stattdessen lieber für die Basisklassen-Funktionen entscheidet.

 struct A{void f(){/*...*/}};
 struct A:B{void f(){/*...*/}};
 void main(){
  B *bp=new B;
  A *p=bp;
  p->f();  // FEHLER: Aufruf von A.f(), statt B.f()
 }

86. SCHNITTSTELLE

Die Schnittstelle einer Basisklasse kann auf drei Arten an abgeleitete Klasse vererbt werden:

  1. rein virtuell: Die abgeleitete Klasse muss die rein virtuellen Funktionen überschreiben (obwohl diese durchaus in der Basisklasse definiert sein können, was aber expliziten Zugriff mittels :: erfordert!). Sie erbt damit EXPLIZIT nur die Schnittstelle.
  2. virtuell: Die abgeleitete Klasse sollte die virtuellen Funktion überschreiben, kann jedoch auch auf eine Default-Implementation davon in der Basisklasse zurückgreifen. Neben der Schnittstelle wird hier auch Funktionalität vererbt.
  3. public: Die abgeleitete Klasse sollte die in der Schnittstelle aufgeführten public-Funktionen nicht überschreiben, sondern auf die - notwendige - Implementationen der Basisklasse zugreifen. Die abgeleitete Klasse erbt hier IMPLIZIT die Interface-Funktionalität der Basisklasse. Achtung: Gefahr der schizophrenen Objekte.

87. SEEHEIM-MODELL

Eine HIC-Architektur (Referenzmodell für Oberflächen) für UIMS. Zu bemängeln ist hier, dass für eine direkte Manipulation (z.B. verschieben eines Icons) u.U. alle Schichten zu durchwandern sind, da sich die Darstellung und der Zustand des Objekts gleichermassen ändern kann. Das Seeheim-Modell besteht aus vier Komponenten (nur 3 berühren die Oberfläche direkt):

  1. Präsentation: Darstellung der Oberfläche; NIMMT EINGABEN ENTGEGEN.
  2. Dialog-Steuerung: Führt Änderungen an Objekten nach oben und unten durch.
  3. Anwendungsinterface: Bietet abstrakte Anwendungsobjekte; heute oft in (2) integriert.
  4. Anwendung: Anwendungsobjekte; Ausgaben werden indirekt über (2) abgewickelt.

88. SELEKTIVE VERERBUNG

Neue OOP erlauben es, an abgeleitete Objekte nur die Teile der Basisklasse zu vererben, die diese auch wirklich benötigen. Hier liegt dann jedoch keine echte "is_a"-Beziehung vor, weshalb die selektive Vererbung der OOE-Philosophie widerspricht.

89. SPEICHERKLASSE

C++ kennt zwei Speicherklassen:

  1. Automatisch: Objekte dieser Speicherklasse werden vom Compiler nach dem Deklarationsaufruf auf dem Stack erzeugt und beim Verlassen des aktuellen Blocks oder des Programms automatisch zerstört.
  2. Statisch: Objekte dieser Speicherklasse werden nicht vom Compiler, sondern explizit vom Programmierer auf dem Heap erzeugt (und auch wieder gelöscht). Im Gegensatz zu automatischen Objekten können statische Objekte vor Verlassen einer Blockanweisung oder des Programmendes zerstört werden.

90. SPEZIFIZIERER

C++ kennt die Spezifizierer class, friend, typedef, extern, static, register, enum, inline, virtual, struct, union, public, private und protected.

91. SPRUNGANWEISUNG

C++ kennt folgende Sprunganweisungen:

 - goto identifier     // springt zu aktuellem Label.
 - continue;           // springt ans Ende von Schleifen
                       // und prüft Bedingungen.
 - break;              // verlässt aktuelle Anweisung
                       // OHNE Prüfung der Bedingungen.
 - return expression;  // springt zum Aufruf zurück.

92. STACK-IMPLEMENTATION

 template<class T>class Stack{
  T *anf, *end;
  int size;
 public:
  Stack(int s){anf=end=new T[size=s];}
  ~Stack(){delete []anf;}
  T pop(){return *--end;}
  void push(T a){*end++=a;}
  int Size(){return end-anf;}
 };

93. STATIC

In Klassen können neben Datenelementen auch Elementfunktionen static deklariert werden. Innerhalb solcher static-Element-Funktionen steht kein impliziter this-Pointer auf das aktuelle Objekt zur Verfügung. Tatsächlich muss gar kein Objekt existieren, um die static-Funktion über den Geltungsbereichs-Operator :: aufrufen zu können! Bei allen anderen Element-Funktionen muss mindesten ein Objekt davon erzeugt sein.

static-Objekte in Funktionen haben wie gewöhnliche Objekte dort lokalen Geltungsbereich. Ansonsten verhalten sie sich aber ähnlich wie globale Objekte, z.B. werden sie wie diese nicht auf dem Stack, sondern im Datenteil abgelegt. Und egal wie oft die Funktion durchlaufen wird, die static-Objekt-Definition und -Initialisierung findet nur beim ersten Mal Beachtung.

static-Datenelemente, static-Elementfunktionen und Konstruktoren können nicht virtuell sein. Warum eigentlich nicht? Der Grund dafür ist, weil der Virtual-Mechanismus einen einem Objekt zugewiesenen virtual table pointer benötigt, static-Members aber ebenso wie Konstruktoren (noch) kein tragendes Objekt anbieten können.

Der Spezifizierer static vor globalen Variablen bewirkt, dass deren interne Default-Bindung "externiert" wird. Fortan kann auf diese Variablen auch noch ausserhalb der Definitionsdatei zugegriffen werden.

Der Vorteil von static-Datenelementen gegenüber globalen Variablen ist, dass sie weniger Gefahr laufen, durch lokale Variablen verdeckt zu werden. Generell gilt, dass der globale Namensraum so klein als möglich zu halten ist.

static-Elementfunktionen werden nur mit extern bei der Deklaration spezifiziert, nicht auch noch bei einer ausserhalb liegenden Definition. Grund: static sorgt für eine externe Bindung der Klasse, die durch ein static im Definitionsbereich in eine interne Bindung umgewandelt werden müsste. Dies ist aber nicht möglich.

Eine static-Variable "static int i;" kann auf drei Arten mit "0" initialisiert werden: (1) static int i; (2) static int i=0; (3) static int i(0);

Template-Funktionen in Klassentemplates können nicht direkt mit static deklariert werden (der Compiler beschwert sich über Speicherklassen-Fehler). In der Klassendeklaration ist jedoch der Spezifizierer static möglich, sofern die Definition ausserhalb des Deklarationsrumpfes und ohne static erfolgt. Dadurch wird die Klasse extern gebunden.

94. STRUKTUR

In der Strukturschicht der OOA werden Generalisierungs-/Spezialisierungs- und Aggregations-/Zerlegungs-Strukturen unterschieden. Erstere liest man im Modell von unten nach oben mit "ist ein", das Zweite liest man von oben nach unten mit "hat ein". Bei Whole/Part-Strukturen ist dabei zu beachten, dass die angegebenen Kardinalitäten immer gelten müssen (und nicht nur für eine bestimmte, zufällige Ausprägung). Bei allen Strukturen ist zu prüfen, ob sie nicht auch einfach durch zusätzliche Attribute modelliert werden könnten!

Bei der Whole/Part-Struktur unterscheiden wir folgende typische Formen:

  1. Ganzes-Teil: Meist Kardinalität von 1,m bei Ganzes (=physisch Existentes).
  2. Container-Inhalt: Meist Kardinalität von 0,m bei Container
  3. Gruppierung-Mitglied: Meist Kardinalität von 0,m bei Gruppierung (=konzeptionell Existentes)

95. STRUKTURIERTE ANALYSE

Die SA besteht aus Datenflussdiagrammen (DFD), der Prozessspezifikation und einem formal aufgebautem Data Dictionary. Zunächst wird in einem Kontextdiagramm ein Hauptprozess mit all seinen Datenquellen/-senken und Datenflüssen dargestellt. Dieser Prozess wird in das DFD der Ebene 0 in die Prozesse 1, 2, ..., n aufgeteilt, und evtl. um Dateien ergänzt. Jeder Prozess kann in weiteren DFD in die Prozesse 1.1, 1.2, ... bzw 2.1, 2.2, ... aufgespaltet werden. Wichtige Konsistenzbedingung: Die Anzahl eingehender und ausgehender Datenflüsse eines DFD der Ebene i+1 stimmt mit der Anzahl eingehender und ausgehender Datenflüsse in den Prozess der Ebene i überein. Ziel ist es, jeden Prozess schliesslich auf jeweils einer DIN A4-Seite in natürlicher Sprache, Entscheidungstabellen (Wenn/Dann:Regeln) oder Entscheidungsbäumen beschreiben zu können (Prozessspezifikation; P-Spec, Min-Spec). Stärker formalisiert können dann die Datentypen und ihre Zusammenhänge in einem Data Dictionary wiedergegeben werden.

96. SW-SCIENCE-MASS

Das SW-Science-Mass ist eine Metrik für die Komplexität eines Programms. Es werden dabei die verschiedenen Operatoren und Operanden ihrer jeweiligen Häufigkeit in einem Programm gegenübergestellt und mit der Programmlänge multipliziert. Als objektorientierte Metrik ist ihr Einsatz nicht zu empfehlen, trotz ihrer Automatisierbarkeit.

97. SZENARIO

Szenarios gehören zur dynamischen Systemanalyse und beschreiben die Wirkung von Ereignisse auf Objekte, die sich in bestimmten Zuständen befinden. Es gilt, dass jedes in einem System mögliche Ereignis in mindestens einem Szenario berücksichtigt wird (und sei es nur, um darauf hinzuweisen, dass ein Ereignis keine Zustandsänderung der Objekte hervorruft, sondern ignoriert wird). Aus den Szenarien werden Ereignisfolge-Diagramme generiert, die zum besseren Verständnis das Zusammenspiel von Ereignisse, Zuständen und Aktionen noch einmal visualisiert wiedergeben. Die im Szenario erwähnten Ereignisse, Objekte und Methodenaufrufe müssen mit dem statischen Modell abgeglichen werden!

98. TASK-MANAGEMENT

Bei Betrachtung eines OOA-Modells sind Überlegungen anzustellen, ob asynchron arbeitende Objekte darin vorkommen müssen. Solche Objekte lassen sich als Task-Objekte betrachten, die voneinander völlig unabhängig sein können. Besonders bei Multitasking-BS werden solches Tasks interessant, wenn z.B. massiv externe Systeme auftreten, die über Hüll-Klassen verfügbar gemacht wurden.

Die Task-Erzeugung erfolgt über den von POSIX standardisierten Aufruf von fork(). Dies aktiviert einen neuen Prozess (Prozess dynamisch mit Signalmaske, Stack, Heap, Code, Umgebung wie Pfadnamen >> statisches Programm!). Nachteilig an fork() ist, dass er die ganzen Prozesse kopiert (Heavyweight-Konzept); nur über den Resultat-Wert auf dem Stack lässt sich feststellen, ob man sich in Vater (fork() liefer Sohn-PID) oder Sohn (fork() liefer 0) befindet. Stirbt der Vater, wird das Kind nur kurz zum Waisen, weil es vom Initialisierungsprozess (PID=1) adoptiert wird. Im Sohn-Teil kann über exec("Programmname") der Sohn-Code gestartet werden, damit der Vater diesen nicht unnötig mitschleifen muss. Achtung: Alle Einträge nach exec() werden ignoriert - nach der return-Anweisung stirbt der Sohn, und der (über "wait(&status)" wartende) Vater agiert weiter.

Moderne Application Frames wie die NIHCL realisieren ein Lightweight-Konzept. Hier wird die Umgebung nicht neu kopiert, sondern die alte von Vater und Sohn gemeinsam genutzt. Noch leichter arbeiten Thread-basierte BS wie z.B. OS/2, Solaris oder die KSR mit ihren 100 Prozessoren. "Sohn-Prozesse" sind hier nur neue Threads, die einem Vaterprozess zugeordnet sind. Sie benötigen keine eigene Umgebung, keinen eigenen Heap oder Datenteil, jedoch wegen der Interrupt-Abarbeitung einen eigenen Registersatz, sowie zur Abarbeitung von Aufrufen einen eigenen Stack. Die Thread-Kommunikation wird über den globalen Datenteil realisiert. Statt max. 20 Prozesse sind über 10.000 Threads erzeugbar! Voraussetzung dazu ist aber ein preemptives Scheduling, d.h. es obliegt alleine den CPUs, die Threads zu suspendieren, nicht ihnen selbst (obwohl ihnen diese Möglichkeit gegeben ist)!

99. TASK-OBJEKT

Ein Task-Objekt wird alternativ durch einen Prozess, einen abgeleiteten Prozess, oder - moderner - durch einen Thread gebildet. Im wesentlich jedoch identisch sind die Zustände nach dem POSIX-Standard, die ein Task-Objekt einnehmen kann:

  1. created: Nach Aufruf von Thread() wird ein Thread erzeugt. Da Threads unabhängig voneinander arbeiten, empfiehlt sich auch ihre jeweilige Einschliessung mit einem try-Block - so bedeutet ein Thread-Fehler noch nicht das Ende des ganzen Prozesses!
  2. running: Wird im Zustand created die Elementfunktion start() aufgerufen, gerät der Task in den running-Zustand und ruft run() auf. Übrigens: Auch der sleep()-Befehl wird in diesem Zustand abgearbeitet!
  3. suspended: Neben einer Suspendierung durch die CPU kann sich der Thread auch mittels suspend() selbst Schlafen legen. Dadurch gerät er vom Zustand suspended oder running in den Zustand suspended. Über ein von aussen kommendes resume() kann er wieder aufgeweckt werden, d.h. er geht wieder in den Zustand running über.
  4. finished: Ist der Thread abgearbeitet, gelangt er vom Zustand running in den Zustand finished. Zu beachten ist, dass das Task-Objekt in diesem Moment immer noch existiert, das selbst start() noch einmal aufrufbar ist!

Eigenen Threads sind von Thread abzuleiten und die rein virtuelle Funktion run() ist zu überschreiben. Werden kritische Ressourcen benötigt, muss jeweils ein Mutex::Lock-Objekt erzeugt werden. Informationen erhält MeinThread t über den Konstruktor. t.start() bewirkt die Erzeugung des Threads im BS, wobei es einen Zeiger auf this und Thread::beginThread() erhält. Das BS ruft sofort beginThread() auf, welches den übergebenen this-Pointer "basiert" und virtuell die Funktion run() aufruft - MeinThread ist damit im Zustand running:

 struct MeinThread:public Thread{
  int ticks;
  MeinThread(const int i){ticks=i;}
  void run(){Mutex::Lock l(mtx); sleep(ticks); cout << "Hurz";}
 };
 void main(){
  MeinThread t(5);
  t.start();
  t.terminationAwaited(); // teile t mit, dass main() auf sein Ende wartet
 }

Was ist mit dem Aufruf terminate()? In den meisten Thread-BS ist er nicht direkt vorgesehen, denn zu viele Gefahren sind damit verbunden: Offene Dateien bleiben offen, blockierte Ressourcen bleiben blockiert, usw. Er lässt sich relativ leicht simulieren.

Wie findet man die Task-Objekte? Man sucht nach Objekten, die nur zu bestimmten Zeiten aktiv sind. Oder die nach bestimmten Ereignissen aktiv sind. Man ermittelt die für sie angemessene Priorität (lohnt sich v.a. bei mehreren CPUs!). Beherrscht das BS keine preemptives Scheduling, so kann die Entwicklung eines Task-Koordinators sinnvoll sein.

Einige typischen Thread-Anwendungen:

  • Timed Task: Bekommt im Konstruktor Pointer auf Alarmfunktion und Weckzeit. Nach start() wird in run() bis zur Weckzeit gewartet und dann Alarmfunktion aufgerufen.
  • Periodic Task: Ähnlich wie Timed Task, nur dass run() Ewigschleife ist.
  • File Wait: Testet in run() bis zum Timeout, ob eine bestimmte Datei erzeugt wurde. Wenn ja, wird über Mutex::Lock ein Exklusiv-Zugriff besorgt, die Datei ausgegeben und terminiert.
  • Poll&Notify: run() bewirkt hier das regelmässige Pollen einer übergebenen Prüffunktion. Wenn Bedingung erfüllt, dann wird Event-Objekt, das direkt nach start() suspendiert wird, running gesetzt.
  • Task-Koordinator: Führt eine Liste<Thread*> und führt Methoden für alle darin aufgeführten Threads, z.B. setAllPrioritys(), suspendAll(), runAll(), add(Thread), remove(Tread), ...
  • Delay,Timer, Mischer: Delay ist ein Thread, der eine übergebene Zeit schläft. Timer ist ein Thread, der Delay setzt und nach dessen Abarbeitung über eine Empfänger-ID einen Ereignis-Namen aufruft [(Empfänger-ID*)t->Ereignis-Name()]. Mischer ist ein Objekt, welches mit Drehzahl=0 initialisiert wird. In mische() wird die Drehzahl hochgesetzt, und es wird an Timer der Ereignis-Namen setzte_Drehzahl übergeben. Timer wartet Delay ab und setzt Drehzahl wieder auf 0.

100. TEMPLATE-FUNKTIONEN

TEMPLATE-FUNKTIONEN müssen ALLE Klassenschablonen als formale Argumente erhalten. Nicht alle ARGUMENTE müssen aber parametrisiert sein.

Wird eine Klassenschablone NICHT als Argument übergeben, so kann sie auch nicht im Funktionsrumpf angewendet werden. Wird sie als Argument übergeben, so kann sie innerhalb des Klassenrumpfs zur Definition von NEÜN Variablen verwendet werden. Bei der Initialisierung INNERHALB des Funktionsrumpf funktionieren triviale Konversionen implizit. Andere Konversionen müssen explizit vorgenommen werden.

Die Übergabe der KLASSENNAMEN in eckigen Klammern (wie bei Klassen-Templates) ist verboten. Der Compiler erzeugt alleine aus den aktuellen Argumenten die Instanzen der Template-Funktionen.

An die Funktionsschablonen des Funktionskopfes dürfen zwar DEFAULT- Parameter übergeben werden, jedoch haben diese nicht die gewünschte Wirkung: Es müssen nach wie vor explizite Typangaben gemacht werden, damit der Compiler weiss, welche Version er zu erzeugen hat. Nicht-parametrisierte Argumente sind im Gegensatz dazu aber weiterhin defaultierbar.

Da Templates nur den Geltungsbereich Funktion (bzw. Klasse) besitzen, kann z.B. die Klassenschablone der Klassen genauso heissen wie die der Funktionen, auch wenn ihr letztlich verschiedene Klassennamen zugewiesen werden.

Beispiel für eine funktionierende Template-Funktion:

template<class X>X f(X x1){
  X x2=x1+5.2;  // funktioniert nur, wenn triviale Konversion möglich ist.
  return x2;
}
void main(){
  f(2);
  f(3.3);
}

101. THIS-POINTER

Der this-Pointer kann benutzt werden, um dynamische von statischen Konstruktoraufrufen zu unterscheiden, denn nur bei ersterem ist this gleich Null, weil noch nicht implizit Speicherplatz allokiert wurde.

struct X{
  X(){
    if(this==0)
      cout << "dynamisch"
    else cout << "statisch";
  }
};
void main(){
  X *x=new X;    // dynamisch
  X x;           // statisch
}

Der implizite this-Zeiger erfährt nie eine (automatische) Konversion, da er für den Compiler nicht sichtbar ist. Das ist v.a. bei überladenen Operatoren zu beachten, die als Elementfunktionen definiert wurden, die nicht static sind, oder die nicht als friend bzw. global definiert sind. Die Nicht-Konvertierbarkeit bringt es auch mit sich, dass solche Operatorfunktionen i.d.R. nicht kommutativ sind, d.h. das die Reihenfolge z.B. beim *-Operator zuerst den benutzerdefinierten Datentyp und dann das formale Argument fordert - umgedreht würde sich der Compiler beschweren.

Der this-Zeiger gestattet es dem Compiler, den Elementfunktionscode einer Klasse einmalig im Code-Segment abzulegen (sofern er nicht inline spezifiziert wurde). Denn nur über diesen Zeiger ist es möglich, dass die in den Elementfunktionen auftauchenden impliziten Datenelemente den richtigen Objekten zugewiesen werden können. Der this-Zeiger kann auch explizit hingeschrieben werden, wie er auch durch X:: substituierbar ist. Dies ist z.B. nötig, wenn ein Datenelement durch eine lokale Variable verdeckt wird.

102. TYP

In C++ unterscheidet man elementare Typen wie "int", "double" und "char" von abgeleiteten Typen wie Funktionspointern, Referenzen, Konstanten, Unions und benutzerdefinierten Typen (wie z.B. Klassen). Bisweilen werden letztere auch als eigenständige Typklasse aufgezählt.

Funktionen gelten in C++ als einfache abgeleitete Datentypen. Z.B. besitzt der Funktionsprototyp "int MAX(int a,int b)" den Datentyp "int(int,int)". Von Standardargumenten wird dabei übrigens stets abstrahiert.

103. TYPEDEF

Es existieren keine typedef-Definitionen - trotz des Namens. Ein typedef ist immer nur eine Deklaration, denn sie bringt nur einen neue Namen in ein Programm ein, ohne diesen Namen jedoch einen Speicherplatz zuzuordnen. Aus diesem Grund gelten typedefs auch als bindungslos (tatsächlich sind sie aber intern gebunden).

104. ÜBERLADEN

Wir eine Funktion mit anderen Funktionen überladen, erkennt der Compiler über die Argumente beim Aufruf, welche Version er anzuspringen hat. Dabei muss jedoch bedacht werden, dass der Überladungsmechanismus nur innerhalb gleicher Geltungsbereiche arbeitet. Werden lokalen Deklarationen vorgenommen, die vom Namen her die globalen überschreiben, so sind ALLE globalen Deklarationen dieses Namens ausgeblendet, unabhängig davon, wie sehr sie sich in den formalen Argumenten unterscheiden.

int f(int i){return i;}
void main(){
  int f();     // f(int) ausgeblendet
  f();         // OK!
  f(1);        // ERROR!
}
int f(){cout << "Hurz!;}

Warum können die Operatoren ".", "::", ".*", "sizeof" und "?:" nicht überladen werden? Für "?:" lässt sich im Zusammenhang mit benutzerdefinierten Typen keine sinnvolle Semantik finden. Die anderen Operatoren können nicht überladen werden, da sie als Operanden den jeweils benutzerdefinierten Datentyp automatisch zugewiesen bekommen.

In C++ können zwar die meisten Operatoren überladen werden, es können jedoch keine neuen Operatoren gebildet werden. Beim Überladen sind Stelligkeit und Assoziativität zu beachten. Als formales Argument benötigen sie einen Verweis auf den Datentyp oder sie führen einen impliziten this-Zeiger mit (bei Elementfunktionen gegeben). Default-Werte für Argumente sind bei überladene Operatoren nicht erlaubt. Alle Operatoren bis auf den Zuweisungsoperator werden weitervererbt.

Auch abgeleitete Datentypen wie "void*" lassen sich als Konversionsfunktionen überladen. Eine solche Funktion ist z.B. nützlich zum Ende erkennen in einer Listen-Implementation:

 operator void*(){
  if(head)return head;
  return NULL;
 }

Nur Funktionen können im gleichen Geltungsbereich überladen werden, d.h. mehrfach definiert werden. Mit Variablen ist dies nicht möglich, da hier Argumente oder ähnliches fehlen, um eine eindeutige Unterscheidung treffen zu können. typedef-Deklarationen sind übrigens als Unterscheidungskriterium ungeeignet, da sie nur neue Typ-Namen, aber keine neuen Typen in ein Programm einführen. "T*" ist zu "T*const" zu ähnlich, als dass das eine mit dem anderen überladen werden könnte. Die Überladbarkeit ist abhängig von der nicht-impliziten Initialisierbarkeit: "T*" und "const T*" sind z.B. als Argumente erlaubt, da "T*" nicht mit "const T*" initialisiert werden kann. Auch "int*i" und "int i[10]" sind zu ähnlich zum überladen, während "int*i" und "int i[][3]" okay ist.

Templates komplizieren noch das Matching der Argumente. Jedoch werden Templates nur bei exakter und bester Alternative genommen. Ist der Aufruf genauso gut wie bei einer Nicht-Template-Funktion, wird diese genommen.

Warum machen die Funktionen "void f(unsigned int);" und "void f(float);" Probleme bei den Aufrufen "f(1);" und "f(1.1)"? Es sieht zuerst so aus, als wäre die "1" viel näher an "unsigned int", und "1.1" näher an "float". Tatsache ist aber, dass "1" ein int-Typ und "1.1" ein double-Typ ist, dass also in jedem Fall eine Standard-Konversion durchgeführt werden muss. Und der Compiler unterscheidet nicht zwischen int nach unsigned int oder float bzw. double nach unsigned int oder float. Daher ist das Matching mehrdeutig; der Compiler beschwert sich.

105. UIMS

User Interface Management Systems helfen bei der Realisation von interaktiven Benutzerschnittstellen für objektorientierte Programme, wobei sie über ihre definierten Widgets und Werkzeuge ein typisches Look&Feel implementieren. Im Gegensatz zu Application Frames können damit keine Anwendungsobjekte erzeugt werden! Eine Verbindung zwischen Oberfläche und Programm wird durch eine ereignisgesteuerte Programmierung erzielt. Hier bestimmt nicht das Programm, sondern der Benutzer den Ablauf. Die Architektur richtet sich dabei meist nach dem Model-View-Controller-Konzept von SMALLTALK. Beispiel für ein Toolkit: DevGuide von SUN.

Über zusätzliche 4GL erhalten UIMS ähnliche Funktionalität wie Application Frameworks, können also auch Anwendungen mit Objekten versorgen. Die Nachteile bleiben jedoch: Die Oberfläche wird weiterhin interpretativ abgearbeitet, für direkte Manipulationen fehlen die passenden abstrakten Objekte, die jeweilige Spezifizierungssprache ist mühsam zu erlernen, und der Interpreter macht Erweiterungen fast unmöglich (da er dazu neu zu programmieren. wäre).

106. UNION-DEKLARATION

Eine union-Deklaration kann helfen, Speicherplatz zu sparen, indem je nach Aufruf nur ein Datenelement von mehreren Alternativen initialisiert wird. Aber Achtung: Borlands C++ allokiert für ein Union-Objekt immer so viel Speicher, wie dass grösste Element darin einnimmt!

union X{              // immer public!
  int i;
  double d;
  char s[10];         // es werden immer 10 Bytes allokiert!
  X(int j=2){i=j;}    // es sei denn, double-Variablen benötigen
  X(double e)[d=e;}   // noch mehr Speicherplatz!
};

107. USE CASE

Use Cases (von Jacobson) werden im Vorfeld von OOA durchgeführt. Das Modell wird als Black Box betrachtet, dessen externes Verhalten zu analysieren ist. Ein System reagiert auf Eingaben durch Actors, das sind externe interaktive Objekte wie z.B. Mensch, Task, Rechner usw., die es zu bestimmen gilt. Jeder Actor spielt eine Rolle, die etwas ganz bestimmtes von einem Modell erwartet, ohne dessen innere Details kennen zu müssen. Diese Erwartungen werden in Use Cases beschrieben, die etwa das für Szenarien darstellen, was Klassen für Objekte sind. Jedes Modell ist durch endlich viele Use Cases vollständig beschreibbar. Use Cases sind dazu über "extends" und "uses" ähnlich wie Klassen erweiterbar. Dennoch werden sie nur in natürlicher Sprache beschrieben, etwa in der Form:

  • Bezeichnung: Anforderung an Pegelstand.
  • Zusammenfassung: Turbinen benötigen für Arbeit grosse Pegeldifferenz.
  • Actors: Energie-Versorgung
  • Voraussetzungen: -
  • Beschreibung: Pegelstand auszutarieren wegen Flussverschmutzungsgefahr.
  • Fehlerfälle: Pegelstand zu hoch, zu niedrig, Wasserverschmutzung.
  • Ergebnis: Optimale Geschwindigkeit für Energiegewinnung und Flusssauberkeit.

108. VIRTUELLER MECHANISMUS

static-Member und Konstruktoren sind objektunabhängig, der virtuelle Mechanismus aber ist objektabhängig, daher funktioniert er für erstere nicht. Ebenso wirft er bei Downcasts Probleme auf, da hierbei nicht der korrekte virtual table pointer angesprochen werden kann.

Sind virtuelle Funktionen in inline-Form möglich? Ja, aber das ist wenig sinnvoll, da virtuelle Funktionen i.d.R. über einen Basiszeiger aufgerufen werden, der abgeleitete Klassen referenziert. Virtuelle Funktionen sind damit zur Laufzeit objektabhängig, d.h. der Compiler muss ihre Adresse kennen, um sie aufrufen zu können. Eine Adressberechnung ist aber nur möglich, wenn die Funktion an einer adressierbaren Stelle im Code steht, d.h. der inline-Mechanismus ist hier stets zuvor ausser Kraft gesetzt worden.

Die vom Compiler generierten Standard-Destruktoren sind nicht virtuell. D.h., wird ein Basiszeiger, der abgeleitete Klassen referenziert, mittels delete gelöscht, und die Basisklasse verfügt über keinen virtuellen Destruktor oder überhaupt keinen Destruktor, dann wird nicht der Destruktor der abgeleiteten Klasse, sondern der Basisklasse aufgerufen. Dadurch wird nur der Basisteil des abgeleiteten Objekts gelöscht, während der Rest als namenlose Leiche im Speicher stehen bleibt.

109. VOID-POINTER

In C++ ist sichergestellt, dass jeder Pointer implizit auf einen void* konvertiert werden kann. Die Rückwandlung ist zwar möglich, verlangt aber einen expliziten Cast, woraus folgt, dass der User Buch führen muss über den Typ, den er dem void* zugewiesen hat.

Da void* auf beliebige Objekte zeigen kann, deren Grösse variieren und über die er nichts aussagen kann, ist eine Zeigerarithmetik mit void* NICHT möglich.

110. VOLLSTÄNDIGE QUALIFIKATION

Bei der vollständigen Qualifikation wird eine Funktion bzw. Datum so aufgerufen, dass alle formalen Argumente exakt übereinstimmen, so wie dass der Zugriff in eindeutiger Weise geklärt ist. Diese Methodik ist nötig, wenn z.B. durch mehrfache Vererbung Ambiguitäten existieren oder lokale Deklarationen die globalen verdecken. Übrigens gilt hier: Der Geltungsbereichsoperator muss nicht auf die aktuelle Klasse verweisen, er kann auch auf die Basisklasse zeigen.

struct Y{int f(){return 1;}};
struct X:Y{int f(int i=2){return i;}};
void main(){
  X x;
  x.Y::f(2);  // vollständige Qualifikation, Fehler z.B.: x.f();
}

111. WEB

WEB ist ein Entwicklungssystem, welches gestattet, Code und Dokumentation in nur einem Dokument mit einer Art TEX-Syntax gleichzeitig bearbeiten zu können. Nach Abschluss der Schreibarbeit kann das Dokument einmal mit einem TANGLE-Compiler zu einem lauffähigen Pascal-Programm, zum anderen mit dem WEAVE-Compiler zu einer dazu passenden Programm-Dokumentation entwickelt werden. Inzwischen existiert auch CWEB.

112. WERKZEUGE

XVT++: Ein Werkzeug ähnlich dem Ressourcen-Compiler von Borland C++ für WINDOWS (stellt dort exakt die gleichen RC-Files her). Eignet sich damit zur Erstellung von Human Interface Clients unabhängig vom BS.

113. ZEIGER

Body-Zeiger sind Zeiger innerhalb von Klassenobjekt-Handles, die seit kurzem die public-Schnittstelle der Klasse nach aussen exportieren. Vorteil für die Entwickler: Die private-Elementfunktionen sind nicht mehr wie früher in den Header-Dateien offen zu erkennen. Dadurch bleibt das Know-how der Klassen-Programmierer besser verborgen.

Indizierte Zeiger, die sich ähnlich wie Felder verwenden lassen, im Unterschied zu diesen aber "umgebogen" werden können, lassen sich folgendermassen realisieren: "int *i=new int[100];". Zu Beachten ist, dass hierbei zweimal initialisiert wird, nämlich einmal "*i" und einmal das namenlose Objekt "int[100]" auf dem Heap. Da C++ im Gegensatz zu Smalltalk (noch) keine Garbage Collection besitzt, kann "int[100]" nur über "*i" wieder gelöscht werden. Kontrolliert werden kann dies übrigens über die Funktion "coreleft()" aus der Datei "alloc.h".

Zeiger könne auf 4 Arten initialisiert werden:

  1. new-Anweisung: int *ip=new int;
  2. Null-Pointer-Zuweisung: int *ip=NULL;
  3. Referenz-Zuweisung: int i=2; int *ip=&i;
  4. Pointer-Zuweisung: int i=2; int *ip=&i; int *jp=ip;

Die Priorität des Dereferenzierungsoperators "*" ist zu beachten. So bedeutet "*x++" nicht etwa "(*x)++", sondern "*(x++)". Damit wird also nicht der Inhalt, auf den "x" verweist, inkrementiert und ausgegeben, sondern zuerst der Zeiger inkrementiert und dann der Inhalt der neuen Adresse ausgegeben.

114. ZUGRIFFSDEKLARATION

Zugriffsdeklarationen sind ein elegantes Mittel, um die über die Zugriffsspezifizierer relativ global geschalteten Zugriffsrechte für einzelne Elemente umgehen zu können. Im folgenden Beispiel wird ein durch die private-Ableitung verborgenes public-Datenelement i der Basisklasse X wieder aufgedeckt:

struct X{
  int i;
  int j;
};
struct Y:private X{
  X::i;        // Zugriffsdeklaration
};
void main(){
  Y y;
  y.i=2;       // OK, aber y.j=2 Zugriffsfehler
}

115. ZUSTAND

Zustände sind mit konkreten Objekten assoziiert. Sie geben wieder, welche Attribute-Ausprägung und welche Beziehungen ein Objekt innehält (und welches Ereignis diesen Zustand herbeigeführt hat). Ein Attributwert- oder Beziehungswechsel geht also stets mit einer Zustandsänderung einher. Jeder Zustandsübergang wird durch ein Ereignis hervorgerufen. Dabei sind drei Formen möglich: (1) Zustand -> Ereignis -> neuer Zustand; (2) Zustand -> Ereignis -> neuer Zustand + sofortige Aktion; (3) Zustand -> Ereignis -> neuer Zustand + sofortige Aktion + warten auf Ereignis. Welcher Zustand auf welches Ereignis folgt, ist am Ereignisfolge-Diagramm erkenntlich: Jeder eingehende Pfeil in ein Objekt bedeutet einen Zustandsübergang des Objekts, jeder ausgehende Pfeil des Objekts bedeutet eine Aktion des Objekts (die ein Ereignis für ein anderes Objekt darstellt). Wichtig ist, dass Zustände im Gegensatz zu Ereignissen dauerhaft sind, und dass gleiche Ereignisse bei gleichen Zuständen stets die gleichen Zustandsübergänge bewirken.

In Zustandsdiagrammen sind alle Ereignisse aus den Ereignisfolge-Diagrammen zu übernehmen. Jede dortige Spalte entspricht einem konkretem Objekt mit seinen möglichen Zuständen. Im Zustandsdiagramm werden für ein Objekt nur die Attribute und Beziehungen berücksichtigt, die seinen Zustand hinreichend identifizieren.

116. ZUWEISUNGSOPERATOR

Ein überladener Zuweisungsoperator wird nicht vererbt. Der Compiler erzeugt aber nur einen Default-Zuweisungsoperator, wenn die Basisklassen-Teile public sind und wenn die Klasse keine const- oder Referenz-Datenelemente führt. Wird der Zuweisungsoperator überladen, so lässt sich über ihn - ähnlich wie mithilfe eines Copy-Konstruktors - ein Deep-Copy realisieren, d.h. Zeiger auf den Heap werden nicht einfach kopiert, sondern die zugehörigen Objekte neu erzeugt. Als Funktionswert liefert der Zuweisungsoperator einen this-Zeiger zurück (return *this;). Um Komplikationen mit Zuweisungen der Form "y=y;" zu vermeiden, fügt man in den Funktionsrumpf "if(this=&argument) return *this;" ein. Der this-Zeiger macht es auch nötig, dass Zuweisungsoperatoren nicht static deklariert werden!

Zuweisungen sind in C++ nicht automatisch bei Typengleichheit ohne Konversion gestattet! Voraussetzung für eine direkte Zuweisung ist vielmehr Namensäquivalenz der Datentypen (Synonyme mittels typedef sind aber möglich)! Es gilt also:

 struct C1{int i;}; struct C2{int i;};
 void main(){
  C1 c1; C2 c2; c1.i=2;
  c2=c1;  // FEHLER, obwohl Typen identisch!
  typedef C1 C3;
  c2=(C3)c1; // OK
 }

Zuweisungen sind Ausdrücke, keine Anweisungen! Dadurch Operationen wie "int i; char ch[(i=5)];" möglich. Ausdrücke sind stets Lvalues, die auswertbar sind! Zuweisungsanweisungen (mit ";" am Ende) sind nicht im globalen Geltungsbereich erlaubt, d.h. "int i=2;" ist als Initialisierung okay, aber "int i; i=2;" ist hier ein Fehler.

117. ZYKLOMATISCHE ZAHL

Die zyklomatische Zahl von McCabe ist ein Mass für die Programm-Komplexität. Sie wird nicht aus dem Code, sondern aus Programm-Graphen abgeleitet. Dabei gilt, dass Knoten die ZZ verringern, Kanten sie aber vergrössern. Nach Möglichkeit sollte ein Gleichgewicht zwischen Knoten und Kanten bestehen, sodass sich die ZZ bei "1" einpendelt. Ist automatisierbar.