Un regard approfondi sur C++ par rapport à Java

Publié: 2022-07-22

D'innombrables articles comparent les fonctionnalités techniques de C++ et de Java, mais quelles sont les différences les plus importantes à prendre en compte ? Lorsqu'une comparaison montre, par exemple, que Java ne prend pas en charge l'héritage multiple et que C++ le fait, qu'est-ce que cela signifie ? Et est-ce une bonne chose ? Certains prétendent que c'est un avantage de Java, tandis que d'autres déclarent que c'est un problème.

Explorons les situations dans lesquelles les développeurs devraient choisir C++, Java ou un autre langage et, plus important encore, pourquoi la décision est importante.

Examen des bases : constructions de langage et écosystèmes

C ++ a été lancé en 1985 en tant que frontal des compilateurs C, de la même manière que TypeScript se compile en JavaScript. Les compilateurs C++ modernes compilent généralement en code machine natif. Bien que certains prétendent que les compilateurs de C++ réduisent sa portabilité et qu'ils nécessitent des reconstructions pour les nouvelles architectures cibles, le code C++ s'exécute sur presque toutes les plates-formes de processeur.

Lancé pour la première fois en 1995, Java ne se construit pas directement sur du code natif. Au lieu de cela, Java construit un bytecode, une représentation binaire intermédiaire qui s'exécute sur la machine virtuelle Java (JVM). En d'autres termes, la sortie du compilateur Java a besoin d'un exécutable natif spécifique à la plate-forme pour s'exécuter.

C ++ et Java appartiennent tous deux à la famille des langages de type C, car ils ressemblent généralement à C dans leur syntaxe. La différence la plus significative réside dans leurs écosystèmes : alors que C++ peut appeler de manière transparente des bibliothèques basées sur C ou C++, ou l'API d'un système d'exploitation, Java est le mieux adapté aux bibliothèques basées sur Java. Vous pouvez accéder aux bibliothèques C en Java à l'aide de l'API JNI (Java Native Interface), mais elle est sujette aux erreurs et nécessite du code C ou C++. C++ interagit également avec le matériel plus facilement que Java, car C++ est un langage de niveau inférieur.

Compromis détaillés : génériques, mémoire, etc.

Nous pouvons comparer C++ à Java sous plusieurs angles. Dans certains cas, la décision entre C++ et Java est claire. Les applications Android natives doivent généralement utiliser Java, sauf si l'application est un jeu. La plupart des développeurs de jeux devraient opter pour C++ ou un autre langage pour une animation en temps réel la plus fluide possible ; La gestion de la mémoire de Java provoque souvent un décalage pendant le jeu.

Les applications multiplateformes qui ne sont pas des jeux sortent du cadre de cette discussion. Ni C++ ni Java ne sont idéaux dans ce cas car ils sont trop verbeux pour un développement efficace de l'interface graphique. Pour les applications hautes performances, il est préférable de créer des modules C++ pour faire le gros du travail et d'utiliser un langage plus productif pour les développeurs pour l'interface graphique.

Les applications multiplateformes qui ne sont pas des jeux sortent du cadre de cette discussion. Ni C++ ni Java ne sont idéaux dans ce cas car ils sont trop verbeux pour un développement efficace de l'interface graphique.

Tweeter

Pour certains projets, le choix peut ne pas être clair, alors comparons davantage :

Caractéristique C++ Java
Adapté aux débutants Non Oui
Performances d'exécution Meilleur Bien
Latence Prévisible Imprévisible
Pointeurs intelligents de comptage de références Oui Non
Collecte globale des ordures par marquage et balayage Non Obligatoire
Allocation de mémoire de pile Oui Non
Compilation en exécutable natif Oui Non
Compilation en bytecode Java Non Oui
Interaction directe avec les API du système d'exploitation de bas niveau Oui Nécessite le code C
Interaction directe avec les bibliothèques C Oui Nécessite le code C
Interaction directe avec les bibliothèques Java Via JNI Oui
Gestion standardisée des builds et des packages Non Maven


Outre les fonctionnalités comparées dans le tableau, nous nous concentrerons également sur les fonctionnalités de programmation orientée objet (POO) telles que l'héritage multiple, les génériques/modèles et la réflexion. Notez que les deux langages prennent en charge la POO : Java l'impose, tandis que C++ prend en charge la POO aux côtés des fonctions globales et des données statiques.

