"Nicht jeder Programmierer hält Klippen und Fallen für eine Herausforderung":

"C" für kommerziellen Einsatz kaum geeignet

01.02.1985

"C - ein Alleskönner", so schätzen viele Nicht-Experten diese Programmiersprache ein. Deshalb kommt es nicht selten zu vorschnellen Entscheidungen: Um den "C"-Zug nicht zu verpassen, werden übereilte Neuentwicklungen in dem vermeintlichen Software-Esperanto geschrieben oder gar bestehende Programme konvertiert.

Konsequenterweise ist die Begeisterung über die Sprache "C" besonders groß bei Programmierern, die sehr systemnah arbeiten oder die vorwiegend kurze Probleme im Alltag bewältigen müssen. Demgegenüber steht das weite Feld der sogenannten kommerziellen Programmierung, auf dem die Sprache (noch) weitgehend unbekannt ist.

Das Einsatzgebiet der Sprache ist sicher für ihre Beurteilung von Bedeutung, bei der Betrachtung der Spracheigenschaften sollten deshalb die folgenden Entwurfsziele gegenwärtig sein:

Die konventionelle Art, Betriebssysteme in Assembler zu programmieren, hatte die negativen Eigenschaften, daß für jede neue Hardware das Betriebssystem neu geschrieben werden mußte; Assembler sind nicht portabel. Bei der heutigen Schnelligkeit der Hardware-Neuentwicklungen ist dies ein schwerwiegender Nachteil. Außerdem sind Programme in Assembler schwer zu schreiben und noch schwerer zu lesen. Hochqualifizierte Programmierer sind dazu unerläßlich.

Der Assembler hat aber den Vorteil, daß die mit seiner Hilfe erstellten Programme durch die optimale Anpassung an die Hardware besonders effizient sind.

Man wird aus diesem Grund einen Kompromiß eingehen müssen, nämlich eine Hochsprache zu entwickeln, die eine hohe Flexibilität bei Datentypen zeigt (wie dies im Assembler der Fall ist) und die einen großen Operatorensatz enthält, so daß eine möglichst weitgehende Beeinflussung der Compileroptimierung durch den Programmierer möglich ist.

Man gewinnt durch diesen Ansatz die Vorteile der Portabilität und der strukturierten Programmierung, muß dafür aber in Kauf nehmen, daß Programme in der Hochsprache gegenüber solchen in Assembler langsamer werden und daß ein Betriebssystem eventuell nicht vollständig in dieser Sprache codierbar ist.

Das vermutlich wichtigste Konzept der Sprache ist das Blockkonzept. Blöcke dienen dazu, Anweisungen zu logischen Einheiten zusammenzufassen, eine Teilaufgabe innerhalb des Programms möglichst unabhängig von anderen Programmteilen zu gestalten. Sie sind Hilfsmittel, um innerhalb einer Funktion logische Strukturen erkennen zu lassen. Zwei Voraussetzungen müssen dazu erfüllt sein:

- Blöcke müssen Blöcke enthalten können (die Definition ist rekursiv).

- Um die Unabhängigkeit der Blöcke zu gewährleisten, dürfen lokale Variable außerhalb des Blocks nicht erreichbar sein. Die optionalen Variablenvereinbarungen am Anfang eines Blocks sind daher in ihrer Gültigkeit auf diesen Block beschränkt.

In einer nicht blockstrukturierten Sprache kann die Gültigkeit von Variablen zwar auch auf Unterprogramme beschränkt werden, aber nur um den Preis, daß dann auch beim Ablauf ein Unterprogrammsprung ausgeführt wird.

Die Struktur eines "C"-Programms besteht aus zwei Abschnitten: den Typ- und Variablendeklarationen und den Funktionsdeklarationen. Die Variablen und Typen, die außerhalb einer Funktion (also im ersten Teil des Programms) erklärt sind, gelten global, das heißt sie können in jeder der aufgelisteten Funktionen angesprochen werden.

