Ein eingehender Blick auf C++ vs. Java

Veröffentlicht: 2022-07-22

Unzählige Artikel vergleichen die technischen Features von C++ und Java, aber welche Unterschiede sind am wichtigsten? Wenn ein Vergleich beispielsweise zeigt, dass Java keine Mehrfachvererbung unterstützt und C++ schon, was bedeutet das? Und ist es eine gute Sache? Einige argumentieren, dass dies ein Vorteil von Java ist, während andere es als Problem deklarieren.

Lassen Sie uns die Situationen untersuchen, in denen Entwickler C++, Java oder eine andere Sprache insgesamt wählen sollten – und, was noch wichtiger ist, warum diese Entscheidung wichtig ist.

Untersuchung der Grundlagen: Sprachaufbau und Ökosysteme

C++ wurde 1985 als Frontend für C-Compiler eingeführt, ähnlich wie TypeScript zu JavaScript kompiliert. Moderne C++-Compiler kompilieren normalerweise zu nativem Maschinencode. Obwohl einige behaupten, dass die Compiler von C++ die Portabilität verringern und Neuerstellungen für neue Zielarchitekturen erforderlich machen, läuft C++-Code auf fast jeder Prozessorplattform.

Java wurde erstmals 1995 veröffentlicht und baut nicht direkt auf nativem Code auf. Stattdessen erstellt Java Bytecode, eine binäre Zwischendarstellung, die auf der Java Virtual Machine (JVM) ausgeführt wird. Mit anderen Worten, die Ausgabe des Java-Compilers benötigt eine plattformspezifische native ausführbare Datei, um ausgeführt zu werden.

Sowohl C++ als auch Java gehören zur Familie der C-ähnlichen Sprachen, da sie C in ihrer Syntax im Allgemeinen ähneln. Der bedeutendste Unterschied sind ihre Ökosysteme: Während C++ Bibliotheken auf Basis von C oder C++ oder die API eines Betriebssystems nahtlos aufrufen kann, eignet sich Java am besten für Java-basierte Bibliotheken. Sie können mit der Java Native Interface (JNI) API auf C-Bibliotheken in Java zugreifen, aber sie ist fehleranfällig und erfordert etwas C- oder C++-Code. C++ interagiert auch einfacher mit Hardware als Java, da C++ eine niedrigere Programmiersprache ist.

Detaillierte Kompromisse: Generika, Speicher und mehr

Wir können C++ aus vielen Perspektiven mit Java vergleichen. In einigen Fällen ist die Entscheidung zwischen C++ und Java klar. Native Android-Anwendungen sollten normalerweise Java verwenden, es sei denn, die App ist ein Spiel. Die meisten Spieleentwickler sollten sich für C++ oder eine andere Sprache entscheiden, um eine möglichst flüssige Echtzeitanimation zu erhalten. Die Speicherverwaltung von Java verursacht häufig Verzögerungen während des Spiels.

Plattformübergreifende Anwendungen, die keine Spiele sind, würden den Rahmen dieser Diskussion sprengen. Weder C++ noch Java sind in diesem Fall ideal, da sie zu ausführlich für eine effiziente GUI-Entwicklung sind. Für Hochleistungs-Apps ist es am besten, C++-Module zu erstellen, um die schwere Arbeit zu erledigen, und eine entwicklerproduktivere Sprache für die GUI zu verwenden.

Plattformübergreifende Anwendungen, die keine Spiele sind, würden den Rahmen dieser Diskussion sprengen. Weder C++ noch Java sind in diesem Fall ideal, da sie zu ausführlich für eine effiziente GUI-Entwicklung sind.

Twittern

Bei einigen Projekten ist die Auswahl möglicherweise nicht klar, also vergleichen wir weiter:

Feature C++ Java
Anfängerfreundlich Nein Ja
Laufzeitleistung Am besten Gut
Latenz Vorhersagbar Unberechenbar
Referenzzählende intelligente Zeiger Ja Nein
Globale Mark-and-Sweep-Speicherbereinigung Nein Erforderlich
Stack-Speicherzuweisung Ja Nein
Kompilierung in eine native ausführbare Datei Ja Nein
Kompilierung in Java-Bytecode Nein Ja
Direkte Interaktion mit Low-Level-Betriebssystem-APIs Ja Erfordert C-Code
Direkte Interaktion mit C-Bibliotheken Ja Erfordert C-Code
Direkte Interaktion mit Java-Bibliotheken Über JNI Ja
Standardisierte Build- und Paketverwaltung Nein Maven


