Sie befinden sich auf http://www.henning-thielemann.de/CHater.html.
Forsche Seite Lockere Seite deutsch

C-Hasser in 10 Tagen

" Hin und wieder stolpern Menschen über die Wahrheit, aber die meisten rappeln sich wieder auf und eilen weiter, als ob nichts geschehen wäre. "

- Winston Churchill

Diese Seite ist den Administratoren in aller Welt gewidmet, die den Kopf für Sicherheitslöcher hinhalten müssen, die von C-Programmierern verursacht wurden.

FAQ - Fragen, die ruhig häufiger gestellt werden könnten

" Pufferüberlauf - Diese Sicherheitslücke wird Ihnen präsentiert mit freundlicher Unterstützung der Programmiersprache C. "

ANSI-C ist out - ANTI-C ist in!

" C-Programmierer sind wie Bauarbeiter. - Wer einen Schutzhelm trägt, ist kein echter Kerl. "

Wir kommen jetzt zur Besprechung aller peinlichen Details von C. Würde ich es nur bei der Beschreibung der Fehler belassen, würden Kritiker einwenden, dass ich ja nur rumnörgele, ohne Alternativen angeben zu können. Allerdings muss es erlaubt sein, Fehler anzukreiden, auch wenn für diese noch keine Alternative umgesetzt wurde. Es entspricht sicher der Philosophie vieler Firmen, erst über Probleme zu sprechen, wenn man auch deren Lösung kennt. Aber ist das so erstrebenswert? Wenn ich andererseits mit einer anderen Sprache vergleiche, vermuten Kritiker, dass ich wohl nur diese Sprache kenne und deswegen C schlecht mache.

Weil also immer irgendjemand Bedenken haben kann, mache ich es, wie ich es für richtig halte. Um nämlich zu demonstrieren, dass die aufgezeigten Fehler mit einem sauberen Konzept problemlos entschärft werden können, habe ich ihnen die Lösungen von Modula-3 gegenüber gestellt. Man erkennt leicht, dass es auch da Probleme gibt, aber eben weit weniger, als bei C. Natürlich gibt es auch andere sauber konzipierte Sprachen, wenn auch nur sehr wenige. Mein Tip: Wer ganz sicher gehen will, bei einer miesen Sprache zu landen, sollte sich bei den weitverbreiteten Sprachen umschauen. Als akademisch verpönte Sprachen wie Haskell und Oberon dagegen sind in der Regel gut durchdacht. Ada und Eiffel/Sather klingen vielversprechend, ich kenne sie leider nur nicht gut genug, um Empfehlungen aussprechen zu können. Deshalb steinigt mich nicht, wenn ich C nicht mit eurer Lieblingssprache vergleiche!