Für Funktionen beziehungsweise deren Namen gilt dies generell, jede Funktion kann von überall her aufgerufen werden. Im Unterschied zu Pascal können Funktionen nicht statistisch geschachtelt und so ihre Gültigkeit beschränkt werden. Die Funktionsdeklarationen müssen in "C" daher alle nacheinander aufgereiht werden .

Auch das Hauptprogramm ist in "C" bemerkenswerterweise eine Funktion, nämlich diejenige mit dem Namen "main" (falls vorhanden) oder einfach die erste Funktion der aufgeführten Liste .Das Hauptprogramm kann daher auch Parameter haben, durch die beim späteren Ablauf die Wörter der Kommandozeile des Aufrufs im Programm sichtbar werden.

Die Struktur einer "C"-Funktion bietet keine Besonderheiten gegenüber einer Funktion in Pascal. Sie besteht aus einem Funktionskopf, in dem der Name und der Typ der Funktion sowie die Namen der Parameter und deren Typen aufgeführt sind. Der Funktionsrumpf besteht aus einem sogenannten Block, dem Konstrukt, dessen Existenz die Sprache als block-"strukturierte Sprache" ausweist.

Blöcke bestehen (nicht nur in "C") aus einer Blockklammer, aus optionalen und lokalen Variablenvereinbarungen und einer Anzahl von Anweisungen, Wesentlich dabei ist, daß diese Anweisungen ebenfalls wieder Blöcke sein können.

Ein anderes wichtiges Konzept, das die Sprache "C" nicht nur von ihrem Vorläufer "B" unterscheidet, ist das Typenkonzept. Variablentypen dienen dazu, Variablen für einen bestimmten Verwendungszweck festzulegen. Sie geben dem Compiler die Möglichkeit, eventuelle Fehler bei der Verwendung einer Variablen zu entdecken und zu melden. Dadurch läßt sich eine wesentlich größere Sicherheit beim Programmieren erzielen.

Dieser Festlegung wird (obwohl in fast allen Sprachen bekannt) von den verschiedenen Sprachkonzepten unterschiedliches Gewicht beigemessen: So gibt es etwa die strenge Definition von Typen, wie sie zum Beispiel im ursprünglichen Entwurf von Pascal oder von Ada vorgesehen ist In diesen Sprachen muß bei Beginn der Programmierung sehr sorgfältig überlegt werden, wann welche Typen eingesetzt werden, so daß später keine Verträglichkeitskonflikte auftreten.

Andere Sprachen, zu denen auch "C" gehört, erlauben eine temporäre Umdefinition der Variablen. Fundamentalisten neigen dazu, diese B(...)genschaft zu verurteilen. Bemerkenswert ist, daß fast alle Dialekte von Pascal (die ursprüngliche Fassung von Pascal wird kommerziell praktisch nicht eingesetzt) eben diese Erweiterung beinhalten.

Der Vorteil dieser "Regelung mit Ausnahmen": Die Prüfmöglichkeit des Compilers bleibt erhalten, weil ein spezieller Operator angewendet werden muß, um eine Typenumwandlung zu erzwingen.

Der Vollständigkeit halber seien hier auch die Typendeklarationen erwähnt, wie sie von Fortran her bekannt sind. Sie dienen lediglich dazu, Konvertierungsregeln festzulegen, werden aber konzeptionell nicht als Prüfmöglichkeit verstanden.

Die Typenlandschaft von "C" ist der von Pascal sehr ähnlich. Neben den einfachen Typen Fixzahl, Gleitzahl (auch doppelt genau) und Buchstabe bietet "C" auch zusammengesetzte Typen, nämlich Felder und Strukturen: Felder sind dabei eine Ansammlung einfacher Typen, die mit Hilfe eines Index angesprochen werden. Dieser Index beginnt in "C" mit dem Wert 0, wie es sich in einer hardwarenahen Sprache gehört. Auf dieser Grundlage werden in "C" die Strings implementiert; ein String ist ein Buchstabenfeld und wird durch ein Null-Byte abgeschlossen.

An dieser Stelle läßt sich die Herkunft von "C" nicht verleugnen: Auf der

