Programmiersprachen im Vergleich

Wie sich C gegen C++, Python & Co. schlägt

18.01.2023
Von  und
Serdar Yegulalp schreibt für unsere US-Schwesterpublikation Infoworld.


Florian Maier beschäftigt sich mit diversen Themen rund um Technologie und Management.
C gehört seit Jahrzehnten zur Grundausstattung für Softwareentwickler. Lesen Sie, wie der Programmiersprachen-Klassiker im Vergleich mit C++, Java, Python und anderen abschneidet.
Die Programmiersprache C ist nicht totzukriegen. Kann sie sich im Vergleich zu betagten und modernen Alternativen (noch) behaupten?
Die Programmiersprache C ist nicht totzukriegen. Kann sie sich im Vergleich zu betagten und modernen Alternativen (noch) behaupten?
Foto: DC Studio - shutterstock.com

Seit 1972 gehört die Programmiersprache C zu den grundlegenden Bausteinen unserer Softwarewelt. Im Laufe der letzten Jahrzehnte sind Dutzende neuer Programmiersprachen hinzugekommen, einige davon mit dem erklärten Ziel, C die Vorherrschaft streitig zu machen. Allerdings fiel es den Herausforderern nicht leicht, C in Sachen Performance, Kompatibilität und Verbreitung zu übertrumpfen. In diesem Artikel vergleichen wir den Programmiersprachen-Klassiker mit folgenden Konkurrenten:

  • C++

  • Java

  • C# und .NET

  • Go

  • Rust

  • Python

  • Carbon

C vs. C++

C++ wurde als Erweiterung von C entwickelt - entsprechend häufig werden diese beiden Programmiersprachen miteinander verglichen. Doch die Unterschiede sind gewaltig. C++ weist zwar in seiner Syntax und seinem grundsätzlichen Ansatz immer noch Ähnlichkeiten mit C auf, bietet aber viele nützliche Funktionen, die C nicht von Haus aus mitbringt. Dazu gehören etwa Namespaces, Templates, Exceptions oder automatisches Speichermanagement. Projekte mit hohen Leistungsanforderungen wie Datenbanken und Machine-Learning-Systeme werden häufig in C++ geschrieben und nutzen diese Funktionen, um das Maximum an Leistung aus den Systemen herauszupressen.

Darüber hinaus entwickelt sich C++ wesentlich schneller weiter als C. Mit C++ in Version 23 halten zum Beispiel Module, Co-Routinen und eine modularisierte Standardbibliothek für schnellere Kompilierung und flüssigeren Code Einzug. Im Gegensatz dazu verspricht der kommende C-Standard C2x nur wenig Neues und fokussiert in erster Linie darauf, die Abwärtskompatibilität aufrechtzuerhalten.

Auch wenn die Vorteile von C++ unübersehbar sind, können sie sich doch auch schnell in Nachteile verwandeln. Je mehr C++-Features Programmierer verwenden, desto komplexer wird das Gesamtkonstrukt. Entwickler, die sich mit einem Subset an C++-Funktionen begnügen, können daher manche Fallstricke vermeiden. Nicht wenige Unternehmen verzichten angesichts der hohen Komplexität lieber ganz. So meidet das Entwicklungsteam des Linux-Kernels C++ wie der Teufel das Weihwasser. Zwar fasst das Team für künftige Kernel-Entwicklungen inzwischen Rust als Programmiersprache der Wahl ins Auge, das Gros von Linux wird aber weiter in C geschrieben.

Wenn Entwickler C statt C++ verwenden, haben sie zwar weniger Funktionen an der Hand, unterstützen aber diejenigen, die den Code später pflegen müssen. Natürlich verfügt C++ über viele großartige High-Level-Funktionen, aber wenn Projektteams Minimalismus und Übersichtlichkeit anstreben, ist es sinnvoller auf C zu setzen.

C vs. Java

Auch Java zählt nach Jahrzehnten zu den Silberrücken der Programmiersprachen, ist jedoch im Zusammenhang mit Enterprise-Software immer noch gesetzt. Die Java-Syntax weist starke Ähnlichkeiten mit der von C und C++ auf. Im Gegensatz zu C wird Java jedoch nicht standardmäßig zu nativem Code kompiliert. Stattdessen kompiliert der JIT-Compiler (Just-in-Time) den Java-Code so, dass er in der Zielumgebung ausgeführt werden kann. Die JIT-Engine optimiert Routinen zur Laufzeit auf der Grundlage des Programmverhaltens und ermöglicht so viele Optimierungen, die bei vorzeitig kompiliertem C-Code nicht möglich sind. Unter den richtigen Umständen kann JIT-kompilierter Java-Code das Performance-Level von C erreichen oder sogar übertreffen.