Meine Erkenntnisse beziehe ich hauptsächlich aus Selbstversuchen mit dem GNU-C/C++-Übersetzer, eine C/C++ - FAQ, eine Einführung, Nachschlagewerke (Eric Huss' C guide, Dinkum STL library), Bücher (Bjarne Stroustrup: "Die C++-Programmiersprache", Scott Meyers: "Effizient programmieren in C++"), Diskussion mit anderen Entwicklern, C-Programme anderer Entwickler. Das einzige was mir noch fehlt, ist ein Studium der C/C++-Standards. :-(

  1. Programmfluss
    1. Geschweifte Klammern
    2. Geschweifte Klammern und else
    3. Geschweifte Klammern und es geht trotzdem nicht
    4. for-Schleife
    5. switch-Konstrukt
  2. Typen
    1. Ganzzahltypen
    2. Zahlen als Wahrheitswerte
    3. Ordnungsrelationen für Wahrheitswerte
    4. Bitlogik und Aussagenlogik
    5. Zahlenbereichschutz
    6. Klassen oder Objekte
  3. Felder und Zeiger
    1. Zeigerdeklaration
    2. Zeigerdeklaration und Arithmetik
    3. Felder von Zeigern und umgekehrt
    4. Zeiger auf Felder fester Länge
    5. Syntax von Deklarationen mit Zeigern und Feldern
    6. Zeiger als offene Felder
    7. Felder fester Länge als Zeiger
    8. Speicherverwaltung für offene Felder
    9. Deklaration mehrdimensionaler Felder
    10. Initialisierung mehrdimensionaler Felder
    11. Feldindizierung
  4. Konstanten
    1. const-Deklarationen
    2. konstante Zeiger
    3. Schreibschutz umgehen
    4. Schreibschutz und Initialisierung
  5. Vorrangregeln
    1. Addition vor Bitverschiebung
    2. Relation vor Bitlogik
  6. Klammern
    1. Typumwandlungen
    2. Prototyp oder Konstruktor
    3. Welcher Konstruktor wird aufgerufen?
  7. Unterprogramme
    1. Parameterlisten und Strukturen
    2. Parameterlisten variabler Länge
    3. Formatierte Ausgabe
    4. Rückgabewerte
    5. Funktionsaufruf oder Zeiger auf Funktion?
  8. Verschiedenes
    1. Komma-Operation
    2. Führende Nullen
    3. Templates
    4. Schiebung beim Ausprägen von Templates
  9. Präprozessor
    1. Hintergrund
    2. Effizienz
    3. Mehrfaches Einbinden
    4. Sichtbarkeit
    5. Inline
    6. geschachtelte Kommentare
    7. mehrzeilige einzeilige Kommentare
    8. Präprozessor-Symbole und C-Bezeichner
    9. Prüfung von Makroinhalten
  10. Komplexität der Sprache C++
    1. Typen
    2. Bezeichnerauflösung
    3. Objektorientierung
    4. Zugriffsrechte
    5. Templates
    6. Speicherverwaltung
C / C++ Modula-3
Programmfluss
Geschweifte Klammern
Schleifen- und Bedingungskonstrukte in C/C++ wie if, switch, for, while umschließen immer eine Reihe von Anweisungen. Der Programmierer kann sich dabei aussuchen, ob er direkt eine Anweisung einsetzt, oder ob er beliebig viele Anweisungen in geschweiften Klammern bündelt. Auch ohne enthaltene Anweisung ist so eine Gruppe sinnvoll, wenn zum Beispiel die Abfrage im while-Kopf bereits die Abbruchbedingung beeinflusst.
while(i--<10) {};
Die geschweiften Klammern funktionieren genauso wie der Begin..End-Block in Pascal, der bei Einzelanweisungen weggelassen werden darf. Dass man die geschweifte Klammer einsparen darf, schlägt natürlich mit einer Tippzeitverkürzung von mindestens mehreren tausend Mikrosekunden zu Buche. Als Ausgleich dafür gibt es folgende Lustigkeiten. Mal angenommen wir haben diese Zeile programmiert:
while(i<10) i--;
und wundern uns, warum die Schleife nicht abbricht und bauen noch schnell eine Testausgabe ein
while(i<10) printf("%d\n", i); i--;
Und zack, schon funktioniert gar nichts mehr. Warum? Weil wir aus Versehen aus einem Befehl zwei gemacht haben, und damit fällt der zweite aus der Schleife heraus. Da nützt auch die schönste Quelltextformatierung nichts. Klar, kann man vorsichtshalber immer geschweifte Klammern setzen, aber der Übersetzer besteht darauf nicht. Deswegen kann einem der Fehler immer passieren und wenn man Quelltexte von einem echten C-Hacker bekommt, darf man auch keine Klammer zu viel erwarten.
Modula vermeidet solche "Kann"-Regelungen. Alle Schleifen und Verzweigungen setzen sich aus einer Kopfzeile und einer Fußzeile zusammen. - In C entspräche das einer Klammerungspflicht.
WHILE i<10 DO
  DEC(i);
END;
IF i<10 THEN
  DEC(i);
END;
Geschweifte Klammern und else
Das Spielchen kann man selbstverständlich noch weiter treiben. Wie wär's mit einer if-Anweisung mit else-Teil. In C wird der else-Teil einfach hinter den if-Teil geschrieben, als wenn er ein neuer eigenständiger Befehl wäre.
if(A)
   if(B)
      printf("A and B\n");
else
   printf("not B or not A?\n");
Zu welcher der beiden if-Anweisungen gehört denn nun der else-Teil? Welche der beiden if-Anweisungen wird denn durch das erste Semikolon abgeschlossen? Oder gilt das Semikolon gar nur dem Befehl innerhalb der if-Verzweigung? Die C-Syntax gibt darüber keine Auskunft, nur durch Probieren oder Wälzen der Standards ermittelt man, dass es genau falsch herum eingerückt ist. Der else-Teil gehört zu if(B).
Was aber, wenn ich den else-Teil wirklich dort haben will, wo ich ihn suggestiv hingeschrieben habe? Das geht nur, indem man der else-Anweisung durch geschweifte Klammern sozusagen die Sicht auf das innere if versperrt:
if(A)
{
   if(B)
      printf("A and B\n");
}
else
   printf("not A!\n");
besser aber
if(A) {
   if(B) {
      printf("A and B\n");
   }
} else {
   printf("not B!\n");
}

Dieses Problem ist übrigens so bekannt, dass es sogar einen eigenen Namen bekommen hat: Das Dangling-Else-Problem.
Hier gibt es dieses Problem nicht einmal ansatzweise: Entweder endet der Anweisungsblock nach dem Schlüsselwort THEN mit einem END, dann ist, wie der Name schon sagt, Schluss, oder es folgt ein ELSE oder ähnliches und dann folgen wieder Anweisungen.
IF A THEN
  IF B THEN
    IO.Put("A and B\n");
  END;
ELSE
  IO.Put("not A\n");
END;
ist genauso unmissverständlich wie
IF A THEN
  IF B THEN
    IO.Put("A and B\n");
  ELSE
    IO.Put("not A\n");
  END;
END;
.
Geschweifte Klammern und es geht trotzdem nicht
Selbst wenn man konsequent geschweifte Klammern setzt, kann man noch tüchtig auf die Nase fallen.
for(a=0;a<10;a++);
{
   printf("%i\n", a);
}
Sieht vorbildlich aus, aber es wird trotzdem nur eine Zeile ausgegeben. Warum? Weil das Semikolon hinter dem for die Schleife bereits abschließt. Kleines Zeichen - große Wirkung. So lieben wir C.
Wie gesagt, geöffnet wird der Anweisungsblock immer nach dem Beginn einer Schleife, abgeschlossen werden muss er auch und zwar deutlich und immer auf die gleiche Weise.
FOR a:=0 TO 9 DO END
  IO.Put (Fmt.Int(a) & "\n");
END;
Das wird vom Übersetzer sofort abgelehnt. Erstens, weil die Variable a nur innerhalb der Schleife existiert (das ist eine Neuerung von Modula-3), welche aber schon vor IO.Put beendet wurde und zweitens, weil es ein END zuviel gibt.
for-Schleife
Die for-Schleife in C wird immer wieder gern wegen ihrer Flexibilität hervorgehoben. Man kann damit nicht nur einfach auf- oder abwärts zählen, sondern auch Zweierpotenzen, quadratische Folgen oder irgendwas anderes abklappern.
int i;
for (i=1; i<2000; i*=2) {
  printf("%d\n", i);
};
Dadurch wird es für den Übersetzer vermutlich schwieriger, die Schleife so zu übersetzen, dass dabei die von vielen Prozessoren angebotenen speziellen Schleifenbefehle verwendet werden. Außerdem muss man immer mal wieder nachschlagen, ob bei den beiden letzten Anweisungen nun zuerst die Abbruchbedingung oder die Weiterzählanweisung stehen muss. Na ja, sei's drum.
Viel ärgerlicher ist eigentlich, dass die drei Anweisungen im Schleifenkopf hintereinander in einem Block stehen, gerade so als ob sie hintereinander ausgeführt werden. "Ja, und? Das ist doch das letzte, was mich gestört hätte!", höre ich da aus den hinteren Reihen. Ja, aber was ist denn, wenn man wirklich mehrere Anweisungen an einer Stelle ausführen will? Etwa so was:
int i,j;
for ({i=0;j=1;}; i<10; {i++;j*=2;}) {
  printf("2^%d = %d\n", i,j);
};
Nee, so geht das nicht, wird Euch der C-Übersetzer dann mitteilen. Die Schreibweise wäre fast noch logisch, aber C ist von Grund auf unlogisch, daher muss man sich hier völlig anders helfen. Zum Glück gibt es den praktischen Komma-Operator. Während er sonst eher Unheil stiftet, bekommt er hier noch eine Existenzberechtigung als Notnagel. Im for-Schleifenkopf bleibt einem nichts anderes übrig, als die Anweisungen mit Komma zu trennen.
int i,j;
for (i=0,j=1; i<10; i++,j*=2) {
  printf("2^%d = %d\n", i,j);
};
Wenn man etwas klammern möchte, muss man dies mit runden Klammern tun, nämlich so:
int i,j;
for ((i=0,j=1); i<10; (i++,j*=2)) {
  printf("2^%d = %d\n", i,j);
};

In C99 ist es erlaubt, direkt im for-Schleifenkopf Variablen lokal für die Schleife zu deklarieren. Zum Beispiel so
for (int i=0; i<10; i++)
Wie überträgt man das auf mehrere Variablen? In
for (int i=0, j=1; i<10; i++,j*=2)
könnte int i=0, j=1; für eine gewöhnliche Deklaration zweier Variablen stehen. Wegen der besonderen Bedeutung des Kommas in for-Schleifenköpfen, könnte es aber auch "lege neue Ganzzahlvariable i an, belege sie mit 0, und belege schon existierende Variable j mit 1" heißen. Tatsächlich wird die erste Interpretation genommen, und wie man die zweite erreicht, sei dem Leser zur Übung überlassen.
Im Prinzip ist die for-Schleife bei C auch nur eine while-Schleife, bei der noch ein paar Befehle von vor der Schleife und vom Ende der Schleife in den Schleifenkopf geschrieben werden. Welche Befehle dazu gehören und welche nicht, ist immer eine Ermessensfrage des Programmierers. Wer Spaß daran hat, kann auch ein ganzes Programm in einer for-Schleife unterbringen.
Andererseits sind
int i=0, j=1;
for (; i<10;) {
  printf("2^%d = %d\n", i,j);
  i++;
  j*=2;
};
oder gleich
int i=0, j=1;
while (i<10) {
  printf("2^%d = %d\n", i,j);
  i++;
  j*=2;
};
völlig legitim. So gesehen ist das for-Konstrukt so mächtig, dass es schon wieder überflüssig ist, weil es in Sachen Sicherheit und Mächtigkeit zur while-Schleife äquivalent ist.
FOR-Schleifen in Modula sind weit weniger mächtig, dafür kann man dort praktisch nichts falsch machen:
FOR i:=0 TO 10 DO
  IO.Put (Fmt.Int(i) & "\n");
END;
Im Gegensatz zu seinen sprachlichen Vorgängern führt Modula-3 automatisch eine Laufvariable eines geeigneten Typs ein, welche nur innerhalb der Schleife verfügbar und obendrein schreibgeschützt ist. Das heißt, dass weder vor Beginn der Schleife noch nach Ende der Schleife an dieser Variablen herumgebastelt werden kann. Will man eine aufwändigere Schleife konstruieren, so muss man sich an die WHILE-Schleife halten, welche nur noch der Laufbedingung eine Sonderstellung einräumt.
i := 1;
WHILE i<2000 DO
  IO.Put (Fmt.Int(i) & "\n");
  i := i*2;
END;
switch-Konstrukt
Die Struktur switch testet einen Ausdruck auf verschiedene Werte und führt in Abhängigkeit des Ausdruckswertes eine bestimmte Anweisung aus. Man kann diese Struktur daher mit Fallunterscheidung umschreiben und als Spezialfall eines geschachtelten ifs ansehen. In C geht es besonders einfach, dass nach Abarbeitung eines Falles gleich die Anweisungen für einen anderen Fall mitabgearbeitet werden. Anscheinend brauchen C-Programmierer diese Eigenschaft sehr häufig, denn sie müssen sie für jeden Fall mit break unterdrücken. Vielleicht wollte man damit am Übersetzer einsparen, dass mit einer Entscheidung mehrere Fälle gleich behandelt werden können.
switch (day) {
  case monday:
  case tuesday:
  case wednesday:
  case thursday:
  case friday:
    work_hard();
    break;
  case saturday:
  case sunday:
    sleep_long();
    break;
  default:
    additional_day();
}
Jeder hatte sicher schon seinen Spaß mit der Suche nach vergessenen break-Anweisungen.
So geht's natürlich auch:
CASE day OF
| Day.Monday .. Day.Friday => WorkHard();
| Day.Saturday, Day.Sunday => SleepLong();
ELSE
  AdditionalDay();
END
Typen
Ganzzahltypen
Einer der großen Fortschritte höherer Programmiersprachen gegenüber maschinenorientierten Sprachen ist das Typenkonzept. Während der Prozessor nicht entscheiden kann, wie die Bitmuster in seinem Speicher zu interpretieren sind, und das in der menschengerecht aufbereiteten Maschinensprache namens Assembler auch nicht geht, kann man in höheren Programmiersprachen Speicherzellen (dort Variablen genannt) Typen zuordnen.
char c;
int i;
bool b;
enum {monday, tuesday, wednesday, thursday,
      friday, saturday, sunday} d;
Hier werden für die vier Variablen c, i, b, d nicht nur Speicherzellen reserviert, sondern sie werden auch mit einem Typen versehen. (Allerdings werden dazu nicht die Speicherzellen markiert. Die Typen kennt nur der Übersetzer und im erzeugten Maschinenprogramm sind sie bereits nicht mehr verzeichnet.) Warum soll das so ein großer Fortschritt sein? Weil zum Beispiel die Bitmuster vom Zeichen 'A' und von der Zahl 65 zwar gleich sind, aber man mit Zeichen und mit Zahlen völlig verschieden umgeht!
Allzu fortschrittlich wollen C-Programmierer lieber nicht sein. Daher dürfen sie alle Typen, die den ganzen Zahlen in irgendeiner Weise ähnlich sind (ordinale Typen), also ganze Zahlen selbst, Zeichen, Wahrheitswerte und Aufzählungselemente, bunt miteinander verrechnen und einander zuweisen. Bei manchen Konstellationen erhebt wenigstens ein C++-Übersetzer wie der G++ Einspruch, im Großen und Ganzen sind der Sinnlosigkeit aber keine Grenzen gesetzt. Es ist sogar so, dass mangels symbolischer numerischer Konstanten in C (weder Präprozessorsymbole noch const int erfüllen die Kriterien) selbst disziplinierte C-Programmierer empfehlen, dass man Aufzählungselemente zur Definition von Ganzzahl- und Mengenkonstanten benutzen sollte. Es braucht nicht erwähnt zu werden, dass Aufzählungen keinen eigenen Namensbereich einrichten. Das führt zu den üblichen Bezeichnerkonflikten, aber da ist C wenigstens konsequent.
Kurzum: Wer öfter Zeichen mit Aufzählungselementen bitweise verknüpfen will, etwa
'a' | tuesday
oder sich an solch tiefliegenden Zusammenhängen wie
'A' + ' ' == 'a'
ergötzen kann, aber Assembler scheut, für den gibt es keinen Weg an C vorbei!
Wer dagegen Wert darauf legt, dass der Übersetzer nicht jeden Tippfehler für bare Münze nimmt, der wird sich schnell mit der Strenge von Modula anfreunden können. Hier werden alle diese Typen strikt getrennt, und es wird darauf geachtet, dass der Programmierer nur sinnvolle Operationen auf die Variablen anwendet.
VAR
  c : CHAR;
  i : INTEGER;
  b : BOOLEAN;
  d : {Monday, Tuesday, Wednesday, Thursday,
       Friday, Saturday, Sunday};
Bitoperationen auf Zeichen sind also genauso unterbunden wie Grundrechenoperationen auf Bitmuster von Mengen. C-Programmierer empfinden das zuweilen als Bevormundung. Das Argument zieht aber nicht, weil man auch in Modula diese Sicherheitsvorkehrungen umgehen kann, indem man einfach die Werte in die Typen konvertiert, die die gewünschte Operation zulassen. Das sieht im Quelltext hässlich aus und das ist durchaus gewollt, denn damit besteht ein Anreiz, doch bitte vorher zu überlegen, welcher Typ am besten zu der Variable passt. Oft benötigen diese Konvertierungen nicht einmal einen Maschinenbefehl im lauffähigen Programm, wenn es sich um eine einfache Umdeklarationen handelt. Um die Effizienz braucht man sich also keine Sorgen zu machen.
Zahlen als Wahrheitswerte
Zusammen mit anderen nett gemeinten Eigenschaften von C können dann auch folgende Fehler auftreten:
int a=1, b=2;
if (a=b) {
  printf("a ist gleich b\n");
};
Wenn man das Programm startet, erscheint die Ausgabe "a ist gleich b", obwohl a und b ganz offensichtlich verschieden sind. Der versierte C-Nutzer sieht bei diesem einfachen Beispiel natürlich sofort den Fehler - gemeint war wohl (a==b) - nur warum merkt der Übersetzer das nicht?
Ja, weil erstens a=b zwar hauptsächlich eine Anweisung ist, welche den Wert von b in a hineinkopiert, zusätzlich ist aber a=b auch noch ein Ausdruck, welcher b als Wert zurückliefert . Für die if-Anweisung hätte es also den gleichen Effekt, als würde man gleich if(b) schreiben. Und das wird zweitens dummerweise auch noch akzeptiert, weil if nicht zwischen Wahrheitswerten und Ganzzahlen unterscheidet. So wird jeder von null verschiedene Wert als "wahr" interpretiert. Diese Festlegung ist rein willkürlich und die interne Repräsentation der Werte "wahr" und "falsch" könnte dem Programmierer eigentlich völlig schnuppe sein und könnte vom Übersetzer so gewählt werden, dass der jeweilige Prozessor effizient damit umgehen kann, aber in C ist man auf diese Sicht festgenagelt.
Zur Ehrenrettung muss man anmerken, dass GCC inzwischen sogar einen Warnhinweis ausgibt und zur Kennzeichnung der verdächtigen Stelle ein zusätzliches Klammerpaar vorschlägt - aber nur Warmduschern, die sich auch Warnungen und nicht nur Fehler anzeigen lassen! Diese Ausgaben sind in der Regel abgeschaltet und müssen mit der Option -Wall angefordert werden.
Modula-Programmierer können über solchen Zirkus nur müde lächeln - Hier wird das Gleichheitszeichen als Gleichheitszeichen verwendet und die Zuweisung wird durch einen zusätzlichen Doppelpunkt hervorgehoben, also ":=". Welche Symbole man bevorzugt, ist hauptsächlich eine Frage der Gewöhnung und des Geschmacks, soll uns also nicht weiter interessieren, entscheidender semantischer Unterschied ist jedoch, dass der Modula-Zuweisungsoperator kein Ergebnis zurückliefert. Das hängt auch damit zusammen, dass Modula strikt zwischen Funktionen mit und ohne Rückgabewert unterscheidet. Die beliebten Zuweisungsketten in C wie a = b = c sind damit zwar unterbunden, ein IF a:=b THEN aber auch.
Ordnungsrelationen für Wahrheitswerte
Zu lustigen Ergebnissen führt auch die unkritische Übertragung abgekürzter mathematischer Notation in C. Zum Beispiel akzeptiert ein C-Übersetzer ohne mit der Wimper zu zucken
int a=10, b=8, c=6;
if (a<=b<=c) {
   printf("%d <= %d <= %d\n", a, b, c);
}
Man staunt und wundert sich, ob der Rechner jetzt wohl die Relationszeichen vertauscht hat, weil dieses Programm tatsächlich behauptet, dass 10 ≤ 8 ≤ 6. Der Grund ist einmal mehr im versuppten Typenkonzept von C zu finden. Der Übersetzer geht so vor: Der Ausdruck wird von links nach rechts ausgewertet, das entspricht der Klammerung ((10≤8)≤6). Jetzt wird der erste Vergleich ausgewertet: 10≤8 ist falsch, für C entspricht das einer 0. Nun wird diese 0 in den zweiten Vergleich gesteckt: 0≤6 ist wahr, also für C gleich 1, also für if wiederum wahr. Dumm gelaufen, was?
Dass wir uns nicht falsch verstehen: Der Übersetzer braucht diese verketteten Vergleiche nicht zu unterstützen. Sie sind Schreibvereinfachungen, die sich aber nicht damit vertragen, dass Vergleiche eigentlich Relationen zwischen zwei und zwar genau zwei Werten sind. Aber man kann von dem Übersetzer erwarten, dass er solche Irrtümer bemerkt, weil es eigentlich keine sinnvolle Interpretation für den Ausdruck "10 ≤ 8 ≤ 6" gibt, wenn man "≤" als Operation mit zwei Argumenten (linke Seite und rechte Seite) auffasst. Diese Sichtweise ist auch sinnvoll, denn alles andere wäre kaum wasserdicht zu definieren.
In Modula wird
VAR
  a : INTEGER := 10;
  b : INTEGER :=  8;
  c : INTEGER :=  6;

BEGIN
  IF a<=b<=c THEN
    IO.Put(Fmt.F("%s <= %s <= %s\n",
                 Fmt.Int(a),Fmt.Int(b),Fmt.Int(c)));
  END;
END;
sofort beim Übersetzen abgeblockt, weil der Vergleich a≤b irgendwas vom Typ Wahrheitswert ergibt und sich dieser nicht mit einem Wert eines anderen Typs in der Größe vergleichen lässt, nämlich der ganzen Zahl c. Natürlich besitzt der Wahrheitswert auch bei Modula im Rechner wieder einen Zahlenwert wie 0 oder 1, in der überwiegenden Anzahl der Fälle kann uns das aber egal sein.
Der korrekte Ausdruck ist übrigens
a<=b AND b<=c
. Alternativ kann man auch mit Mengen arbeiten, etwa
b IN SET OF [0 .. 31] {a .. c}
was aber nur mit ordinalen Typen funktioniert und bei größeren Grundbereichen möglicherweise nicht effizient übersetzt wird, da Mengen als Bitfelder implementiert sind. Bei einem Grundbereich wie [0 .. 31] und konstantem a und c ist diese Variante aber wahrscheinlich sogar die schnellste.
Bitlogik und Aussagenlogik
Was ist eigentlich der Unterschied zwischen dem einfachen Und "&" und dem Doppel-Und "&&" ? Ein Kommilitone meinte einst, dass der wesentliche Unterschied darin besteht, dass der eine Operator (hm, welcher eigentlich?) effizient auswertet und der andere nicht. Ok, es ist das Doppel-Und, das effizient arbeitet, und zwar in dem Sinn, dass die Berechnung der Ausdrücke abgebrochen wird, sobald das Ergebnis feststeht. Ist der linke Operand von "&&" falsch, ist der gesamte Ausdruck falsch und der zweite Ausdruck braucht nicht mehr ausgewertet zu werden. Das ist mehr als eine nützliche Optimierung, von ihr kann die einwandfreie Funktion eines Programmes abhängen:
bool check_content (const char *c) {
  return (c != NULL) && (c[0]!=0);
}
Ist c der NULL-Zeiger wird der Ausdruck c[0]!=0 gar nicht erst ausgewertet. Das ist auch wichtig, denn im Falle eines NULL-Zeigers würde man mit "c[0]" den Wert aus der ungültigen Speicherzelle 0 auslesen, was bereits den Speicherschutz verletzt.
Diese effiziente Auswertung logischer Ausdrücke wird zuweilen als "Lazy evaluation" bezeichnet. Das allein zeichnet also das Doppel-Und aus?
Ja, klar.
Nein, ganz und gar nicht! In erster Linie besteht der Unterschied darin, dass die einfachen Symbole "|" und "&" bitweise verknüpfen, die doppelten Zeichen "||" und "&&" dagegen einfache Wahrheitswerte verarbeiten. Durch die konkrete Darstellung der Wahrheitswerte in C ergibt sich bei den Oder-Verknüpfungen "|" und "||" zufälligerweise kein Unterschied, sollte man statt "||" aus Versehen "|" verwenden (bis auf die effiziente Auswertung). Bei "Und" verhält sich das anders:
0x80 && 0x08 == 1 also "wahr"
0x80 & 0x08 == 0 also "falsch"
Wie gewohnt überlässt es C genüsslich dem Programmierer, ab und zu ordentlich auf die Schnauze zu fallen, wenn er ein &-Zeichen vergisst.
In Modula können die Bitverknüpfungsoperationen "|" und "&" nur auf Typen angewendet werden, für die das einen Sinn ergibt, nämlich Mengentypen. Sie heißen dann "+" und "*" in Anlehnung an die gebräuchlichen Schreibweisen in der booleschen Algebra.
VAR
  a, b, c : SET OF {Red, Green, Blue};
  d := a+b*c;
Demgegenüber gibt es die logischen Operationen "OR" und "AND" welche nur Wahrheitswerte verarbeiten und zurückgeben.
PROCEDURE Test (a : BOOLEAN;) =
  BEGIN
    IF a AND 2<3 THEN
      IO.WriteString ("Wahrsager!\n");
    END;
  END;
Verwechslungen sind praktisch ausgeschlossen.
Zahlenbereichschutz
Die Schluderei mit den Typen geht bei C bei Zahlenbereicheinschränkungen im gleichen Stil weiter. Die Unterscheidung zwischen vorzeichenlosen und vorzeichenbehafteten Zahlen wird gerade noch bei der Deklaration von Variablen zugelassen, weil manche Operationen wie Vergleiche oder Multiplikationen sich auf vorzeichenbehafteten Zahlen anders verhalten als auf vorzeichenlosen Zahlen. Beim Zuweisen und Addieren von ganzen Zahlen fallen aber alle Hemmungen:
unsigned int i = -5;
signed int j = 7
unsigned int k = i*j;
Überläufe werden in C auch nicht abgefragt,
if ((char)(-100) == (char)(78+78)) {
  printf("Überlauf nicht bemerkt!\n");
};
warum auch, dass die Summe zweier positiver Zahlen eine negative ergibt, ist doch der natürlichste Vorgang der Welt! Selbst wenn man in C ganz sauber programmieren wollte, gibt es keine einfache Möglichkeit vor oder nach einer Berechnung herauszufinden, ob das Ergebnis korrekt ist oder nicht. Jeder Mikroprozessor stellt diese Information aber nach einer Berechnung bereit, aber in C lässt sie sich nicht abfragen. So viel zu Cs Maschinennähe.
Bei Modula gibt es einige Sicherheitsnetze mehr. Erstens werden zur Laufzeit die Ergebnisse aller Berechnungen soweit möglich überprüft, und wenn ein Überlauf festgestellt wurde, wird eine Ausnahme ausgelöst. Problematisch ist, wenn ein Modula-3-Übersetzer auf einem Code-Generator wie dem vom GNU-C aufsetzt, welcher solchen Überlaufschutz nicht anbietet. Das ist aber eine Verletzung des Modula-3-Standards. Zusätzlich gibt es die Möglichkeit schon im Programm den Wertebereich einer Zahl einzuschränken. Wenn man zum Beispiel weiß, dass man immer nur mit drei Farbkomponenten arbeiten wird, könnte man
VAR
  color0 : [0..2];
  color1 : {Red, Green, Blue};
deklarieren und der Übersetzer und das Laufzeitsystem würden ständig darüber wachen, dass niemals mit zu großen Werten gerechnet wird. Leider ermöglicht auch Modula-3 keine Abfrage der Prozessorflags nach einer arithmetischen Operation.
Klassen oder Objekte
Was objektorientierte Programmierung anbetrifft, ist es um den Gebrauch der deutschen Sprache nicht besonders gut bestellt. Fehlübersetzungen wie Instanz (statt konkretes Objekt einer Klasse) oder auch Ungetüme wie Objekt-Instantiierung (statt Erzeugung eines Objektes einer Klasse) helfen dem Verständnis ganz bestimmt nicht weiter. Einen kleinen Schnitzer hat man sich auch bei den Schlüsselwörtern in C++ erlaubt. Typennamen in C wie char (character = Zeichen), int (integer = ganze Zahl), float (float = Gleitkommazahl) bezeichnen einzelne Objekte. Daher lesen sich
char c;
int n;
float x;
wie "das Zeichen c", "die ganze Zahl n" und "die Gleitkommazahl x". Nicht ganz so leicht ordnet sich hier der Typkonstruktor struct (Datenstruktur oder strukturierter Datensatz?) ein. Aber eindeutig aus der Reihe tanzt class, denn das bezeichnet nicht ein Objekt sondern eine Menge von Objekten. Die Deklaration
class car c;
suggeriert, dass c eine Klasse von Autos bezeichnet.
In Modula-3 heißt der Typkonstruktor für Objekte OBJECT und auch sonst hat man sich an die Einzahl- und Elementbezeichnungskonvention gehalten: INTEGER, CARDINAL, REAL, LONGREAL, CHAR, BOOLEAN. Insbesondere bei den Typkonstruktoren RECORD, ARRAY, REF erhöht das die Lesbarkeit enorm. Nicht auszudenken, wenn es REFS ARRAYS [0..9] OF CHARS hieße oder REFCLASS ARRAYCLASS [0..9] OF CHARSET.
Typen immer Namen einzelner Objekte zu geben ist auch für selbstdefinierte Typen eine gute Faustregel, die einem manches Durcheinander ersparen kann.
Felder und Zeiger
Zeigerdeklaration
Heißt es
char* c;
oder
char *c;
? Wo da der Unterschied ist? Na ich meine, ob der Stern stärker an den Typ- oder an den Variablenbezeichner bindet? Oder anders gefragt:
char* c,d;
Bezeichnet das
  1. zwei Zeiger c, d auf Zeichen oder
  2. einmal einen Zeiger c auf ein Zeichen und ein Zeichen d?
Wenn man C oft benutzt, weiß man, dass die zweite Interpretation diejenige ist, die auch der Übersetzer bevorzugt. Dumm nur, dass die Syntax keinerlei Schlüsse zulässt.
Bei Modula stellt sich so eine Frage gar nicht. Typ- und Variablenbezeichner sind durch Doppelpunkt getrennt und es wird anhand der Notation sofort klar, dass Zeiger immer Bestandteil des Typen sind (im Gegensatz zu C). Für jede Liste von Variablen gibt es genau einen Typen.
VAR
  c    : CHAR;
  d, e : REF CHAR;
Zeigerdeklaration und Arithmetik
Preisfrage: Was steht hier?
pi * diameter;
Blöde Frage, das ist natürlich die bekannte Formel zur Berechnung des Kreisumfanges aus dem Kreisdurchmesser. Lediglich das Speichern des Ergebnisses wurde vergessen.
Schaut noch einmal genau hin. Ganz genau. Würdet ihr eure Antwort ändern, wenn ich noch 100 € drauf lege? Ach nein, das ist die falsche Sendung.
Ich habe noch nicht verraten, dass pi ein Typbezeichner ist. Er steht für "product information" oder so und wird weiter oben im Programm definiert. Demnach wird hier die Zeigervariable diameter eingeführt, welche auf Objekte vom Typ pi zeigt.
Wir sehen, dass in C die Syntax für Deklarationen und arithmetische Ausdrücke nicht unterschieden werden kann, dass man also zum Erkennen der Programmstruktur bereits Informationen über die Bedeutung der Bezeichner benötigt. Bei C++ wird übrigens alles noch viel besser.
In Modula-3 gibt es dieses Problem aus zweierlei Gründen nicht. Erstens muss man mit EVAL ausdrücklich erwähnen, dass man das Ergebnis eines arithmetischen Ausdrucks verwerfen will. Zweitens wird bei einer Variablendeklaration Variablen- und Typname durch einen Doppelpunkt getrennt.
Felder von Zeigern und umgekehrt
Bezeichnet
char *c[10];
ein Feld von 10 Zeigern auf Zeichen oder einen Zeiger auf ein Feld mit 10 Zeichen? Da konnten sich die C-Vordenker anscheinend nicht entscheiden, ob sie "Typkonstruktoren" lieber vorne oder lieber hinten anbauen. Der geschulte C-Programmierer ist wieder mal im Vorteil und weiß, dass es sich hier um ein Feld aus 10 Zeigern handelt.
Auch hier zeigt Modula eine klare und unmissverständliche Linie: Typkonstruktoren werden immer von rechts nach links angebaut.
VAR
  c : REF ARRAY [0..9] OF CHAR;
  d : ARRAY [0..9] OF REF CHAR;
Da bleibt keine Frage offen und man kann direkt vorlesen: "Zeiger auf Feld von 10 Zeichen" bzw. "Feld von 10 Zeigern auf jeweils ein Zeichen"
Zeiger auf Felder fester Länge
Nachdem wir gesehen haben, dass
char *c[10];
ein Feld von Zeigern deklariert, möchten wir trotzdem wissen, wie man nun einen Zeiger auf ein Feld mit 10 Elementen anlegt. Bekommt ihr es selbst heraus? Kleiner Tip: Versucht es nicht mit Logik! Die Antwort lautet:
char (*c)[10];
Klasse, nicht wahr? Wer es den Lesern seiner Programme leichter machen möchte, sollte die Deklaration besser in
typedef char array10char[10];
array10char *c;
zerlegen.
Syntax von Deklarationen mit Zeigern und Feldern
Nun fragt man sich natürlich besorgt, wieso C diese merktümliche Syntax verpasst bekommen hat. Oder ist sie gar nicht merkwürdig, sondern erscheint nur so durch die Brille der Typkonstruktoren, also Funktionen welche Typen in Typen verwandeln?
Folgendes zur Erklärung und als Eselsbrücke, falls ihr wieder über Zeigerdeklarationen verzweifelt: Die Deklaration ist der Anwendung der Variablen nachempfunden. Daher sollte man
char (*c)[10];
nicht lesen als "c ist ein Zeiger auf ein Feld bestehend aus 10 Zeichen", sondern besser als "Es ist möglich die Variable c zu dereferenzieren und dann auf das zehnte Zeichen (na gut, das gerade nicht mehr) zuzugreifen." So ergibt auch die in C-Büchern zu findende Aussage einen Sinn, dass [] stärker als * bindet. Sie besagt, dass char *c[10]; und char *(c[10]); das gleiche bedeuten. Wären allerdings * und [] Typkonstruktoren, dann würde char *(c[10]); heißen, dass c im Inneren ein Feld von zehn Zeichen ist, aber insgesamt eher ein Zeiger auf ein solches. Bei der Verwendung von c verhält es sich aber genau umgekehrt zur Deklaration. *c[3], oder äquivalent *(c[3]), bedeutet dann nämlich, dass zuerst das dritte Element von c ausgelesen wird und dem darin enthaltenen Zeiger gefolgt wird. Das ist aber nur möglich wenn c ein Feld von Zeigern ist. Die Deklaration von Variablen ist in gewisser Weise implizit. Man sagt nur, was man im Extremfall mit der Variablen vorhat.
Jetzt noch ein Brückenschlag zurück zu den Typkonstruktoren: Während die Operatoren [] und * Werte komplexer Datentypen auf Werte einfacherer Typen abbilden, ist es bei Typkonstruktoren genau umgekehrt, zumindest wenn man davon absieht, dass [] und * auf Werten operieren, und Typkonstruktoren auf Typen. Mit einigem Recht können wir daher Typkonstruktoren mit []-1 und *-1 bezeichnen. [] ermittelt von einem Feld ein einzelnes Element, []-1 dagegen baut aus dem Typ eines einzelnen Elementes ein Feld über diesen Typ. * löst einen Zeiger auf, um ein Element zu ermitteln, und umgekehrt baut *-1 um einen Elementtyp einen Zeigertyp.
Man kann die Variablendeklaration in C so auffassen, dass links der Typ steht und rechts eine exemplarische Anwendung. Nun kann man diese Deklaration Schritt für Schritt in eine Deklaration umformen, in der links ein komplexer Typ steht und rechts nur noch der Variablenname.
char *c[10];
char *(c[10]);
(*^-1 char) c[10];
(*^-1 char)[10]^-1 c;
Es sei an dieser Stelle darauf hingewiesen, dass Pascal, also ein Vorgänger von Modula aus dem Hause Niklaus Wirth, ebenfalls das gleiche Zeichen für Dereferenzierung und Zeigerdeklaration verwendet. Man schreibt etwa
VAR
  p: ^INTEGER;
BEGIN
  p := q;
  Write(p^);
. Man sieht schon, dass die beiden Dächer unterschiedlich platziert werden: Das Zeigerdeklarationsdach vor dem Elementtyp und das Dereferenzierdach nach dem Zeiger. Dennoch dürfte das gleiche Symbol für verschiedene Zwecke für einige Verwirrung sorgen. In Modula-2 wurde das Dach zum POINTER TO, während das Dereferenzierdach geblieben ist.
VAR
  p: POINTER TO INTEGER;
BEGIN
  p := q;
  Write(p^);
In Modula-3 dann wurde dann POINTER TO zu REF und UNTRACED REF.
Zeiger als offene Felder
Wenn c ein Zeiger auf ein Zeichen ist, also
char *c;
wieso kann man den auch wie ein Feld behandeln und Elemente abfragen
char d = c[4];
? Na ist doch praktisch, da muss man sich nicht vorher überlegen, ob man lieber mit einem Feld oder einem Zeiger arbeiten will. Ja, so eine Entscheidung ist auch unheimlich schwer zu fällen, weil ein Zeiger und ein Feld doch fast das gleiche sind, oder wie? Na mal im ernst: Dass in C alle Zeiger zugleich offene Felder (also Felder ohne festgeschriebene Länge) sind, ist vor allem gefährlich. Denn wenn man aus Versehen den Zeiger mit einem Feld verwechselt und irgendein Element liest oder schreibt, das es gar nicht gibt, können weder der Übersetzer noch das Laufzeitsystem helfen. Im günstigsten Fall stürzt der Rechner sofort ab und löscht den C-Übersetzer gleich mit von der Platte. Im schlechtesten Fall wird der Fehler gar nicht entdeckt, sondern meldet sich während einer Präsentation auf einem fremden Rechner, die potentielle Geldgeber von einem Programmprojekt überzeugen sollte.
Bei Modula wird streng unterschieden zwischen Zeigern auf Objekte und Zeigern auf Feldern von Objekten. Das ist so simpel wie logisch und sicher.
VAR
  c : REF CHAR;
  d : REF ARRAY OF CHAR;
Insbesondere merkt sich das Laufzeitsystem bei Zeigern auf offene Felder deren Länge und überprüft auch bei jedem Feldzugriff, ob die Indizes im grünen Bereich sind. Man kann diese Tests umgehen, indem man fleißig direkt mit den Zeigern hantiert. Solche Dinge müssen aber in als unsicher deklarierte Module verbannt werden.
Felder fester Länge als Zeiger
Wenn wir
char c[4], d[6];
deklarieren, ist dann wenigstens klar, dass mit d ein Feld gemeint ist und kein Zeiger? Auf der einen Seite schon, denn sizeof(d) liefert als Ergebnis die Länge des Feldes, nämlich 6, und nicht die Länge des Zeigers, etwa 4 bei 32-Bit-Maschinen. Auf der anderen Seite wieder nicht, denn die Wertumwandlung (cast) (int)(d) sollte erstens wegen unterschiedlicher Größe von Feld und ganzen Zahlen vom Übersetzer zurückgewiesen werden, und zweitens sollte (int)(c) das Bitmuster des Feldinhaltes von c als ganze Zahl interpretieren. Tatsächlich wird die Umwandlung aber immer erlaubt und man erhält die Adresse des ersten Elementes als ganze Zahl.
In Modula-3 ist das ganz klar geregelt.
VAR
  c : ARRAY [0..3] OF CHAR;
  d : ARRAY [0..5] OF CHAR;
Dann ergibt NUMBER(d) den Wert 6, ADDRESS(d) gibt die Adresse des Feldes als ganze Zahl zurück (nur in unsicheren Modulen erlaubt), LOOPHOLE(d,INTEGER) ist verboten und LOOPHOLE(c,INTEGER) interpretiert das Bitmuster von c als ganze Zahl und ist wiederum nur in unsicheren Modulen erlaubt.
Speicherverwaltung für offene Felder
In C++ kann man Speicher für offene Felder besonders bequem reservieren:
char *c = new char[len];
Nicht selten geben Programmierer aber später nur das erste Element wieder frei
delete c;
, korrekt ist aber
delete [] c;
. Die Funktion new legt im Falle eines reservierten Feldes nämlich noch irgendwo die Länge des Feldes ab, und das kann delete nicht wissen. Der Übersetzer hat beim Analysieren des Programmtextes keine Möglichkeit herauszubekommen, ob c auf ein einzelnes Zeichen oder ein ganzes Feld zeigt. Daher kann er nicht prüfen, ob delete oder delete [] korrekt ist.
In Modula-3 reserviert man Speicher für Felder so:
c := NEW(REF ARRAY OF CHAR, len);
Derart reservierten Speicher gibt man gar nicht selbst frei, das macht der Garbage-Collector (Müllsammler) ganz allein und viel zuverlässiger als jeder menschliche Programmierer. Wenn man glaubt, dass man es effizienter als die automatische Müllabfuhr beherrscht, verwendet man
c := NEW(UNTRACED REF ARRAY OF CHAR, len);
. Anhand des Types erkennt der Übersetzer, dass c auf ein Feld zeigt und dass dessen Speicher niemals automatisch freigegeben wird. Mit
DISPOSE(c);
gibt man den Speicher frei. DISPOSE ist übrigens nur für Experten und darf daher nur in unsicheren Modulen verwendet werden. Deshalb braucht man mehrfache Speicherfreigabe nur in Modulen zu suchen, die mit einem rot blinkenden UNSAFE beginnen.
Deklaration mehrdimensionaler Felder
Wie deklariert man eigentlich mehrdimensionale Felder in C? Logisch wäre es, wenn man sie als Felder von Feldern deklarierte. Das bedeutet, dass viele kleine Felder in einem großen Feld zusammengefasst werden, so dass jeder Eintrag des großen Feldes (zweidimensional) wieder ein kleines Feld (eindimensional) ist. Gut, aber wie verklickert man das C?
int a[10][20];
Das deklariert ein Feld namens a mit der Länge 10, das Felder bestehend aus 20 ganzen Zahlen beherbergt. Es handelt sich aber wirklich um ein Feld von Feldern, nicht etwa um ein Feld von Zeigern auf Felder.

Wie gelangt man nun zu den einzelnen Einträgen des Feldes, also den Zahlen? Die Deklaration

int b = a[3][5];
schreibt den Wert des fünften Eintrag des dritten Unterfeld in die Variable b. Wenn wir uns a als Matrix vorstellen, würden wir sagen: Das Element in der dritten Zeile und der fünften Spalte. Nun sind wir aber gewohnt, die Indizes von Matrizen mit Kommata zu trennen, so wie in Basic, Pascal, Modula usw. das probieren wir doch gleich aus:
int b = a[3,5];
Uff, was ist denn da für eine seltsame Zahl in b angekommen? Tja, C fand es anscheinend sinnvoller, nur den zweiten Index zu beachten. Häh? Jawohl, die drei Ausdrücke a[i,j], a[j], a+j bezeichnen alle dasselbe, nämlich den Zeiger auf das j. Unterfeld! Warum? Wieso nicht, ist doch eine praktische Einrichtung. Ich benötige das laufend, Ihr nicht? :-) Man kann übrigens auch mit
int a[10,20];
ein Feld mit 20 Ganzzahlen anlegen, mit
int a[-10,3.141,20];
selbstverständlich auch.