PDP-11 können Felder mit dieser Begrenzung besonders optimal bearbeitet werden. Auch für gewisse Textoperationen erwies sich diese Verwirklichung als vorteilhaft, so daß sich "C" besonders auf dem Feld der Textverarbeitung einen Namen erwerben konnte.

Die Strukturen in "C" entsprechen den Records in Pascal, sie sind eine Gruppierung von Variablen verschiedener Typen unter einem Namen.

Als hardwarenahe Sprache bietet "C" eine Variante des Basistyps "Fixzahl": die vorzeichenlose Fixzahl. Viele Maschinen kennen eine entsprechende vorzeichenlose Arithmetik, auch die ursprüngliche Zielmaschine für "C", die PDP 11, unterstützt dies. Dieser Spezialtyp kann zusätzlich in Bitfelder unterteilt werden, was für die Programmierung von Hardwareregistern notwendig, aber auch zum Beispiel für die Synthese von Fehlercodes von Vorteil ist.

Zur Abrundung soll auch der Aufzählungstyp erwähnt werden, der es ermöglicht, zum Beispiel einer Ampel den Wert "grün" zuzuweisen; für die Lesbarkeit eines Programms eine ganz wesentliche Hilfe. Diese Fähigkeit ist eine der neueren Errungenschaften der Sprache; erst seit der Unix-Version 7 ist diese Möglichkeit eingebunden.

Eigene Datentypen können in "C" ebenfalls definiert werden, sie bedeuten jedoch lediglich Abkürzungen für eine komplexere Schreibweise. Neben der Übersichtlichkeit dient diese Möglichkeit auch zur Zentralisierung von Definitionen, so daß sie sich besonders in großen Programmsystemen als nützlich erwiesen haben.

Der Begriff der "Sets" (Pascal) ist dagegen in "C" (noch).nicht bekannt. Auch Mengenoperationen sucht man daher vergeblich.

Ein wesentlicher Aspekt der Assemblerprogrammierung ist die Möglichkeit, mehrfach indirekt adressieren zu können. Deshalb konnte auch in "C" nicht auf diese Fähigkeit verzichtet werden. Ähnlich wie in Pascal gibt es in "C" zu jedem Variablentyp einen entsprechenden Zeigertyp, der die Adresse der Variablen beinhaltet. Selbstverständlich kann in "C" wiederum ein Zeiger zu diesem Zeigertyp definiert werden, so daß mehrfache Indirektion problemlos möglich ist. Schwierig ist es lediglich, beim Programmieren nicht die Übersicht darüber zu verlieren.

An dieser Stelle ist auch anzumerken, daß auf keine der Strukturen, auch nicht auf Felder, entsprechende Operationen definiert sind. Eine Struktur läßt sich also in "C" nicht als Einheit bearbeiten, dazu sind immer entsprechende Programmschleifen erforderlich.

Kontrollstrukturen in illustrer Vollständigkeit

Außer durch die Anzahl und den Charakter der Typen wird eine Sprache beschrieben durch die Auswahl der Anweisungen, die sie dem Programmierer zur Verfügung stellt. Neben der einfachen Zuweisung bietet "C" eine Reihe von Kontrollstrukturen, die schon von dieser oder jener anderen Sprache bekannt sind, sich in "C" jedoch in illustrer Vollständigkeit versammeln.

Die am weitesten verbreitete FOR-Schleife (in Fortran DO-Schleife genannt) findet sich auch in "C", allerdings mit einer nützlichen kleinen Erweiterung: Man gibt in "C" keinen Schrittwert an, der die Kontrollvariable verändert, sondern eine Schlußanweisung, die am Ende jedes Schleifendurchlaufs ausgeführt wird. Diese Anweisung kann natürlich das Inkrement der Kontrollvariable bedeuten, hat aber gegebenenfalls ein größeres Potential.

Die WHILE-Schleife, bei der die Endebedingung am Anfang überprüft wird und die DO-Schleife mit der Prüfung am Ende, sind die beiden Variationen der logischen Schleifenkontrolle.

