et un peu de
Programmation par Objets
Deuxième Partie: Relations
Entre Classes en C++.
Najib TOUNSI ( ntounsi@emi.ac.ma)
Creation : Mai 1998
Derniere MAJ: Oct 98
Un programme C++, se présente comme un ensemble de classes. La fonction main(), ou programme principale, doit être juste un lancement de l'application.
En programmation à objets, il existe une relation fondamentale entre les classes. C'est la relation utilise (uses). Une classe A utilise une autre classe B si elle déclare en son sein une entité de la classe B et en appelle des méthodes. Par exemple une classe commande peut utiliser une classe article:
|
|
|

Ainsi, une classe A:
Il est utile, comme le suggère Bertand MEYER, de considérer que la classe A est cliente de la classe B. Le principe d'abstraction se traduit alors par le fait que le client n'a pas besoin de savoir comment le fournisseur, en l'occurrence la méthode appelée, s'organise et répond à son message pourvu qu'il respecte le contrat spécifié dans l'interface. Dans la facturation d'une commande par exemple, un objet commande délègue à l'objet article le calcule d'un prixTTC.
La relation utilise est considérée comme l'une des relations critiques entre modules en génie logiciel. Une modification intervenant dans une classe n'a de répercussion, s'il y a lieu, que sur ses clientes. Si la modification ne touche que l'implantation -l'interface ne change pas- il n'y a aucune répercussion. L'art de la construction d'un logiciel, est de savoir quelles classes avec quelles méthodes.
Une deuxième relation fondamentale entre classes est la relation est_un (ISA). Un objet de classe B est_un objet de classe A s'il possède les caractéristiques de A (éventuellement révisées) avec d'autres en plus. On dit que B hérite de A.
Conceptuellement cette relation permet de nuancer des classes d'objets relativement proches par leurs caractéristiques, dans le sens où à un certain niveau d'analyse on a envie de considérer des objets comme appartenant à une même classe (mère) et à un niveau d'analyse plus bas et donc plus fin, on a envie de distinguer et classer ces objets (fils) en des classes différentes[10].
Imaginer par exemple un système qui gère divers types d'articles commerciaux d'un magasin grande surface. Une approche serait de les classer tous dans une même classe qui contient toutes les caractéristiques possibles de ces articles. Tous les articles seront alors traités de la même façon et sans discernement, du plus simple comme une confiserie au plus complet comme téléviseur, et certains articles contiendront des caractéristiques bien inutiles, sinon absurdes (e.g. voltage pour un chocolat). Une autre approche, est de créer une classe distincte pour chaque genre d'article, ne contenant que les caractéristiques propres à ce genre. Ces classes seront supposées n'ayant aucune représentation commune et ce serait dommage d'être incapable d'utiliser la simplicité des articles de base qui contiennent des caractéristiques communes à beaucoup d'autres articles -tel le numéro, le nom, le prixHT etc...

Figure 4. Relation
d'Héritage Entre Classes:
Arbre d'Héritage
L'approche idéale, est de fournir une classe générale article,
définissant les caractéristiques communes à différents genres
d'articles, et de spécialiser au fur et à mesure cette
classe en sous classes , pour tout autre article
(e.g. T.V.) ayant des caractéristiques propres supplémentaires.
Car après tout un téléviseur, même avec ses propres
caractéristiques comme le voltage, est également <<est_un>>
un article. Il y aura alors un lien d'héritage entre
une classe A donnée et les sous-classes dérivées, dans le
sens où toutes les caractéristiques de A s'appliquent à
ses sous classes (sauf indication contraire, voir plus loin
masquage et fonctions virtuelles). On dit qu'on réutilise
la classe A.
Dans la figure 4, un objet vêtement a neuf caractéristiques: celles de article plus les deux champs taille et coloris. Un objet T.V. a dix caractéristiques: celles de article dont prixTTC() redéfinie (voir plus loin), plus les champs voltage et duréeGarantie et la méthode validerGarantie().
On a ainsi, l'autre relation ISA entre classes, par exemple:
vêtementarticle
Cette relation est transitive - comme la voie hiérarchique- et le «graphe» d'héritage est un arbre: héritage simple.
Remarque: Il existe aussi ce qu'on appelle un héritage multiple, où une classe hérite de caractéristiques provenant de plusieurs classes «mères». Cependant l'héritage multiple ne correspond pas bien à une vision hiérarchique des choses, seulement il se révèle pratique à l'implantation dans la mesure où il permet de factoriser des descriptions de membres de classes quand des objets partagent encore des caractéristiques provenants de classes mères éloignées dans l'héritage simple.
Les deux relations USES (classe cliente) et ISA (sous classe) sont orthogonales. Une sous classe D d'une classe B qui utilise une classe A, est aussi cliente de cette classe A. Une classe B qui utilise une sous classe D d'une classe A, utilise aussi (est client de) cette classe A.

