Je vous ai jusqu'à maintenant présenté les bases du C++, nous allons pouvoir à présent nous intéresser à des domaines plus précis tels que l'héritage multiple, les fonctions virtuelles et les classes abstraites cette fois-ci. En fin d'article nous verrons les formes d'héritage non publiques, rarement utilisées mais disponibles, protected et private.
L'héritage nous permet d'étendre une classe A en lui ajoutant des fonctionnalités supplémentaires pour créer une classe F. C'est bien mais admettons que les fonctionnalités existent déjà dans un autre classe existante, appelons la B. Comment choisir de quelle classe la classe F doit elle hériter pour avoir les qualités des deux ? Afin de ne pas avoir à se poser ce genre de questions, le C++ propose d'utiliser l'héritage multiple, qui permet à F d'hériter de A et de B. Cela se note de la façon suivante :
/* ex_5_1.c++ */ #include <iostream> using namespace std; class A { protected: int a; public: A() { cout << "A : " << a << endl; }; void fait_a() { cout << "a" << endl; } }; class B { protected: int b; public: B() { cout << "B : " << b << endl; }; void fait_b() { cout << "b" << endl; } }; class F : public A, public B { public: F(); }; F::F() { cout << "F : " << a << " " << b << endl; } int main (void) { F f; f.fait_a(); f.fait_b(); return 0; }
On compile et on exécute :
$ g++ -o ex_5_1 ex_5_1.c++ [xavier@zaz2 5]$ ./ex_5_1 A : 134518812 B : 134519356 F : 134518812 134519356 a b
Pas de grosse surprise dans le code si vous avez suivi jusqu'ici ... La classe F
est dérivée de A
et B
. Il s'agit d'héritage public comme le montre le mot clé public
placé avant le nom des classes parentes (voir fin d'article).
Cet exemple montre la création des instances de A
et B
et la récupération de leurs membres protégés pour l'instanciation de F
. On voit également l'utilisation des méthodes publiques fait_a
et fait_b
de a
et b
par l'intermédiaire de l'objet f
. L'héritage multiple, c'est aussi simple que celà.
Dans la pratique, l'héritage multiple peut servir, comme ici, à créer de nouvelles classes en en combinant plusieurs, on peut d'ailleurs en combiner plus de deux soit dit en passant, mais la vraie puissance de l'héritage multiple est libérée lorsqu'on la combine à la notion de fonction virtuelle, ce que nous allons voir tout de suite.
Reprenons nos personnes, travailleurs et directeurs qui nous suivent depuis quelques articles (Je rappelle pour ceux qui n'ont pas suivi le début de la série C++ que les sources des exemples se trouvent sur mon site, dont l'url est donnée en fin d'article). On va placer 5 personnes, 4 travailleurs et une directrice dans un bus et leur demander de se présenter. Nous allons pour celà remplir une pile (stack
, classe de la STL vue lors du premier article de la série), puis la vider. Lors de chaque "descente" du bus, la personne se présente :
/* ex_5_2.c++ */ #include <iostream> #include <cstdlib> #include <stack> #include <personne.h> #include <directeur.h> #include <travailleur.h> using namespace std; int main (void) { srand(time(NULL)); stack<personne*> bus; int place; // Montée dans le bus for (place=0; place<5; place++) { bus.push(new personne("X1", "Y1", 27)); } for (place=0; place<4; place++) { bus.push(new travailleur("X2", "Y2", 27, SURPRISE, "Boulot quelconque")); } bus.push(new directeur("Foobar", "Toto", 45, FEMME)); // Descente for (place=0; place<MAX_PLACES; place++) { personne* p = bus.top(); cout << p->get_identification() << endl; bus.pop(); // delete p; } return 0; }
On retrouve l'initialisation du générateur de nombres aléatoires, la déclaration du bus, qui est une pile de personnes (une pile de pointeurs sur des objets de type personne
pour être exact). Viennent ensuite les "montées" des personnes, des travailleurs puis de la directrice et la descente de tout ce petit monde.
La ligne delete p;
a été commentée pour améliorer la lisibilité de la sortie du programme. Oublier un delete
n'est pas une faute de programmation car le ménage est fait à la fin du programme... Mais il est tout de même recommandé de mettre ceux auquels vous pensez. C'est quoi qu'il en soit une bonne habitude à prendre de libérer ce que l'on réserve lorsqu'on ne s'en sert plus. Cela sert lors du développement de modules, de composants du kernel, d'applications temps réel ou d'applications serveurs dont la durée d'exécution est très longue et qu'on ne peut se contenter du nettoyage automatique.
Pour compiler cet exemple, le répertoire doit contenir les fichiers personne.c++, personne.h, travailleur.c++, travailleur.h, directeur.c++, directeur.h et Makefile. Le Makefile en question est le suivant, il a été commenté au cours de l'article paru dans linuxmag n°47.
CXXFLAGS=-I. OBJS=directeur.o travailleur.o personne.o ex_5_2.o all : ex_5_2 clean_and_all: clean all ex_5_2: $(OBJS) @echo "Liaison de" $(OBJS) "en" $@ $(CXX) $(CXXFLAGS) -o $@ $(OBJS) @echo "Fini, OK" @echo clean: @echo -n "Nettoyage en cours..." @-$(RM) $(OBJS) ex_5_2 @echo @echo "C'est propre" @echo %.o: %.c++ @echo "Compilation de" $< $(CXX) $(CXXFLAGS) -c $<
L'exécutable s'obtient traditionnellement en tapant make
et l'exécution par ./ex_5_2
:
$ ./ex_5_2 Création d'une femme : X1 Y1 * 27 ans Création d'une femme : X1 Y1 * 27 ans Création d'un homme : X1 Y1 * 27 ans Création d'un homme : X1 Y1 * 27 ans Création d'une femme : X1 Y1 * 27 ans Création d'un homme : Y2 X2 * 26 ans Création d'une femme : Y2 X2 * 26 ans Création d'un homme : Y2 X2 * 26 ans Création d'un homme : Y2 X2 * 26 ans Création d'une femme : Foobar Toto * 45 ans Foobar Toto Y2 X2 Y2 X2 Y2 X2 Y2 X2 X1 Y1 X1 Y1 X1 Y1 X1 Y1 X1 Y1
Que faut-il retenir de cette exécution ? Premièrement, on peut manipuler un travailleur
ou un directeur
comme une personne
, puisqu'ils en sont dérivés.
Deuxièmement, on voit que pour chaque type de personne
, la méthode get_identification
appelée est celle de la classe personne
. C'est logique puisqu'on travaille sur des pointeurs sur des personne
. Il serait toutefois utile de pouvoir utiliser les méthodes get_identification
des classes dérivées puisqu'elles complètent la version de la classe de base.
On obtient ce comportement en déclarant la méthode comme étant virtual
. Ainsi, lors de l'exécution, la méthode appelée est celle de la classe dont est instancié l'objet pointé et non plus celle de la classe "censée" être pointée. Toutefois, si la classe dérivée ne contient pas de surcharge de cette méthode virtuelle, c'est celle de la classe parente qui est utilisée. On remonte ainsi la hiérarchie des classes jusqu'à en trouver une qui fournisse une implémentation de la méthode recherchée.
Concrètement maintenant, dans notre cas, ajoutez virtual
devant les déclarations de get_identification
des classes personne
et travailleur
et éventuellement directeur
puis recompilez en tapant make clean_and_all
(nécessaire car on ne gère pas les dépendances avec les fichiers .h dans le Makefile) et exécutez.
Les modifications pour personne.h
virtual std::string get_identification() const { return (prenom + " " + nom); }
Les modifications pour travailleur.h
virtual std::string get_identification() const { return personne::get_identification()+" : "+metier; }
La sortie de l'exécution devient alors :
$ ./ex_5_2 2 Création d'un homme : X1 Y1 * 27 ans Création d'un homme : X1 Y1 * 27 ans Création d'une femme : X1 Y1 * 27 ans Création d'une femme : X1 Y1 * 27 ans Création d'un homme : X1 Y1 * 27 ans Création d'une femme : Y2 X2 * 26 ans Création d'un homme : Y2 X2 * 26 ans Création d'un homme : Y2 X2 * 26 ans Création d'une femme : Y2 X2 * 26 ans Création d'une femme : Foobar Toto * 45 ans Madame Foobar Toto : Directeur Y2 X2 : Boulot quelconque Y2 X2 : Boulot quelconque Y2 X2 : Boulot quelconque Y2 X2 : Boulot quelconque X1 Y1 X1 Y1 X1 Y1 X1 Y1 X1 Y1
On constate que la méthode get_identification
appelée est celle de la classe de l'objet pointé (personne
, travailleur
ou directeur
) et non plus celle de la classe qui est théoriquement pointée (personne
). C'est ce que l'on appelle le polymorphisme, qui est semblable à la surcharge mais à l'envers ;-).
Plus simplement, si on a une classe personne
et une classe travailleur
dérivée de personne
comme ici, utiliser des membres de personne
à partir d'un pointeur sur travailleur
est normal puisqu'un travailleur
est avant tout une personne
. A l'inverse, utiliser les membres d'un travailleur
à partir d'un pointeur sur personne
demande un peu plus de remue ménage intellectuel et s'appelle le polymorphisme. On dit, quand ça marche, que la classe de base est polymorphique. La limitation du polymorphisme est que pour appeler une méthode de travailleur
, elle a du être définie dans personne
et en plus, être virtuelle. Si la méthode n'est pas virtuelle, on devra au préalable, pour appeler la bonne méthode, "caster" le pointeur, c'est à dire forcer son changement de type. Nous verrons les différents types de "cast" lors d'un article futur sur le RTTI. (Quelqu'un veut une aspirine ?).
Les plus perspicaces d'entres vous vont se dire : "Mais alors, dans le cas présent, le destructeur appelé est celui de la classe personne
?". Question à laquelle je réponds "OUI". C'est pour cette raison que les destructeurs sont le plus souvent virtuels. Dans le cas des classes servant comme classe de base, les destructeurs doivent être ( - il est fortement recommandé qu'ils soient - ) virtuels sinon, vous aurez tôt ou tard des problèmes. Il suffit pour s'en convaincre d'ajouter -Wall
à la variable CXXFLAGS
dans le Makefile, La ligne de définition de CXXFLAGS
devient donc :
CXXFLAGS=-I. -Wall
Suite à celà, taper make clean_and_all
vous affichera de nouveaux Warning (tous, théoriquement. -Wall
signifiant Warnings : all). Il ressembleront (entre autres) à :
personne.h:32: warning: `class personne' has virtual functions but non-virtual destructor
Ces warnings vous préviennent que g++ a détécté une classe qui sera vraisemblablement destinée à être dérivée puisque contenant des fonctions virtuelles mais que bizarrement le destructeur, lui, n'est pas virtuel.
Pour comprendre le concept de classe abstraite, on doit connaître celui de fonction virtuelle pure. Je vais vous exposer ce que sont ces deux choses.
class identifiable { public: virtual string get_identification() = 0; }
Une fonction virtuelle pure est une fonction virtuelle qui n'est pas définie dans la classe où elle est déclarée. Pour éviter que le compilateur ne se plaigne et pour ne pas les confondre avec les fonctions virtuelles "normales", on ajoute comme suffixe à leur déclaration = 0
.
Une classe abstraite est une classe comprenant au moins une fonction virtuelle pure. Elles sont utilisées comme classes de bases uniquement et aident à la construction d'interfaces. Elles peuvent assurer une partie de traitement mais forcent leurs classes filles destinées à être instanciées à définir les fonctions virtuelles. Une classe fille d'une classe abstraite qui n'implémente pas toutes les fonctions virtuelles pures de sa classe parente est elle même une classe abstraite. Enfin, il faut savoir qu'une classe abstraite ne peut pas être instanciée quelque soit le nombre de fonctions virtuelles pures qu'elle contient.
Prenons un exemple illustrant tout ce qui vient d'être dit :
/* ex_5_3.c++ */ #include <iostream> #include <string> using namespace std; /* *** identifiable *** */ class identifiable { string article; protected: void set_genre(const int&); string get_article() const { return article; }; public: virtual string get_identification() const = 0; }; void identifiable::set_genre(const int& genre) { switch (genre) { case 1: article = "Un "; break; case 2: article = "Une "; break; default: article = ""; } } /* *** utile *** */ class utile { public: virtual string get_fonction() const = 0; }; /* *** objet *** */ class objet : public identifiable, public utile { string nom, fonction; public: objet(string n, string f, int genre); string get_identification () const { return (get_article() + nom); }; string get_fonction () const { return fonction; }; }; objet::objet(string n, string f, int genre=0) : nom(n), fonction(f) { set_genre(genre); } /* *** main *** */ int main (void) { // identifiable i; // provoque une erreur à la compilation objet o("balle", "rebondit", 2); string verite = o.get_identification(); verite += " "; verite += o.get_fonction(); verite += "."; cout << verite << endl; return 0; }
On compile et exécute :
$ g++ -o ex_5_3 ex_5_3.c++ $ ./ex_5_3 Une balle rebondit.
J'en entends déjà dire "ça on le savait déjà ...". Certes, on n'apprend rien sur la vie avec ce programme, par contre on peut facilement en examiner la structure. Il comporte deux classes abstraites, identifiable
et utile
, qui vont nous servir à définir des objets identifiables et utiles, comme vous auriez pu vous en douter.
La classe identifiable
contient une propriété privée, l'article ("Un ", "Une " ou rien) qui servira à construire son identification. Cette propriété étant privée, la classe fournit deux méthodes permettant d'en modifier et récupérer le contenu, set_sexe
assigne sa valeur à l'article, "Un " pour un sexe masculin, "Une " pour un sexe féminin et rien pour les autres. Ces méthodes sont protected
afin de garantir leur utilisation uniquement par les classes dérivées (cf. article de février). Enfin, pour permettre la récupération de l'identification des objets identifiables, on impose la définition par les classes dérivées non abstraites de la méthode publique get_identification
. La déclaration de cette fontion virtuelle pure rend impossible la création d'objets de la classe identifiable. Votre compilateur préféré devrait donc vous reporter une erreur si vous tentez de compiler le programme ci-dessus en ayant décommenté la ligne suivante :
identifiable i;
Par exemple, chez moi, g++ (version 2.95.3) m'informe que je ne peux créer un objet i
de type identifiable
car la classe contient des fonctions virtuelles pures. Vous devriez tous obtenir une réponse de ce type, même si vous utilisez un autre compilateur et même avec un autre système d'exploitation. Sinon, pressez vous d'en changer.
$ g++ -o ex_5_3 ex_5_3.c++ ex_5_3.c++: In function `int main()': ex_5_3.c++:54: cannot declare variable `i' to be of type `identifiable' ex_5_3.c++:54: since the following virtual functions are abstract: ex_5_3.c++:13: class string identifiable::get_identification() const
La classe utile
est extrêmement simple puisqu'elle n'est constituée que d'une fonction virtuelle pure publique, get_fonction
. Les classes héritant de utile
devront donc définir une méthode get_fonction
pour être instanciables.
La classe objet
hérite des deux classes précédentes. Pour pouvoir instancier des objets identifiables et utiles, la classe objet
fournit l'implémentation des deux fonctions virtuelles pures get_identification
de la classe identifiable
et get_fonction
de la classe utile
. On retrouve la méthode identifiable::get_article
utilisée pour construire l'identification de l'objet et set_genre
utilisée dans le constructeur de la classe objet
.
Bien, ça marche mais ça semble bien complexe pour finalement pas grand chose. On est passés par trois classes et des fonctions virtuelles pures pour faire un objet ... L'intérêt n'est pas évident.
Admettons maintenant que nous ayons besoin de travailler avec des trucs et des machins en plus de nos objets. Si on souhaite afficher l'identification de chacun, le fait qu'ils héritent tous d'identifiable
rend la procédure immédiate. Ajoutons les classes truc
et machin
à notre programme et modifions le main
pour démontrer ce qui vient d'être dit :
/* ex_5_4.c++ */ [...] #include <queue> [...] /* *** truc *** */ class truc : public identifiable { public: string get_identification () const { return "un truc."; }; }; /* *** machin *** */ class machin : public identifiable { public: string get_identification () const { return "un machin."; }; }; /* *** main *** */ int main (void) { queue<identifiable*> la_queue; int nb; cout << "Combien de choses ?" << endl << "> "; cin >> nb; while(nb--) la_queue.push(new objet("chose", "utile", 2)); cout << "Combien de trucs ?" << endl << "> "; cin >> nb; while(nb--) la_queue.push(new truc()); cout << "Combien de machins ?" << endl << "> "; cin >> nb; while(nb--) la_queue.push(new machin()); while (!la_queue.empty()) { identifiable* ptr = la_queue.front(); cout << ptr->get_identification() << endl; delete ptr; la_queue.pop(); } return 0; }
Les classes truc
et machin
sont extrêmement simples, elles dérivent d'identifiable
mais n'en utilisent même pas les membres set_genre
ou get_article
. Elles se contentent de fournir l'implémentation nécessaire de la fonction viruelle pure get_identification
.
On peut voir celà comme le respect d'une norme. Si on veut se réclamer compatible avec une norme, on doit fournir une implémentation qui en respecte les règles. La construction d'objets par assemblage d'interfaces (ou classes abstraites) est très semblable à ce principe dans le sens où la création des classes objet
, truc
et machin
est un peu plus complexe mais assure qu'elles sont "conformes" à la "norme" identifiable
. On peut ainsi en manipuler les instances comme des objets identifiables et profiter de toutes les facilités de la norme (ici, le traitement lié à l'article par exemple).
Le main
confirme cela en démontrant comment ajouter à une queue de pointeur sur des objets de type identifiable
un nombre quelconque d'objets "respectant" (héritant et implémentant) l'interface identifiable
. Il est constitué de la déclaration de la queue (n'oubliez pas le #include
correspondant en début de programme.
On demande ensuite à l'utilisateur de spécifier le nombre de chaque type d'objet à ajouter à la queue. Les objets sont alors créés et ajoutés.
L'exécution se termine par l'affichage et la suppression des éléments de la queue. Attention : on n'oublie pas le delete
pour libérer la mémoire ! La méthode pop()
detruit le pointeur, pas l'objet pointé.
En C, on pourrait faire ça avec des pointeurs void*, des casts, des structures contenant des pointeurs sur fonctions et une implémentation de queue (ou en utilisant la glib). Qui a dit que le C++ est un langage compliqué ? ;-)
Une compilation / exécution :
$ g++ -o ex_5_4 ex_5_4.c++ $ ./ex_5_4 Combien de choses ? > 3 Combien de trucs ? > 2 Combien de machins ? > 4 Une chose Une chose Une chose un truc. un truc. un machin. un machin. un machin. un machin.
Je vais finir cet article par un paragraphe sur les modes d'héritage non publics, c'est à dire les modes protected
et private
. Ce sont les mêmes mots clés que l'ont utilise pour définir les "droits d'accès" aux membres de classes et nous allons voir que ce n'est pas un hasard.
L'héritage le plus utilisé (celui que nous avons utilisé jusque là) est l'héritage public
. C'est à dire que les membres publics de la classe parente deviennent des membres publics de la classe fille, de même que les membres protégés restent protégés. Ce n'est toute fois pas le cas par défaut. L'héritage par défaut est privé, c'est à dire que tous les membres publics et protégés de la classe de base sont accessible à la classe fille mais deviennent privés dans cette dernière et ne sont donc plus héritables. L'héritage protégé rend les membres publics et protégés de la classe de base protégés dans la classe fille. C'est à dire que les membres publics ne sont plus accessibles en passant par les objets instanciés de la classe fille. Bien sûr, quel que soit le mode d'héritage utilisé les membres privés de la classe de base ne sont jamais hérités.
Pour résumer, voici un tableau expliquant comment évoluent les membres de la classe de base dans la classe dérivée selon le mode d'héritage utilisé :
Classe de base | Classe dérivée | |
---|---|---|
héritage public | public | public |
protected | protected | |
héritage protected | public | protected |
protected | protected | |
héritage private | public | private |
protected | private |
Et pour bien enfoncer le clou, prenons un exemple.
01: /* ex_5_5.c++ */ 02: #include <iostream> 03: using namespace std; 04: 05: class A { 06: int i_priv; 07: protected: 08: int i_prot; 09: public: 10: int i_publ; 11: A(); 12: }; 13: 14: A::A() : i_priv(1), i_prot(2), i_publ(3) { 15: cout << "--------" << endl; 16: cout << "A :" << endl; 17: cout << i_publ << endl; 18: cout << i_prot << endl; 19: cout << i_priv << endl; 20: } 21: 22: 23: class B_priv : A { 24: public: 25: B_priv(); 26: }; 27: 28: B_priv::B_priv() { 29: cout << "B_priv :" << endl; 30: cout << i_publ << endl; 31: cout << i_prot << endl; 32: // cout << i_priv << endl; 33: } 34: 35: class B_prot : protected A { 36: public: 37: B_prot(); 38: }; 39: 40: B_prot::B_prot() { 41: cout << "B_prot :" << endl; 42: cout << i_publ << endl; 43: cout << i_prot << endl; 44: // cout << i_priv << endl; 45: } 46: 47: class B_publ : public A { 48: public: 49: B_publ(); 50: }; 51: 52: B_publ::B_publ() { 53: cout << "B_publ :" << endl; 54: cout << i_publ << endl; 55: cout << i_prot << endl; 56: // cout << i_priv << endl; 57: } 58: 59: class C_prot : public B_prot { 60: public: 61: C_prot(); 62: }; 63: 64: C_prot::C_prot() { 65: cout << "C_prot :" << endl; 66: cout << i_publ << endl; 67: cout << i_prot << endl; 68: // cout << i_priv << endl; 69: } 70: 71: class C_priv : public B_priv { 72: public: 73: C_priv(); 74: }; 75: 76: C_priv::C_priv() { 77: cout << "C_priv :" << endl; 78: // cout << i_publ << endl; 79: // cout << i_prot << endl; 80: // cout << i_priv << endl; 81: } 82: 83: int main() { 84: A a; 85: B_publ b_publ; 86: B_prot b_prot; 87: B_priv b_priv; 88: C_prot c_prot; 89: C_priv c_priv; 90: 91: cout << "--------" << endl; 92: cout << "main :" << endl; 93: cout << a.i_publ << endl; 94: cout << b_publ.i_publ << endl; 95: // cout << b_prot.i_publ << endl; 96: // cout << b_priv.i_publ << endl; 97: return 0; 98: }
Dans ce code, les lignes commentées sont celles qui provoqueraient des erreurs à la compilation.
Nous avons là une classe A
avec 3 membres, i_priv
, private, i_prot
, protected et i_publ
, public. Ces trois propriétés sont accessibles dans A
(lignes 17-19) et la seule la propriété publique est accessible dans le main, par le biais d'une instance de A
.
Viennent ensuite B_priv
, B_prot
et B_publ
, qui toutes trois sont des classes filles de A mais utilisant respectivement les modes d'héritage privé, protégé et public. On voit que les propriétés publiques et protégées sont hérités par les trois modes d'héritages puisqu'elles sont utilisées dans les trois constructeurs (lignes 30-32, 42-44, 54-56). Bien entendu, on voit également que nulle part les propriétés privées de la classe de base ne sont accessibles aux classes filles. Ce qui est plus inhabituel est que la propriété publique n'est accessible qu'au travers de l'instance de la classe B_publ
puisque, comme dit dans le tableau précédent, la propriété i_publ
, publique dans A
et B_publ
est devenue protégée dans B_prot
et privée dans B_priv
.
Enfin, on a la preuve que i_publ
et i_prot
sont bien protégées et non privées dans B_prot
mais privées dans B_priv
en créant C_prot
et C_priv
, classes héritant selon le mode public de B_prot
et B_priv
(lignes 59-81). Une classe C_publ
héritant publiquement de B_publ
n'a pas été créée car elle aurait été identique à B_publ
au niveau des droits d'accès.
La sortie du programme :
$ ./ex_5_5 -------- A : 3 2 1 -------- A : 3 2 1 B_publ : 3 2 -------- A : 3 2 1 B_prot : 3 2 -------- A : 3 2 1 B_priv : 3 2 -------- A : 3 2 1 B_prot : 3 2 C_prot : 3 2 -------- A : 3 2 1 B_priv : 3 2 C_priv : -------- main : 3 3
On arrête temporairement "la prise de tête" pour passer le mois prochain à l'étude pratique des flux et les possibilités fournies par la librairie standard pour les manipuler. Au menu, il y aura au moins les entrées/sorties sur fichier et les flux de/sur chaînes (enfin, stringstream
, traduisez comme il vous plaît)...
Je vous rapelle que les anciens articles sont diponibles dans les vieux numéros de linuxmag pour une version sur papier glacé ou online sur mon site où se trouvent également les codes sources des exemples.
@+
Xavier Garreau - <xavier@xgarreau.org>
http://www.xgarreau.org/
Ingénieur de recherche PRIM'TIME TECHNOLOGY
http://www.prim-time.fr/
Membre fondateur du ROCHELUG
http://www.rochelug.org/
Précédent | Index | Suivant |
a+