Die IF-Anweisung bietet auch in "C" die übliche Konfusion für den Programmierer, der bei mehrfach geschachtelten IFs die (optionalen) ELSE-Zweige nicht treffsicher zuordnen kann, ohne Linien auf Papier zu zeichnen.

Eine gewisse Abhilfe bietet hier die Switch-Anweisung (in Pascal Case genannt), mit deren Hilfe ohne IF-Schachtelung zwischen mehreren. Möglichkeiten ausgewählt werden kann. Eine empfehlenswerte Alternative.

Die BREAK-Anweisung kann vorteilhaft eingesetzt werden, wenn ein Fehlerausgang aus einer Kontrollschleife benötigt wird. Sie führt zum sofortigen Verlassen der innersten Schleife.

Das altbekannte GOTO bietet trotzdem unverdrossen seine Dienste an, obwohl es gerade in "C" die letzte seiner Berechtigungen durch obige Anweisung verliert.

Zu Beginn wurde gefordert, durch möglichst viele Operationen die Sprache konkurrenzfähig zum Assembler zu gestalten. So bietet "C" heute insgesamt 41 Operatoren an (zum Vergleich: Pascal: 22), wobei allerdings auch Varianten mitgezählt sind. Es stellt sich die Frage, welche Operatoren nun als typisch für eine generische Hardware angesehen wurden.

Leckerbissen wird zu oft genossen

Die üblichen arithmetischen Operatoren werden ergänzt durch einen Inkrement- und einen Dekrement-Operator, die es zum Beispiel erlaubt, mit einem Feldzugriff gleich auch den Feldindex zu verändern. Die daraus möglichen eleganten Formulierungen dienen der Übersichtlichkeit des Programms und geben Hilfestellung für die Optimierung des Compilers, der dann eventuell vorhandene Hardwarefunktionalität ausnutzen kann. Dieser "Leckerbissen" für jeden erfahrenen Programmierer wird deshalb auch leider oft überstrapaziert.

Die in "C" vorhandenen Bitoperatoren wie Shift, Bitmaskierung oder Komplementierung sind unabdingbar für eine Systemimplementierungssprache.

Die in "C" verfügbaren Varianten der Zuweisung finden sich zwar in sonst keiner bekannten Programmiersprache, tragen aber sicher wesentlich zur Beliebtheit dieser Sprache bei. Jeder binäre Operator läßt sich mit der Zuweisung so kombinieren, daß die Veränderungen einer Variablen ihre Nennung nur einmal erfordert. So bewirkt die Notation X + = 5 dasselbe wie die herkömmliche Schreibweise X: = X + 5.

Versicherung gegen Schreibfehler

In Sprachen mit Strukturen und langen Variablennamen bedeutet dies keineswegs nur eine Ersparnis beim Schreiben (der geringste Vorteil), sondern was viel wichtiger ist, auch eine Verdeutlichung der Absicht (Dokumentierungshilfe) und außerdem eine Versicherung gegen Schreibfehler.

Zum Abschluß der Syntaxdefinition in "C" sind noch einige Nachträge zur Funktionsdefinition angebracht:

- Die Parameterübergabe in "C" geschieht definitionsgemäß per "Wert" . Dadurch wird rekursive Programmierung möglich. Weil aber die Übergabe von Adressen durch die Verwendung von Zeigern ebenso ohne Erweiterung der Sprache möglich ist, kann die Emulation der Übergabe per "Adresse" leicht durchgeführt werden. So sind beliebige Bibliotheken auch in Fremdsprachen leicht anschließbar.

- Die Fähigkeit zur rekursiven Programmierung setzt voraus, daß Vorwärtsdeklarationen von Funktionen möglich sein müssen. "C" erzwingt diese zwar nicht, impliziert jedoch einen Typ für die betreffende Funktion, die nachher bei der Deklaration der Funktion nicht nachträglich berichtigt werden können.

- Ganz generell muß in "C" der Ergebnistyp einer Funktion nicht definiert werden, bei der Benutzung werden vom Compiler stillschweigend gewisse Annahmen getroffen. Sie stellen eine ständige Fehlerquelle dar, nur die Evolution kann die Toleranz ihnen gegenüber erklären.