Ihr findet das seltsam? Ich nicht, denn alles hier gezeigte geht auf die Segnungen des Komma-Operators zurück.

In Modula-3 kann man mehrdimensionale Felder durch zweierlei Schreibweisen anlegen:
VAR
  a : ARRAY [0..9] OF ARRAY [0..19] OF INTEGER;
  b : ARRAY [0..9],[0..19] OF INTEGER;
Die zweite Variante ist syntaktischer Zucker, also eine Schreibvereinfachung. Auch beim Zugriff auf die Feldelemente gibt es die logische und die verzuckerte Variante:
c := a[2][5];
c := a[2,5];
a[2] bezeichnet das zweite Unterfeld, a[2][5] dessen fünften Eintrag. Die Kurzschreibweise mit Komma-getrennten Indizes ist ebenso erlaubt und führt zum erwarteten Ergebnis. In Modula-3 gibt es keine eigenständige Komma-Operation.
Es gibt leider bei mehrdimensionalen Feldern eine Ausnahme, die die Logik aushebelt. Mit ARRAY OF INTEGER bezeichnet man ein Feld offener Länge, d.h. die Länge des Feldes wird erst zur Laufzeit festgelegt, nämlich sobald Speicher für das Feld reserviert wird. Demzufolge müsste ARRAY OF ARRAY OF INTEGER ein Feld offener Größe sein, das selbst Felder verschiedener Größen enthalten kann. Etwa:
CONST
  CrazyArray =
    ARRAY OF ARRAY OF INTEGER {
      ARRAY OF INTEGER {1},
      ARRAY OF INTEGER {1, 1},
      ARRAY OF INTEGER {1, 2, 1},
      ARRAY OF INTEGER {1, 3, 3, 1},
      ARRAY OF INTEGER {1, 4, 6, 4, 1}
    };
