Der wahre Grund liegt in einem grundsätzlichen Unterschied in der Absicht zwischen C und C ++ einerseits und Java und C # (für nur einige Beispiele) andererseits. Aus historischen Gründen geht es in den meisten Diskussionen hier eher um C als um C ++, aber (wie Sie wahrscheinlich bereits wissen) ist C ++ ein ziemlich direkter Nachkomme von C, und das, was über C gesagt wird, gilt auch für C ++.
Obwohl sie größtenteils in Vergessenheit geraten sind (und ihre Existenz manchmal sogar geleugnet wird), wurden die allerersten Versionen von UNIX in Assemblersprache geschrieben. Ein Großteil (wenn nicht nur) des ursprünglichen Zwecks von C bestand darin, UNIX von der Assemblersprache auf eine höhere Sprache zu portieren. Teil der Absicht war es, so viel wie möglich des Betriebssystems in einer höheren Sprache zu schreiben - oder es aus der anderen Richtung zu betrachten, um die Menge zu minimieren, die in Assemblersprache geschrieben werden musste.
Um dies zu erreichen, musste C nahezu den gleichen Grad an Zugriff auf die Hardware bieten wie die Assemblersprache. Das PDP-11 (zum Beispiel) hat E / A-Register auf bestimmte Adressen abgebildet. Beispielsweise würden Sie einen Speicherort lesen, um zu überprüfen, ob eine Taste auf der Systemkonsole gedrückt wurde. Ein Bit wurde an dieser Stelle gesetzt, als Daten darauf warteten, gelesen zu werden. Sie haben dann ein Byte von einem anderen angegebenen Speicherort gelesen, um den ASCII-Code der gedrückten Taste abzurufen.
Wenn Sie einige Daten drucken möchten, überprüfen Sie einen anderen angegebenen Speicherort, und wenn das Ausgabegerät bereit ist, schreiben Sie Ihre Daten an einen anderen angegebenen Speicherort.
Um das Schreiben von Treibern für solche Geräte zu unterstützen, haben Sie in C die Möglichkeit, einen beliebigen Speicherort mit einem ganzzahligen Typ anzugeben, ihn in einen Zeiger zu konvertieren und diesen Speicherort im Speicher zu lesen oder zu schreiben.
Natürlich hat dies ein ziemlich ernstes Problem: Nicht jede Maschine auf der Erde verfügt über einen Speicher, der mit einem PDP-11 aus den frühen 1970er Jahren identisch ist. Wenn Sie also diese Ganzzahl nehmen, in einen Zeiger konvertieren und dann über diesen Zeiger lesen oder schreiben, kann niemand eine angemessene Garantie dafür geben, was Sie erhalten werden. Nur für ein naheliegendes Beispiel: Lesen und Schreiben werden möglicherweise separaten Registern in der Hardware zugeordnet. Wenn Sie also etwas schreiben (im Gegensatz zum normalen Speicher), versuchen Sie, es zurückzulesen. Das Gelesene stimmt möglicherweise nicht mit dem überein, was Sie geschrieben haben.
Ich sehe ein paar Möglichkeiten, die sich ergeben:
- Definieren Sie eine Schnittstelle zu aller möglichen Hardware - geben Sie die absoluten Adressen aller Speicherorte an, die Sie lesen oder schreiben möchten, um auf irgendeine Weise mit der Hardware zu interagieren.
- Verbieten Sie diese Zugriffsebene, und legen Sie fest, dass jeder, der dies tun möchte, die Assemblersprache verwenden muss.
- Erlauben Sie es den Leuten, dies zu tun, aber überlassen Sie es ihnen, (zum Beispiel) die Handbücher für die Hardware, auf die sie abzielen, zu lesen und den Code zu schreiben, der zu der von ihnen verwendeten Hardware passt.
Von diesen scheint 1 so absurd, dass es kaum einer weiteren Diskussion wert ist. 2 wirft im Grunde die grundlegende Absicht der Sprache weg. Damit bleibt die dritte Option im Wesentlichen die einzige, die sie vernünftigerweise überhaupt in Betracht ziehen könnten.
Ein weiterer Punkt, der ziemlich häufig auftritt, ist die Größe von Ganzzahltypen. C nimmt die "Position" ein, int
die der natürlichen Größe entsprechen soll, die von der Architektur vorgeschlagen wird. Wenn ich also ein 32-Bit-VAX programmiere, int
sollte es wahrscheinlich 32 Bit sein, aber wenn ich ein 36-Bit-Univac programmiere, int
sollte es wahrscheinlich 36 Bit sein (und so weiter). Es ist wahrscheinlich nicht sinnvoll (und möglicherweise auch nicht möglich), ein Betriebssystem für einen 36-Bit-Computer nur mit Typen zu schreiben, deren Größe garantiert ein Vielfaches von 8 Bit beträgt. Vielleicht bin ich nur oberflächlich, aber wenn ich ein Betriebssystem für eine 36-Bit-Maschine schreibe, möchte ich wahrscheinlich eine Sprache verwenden, die einen 36-Bit-Typ unterstützt.
Aus sprachlicher Sicht führt dies zu noch undefiniertem Verhalten. Was passiert, wenn ich 1 addiere, wenn ich den größten Wert nehme, der in 32 Bit passt? Bei typischer 32-Bit-Hardware wird ein Rollover ausgeführt (oder möglicherweise ein Hardwarefehler). Auf der anderen Seite, wenn es auf 36-Bit-Hardware läuft, wird es nur ... eine hinzufügen. Wenn die Sprache das Schreiben von Betriebssystemen unterstützt, können Sie keines der beiden Verhalten garantieren - Sie müssen lediglich zulassen, dass sowohl die Größen der Typen als auch das Verhalten des Überlaufs von einem zum anderen variieren.
Java und C # können all das ignorieren. Sie unterstützen nicht das Schreiben von Betriebssystemen. Mit ihnen haben Sie eine Reihe von Möglichkeiten. Eine besteht darin, die Hardware so zu gestalten, wie sie es erfordert - da sie Typen mit 8, 16, 32 und 64 Bit erfordert, müssen Sie nur Hardware erstellen, die diese Größen unterstützt. Die andere naheliegende Möglichkeit besteht darin, dass die Sprache nur auf einer anderen Software ausgeführt wird, die die gewünschte Umgebung bietet, unabhängig davon, welche zugrunde liegende Hardware gewünscht wird.
In den meisten Fällen ist dies keine Entweder-Oder-Wahl. Vielmehr machen viele Implementierungen ein wenig von beidem. Normalerweise führen Sie Java auf einer JVM aus, die auf einem Betriebssystem ausgeführt wird. Meistens ist das Betriebssystem in C und die JVM in C ++ geschrieben. Wenn die JVM auf einer ARM-CPU ausgeführt wird, stehen die Chancen gut, dass die CPU die Jazelle-Erweiterungen von ARM enthält, um die Hardware besser an die Anforderungen von Java anzupassen, sodass weniger Software erforderlich ist und der Java-Code schneller (oder weniger) ausgeführt wird langsam sowieso).
Zusammenfassung
C und C ++ haben ein undefiniertes Verhalten, da niemand eine akzeptable Alternative definiert hat, die es ihnen ermöglicht, das zu tun, was sie beabsichtigt haben. C # und Java verfolgen einen anderen Ansatz, aber dieser Ansatz passt (wenn überhaupt) schlecht zu den Zielen von C und C ++. Insbesondere scheint keines der beiden Verfahren eine vernünftige Möglichkeit zu bieten, Systemsoftware (z. B. ein Betriebssystem) auf die am meisten willkürlich ausgewählte Hardware zu schreiben. Beides hängt in der Regel von Funktionen ab, die von vorhandener Systemsoftware (normalerweise in C oder C ++ geschrieben) bereitgestellt werden, um ihre Arbeit zu erledigen.