- "C" kennt keine Prozeduren. Es ist immer erlaubt, beim Aufruf eines Unterprogramms einen Rückgabewert zu verlangen. Wird dieser Wert von der Funktion nicht belegt, so ist er undefiniert. Andererseits kann man Funktionen in "C" wie Prozeduren aufrufen, das heißt den Rückgabewert der Funktion nicht beachten. Die Funktion wirkt dann lediglich über ihre Seiteneffekte.

Damit ist die Syntaxbetrachtung bereits abgeschlossen. Alle übrigen Fähigkeiten müssen in "C" entweder über einen Preprocessor oder über die Systembibliothek erreicht werden. Ein "C"-System besteht also nicht nur aus dem "C"-Compiler, sondern auch aus einem Programm zur Vorverarbeitung des Quellcodes (dieses Programm kann natürlich organisatorisch mit dem Compiler zusammengebunden sein) und einer Bibliothek, die als Wichtigstes die E/A-Routinen enthält.

Der Preprocessor wird für mehrere Aufgaben benötigt:

Zur Definition der Bibliotheksroutinen werden sogenannte Headerfiles gebraucht, die der Preprocessor einkopiert. Diese Fähigkeit kann auch bei größeren Projekten genutzt werden, um Konventionen allgemein verfügbar zu machen.

Die in "C" mögliche bedingte Übersetzung wird ebenfalls durch den Preprocessor verwirklicht. Die Möglichkeit der bedingten Übersetzung ist bei großen Projekten von Vorteil, wenn Zusätze, die dem Testen dienen, auch von fremden Personen an- und abgeschaltet werden können.

Durch den Preprocessor stellt die Sprache "C" dem Programmierer Textmakros zur Verfügung. Da die Aufrufe dieser Makros die gleiche Syntax wie die Aufrufe von Funktionen haben, ist so der Einsatz von Inline-Funktionen möglich. Viele der Funktionen der Systembibliothek sind in Wirklichkeit Makros, die durch ein Headerfile definiert werden.

Systembibliothek mit rund 150 Funktionen

Die einfachste Anwendung der Makros sind die Konstantendefinitionen, also Makros ohne Parameter. Sie können zum Beispiel eingesetzt werden, um Systemgrößen an bestimmten Stellen zentral zu definieren.

Der Preprocessor beschränkt sich ausschließlich auf textuelle Operanden, auch wenn die Auswirkungen weit mehr vermuten lassen.

Die bereits erwähnte Systembibliothek kann als reichhaltig bezeichnet werden, sie bietet ungefähr (je nach Hersteller) 150 Funktionen.

Neben den Funktionen, die die E/A übernehmen, gibt es noch eine mathematische Unterbibliothek, die Buchstabenanalyse, die Stringverarbeitung, allgemeine Konvertierungsroutinen sowie Speicherverwaltung und die Unix-Systemfunktionen.

Wie bereits erwähnt, wird die E/A in "C" vollständig durch Funktionen gelöst, nicht durch Sprachkonstrukte, wie in anderen Sprachen. Der Vorteil besteht in der Flexibilität, die ja in "C" besonders betont werden soll.

Nachteile sind jedoch auch zu verzeichnen. Die Funktionen müssen bei jeder Übersetzung neu durch ein Headerfile definiert werden; dadurch wird der Übersetzungsvorgang verlangsamt. Außerdem werden die Prüfungsmöglichkeiten des Compilers eingeschränkt, das Programm dadurch fehleranfälliger.

Die Sprache "C" eignet sich sowohl zur Lösung kleiner Alltagsproblemchen als auch für große Projekte. Wenn kurze Programmierungsaufgaben zu bewältigen sind, wird Wert darauf gelegt, einen schnellen Programmierungs- und Testzyklus zu haben, außerdem darf die Konzipierung des Programms nicht zu viel Aufwand bedeuten.

Kleine Konzeptionsfehler fallen nicht ins Gewicht