Neben den in der Tabelle verglichenen Funktionen konzentrieren wir uns auch auf Funktionen der objektorientierten Programmierung (OOP) wie Mehrfachvererbung, Generics/Templates und Reflektion. Beachten Sie, dass beide Sprachen OOP unterstützen: Java schreibt dies vor, während C++ OOP neben globalen Funktionen und statischen Daten unterstützt.

Mehrfachvererbung

In OOP ist Vererbung, wenn eine untergeordnete Klasse Attribute und Methoden von einer übergeordneten Klasse erbt. Ein Standardbeispiel ist eine Rectangle -Klasse, die von einer allgemeineren Shape -Klasse erbt:

 // Note that we are in a C++ file class Shape { // Position int x, y; public: // The child class must override this pure virtual function virtual void draw() = 0; }; class Rectangle: public Shape { // Width and height int w, h; public: void draw(); };

Mehrfachvererbung liegt vor, wenn eine Kindklasse von mehreren Eltern erbt. Hier ist ein Beispiel mit den Klassen Rectangle und Shape und einer zusätzlichen Clickable -Klasse:

 // Not recommended class Shape {...}; class Rectangle: public Shape {...}; class Clickable { int xClick, yClick; public: virtual void click() = 0; }; class ClickableRectangle: public Rectangle, public Clickable { void click(); };

In diesem Fall haben wir zwei Basistypen: Shape (der Basistyp von Rectangle ) und Clickable . ClickableRectangle erbt von beiden, um die beiden Objekttypen zusammenzusetzen.

C++ unterstützt Mehrfachvererbung; Java nicht. Mehrfachvererbung ist in bestimmten Grenzfällen nützlich, wie zum Beispiel:

  • Erstellen einer erweiterten domänenspezifischen Sprache (DSL).
  • Durchführen anspruchsvoller Berechnungen zur Kompilierzeit.
  • Verbesserung der Sicherheit von Projekttypen auf eine Weise, die in Java einfach nicht möglich ist.

Es wird jedoch im Allgemeinen davon abgeraten, Mehrfachvererbung zu verwenden. Es kann den Code verkomplizieren und die Leistung beeinträchtigen, wenn es nicht mit Template-Metaprogrammierung kombiniert wird – etwas, das am besten nur von den erfahrensten C++-Programmierern durchgeführt wird.

Generika und Vorlagen

Generische Versionen von Klassen, die mit beliebigen Datentypen funktionieren, sind praktisch für die Wiederverwendung von Code. Beide Sprachen bieten diese Unterstützung – Java durch Generika, C++ durch Vorlagen – aber die Flexibilität von C++-Vorlagen kann die erweiterte Programmierung sicherer und robuster machen. C++-Compiler erstellen jedes Mal neue benutzerdefinierte Klassen oder Funktionen, wenn Sie verschiedene Typen mit der Vorlage verwenden. Darüber hinaus können C++-Vorlagen benutzerdefinierte Funktionen aufrufen, die auf den Typen der Parameter der Funktion der obersten Ebene basieren, wodurch bestimmten Datentypen ein spezieller Code ermöglicht wird. Dies wird als Template-Spezialisierung bezeichnet. Java hat keine entsprechende Funktion.

Im Gegensatz dazu erstellen Java-Compiler bei der Verwendung von Generika allgemeine Objekte ohne Typen durch einen Prozess, der als Typlöschung bezeichnet wird. Java führt während der Kompilierung eine Typprüfung durch, aber Programmierer können das Verhalten einer generischen Klasse oder Methode nicht basierend auf ihren Typparametern ändern. Um dies besser zu verstehen, sehen wir uns ein kurzes Beispiel einer generischen std::string format(std::string fmt, T1 item1, T2 item2) Funktion an, die eine Vorlage, template<class T1, class T2> , aus C++ verwendet Bibliothek, die ich erstellt habe:

 std::string firstParameter = "A string"; int secondParameter = 123; // Format printed output as an eight-character-wide string and a hexadecimal value format("%8s %x", firstParameter, secondParameter); // Format printed output as two eight-character-wide strings format("%8s %8s", firstParameter, secondParameter);

