POLYCOPIE DE C++
Rappel sur les pointeurs et références
Ce paragraphe rappelle le strict minimum sur ce que sont un pointeur ou une référence. Pour des détails, se reporter par exemple à [Schaum].
Quand on déclare une variable avec un nom et un type, un emplacement mémoire du type de la variable est créé à une certaine adresse avec son nom pour y accéder. L’emplacement mémoire recevra la valeur de la variable lors d’une affectation.
int x ; // une déclaration
x = 3 ; // une affectation
Le C permet de manipuler dans le programme, les adresses des emplacements mémoire des variables. &x désigne l’adresse de la variable x. On peut déclarer des pointeurs d’un type qui sont des variables contenant des adresses de variables de ce type avec le symbole *.
int * p ; // un pointeur sur un entier
p = &x ; // p vaut l’adresse de x
L’opérateur * s’appelle l’opérateur de déréférencement et *p désigne la valeur contenue dans l’emplacement mémoire dont l’adresse et p.
int y = *p ; // y vaut 3
On peut aussi déclarer une référence à une variable existante. Une référence se déclare avec l’opérateur &. Une référence est un synonyme – un alias (un nom supplémentaire) – pour désigner l’emplacement mémoire d’une variable.
int & z = x ; // z est une référence a x, z vaut 3
Si on change la valeur de x, la valeur de z est aussi changée et inversement. Idem avec *p.
x = 4; // x et z valent 4, *p aussi
z = 5; // x et z valent 5, *p aussi
*p = 6; // *p, x et z valent 6
Noter que les opérateurs & et * sont inverses l’un de l’autre. On a toujours :
*(&x) = x et &(*p) = p
Attention, on ne peut pas déréférencer un pointeur qui ne contient pas une adresse valide.
int * q ;
*q = 7 ; // plantage
Il faut initialiser le pointeur avant :
int * q = &x;
*q = 7 ; // ok
NB : Il est très important de très bien connaître le fonctionnement des pointeurs et des références car l’intérêt de la programmation objet en C++ repose sur leur utilisation intensive.
Bruno Bouzy 4 09/10/03
Notes de cours C++
Spécificités de C++ surlangage de C
C++ dispose d’un certain nombre de spécificités par rapport a C qui ne sont pas axées sur l’orienté objet :
le commentaire
l’emplacement libre des déclarationsles arguments par défaut
la surdéfinition de fonctionles opérateurs new et deleteles fonctions en ligne
Commentaire
Pour écrire des commentaires dans un programme, le programmeur dispose toujours des commentaires C avec /* et */ et aussi de commentaires en fin de ligne avec //
cout << « bonjour » ; // ceci est une formule de politesse
On peut mêler les deux techniques :
/* ceci est un commentaire en C // donc ce double slash ne sert a rien */
ou encore
// ceci est un commentaire en C++ /* donc ce slash etoile ne sert à rien */
mais pas :
// ceci est un commentaire en C++ /* donc ce slash etoile ne sert à rien
et cela donne une erreur de compilation si le etoile slash n’est pas sur la même ligne */
Emplacement des déclarations de variables
L’emplacement des déclarations est libre en C++ ; le programmeur n’est pas obligé de les mettre au début d’une fonction. Par contre, on ne peut utiliser la variable déclarée que dans les instructions du bloc où est effectuée la déclaration et postérieures à la déclaration. L’avantage de cette spécificité du C++ est de pouvoir déclarer une variable juste au moment où l’on en a besoin et cela clarifie le programme. Mais cet avantage est bien maigre.
Bruno Bouzy 5 09/10/03
Notes de cours C++
void f() {
…
i = 4 ; // incorrect
…
int i ; // i est déclaré ici
…
i = 4 ; // correct
…
{
…
float f ; // f est declare ici
…
f = 4.0 ; // correct
…
}// fin de la portee de f
…
f = 4.0 ; // incorrect
…
i = 5 ; // correct
…
} // fin de la portee de i
Passage de paramètres par référence
En C, les arguments sont passés par valeur. Ce qui signifie que les paramètres d’une fonction C sont toujours en entrée de la fonction et pas en sortie de la fonction ; autrement dit les paramètres d’une fonction ne peuvent pas être modifiés par la fonction.
Ci-dessous la fonction echange est censée échanger les valeurs des deux paramètres n et p.
void echange(int a, int b) {
int c = a ;
a = b ;
b = c ;
}
main() {
int n = 10 ; int p = 20 ;
cout << « avant appel : « << n << « « << p << endl ;
echange(n, p) ;
cout << « après appel : « << n << « « << p << endl ;
}
Malheureusement, pour la raison invoquée plus haut, la sortie de ce programme est :
avant appel : 10 20
apres appel : 10 20
Les programmeurs C ont l’habitude de palier à cet inconvénient du C en passant l’adresse des paramètres en sortie d’une fonction au lieu de passer la valeur. Ainsi, notre exemple sera écrit :
void echange(int * a, int * b) {
int c = *a ;
*a = *b ;
*b = c ;
}
main() {
int n = 10 ; int p = 20 ;
cout << « avant appel : « << n << « « << p << endl ; echange(&n, &p) ;
cout << « apres appel : « << n << « « << p << endl ;
Bruno Bouzy 6 09/10/03
Notes de cours C++
}
La sortie de ce programme correspond à ce que notre intuition attend :
avant appel : 10 20
apres appel : 20 10
Cette manipulation de pointeurs est lourde à gérer pour le programmeur. C++ palie à cet inconvénient en permettant le passage de paramètres par référence ; la programmation devient plus légère et le résultat correct. Il suffit de changer la déclaration de la fonction echange :
void echange(int & a, int & b) { … }
L’appel de la fonction est le même que pour le passage par valeur :
echange(n, p) ;
On notera qu’il n’y a rien d’extraordinaire. Pascal possède cette propriété de passage de paramètres par référence et C++ ne fait que combler un manque important du C.
Arguments par défaut
En C il est indispensable que l’appel de la fonction contienne exactement le même nombre et type d’arguments que dans la déclaration de la fonction. C++ permet de s’affranchir de cette contrainte en permettant l’usage d’arguments par défaut.
void f(int, int = 12) ;
main () {
int n = 10 ; int p = 20 ;
f(n, p) ;
f(n) ;
}
void f(int a, int b) {
cout << « premier argument : « << a ;
cout << « second argument : « << b << endl ;
}
La sortie de ce programme est :
premier argument : 10 second argument : 20
premier argument : 10 second argument : 12
Lors d’une déclaration avec des arguments par défaut, ceux-ci doivent être les derniers de la liste des arguments.
Le programmeur doit fixer les valeurs par défaut dans la déclaration de la fonction ou dans la définition.
Surdéfinition de fonction
On parle de surdéfinition lorsqu’un symbole prend plusieurs significations différentes. Ci-dessous le symbole sosie possède deux significations.
void sosie(int) ;
void sosie(float) ;
main () {
int n = 10 ; float x = 4.0 ;
sosie(n) ;
sosie(x) ;
}
void sosie(int a) {
cout << « sosie avec INT : « << a << endl ;
}
void sosie(float b) {
cout << « sosie avec FLOAT : « << b << endl ;
}
La sortie de ce programme est :
sosie avec INT : 10
sosie avec FLOAT : 4.0
Opérateurs new et delete
En C, la gestion dynamique de la mémoire fait appel aux fonctions malloc et free. En C++, on utilise les fonctions new et delete.
Avec la déclaration :
int * ad ;
en C++ on alloue dynamiquement comme cela :
ad = new int ;
alors qu’en C il fallait faire comme ceci :
ad = (int *) malloc (sizeof(int)) ;
Plus généralement, si on a un type donné type, on alloue une variable avec :
new type ;
ou un tableau de n variables avec :
new type [n] ;
On désalloue dynamiquement de la mémoire (allouée avec new) comme cela :
delete ad;
Plus généralement, on désalloue une variable x (allouée avec new) avec :
delete x;
ou un tableau de variables (allouée avec new []) avec :
delete [] x ;
En C++, bien que l’utilisation de malloc et free soit toujours permise, il est très conseillé de n’utiliser que new et delete.
Spécification « inline »
Rappel sur les macros en C
En C, on peut définir une macro avec le mot-clé define :
#define carre(A) A*A
Cela permet à priori d’utiliser carre comme une fonction normale :
int a = 2 ;
int b = carre(a) ;
cout << « a = « << a << « b = « << b << endl ;
le résultat sera correct dans ce cas simple :
a = 2 b = 4
Quand il voit une macro, le C remplace partout dans le code les expression carre(x) par x*x.
L’avantage d’une macro par rapport à une fonction est la rapidité d’exécution. En effet, le temps a recopier les valeurs des paramètres disparaît. La contrepartie est un espace mémoire du programme plus grand.
Plus embêtant sont les effets de bord des macros. Si l’on programme :
b = carre(a++) ;
On attendrait le résultat suivant :
a = 3 b = 4
En réalité, ce n’est pas le cas car, à la compilation, le C remplace malheureusement carre(a++) par a++*a++. Et à l’exécution, le programme donne le résultat suivant :
a = 4 b = 6
Les fonctions inline
Le C++ palie à cet inconvénient en permettant de définir des fonctions, dites en ligne, avec le mot clé ‘inline’.
inline int carre(int x) { return x*x ; }
Une fonction définie avec le mot-clé inline aura le même avantage qu’une macro en C, à savoir un gain de temps d’exécution et le même inconvénient, à savoir une plus grande place mémoire occupée par le programme.
L’avantage des fonctions en ligne est la disparition des effets de bord.
Classes et objets
Ce paragraphe aborde les caractéristiques de C++ vis-à-vis de la programmation orientée objet. Il rappelle d’abord le vocabulaire objet, notamment ce que signifie encapsulation, puis il montre comment déclarer une classe C++, comment définir le corps des fonctions membres, la distinction entre membres privés et publiques, l’affectation d’objets, les constructeurs et les destructeurs, l’exploitation d’une classe avec des fichiers .h et .cpp et enfin les membres statiques d’une classe.
Vocabulaire objet
Ce paragraphe rappelle le vocabulaire employé en orienté objet. Et il donne la correspondance entre le vocabulaire objet et le vocabulaire spécifique de C++.
Objet
Un objet est une entité, qui possède un état et un comportement. Pour définir ce qu’est un objet, les livres donnent souvent une équation du style de :
objet = état + comportement
Classe
Vue de la programmation objet, une classe est un type structuré de données. Nous verrons qu’une classe C++ est le prolongement des structures C (mot-clé struct).
Vue de la modélisation objet, une classe correspond à un concept du domaine modélisé. Une classe regroupe des objets qui ont des propriétés et des comportements communs. En C++, pour une classe, on dit aussi une classe.
Instance
Pour désigner un objet de la classe, on dit aussi une instance. « instance » est un anglicisme qui possède une signification proche de celle de « exemple » en français. On dit souvent qu’une instance « instancie » une classe. Cela signifie que l’instance est un exemple de la classe. En C++, pour désigner une instance on dit plutôt un objet.
Attribut ou membre
L’état d’un objet est l ‘ensemble des valeurs de ses attributs. Un attribut est une propriété de l’objet. En C++, on dit membre.
Méthode ou fonction membre
Vue de la modélisation objet, une méthode est une opération que l’on peut effectuer sur un objet. Vue de la programmation objet, une méthode est une fonction qui s’applique sur une instance de la classe. En C++, on dit fonction membre.
Message
A l’origine de la programmation orientée objet, un objectif était d’avoir des comportements d’objets concurrents. On voulait qu’ils « communiquent » en « envoyant » et « recevant » des
« messages ». Actuellement, tous les langages objet implémentent les objets de manière fonctionnelle. C’est-à-dire qu’au lieu « d’envoyer un message à un objet », on appelle une méthode, c’est-à-dire que l’on appelle une fonction qui manipulent des données. Donc, il faut être conscient de la correspondance suivante :
envoyer un message à un objet = appeler une méthode associée à l’objet
recevoir un message = entrer dans le corps de la méthode appelée Encapsulation des données
En POO pure, on suit le principe d’encapsulation :
Tout attribut d’une classe est caché dans l’implémentation de la classe et il est accédé par une méthode de lecture et une méthode d’écriture située dans l’interface de la classe.
Pour répondre à ce principe, C++ prévoit les mot-clés ‘private’ et ‘public’ pour dire si un attribut est visible d’un programme utilisateur de la classe. En ce sens, il permet de ne pas encapsuler tous les attributs d’une classe et permet de ne pas suivre le principe d’encapsulation a la lettre. Nous pensons que c’est une bonne chose car de nombreuses applications ne nécessitent pas d’encapsuler toutes les données, d’autres oui. D’autres langages objet, au contraire de C++, obligent le programmeur à suivre ce principe (Smalltalk par exemple).