Obwohl die Java-Laufzeitumgebung das Speichermanagement automatisiert, ist es möglich, diesen Vorgang zu umgehen. Apache Spark optimiert beispielsweise die In-Memory-Verarbeitung zum Teil durch das Nutzen "unsicherer" Teile der Java-Runtime, die dazu verwendet werden, Speicher direkt zuzuweisen und zu managen und den Overhead des JVM-Systems für die Garbage Collection zu vermeiden.

Die Java-Philosophie "write once, run anywhere" macht es möglich, Java-Programme mit relativ geringen Anpassungen in einer Zielarchitektur zum Laufen zu bringen. Im Gegensatz dazu kann ein C-Programm, um beispielsweise unter Windows oder Linux korrekt zu laufen, noch Anpassungen erfordern, obwohl es auf eine Vielzahl von Architekturen portiert wurde.

Die Kombination aus Portabilität und hoher Leistung sowie ein umfangreiches Ökosystem von Softwarebibliotheken und Frameworks machen Java zu einer bevorzugten Sprache und Laufzeitumgebung, wenn es darum geht, Business-Applikationen zu coden. Allerdings bleibt Java bei der maschinennahen Ausführung oder dem direkten Zusammenspiel mit der Hardware hinter C zurück.

Bei C ist es so, dass der Code direkt in Maschinencode kompiliert und vom Prozess ausgeführt wird. Java wird zunächst in Bytecode kompiliert, einen Zwischencode, den der JVM-Interpreter dann in Maschinencode umwandelt. Obwohl die automatische Speicherverwaltung von Java in den meisten Fällen ein Segen ist, eignet sich C aufgrund seines geringen Speicherbedarfs besser für Programme, die begrenzte Speicherressourcen optimal nutzen müssen.

C vs. C# und .NET

Auch C# und .NET haben knapp zwei Jahrzehnte auf dem Buckel - sind jedoch ebenfalls weiterhin wichtige Komponenten der Enterprise-Softwarewelt. Das Managed-Code-Compilersystem C# und die universelle Laufzeitumgebung .NET wurden oft als Microsofts Antwort auf Java dargestellt. Entsprechend gelten viele der eben gemachten Aussagen auch für den Vergleich von C mit C# und .NET.

Wie Java (und in gewissem Maße auch Python) bietet .NET Portabilität über eine Vielzahl von Plattformen hinweg und ein großes Ökosystem integrierter Software. Das sind keine unwesentlichen Vorteile, wenn man bedenkt, wieviel unternehmensnahe Softwareentwicklung in der .NET-Welt stattfindet. Wenn Sie ein Programm in C# oder einer anderen .NET-Sprache entwickeln, können Sie auf eine Vielzahl von Tools und Bibliotheken zurückgreifen, die für die .NET-Laufzeit geschrieben wurden.

Ein weiterer .NET-Vorteil ist - ähnlich wie bei Java - die JIT-Optimierung: C#- und .NET-Programme können (wie C) vorab kompiliert werden. Hauptsächlich werden sie jedoch von der .NET-Runtime just-in-time kompiliert und mit Laufzeitinformationen optimiert. Die JIT-Kompilierung ermöglicht alle Arten von Optimierungen an Ort und Stelle für ein laufendes .NET-Programm. Das ist bei C nicht möglich.

Wie C (und bis zu einem gewissen Grad auch Java) bieten auch C# und .NET verschiedene Mechanismen, um direkt auf den Speicher zuzugreifen. Heap, Stack und nicht verwalteter Systemspeicher sind über .NET-APIs und -Objekte zugänglich. Um noch mehr Performance zu erzielen, steht Entwicklern die Nutzung des unsafe-Modus in .NET frei. Das zieht jedoch auch "Kosten" nach sich: Gemanagte und unsafe-Objekte können nicht beliebig ausgatauscht werden und Marshaling ist mit Leistungseinbußen verbunden. Um die Performance von .NET-Applikationen zu optimieren, müssen die Bewegungen zwischen verwalteten und nicht verwalteten Objekten daher auf ein Minimum beschränkt werden.