C++ würde die Formatfunktion als std::string format std::string format(std::string fmt, std::string item1, int item2) , während Java sie ohne die spezifischen string und int -Objekttypen für item1 und item2 erstellen würde. In diesem Fall weiß unser C++-Template, dass der letzte eingehende Parameter ein int ist, und kann daher die erforderliche std::to_string Konvertierung im zweiten format durchführen. Ohne Vorlagen würde eine printf -Anweisung von C++, die versucht, eine Zahl als Zeichenfolge wie im zweiten format auszugeben, ein undefiniertes Verhalten aufweisen und die Anwendung zum Absturz bringen oder Datenmüll ausgeben. Die Java-Funktion könnte eine Zahl nur beim ersten format als String behandeln und würde sie nicht direkt als hexadezimale Ganzzahl formatieren. Dies ist ein triviales Beispiel, aber es demonstriert die Fähigkeit von C++, ein spezialisiertes Template auszuwählen, um ein beliebiges Klassenobjekt zu handhaben, ohne seine Klasse oder die format zu ändern. Wir können die Ausgabe in Java korrekt erzeugen, indem wir Reflektion anstelle von Generika verwenden, obwohl diese Methode weniger erweiterbar und fehleranfälliger ist.

Betrachtung

In Java ist es möglich, (zur Laufzeit) strukturelle Details herauszufinden, z. B. welche Mitglieder in einer Klasse oder einem Klassentyp verfügbar sind. Dieses Merkmal wird Reflexion genannt, vermutlich weil es so ist, als würde man einen Spiegel an das Objekt halten, um zu sehen, was sich darin befindet. (Weitere Informationen finden Sie in der Reflection-Dokumentation von Oracle.)

C++ hat keine vollständige Reflektion, aber modernes C++ bietet Runtime Type Information (RTTI). RTTI ermöglicht die Laufzeiterkennung bestimmter Objekttypen, kann jedoch nicht auf Informationen wie die Mitglieder des Objekts zugreifen.

Speicherverwaltung

Ein weiterer entscheidender Unterschied zwischen C++ und Java ist die Speicherverwaltung, die zwei Hauptansätze hat: manuell, bei dem Entwickler den Speicher manuell verfolgen und freigeben müssen; und automatisch, bei dem die Software verfolgt, welche Objekte noch verwendet werden, um ungenutzten Speicher zu recyceln. Ein Beispiel in Java ist die Garbage Collection.

Java erfordert Garbage Collection-Speicher, bietet eine einfachere Speicherverwaltung als der manuelle Ansatz und eliminiert Speicherfreigabefehler, die häufig zu Sicherheitslücken beitragen. C++ bietet nativ keine automatische Speicherverwaltung, aber es unterstützt eine Form der Garbage Collection namens Smart Pointer. Intelligente Zeiger verwenden Referenzzählung und sind sicher und leistungsfähig, wenn sie richtig verwendet werden. C++ bietet auch Destruktoren, die bei der Zerstörung eines Objekts Ressourcen bereinigen oder freigeben.

Während Java nur die Heap-Zuweisung anbietet, unterstützt C++ sowohl die Heap-Zuweisung (unter Verwendung von new und delete oder den älteren malloc -Funktionen von C) als auch die Stack-Zuweisung. Die Stack-Zuweisung kann schneller und sicherer sein als die Heap-Zuweisung, da ein Stack eine lineare Datenstruktur ist, während ein Heap baumbasiert ist, sodass Stapelspeicher viel einfacher zuzuweisen und freizugeben ist.