Héritage multiple

En POO, l'héritage se produit lorsqu'une classe enfant hérite des attributs et des méthodes d'une classe parent. Un exemple standard est une classe Rectangle qui hérite d'une classe Shape plus générique :

 // 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(); };

L'héritage multiple se produit lorsqu'une classe enfant hérite de plusieurs parents. Voici un exemple, utilisant les classes Rectangle et Shape et une classe Clickable supplémentaire :

 // 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(); };

Dans ce cas, nous avons deux types de base : Shape (le type de base de Rectangle ) et Clickable . ClickableRectangle hérite des deux pour composer les deux types d'objets.

C++ prend en charge l'héritage multiple ; Java ne le fait pas. L'héritage multiple est utile dans certains cas extrêmes, tels que :

  • Création d'un langage avancé spécifique au domaine (DSL).
  • Effectuer des calculs sophistiqués au moment de la compilation.
  • Améliorer la sécurité du type de projet d'une manière qui n'est tout simplement pas possible en Java.

Cependant, l'utilisation de l'héritage multiple est généralement déconseillée. Cela peut compliquer le code et avoir un impact sur les performances à moins d'être combiné avec la métaprogrammation de modèles, ce qui est mieux fait uniquement par les programmeurs C++ les plus expérimentés.

Génériques et modèles

Les versions génériques des classes qui fonctionnent avec n'importe quel type de données sont pratiques pour la réutilisation du code. Les deux langages offrent cette prise en charge (Java via les génériques, C++ via les modèles), mais la flexibilité des modèles C++ peut rendre la programmation avancée plus sûre et plus robuste. Les compilateurs C++ créent de nouvelles classes ou fonctions personnalisées chaque fois que vous utilisez différents types avec le modèle. De plus, les modèles C++ peuvent appeler des fonctions personnalisées basées sur les types des paramètres de la fonction de niveau supérieur, permettant à des types de données particuliers d'avoir un code spécialisé. C'est ce qu'on appelle la spécialisation des modèles. Java n'a pas de fonctionnalité équivalente.

En revanche, lors de l'utilisation de génériques, les compilateurs Java créent des objets généraux sans types via un processus appelé effacement de type. Java effectue une vérification de type lors de la compilation, mais les programmeurs ne peuvent pas modifier le comportement d'une classe ou d'une méthode générique en fonction de ses paramètres de type. Pour mieux comprendre cela, regardons un exemple rapide d'une fonction générique std::string format(std::string fmt, T1 item1, T2 item2) qui utilise un modèle, template<class T1, class T2> , d'un C++ bibliothèque que j'ai créé:

 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++ produirait la fonction de format comme std::string format(std::string fmt, std::string item1, int item2) , alors que Java la créerait sans les types d'objet string et int spécifiques pour item1 et item2 . Dans ce cas, notre modèle C++ sait que le dernier paramètre entrant est un int et peut donc effectuer la conversion std::to_string nécessaire dans le deuxième appel de format . Sans modèles, une instruction printf C++ essayant d'imprimer un nombre sous forme de chaîne comme dans le second appel de format aurait un comportement indéfini et pourrait planter l'application ou imprimer des ordures. La fonction Java ne serait capable de traiter un nombre que comme une chaîne dans le premier appel de format et ne le formaterait pas directement comme un entier hexadécimal. Ceci est un exemple trivial, mais il démontre la capacité de C++ à sélectionner un modèle spécialisé pour gérer n'importe quel objet de classe arbitraire sans modifier sa classe ou la fonction de format . Nous pouvons produire correctement la sortie en Java en utilisant la réflexion au lieu des génériques, bien que cette méthode soit moins extensible et plus sujette aux erreurs.

Réflexion