"C" erfüllt diese Kriterien. Man benötigt nur sehr wenig Vorbereitungszeit, weil kein großer Vorspann im Programm gebraucht wird, bevor man zur Lösung des Problems fortschreitet (Gegenbeispiel: Cobol). Außerdem zeigt sich die Sprache flexibel genug, um auch mit kleinen Konzeptionsfehlern besonders im Bereich der Datentypen fertig zu werden. Nicht zuletzt ist die Sprache einfach genug, um mit einem kleinen, kompakten und deshalb schnellen Compiler auszukommen.

Große Projekte profitieren von anderen Fähigkeiten der Sprache: "C" bietet die Möglichkeit der separaten Kompilierung. Mehrere Programmierer können daher unabhängig (oder fast unabhängig) an ihren Modulen arbeiten. Gemeinsame Files, die etwa Hilfsroutinen, Returncodes oder lediglich Dokumentationskonventionen enthalten, können gemeinsam benutzt und durch den Preprocessor allgemein zugänglich gemacht werden.

Karl Kramer ist tätig in der Abteilung Software Vetriebsunterstützung der Digital Equipment GmbH, Geschäftsstelle Stuttgart.

Eine Zusammenfassung der Vor- und Nachteile von "C" lasen schnell bevorzugte Einsatzgebiete, aber auch die Grenzen dieser Sprache deutlich werden.

-Die Sprache vereinigt in sich praktisch alle modernen Spracheigenschaften, die von herkömmlichen Programmiersprachen bekannt sind Die wesentlichen sind: Blocks, Strukturen und Zeiger.

- "C" besitzt einen komfortablen Präprozessor, der insbesondere Makroeigenschaften aufweist.

- "C" ist sowohl schnell in der Compilierung als auch zur Laufzeit.

- "C" bietet vielfältige Möglichkeiten der Programmierung, besonders im Hinblick auf Systemprogrammierung oder Hardwarenahe Programmierung und für zeitkritische Anwendungen.

- "C" bietet eine sehr kompakte Schreibweise. Obwohl auch Argumente gegen eine derartige Optimierung sprechen, erhöht sich hierdurch nach Meinung von Experten die Übersichtlichkeit eines Programms. Man kann Strukturen auf engerem Raum besser überblicken.

Den Vorteilen stehen aber auch einige Aspekte entgegen, die den Siegeszug von "C" auf manchen Gebieten sicherlich verlangsamt haben.

- Die kompakte Schreibweise von "C"-Programmen impliziert minimale Redundanz des Programmtextes. Ein Tippfehler wird sehr häufig vom Compiler nicht bemerkt, weil der fehlerhafte Text trotzdem syntaktisch korrekt erscheinen kann.

- Die langsame Evolution der Sprache hinterließ einige Anachronismen, die zufällig zu unbeabsichtigten Fehlern führen können.

- "C" ist nicht standardisiert .Obwohl entsprechende Bestrebungen im Gange sind , Kann nicht davon ausgegangen werden, daß diese auch zum Erfolg führen. Die Referenz, die als Grundlage der verschiedenen Implementierungen dient ,ist das Buch von Kernighan und Ritchie, "The C Programming Language". Dieses Buch wurde aber nicht als eine solche Referenz konzipiert. Kritiker werden jedoch zugeben müssen ,daß der Quasi-Standard ,den die aktuellen Implementierungen verkörpern wesentlich konsistenter ist als von vielen normierten Sprachen. Die Sprache "C" hat den berechtigen Ruf ,besonders portabel zu sein.

- Ein weiterer Schwachpunkt ist die fehlende Semantikprüfung beim Funktionsaufruf. Aktuelle Parameter von Funktionen werden bei der Übergabe nicht auf die Übereinstimmung mit den entsprechenden formalen Parametern überprüft.

- Zeigeoperationen sind bei aller Nützlichkeit fehlerträchtig. Durch die Zeigeraritmetik werden Prüfungen des Compilers bei der Übersetzung zur Zeitverschwendung. Konsequent wird daher auch darauf verzichtet.

- Kernighan und Ritchie erwähnen keine ISAM-Files. Implementierungen auf der Grundlage der Unix-I/O sind ebenso zahlreich wie verschieden.