Modula-3 erlaubt aber nur rechteckige Felder variabler Größe, und obige Struktur lässt sich nur als ein Feld aus Zeigern auf offene Felder realisieren. Rechteckige Felder sind wahrscheinlich die häufiger verwendeten, sie widersprechen aber der ARRAY OF ARRAY OF INTEGER-Deklaration. Man merkt das zum Beispiel auch daran, dass man zum Abfragen der Ausdehnung in einer Dimension immer die niederen Dimensionen adressieren muss. Man muss zum Beispiel NUMBER(CrazyArray[0]) schreiben, um die Ausdehnung in der zweiten Dimension zu erfragen, obwohl der Wert 0 in den eckigen Klammern völlig unerheblich ist. Schlimmer noch, wenn das Feld in der ersten Dimension keine Ausdehnung hat (NUMBER(CrazyArray) = 0), führt der Ausdruck CrazyArray[0] bereits zu einer Bereichsverletzung. Besser wäre also ein Typkonstruktor wie TENSOR, den man etwa so verwendet:
VAR
  block: TENSOR 2 OF INTEGER;
  size0 := NUMBER(block, 0);
  size1 := NUMBER(block, 1);
Es gibt C++-Programmierer, die wegen der gezeigten Sonderlichkeiten von den integrierten C-Feldern abraten und einem die C++-Template-Klasse vector empfehlen. Ich möchte durchaus unterstellen, dass durch geeignete Code-Optimierung die Vektorklasse genauso speicher- und zeiteffizient sein kann, wie die originalen C-Felder, aber man wird mit dieser Implementierung niemals statische Fehler sofort aufdecken können. Wenn man beispielweise
int a[10];
a[20] = 34;
mit der Vektor-Klasse implementiert, etwa so:
vector <int> a(10);
a[20] = 34;
dann wird sie den Fehler bestenfalls entdecken, wenn dieser Programmteil ausgeführt wird, obwohl der Fehler schon beim Übersetzen offensichtlich ist. Da aber Wertebereiche von C auch bei den einfachen C-Feldern nicht statisch überprüft werden, verlieren C-Programmierer damit nichts.
Außerdem zählt einmal mehr der Vorwurf, dass Sprachfehler nicht durch Hinzufügen weiterer Lösungsansätze aus der Welt zu schaffen sind. Die C-Felder gibt es weiterhin in C++, dürfen benutzt werden und werden auch fleißig genutzt. C++ bietet unter dem Strich zwei Varianten von Feldern an: Eine unzulängliche und eine völlig unzulängliche. Der Programmierer muss sich jedesmal neu für das jeweils kleinere Übel entscheiden.
Initialisierung mehrdimensionaler Felder
Felder möchte man nicht nur anlegen, sondern gelegentlich auch mit Inhalt füllen. Nehmen wir doch einfach mal ein Feld konstanten Inhalts:
const static double plane[5][3] = {
  {0, 1, 2},
  {10, 11, 12},
  {20, 21},
  {30, 31, 32},
  {40, 41, 42},
};
Oops, der C-Übersetzer hat sich gar nicht darüber beschwert, dass wir in der mittleren Zeile einen Wert vergessen haben. Aha, er beschwert sich auch nicht, wenn ich die geschweiften Klammern ganz weglasse.
const static double plane[5][3] = {
  0, 1, 2,
  10, 11, 12,
  20, 21,
  30, 31, 32,
  40, 41, 42,
};

Tja, ob es auch möglich ist, eine Zeile des neuen Feldes mit dem Inhalt eines bereits existierenden konstanten eindimensionalen Feldes zu initialisieren?
Mehrdimensionale Felder werden hier völlig logisch aus niederdimensionalen Feldern zusammengesetzt. Das ist mit mehr Schreibaufwand verbunden, den man durch Typdefinitionen reduzieren sollte. Vielleicht ist es das, was die C-Schöpfer zu der fehlerträchtigen Initialisierung mit eindimensionalen Feldern bewog.
TYPE
  Arr3 = ARRAY [0..2] OF LONGREAL;

CONST
  Plane = ARRAY OF Arr3 {
            Arr3 {0, 1, 2},
            Arr3 {10, 11, 12},
            Arr3 {20, 21, 22},
            Arr3 {30, 31, 32},
            Arr3 {40, 41, 42}
          };
Auch das Initialisieren mit einem existierenden Feld bereitet keinerlei Kopfschmerzen.
CONST
  Line  = Arr3 {20, 21, 22};
  Plane = ARRAY OF Arr3 {
            Arr3 {0, 1, 2},
            Arr3 {10, 11, 12},
            line,
            Arr3 {30, 31, 32},
            Arr3 {40, 41, 42}
          };
Feldindizierung
Nachdem wir nun schon so viel Spaß mit dem Anlegen von Feldern hatten, wie ist es dann mit der Benutzung derselben? Wenn a ein Feld ist, dann bezeichnet a[i] dessen i. Eintrag. Lustigerweise bezeichnet i[a] aber dasselbe! Der Grund ist, dass a[i] syntaktischer Zucker für *(a+i), wobei a+i Zeigerarithmetik ist. Bei der Addition eines Zeigers a und einer ganzen Zahl i (Reihenfolge egal) hat das Ergebnis den Typ des Zeigers. Allerdings wird nicht einfach der Wert von i zu dem Zahlenwert des Zeigers a addiert, sondern es wird das Produkt von i und der Größe des Datums, auf das a zeigt, zu a addiert. Diese Erkenntnisse sind essentiell, um bei Wettbewerben um möglichst unverständlichen C-Code gut abschneiden zu können.
Da muss noch mehr herauszuholen sein! Wie wär's denn mit zweidimensionalen Feldern? Normalerweise indiziert man ein zweidimensionales Feld a so: a[i][j]. Aber mit unserem Wissen können wir das gleiche viel schöner schreiben, etwa:
  • i[a][j]
  • j[i[a]]
  • j[a[i]]
  • (a+i)[0][j]
  • ((a+i)[0]+j)[0]
  • j[*(a+i)]
  • (*(a+i))[j]
  • *((a+i)[0]+j)
