Nicht nur erweitertes, sondern auch verbessertes C

C+ + macht den schrittweisen Übergang zu OOP leichter

16.08.1991

Objektorientierte Programmierung ist längst nicht mehr auf den akademischen Bereich beschränkt. Immer stärker zeigt sich, daß sie in der Lage ist, auch für die industrielle Produktion von Software wesentliche Verbesserungen zu bringen. Ein Hindernis beim Umstieg ist jedoch der damit verbundene verhältnismäßig große Aufwand. Mit C + + existiert nun eine objektorientierte Programmiersprache, die einen schrittweisen Übergang von der konventionellen zur objektorientierten Programmierung ermöglicht.

Die objektorientierte Programmierung (OOP) war lange Zeit eine Entwurfs- und Programmiermethodik, deren Nutzen auf Spezialanwendungen wie Simulation und Rapid Prototyping beschränkt zu sein schien. Daß das objektorientierte Vorgehensmuster (Paradigma) jedoch einen vielversprechenden Ansatz für eine wesentliche Verbesserung des Software-Engineerings darstellt, wurde erst in jüngster Zeit erkannt.

Auch im in industriellen Bereich beliebt

Ausgelöst durch die Erfolge der 1983 als objektorientierte Erweiterung von C eingeführten Programmiersprache C + +, erfreut sich OOP momentan auch im industriellen Bereich einer großen Beliebtheit. Das zeigt sich nicht zuletzt darin, daß immer mehr C+ + -Compiler, Entwicklungsumgebungen, Tools und Klassenbibliotheken im Entstehen begriffen beziehungsweise bereits zu kaufen sind. Allgemein wird der objektorientierten Programmierung prophezeit, in den 90er Jahre das zu sein, was die strukturierte Programmierung in den 70er und 80er Jahren gewesen ist.

Doch OOP erfordert ein Umdenken - und damit auch ein Umorganisieren - in weiten Teilen des Software-Entwicklungsprozesses. Eingespielte Denkansätze und Lösungsverfahren müssen zugunsten von neuen Vorgehensweisen aufgegeben werden, deren Auswirkungen nicht immer vollends abzuschätzen sind. Ist schon der Umstieg auf eine neue Programmiersprache ein mit viel Aufwand und Unsicherheiten verbundenes Unterfangen, so steigert sich das noch beträchtlich, wenn zusätzlich eine neue Entwicklungsmethode einzuführen ist.

In dieser Situation hat man überall dort, wo mit C programmiert wird, also zum Beispiel in der Unix-Welt, eine günstige Ausgangsposition. Besitzt man doch mit C + + eine vollwertige objektorientierte Programmiersprache, die aufwärts kompatibel ist zur Haussprache C und zudem noch das Potential besitzt, zur Nummer eins unter den objektorientierten Programmiersprachen zu werden - wenn sie es nicht sogar schon ist. Diese Kompatibilität zu C ist denn auch eine wesentliche Eigenschaft, die den Übergang zur OOP erleichter. Hinzu kommt, daß C+ + verschiedene Programmiertechniken unterstützt so daß es auch in methodischer Hinsicht möglich ist, den Übergang zur OOP zu einem Prozeß zu machen, der schrittweise durchlaufen werden kann.

Im folgenden werden die drei Phasen eines solchen Prozesses skizziert. In der ersten Phase kommt C+ + unter Beibehaltung der alten Programmiertechniken lediglich als verbessertes C zum Einsatz. In der zweiten Phase erfolgt mit dem Übergang zur Methode des Programmierens mit abstrakten Datentypen der erste Schritt zur objektorientierten Programmierung. In der dritten Phase ist durch Hinzunahme des Konzeptes der Vererbung und entsprechender Anpassung des Entwurfsvorgehens die objektorientierte Programmierung erreicht.

Phase 1: C+ + präsentiert sich als besseres C

An einzelnen Symptomen werden außerdem Schwächen beleuchtet, die der jeweiligen Phase noch anhaften. Allerdings können die Konzepte der jeweils nächsten Phase diese Schwächen zu beseitigen helfen.

C + + ist nicht lediglich ein um objektorientierte Konzepte erweitertes C, es ist auch ein verbessertes C. So umfaßt es den ANSI-C-Standard mit strenger Typprüfung von Funktionsparametern, Konstanten und einige neue Konzepte.

- Inline-Funktionen: Sie führen an der Aufrufstelle nicht zu einem Unterprogramm-Sprung, sondern zur Expansion des Codes, besitzen aber sonst alle Eigenschaften von normalen Funktionen.

