Sommaire du chapitre :
1. Méthodes retournant des adresses de variables
1. Méthodes retournant des adresses de variables
int a, b ;
int & f() { return a }
main()
{
f() = 2 ; /* REPERE */
b = f() ;
}
En C++, il est possible, en plus de ce qui a été décrit à la section de retourner
une adresse de variable, en mettant & à la place de l'étoile indiquant
un pointeur.
La fonction f() peut alors être utilisée comme si c'était un nom de variable,
ce qui donne la ligne REPERE, assez surprenante. La ligne suivante est plus
normale et fonctionne de la même manière que si l'on n'avait pas mis de & à
la ligne 2.
Il est possible de définir des références de variables comme membre de classe, ou comme variable locale ou globale semblables à celles qu'on trouve comme paramètre dans les méthodes et dans les fonctions.
int a (8) ;
int &b (a) ;
int *i = new int ;
int &j = *i ;
class C {
public :
int &m ;
C(int z):m(z) { } ;
} ;
Dans le schéma ci-dessus, on a illustré la déclaration et l'initialisation
de ces variables.
La ligne 1 déclare une variable et l'initialise.
La ligne 2 déclare une référence.
L'initialisation est indispensable.
La variable b obtient la même adresse que a et les affectations qu'on fait à
b changent donc la valeur de a également.
b a certains aspects d'un pointeur, mais on ne peut pas changer l'endroit pointé
après l'initialisation.
C'est une façon d'éviter les pointeurs qui sont toujours "dangereux".
La ligne 4 montre une initialisation faite à partir d'un pointeur.
Si l'on n'a pas écrit new à la ligne 3, la référence est incorrecte et
le compilateur ne détecte rien !
La ligne 7 définit une référence dans une classe.
Elle doit être initialisée, mais comme cela est toujours le cas dans les classes,
l'initialisation se fait sur la ligne du constructeur, ligne 8.
le mot-clé const peut être utilisé dans un certain nombre de cas différents qui sont représentés dans l'exemple ci-dessous :
int u ;
class A {
public :
int v ;
const float PI = 3.14 ;
const int &m (const int i) const {
v = 0 ; /* erreur */
i = 0 ; /* erreur */
return u ;
} ;
} ;
A Objet ;
int j ;
j = Objet.m(4) ;
Objet.m(5) = 6 ; /* erreur */
A la ligne 5, on a déclaré une variable qui doit être initialisée à sa déclaration
et qui ne peut plus être modifiée par la suite.
A la ligne 6, on a concentré trois utilisations distinctes du const .
La première fois qu'il apparaît, il indique que la variable retournée ne peut
pas être modifiée.
Cet usage de const n'est valable qu'avec le &.
Il n'a pas d'effet si l'on retourne un pointeur (* au lieu de &).
Le deuxième const interdit la modification de la variable dans le corps
de la méthode.
C'est en quelque sorte un garde-fou pour le programmeur de cette méthode, qui
l'empêche de modifier cette variable par inadvertance.
Le troisième const assure que la méthode ne modifie aucun des membres
de la classe.
4. Classes génériques (templates)
Il est possible de créer des classes avec paramètres (autres classes, types ou constantes) qui peuvent être instanciées en fonction de ces paramètres.
Par exemple on désire créer une classe qui manipule soit des entiers soit
des réels mais de la même façon.
On peut alors créer une classe générique qui contient un symbole dans les paramètres.
Lorsque l'on instancie un objet, on indique quel est le type qu'il faut utiliser
pour cette instance-là.
La classe créée remplace partout le symbole par le type indiqué.
template <class T , int N >
class Clas {
public :
T arr[N] ;
Clas(T var) { ar[0] = var ; }
T f(T *y, int i) {
arr[i] = *y ;
return arr[0] ;
}
} ;
Clas<int , 10> Obj(1) ;
int n = Obj.f(&m , 2) ;
La ligne 1 contient une indication qui précède la classe proprement dite.
Deux paramètres T et N sont définis entre crochets.
Ces paramètres sont utilisés pour spécifier la classe Clas aux lignes 2 à 10.
T peut être utilisé partout où un nom de classe ou de type peut l'être.
N devra être remplacée par une constante entière à l'instanciation (ligne 4).
La ligne 5 montre comment définir le constructeur.
Les lignes 6 à 9 définissent une méthode.
Dans ces deux fonctions, T est utilisé comme s'il était un nom de classe: type
de paramètre, type de la valeur retournée.
L'instanciation de la classe est faite à la ligne 11.
T est remplacé par le type int et N par la constante 10.
L'exemple ci-dessous montre comment définir les codes à l'extérieur de la classe (offline) :
template <class T , int N>
Clas<T,N>::Clas(T var) { ... }
template <class T , int N>
Clas<T,N>::f(T *var, int j) { ... }
Les définitions sont précédées du même en-tête que la définition initiale.
Entre le signe :: et le nom de la classe, on répète les noms des paramètres
entre crochets.
Pour le reste, on suit les règles habituelles.
Lorsque l'on définit une nouvelle classe, par exemple une classe qui contient
des nombres complexes, il est intéressant de définir une opération d'addition
qui s'applique à cette nouvelle classe.
Le C++ permet cela et même beaucoup plus.
La liste des opérateurs qu'il est possible de redéfinir (surcharger) est donnée
ci-dessous.
new
delete
+
-
*
/
%
'
&
~
!
<
>
/-
<<
>>
&&
++
--
,
->*
->
()
Les opérateurs gardent leur forme originale (l'addition a 2 arguments, la négation n'en a qu'un) et leur priorité (on exécute la multiplication avant l'addition).
Dans l'exemple ci-dessous, on a défini une classe qui comporte 2 entiers et
un opérateur qui agit sur les membres de la classe.
Cet opérateur n'a qu'un seul argument, comme la négation dont il est dérivé.
class A {
public :
int x , y ;
A( int a , int b ) { x=a ; y=b ; }
A() { }
A operator- () {
A v ;
v.x = -x ;
v.y = -y ;
return (v) ;
}
} ;
A var1,var2(0,1) ;
var1 = var2 ;
La seule différence par rapport à une classe normale est la méthode définie
aux lignes 6 à 11.
Le nom de cette méthode est formé du nom operator suivi du signe -, qui est
l'opérateur défini par la méthode.
On pourrait le remplacer par un des autres opérateurs unaires.
On sait que c'est le - unaire, car la fonction n'a pas de paramètre.
La ligne 13 montre des instanciations faites de la façon habituelle.
var2 est initialisée par le constructeur.
La méthode qui définit l'opérateur de la ligne 6 n'a pas de paramètre.
Sur les lignes 8 et 9, on utilise -x et -y , qui repèrent les variables locales
de la classe (ligne 3).
Lorsque l'on écrit -var2 (ligne 14), on appelle en fait une fonction-opérateur
placée à l'intérieur de l'objet var2 de classe A .
Les membres locaux vus par cette fonction correspondent aux membres x et y de
var2.
- x et - y sont les valeurs inverses des champs de var2 , valeurs qui sont déposées
dans la variable temporaire v elle-même retournée pour être affectée à var1.
Il y a deux autres possiblités de définir l'opérateur.Elles sont illustrées ci-dessous.
class A {
...
friend A operator- (A & p) {
A v ;
v.x = -p.x ;
v.y = -p.y ;
return (v) ;
}
A opertor- (A & p) {
A v ;
v.x = -p.x ;
v.y = -p.y ;
return (v) ;
}
Aux lignes 3 à 8, on définit le même opérateur qu'avant, mais le nom est précédé
de friend.
Dans ce cas on ne peut plus utiliser les paramètres locaux (this n'est
pas défini).
On utilise à leur place les membres du paramètre entre parenthèse.
Le compilateur reconnaît si on utilise cette forme ou la précédente grâce au
mot-clé friend .
Aux lignes 5 et 6, on utilise le paramètre plutôt que les champs locaux.
Aux lignes 10 et 15, on a placé une troisième forme de définition.
Elle est semblable à la précédente, mais comme elle se trouve à l'extérieur
de la classe, il ne faut pas mettre friend.
Remarque : Bien évidemment lorsque l'on écrit un programme il faut choisir une seule forme...
class A {
...
A operator| (A &q) {
A v ;
v.x = x | q.x ;
v.y = y | q.y ;
return(v) ;
}
friend A operator| (A &p , A &q) {
A v ;
v.x = p.x | q.x ;
v.y = p.y | q.y ;
return(v) ;
}
A m , n(0,0) , l(1,1) ;
m = n | l ;
m = n | A(22) ;
Ci-dessus on a défini l'opérateur |.
La ligne 2 représente les opérations définies dans les cadres précédents.
Les opérateurs binaires (à deux arguments) peuvent également être définis selon
les trois formes présentées précédemment.
La première est placée sur les lignes 3 à 8 et la deuxième sur les lignes
9 à 14.
La forme déclarée à l'extérieur de la classe est la même que la deuxième forme,
mais sans le mot-clé friend.
Sur la ligne 17, n est l'élément de gauche et ll'élément de droite.
Dans la première forme (lignes 3 à 8) l'élément de gauche correspond aux variables
locales (this) et celui de droite correspond à l'unique paramètre.
Dans la deuxième et la troisième forme, l'élément de gauche correspond au premier paramètre et celui de droite au deuxième.
8. Opérateurs de lecture et d'écriture (cin, cout)
C++ offre de nouvelles instructions d'entrées-sorties.
Ces instructions tirent parti des possibilités de redéfinir les opérateurs appliqués
à certains objets.
Des objets clavier, écran, fichier ont été définis avec des opérateurs >> et
<< pour décrire les transferts de données.
Ces fonctions ne font donc pas partie de C++, mais appartiennent à des librairies
spécifiques.
#include <iostream.h>
int i ;
cout << "valeur ?" ;
cin >> i ;
cout << 2*i << endl ;
La ligne 3 montre comment coder un affichage sur l'écran.
cout est le terminal en sortie.
L'opérateur << indique qu'on veut sortir une valeur.
Ceci est illustré par le fait que les flèches vont de la chaîne de caractères
vers le périphérique.
L'information est donc redondante, cout demande toujours <<.
La ligne 4 représente une lecture du clavier.
cin indique le clavier et la flèche un mouvement des données en provenance
de ce périphérique.
La ligne 5 montre qu'on peut afficher plusieurs variables ou constantes sur la même ligne.
endl est la constante "\n".
Elle sort donc une fin de ligne.
Il en est de même pour les entrées.
#include <iostream.h>
#include <iomanip.h>
char str[80] ;
cin >> str ;
cin >> setw(5) >> str ;
cin.getline(str , 5 , '\n');
cout.write(str , 3)
La ligne 4 lit les caractères entrés jusqu'au premier blanc.
Il est possible d'entrer plus de caractères que ce que peut contenir le buffer.
La ligne 5 limite le nombre de caractères à 5.
La ligne 6 également. Elle permet de plus de modifier la valeur du séparateur.
Finalement la ligne 7 permet d'écrire les 3 premiers caractères de str.
Ces commandes font donc automatiquement le formatage des données et cela dans les 2 sens, lecture et écriture.
Pour lire d'un fichier ou y écrire, on utilise les commandes suivantes :
#include <iostream.h>
#include <fstream.h>
int i , j ;
char str[80] ;
ofstream outfile ("NomFichier" , ios::out) ;
ofstream outfile ("NomFichier" , ios::app) ;
ifstream infile ("NomFichier" , ios::in) ;
i = outfile ;
j = infile ;
infile >> str ;
outfile << str ;
outfile.close() ;
Les lignes 5 et 6 ouvrent des fichiers en mode d'écriture.
La ligne 6 fait ajouter les caractères à la fin du fichier, alors que la ligne
5 réécrit à partir du début.
La ligne 7 ouvre un fichier en lecture.
Les lignes 8 et 9 retournent un code qui indique si l'ouverture des fichiers
a été faite correctement.
Retour de 0 en cas d'erreur.
Les lignes 11 et 12 montrent que les mêmes commandes que celles qui sont utilisées sur les entrées standards peuvent être utilisées.