Wenn Sie es sich diesen Nachteil nicht leisten können oder wenn die .NET-Laufzeitumgebung für die Zielumgebung (zum Beispiel Kernel-Space) ungeeignet ist (oder möglicherweise überhaupt nicht zur Verfügung steht) dann ist C die richtige Wahl für Sie. Im Gegensatz zu C# und .NET bietet C standardmäßig direkten Speicherzugriff.

C vs. Go

Auch die Syntax von Go ist stark an C angelehnt - geschweifte Klammern als Begrenzungszeichen und mit Semikolons abgeschlossene Anweisungen sind nur zwei Beispiele. Entwickler die C beherrschen, können in der Regel ohne große Schwierigkeiten direkt in Go einsteigen, trotz weiterer Go-Funktionen wie Namespaces und Paketverwaltung.

Lesbarer Code war eines der wichtigsten Designziele von Go: Entwicklern sollten sich möglichst leicht in jedes Go-Projekt einarbeiten können und die Codebasis in kürzester Zeit beherrschen. Im Gegensatz dazu sind C-Codebasen oft schwer zu verstehen, weil sie dazu neigen, sich in ein Rattennest aus Makros und #ifdefs zu verwandeln. Die Syntax von Go und seinen eingebauten Code-Formatierungs- und Projektmanagement-Tools sollen dieses Problem in Schach halten.

Go verfügt zudem über Extras wie Goroutines und Channels. Das erleichtert die Erstellung entsprechender Software erheblich. In C müssten solche Dinge von Hand programmiert oder über eine externe Bibliothek bereitgestellt werden.

Der größte Unterschied zwischen Go und C besteht im Speichermanagement. Go-Objekte werden automatisch gemanagt und standardmäßig bereinigt. Für die meisten Programmieraufgaben ist das äußerst praktisch. Es bedeutet aber auch, dass jedes Programm, das einen deterministischen Umgang mit dem Speicher erfordert, schwieriger zu schreiben ist.

Go enthält das unsafe-Paket, mit dem etwa Lesen und Schreiben von beliebigem Speicher mit einem Pointer-Type umgangen werden kann. Allerdings sind Programme, die unsafe geschrieben werden, möglicherweise nicht portierbar beziehungsweise kompatibel.

Go eignet sich hervorragend, um Kommandozeilen-Ultilities und Netzwerk-Dienste zu erstellen, weil diese selten solche granularen Einstellungen benötigen. Für Low-Level-Gerätetreiber, Kernel-Space-Betriebssystem-Komponenten und andere Aufgaben, die eine genaue Kontrolle über Speicherlayout und -verwaltung erfordern, empfiehlt sich C.

C vs. Rust

In gewisser Weise ist Rust eine Antwort auf die Probleme mit der Speicherverwaltung in C und C++. Und Rust beseitigt nicht nur diese Unzulänglichkeit der beiden Programmiersprachen. Rust lässt sich in nativen Maschinencode kompilieren und ist daher in Bezug auf die Leistung mit C gleichzusetzen. Das Hauptargument für Rust ist jedoch seine standardmäßige Speichersicherheit.

Die Syntax und die Kompilierungsregeln von Rust helfen Entwicklern, häufige Memory-Management-Fehler zu vermeiden. Wenn ein Programm ein Speicherverwaltungsproblem hat, das die Rust-Syntax überschreitet, lässt es sich einfach nicht kompilieren. Neueinsteiger, vor allem, wenn sie von einer Sprache wie C kommen, verbringen die erste Phase ihrer Rust-Ausbildung damit zu lernen, wie sie den Compiler besänftigen können. Befürworter von Rust argumentieren allerdings, dass sich die steile Lernkurve langfristig in Form von sichererem Code ohne Geschwindigkeitseinbußen auszahlt.

Das Tooling von Rust ist besser als das von C: Projekt- und Komponentenmanagement sind Teil der Toolchain, die mit Rust ausgeliefert wird. Es gibt einen standardmäßig empfohlenen Weg, um Pakete zu verwalten, Projektordner zu organisieren und viele andere Dinge zu handhaben, die in C bestenfalls ad-hoc ablaufen und von jedem Projekt und Team anders gehandhabt werden.

Doch was als Vorteil von Rust angepriesen wird, mag einem C-Entwickler nicht unbedingt als solcher erscheinen: Die Sicherheitsfunktionen von Rust zur Kompilierzeit können nicht deaktiviert werden. Deshalb muss selbst das trivialste Rust-Programm den strengen Anforderungen an die Speichersicherheit genügen. C mag standardmäßig weniger sicher sein, ist aber deutlich flexibler und nachsichtiger, wenn es um Fehler geht.