- Default-Argumente: Funktionsargumente können an der Definitionsstelle mit Default-Werten vorbelegt werden. Diese kommen dann zum Einsatz, wenn der entsprechende Parameter an der Aufrufstelle nicht besetzt wird.

- Überladung: Operatoren und Funktionen lassen sich überladen, das heißt unter dem selben Namen mehrfach definieren. Eindeutigkeit wird durch Verschiedenheit der Operanden beziehungsweise Argumente hergestellt.

- Type-safe-linkage: Die Typinformation von Funktionsschnittstellen geht in die Bindephase ein, wodurch zum Beispiel Schnittstellen-Inkonsistenzen zwischen Aufruf- und Definitionsstelle modulübergreifend entdeckt werden.

- Für die dynamische Speicherverwaltung stehen die Operatoren New und Delete zur Verfügung. New entnimmt die Größeninformation für den benötigten Speicher direkt der Typangabe.

In einem ersten Schritt zur OOP konzentriert man sich also zunächst auf die Einführung einer neuen Programmiersprache unter Beibehaltung der alten Vorgehensweisen. Dies geschieht durch Verwendung des C+ +- statt des C-Compilers und unter Einbeziehung der oben aufgeführten Sprachkonzepte. Man gewinnt auf diese Weise nicht nur erste Erfahrungen mit der Sprache C + +, dem Compiler und Debugger, sondern kommt auch in den Genuß der zahlreichen Verbesserungen von C + + gegenüber C.

Strenge Prüfungen des C+ +-Compilers

Oft wird es so sein, daß die Einführung der neuen Entwicklungs und Programmiertechnik integrativ, das heißt unter Einbeziehung vorhandener Software, vonstatten gehen muß. Dann ist es ein lohnenswerter erster Schritt, zunächst vorhandenen C-Sourcecode so aufzubereiten, daß er vom C + + -Compiler akzeptiert wird. Dies führt wegen der strengeren Prüfungen des C+ + -Compilers nicht selten zum Entdecken bis dahin noch nicht erkannter Fehler.

Damit für den C-Compiler geschriebene Quellen auch vom C + + -Compiler verarbeitet werden können, sind trotz aller Kompatibilität in der Regel mehr oder weniger leichte Anpassungen notwendig. So müssen im C-Programm verwendete Bezeichner, die wie C+ +-Schlüsselworte lauten (zum Beispiel Class, Operator), geändert werden. Funktionsköpfe sind, soweit sie noch nicht dem ANSI-C-Standard entsprechen, in diesen umzuwandeln. Für die erste Aufgabe schreibt man sich ein Tool schnell selbst, für die zweite gibt es im Public-Domain-Pool entsprechende Tools.

Schwierigkeiten können außerdem Stellen in den C-Quellen bereiten, an denen allzu frei mit Typen umgegangen worden ist. Abhängig von der Entwicklungsumgebung kann es zu Problemen kommen, die daraus resultieren, daß der vom C+ + -Compiler erzeugte C-Code noch vom C-Compiler verarbeitet werden muß. Trotzdem zeigt die Praxis, daß die Anpassung des C-Codes an die Erfordernisse von C + + mit relativ geringem Aufwand durchführbar ist.

Phase 2: Programmierung mit abstrakten Datentypen

Für weitere Flexibilität bei Integration und Portierung sorgt die Eigenschaft der meisten C+ + -Übersetzer, ein sogenannter Translator zu sein, der das C + + -Programm in C-Code übersetzt.

Der C-Code wird dann dem C-Compiler der jeweiligen Plattform zur Weiterverarbeitung übergeben.

Die Zerlegung eines Systems in einzeln handhabbare und von der Gesamtkomplexität befreite Komponenten wird um so wichtiger, je größer ein System ist. Dem konventionellen Zerlegungsprinzip der funktionalen Dekomposition steht das datenorientierte Prinzip der Zerlegung in abstrakte Datentypen (ADT) gegenüber.

Hierbei wird nicht so sehr von der funktionalen Struktur des Systems, als vielmehr von den zugrundeliegenden Daten ausgegangen.

Daten, die zusammen mit den auf ihnen arbeitenden Operationen das programmtechnische Abbild einer als Einheit faßbaren Größe des Systems darstellen, werden zu einem abstrakten Datentyp zusammengefaßt - "abstrakt" deshalb, weil der ADT nach außen nicht mehr durch die Daten selbst, sondern durch die auf ihnen ausführbaren Operationen gekennzeichnet ist.