Das ist ein absolut unverzichtbares Werkzeug um den Lesern seiner Programme überdurchschnittliches Wissen über C zu demonstrieren. Entwickler aller Länder schätzen es, Programme von solch erfahrenen Programmierern weiterentwickeln zu dürfen.
In Modula ist ein Feld etwas völlig anderes als ein Zeiger. Daher kann man mit Feldern keine Zeigerarithmetik betreiben. In Modula-3 ist darüber hinaus das Rechnen mit Zeigern nur in unsicheren Modulen erlaubt.
Konstanten
const-Deklarationen
Was das const-Attribut anbelangt, gibt es einige Verwirrungen. Vor eine Variablendeklaration geschrieben, bedeutet es eigentlich, dass diese Variable unveränderbar ist. Die Variable kann aber durchaus mit zur Laufzeit bestimmten Werten initialisiert werden. Das hält aber zum Beispiel den GCC 3.3.1 nicht davon ab, das folgende Programm ohne Murren zu übersetzen:
const int a = 6;
a = 8;
printf ("%i %x\n", a, (int) &a);
Ist a eine lokale Variable, läuft das Programm sogar. Ist a dagegen global, dann stürzt das Programm ab, weil a in einem schreibgeschützten Speicherbereich abgelegt wird.
Muss der Übersetzer immer einen Speicherplatz für solche Konstanten bereithalten? Darf er den Wert einer zur Übersetzungszeit bekannten Konstante direkt in das folgende Programm integrieren, oder muss das Programm den Wert immer aus der zugehörigen Speicherzelle laden? Darf er also b = a durch b = 6 ersetzen, oder muss er es so lassen, wie es da steht?
In Modula gibt es auch Konstanten, sie sind aber noch etwas "konstanter" als in C, denn sie dürfen nur mit Werten belegt werden, die schon beim Übersetzen des Programmes bekannt sind. (Leichte Ausnahme: Prozedurkonstanten) Damit sind sie eher mit enum-Definitionen in C zu vergleichen.
Zusätzlich gibt es die Möglichkeit mit WITH Ausdrücken Abkürzungen zuzuweisen. Es bedeutet
WITH a = b DO
  a := 5;
END;
das gleiche wie
b := 5;
. Bezeichnet b etwas Schreibbares (eine Variable, ein Feldelement), dann ist auch a schreibbar. Enthält der Ausdruck b eine Berechnung, also nichts Überschreibbares, dann ist auch a nicht überschreibbar. Eine Rechnung wird nur einmal ausgeführt, nicht etwa bei jedem Auftreten von a. In letzterem Falle hätte man also so etwas wie eine Konstante angelegt, die an einer gewissen Stelle initialisiert wird und später nicht mehr geändert werden kann.
konstante Zeiger
Man kann auch Zeiger mit Schreibverbot belegen. Man kann sogar genau steuern, ob Zeiger oder Speicherinhalt schreibgeschützt sein sollen:
const char * s0 = new char[10];
char const * s1 = new char[10];
char * const s2 = new char[10];
Nanu, das sind drei Markierungsmöglichkeiten für zwei Dinge - ein Zeiger und ein Feld. Die Auflösung des Rätsel ist, dass eine Aussage auf zwei verschiedene Weisen formuliert werden kann. Die letzte Zeile bedeutet, dass der Zeiger schreibgeschützt ist, die ersten beiden Zeilen bedeuten dagegen, dass die Zeichenkette schreibgeschützt sein soll. Eine konsistente Regel ist damit: Das Schlüsselwort const steht immer hinter dem Objekt, das schreibgeschützt werden soll. Meistens bekommt man die erste Variante zu sehen, also genau die einzig inkonsistente. Aber daran haben wir uns inzwischen gewöhnt.
In Modula-3 gibt es keine Schreibschutzattribute für Typen, deswegen gibt es auch keine inkonsistente Syntax. Es gibt nur
VAR
  a : INTEGER := 5;

CONST
  b : INTEGER = 5;
und Übergabemodi.
Schreibschutz umgehen
So hilfreich das Konzept der unveränderlichen Inhalte auch ist - es wirft Probleme auf:
char *s0 = new char[10];
const char *s1 = s0;
s0[0] = '!';
Das const-Attribut sichert also lediglich, dass man den Inhalt nicht über s1 ändern kann, nicht aber, dass der Inhalt immer gleich bleibt.
Es gibt in Modula-3 ein ganz ähnliches Problem mit den Übergabemodi der Prozedurparameter:
PROCEDURE Work (VAR a : CHAR; READONLY b : CHAR; );
Weder die Prozedur Work noch der Aufrufer dieser Prozedur können davon ausgehen, dass b während der Abarbeitung von Work nicht beschrieben wird. Gefahr ist dann im Verzug, wenn die gleiche Variable an die Parameter a und b übergeben wird.
Schreibschutz und Initialisierung
Wir halten also fest, dass const nicht wirklich Konstantheit sichert. Aber wenn eine Variable wirklich schreibgeschützt ist, kann man ja nie etwas hineinschreiben! Deswegen wird bei C und C++ zwischen Beschreiben und Initialisieren unterschieden. Eine schreibgeschützte Variable darf man einmal initialisieren und danach nur noch lesen.
Um schreibgeschützte Variablen in jedem Falle mit den gewünschten Werten ins Rennen schicken zu können, musste man in C++ noch etliche Zusätze einbauen. Da const für jeden Typ erlaubt ist, kann man konsequenterweise auch einzelne Objektelemente vor dem Überschreiben schützen. Um diese Elemente initialisieren zu können, muss man direkt beim Anlegen des Objektes eingreifen. Deshalb braucht C++ neben normalen Objektmethoden noch Konstruktoren. Wenn die Werte der schreibgeschützten Objektelemente außerdem voneinander abhängen, oder wenn eine Berechnung von mehreren Konstruktoren einer Klasse genutzt werden soll, wird das ganze beliebig kompliziert oder man muss letzten Endes doch auf den Schreibschutz verzichten.
In Modula-3 hat man alles ausgelassen, was nicht ausgegoren ist. So praktisch der Schreibschutz in C und C++ auch häufig ist, er zieht doch viele andere Flickereien an der Sprache nach sich und kann nicht konsequent angewendet werden.
In Modula-3 gibt es daher keine schreibgeschützten Typen und auch keine Zeiger auf schreibgeschützte Inhalte. Es gibt lediglich Übergabemodi für Prozedurparameter.
PROCEDURE Test (         a : INTEGER;
                VAR      b : INTEGER;
                READONLY c : INTEGER; );
Die Prozedur Test bekommt in a eine Kopie des übergebenen Wertes, in b einen Zugriff mit Schreibrechten auf die übergebene Variable, in c einen Zugriff mit Schreibverbot auf die übergebene Variable. (Im übrigen finde ich, dass die Schlüsselwörter INOUT statt VAR und IN statt READONLY die bessere Wahl gewesen wären. Erstens weil so der Unterschied zwischen beiden Varianten schneller klar wird und zweitens weil VAR suggeriert, dass wir es hier mit einer Variablendeklaration zu tun haben. (Hm, IN ist allerdings schon für die Enthaltenseinsrelation bei Mengen vergeben.))
Es gibt in Modula-3 keine Initialisierung und es bedarf daher auch keiner Konstruktoren. Man kann bei der Deklaration von Datenverbund- und Objekttypen Vorgabewerte vereinbaren, aber diese müssen konstant sein. Es bleibt leider die Gefahr, dass Objekte erzeugt, aber nicht initialisiert werden.
Vorrangregeln
Addition vor Bitverschiebung
Wir haben in der Schule einst gelernt, dass Punktrechnungen vor Strichrechnungen auszuführen sind. Nun kann diese Festlegung sinnvoll sein oder auch nicht, aber in der Praxis hat sich zumindest für mich erwiesen, dass sich dadurch gegenüber der Regelung "von links nach rechts" oder gar "Strichrechnung vor Punktrechnung" viele Klammern sparen lassen. Man denke nur an Polynomausdrücke, etwa a+b*x+c*x^2 statt a+(b*x)+(c*(x^2)). Ein Schelm, wer hier mit (x-a)*(x-b)*(x-c) kontert! C hat diese Regelung teilweise verwirklicht, aber natürlich nicht konsequent. Auch in C haben '*' (Multiplikation) und '/' (Division) Vorrang vor '+' (Addition) und '-' (Subtraktion). Aber es gibt auch Multiplikationen, die nicht sofort als Multiplikationen zu erkennen sind. Zum Beispiel entsprechen die Bitverschiebungen '<<' und '>>' im Prinzip Multiplikationen mit Zweierpotenzen.
a << b = a * 2b
a >> b = a / 2b
Das hindert C aber nicht daran, Additionen auf der linken oder rechten Seite mit dem linken Faktor bzw. dem rechten Exponenten zusammenzuziehen, d.h.
a + b<<c + d = (a+b) << (c+d)
In Modula gibt es die (maschinennahen) Verschiebeoperation nicht mit Infix-Notation sondern als Inline-Funktionen Word.Shift und Word.Rotate, so dass sich die Frage nach einer sinnvollen Vorrangregelung nicht stellt. Handelt es sich um eine konstante Verschiebungsweite, sollte man auch überlegen, ob man nicht eigentlich eine Division oder eine Multiplikation meint, die nur zufällig eine Zweierpotenz als Operanden besitzt. Dann sollte man das auch so hinschreiben, denn der Übersetzer weiß selber, wie er es auf dem jeweiligen Rechner möglichst effizient umsetzen kann. Statt Word.Shift(x,3) sollte man dann x*8 und statt Word.Shift(x,-3) sollte man x DIV 8 schreiben und statt Word.And(x,7) natürlich x MOD 8.
Relation vor Bitlogik
Das gleiche Spiel mit den 'größer'- und 'kleiner'-Relationen: Da es etliche Operationen gibt, die Zahlen als Eingabe nehmen und Wahrheitswerte zurückliefern (eben jene Ordnungsrelationen, wie '<' und '>'), es anderseits aber keine Operation gibt, welche Wahrheitswerte erwartet und Zahlen ausgibt, sind logische Verknüpfungen wie 'und' und 'oder' in der Regel diejenigen, die ganz zum Schluss ausgeführt werden, also geringen Vorrang haben. Etwas höheren Rang haben die Operationen, welche Zahlen zu Wahrheitswerten verarbeiten (Relationen) und den höchsten Rang haben die Operationen, welche Zahlen zu Zahlen machen.
Auch hier muss C ein wenig Unordnung stiften, indem es den Relationen höheren Rang einräumt, als den Bitlogik-Ausdrücken, wobei Bitlogik im Sinne von C auch Verknüpfen von Zahlen bedeutet. Aber auch wenn man Bit-Operationen nur auf Bitvektoren zulassen würde, wäre die jetzige Vorrangregelung in C ungünstig.
a < b & c = (a < b) & c
a & b < c = a & (b < c)
Anscheinend betrachteten es die Schöpfer von C als sinnvoll, mit Bitlogikoperationen an Wahrheitswerten herumzudoktorn. Der gleiche Denkfehler, der auch oben erwähntem C-Programmierer unterlief.
In Modula haben die Symbole für arithmetische Operationen und Relationen immer noch eine andere Bedeutung für Mengen, z.B. Vereinigung, Durchschnitt oder Teilmengenbeziehung. Der Modula-Ausdruck a*b < c hat also Sinn und wird vom Übersetzer interpretiert als der Test, ob der Schnitt der Mengen a und b echt in der Menge c enthalten ist.
Klammern
Typumwandlungen
Was bedeutet ein Ausdruck von so ergreifender Schlichtheit wie
(A)(B)
? In C gibt es bereits zwei Möglichkeiten:
  1. Der Wert von B wird in den Typen A konvertiert.
  2. Die Funktion A wird mit dem Wert B aufgerufen.
Was es genau sein soll, weiß man erst, wenn man festgestellt hat, ob A ein Typ oder eine Funktion ist. Schlimmer noch: Funktionsauswertung hat Vorrang vor Typumwandlung. Das bedeutet zum Beispiel, dass nicht klar ist, ob (A)(B)->C
  1. (A)((B)->C) oder
  2. ((A)(B))->C
bedeutet, bevor man weiß, was A ist. Mit anderen Worten, ein Programm, das lediglich die Syntax analysiert, ohne sich Bezeichner und deren Bedeutung zu merken, wird nicht funktionieren, denn zur Analyse der Syntax muss diese Information ebenfalls vorhanden sein. In C++ wird es noch erlesener, denn dort kann man die Bedeutungen der Klammern für Funktionsaufrufe sogar umdefinieren.
In Modula-3 werden Konvertierungen aller Art immer durch (eingebaute) Funktionen erledigt. Das ist schon deswegen wichtig, weil es verschiedene Arten gibt, einen Wert von einem Typ in einen anderen Typ umzudeuten. Zum Beispiel konvertiert man mit Funktionen wie FLOAT und ROUND zwischen numerisch ähnlichen Werten, während LOOPHOLE einfach das interne Bitmuster übernimmt.
Prototyp oder Konstruktor
In
class point
{
public:
  point (int x, int y) { };
};

point p (x, y);
kann die Deklaration point p(x,y); bedeuten:
  1. Falls es irgendwo ein int x, y; gibt, dann erzeugt es ein point-Objekt mit Namen p abhängig von den Parametern x und y.
  2. Falls irgendwo ein typedef int x, y; herumschwirrt, dann ist es wohl ein Prototyp für die Funktion p welche Argumente mit Typen x und y erwartet und point zurückgibt. Es sei denn es gibt darüber hinaus noch ein #define typedef ...
In Modula-3 werden Deklarationen in Blöcken zusammengefasst, etwa TYPE, CONST, VAR, so dass immer klar ist, welche Art Objekte den Leser des Programmes als nächstes erwartet. Leider wurde das nicht ganz konsequent durchgehalten, denn eigentlich sind Unterprogramme so was wie Konstanten. Statt
PROCEDURE Test (a: INTEGER; x: LONGREAL; );
wäre
CONST
  Test : PROCEDURE (a: INTEGER; x: LONGREAL; );
die konsistentere Schreibweise. Mit dieser Schreibweise würde man auch durch Analogiebetrachtung leichter herausbekommen, wie man Prozeduren unter einem zusätzlichen Namen sichtbar machen kann. Nämlich so:
CONST
  Toast = Test;
Außerdem wäre es so leichter, mehrere Unterprogramme gleichen Typs zu deklarieren:
CONST
  Test0, Test1, Test2 : PROCEDURE (a: INTEGER; x: LONGREAL; );
Diese Schreibweise dürfte Kenner stark an die Notation funktionaler Sprachen erinnern.
Immerhin kann es aber in Modula-3 nicht Verwechlsungen wie in C geben und es ist auch nicht möglich, Schlüsselworte der Sprache umzudefinieren.
Welcher Konstruktor wird aufgerufen?
Angenommen wir haben dir die Definition:
class number
{
public:
  number () {};
  number (int x) {};
};
, was bedeutet dann die Zeile
number(a) = number(b);
? Ganz einfach? Na, passt mal auf:
  1. Falls a eine (vorher) deklarierte Variable ist, ruft number(a) den zweiten Konstruktor der Klasse number auf. Das so erzeugte Objekt wird aber gleich durch das von number(b) erzeugte Objekt überschrieben.
  2. Falls a noch nicht existiert, dann deklariert man mit number(a); ein neues Objekt mit dem Namen a und ruft den ersten Konstruktor auf. Das gleiche würde man auch mit number a; erreichen. Klammern um den neu eingeführten Bezeichner sind aber erlaubt. Manchmal sind sie schließlich sogar notwendig, z.B. bei Zeigern auf Felder.