Ein weiterer Vorteil von C++ im Zusammenhang mit der Stapelzuweisung ist eine Programmiertechnik, die als Resource Acquisition Is Initialization (RAII) bekannt ist. In RAII sind Ressourcen wie Referenzen an den Lebenszyklus ihres steuernden Objekts gebunden; Die Ressourcen werden am Ende des Lebenszyklus dieses Objekts zerstört. RAII ist die Art und Weise, wie intelligente C++-Zeiger ohne manuelles Dereferenzieren funktionieren – ein intelligenter Zeiger, auf den am Anfang einer Funktion verwiesen wird, wird beim Beenden der Funktion automatisch dereferenziert. Der angeschlossene Speicher wird auch freigegeben, wenn dies der letzte Verweis auf den Smart Pointer ist. Obwohl Java ein ähnliches Muster bietet, ist es umständlicher als RAII von C++, insbesondere wenn Sie mehrere Ressourcen im selben Codeblock erstellen müssen.

Laufzeitleistung

Java hat eine solide Laufzeitleistung, aber C++ hält immer noch die Krone, da die manuelle Speicherverwaltung schneller ist als die Garbage Collection für reale Anwendungen. Obwohl Java C++ in bestimmten Eckfällen aufgrund der JIT-Kompilierung übertreffen kann, gewinnt C++ die meisten nicht-trivialen Fälle.

Insbesondere die Standard-Speicherbibliothek von Java überlastet den Garbage Collector mit seinen Zuweisungen im Vergleich zu C++s reduzierter Verwendung von Heap-Zuweisungen. Java ist jedoch immer noch relativ schnell und sollte akzeptabel sein, es sei denn, die Latenz ist ein Hauptanliegen – beispielsweise in Spielen oder Anwendungen mit Echtzeitbeschränkungen.

Build- und Paketverwaltung

Was Java an Leistung fehlt, macht es durch Benutzerfreundlichkeit wett. Eine Komponente, die sich auf die Entwicklereffizienz auswirkt, ist das Build- und Paketmanagement – ​​wie wir Projekte erstellen und externe Abhängigkeiten in eine Anwendung einbringen. In Java vereinfacht ein Tool namens Maven diesen Prozess in ein paar einfache Schritte und lässt sich in viele IDEs wie IntelliJ IDEA integrieren.

In C++ existiert jedoch kein standardisiertes Paket-Repository. Es gibt nicht einmal eine standardisierte Methode zum Erstellen von C++-Code in Anwendungen: Einige Entwickler bevorzugen Visual Studio, während andere CMake oder andere benutzerdefinierte Tools verwenden. Die Komplexität wird noch dadurch erhöht, dass bestimmte kommerzielle C++-Bibliotheken binär formatiert sind und es keine konsistente Möglichkeit gibt, diese Bibliotheken in den Erstellungsprozess zu integrieren. Darüber hinaus können Abweichungen in den Build-Einstellungen oder Compiler-Versionen zu Herausforderungen führen, wenn es darum geht, Binärbibliotheken zum Laufen zu bringen.

Anfängerfreundlichkeit

Friktionen bei der Build- und Paketverwaltung sind nicht der einzige Grund, warum C++ weitaus weniger anfängerfreundlich ist als Java. Ein Programmierer kann Schwierigkeiten haben, C++ sicher zu debuggen und zu verwenden, es sei denn, er ist mit C, Assemblersprachen oder der Funktionsweise eines Computers auf niedrigerer Ebene vertraut. Stellen Sie sich C++ wie ein Elektrowerkzeug vor: Es kann viel bewirken, aber es ist gefährlich, wenn es missbraucht wird.

Der oben erwähnte Speicherverwaltungsansatz von Java macht es auch viel zugänglicher als C++. Java-Programmierer müssen sich nicht um die Freigabe von Objektspeicher kümmern, da sich die Sprache automatisch darum kümmert.

Zeit der Entscheidung: C++ oder Java?