Jeder Zugriff auf den ADT erfolgt ausschließlich über diese Operationen. Der auf Implementierungsebene aus Datenstrukturen und Zugriffsfunktionen bestehende ADT ergibt damit auf natürliche Weise ein Modul. Dieses bietet über die Zugriffsfunktionen eine wohldefinierte Schnittstelle und stellt eine Kapselung der Daten sicher. Die Bezugnahme auf Implementierungsdetails von außen ist so von vornherein ausgeschlossen, womit Änderungen in der Implementierung eines Moduls keine Auswirkungen auf die Verwender des Moduls haben. Man spricht von Datenkapselung und Information Hiding.

Ein ADT besitzt in seiner Eigenschaft als Datentyp einen Typaspekt und in seiner Eigenschaft als programmtechnische Einheit einen Modulaspekt. Der Modulaspekt findet in den konventionellen Sprachen mehr oder weniger stark seine Berücksichtigung (Module in Chill und Modula, Package in Ada, in C so gut wie keine Unterstützung), der Typaspekt jedoch nicht. Wohl lassen sich auch dort "nackte" Datenstrukturen zu neuen Typen erklären, nicht jedoch die Einheit aus Datenstruktur und Zugriffsfunktionen, die einen ADT aber nun einmal ausmacht.

Auch ADT-Verfahren bei der Definition anlegen

Die fehlende Unterstützung der Typeigenschaft eines ADT schlägt sich in diesen Sprachen in einer Vergrößerung der Programmkomplexität und damit in einer beträchtlichen Steigerung des Programmieraufwandes bei gleichzeitiger Verringerung der Programmiersicherheit nieder. An zwei Punkten ist dies besonders deutlich zu erkennen. ADT-Variablen müssen, wie andere Variablen auch, bei ihrer Definition angelegt werden. Dies kann aufgrund der anwendungsspezifischen Speicherbelegungs- und Initialisierungsfunktionen jedoch nicht, wie bei den herkömmlichen Datentypen, nach einem starren Muster vom Compiler erfolgen, sondern ist explizit zu programmieren.

Konventionelle Sprachen bieten hierfür keine Unterstützung, so daß das Anlegen und die eventuelle Beschaffung von Freispeicher sowie dessen Initialisierung (die Instantiierung) durch expliziten Aufruf entsprechender Funktionen im Anschluß an die Deklaration vorgenommen werden muß.

Freigabefunktionen an der richtigen Stelle

Ebenso verhält es sich bei der Zerstörung solcher Variablen, welche die Freigabe reservierter Bereiche und die Abmeldung im System nach sich ziehen muß. Auch hier muß der Programmierer darauf achten, daß er die entsprechenden Freigabefunktionen an der richtigen Stelle aufruft. Jeder C-Programmierer, der schon einmal an einem hinreichend großen Programm gearbeitet hat, wird wissen, wieviel Kopfzerbrechen der Umgang mit dem Freispeicher bereiten kann.

In C+ + erfolgt die Implementierung der Initialisierung und Vernichtung von Instanzen durch spezielle Funktionen (Konstruktor, Destruktor), die an den entsprechenden Stellen automatisch aufgerufen werden. Die Instantiierung unterliegt in C+ + somit vollständig der Kontrolle der Sprache und ist damit sicher handhabbar.

Es muß allerdings darauf hingewiesen werden, daß auch in C+ + der Umgang mit Pointern auf dynamisch erzeugte ADT-Instanzen mit denselben Tücken behaftet ist wie in C und anderen Sprachen der Umgang mit Pointern in den Freispeicher.

Bei Wertzuweisung und Initialisierung (Initialisierung ist Wertzuweisung im Moment der Erzeugung einer Variablen) handelt es sich um das Kopieren von Variableninhalten. Hier ist es bei ADT-Variablen in der Regel nicht mit einem Byte-weisen Kopieren getan. Denn beispielsweise dann, wenn der Empfänger (linker Operand) Verweise in den Freispeicher enthält, würde das zu einem Überschreiben dieser Pointer führen, wo statt dessen aber Freigabe des belegten Speichers und Neuanforderung für den zu kopierenden Inhalt erforderlich wären. Diese Wertzuweisungs-Funktionalität ist also den Anforderungen des jeweiligen ADTs entsprechend zu implementieren und an den erforderlichen Stellen automatisch auszuführen.

C+ + unterstützt den Typaspekt auch bei ADTs. Dazu stehen im wesentlichen die Konzepte Klasse, Objekt/Instanz, Methode (in C+ + "Member function") zur Verfügung. Sie dehnen den Typbegriff von den einfachen Typen (Standarddatentypen und Strukturen) hin zu ADTs aus, was eine den einfachen Typen entsprechende Handhabung zur Folge hat.

