Académique Documents
Professionnel Documents
Culture Documents
L3Maths : 2019-2020
B. ONDAMI
Email : bondami@gmail.com
return j ;
}
En C++, on a donc le choix de déclarer toutes les variables locales au début du bloc, ou un
peu avant leur première utilisation. Cependant, c’est dans le cas des fonctions longues que
cette seconde solution trouve son intérêt.
int main()
{
int i =2;
int j =3;
produit(i,j);
float a=2.;
float b=3.;
produit(a,b);
return 0 ;
Ces deux fonctions portent le même nom, et le compilateur les acceptera toutes les deux. Lors
de l'appel de produit (2,3), ce sera la première qui sera appelée, car 2 et 3 sont des entiers.
Lors de l'appel de produit(2.,3.), ce sera la deuxième, parce que 2. et 3. sont réels.
Attention ! Dans un appel tel que produit(2.5,3), le flottant 2.5 sera converti en entier et la
première fonction sera appelée. Il convient donc de faire très attention aux mécanismes de
surcharge du langage, et de vérifier les règles de priorité utilisées par le compilateur.
2-1-1 Encapsulation
L’encapsulation permet de lier le code et les données que celui-ci traite. Elle préserve ces
éléments d’une intervention extérieure ou d’un mauvais usage. Dans un langage de POO on
peut allier le code et les données de manière à créer une « boite noire » autonome. Lorsqu’on
établit un tel lien, on crée un objet. Ce dernier est donc un moyen de prendre en charge
l’encapsulation.
Dans un objet, les membres peuvent être privés ou publics, qu’il s’agisse du code, d’une
donnée ou des deux. Dans le premier cas, ils ne sont connus et accessibles qu’a l’intérieur de
l’instance. En conséquence, un programme extérieur ne peut pas atteindre ces éléments
privés. En revanche, il peut accéder aux membres publics définis dans un objet. Celui-ci sert
habituellement à fournir une interface contrôlée aux éléments privés.
Un objet est à toute fins pratiques une variable, dont le type est défini par l’utilisateur, ce qui
peut paraître étrange pour une instance liant du code et des données. En POO, le cas se
présente pourtant. Lorsque vous définissez une nouvelle catégorie d’objets, vous créer un
type de données. Chaque instance associée à ce dernier constitue une variable composée.
2-1-2 Polymorphisme
Les langages de programmation orientée objet prennent en charge le polymorphisme. Cela se
caractérise par « une interface, plusieurs méthodes ». En termes simples, le polymorphisme est
l’attribut qui permet à une interface de gérer l’accès à une catégorie générale d’actions. Pour
une opération spécifique, le choix est déterminé par la nature exacte de la situation.
Les premiers langages de POO étaient des langages interprétés. En conséquence, le
polymorphisme était pris en charge lors de l’exécution.
2-1-3 Héritage
L’héritage est le processus par le quel un objet peut acquérir les propriétés d’un autre.
class Voiture {
public:
int nombreDeChevaux ;
void demarrer() ;
};
Dans cet exemple, on définit la variable poidsBrut de type entier non signé ainsi que l’objet
Rav4 de la classe (ou de type) Voiture.
Ainsi Rav4 est une instance de la classe Voiture.
Le compilateur détecte l’erreur car il est impossible d’affecter 2 au type entier. En revanche,
vous pouvez définir une variable de type entier et lui affecter la valeur 2. Exemple,
int x ; // x est un entier
x=2 ; // x est égal à 2
Le compilateur génère une erreur car il est impossible d’affecter la valeur 70 au membre
nombreDeChevaux de la classe Voiture. Pour cela vous devez créer un objet Voiture, puis lui
attribuer cette valeur. Exemple :
Voiture Toyota_corrola; //instanciation : on définit une instance (i.e un
objet) de la classe Voiture
Toyota_corrola.nombreDeChevaux = 70; //affectation
class A{
public :
int attribut_public ;
void methode_public(){} ;
private :
int attribut_private ;
void methode_private(){} ;
} ;
int main()
{
A a ; // déclaration d’un ojet (une instance) a de la classe A
a.attribut_public =0 ; // ok
a.methode_public () ; // ok
a. attribut_private =0; // erreur
a.methode_private() ; // erreur
return 0;
}
Ainsi un attribut public pourra être modifié par tous et l'objet ne pourra finalement pas
contrôler les modifications effectuées sur son propre attribut. Pour éviter ce manque de
contrôle, il est préférable de toujours protéger les attributs (avec le mode d’accès private) et de
mettre en place une méthode qui permette de récupérer la valeur d'un attribut et une
méthode permettant de modifier la valeur d'un attribut (si l'on veut pouvoir modifier cet
attribut, bien sûr). C’est ce que nous allons voir dans l’exemple dans le paragraphe suivant.
void B::setAttribut1(int v) {
attribut1 = v;
}
double B::getAttribut2() {
return attribut2;
}
void B::setAttribut2(double v) {
attribut2 = v;
}
Il est également possible de définir une variable, que vous initialisez aussitôt.
Exemple: int poids =7;//déclaration et initialisation de la variable poids
L’initialisation consiste à définir une variable et à lui affecter une valeur. Vous pouvez modifier
cette valeur ultérieurement, car l’initialisation n’est pas une opération irréversible et définitive.
Comment initialiser les données membres d’une classe ?
Toute classe comprend une fonction membre spéciale appelée constructeur. Ce dernier peut
prendre des paramètres, mais ne renvoie pas de valeurs (même void). En fait le constructeur
est une méthode qui porte le même nom que la classe dans laquelle il réside.
Lorsque vous créer un constructeur, il est souhaitable de déclarer aussi le destructeur associé.
Alors que les constructeurs créent et initialisent des objets dans la classe, les destructeurs
libèrent la mémoire allouée aux objets, puis les suppriment. Un destructeur porte le nom de la
classe précédé d’un tilde (~). Il ne prend pas d’arguments en charge et ne récupère pas de
valeur de renvoi.
Class nom_de_la_classe{
données et fonction privées
spécificateur_accès :
données et fonctions
// …………………………
spécificateur_accès :
données et fonctions
} ensemble_objets
Dans ce cas, ensemble_objets est facultatif. Il permet de déclarer des objets de la classe. Par
ailleurs, spécificateur_accès est l’un des trois clé suivant de C++ :
public (pour des données et fonctions publics),
private (pour des données et fonctions privées),
protected (pour des données et fonctions protégées).
Par defaut, les fonctions et les données d’une classe sont privées: Seuls les autres membres de
la classe peuvent donc les atteindre. Pour rendre les fonctions ou les données accessibles à
d’autres parties du programme, définissez-les avec le spécificateur public. Quant à
protected, il se révèle nécessaire, seulement en cas d’héritage (on le verra au prochain
chapitre).
A noter que vous êtes autorisez à changer les caractéristiques d’accès aussi souvent que vous
le souhaitez. Ainsi, vous pouvez parfois passer à public, puis à private, de nouveau.
class douanier {
char name[80] ; // private par défaut
public :
void setname(char *pnom) ; // public
void getname(char *pn) ; // public
private :
double age ; // private maintenant
public :
void setage(double dage) ; // public de nouveau
double getage() ; // public
} ;
double douanier::getage()
{
return age ;
}
int main()
{
douanier d;
char nom[80] ;
d.setname("Paul TATA") ;
d.setage(40) ;
d.getname(nom) ;
cout << nom <<" a " << d.getage() <<" ans."<< endl;
return 0;
}
Notez que du point de vue des programmeurs, il est plus aisé de disposer en réalité d’une
seule section par catégorie de spécificateur d’accès dans chaque classe.
Lorsqu’une fonction est déclarée dans une classe, elle est qualifiée de « membre ». Dans ce
cas elle peut accéder aux autres éléments de sa classe. Les variables d’une classe sont
appelées par données membres. Les données et fonctions membres peuvent tous être
désignés par « membre ».
Notez aussi qu’une structure (définie dans le module 1) offre un autre moyen de spécifier une
classe. Par défaut, tous ses membres sont publics et non privés.
#include <iostream>
using namespace std ;
class myclass{
int a, b ;
public :
friend int sum(myclass x); //sum est une fonction extérieure,
amie de la classe
void set_ab(int i, int j) ;
} ;
int main()
{
myclass y ;
y.set_ab(3,4) ;
cout << "a+b ="<<sum(y) << endl ;
return 0;
}
#include <iostream>
using namespace std ;
int main()
{
cout << max(10,20)<<endl;
cout << "" << max(99,88)<<endl ;
return 0;
}
La très grande efficacité du code crée par les fonctions en ligne justifie l’importance de leur
incorporation dans C++. L’intérêt est essentiel, car les classes nécessitent habituellement
l’exécution fréquente de plusieurs fonctions d’interfaces (les quelles donnent accès aux
données privées). Comme vous le savez probablement, le mécanisme d’appel et de retour
mobilise chaque fois une quantité considérable de temps système. Lorsque vous développer
en revanche le code d’une fonction en ligne, aucun délai ne se produit.
Notez qu’à l’instar du spécificateur register, inline permet en réalité de faire une
suggestion et non donner un ordre au compilateur. Ce dernier peut donc choisir de l’ignorer.
inline peut s’appliquer à une fonction membre d’une classe comme le montre l’exemple
suivant :
class myclass{
int a, b ;
public :
void init(int i, int j) ;
void show() ;
} ;
int main()
{
myclass x ;
x.init(10,20) ;
x.show() ;
return 0 ;
}
Enfin notez qu’il est possible de définir entièrement une petite fonction au moment de la
déclaration d’une classe. Automatiquement, elle devient alors, une fonction en ligne (sauf cas
d’impossibilité). Il n’est ni nécessaire, ni erroné de faire précéder sa déclaration du mot clé
inline, comme le montre l’exemple suivant :
class myclass{
int a, b ;
public :
// Fonctions automatiquement en ligne
void init(int i, int j) { a = i ; b = j ;}
void show() { cout <<"a="<<a <<" b="<<b<< endl ;}
} ;
int main()
{
myclass x ;
x.init(10,20) ;
x.show() ;
return 0 ;
}
int main()
{
myclass ob(3,5); //initialisation de a à 3 et de b à 5.
ob.show() ;
return 0 ;
int main()
{
X ob = 98 ; // Transmet 98 à j
cout << ob.geta()<<endl; // affiche 98
return 0 ;
}
class myclass
{
static int i; //Déclaration dans la classe.
...
};
La variable ::i sera partagée par tous les objets de classe test, et sa valeur initiale est 3.
int main()
{
myclass objet1, objet2;
cout << objet1.getcompte () << endl ; // affiche 0
cout << objet2.getcompte () << endl ; // affiche 1
return 0 ;
}
affiche 0 et 1 parce que la variable statique compte est la même pour les deux objets.
#include <iostream>
class Entier
{
static int i;
public:
static int get_value(void);
};
int Entier::i=3;
int Entier::get_value(void)
{
return i;
}
int main(void)
{
// Appelle la fonction statique get_value :
int resultat = Entier::get_value();
cout << "resultat="<<resultat<< endl ;
return 0;
}
class Point {
void placer(int a, int b); // peut modifier l’objet
float distance(Point p) const; // ne peut pas modifier l’objet.
};
#include <iostream>
using namespace std ;
class Entier
{
static int i;
public:
static int get_value(void);
};
int Entier::i=3;
int Entier::get_value(void)
{
return i;
}
class Point {
void placer(int a, int b); // peut modifier l’objet
float distance(Point p) const; // ne peut pas modifier l’objet.
};
class myclass{
public :
int who ;
myclass(int id) ; //constructeur avec un seul paramètre ;
~myclass() ; //destructeur
myclass ::~myclass()
{
cout << "Destructeur" << who <<endl ;
}
int main()
{
myclass local_ob1(3) ;
cout << "On est apres declaration de local_ob1(3)" << endl ;
myclass local_ob2(4);
return 0 ;
}
class myclass {
int i ;
public :
void set_i(int n) {i=n ;}
int get_i (){return i ;}
} ;
myclass f() // retourne un objet
{
myclass x ;
x.set_i(1) ;
return x ;
}
int main()
{
myclass o ;
o = f() ;
cout<< "i="<<o.get_i()<<endl ;
return 0 ;
}
class myclass {
int i ;
public :
void set_i(int n) {i=n ;}
int get_i (){return i ;}
} ;
int main()
{
myclass ob1, ob2 ;
ob1.set_i(78) ;
ob2 = ob1 ; // affecte les données d’ob1 à ob2
cout << "ob2.i=" << ob2.get_i() <<endl ;
return 0 ;
}
class cl {
int a ;
public :
void set_a(int i) { a = i ;}
int get_a() {return a ;}
} ;
int main()
{
cl ob[3] ;
return 0 ;
}
Si une classe possède un constructeur paramétré, vous pouvez définir les divers objets du
tableau en fournissant une liste de paramètres d’initialisation, de la même façon que pour tout
autre type de tableau. Dans ce cas particulier, le format exact de la liste d’initialisation
dépendra du nombre de paramètres attendus par le constructeur des objets contenus dans le
tableau.
Pour ceux dont le constructeur ne requiert qu’un seul paramètre, vous pouvez utiliser le
format normal, en spécifiant successivement les valeurs d’initialisation. Lors de la création de
chacun des éléments du tableau, une valeur est extraite de la liste, et passé en paramètre au
constructeur de l’objet.
Le programme suivant est une version légèrement différente de l’exemple précédent, utilisant
une liste de paramètres d’initialisation :
#include <iostream>
using namespace std ;
class cl {
int a ;
public :
cl(int i) { a = i ;} // constructeur
int get_a(){ return a ;}
} ;
int main()
{
cl ob[3] ={1,2,3} ;
return 0 ;
}
En réalité la syntaxe employée ci-dessus est un abrégé de la forme suivante, plus longue :
cl ob[3] ={ cl(1), cl(2), cl(3) };
Pour les objets dont le constructeur compte deux paramètres ou plus, le format plus long,
sera requis.
Par ailleurs il convient de noter que, si le constructeur contient un paramètre, tout tableau
contenant ce type d’éléments doit être initialisé par le biais du constructeur. Par exemple
l’instruction suivante est fausse :
cl a[9] ; // erreur car le constructeur requiert un paramètre.
class cl {
int a ;
public :
cl(int i) { a = i ;}
int get_a() {return a ;}
} ;
int main()
{
cl ob(88), *p ;
p= &ob ; // récupération de l’adresse de ob
cout <<"p->a="<< p->get_a() <<endl ; // utilisation de l’opérateur -
>, pour l’appel de get_a().
return 0 ;
}
Comme nous l’avions vu dans le module1, une fois incrémenté, un pointeur désigne le
prochain élément du même type. De manière générale, les opérations sur les pointeurs sont
relatives au type de base du pointeur, c’est –à-dire au type de données que le pointeur
désigne. Ces règles sont par ailleurs valables pour les pointeurs sur objet, comme le montre
l’exemple suivant :
#include <iostream>
using namespace std ;
class cl {
int a ;
public :
cl(int i){a = i ;}
int get_a() {return a ;}
} ;
int main()
{
cl ob[3] = {1, 2, 3} ; // cl est la classe précédente
cl *p ;
p = ob ; // on initialise à l’adresse du premier élément du tableau
ob.
for(int i=0 ; i<3 ; i++){
cout<<"b["<<i<<"]="<<p->get_a()<<endl ;
p++ ; // pointeur sur le prochain objet
}
return 0 ;
}
class cl {
public :
int a ;
cl(int i){a = i ;}
int get_a() {return a ;}
} ;
int main()
{
cl ob(1) ;
int *p ;
p = &ob.a ; // on affecte à p l’adresse de ob.a
cout << "*p="<<*p <<endl; // on accède à ob.a via le pointeur p
return 0 ;
Note : Il est essentiel de retenir qu’en C++, vous pouvez assigner la valeur d’un pointeur à un
autre pointeur, uniquement si les deux sont compatibles, c’est-à-dire si les deux pointeurs
sont définis pour désigner le même type de donnée.
Considérons par exemple :
int *pi ;
float *pf ;
L’affectation suivante est impossible en C++ :
pi = pf ; // erreur : incompatibilité des types
Il est bien entendu possible de transtyper (cast) les variables pour échapper à ces problèmes
d’incompatibilité.
class A {
int a ;
public :
A(int i){ a = i ;} // pas d’utilisation explicite de this
} ;
class A {
int a ;
public :
Note : Les fonctions amies ne sont membre d’une classe, et ne reçoivent par conséquent pas
le pointeur this en paramètre. Les méthodes static (méthode de classe) ne peuvent pas
utiliser le pointeur this.
4-4 Références
Le langage C++ à la particularité de proposer un type proche des pointeurs, appelé
référence.
Il existe trois façons d’utiliser une référence :
- En tant paramètre d’une fonction
- En tant que type de retour d’une fonction
- En tant référence autonome.
int main()
{
int x ;
x=10 ;
cout <<"avant appel de neg(x), x="<< x << endl ;
neg(x) ; // plus besoin de l’opérateur &
cout <<"apres appel de neg(x), x="<< x << endl ;
return 0 ;
}
class A {
int id ;
public :
int i ;
A(int num) ; // constructeur avec un paramètre
~A() ; // destructeur
void neg( A &o) { o.i = -o.i ; } // aucune copie générée
} ;
A ::A(int num)
{
cout << "On passe dans le constructeur."<< endl ;
id = num ;
}
A ::~A()
{
cout << "On passe dans le destructeur."<< endl ;
}
int main()
{
A o(1) ;
o.i = 10 ;
o.neg(o) ;
return 0 ;
}
int main()
{
replace(5) = 'X' ; //remplace par X l’espace suivant Hello
cout <<"la chaine de caracteres devient: "<< s <<endl;;
return 0 ;
char &replace(int i)
{
return s[i] ;
}
int b = 20 ;
ref = b ; // a prend donc la valeur de b
return 0 ;
}
int main()
{
int *p ;
try {
p = new int ; // allocation de la zone mémoire utile pour un
entier
} catch (bad_alloc xa) {
cout << "Echec de l’allocation mémoire" << endl ;
return 1 ;
}
*p = 100 ;
cout << " a l'adresse "<< p << endl;
cout << "il y a la valeur " << *p << endl ;
delete p ;
return 0 ;
}
Il faut bien entendu que l’argument d’initialisation de type soit compatible avec le type de
donnée pour lequel la zone mémoire a été allouée. Vous pouvez remplacer dans le
programme précédent, la ligne d’allocation dynamique par la ligne suivante :
p = new int(87); // initialisation à 87
int main()
{
int *p;
try {
p = new int [10]; // allocation d’un tableau de 10 entiers
}catch (bad_alloc xa) {
cout << "Echec de l’allocation mémoire" << endl ;
return 1 ;
}
for(int i =0 ; i<10 ; i++)
p[i] = i ;
Notez qu’il ne peut pas y avoir d’argument d’initialisation lors de l’allocation dynamique de
tableaux.
class balance{
double cur_bal ;
char name[80] ;
public :
void set(double n, char *s){
cur_bal = n ;
strcpy(name, s) ; }
void get_bal(double &n, char *s){
n = cur_bal ;
strcpy(s,name) ; }
} ;
int main()
{
balance *p ;
char s[80] ;
double n ;
try {
p = new balance ;
}catch (bad_alloc xa) {
cout << "Echec d’allocation " << endl ;
return 1 ;
}
p->set(12387.87, "Papa") ;
p->get_bal(n,s) ;
delete p ;
return 0 ;
}
Le programme suivant crée une classe de tableau. Dans la mesure où l’espace pour le tableau
est alloué avec new, un constructeur par copie est prévu pour allouer la mémoire quand un
objet tableau sert à en initialiser un autre.
#include <iostream>
#include <new>
#include <cstdlib>
using namespace std ;
class tableau {
int *p ;
int taille ;
public :
tableau(int t) {
try {
p = new int[t] ;
}catch (bad_alloc xa){
cout << "Echec allocation" << endl ;
exit(EXIT_FAILURE) ;
}
taille = t ;
}
~tableau(){ delete [ ] p ;}
}
} ;
// constructeur par copie
tableau::tableau(const tableau &a){
try {
p = new int[a.taille] ;
}catch(bad_alloc xa){
cout << "Echec allocation" << endl ;
exit(EXIT_FAILURE) ;
}
for(int i=0 ; i<a.taille ; i++) p[i] = a.p[i] ;
}
int main()
{
tableau num(10) ;
int i ;
for(i=0 ; i<10 ; i++) num.set(i,i) ;
for(i=9 ;i >=0 ; i--) cout << num.get(i)<<endl ;
cout << endl ;
return 0 ;
}
}
Cette fonction peur alors être invoquée de deux manières différentes, comme le montre les
exemples suivants :
mafonc (198.25) ; // passe une valeur explicitement
mafonc () ; //laisse la fonction utiliser la valeur par défaut
L’exemple ci-dessous utilise une fonction ayant des arguments par défaut :
#include <iostream>
using namespace std ;
class Cube {
int x, y, z ;
public :
Cube(int i=0, int j =0, int k=0) {
x=i ;
y=j ;
z=k ;
}
int volume(){
return x*y*z ;
}
} ;
int main()
{
Cube a(2,3,4), b ;
cout << "volume pour a="<<a.volume() << endl ;
cout << "volume pour b="<<b.volume() << endl ;
return 0 ;
}
int main()
{
Derived ob(3) ;
ob.set(1, 2) ; // accès possible aux membres de la classe de base
ob.show() ; // accès possible aux membres de la classe de base
class Base {
int i, j ;
public :
void set(int a, int b) { i = a, j=b ;}
void show(){ cout <<"i="<< i << " "<<"j="<<j << endl; }
} ;
int main()
{
Derived ob(3) ;
ob.set(1, 2) ; // erreur accès refusé
ob.show() ; // erreur
return 0 ;
}
#include <iostream>
using namespace std ;
class Base {
protected :
int i, j ; // privés pour la classe mère, mais accessible par
les classes dérivées.
public :
void set(int a, int b) { i = a, j=b ;}
void show(){ cout <<"i="<<i << " " <<"j="<<j<< endl ;}
} ;
} ;
int main()
{
Derived ob ;
ob.set(2, 3) ; // Ok, connu par les classe derivée
ob.show() ; // Ok
ob.setk() ;
ob.showk() ;
return 0 ;
}
#include <iostream>
using namespace std ;
class Base {
protected :
int i, j ; // restent accessible pour les classes dérivées.
public :
void setij(int a, int b) { i = a, j=b ;}
void showij(){ cout << i << " " << j << endl ;}
} ;
// Dérivation de la classe en mode protected
class Derived : protected Base {
int k ;
public :
// la classe dérivée peut accéder aux membres hérités
void setk(){ setij(10,12) ; k = i*j ;}
void showall(){ cout << k << " " << endl ; showij() ;}
} ;
int main()
{
Derived ob ;
ob.setij(2, 3) ; // Impossible setij est un membre protégé
ob.setk() ; // ok
ob.showall() ; // ok
ob.showij() ; // impossible
return 0 ;
}
class Base {
public :
virtual void vfunc() { cout << "on est dans vfunc de Base." <<
endl;}
} ;
} ;
} ;
int main()
{
Base *p, b;
Derived1 d1 ;
Derived2 d2 ;
p = &b ;
p-> vfunc() ; // accès à vfunc de Base
p = &d1 ;
p-> vfunc() ; // accès à vfunc de Derived1
p= &d2 ;
p-> vfunc() ; // accès à vfunc de Derived2
Note : Tout pointeur sur une classe mère peut être utilisé pour pointeur sur un objet d’une
classe dérivée de celle-ci. Quand un pointeur de base pointe sur une classe dérivée qui
contient une fonction virtuelle, C++ détermine quelle version de la fonction doit être
invoquée en se fondant sur le type de l’objet pointé. Ce choix intervient lors de l’exécution.
Ainsi, quand des objets différents sont pointés, des versions distinctes de la fonction sont
exécutées. C’est le polymorphisme d’exécution.
A première vue, la redéfinition d’une fonction virtuelle dans une classe dérivée peut paraître
similaire à la surcharge de fonction. Cependant, ce n’est pas le cas et le terme surcharge ne
s’applique pas aux fonctions virtuelles, car il existe quelques différences. La plus significative
est probablement que le prototype d’une fonction virtuelle redéfinie doit correspondre
exactement au prototype spécifié dans la classe mère.
7-2 Appel d’une fonction virtuelle par une référence sur une classe mère
Dans l’exemple précédent, une fonction virtuelle était invoquée par l’intermédiaire d’un
pointeur sur classe mère ; mais la nature polymorphe d’une fonction virtuelle est également
accessible par un appel par le biais d’une référence sur classe mère comme le montre
l’exemple suivant (on utilise les classes du paragraphe précédent):
return 0 ;
}
class Nombre{
protected:
int val;
public:
void setval(int i){val =i;}
// affiche() est une fonction virtuelle pure
virtual void affiche() = 0;
};
};
};
int main()
{
Nombre1 N1;;
Nombre2 N2;
Nombre3 N3;
N1.setval(20);
N1.affiche();
N2.setval(22);
N2.affiche();
N3.setval(23);
N3.affiche();