Lustig sieht auch
number(a)(b);
aus, gell?
Da fällt mir echt nichts vergleichbares ein, weil es in Modula-3 einfach keine Konstruktoren gibt.
Unterprogramme
Parameterlisten und Strukturen
Ursprünglich gab es in C die heute gängige Form der Parameterliste für eine C-Funktion überhaupt nicht. Als sie dann aber eingebaut wurde, durfte es natürlich nicht versäumt werden, eine kleine Inkonsistenz einzuflechten. Eine Parameterliste ist eigentlich nichts anderes als ein C-struct, der für die Funktion auf dem Stapelspeicher angelegt und dadurch übergeben wird. Das könnte vom gleichen Stückchen Code im Übersetzer erledigt werden und beanspruchte nur ein einzelnes Stück der Gedächtniskapazität des Programmiereres. So einfach ist es aber zur Freude aller Beteiligten nicht:
struct test_struct {
  int i,j;
  float x,y;
};

void test_func (int i, int j, float x, float y);
Parameter in Parameterlisten müssen durch Kommata getrennt werden, Einträge in C-Strukturen dagegen mit Semikola. In C-Strukturen kann man bequem mehrere Variablen gleichen Typs anlegen, indem man den Typ einmal hinschreibt und dann alle Variablen dieses Typs mit Kommata getrennt aneinanderreiht. Bei Parameterlisten ist diese Vereinfachung verbaut. Das ist eher ungewöhnlich für eine Sprache, die für tippfaule Hieroglyphenanhänger entwickelt wurde. Ich warte allerdings noch darauf, dass für Parameterlisten das Semikolon als Trenner zwischen Variablen gleichen Typs eingeführt wird. :-) (Vergleiche for-Schleife)
In Modula-3 konnte man sich zwar auch nicht ganz zur Gleichsetzung von Datenverbünden (RECORD) und Prozedurparameterlisten durchringen - für die Übergabe per Referenz gibt es zum Beispiel extra Schlüsselwörter. Man kann aber jede RECORD-Definition ausschneiden und ohne Änderung als Parameterliste für eine Prozedur einsetzen.
TYPE
  TestRecord =
    RECORD
      i,j : INTEGER;
      x,y : REAL;
    END;

PROCEDURE TestFunc (i,j : INTEGER; x,y : REAL; );

Die Asymmetrie bezüglich Funktionsargumenten und Funktionswert ist aber in beiden Sprachen enthalten. Gewissermaßen wird als Funktionsargument immer ein Datenverbund erwartet und die übliche Syntax zum Anlegen eines Datenverbundes drumherum wird weggelassen. Für den Funktionswert wird dagegen kein Datenverbund gefordert, aber in Modula-3 darf es auch einer sein.
Parameterlisten variabler Länge
Eine Eigenheit von C sind Parameterlisten variabler Länge. Diese wird unter dem Stichwort vararg geführt. Damit kann man solche Sachen schreiben wie
fprintf (file, "%d, %d, %d\n", 1, 2, 3);
wobei die Zahlenliste beliebig lang sein kann. Dies erscheint vielen Programmierern so praktisch, dass sie diese Eigenschaft ständig für andere Programmiersprachen vorschlagen. Hier haben wir es mit so einer Versuchung zu tun, der man wirklich widerstehen muss, denn Parameterlisten variabler Länge werfen schwerwiegende Probleme auf:
  1. Die Idee hinter dieser Art Parameterlisten ist ja, dass man versucht, die Einschränkung einer konstanten Anzahl Funktionsparameter zu durchbrechen. Daher gibt es syntaktisch keine Trennung zwischen normalen Parametern und Werten einer Parameterliste variabler Länge. Es ist also schon rein syntaktisch nicht möglich, für eine Funktion mehrere solcher Parameterlisten zu verwenden.
  2. Es ist nicht möglich eine erhaltene Parameterliste an eine andere Funktion weiterzureichen. Es wäre zum Beispiel sinnvoll, dass printf einfach fprintf aufruft und dabei stdout, den Formattierungstext und die zu formatierenden Werte übergibt.
  3. Konkret in C wird die Anzahl der Parameter nicht an die aufgerufene Funktion übergeben! Das ist ein weit geöffnetes Tor für Fehler, daraus resultierende Hackerangriffe und Probleme beim Aufruf solcher Funktionen aus anderen Sprachen heraus.
Was hat eigentlich eine Parameterliste variabler Länge einem Feld als Parameter voraus? Außer der knapperen Syntax eigentlich nichts. Genau deswegen gibt es in Modula-3 Parameterlisten nur mit fester Länge (eben wie Datenverbünde). In Modula-3 kann man bequem Felder mit Variablen initialisieren. Das beseitigt alle drei für C aufgeführten Probleme und spart darüber hinaus ein paar Seiten in der Sprachdefinition.
PROCEDURE Test (READONLY a : ARRAY OF INTEGER;
                READONLY b : ARRAY OF TEXT; );
...
Test (ARRAY OF INTEGER {1, 2, 3},
      ARRAY OF TEXT {"abc", "def", "ghj"});
Formatierte Ausgabe
Die prominenteste Anwendung von Parameterlisten in C ist sicher die Ausgabefunktion printf. Diese ist so tief im Bewusstsein aller C-Programmierer und ihren Programmen verankert, dass es praktisch nicht möglich ist, ein C-Programm zu bekommen, in dem nicht Parameterlisten variabler Länge gebraucht werden.
Die Routine printf übernimmt drei Aufgaben: Sie wandelt vielerlei Arten von Werten in eine Textform um, passt diese in eine Textschablone ein und gibt alles auf die Standardausgabe aus.
printf("%d %f %c %s\n",
   12, 3.14, 'g', "bla");
Da Werte bunt gemischter Typen in die Parameterliste geschrieben werden dürfen, ist es für den Übersetzer nur schwer möglich, zu testen, ob die Art der Platzhalter zum Typ der korrespondierenden Werte gehört. Da der Übersetzer eigentlich nicht seine Nase in den Formatierungstext stecken soll, ist es ihm auch kaum möglich, eine übereinstimmende Anzahl von Platzhaltern und Werten sicherzustellen.
Nun kann zum Beispiel ein GNU-C-Anwender triumphierend einwenden, dass mit der -Wall-Option genau diese Tests durchgeführt werden. Aber damit ist das grundlegende Problem nicht aus der Welt geschafft.
  1. Eigene Formatierungsroutinen mit leicht geänderten oder erweiterten Möglichkeiten der Schablone können diesen Service nicht in Anspruch nehmen.
  2. Wenn die Formatierungsschablone eine Textvariable ist, also die Schablone zum Zeitpunkt der Übersetzung gar nicht bekannt ist, kann der beste statische Test nichts ausrichten. Zwar ist ein statischer Test besser, aber wenn er nicht anschlägt, dann kann C auch nicht mehr mit einem Test zur Laufzeit helfen.
In Modula-3 nimmt man die Typstrenge ernst und gibt sie auch nicht wegen einer häufig benutzten Routine wie printf auf. Hier werden die drei Aufgaben von printf von verschiedenen Routinen übernommen. Im Modul Fmt der Standardbibliothek gibt es die Routinen F und FN welche Texte in eine Schablone einfügen und daneben eine Reihe von Funktionen, die Werte aller Art in ihre Textdarstellung umwandeln. Mit der Funktion IO.Put gibt man alles letztendlich aus.
IO.Put (Fmt.F ("%s %s %s %s\n",
               Fmt.Int(12), Fmt.Real(3.14), Fmt.Char('g'), "bla"));

IO.Put (Fmt.FN ("%s %s %s %s\n",
                ARRAY OF TEXT {Fmt.Int(12), Fmt.Real(3.14),
                               Fmt.Char('g'), "bla"}));
Hier haben wir endlich mal was, wo Abwiegeln sogar gerechtfertigt ist. Die Vorgehensweise von Modula-3 und seiner Standardbibliothek ist nämlich nicht so effizient wie die von printf, denn die Texte müssen mehrfach umkopiert werden, bevor sie endlich ausgegeben werden. Aber diese Vorgehensweise ist sicher und man darf hoffen, dass die weitere Ausgabe auf Datenträger, Netzwerke oder Bildschirme durch das Betriebssystem noch viel mehr Zeit beansprucht als das Aufbereiten des Textes.
Leider kann die Funktion Fmt.F die korrekte Anzahl der Parameter erst zur Laufzeit überprüfen. Das tut sie dafür zuverlässig. Die richtigen Typen sind aber immer statisch sichergestellt, auch bei selbstgeschriebenen Formatierungsfunktionen.
Rückgabewerte
Ein gern genutzter Service von C ist das Vernachlässigen von Rückgabewerten von Funktionen. Ja, ganz richtig, diese Wahlfreiheit zwischen Verwenden oder Verwerfen von Rückgabewerten hat in C Prinzip und man verwendet das sehr häufig unbewusst. Bereits wenn man
printf("Nur Weicheier beachten Rückgabewerte!\n");
schreibt, oder auch
a=b;
ist es passiert. Die Zuweisung ist tatsächlich eine Funktion mit Nebenwirkung: Der Ausdruck a=b besitzt den Wert von b und überschreibt außerdem die Variable a. Wohin das führen kann, haben wir schon in dem if-Beispiel kennengelernt. Wahrscheinlich gibt es in C diese Interpretation der Zuweisungsoperation, damit so was wie
if (p = malloc(1000)) {
   printf("Ätsch, ich habe 1000 Bytes und Du nicht!\n");
   free(p);
}
oder
a = b = c;
funktioniert. Im zweiten Beispiel wird der Wert von c den Variablen a und b zugewiesen. Damit das aber so hinhaut, musste für den Zuweisungsoperator noch die sonst übliche Regel "von links nach rechts auflösen" (Linksassoziativität) umgekehrt werden. Während a-b+c als (a-b)+c verstanden wird und nicht als a-(b+c), ist es bei a=b=c genau andersherum, nämlich a=(b=c). (a=b)=c ginge überhaupt nicht, weil sich dem Ergebnis einer Berechnung (hier vom Ausdruck a=b) nicht noch einmal etwas anderes zuweisen lässt.
Auf Grund dieser Eigenheiten, wird folgende Verwechslung vom C-Übersetzer widerstandslos übersetzt:
if (a=b) {
   a==b+1;
}
Die Zuweisungsoperation ist die eine Seite der Medaille. Es ist in C aber generell möglich und auch nicht verpönt, wichtige Funktionsrückgabewerte zu ignorieren, wie zum Beispiel Informationen über den Erfolg der Anweisung. "Muss man halt aufpassen" ist der Standpunkt eines C-Programmierers. Wenn man einer Routine nachträglich einen Rückgabewert spendiert, dann kann man sehen wo man bleibt. "Dafür hat doch jeder normale Texteditor eine Suchfunktion!" Aber wehe, man hat diese Routine schon in anderen entfernten Projekten eingebunden. Ich sag's ja nur - statische Sicherheit soll die Konsistenz verschiedener Programmteile wahren. - Der C-Programmierer dagegen beherrscht sein Chaos jederzeit aus dem FF.
Für die C-Programmierer wider Willen gibt es allerdings zwei Hilfestellungen vom GCC:
  1. Wenn Sie die Option -Wall beim Übersetzen angeben, dann bekommen Sie zum einen Warnungen über eine möglicherweise unbeabsichtigte Verwendung des einfachen Gleichheitszeichens, aber auch über eine möglicherweise unbeabsichtigte Verwendung des doppelten Gleichheitszeichens: Bei a==b+1; sagt der GCC, dass diese Anweisung nichts bewirkt.
  2. Für andere Funktionen kann GCC diese Warnung nicht ausgeben, denn viele Standard-C-Funktionen sind so organisiert, dass man den Funktionswert ignorieren kann. Der GCC bietet aber für Ihre selbstgeschriebenen Funktionen an, Warnungen auszugeben, wenn deren Rückgabewert beim Aufruf ignoriert wird. Beispiel:
    float __attribute__((warn_unused_result)) sin(float x) {
      ...
    }
    
In Modula gab es das Ignorieren von Rückgabewerten nie. Dafür war es manchmal umständlich und ineffizient, Rückgabewerte rückstandslos zu entsorgen, zum Beispiel mit
IF Calc()=0 THEN END;
Das lässt aber nicht erkennen, dass wir hier lediglich Calc aufrufen wollten, ohne dessen Ergebnis zu verwerten. Deshalb gibt es in Modula-3 dafür extra ein Konstrukt.
EVAL Calc();
Funktionsaufruf oder Zeiger auf Funktion?
void message() {
   printf ("wichtig\n");
};

int main(int argv, char *argc[]) {
   message;
   return 0;
}
Man könnte erwarten, dass dieses Programm den Text "wichtig" ausgibt oder man könnte erwarten, dass der Übersetzer einen Fehler ausgibt, da trotz der message-Anweisung im Hauptprogramm nichts ausgegeben wird. Beides ist nicht der Fall, das Programm ist korrekt, aber es tut nicht, was es soll. Der Grund ist, dass die Zeile message nur eine Funktion nennt, sie aber nicht aufruft. message hat den Typ "Funktion ohne Parameter und ohne Rückgabewert" und als Wert die Adresse des Maschinencodes der Routine. Das ist soweit sinnvoll, denn dadurch kann man Funktionen an andere Funktionen übergeben, zum Beispiel die Cosinus-Funktion an eine Routine, welche numerisch Nullstellen berechnet.
Was passiert aber in unserem Programm? Die Einsprungadresse von message wird lediglich ermittelt und danach gleich wieder verworfen - wegen der praktischen Rückgabewertentsorgung von C. Richtig muss der Aufruf
   message();