Ein weiterer möglicher Nachteil von Rust ist sein Funktionsumfang: C hat relativ wenige Funktionen, selbst wenn man die Standardbibliothek mit einbezieht. Der Funktionsumfang von Rust ist massiv und wächst ständig weiter. Wie bei C++ bedeutet das mehr Leistung, aber auch mehr Komplexität. C ist weniger komplex, aber viel leichter zu modellieren, also wohl besser für Projekte geeignet, die nicht ausufern sollen.

C vs. Python

Wenn heutzutage von Softwareentwicklung die Rede ist, scheint Python immer im Gespräch zu sein. Schließlich handelt es sich mit Tausenden von Drittanbieter-Bibliotheken zweifellos um eine der vielseitigsten Programmiersprachen.

Dass die Entwicklungs- Vorrang vor der Ausführungsgeschwindigkeit hat, ist das, was Python besonders auszeichnet und am deutlichsten von C abhebt. Applikationen, die mit C in einer Stunde erstellt werden, lassen sich mit Python in Minuten zusammenstellen. Umgekehrt kann es Sekunden dauern, ein solches Programm in C auszuführen, in Python dafür eine Minute. Als Faustregel gilt, dass Python-Programme in der Regel viel langsamer sind als ihre C-Pendants. Dennoch ist Python schnell genug, um sich für etliche Aufgaben auf moderner Hardware zu eignen.

Ein wichtiger Unterschied besteht auch hier in der Speicherverwaltung: Python-Programme werden vollständig von der Python-Laufzeitumgebung verwaltet. Entwickler müsse sich also nicht um die Feinheiten der Zuweisung und Freigabe von Speicher kümmern. Aber auch hier wird die Laufzeitleistung der Bequemlichkeit geopfert. C-Programme zu schreiben, erfordert sorgfältiges Memory Management, aber die Ergebnisse sind oft Goldstandard, wenn es rein um Maschinengeschwindigkeit geht.

Unter der Oberfläche weisen Python und C jedoch eine tiefe Verbindung auf: Die Referenz-Python-Laufzeitumgebung ist in C geschrieben. Das ermöglicht es Python-Programmen, in C und C++ geschriebene Bibliotheken zu verpacken. Große Teile des Python-Ökosystems mit Bibliotheken von Drittanbietern, etwa für maschinelles Lernen, enthalten im Kern C-Code. In vielen Fällen stellt sich nicht die Frage C oder Python, sondern eher die, welche Teile der Anwendung in C und welche in Python geschrieben werden sollten.

Wenn die Entwicklungsgeschwindigkeit wichtiger als die der Ausführung ist und die meisten leistungsfähigen Teile des Programms in eigenständigen Komponenten isoliert werden können (und nicht über den gesamten Code verteilt sind), ist entweder reines Python oder eine Mischung aus Python- und C-Bibliotheken eine bessere Wahl als nur C. Ansonsten nicht.

C vs. Carbon

Ein weiterer möglicher Konkurrent für C und C++ ist die relativ neue Programmiersprache Carbon, an der derzeit intensiv entwickelt wird. Carbon soll eine moderne Alternative zu C und C++ darstellen und sich durch eine einfache Syntax, moderne Werkzeuge und Code-Organisationstechniken auszeichnen. Darüber hinaus will die Coding-Sprache mit Lösungen für Probleme überzeugen, die C- und C++-Programmierer schon seit langem plagen. Carbon soll auch eine Interoperabilität mit C++-Codebasen bieten, so dass vorhandener Code schrittweise migriert werden kann. All diese Bemühungen sind begrüßenswert, da C und C++ im Vergleich zu neueren Sprachen oft über eher primitive Werkzeuge und Prozesse verfügen.

Was ist also der Nachteil? Im Moment ist Carbon ein experimentelles Projekt, das nicht im Entferntesten für den Produktiveinsatz bereit ist. Es gibt nicht einmal einen funktionierenden Compiler, sondern nur einen Online-Code-Explorer. Bis Carbon also eine echte Alternative zu C oder C++ ist, wird noch viel Zeit vergehen - wenn es überhaupt jemals dazu kommt.

Dieser Beitrag basiert auf einem Artikel unserer US-Schwesterpublikation InfoWorld.