Modulaspekt wird vernachlässigt

So wird auch das Kopieren von ADT-Instanzen im Rahmen dieser Konzepte geschlossen erfaßt. Dafür stehen in C + + bestimmte Konstruktoren und die Möglichkeit der Überladung des = -Operators bereit.

Leider wird der Modulaspekt in C + + genauso vernachlässigt wie in C. Zwar ist es möglich, das Programm beliebig auf verschiedene Dateien zu verteilen, aber auf Sprachebene kommen diese Module nicht vor. So muß man sich bei Versionsverwaltung, Konsistenzsicherung, Import-Export-Beziehungen und Steuerung der Kompilation auf externe Tools verlassen. Das bringt zwar Flexibilität, aber auch eine gehörige Portion an Unsicherheit und Kompilations-Overhead.

In dieser zweiten Phase des Übergangs zur OOP wird also eine neue Methode - nämlich die des Programmierens mit abstrakten Datentypen - und damit gleichzeitig die grundlegende Denkweise der OOP eingeführt. Die Zerlegung des Systems orientiert sich nicht mehr an der Funktion, sondern an den Begriffen, Einheiten, Strukturen der realen Welt, die bestimmt, zueinander in Beziehung gesetzt, mit Funktionalität ausgestattet werden und sich so zu ADTs verwandeln. In der OOP heißen diese Einheiten dann nicht mehr ADTs, sondern Klassen. Bei der Abbildung eines solchen, aus ADTs bestehenden Entwurfes in ein Programm finden die genannten Konzepte von C+ + Verwendung.

Phase 3: Objektorientierte Programmierung

Wiederverwendbarkeit von bereits bestehenden Komponenten ist ein wesentliches Prinzip zur Erhöhung von Produktivität. Die Programmierung mit abstrakten Datentypen bringt die Softwaretechnik diesem Ziel schon ein gutes Stück näher, weil sie die intermodularen Abhängigkeiten gegenüber früheren Ansätzen reduziert. Aus zwei Gründen kann hierbei jedoch noch nicht von inhärenter Wiederverwendbarkeit gesprochen werden.

Mit den bis hier zur Verfügung stehenden Konzepten ist es nicht möglich, die allgemeine, mit anwendungsübergreifender Bedeutung behaftete Komponente eines Sachverhaltes von dem speziellen und für den jeweiligen Anwendungszweck spezifischen Anteil zu trennen. Diese Trennung ist aber Voraussetzung für weitestgehende Wiederverwendbarkeit. Denn Wiederverwendbarkeit soll ja nicht auf den trivialen Fall beschränkt bleiben, bei dem eine ganz bestimmte Speziallösung erneut verwendet werden kann.

Es gilt daher, das allgemeine Prinzip, welches einer speziellen Lösung zugrunde liegt und im Rahmen dieser Lösung implizit mitbeschrieben worden ist, aus der Gesamtlösung zu extrahieren. Dann ist es für eine andere, auf demselben Prinzip beruhende Speziallösung erneut verwendbar. Die Spezialisierung des allgemeinen Prinzips in die verschiedenen Richtungen ergibt sich dann nur noch durch das Hinzufügen des jeweiligen speziellen Anteils.

Für Abstraktionen kein Spielraum

Die einzelnen Komponenten, also Module beziehungsweise ADT-Instanzen, sind noch zu stark mit dem Kontext verbunden, in den sie eingebettet sind. Der Grund dafür liegt im wesentlichen darin, daß die auf Implementierungsebene gegebenen Schnittstellen so beschaffen sind, daß sie bis ins letzte Detail Festlegungen treffen und für Abstraktionen keinen Spielraum lassen. Abstraktion ist aber das Prinzip, mit dem es gelingen würde, die Schnittstelle einerseits exakt zu beschreiben (auf einer höheren Abstraktionsebene), andererseits aber genügend Spielraum zu schaffen für verschiedene Ausprägungen (auf niedrigerer Abstraktionsebene) des Festgeschriebenen.

Wenn es gelingt, einen Mechanismus zu finden, der es erlaubt, Abstraktionen auch auf Implementierungsebene zu erhalten, dann würde man diese Übertragbarkeit von Code, also die Wiederverwendbarkeit, auch für die Implementierung erreichen.