Ein Flussdiagramm mit einer dunkelblauen „Start“-Blase in der oberen linken Ecke, die schließlich über eine Reihe von weißen Entscheidungskreuzungen mit dunkelblauen Zweigen für „Ja“ und andere Optionen mit einem von sieben hellblauen Schlussfolgerungskästchen darunter verbunden ist, und hellblaue Äste für „Nein“. Die erste ist "Plattformübergreifende GUI-App?" von denen ein "Ja" auf die Schlussfolgerung hinweist: "Wählen Sie eine plattformübergreifende Entwicklungsumgebung und verwenden Sie ihre primäre Sprache." Ein "Nein" weist auf "Native Android App?" von denen ein "Ja" auf eine sekundäre Frage hinweist: "Ist es ein Spiel?" Bei der zweiten Frage weist ein „Nein“ auf die Schlussfolgerung „Verwende Java (oder Kotlin)“ und ein „Ja“ auf eine andere Schlussfolgerung hin: „Wähle eine plattformübergreifende Spiele-Engine und verwende die empfohlene Sprache.“ Von der "nativen Android-App?" Frage, zeigt ein "Nein" auf "Native Windows-App?" von denen ein "Ja" auf eine sekundäre Frage hinweist: "Ist es ein Spiel?" Bei der sekundären Frage weist ein „Ja“ auf die Schlussfolgerung „Wählen Sie eine plattformübergreifende Spiel-Engine und verwenden Sie die empfohlene Sprache“ und ein „Nein“ auf eine andere Schlussfolgerung hin: „Wählen Sie eine Windows-GUI-Umgebung und verwenden Sie ihre primäre Sprache (normalerweise C++ oder C#)." Von der "nativen Windows-App?" Frage, zeigt ein "Nein" auf "Server-App?" von denen ein "Ja" auf eine sekundäre Frage hinweist, "Entwicklertyp?" Ausgehend von der sekundären Frage verweist eine Entscheidung „Mittlere Qualifikation“ auf die Schlussfolgerung „Verwenden Sie Java (oder C# oder TypeScript)“ und eine Entscheidung „Erfahren“ verweist auf eine dritte Frage „Höchste Priorität?“. Ausgehend von der dritten Frage weist eine Entscheidung „Entwicklerproduktivität“ auf die Schlussfolgerung „Verwende Java (oder C# oder TypeScript)“ und eine Entscheidung „Leistung“ auf eine andere Schlussfolgerung „Verwende C++ (oder Rust)“. Von der "Server-App?" Frage, ein "Nein" weist auf eine Nebenfrage hin, "Treiberentwicklung?" Bei der sekundären Frage weist ein „Ja“ auf eine Schlussfolgerung hin, „Verwenden Sie C++ (oder Rust)“, und ein „Nein“ weist auf eine dritte Frage hin, „IoT-Entwicklung?“. Von der tertiären Frage zeigt "Ja" auf die Schlussfolgerung "Verwenden Sie C++ (oder Rust)" und ein "Nein" zeigt auf eine quaternäre Frage "Hochgeschwindigkeitshandel?" Von der quaternären Frage zeigt ein „Ja“ auf die Schlussfolgerung „Verwenden Sie C++ (oder Rust)“ und ein „Nein“ auf die letzte verbleibende Schlussfolgerung „Fragen Sie jemanden, der mit Ihrer Zieldomäne vertraut ist“.
Ein erweiterter Leitfaden zur Auswahl der besten Sprache für verschiedene Projekttypen.

Nachdem wir nun die Unterschiede zwischen C++ und Java ausführlich untersucht haben, kehren wir zu unserer ursprünglichen Frage zurück: C++ oder Java? Selbst mit einem tiefen Verständnis der beiden Sprachen gibt es keine allgemeingültige Antwort.

Softwareingenieure, die mit Low-Level-Programmierkonzepten nicht vertraut sind, sind möglicherweise besser beraten, Java zu wählen, wenn sie die Entscheidung auf C++ oder Java beschränken, mit Ausnahme von Echtzeitkontexten wie Spielen. Entwickler, die ihren Horizont erweitern möchten, können dagegen möglicherweise mehr lernen, wenn sie sich für C++ entscheiden.

Allerdings dürften die technischen Unterschiede zwischen C++ und Java nur ein kleiner Faktor bei der Entscheidung sein. Bestimmte Arten von Produkten erfordern besondere Entscheidungen. Wenn Sie sich immer noch nicht sicher sind, können Sie das Flussdiagramm konsultieren – aber denken Sie daran, dass es Sie letztendlich auf eine dritte Sprache hinweisen kann.