heißen.
Im Übrigen schafft auch hier die -Wall-Option des gcc Linderung, wenn man versucht, das Nichtstun umständlicher als nötig zu beschreiben.
Auch in Modula wird zwischen Funktionen und ihrem Aufruf unterschieden, genau wie übrigens auch in der Mathematik zwischen f und f(x) unterschieden wird, abgesehen davon, dass sich viele nicht daran halten, die's eigentlich wissen müssten. Nur dass Modula, wie gesagt, darauf besteht, dass man mit EVAL kennzeichnet, wenn man den gerade ermittelten Wert nicht weiterverwenden möchte.
EVAL Message;
würde dann in nichts übersetzt werden, nicht mal in ein NOP (No-OPeration).
Verschiedenes
Komma-Operation
Auch die Kommaoperation ist etwas, das man häufiger unbewusst als bewusst einsetzt. Tatsächlich ist der Ausdruck
2,4
ein korrekter C-Ausdruck und hat den Wert 4. Allgemein gilt, dass in dem Ausdruck
links,rechts
die Teilausdrücke links und rechts nacheinander berechnet werden und der Gesamtausdruck den Wert von rechts besitzt. Damit lässt sich eine Anweisung an Stellen mogeln, wo eigentlich ein Wert stehen sollte:
while(i++,i<10) {
  printf("%d\n", i);
}
Der Kommaoperator ist im Zusammenhang mit der for-Schleife und mehrdimensionalen Feldern interessant.
Nicht jedes Komma ist ein Kommaoperator, deswegen fällt er einem so selten auf:
double a=0,b,c;
Wären diese Kommas Komma-Operatoren, dann bedeutete der Ausdruck, dass die Variable a angelegt und mit dem Wert des Ausdruckes 0,b,c, nämlich c initialisiert wird. Da es sich aber um gewöhnliche Kommas handelt, bedeutet es, dass a mit 0 initialisiert wird und darüber hinaus die Variablen b und c uninitialisiert erzeugt werden. Auch bei einem Funktionsaufruf, etwa add(a,b) ist das Komma kein Kommaoperator, bei add((a,b)) sieht das wieder anders aus.
Der Kommaoperator ist entbehrlich und fehlt daher in Modula-3. Sollte man ihn einmal dringend benötigen, kann man entweder ein Unterprogramm mit Nebenwirkungen schreiben oder aber eine spezielle Lösung wie im Falle der Schleife probieren:
LOOP
  INC(i);
  IF i>=10 THEN EXIT END;
  IO.Put(Fmt.Int(i) & "\n");
END;
.
Führende Nullen
Wer seine Programme schon einmal besonders nett formatieren wollte, etwa so:
printf("%d, %d, %d, %d\n",
       0001,
       0010,
       0100,
       1000);
wird beim Anblick der Ausgabe "1, 8, 64, 1000" schon geglaubt haben, einen Übersetzer-Fehler entdeckt zu haben. In der Tat ist dieses Fehlverhalten aber so vorgeschrieben: Zahlen mit führender Null werden als Oktalzahlen, also Zahlen zur Basis 8 interpretiert.
In Modula-3 kann man Zahlen bezüglich aller Basen von 2 bis 16 eingeben. Die Basis wird einfach mit einem Unterstrich getrennt der eigentlichen Zahl vorangestellt:
 2_100 =   4
 8_100 =  64
10_100 = 100
16_100 = 256
Aber mal ehrlich: Die Basen 2 und 16 braucht man oft nur für Zahlenangaben, die eigentlich besser mit Mengentypen formuliert werden sollten oder aber wenn einem das menschenorientierte Zehnersystem nicht als natürlich genug erscheint. Die Basis 8 braucht man praktisch nie und die anderen Basen überhaupt nie.
Templates
Unter anderem auch, um die Präprozessor-Makros überflüssig zu machen, wurden in C++ Templates eingeführt. Diese erlauben es, Programmstücke zu schreiben, die Variablen verwenden, deren Typ man noch nicht festlegen muss. Das gleiche Programmstück kann damit für verschiedene Typen verwendet werden. Zum Beispiel kann man die gleichen Berechnungen für verschieden genaue Gleitkommazahlenformate implementieren. Dafür sind Templates sehr sinnvoll und dass für verschiedene Gleitkommaformate unterschiedlicher Programmcode erzeugt wird, lässt sich gar nicht vermeiden.
Allerdings hatte man mit Templates auch andere sehr häufige Anwendungen im Auge, nämlich Datenstrukturen unabhängig vom verwalteten Inhalt entwickeln zu können. Hier ist es allerdings so, dass die Verwaltung der Datenstruktur so gut vom verwalteten Inhalt getrennt werden könnte, dass man sogar den gleichen Maschinencode zum Beispiel für die Datenstruktur 'Liste' für unterschiedliche Datentypen verwenden könnte. So weit geht die Optimierung bei den gängigen C++-Datenstruktur-Bibliotheken allerdings nicht. Diese Optimierung ist auch etwas aufwändiger und wird von C++ nicht direkt unterstützt. Aber da eine entsprechende Unterstützung die Sprache nur noch komplizierter machen würde, ist das sicher legitim.
Nun ist es aber in C++ so vorgesehen, dass Templates immer erst dort ausgeprägt werden, wo sie verwendet werden. Verwenden also zwei Programmteile, welche in getrennte Objektdateien übersetzt werden, aber die gleiche Ausprägung eines Templates einsetzen, dafür den gleichen Programmcode? Wenn also zwei Programmteile, oder sagen wir, zwei verschiedene Bibliotheken, jeweils Listen von Ganzzahlen verwenden (list<int>), benutzt dann jede Bibliothek ihren eigenen Code dafür? Das C++-Konzept macht es den Übersetzer-Entwicklern nicht leicht, solche Verschwendung abzuwenden.
Ehrlich gesagt, hat mit Speicherverschwendung keiner der heutigen Entwickler ein Problem. Eigentlich kommt den meisten die Speicherverschwendung durch Templates und unflexibles Binden bei Klassen sehr gelegen, denn damit lässt sich den unverschämt wachsenden Speichergrößen paroli bieten und auch der letzte Benutzer kann davon überzeugt werden, dass man für "moderne" Programme auch moderne Rechner braucht!
Das nur nebenbei: Folgendes Programm, das eigentlich nichts tut, wurde auf der Linux-Kiste, auf der ich diesen Text zu schreiben begann (AMD Athlon-XP mit G++ 2.95.3) zu einem ausführbaren Programm der Länge 335 KB übersetzt.
#include <list>

int main(int argv, char *argc[]) {
  std::list<int> li;
  return 0;
}
Der Kickstart-1.3-ROM des Amiga 500 war nur 250 KB groß und enthielt ein 32-Bit-Betriebssystem mit graphischer Bedienoberfläche, Dateisystem (ein richtiges, nicht sowas mit 8.3-Dateinamen-Beschränkung), Multitasking-Kernel usw. usf. !
Der G++ in der Version 3.3.4 scheint besser damit umgehen zu können und erzeugt nur noch 11 KB Code (nahezu unabhängig von der Optimierung), was rund 2 KB mehr sind als das Programm ohne #include und ohne li. Das entspricht immer noch 2/3 der Größe des Betriebssystems des ZX-Spectrum mit BASIC-Interpreter.
In Modula-3 gibt es Templates ähnlich wie in C++. Auch bei ihnen wird Programmcode für verschieden ausgeprägte Datenstrukturen der Standardbibliotheken vervielfältigt. Das ist natürlich genau so unschön wie bei C++ und verlockt in Modula-3 dazu, mit REFANY oder INTEGER ausgeprägte Datenstrukturen zu verwenden, wo ein Zeiger auf einen konkreten Datenverbundstyp bzw. eine Aufzählung die bessere Wahl ist. Ausprägungen dieser beiden Typen werden nämlich meistens gleich mitgeliefert. Allerdings wird dem Vervielfältigen von Code für gleiche Datenstrukturen mit gleicher Ausprägung ein Riegel vorgeschoben, indem man für jede Ausprägung immer erst ein Modul anlegen muss, dessen einzige Aufgabe es ist, ein Template für einen bestimmten Satz von Typen auszuprägen. Immerhin wird diese Vorgehensweise direkt vom Modula-Make unterstützt und bedeutet keinen großen Aufwand.
Das entsprechende Modula-3-Programm sieht übrigens so aus
MODULE Main;

IMPORT IntList;

VAR li: IntList.T;

BEGIN
END Main.
bringt ebenfalls stolze 12 KB auf die Waage und wird 90 Byte kürzer, wenn man das Modul IntList nicht importiert (und natürlich li nicht deklariert). Anscheinend müssen Programme heute unabhängig von der Programmiersprache so groß sein. Längere Maschinenbefehlscodes? Mehr Verwaltungsaufwand? Höhere Sicherheit? Na gut, das hat jetzt nix mehr mit Templates zu tun.
Schiebung beim Ausprägen von Templates
In C++ war man mutig genug, um die Größer- und Kleinerzeichen als spitze Klammern beim Ausprägen von Templates wiederzuverwenden. Interessant ist die Übersetzung der folgenden Deklaration.
std::vector<std::complex<double>> x;
Wenn man einen schlechten Übersetzer hat, wird dieser den Programmierer mit einem Fehler der Art "Syntaxfehler vor x" in Ratlosigkeit hinterlassen. Ein schlauer Übersetzer hingegen (sagen wir G++ 3.3.4) wird darauf hinweisen, dass die beiden schließenden Klammern zu dem Bitschiebeoperator >> verschmolzen sind.
Präprozessor
Hintergrund
Eines der dunkelsten Kapitel von C ist sicher sein Präprozessor. Wem die Sprache noch nicht verworren genug ist, der wird seine helle Freude an etlichen Ungereimtheiten haben, die durch den Präprozessor verursacht werden.
Normalerweise schreibt man keinen reinen C-Text, sondern vermengt den Programmtext mit Präprozessor-Anweisungen. Diese werden in einem ersten Lauf von dem Präprozessor herausgefiltert und ausgewertet und die entstehende Datei wird dann dem eigentlichen Übersetzer vorgelegt. Der Präprozessor kann Kommentare entfernen, Macros expandieren, andere Quelltextstücke einbinden oder je nach Bedingungen bestimmte Textteile auslassen. Die Bedingungen können selbst wieder arithmetische Ausdrücke sein, womit der Präprozessor fast schon wieder eine eigene Programmiersprache anbieten muss.
Alles in allem kann der Präprozessor eine ganz nützliche Einrichtung sein, sein Einsatz zur Verwaltung größerer modularisierter Projekte (das Betriebssystem und seine Bibliotheken sind ein solches - man kommt also nicht drum herum) ist allerdings sehr problematisch.
Für Modula-3 gibt es keinen Präprozessor im Sinne des C-Präprozessors. Sämtliche Vorzüge des C-Präprozessors werden auf andere Weise erreicht.
  • Kommentare werden direkt bei der lexikalischen Analyse vom Übersetzer ausgeblendet. In den Kommentaren steht nichts, was den Übersetzer interessieren könnte, denn für dezente Hinweise an den Übersetzer gibt es Pragmas, etwa <* ASSERT a=0 *>.
  • Programme lassen sich in Modulen gliedern (für irgendwas muss der Name der Sprache ja stehen) und diese sind nicht nur Textbausteine sondern abgeschlossene, eigenständig übersetzbare Einheiten.
  • Statt bedingter Übersetzung zum Beispiel für systemabhängige Programmteile nutzt man systemabhängige Module, die vom Modula-Make-System passend zusammengesucht werden.
  • Statt Macros schreibt man (Inline-)Funktionen.