En Java, il est possible de trouver (au moment de l'exécution) des détails structurels tels que les membres disponibles dans une classe ou un type de classe. Cette fonctionnalité est appelée réflexion, probablement parce que c'est comme tenir un miroir sur l'objet pour voir ce qu'il y a à l'intérieur. (Plus d'informations peuvent être trouvées dans la documentation de réflexion d'Oracle.)

C++ n'a pas de réflexion complète, mais le C++ moderne offre des informations de type d'exécution (RTTI). RTTI permet la détection à l'exécution de types d'objets spécifiques, bien qu'il ne puisse pas accéder aux informations telles que les membres de l'objet.

Gestion de la mémoire

Une autre différence critique entre C++ et Java est la gestion de la mémoire, qui a deux approches principales : manuelle, où les développeurs doivent suivre et libérer la mémoire manuellement ; et automatique, où le logiciel suit les objets qui sont encore utilisés pour recycler la mémoire inutilisée. En Java, un exemple est la récupération de place.

Java nécessite une mémoire récupérée, ce qui facilite la gestion de la mémoire par rapport à l'approche manuelle et élimine les erreurs de libération de mémoire qui contribuent généralement aux vulnérabilités de sécurité. C++ ne fournit pas de gestion automatique de la mémoire de manière native, mais il prend en charge une forme de récupération de place appelée pointeurs intelligents. Les pointeurs intelligents utilisent le comptage de références et sont sécurisés et performants s'ils sont utilisés correctement. C++ propose également des destructeurs qui nettoient ou libèrent des ressources lors de la destruction d'un objet.

Alors que Java n'offre que l'allocation de tas, C++ prend en charge à la fois l'allocation de tas (en utilisant new et delete ou les anciennes fonctions C malloc ) et l'allocation de pile. L'allocation de pile peut être plus rapide et plus sûre que l'allocation de tas car une pile est une structure de données linéaire tandis qu'un tas est basé sur une arborescence, de sorte que la mémoire de la pile est beaucoup plus simple à allouer et à libérer.

Un autre avantage de C++ lié à l'allocation de pile est une technique de programmation appelée RAII (Resource Acquisition Is Initialization). Dans RAII, les ressources telles que les références sont liées au cycle de vie de leur objet de contrôle ; les ressources seront détruites à la fin du cycle de vie de cet objet. RAII est la façon dont les pointeurs intelligents C++ fonctionnent sans déréférencement manuel : un pointeur intelligent référencé en haut d'une fonction est automatiquement déréférencé à la sortie de la fonction. La mémoire connectée est également libérée s'il s'agit de la dernière référence au pointeur intelligent. Bien que Java offre un modèle similaire, il est plus gênant que le RAII de C++, surtout si vous devez créer plusieurs ressources dans le même bloc de code.

Performances d'exécution

Java a de solides performances d'exécution, mais C++ détient toujours la couronne puisque la gestion manuelle de la mémoire est plus rapide que la récupération de place pour les applications du monde réel. Bien que Java puisse surpasser C++ dans certains cas extrêmes en raison de la compilation JIT, C++ remporte la plupart des cas non triviaux.

En particulier, la bibliothèque de mémoire standard de Java surcharge le ramasse-miettes avec ses allocations par rapport à l'utilisation réduite des allocations de tas par C++. Cependant, Java est encore relativement rapide et devrait être acceptable à moins que la latence ne soit une préoccupation majeure, par exemple, dans les jeux ou les applications avec des contraintes en temps réel.

Gestion des builds et des packages

Ce que Java manque en performances, il le compense en facilité d'utilisation. L'un des composants affectant l'efficacité des développeurs est la gestion des builds et des packages, c'est-à-dire la manière dont nous créons des projets et intégrons des dépendances externes dans une application. En Java, un outil appelé Maven simplifie ce processus en quelques étapes simples et s'intègre à de nombreux IDE tels qu'IntelliJ IDEA.

En C++, cependant, aucun référentiel de packages standardisé n'existe. Il n'existe même pas de méthode standardisée pour créer du code C++ dans les applications : certains développeurs préfèrent Visual Studio, tandis que d'autres utilisent CMake ou un autre ensemble d'outils personnalisés. Ajoutant encore à la complexité, certaines bibliothèques C++ commerciales sont au format binaire, et il n'existe aucun moyen cohérent d'intégrer ces bibliothèques dans le processus de construction. De plus, les variations dans les paramètres de construction ou les versions du compilateur peuvent poser des problèmes pour faire fonctionner les bibliothèques binaires.

Convivialité pour les débutants

La friction de la gestion de la construction et des packages n'est pas la seule raison pour laquelle C++ est beaucoup moins convivial pour les débutants que Java. Un programmeur peut avoir des difficultés à déboguer et à utiliser C++ en toute sécurité s'il n'est pas familiarisé avec le C, les langages d'assemblage ou le fonctionnement de niveau inférieur d'un ordinateur. Considérez C++ comme un outil puissant : il peut accomplir beaucoup de choses, mais il est dangereux s'il est mal utilisé.

L'approche de gestion de la mémoire susmentionnée de Java le rend également beaucoup plus accessible que C++. Les programmeurs Java n'ont pas à se soucier de libérer de la mémoire objet puisque le langage s'en charge automatiquement.

Temps de décision : C++ ou Java ?

Un organigramme avec une bulle "Démarrer" bleu foncé dans le coin supérieur gauche qui se connecte finalement à l'une des sept cases de conclusion bleu clair en dessous, via une série de jonctions de décision blanches avec des branches bleu foncé pour "Oui" et d'autres options, et des branches bleu clair pour "Non". Le premier est "Application graphique multiplateforme ?" à partir de laquelle un "Oui" pointe vers la conclusion, "Choisissez un environnement de développement multiplateforme et utilisez son langage principal." Un "Non" indique "Application Android native ?" à partir de laquelle un "Oui" pointe vers une question secondaire, "Est-ce un jeu ?" À partir de la question secondaire, un "Non" pointe vers la conclusion : "Utilisez Java (ou Kotlin)" et un "Oui" pointe vers une conclusion différente : "Choisissez un moteur de jeu multiplateforme et utilisez son langage recommandé". À partir de "l'application Android native ?" question, un "Non" indique "Application Windows native ?" à partir de laquelle un "Oui" pointe vers une question secondaire, "Est-ce un jeu ?" De la question secondaire, un "Oui" pointe vers la conclusion, "Choisissez un moteur de jeu multiplateforme et utilisez son langage recommandé", et un "Non" pointe vers une conclusion différente, "Choisissez un environnement d'interface graphique Windows et utilisez son principal langage (généralement C++ ou C#)." À partir de "l'application Windows native ?" question, un "Non" pointe vers "Application serveur ?" à partir de laquelle un "Oui" pointe vers une question secondaire, "Type de développeur ?" À partir de la question secondaire, une décision "Moyen" pointe vers la conclusion, "Utiliser Java (ou C# ou TypeScript)", et une décision "Qualifié" pointe vers une question tertiaire, "Priorité absolue ?" À partir de la question tertiaire, une décision "Productivité du développeur" pointe vers la conclusion, "Utiliser Java (ou C# ou TypeScript)", et une décision "Performance" pointe vers une conclusion différente, "Utiliser C++ (ou Rust)." Depuis l'application "Serveur ?" question, un « Non » pointe vers une question secondaire, « Développement du pilote ? » À partir de la question secondaire, un "Oui" pointe vers une conclusion, "Utiliser C++ (ou Rust)", et un "Non" pointe vers une question tertiaire, "Développement IoT ?" À partir de la question tertiaire, "Oui" pointe vers la conclusion, "Utiliser C++ (ou Rust)", et un "Non" pointe vers une question quaternaire, "Trading à grande vitesse ?" À partir de la question quaternaire, un "Oui" pointe vers la conclusion, "Utilisez C++ (ou Rust)", et un "Non" pointe vers la dernière conclusion restante, "Demandez à quelqu'un qui connaît votre domaine cible".
Un guide étendu pour choisir la meilleure langue pour différents types de projets.

Maintenant que nous avons exploré en profondeur les différences entre C++ et Java, revenons à notre question initiale : C++ ou Java ? Même avec une compréhension approfondie des deux langues, il n'y a pas de réponse unique.

Les ingénieurs logiciels peu familiarisés avec les concepts de programmation de bas niveau feraient peut-être mieux de sélectionner Java lorsqu'ils restreignent la décision à C++ ou à Java, sauf pour les contextes en temps réel comme les jeux. Les développeurs qui cherchent à élargir leurs horizons, d'autre part, pourraient en apprendre davantage en choisissant C++.

Cependant, les différences techniques entre C++ et Java peuvent n'être qu'un petit facteur dans la décision. Certains types de produits nécessitent des choix particuliers. Si vous n'êtes toujours pas sûr, vous pouvez consulter l'organigramme, mais gardez à l'esprit qu'il peut éventuellement vous orienter vers une troisième langue.