Mit der Vererbung steht solch ein Konzept zur Verfügung. Sie stellt die Lösung der in den Phasen eins und zwei skizzierten Problempunkte dar und findet in objektorientierten Programmiersprachen mit geeigneten Mechanismen (Vererbung, Polymorphismus, dynamisches Binden) direkte Unterstützung.

Die Vererbung setzt Klassen (das, was vorher ADT geheißen hat) in eine Beziehung zueinander, die man plakativ als "allgemeiner" im Gegensatz zu "spezieller" oder "abstrakter" zu "konkreter" charakterisieren kann. Die allgemeinere beziehungsweise abstraktere Klasse ist die Oberklasse der spezielleren beziehungsweise konkreteren. Die speziellere wird als Unterklasse bezeichnet. Die Oberklasse vererbt ihre Eigenschaften an die Unterklasse, beziehungsweise Unterklassen, die Unterklassen erweitern, verfeinern, spezialisieren diese Eigenschaften durch Hinzufügen neuer oder Ersetzen alter durch neue Eigenschaften.

Phase drei bringt mit der zunächst einfachen Vererbung die objektorientierte Programmierung. Das Entwicklungsvorgehen wird nun zusätzlich dadurch bestimmt, daß man Klassen nach ihrem Gehalt an allgemeinen und speziellen Prinzipien und ihrem "Ähnlichkeitsverhältnis" zueinander strukturiert. So werden einerseits einzelne Klassen in Unterklasse und Oberklasse zerlegt, die auf diese Weise nach ansteigendem Spezialisierungsgrad in eine Vererbungshierarchie geordnet sind (Top-Down-Vorgehen). Aus einander ähnlichen Klassen wird andererseits der gemeinsame Anteil "extrahiert" und in eine eigene Oberklasse gebracht, von der die zugehörigen Unterklassen nun erben (Bottom-Up-Vorgehen). Auf diese Weise kann auch Software, die nach den Prinzipien der zweiten Phase entstanden ist, nachträglich noch (begrenzt) objektorientiert umstrukturiert werden.

C + + bietet volle Unterstützung für Einfachvererbung (eine Klasse erbt von höchstens einer direkten Oberklasse) wie auch für Mehrfachvererbung (eine Klasse kann beliebig viele direkte Oberklassen besitzen). Für den Programmierer äußert sich das nicht so sehr in weiteren Schlüsselworten, die es zu erlernen gilt, als vielmehr darin, daß die Komponenten, die er in der Phase eins und zwei kennengelernt hat, auf neue Art zusammenwirken und einen guten Teil ihrer bisher statischen Eigenschaften verlieren. So ergeben sich durch die Vererbung Datenstrukturen als Zusammensetzungen anderer Datenstrukturen; der auf einen Funktionsaufruf hin zur Ausführung kommende Funktionsrumpf liegt erst zur Laufzeit fest etc.

Am Anfang sollte auf die Mehrfachvererbung zunächst verzichtet werden, da sie weitere Freiheitsgrade schafft, die zu beherrschen wieder viel Erfahrung mit der Vererbung an sich voraussetzt.

C+ + ist eine objektorientierte Programmiersprache, die aber auch das Programmieren im C-Stil und das Programmieren mit abstrakten Datentypen unterstützt. Dies macht sie zu einer idealen Sprache für eine schrittweise Einführung der OOP. Jeder Schritt stellt eine sinnvolle Vorbereitung des nächsten dar, beziehungsweise baut auf dem vorangegangenen auf.

Die Verträglichkeit mit C und die Beibehaltung der von C bekannten Flexibilität geben dabei genügend Freiheit, die sich bei diesem Prozeß eventuell stellenden Probleme auf pragmatische Weise zu lösen.

Auf der anderen Seite sind es aber auch diese Eigenschaften, die C+ + zu einem wahren Herkules unter den Programmiersprachen machen:

- C+ + ist nicht leicht zu erlernen.

- Falsch oder undiszipliniert angewandt, können mit C+ + Programme entstehen, deren Lesbarkeit erheblich leidet.

- C+ + braucht - stärker als andere Sprachen - eine mächtige Browser- und Entwicklungsumgebung.

Diese Nachteile sollen nicht pauschal gegen die Verwendung von C+ + sprechen, denn auch ohne entsprechende Maßnahmen sind die Vorteile gegenüber beispielsweise C wohl noch groß genug. Es zeigt sich aber, daß C+ + stärker als andere Sprachen auf eine gute Schulung der Entwickler, Programmierkonventionen, Werkzeuge und eine Zusammenarbeit der programmierenden Gemeinschaft angewiesen ist.