Effizienz
Das Verfahren ist ziemlich uneffizient, weil für das Übersetzen eines Quelltextes sämtliche benötigte Schnittstellendateien (sogenannte Header) und die von ihnen eingebundenen Schnittstellen mit dem Hauptprogramm zu einem großen Text zusammengeschweißt werden und dann der gesamte Text analysiert und übersetzt wird. Modula trennt strenger zwischen den Schnittstellen und den Implementationen. Während es in C problemlos möglich ist, in den Schnittstellen Programmtext unterzubringen, ist das in Modula nicht möglich. In Modula werden die Schnittstellendateien auch nicht einfach textuell zum Hauptprogramm hinzugefügt, vielmehr werden Schnittstellen- (.i3) und Implementationsdateien (.m3) getrennt übersetzt (in .io und .mo), sind also schon vor der Erstbenutzung auf syntaktische Korrektheit und Konsistenz überprüft und können vom Übersetzer zudem schneller wieder eingelesen werden als Header-Texte.
Mehrfaches Einbinden
Es gibt keinen Schutz gegen mehrfaches Einbinden einer Header-Datei. Um Anwender einer Header-Datei vor doppeltem Einbinden zu schützen, muss der Programmierer selbst Hand anlegen: Er muss testen ob ein Präprozessor-Symbol der Art _HEADER_XXX_WURDE_BEREITS_EINGEBUNDEN existiert und wenn nicht, dann muss er dieses Symbol definieren und die Header-Definitionen durchlassen. Vergisst der Programmierer diese Vorkehrung oder macht etwas verkehrt, wird er diesen Fehler erst beim Einbinden der Header-Datei bemerken, zum Beispiel, wenn er die Reihenfolge zweier #include-Anweisungen ändert. Im Gegensatz zu C ist es in Modula nicht möglich, eine Definition in einer Datei zu beginnen und in einer anderen einfach fortzusetzen. Derart abgekapselt, ist es kein Problem, Schnittstellen in irgendeiner Reihenfolge einzubinden und mehrfaches Einbinden einer Schnittstelle verursacht keinerlei Bauchschmerzen.
Sichtbarkeit
Die Inhalte aller Header-Dateien werden auf gleicher Sichtbarkeitsebene zusammengeworfen. Jeder Autor einer Header-Datei muss sicherstellen, dass keines seiner Symbole in irgendeiner anderen Header-Datei der Welt auftaucht. Das lässt sich meist nur durch lange unhandliche Symbolnamen erreichen, welche z.B. den Modulnamen enthalten. Unter C++ kann man auch Namensbereiche einführen und Klassen können ebenfalls helfen, das Problem zu mildern - benutzen muss man natürlich beides nicht. Ihr wisst, was das heißt ...
Noch viel verworrener können Fehler sein, die von Präprozessor-Symbolen verursacht werden. Wenn der Übersetzer die Zeilen
const int _N = 32;
mit dem Fehler "parse error before numeric constant" ablehnt, reibt man sich verwundert die Äuglein, bis man feststellt, dass in einer weit entfernten Header-Datei der Bezeicher _N als Präprozessor-Symbol definiert wurde.
#define _N 64
Die Bezeichner jeder Schnittstelle sind streng voneinander getrennt und können sich nicht gegenseitig beeinflussen. Man kann auf alle Bezeichner mit Modulname.Bezeichner zugreifen, oft benötigte Bezeichner können mit IMPORT auch direkt sichtbar gemacht werden. Das einzige Problem, was noch auftreten kann, ist, dass zwei Module den gleichen Namen tragen.
Inline
Macros werden vom Präprozessor völlig unabhängig vom Kontext des Programmes rein textuell ersetzt, etwaige Unstimmigkeiten entdeckt erst der Übersetzer. Der kann die Fehler aber nicht mehr auf der Ebene des Quelltextes ausdrücken, sondern nur noch in der Form, wie er sie vom Präprozessor übergeben bekommen hat. Deswegen empfehlen C++-Übersetzer-Entwickler inline-Routinen. Die funktionieren wie Makros, ihr Inhalt wird also direkt in den laufenden Programmfluss kopiert. Allerdings werden die Parameter auf korrekten Typ getestet. Statt
#define fmin(x,y) x<y?x:y
nehme man lieber
inline float fmin(float x, float y) {
   return x<y?x:y;
};
Mithin haben wir hier eine weitere klassische Kann-Regelung, welche geschaffen wurde, um ignoriert zu werden.
Da die Frage, ob eine Funktion "inline" sein soll oder nicht, nicht an der Funktion eines Programmes rührt, werden in Modula-3 derartige Funktionen mit einem Pragma namens <* INLINE *> gekennzeichnet.
Allerdings schaffen Inline-Funktionen neue Abhängigkeiten und damit neue Probleme. Ruft man in einem Modula-3-Programm eine Funktion auf, muss deren Implementierung beim Übersetzen nicht bekannt sein. Sie braucht also auch nicht zu existieren. Der Übersetzer markiert die Stelle des Aufrufes und der Lader trägt vor dem Starten des Programmes die richtige Einsprungadresse ein. Bei einer Inline-Funktion ist das komplett anders, hier muss bereits beim Übersetzen die Implementation bekannt sein. Die vom Übersetzer erzeugten Schnittstellen von Modulen mit Inline-Funktionen müssen Implementationsdetails enthalten. Auch wenn sich nur die Implementation einer Inline-Funktion ändert, müssen alle Programmteile neu übersetzt werden, die diese Funktion aufrufen. Von Änderungen an Inline-Funktionen einer dynamisch gebundenen Bibliothek (zum Beispiel eine Fehlerbereinigung) profitieren Anwenderprogramme ebenfalls erst nach einer erneuten Übersetzung. Kurzum: Mir ist kein Modula-3-Übersetzer bekannt, der Modul-übergreifende Inline-Funktionen unterstützt.
geschachtelte Kommentare
Der Präprozessor ist selbstverständlich nicht in der Lage, geschachtelte Kommentare zu erkennen. Das ist im Standard so vorgeschrieben, denn gespart werden muss, koste es was es wolle! Diese Routine wird deshalb etwas völlig unvorhergesehenes tun:
float dividesub (float p, float *q, float s) {
  return p/*q /* *q darf nicht null sein! */
          - s;
}
Mit der -Wall-Option wird man in diesem Fall wenigstens gewarnt.
Kommentare dürfen in Modula geschachtelt werden.
mehrzeilige einzeilige Kommentare
Ein schönes Beispiel aus der Windowswelt liefert uns Galileo Computing: C von A bis Z:
#include <stdio.h>
int main(void) {
  // das Programm befindet sich im Pfad C:\programme\
  printf("Hallo Welt\n");
  return 0;
}
Dieses Programm wird anstandslos übersetzt und gibt nichts aus. Warum? Weil der abschließende Rückwartsschrägstrich den Kommentar auf die nächste Zeile ausdehnt.
Die -Wall-Option des GCC gibt hierfür den Hinweis auf einen möglicherweise unbeabsichtigten mehrzeiligen Kommentar aus.
In Modula gibt es nur mehrzeilige Kommentare.
Präprozessor-Symbole und C-Bezeichner
Die Verwicklung von Präprozessor und Übersetzer dürfte so manchen C-Neuling etliche Haare kosten, wenn er sich wundert, warum es nicht so einfach möglich ist, zu testen, ob bestimmte Routinen schon existieren:
#ifndef fmin
float fmin(float x, float y) {
   return x<y?x:y;
};
#endif
Das wird nicht funktionieren, weil das Präprozessor-Symbol fmin nichts mit dem C-Bezeichner fmin zu tun hat. Der Präprozessor erledigt zuerst seine Arbeit, dann der C-Übersetzer seine. Der Präprozessor erfährt nichts von den C-Bezeichnern und umgekehrt.
Da es keine Präprozessor-Symbole gibt, kann es eine solche Verwechslung nicht geben. Es ist in Modula deshalb auch nicht möglich, zu testen, ob eine Routine mit einem bestimmten Namen bereits definiert wurde, um sie gegebenenfalls selbst zu implementieren.
Prüfung von Makroinhalten
Makrodefinitionen werden allein von Präprozessor beachtet, der kennt aber die C-Syntax nicht. Man kann in eine Makrodefinition sehr viel Unsinn hineinstecken und bemerkt Fehler erst, wenn das Makro angewendet wird. Sehr viel Spaß bereiten daher Definitionen wie
#define MORITZINT = 1000
Und auch nur erfahrenen C-Programmierern dürfte die Gefahr bewusst sein, die von
#define z x+y
ausgeht. Wer erwartet denn schon, dass der Ausdruck z*w ganz und gar nicht (x+y)*w berechnet, sondern x+(y*w)!
Es gibt in Modula wirklich keinen Präprozessor!
Komplexität der Sprache C++
Typen
Synonyme: char, bool, int, enum Eigenständige Typen: CHAR, BOOLEAN, INTEGER, Aufzählungen, SET
Bezeichnerauflösung
Präprozessor, Qualifikation mit Namensräumen oder Klassennamen, Überladen (insbesondere von Operatoren), Objektmethoden Qualifikation mit Modulnamen, Objektmethoden
Objektorientierung
Konstruktoren, Destruktoren, Kopierkonstruktoren, Zuweisungoperatoren, Initialisierer, virtuelle und statische Methoden, Mehrfacherben METHODS, OVERRIDES, Einfacherben
Zugriffsrechte
öffentliches und privates Erben, private, protected, public, friend <:, REVEAL
Templates
Templates von Klassen und von Funktionen Templates von Modulen
Speicherverwaltung
new, delete, new [], delete [] NEW, automatische Freigabe oder DISPOSE

Verweise

" Das letzte schöne, was in C geschrieben wurde, war Franz Schuberts Große C-Dur-Sinfonie. - The last good thing written in C was Franz Schubert's Symphony number 9. " (Erwin Dieterich)

Nachdem ihr euch durch die ganzen Beispiele gequält habt, könnt ihr vielleicht nachvollziehen, warum ich inzwischen nur noch unter Gewaltandrohung mit C oder C++ programmiere. Damit stehe nicht einmal alleine da, wie folgende Artikel beweisen.

  1. Hermann Wacker: Die C-Katastrophe
  2. Stein Jorgen Ryan: Modula-2 vs. C
  3. Bernd Leitenberger: Warum "C" nicht meine Lieblingsprogrammiersprache ist
  4. P. J. Moylan: The case against C.
  5. Validome.org-Blog: Rusty Russell: Ist C noch relevant?
  6. Ian Joyner: C++?? : A Critique of C++, (3rd Ed.)
  7. Ian Barland: Why C and C++ are bad
  8. ReliSoft: What's wrong with C++?
  9. Herbert Sutter: Guru of the Week - Für alle die glauben, dass man C++ nach überschaubarer Zeit beherrschen kann

Nach dieser Blitzausbildung zum C/C++-Hasser könnte man denken, dass die Programmierwelt gerettet ist, sobald nur C und C++ ausgerottet sind. Dem ist nicht so. Die Konkurrenz, die sich gerade in Sicherheit wiegt, kriegt auch noch ihr Fett weg!

  1. Skriptsprachen
  2. Fortran 90
  3. HTML
  4. Was wir schon immer über Softwareentwicklung geahnt haben.
  5. Manche lernen einfach nichts dazu: C hält Einzug in die sicherheitskritische Programmierung in der Flugtechnik.
  6. Kritik an Programmiersprachen zusammengetragen von Prof. Klaeren in Tübingen: Skript zur Vorlesung "Konzepte von Programmiersprachen", Abschnitte 'Gruselkabinett' und 'Kritik an Programmiersprachen'
  7. Wie man möglichst schwer zu wartende Programme schreibt: How to write unmaintainable code

Schlusswort

Das hat Prof. László Böszörményi bereits in seinem Buch "Programmieren mit Stil - Eine Einführung mit Modula-3" sehr schön formuliert:

Der Leser dieses Buches wurde während seines Studiums bestimmt mit den folgenden zwei Fragen konfrontiert: Wozu überhaupt Programmieren lernen? Und wenn schon, warum dann in Modula-3? Wir versuchen zum Schluss diese Fragen kurz zu beantworten.

Warum Programmieren?

Viele Informatiker und Informatikanwender sind heute der Meinung, dass Programmierung eine zweitrangige Angelegenheit ist. Die wirklich wichtigen Phasen der Softwareentwicklung sind die Analyse, die Spezifikation und der Entwurf. Die Programmierung ist nur noch Knochenarbeit.

Zu den Anfangszeiten des "Computerzeitalters" war das Programmieren von vielen als eine Kunst hochangesehenen. Dementsprechend hatte der Beruf "Programmierer" auch einen hohen Stellenwert. Als die Softwaresysteme immer größer und komplexer wurden, war die eher intuitive Programmierkunst nicht mehr ausreichend. Man hat die Bedeutung der vorbereitenden Phasen immer mehr erkannt. Im Kampf gegen die "traditionelle" Sichtweise hat man sich oft polemisch und etwas übertrieben ausgedrückt. Somit hat die Programmierung ihre primäre Rolle verloren und ist sekundär geworden.

Wir glauben, dass es höchste Zeit ist, dass auf diesem Gebiet eine Versöhnung eintritt, und alle Phasen der Softwareentwicklung als gleichwertig erkannt werden. Es ist klar, dass z. B. ohne eine gute Analyse ein Softwareprojekt von vornherein zum Scheitern verurteilt ist. Es sollte aber auch klar sein, dass Software letztlich doch durch die Programmierer erstellt wird. Wenn sie schlecht ausgebildet oder unmotiviert sind, dann hilft auch die beste Analyse nichts.

Niklaus Wirth hat im März, 1995, in einem Vortrag an der Universität Klagenfurt das Phänomen "Software-Chaos" analysiert. Er hat darauf hingewiesen, dass die immer komplizierter werdende Software gar keine Notwendigkeit darstellt, vielmehr mit dem Verlust gewisser ingenieursmäßigen Fähigkeiten - wie etwa das Gefühl für Effizienz und für Einfachheit - zu tun hat. Deswegen halten wir es für die Pflicht eines jeden Informatikers sauber und mit Stil Programmieren zu lernen - auch wenn er in seiner späteren Karriere möglicherweise wenig programmieren wird.

Warum Modula-3?

Wenn Programmierung als zweitrangig, dann wird die Wahl einer Programmiersprache oft sogar als drittrangig angesehen. Klarerweise ist die Programmiersprache nur ein Werkzeug. Auf den meisten anderen Gebieten des Lebens ist aber die Bedeutung guter Werkzeuge weitgehend anerkannt. Auf dem Softwaregebiet ist der einzige Gesichtspunkt, der üblicherweise berücksichtigt wird, die allgemeine Verfügbarkeit. Das führt notwendigerweise zur Konservierung von veralteten Programmiersprachen. Die heute am meisten verwendeten Programmiersprachen (wie Cobol, Fortran und C) sind alle mehr als zwanzig (Fortran mehr als vierzig) Jahre alt. Ihre größte Schwäche besteht in ihren Sicherheitsmechanismen: Sie bieten nur sehr eingeschränkte statische Kontrollen an.

Wir haben Modula-3 für unser Buch deswegen ausgewählt, weil es die Erkenntnisse der letzten zwanzig Jahre auf dem Gebiet des Sprachentwurfs in einer sauberen und eleganten Weise integriert. Wir wollen keineswegs behaupten, dass dies nur für Modula-3 zutrifft, die Anzahl solcher Programmiersprachen ist allerdings nicht allzu groß. Es ist jedenfalls wichtig, dass die erste Programmiersprache die man lernt - die "Muttersprache" eines Programmierers - diese Eigenschaften besitzt. Wir wollen uns wiederum auf Niklaus Wirth beziehen, der auf die Verantwortung der Universitäten in diesem Zusammenhang hingewiesen hat. Wenn die Universitäten hinter der Praxis laufen, anstatt die neuen Erkenntnisse hochzuhalten, dann ist die Hoffnung auf eine Verbesserung der chaotischen Softwaresituation verloren.

Ist es uns gelungen, zu dieser Hoffnung etwas beizutragen, so war unsere Arbeit der Mühe Wert. Somit wünschen wir dem Leser viel Spaß beim Programmieren in Modula-3.


Erstellt:2002.05 Henning Thielemann
Mit HSC verarbeitet seit:2002-05-29
Zuletzt geändert:2011-09-17, 09:59
Meinung des HTML-Prüfers: Valid HTML 4.0!
Seitenstatistiken: eXTReMe web tracking Test for JavaScript disabled
VG-Wort-Vorpixel
Überwachungszustand:Aktion UBERWACH!
Hier noch ein paar schwachsinnige E-Mail-Adressen zur Fütterung von SPAM-Adresssammlern. Die Adressen sind mit Hilfe von Markov-Ketten zufällig aus realen Namen zusammengewürfelt und existieren mit sehr großer Wahrscheinlichkeit nicht.