Figure 5. Orthogonalité des Relations USES et ISA
Le mécanisme de classe dérivée de C++, permet d'implanter le
concept de sous classe et d'héritage. Une classe dérivée
(soit D) , est une classe dont les caractéristiques sont celles
d'une autre classe (soit B), dite de base, auxquelles
viennent se rajouter d'autres caractéristiques spécifiques à D.
Elle est déclarée par la notation
class D: B
Exemples:
typedef enum {S,M,L,XL} SIZE;
class vetement: public article{
SIZE taille;
char coloris[10];
// ...
};
déclare vetement comme une classe ayant comme membres ceux de la classe article (supposée déjà définie), plus les deux champs supplémentaires taille et coloris qui sont naturellement des données propres à un vêtement.
class audioVisuel: public article {
int dureeGarantie;
// ...
};
class TV: public audioVisuel {
int voltage;
// ...
};
Ici, la classe audioVisuel hérite de article et la classe TV hérite de audioVisuel et, par voie hiérarchique (indirectement donc), de la classe article. ( L'ajout de public: sera justifié plus loin).
Une classe dérivée peut avoir son propre constructeur. Si la classe de base en a un, il doit être appelé en premier (avec ses paramètres éventuels) et la déclaration du constructeur d'une classe dérivée doit donc tenir compte des paramètres nécessaires à l'initialisation des champs provenant de sa classe de base.
class vetement : public article {
// ...
public:
vetement(int , char*, float, int, SIZE, char*);
// les caractéristiques de base d'un article plus // celles d'un vetement
};
vetement:: vetement(int n, char* m,float p,int q, SIZE s, char* c)
: article (n,m,p,q)
{ taille=s;
strcpy(coloris,c);
}
L'expression :article (n, m, p, q) est l'appel au constructeur de base. Il est appelé avant l'exécution du code du constructeur dérivée. De façon générale, l'instanciation d'une classe donnée est faite de façon descendante dans l'ordre de la hiérarchie d'héritage: de la classe plus générale, à la classe plus particulière.
Pour instancier un objet TV, on aurait le constructeur
TV::TV(int n, char* m, float p, int q, int g, int v)
:audioVisuel(n,m,p,q,g)
{ voltage=v;
}
avec l'appel concernant sa classe mère audioVisuel (qui, elle, fera appel au constructeur article()).
Quand aux destructeurs, ils sont déclarés normalement ~classe() et appelés dans l'ordre inverse cette fois-ci: celui de la classe dérivée d'abord, ensuite ceux de la hiérarchie remontante.
Dans une classe dérivée, une caractéristique qui a le même nom qu'une autre caractéristique de classe parente, masquera cette dernière en la redéfinissant.
class B{ ... x; ...};
class D:public { ... x; ...};
dans D toute référence à x concerne le x de D. Pour adresser x de B, il faut écrire B::x.
On a muni les classes ci-dessus d'une fonction affiche() qui affiche les valeurs des champs.
void article::affiche() {
cout <<numero<<" "<< nom<<" " <<pht<<" "<<qte;
}
void audioVisuel::affiche(){
article::affiche();cout<<" "<<dureeGarantie;
}
void TV::affiche(){
audioVisuel::affiche();cout<<" "<<voltage;
}
Chaque fonction affiche() redéfinit celle héritée pour imprimer les champs de sa classe. Pour cela elle fait appel à la fonction affiche() de la classe mère - qui imprime les champs hérités- avant d'imprimer ses champs propres. Si v est de type TV, v.affiche() sortira les six champs de v.
Qu'en est-il du statut publique/privée dans les classes dérivées? En C++ la protection des informations est très élaborée. On va considérer différents cas et énoncer quelques règles générales avant d'en illustrer le mécanisme (complexe) par un exemple figuratif. Appelons membre accessible pour un objet x d'une classe X, le fait de pouvoir écrire x.membre, i.e. accès à un champ ou appel d'une méthode pour x.
[1] Un membre d'une classe X n'est accessible dans une classe cliente de X que s'il est déclaré public dans X. C'est dire qu'aucun membre privé de X n'est accessible ailleurs que dans X. (Mais voir cas 3 ci dessous).
[2] Cette règle s'applique aussi pour les sous classe -directes ou indirectes- de X: les membres privés de X n'y sont pas accessibles. Bien que dans ce cas ils soient membres (hérités) de la sous classe!
[3] On peut néanmoins (tout de même) rendre un membre d'une
classe X <<privé>> dans les classes clientes
de X, et <<publique>> dans ses sous classes.
On le déclare protected.
classe X {protected: membres...};
[4] Une sous classe ne transmet pas forcément à ses sous classes
propres, les privilèges qu'elle a sur ses membres (public
et protected) hérités. Pour cela, elle doit déclarer sa
classe mère public.
class Y: public X { ... };
Car, sinon, tous les membres de X seront inaccessibles pour les descendants de Y. (Mais accessibles pour un objet Y dans ces mêmes descendants bien sûr).
On va illustrer ces différents mécanismes par un exemple (figuratif) qui englobera une majorité de cas. On a une classe X, dont dérive Y ensuite Z, et une classe U qui utilise X, Y et Z. En outre, Y utilise X et Z utilise X et Y. C'est la situation suivante:
Z ISA Y ISA X
Y USES X
Z USES X Y et
U USES X Y Z
Un membre A de X sera alternativement rendu public, private ensuite protected et cela pour chaque cas de la sous classe Y déclarant : public X ou : X
|
|
Dans chaque tableau, N veut dire accès non autorisé, et O accès autorisé.
|
Accès |
Dans Classe |
||
|---|---|---|---|
|
Y |
Z |
U |
|
|
x.A |
N |
N |
N |
|
y.A |
N |
N |
N |
|
z.A |
- |
N |
N |
A n'est accessible nul part (sauf dans la classe X, cas non traité ici, évidemment).
|
Accès |
Dans Classe |
||
|---|---|---|---|
|
Y |
Z |
U |
|
|
x.A |
N |
N |
N |
|
y.A |
N |
N |
N |
|
z.A |
- |
N |
N |
Cas identique au précédent. public X n'y fait rien.
|
Accès |
Dans Classe |
||
|---|---|---|---|
|
Y |
Z |
U |
|
|
x.A |
O |
O |
O |
|
y.A |
O |
O |
O |
|
z.A |
- |
N |
N |
A publique pour X et Y (par héritage) mais pas pour Z où il est privé
|
Accès |
Dans Classe |
||
|---|---|---|---|
|
Y |
Z |
U |
|
|
x.A |
O |
O |
O |
|
y.A |
O |
O |
O |
|
z.A |
- |
O |
O |
Ici c'est class Y: public X qui a fonctionné. A est transmis publique à Z.
|
Accès |
Dans Classe |
||
|---|---|---|---|
|
Y |
Z |
U |
|
|
x.A |
O |
O |
N |
|
y.A |
O |
O |
N |
|
z.A |
- |
N |
N |
Ici, on voit l'effet de protected: A est utilisable dans Z (pour X et Y!), mais pas dans U. Comparer au cas 3.
|
Accès |
Dans Classe |
||
|---|---|---|---|
|
Y |
Z |
U |
|
|
x.A |
O |
O |
N |
|
y.A |
O |
O |
N |
|
z.A |
- |
O |
N |
Même cas que précédemment, mais public X a joué pour Z (seulement).
Il est intéressant de considérer les messages d'erreurs qu'indique le compilateur (le mien étant GNU C++ du shareware) qui nuance entre différents cas N de non autorisation d'accès. (Ce n'est pas toujours nuancé dans les autres compilateurs...)
Voici les textes sorties:
c'est quand on fait x.A, A privé, ailleurs que dans X.
c'est le cas d'accès y.A ou z.A, A privé, et Y Z héritant A.
quand on accède à A dans Z (Z ISA Y ISA X) et X non déclaré public dans Y.
quand on fait x.A dans U est A déclaré protected.
quand on fait y.A ou z.A dans U, toujours pour A protected.
Plus encore, le compilateur turbo C++, a une autre politique de public dans les sous classes et surtout protected. Un membre protected est privé sauf pour les sous classes mais uniquement pour des objet de cette sous classe (si on lui a transmis le droit). La matrice des droits est:
| cas 5 |
N N N O N N - N N |
cas 6 |
N N N O N N - O N |
| cas 3 |
0 0 0 O N N - N N |
cas 4 |
O O O O O O - O N |
C++ est un langage à typage statique (comme on vient juste de le voir), nécessaire pour une programmation fiable, mais offre la possibilité de liaison (édition de lien) dynamique -nécessaire à la programmation objets. Le mécanisme de fonctions virtuelles sert à implanter ce dernier aspect. Une fonction virtuelle est une fonction membre d'une classe qui peut être redéfinie dans ses sous classes avec la bonne fonction appelée pour chaque instance à l'exécution. Elles sont déclarées virtual dans la classe. Ce mot indique que la fonction possède différentes versions dans différentes classes dérivées et qu'il s'agit, pour le compilateur, d'appeler à chaque fois la bonne fonction. Reprenons l'exemple de la fonction affiche() des classes article et ses dérivée.
class article{
// ...
virtual void affiche() {
cout <<numero<<" "<< nom<<" " <<pht<<" "<<qte;
}
// ...
};
class audioVisuel: article{
// ...
void affiche(){
article::affiche();
cout<<" "<<dureeGarantie;
}
// ...
};
La classe article déclare affiche() comme une fonction virtual, et en fournit une version de base. On doit toujours fournir une version de base pour une fonction virtuelle. La sous classe audioVisuel la redéclare et en fournit une autre version (le mot virtual n'est pas nécessaire à redéclarer pour les sous classes).
Où joue le fait virtuel par rapport au cas non virtuel? C'est lié au polymorphisme et à la liaison dynamique. Considérons les déclarations
article *a = new article (...);
audioVisuel *b = new audioVisuel(...);
et la séquence de code
a->affiche(); (*)
b->affiche();
Dans chaque cas c'est la fonction correspondante qui est appelée: pour a, la version de affiche() définie dans article, et pour b, la version de affiche() définie dans audioVisuel. Ceci est normal bien sûr, que ce soit virtuel ou non. Mais maintenant, considérons l'affectation
a = b;
qui fait pointer a , déclaré pointeur article, sur une instance d'une autre classe, en l'occurrence une sous classe. Le typage statique l'autorise exceptionnellement mais entre classes et sous classes et dans le sens
instance de classe <- instance sous-classe.
C'est le polymorphisme. Car en effet, comme un objet audioVisuel est aussi (isa) un article, il peut être désignée par une variable article. Par contre l'inverse est incorrecte: un article n'est pas forcément un genre spécifique comme vêtement ou TV...
Si maintenant on fait
a->affiche();
c'est la fonction affiche() (la bonne) de la classe audioVisuel qui est appelée, ce qui est normal. Alors que dans l'appel précédent (*) c'est la fonction de base qui était appelée.
En d'autre termes, la méthode à appliquer à un objet dépend de son instance actuelle. Ce mécanisme n'est possible que par la liaison dynamique. En outre, il faut que les variables désignant les objets soient des pointeurs. C'est pour cela qu'on a déclaré a et b comme des pointeurs.
Si la fonction affiche() n'était par virtual, ce qui était le cas précédemment, cela ne gênerait en rien le programme, seulement, on doit appeler affiche() sur des variables de la classe appropriée et éviter le polymorphisme. Car dans ce cas
a->affiche();
sera toujours traduit ( dû au typage statique) par un appel à la méthode de base.
Le polymophisme et le mécanisme de fonctions virtuelles est intéressant, car il est souvent nécessaire d'avoir une entité qui peut revêtir plusieurs formes (d'où le mot polymorphisme), et donc avoir à appeler des méthodes qui s'appliquent de façon appropriée à chaque cas. Imaginer par exemple un tableau (caddie) d'articles qui peut contenir tout type d'article, ou une fonction à paramètre article qui peut recevoir tout aussi tout article. Les méthodes à appeler sur les éléments du tableau doivent, si redéfinies, correspondre au type de l'élément en cours. C'est ce qui fait la souplesse et le charme de la programmation objets.
Exercice: La fonction prixTTC() est redéfinie ( voir fig. 4) pour un objet audioVisuel. En effet on peut distinguer deux taux de TVA différents entre un article de base et un appareil audiovisuel. En faire une fonction virtuelle et tester par un programme.
Fonctions Virtuelles Pures
Les fonctions virtuelles pures, sont des fonctions membres sans corps déclarées
virtual entete_fonction =0;
par exemple
virtual void f() =0;
Elles caractérisent une classe pour laquelle il n'y aura pas d'instances. Une telle classe est dite abstraite. Elle sert a détenir des fonctions, dont le corps devra être fourni par des classe dérivées éventuelles qui, elles, seront instanciées. Justement, l'indicateur =0; pour une fonction virtuelle, diffère sa définition vers des sous classes. L'utilité en est que chacune de ces sous classes pourra donner sa version de la fonction virtuelle.
Imaginons par exemple une classe figure qui englobe plusieurs types de figures géométriques pour lesquelles le calcul de la surface, le dessin etc. est différent selon que c'est un carre, un cercle ou tout autre figure. On pourra écrire:
class figure{
virtual void rotation(float)=0;
virtual void afficher()=0;
virtual float surface()=0;
// ...
};
On voit bien que cette classe n' a pas besoin d'instance, cela n'aurait aucun sens. Par contre, on peut en avoir pour des classes dérivées:
class rectangle : figure {
rectangle(float hg, float bd){ ... }
virtual void rotation(float alpha) {
// rotation d'un rectangle
}
virtual void afficher() //...
// ...
};
class cercle : figure {
// ...
};
Les classes cercle et rectangle donneront chacune leur version des fonctions virtuelles. C++ exige que toutes les fonctions virtuelles données dans la classe mère soient implantées dans les classes dérivées, car sinon il ne pourra pas instancier ces dernières (il y aurait une caractéristique héritée dont il ne connaît pas la réalisation).
Remarque: Cela n'empêche pas de mettre des fonctions membres normales dans une classe avec fonctions virtuelles. Elles seront héritées comme a l'accoutumé. Néanmoins cela diminuerait la caractéristique abstraite de la classe. On verra plus loin ([[section]] 15) qu'alliées à la généricité, les fonctions virtuelles pures deviennent un outil d'une grande puissance de développement.
La capacité de surcharge (sic) est une facilité très intéressante offerte par C++. Nous en avons rencontré des exemples pour écrire des opérations arithmétiques sur des objets de type complexes, ou pour redéfinir l'affectation ou la comparaison entre objets délicats. En fait ce moyen s'est révèlé d'une étonnante efficacité pour l'utilisateur qui a la liberté d'étendre les fonctionnalités de base du langage en même temps qu'il en étend les objets possibles.
Surcharge des Opérateurs << et >> d'Entrée/Sortie
Parmi les opérateurs intéressants à surcharger on a << et >> qu'on a utilisé pour les entrées/sorties. En C++, ils sont définis pour les types de base int, char, float... Qu'en est-il pour un type (classe) utilisateur? Car en effet il peut s'avérer utile de faire par exemple:
cout << c1<< c2;
cin >> c1;
où c1 et c2 sont deux complexes (cf. §7).
Notons d'abord que les noms cout et cin sont des variables externes qui désignent les fichiers (dits flots) entrées/sorties. Tous ces éléments sont définies dans <stream.h> qui est la bibliothèque générale des entrées/sorties et dont la tâche essentielle est de convertir les données standards vers des caractères imprimables pour les échanger avec l'extérieur. On y trouve deux classes particulières: istream et ostream qui correspondent respectivement aux flots (fichiers) d'entrée et de sortie.
Exemple: (extrait simplifié)
class ostream{
// .. usage du type FILE
public:
ostream& operator<<(int&);
// sortie d'un entier
ostream& operator<<(char*);
// sortie d'une chaîne
// sorties des autres types de base...
};
//même principe pour istream;
ostream& cout;
istream& cin;
cin et cout sont alors des variables déclarées du type référence à istream et ostream[11]. Les symboles << ou >> sont des opérateurs (fonctions membres de cout et cin) qui s'emploient selon le schéma:
ostream& << expr ou istream& >> expr
c'est à dire
cout.operator<< (expr) ou cin.operator<<(expr)
Ces opérateurs sont prédéfinis pour des expressions de base comme l'indique l'exemple ci-dessus. Noter qu'ils surchargent les opérateurs de décalage de C[12], si utilisés dans ce contexte. Ainsi:
cout << i;
est une expression qui imprime i, et qui a pour type résultat un flot ostream&; ce qui permet d'écrire:
cout << i << "\n";
qui s'évalue de gauche à droite comme
(cout << i) << "\n"
Entrée/Sortie d'Autres Objets
Revenons maintenant à comment faire une entrée/sortie sur un nombre complexe par exemple. On définit des fonctions membres qui font la tâche et on les formule sous forme d'opérateurs << ou >>.
class complexe{
double im,re;
public:
complexe(double = 0, double = 0);
friend complexe operator+(complexe, complexe);
// ...
friend ostream& operator<< (ostream&, complexe);
friend istream& operator>> (istream&, complexe);
};
ostream& operator<< (ostream& out, complexe x){
out <<x.re<<" + "<<x.im<<"i";
return out;
}
istream& operator>> (ostream& in, complexe x){
// lecture in >> x.re, in >> x.im
// selon un format choisi
return in;
}
Noter en particulier les paramètres formels in et out qui correspondront aux flots cin et cout lors de l'appel:
cin >> c1; // operator<<(cin, c1)
cout << c2; // operator>>(cout, c2)
et les instructions return qui retournent les mêmes flots en résultat.
cin >> c1 >> c2; // correcte
Exercice: Munir la classe String ([[section]] 8) de ces opérateurs.
Surcharge de l'Opérateur cast de conversion
Reprenons la classe String de [[section]] 8. On pourrait avoir besoin de convertir un objet String vers sa nature de base, char*. Il suffit pour cela de rajouter la fonction membre:
operator char* () {
char *p = new char[strlen(str)+1];
strcpy(p,str);
return p;
}
qui s'utilise ainsi:
String b("toto");
Char* a;
a = (char*) b;
pour affecter à a la chaîne "toto".
Exercice: Reprendre la classe complexe, et la munir d'un opérateur de conversion vers double, qui projette le complexe sur sa partie réelle.
Surcharge de l'Opérateur () de Fonction
En C et C++, () est un opérateur, qui a pour opérandes
une fonction (opérande gauche) et ses paramètres (opérandes
droits). Il s'évalue par l'exécution du code de la fonction sur
les paramètres. Ainsi il est susceptible de surcharge. Une
fonction membre d'une classe peut s'en charger. La syntaxe est la
même que pour un autre opérateur, par exemple:
int operator () (int);
pour un opérateur fonction d'un paramètre int et retournant un int.
Exemple:
class X{
int x;
public:
X(int a){ x = a;}
int operator() (int p){ return x+p;}
// ...
};
Si on déclare
X f(4) //f instance de X initialisee à 4
on peut écrire
a=f(3);
qui signifie donc
a=f.operator() (3);
L'effet en sera d'affecter 7 à a (la valeur de f incrémentée de 3). Car le code de l'opérateur est celui de la fonction membre.
Ce genre de fonction membre permet à un objet (f dans l'exemple) de devenir fonction. Fonction qui peut utiliser les données de l'objet (x dans l'exemple) comme une ressource. Cela est surtout utile dans la description d'itérateurs, sorte de curseurs retournant un à un les objets d'une collection (voir plus bas [[section]]16). On a besoin d'instancier des curseurs, un par itération, qui constitueront la ressource, et de les utiliser avec une syntaxe d'appel de fonction, laquelle retourne un objet qui sera l'élément courant de l'itération ([[section]]16).
Noter enfin que cet opérateur () ne peut pas être une fonction friend. (Pourquoi? Penser au profile de l'opération et à la forme de l'appel.)
Surcharge de l'opérateur [] d'indexation
On surcharge l'opérateur d'indexation [] de la même façon. Soit la classe
class Table{
int* x;
int size;
public:
Table(int a) { size = 0;}
int operator[] (int i) {
if (i<0 || i>=size)
cout <<"index hors espace\n";
else return x[i];
}
// ...
};
Ici on a un index d'une dimension (un seul paramètre int i) qui s'applique a un objet Table. Si on a
Table t;
on peut écrire
n = t[i] ; // i.e n = t.operator[] (i);
qui a pour résultat la valeur retour de la fonction membre, en l'occurrence l'élément d'indice i de la table t.
Une fonction operator[] peut donner aux indexes une vrai signification. En effet, on peut indexer par n'importe quel type, et faire des tables associatives: on donne à une classe, annuaire par exemple, une fonction
int operator[] (char* nom);
qui donne le numéro téléphone d'un nom donné
Annuaire annuaire;
tel = annuaire ["Ali"];
L'opérateur [] non plus ne peut pas être une fonction friend.
Exercice: Programmer une table ASCII de 128 entiers short,
telle que ASCII['c'] soit égale à la valeur ASCII du
caractère c.