De Java à C++

1 - Préambule

Cette page permet de faire la transition vers C++ pour ceux qui ont une bonne connaissance de java. Il s'agit simplement d'une introduction, qui ne dispense pas d'un cours de C++.

2 - Les points communs entre C++ et Java

C++ et Java sont tous deux des langages objet impératifs, supportant "les génériques" (depuis 1.5 pour Java), donc rien de bien nouveau question conception objet, et résolution d'un problème par une approche objet. Comme Java, C++ est compilé, et donc tout comme on écrit des fichiers .java en Java, que le compilateur transforme en exécutables .class, on ecrit des fichiers .cc en C++, que le compilateur transforme en fichiers .o, et c'est un rassemblement des .o correspondant aux classes que l'on a définies qui fournira l'exécutable final. Là aussi, il faudra que dans ce rassemblement, le système puisse trouver une fonction "main" par où démarrer.

3 - Les différences de C++ vis à vis de Java

3.1 - Le pré-compilateur

La compilation d'un fichier C++ passe par deux étapes. La première est une étape de précompilation. Il s'agit de prendre le fichier source C++ (disons toto.cc), et de le trafiquer pour le transformer en un autre fichier source (disons toto'.cc). Les différences entre toto.cc et toto'.cc sont mineures, et sont commandées par des directives de précompilation exprimées dans le fichier toto.cc. Ces directives sont faciles à reconnaitre, ce sont des lignes commençant par un #. En pratique, vous ne verrez jamais le fichier toto'.cc, mais faisons comme si il existait.

Le rôle de ces directives est de "paramètrer" le texte. Par exemple, si on écrit dans toto.cc
#define N_MAX 10
alors toto'.cc sera une version de toto.cc où toutes les apparitions du texte N_MAX de toto.cc sont remplacées par 10 dans toto'.cc.

Une autre directive bien pratique est la directive #include
#include "toto.h"
#include <math.h>
qui permet de recopier le contenu d'un autre fichier (toto.h et math.h ici) à cet endroit là dans toto'.cc. Nous verrons ce que sont les fichiers .h, mais disont ici que la différence entre #include suivie de guillemets où de crochets est minime : dans le second cas, on dit au précompilateur que le fichier à recopier se trouve dans des répertoires système, et non dans le même répertoire que toto.cc.

3.2 - Les fonctions hors classe

En java, toute fonction est méthode d'une classe. En C++ non. En faite, une fonction hors classe est l'équivalent d'une fonction publique et statique (ces notions existent aussi en C++). La fameuse fonction main, par où commence le programme, en est un exemple, et cette fonction est justement public static dans vos codes Java.

3.3 - Les .h et .cc

Le gros des troupes du code C++ que vous ecrivez pour la définition d'une classe Toto se trouve dans toto.cc. Mais imaginons que vous vouliez écrire un programme prog.cc, disons celui qui a la fonction main, dans lequel vous utilisez des instances de Toto... En java, on ne s'inquiète de rien, le compilateur y retrouve ces petits sous réserve que tous les .class sont bien là, et que les import soient bien mis en tête de fichier. En C++, c'est un peu pareil, quoi que la logique est malgré tout différente.

Pour compiler prog.cc, et donc fabriquer prog.o, le compilateur n'a que faire de la façon dont vous avez écrit les méthodes de la classe Toto, il a uniquement besoin de connaitre le type de leurs arguments pour compiler l'appel à ces méthodes... C'est le linker qui, en phase finale de compilation, vérifiera qu'il y a bien une fonction compilée dans Toto.o qui correspond à un appel (bien fait donc) compilé dans prog.o.

Donc pour que prog.cc puisse utiliser Toto, seuls les profiles des méthodes comptent. C'est justement ce que l'on met dans un fichier .h. La définition de la classe Toto se fait donc par l'écriture d'un fichier Toto.h qui déclare les profiles des méthodes, et d'un fichier Toto.cc qui dit comment elles sont programmées. Dans le programme prog.cc, ils suffit d'écrire
#include "toto.h"
	
et le tour est joué.

3.4 - Interlude

A ce stade, je vous recommande de faire le tutoriel sur les makefiles que je vous propose sur ma page. Ne cherchez pas à comprendre le contenu des fichiers, il s'agit juste de voir comment on les compile une fois qu'ils ont été écrits.

3.5 - Gestion de mémoire

En Java, comme dans d'autres langages, on demande de la mémoire explicitement par un "new" puis on laisse le "garbage collector" se charger de vérifier qu'elle n'est plus utilisée et, le cas échéant, de rendre cette mémoire disponible à nouveau. En C++, pas de garbage collector..., il faut donc dire au système qu'on a fini de travailler avec un objet. Attention, si vous ne le faites pas, vos programmes riquent de grossir en taille mémoire au cours de leur exécution. mais quoi qu'il en soit, ca n'empêchera pas le système d'exploitation de récupérer cette place quand votre processus se termine.
Petit rappel. Quand en Java vous ecrivez
... UneFonction(....) {
  Toto a;

  a = new Toto; 
  f(a);
}

void f(Toto b) {
  b.un_champ = 2;
  b.une_methode();
}
le symbole "a" désigne une référence sur un bout de mémoire, bout suffisamment grand pour contenir une instance de la classe Toto. Lors du passage de "a" comme argument de la fonction f, le paramètre formel "b" est une référence... sur le même bout de mémoire, et donc f pourra modifier l'objet qu'on lui passe en argument. En C++, on peut avoir le même comportement, mais le fait que l'on parle de référence apparait de façon visible dans la syntaxe du langage, via de charmantes petites étoiles. Nota : Bien que des puristes diront que non, les termes "référence" et "pointeurs" pourront être considérés comme équivalent dans un premier temps. Ecrivons donc l'équivalent du code Java ci-dessus en C++, en n'oubliant pas de "détruire" l'objet quand on en a plus besoin pour laisser de la place mémoire disponnible. Tout comme un constructeur de nom "Toto" est appelé quand on alloue de la mémoire, un destructeur, à définir dans votre classe Toto, qui se nomme par convention "~Toto", est aussi appelé avant de libérer la mémoire.
... UneFonction(....) {
  Toto* a;

  a = new Toto; 
  f(a);
  delete a;
}

void f(Toto* b) {
  b->un_champ = 2;
  b->une_methode();
}
C'est pas violent comme changement non ? Et le comportement de C++ est dans ce cas le même que celui de Java.
Corsons un peu l'affaire. Supposons qu'on ne mette pas les étoiles (en C++), et qu'on eût écrit le code suivant.
... UneFonction(....) {
  Toto a;

  // a = new Toto;   Ca on ne le met plus....
  f(a);
  // delete a;       ... et ca non plus
}

void f(Toto b) {
  b.un_champ = 2;   // On remet des . au lieu des -> ...
  b.une_methode();  // ... la syntaxe l'exige.
}
Et bien dans ce cas, a n'est plus une référence mais un bout de mémoire de la taille d'un toto, que le compilateur alloue tout seul au moment de la déclaration "Toto a", en appelant le constructeur. Quand on fait "f(a)", le paramètre formel "b" n'est plus "a" lui-même, mais une copie de "a". La fonction f ne travaille donc que sur une copie. Attention, cette copie lors de l'appel d'une fonction peut faire perdre du temps ! Le passage à d'argument à la Java, avec les *, est lui instantané même pour de gros objets puisqu'aucune copie n'est invoquée. Un point à bien comprendre. Une copie est une allocation mémoire, donc un constructeur est appelé pour allouer "b", cet appel est implicite. De même, b est "détruit" en fin de fonction f, tout comme l'est "a" en fin de fonction UneFonction. Ca fait pas mal de magouille de mémoire dans votre dos, et je vous déconseille de trop jouer avec ca au début.
Je vous montre une variante possible pour écrire le code
void f(Toto* b) {
  b->un_champ = 2;
  b->une_methode();
}
que nous avons déjà vu. En effet, le code suivant y est équivalent, c'est-à-dire que l'argument reste passé "à la Java", même si à part un "et commercial", la fonction ressemble à s'y tromper à la celle qui copie l'argument.
void f(Toto& b) {
  b.un_champ = 2;
  b.une_methode();
}
Cette écriture est faite pour passer des objets déclarés sans l'étoile (Toto a) sans toutefois les recopier.
... UneFonction(....) {
  Toto a;

  f(a);
}
void f(Toto& b) {
  b.un_champ = 2;
  b.une_methode();
}
La, la fonction f modifie bien l'objet a. Comme de toute façon cette écriture de f est identique à celle avec l'étoile, on devrait bien pouvoir lui passer un objet déclaré à la Java (Toto* a; a = new Toto...). C'est en effet possible, en modifiant très légèrement la syntaxe de l'appel de fonction.
... UneFonction(....) {
  Toto* a;

  a = new Toto; 
  f(*a); // il faut mettre *a ici
  delete a;
}

void f(Toto& b) {
  b.un_champ = 2;
  b.une_methode();
}

4 - Un exemple

Nous allons réaliser l'exemple bateau suivant, la définition d'une classe Personne (donc Personne.h et Personne.cc), d'une classe fille Artisan, qui hérite de Personne en y ajoutant un métier. On fera tout à la java, c'est à dire qu'on ne travaillera qu'avec des références. Une exception sera faite, juste pour vous montrer, lorqu'on implémentera une fonction qui met dans une liste les attributs de la personne. En effet, pour les collections, qui sont gérées par une librairie appelée STL, la mémoire est bien gérée et on ne risque rien... mais pour vous, encore une fois, collez à la philosophie Java pour la mémoire.
Lisez dans cet ordre Personne.h, Personne.cc, Artisan.h, Artisan.cc et Main.cc.

Pour compiler :
g++ -c Personne.cc
g++ -c Artisan.cc
g++ -c Main.cc
g++ -o exe Personne.o Artisan.o Main.o
./exe
ou plus directement
g++ -o exe Personne.cc Artisan.cc Main.cc
./exe

5 - Après ca ?

Vous pouvez lire un livre de C++, les tutoriaux du Web... mais aussi venir me poser des questions.




Herve.Frezza-Buet