Accueil
Rechercher:
sur developpez.com sur les forums
Forums | Tutoriels | F.A.Q's | Participez | Hébergement | Contacts
Club Emploi Blogs   TV   Dév. Web PHP XML Python Autres 2D-3D-Jeux Sécurité Windows Linux PC Mac
Accueil Conception Java DotNET Visual Basic  C  C++ Delphi MS-Office SQL & SGBD Oracle  4D  Business Intelligence
FORUM C++ FAQs C++ TUTORIELS C++ LIVRES C++ COMPILATEURS C++ SOURCES C++ Qt

Le langage C++

Date de publication : 19 mars 2002 , Date de mise à jour : 7 Janvier 2008

Par Henri Garreta
 

Ce document est le support du cours sur le langage C++, considéré comme une extension de C (tel que normalisé par l'ISO en 1990), langage présumé bien connu.

               Version PDF   Version hors-ligne

I. Eléments préalables
I-A. Placement des déclarations de variables
I-B. Booléens
I-C. Références
I-C-1. Notion
I-C-2. Références paramètres des fonctions
I-C-3. Fonctions renvoyant une référence
I-C-4. Références sur des données constantes
I-C-5. Références ou pointeurs ?
I-D. Fonctions en ligne
I-E. Valeurs par défaut des arguments des fonctions
I-F. Surcharge des noms de fonctions
I-G. Appel et définition de fonctions écrites en C
I-H. Entrées-sorties simples
I-I. Allocation dynamique de mémoire
II. Classes
II-A. Classes et objets
II-B. Accès aux membres
II-B-1. Accès aux membres d'un objet
II-B-2. Accès à ses propres membres, accès à soi-même
II-B-3. Membres publics et privés
II-B-4. Encapsulation au niveau de la classe
II-B-5. Structures
II-C. Définition des classes
II-C-1. Définition séparée et opérateur de résolution de portée
II-C-2. Fichier d'en-tête et fichier d'implémentation
II-D. Constructeurs
II-D-1. Définition de constructeurs
II-D-2. Appel des constructeurs
II-D-3. Constructeur par défaut
II-E. Construction des objets membres
II-F. Destructeurs
II-G. Membres constants
II-G-1. Données membres constantes
II-G-2. Fonctions membres constantes
II-H. Membres statiques
II-H-1. Données membres statiques
II-H-2. Fonctions membres statiques
II-I. Amis
II-I-1. Fonctions amies
II-I-2. Classes amies
III. Surcharge des opérateurs
III-A. Principe
III-A-1. Surcharge d'un opérateur par une fonction membre
III-A-2. Surcharge d'un opérateur par une fonction non membre
III-B. Quelques exemples
III-B-1. Injection et extraction de données dans les flux
III-B-2. Affectation
III-C. Opérateurs de conversion
III-C-1. Conversion vers un type classe
III-C-2. Conversion d'un objet vers un type primitif
IV. Héritage
IV-A. Classes de base et classes dérivées
IV-B. Héritage et accessibilité des membres
IV-B-1. Membres protégés
IV-B-2. Héritage privé, protégé, public
IV-C. Redéfinition des fonctions membres
IV-D. Création et destruction des objets dérivés
IV-E. Récapitulation sur la création et destruction des objets
IV-E-1. Construction
IV-E-2. Destruction
IV-F. Polymorphisme
IV-F-1. Conversion standard vers une classe de base
IV-F-2. Type statique, type dynamique, généralisation
IV-G. Fonctions virtuelles
IV-H. Classes abstraites
IV-I. Identification dynamique du type
IV-I-1. L'opérateur dynamic cast
IV-I-2. L'opérateur typeid
V. Modèles (templates)
V-A. Modèles de fonctions
V-B. Modèles de classes
V-B-1. Fonctions membres d'un modèle
VI. Exceptions
VI-A. Principe et syntaxe
VI-B. Attraper une exception
VI-C. Déclaration des exceptions qu'une fonction laisse échapper
VI-D. La classe exception
Références


I. Eléments préalables

Ce document est le support du cours sur le langage C++, considéré comme une extension de C (tel que normalisé par l'ISO en 1990), langage présumé bien connu.

warning Attention, la présentation faite ici est déséquilibrée : des concepts importants ne sont pas expliqués, pour la raison qu'ils sont réalisés en C++ comme en C, donc supposés acquis. En revanche, nous insistons sur les différences entre C et C++ et notamment sur tous les éléments « orientés objets » que C++ ajoute à C.
Cette première section expose un certain nombre de notions qui, sans être directement liés à la méthodologie objets, font déjà apparaitre C++ comme une amélioration notable de C.


I-A. Placement des déclarations de variables

En C les déclarations de variables doivent apparaitre au début d'un bloc. En C++, au contraire, on peut mettre une déclaration de variable partout oµu on peut mettre une instruction 1.

Cette difference parait mineure, mais elle est importante par son esprit. Elle permet de repousser la déclaration d'une variable jusqu'à l'endroit du programme oµu l'on dispose d'assez éléments pour l'initialiser. On lutte aussi contre les variables « déjà déclarées mais non encore initialisées » qui sont un important vivier de bugs dans les programmes.

Exemple, l'emploi d'un fichier:
Version C

{
	FILE *fic;
	...
	obtention de nomFic, le nom du fichier à ouvrir
	Danger ! Tout emploi de fic ici est erroné
	...
	fic = fopen(nom, "w");
	...
	ici, l'emploi de fic est légitime
	...
	}
Version C++

{
	...
	obtention de nomFic, le nom du fichier à ouvrir
	ici pas de danger d'utiliser fic à tort
	...
	FILE *fic = fopen(nom, "w");
	...
	ici, l'emploi de fic est légitime
	...
}
1Bien entendu, une rµegle absolue reste en vigueur : une variable ne peut ^etre utilisée qu'aprµes qu'elle ait été déclarée.


I-B. Booléens

En plus des types définis par l'utilisateur (ou classes, une des notions fondamentales de la programmation orientée objets) C++ possède quelques types qui manquaient à C, notamment le type booléen et les types références.

Le type bool (pour booléen) comporte deux valeurs : false et true. Contrairement à C :
  • le résultat d'une opération logique (&&, ||, etc.) est de type booléen,
  • là ou une condition est attendue on doit mettre une expression de type booléen et
  • il est déconseillé de prendre les entiers pour des booléens et réciproquement.
Conséquence pratique : n'écrivez pas « if (x) ... » au lieu de « if (x != 0) ... »


I-C. Références


I-C-1. Notion

A côté des pointeurs, les références sont une autre manière de manipuler les adresses des objets placés dans la mémoire. Une référence est un pointeur géré de manière interne par la machine. Si T est un type donné, le type « référence sur T » se note T&. Exemple :

int i;
int & r = i; 		// r est une référence sur i	
Une valeur de type référence est une adresse mais, hormis lors de son initialisation, toute opération effectuée sur la référence agit sur l'objet référencé, non sur l'adresse. Il en découle qu'il est obligatoire d'initialiser une référence lors de sa création ; après, c'est trop tard :

r = j; 				// ceci ne transforme pas r en une référence
					// sur j mais copie la valeur de j dans i

I-C-2. Références paramètres des fonctions

L'utilité principale des références est de permettre de donner aux fonctions des paramètres modifiables, sans utiliser explicitement les pointeurs. Exemple :

void permuter(int & a, int & b) {
	int w = a;
	a = b; b = w;
	}
Lors d'un appel de cette fonction, comme

permuter(t[i], u);
ses paramètres formels a et b sont initialisés avec les adresses des paramètres effectifs t[i] et u, mais cette utilisation des adresses reste cachée, le programmeur n'a pas à s'en occuper. Autrement dit, à l'occasion d'un tel appel, a et b ne sont pas des variables locales de la fonction recevant des copies des valeurs des paramètres, mais d'authentiques synonymes des variables t[i] et u. Il en résulte que l'appel ci-dessus permute effectivement les valeurs des variables t[i] et u 2.


I-C-3. Fonctions renvoyant une référence

Il est possible d'écrire des fonctions qui renvoient une référence comme résultat. Cela leur permet d'être le membre gauche d'une affectation, ce qui donne lieu à des expressions élégantes et efficaces. Par exemple, voici comment une fonction permet de simuler un tableau indexé par des chaines de caractères 3:

char *noms[N];
int ages[N];

int & age(char *nom) {
	for (int i = 0; i < N; i++)
		if (strcmp(nom, noms[i]) == 0)
			return ages[i];
}
Une telle fonction permet l'écriture d'expressions qui ressemblent à des accès à un tableau dont les indices seraient des chaines :

age("Amélie") = 20;
ou encore

age("Benjamin")++;

I-C-4. Références sur des données constantes

Lors de l'écriture d'une fonction il est parfois souhaitable d'associer l'efficacité des arguments références (puisque dans le cas d'une référence il n'y a pas recopie de la valeur de l'argument) et la sécurité des arguments par valeur (qui ne peuvent en aucun cas être modifiés par la fonction). Cela peut se faire en déclarant des arguments comme des références sur des objets immodifiables, ou constants, à l'aide du qualifieur const :

void uneFonction(const unType & arg) {
	...
	ici arg n'est pas une recopie de l'argument effectif (puisque référence)
	mais il ne peut pas être modifié par la fonction (puisque const)
	...
}

I-C-5. Références ou pointeurs ?

Il existe quelques situations, nous les verrons plus loin, oµu les références se révèlent indispensables. Cependant, la plupart du temps elles font double emploi avec les pointeurs et il n'existe pas de critères simples pour choisir des références plutôt que des pointeurs ou le contraire.

Par exemple, la fonction permuter montrée à la page précédente peut s'écrire aussi :

void permuter(int *a, int *b) {
	int w = *a;
	*a = *b; *b = w;
}
son appel s'écrit alors

permuter(&t[i], &u);
warning Attention On notera que l'opérateur & (à un argument) a une signification très differente selon le contexte dans lequel il apparait :
  • employé dans une déclaration, comme dans

int &r = x;
il sert à indiquer un type référence : « r est une référence sur un int »

  • employé ailleurs que dans une déclaration il indique l'opération « obtention de l'adresse », comme dans l'expression suivante qui signifie « affecter l'adresse de x à p » :

p = &x;
  • enfin, on doit se souvenir qu'il y a en C++, comme en C, un opérateur & binaire (à deux arguments) qui exprime la conjonction bit-à-bit de deux mots-machine.
2Notez qu'une telle chose est impossible en C, oµu une fonction appelée par l'expression permuter(t[i], u) ne peut recevoir que des copies des valeurs de de t[i] et u.

3C'est un exemple simpliste, pour faire court nous n'y avons pas traité le cas de l'absence de la chaine cherchée.


I-D. Fonctions en ligne

Normalement, un appel de fonction est une rupture de séquence : à l'endroit oµu un appel figure, la machine cesse d'exécuter séquentiellement les instructions en cours ; les arguments de l'appel sont disposés sur la pile d'exécution, et l'exécution continue ailleurs, là oµu se trouve le code de la fonction.

Une fonction en ligne est le contraire de cela : là oµu l'appel d'une telle fonction apparait il n'y a pas de rupture de séquence. Au lieu de cela, le compilateur remplace l'appel de la fonction par le corps de celle-ci, en mettant les arguments affectifs à la place des arguments formels.

Cela se fait dans un but d'efficacité : puisqu'il n'y a pas d'appel, pas de préparation des arguments, pas de rupture de séquence, pas de retour, etc. Mais il est clair qu'un tel traitement ne peut convenir qu'à des fonctions fréquemment appelées (si une fonction ne sert pas souvent, à quoi bon la rendre plus rapide ?) de petite taille (sinon le code compilé risque de devenir démesurément volumineux) et rapides (si une fonction effectue une opération lente, le gain de temps obtenu en supprimant l'appel est négligeable).

En C++ on indique qu'une fonction doit être traitée en ligne en faisant précéder sa déclaration par le mot inline :

inline int abs(int x) {
	return x >= 0 ? x : -x;
	}
Les fonctions en ligne de C++ rendent le même service que les macros (avec arguments) de C, mais on notera que les fonctions en ligne sont plus fiables car, contrairement à ce qui se passe pour les macros, le compilateur peut y effectuer tous les contrôles syntaxiques et sémantiques qu'il fait sur les fonctions ordinaires.

La portée d'une fonction en ligne est réduite au fichier oµu la fonction est définie. Par conséquent, de telles fonctions sont généralement écrites dans des fichiers en-tête (fichiers « .h »), qui doivent être inclus dans tous les fichiers comportant des appels de ces fonctions.


I-E. Valeurs par défaut des arguments des fonctions

Les paramètres formels d'une fonction peuvent avoir des valeurs par défaut. Exemple :

void trier(void *table, int nbr, int taille = sizeof(void *), bool croissant = true);
Lors de l'appel d'une telle fonction, les paramètres effectifs correspondants peuvent alors être omis (ainsi que les virgules correspondantes), les paramètres formels seront initialisés avec les valeurs par défaut. Exemples :

trier(t, n, sizeof(int), false);
trier(t, n, sizeof(int)); 	// croissant = true
trier(t, n); 				// taille = sizeof(void *), croissant = true

I-F. Surcharge des noms de fonctions

La signature d'une fonction est la suite des types de ses arguments formels (et quelques éléments supplé- mentaires, que nous verrons plus loin). Le type du résultat rendu par la fonction ne fait pas partie de sa signature.

La surcharge des noms des fonctions consiste en ceci : en C++ des fonctions differentes peuvent avoir le même nom, à la condition que leurs signatures soient assez differentes pour que, lors de chaque appel, le nombre et les types des arguments effectifs permettent de choisir sans ambiguijté la fonction à appeler. Exemple :

int puissance(int x, int n) {
	calcul de xn avec x et n entiers
}
double puissance(double x, int n) {
	calcul de xn avec x flottant et n entier
}
double puissance(double x, double y) {
	calcul de xy avec x et y flottants
}
On voit sur cet exemple l'intérêt de la surcharge des noms des fonctions : la même notion abstraite « x à la puissance n » se traduit par des algorithmes très differents selon que n est entier (xn = x:x:::x) ou réel (xn = enlog x) ; de plus, pour n entier, si on veut que xn soit entier lorsque x est entier, on doit écrire deux fonctions distinctes, une pour x entier et une pour x réel.

Or le programmeur n'aura qu'un nom à connaitre, puissance. Il écrira dans tous les cas

c = puissance(a, b);
le compilateur se chargeant de choisir la fonction la plus adaptée, selon les types de a et b.

Notes.

  • 1. Le type du résultat rendu par la fonction ne fait pas partie de la signature. Par conséquent, on ne peut pas donner le même nom à deux fonctions qui ne different que par le type du résultat qu'elles rendent.
  • 2. Lors de l'appel d'une fonction surchargée, la fonction effectivement appelée est celle dont la signature correspond avec les types des arguments effectifs de l'appel. S'il y a une correspondance exacte, pas de problème. S'il n'y a pas de correspondance exacte, alors des règles complexes (trop complexes pur les expliquer ici) s'appliquent, pour déterminer la fonction à appeler. Malgré ces règles, il existe de nombreux cas de figure ambigus, que le compilateur ne peut pas résoudre.
Par exemple, si x est flottant et n entier, l'appel puissance(n, x) est erroné, alors qu'il aurait été correct s'il y avait eu une seule définition de la fonction puissance (les conversions entier $ flottant nécessaires auraient alors été faites).


I-G. Appel et définition de fonctions écrites en C

Pour que la compilation et l'édition de liens d'un programme avec des fonctions surchargées soient possibles, le compilateur C++ doit fabriquer pour chaque fonction un nom long comprenant le nom de la fonction et une représentation codée de sa signature ; c'est ce nom long qui est communiqué à l'éditeur de liens.

Or, les fonctions produites par un compilateur C n'ont pas de tels noms longs ; si on ne prend pas de disposition particulière, il sera donc impossible d'appeler dans un programme C++ une fonction écrite en C, ou réciproquement. On remédie à cette impossibilité par l'utilisation de la déclaration « extern "C" » :

extern "C" {
		déclarations et définitions d'éléments
		dont le nom est représenté à la manière de C
}

I-H. Entrées-sorties simples

Cette section traite de l'utilisation simple des flux standard d'entrée-sortie, c'est-à-dire la manière de faire en C++ les opérations qu'on fait habituellement en C avec les fonctions printf et scanf.

Un programme qui utilise les flux standard d'entrée-sortie doit comporter la directive

#include <iostream.h>
ou bien, si vous utilisez un compilateur récent et que vous suivez de près les recommandations de la norme 4 :

#include <iostream>
using namespace std;
Les flux d'entrée-sortie sont représentés dans les programmes par les trois variables, prédéclarées et préini- tialisées, suivantes :

  • cin, le flux standard d'entrée (l'équivalent du stdin de C), qui est habituellement associé au clavier du poste de travail,
  • cout, le flux standard de sortie (l'équivalent du stdout de C), qui est habituellement associé à l'écran du poste de travail,
  • cerr, le flux standard pour la sortie des messages d'erreur (l'équivalent du stderr de C), également associé à l'écran du poste de travail.
Les écritures et lectures sur ces unités ne se font pas en appelant des fonctions, mais à l'aide des opérateurs <<, appelé opérateur d'injection (« injection » de données dans un flux de sortie), et >>, appelé opérateur d'extraction (« extraction » de données d'un flux d'entrée). Or, le mécanisme de la surcharge des opérateurs (voir la section 3) permet la détection des types des données à lire ou à écrire. Ainsi, le programmeur n'a pas à s'encombrer avec des spécifications de format.

La syntaxe d'un injection de donnée sur la sortie standard cout est :

cout << expression à écrire
le résultat de cette expression est l'objet cout lui-même. On peut donc lui injecter une autre donnée, puis encore une, etc. :

((cout << expression ) << expression ) << expression
ce qui, l'opérateur << étant associatif à gauche, se note aussi, de manière bien plus agréable :

cout << expression << expression << expression
Le même procédé existe avec l'extraction depuis cin. Par exemple, le programme suivant est un programme C++ complet. Il calcule xn (pour x flottant et n entier).

#include <iostream.h>

double puissance(double x, int n) {
	algorithme de calcul de xn
}

void main() {
	double x;
	int n;
	cout << "Donne x et n : ";
	cin >> x >> n;
	cout << x << "^" << n << " = " << puissance(x, n) << "\n";
}
Exemple d'exécution de ce programme :

Donne x et n : 2 10
2^10 = 1024	
4C'est-à-dire, si vous utilisez les espaces de noms, ou namespace (tous les éléments de la bibliothµeque standard sont dans l'espace de noms std).


I-I. Allocation dynamique de mémoire

Des differences entre C et C++ existent aussi au niveau de l'allocation et de la restitution dynamique de mémoire.

Les fonctions malloc et free de la bibliothèque standard C sont disponibles en C++. Mais il est fortement conseillé de leur préférer les opérateurs new et delete. La raison principale est la suivante : les objets créés à l'aide de new sont initialisés à l'aide des constructeurs (cf. section 2.4) correspondants, ce que ne fait pas malloc. De même, les objets liberés en utilisant delete sont finalisés en utilisant le destructeur (cf. section 2.6) de la classe correspondante, contrairement à ce que fait free.

  • Pour allouer un unique objet : - new type
  • Pour allouer un tableau de n objets : - new type[n]
Dans les deux cas, new renvoie une valeur de type pointeur sur un type, c'est-à-dire « type * ». Exemples (on suppose que Machin est un type défini par ailleurs) :

Machin *ptr = new Machin; 		// un objet Machin
int *tab = new int[n]; 			// un tableau de n int
Si type est une classe possédant un constructeur par défaut, celui-ci sera appelé une fois (cas de l'allocation d'un objet simple) ou n fois (allocation d'un tableau d'objets) pour construire l'objet ou les objets alloués. Si type est une classe sans constructeur par défaut, une erreur sera signalée par le compilateur.

Pour un tableau, la dimension n peut être donnée par une variable, c'est-à-dire être inconnue lors de la compilation, mais la taille des composantes doit être connue. Il en découle que dans le cas d'un tableau à plusieurs indices, seule la première dimension peut être non constante :

double (*M)[10]; 			// pointeur de tableaux de 10 double
...
acquisition de la valeur de n
...
M = new double[n][10]; 		// allocation d'un tableau de n tableaux de 10 double
M pourra ensuite être utilisé comme une matrice à n lignes et 10 colonnes. L'opérateur delete restitue la mémoire dynamique. Si la valeur de p a été obtenue par un appel de new, on écrit

delete p;
dans le cas d'un objet qui n'est pas un tableau, et

delete [] p;
si ce que p pointe est un tableau. Les conséquences d'une utilisation de delete là oµu il aurait fallu utiliser delete[], ou inversement, sont imprévisibles.


II. Classes


II-A. Classes et objets

Un objet est constitué par l'association d'une certaine quantité de mémoire, organisée en champs, et d'un ensemble de fonctions, destinées principalement à la consultation et la modification des valeurs de ces champs. La définition d'un type objet s'appelle une classe. D'un point de vue syntaxique, cela ressemble beaucoup à une définition de structure, sauf que

  • le mot réservé class remplace5 le mot struct,
  • certains champs de la classe sont des fonctions.
Par exemple, le programme suivant est une première version d'une classe Point destinée à représenter les points affichés dans une fenêtre graphique :

class Point {
public:
	void afficher() {
		cout << '(' << x << ',' << y << ')';
	}
	void placer(int a, int b) {
		validation des valeurs de a et b;
		x = a; y = b;
	}
private:
	int x, y;
};
Chaque objet de la classe Point comporte un peu de mémoire, composée de deux entiers x et y, et de deux fonctions : afficher, qui accède à x et y sans les modifier, et placer, qui change les valeurs de x et y.

L'association de membres et fonctions au sein d'une classe, avec la possibilité de rendre privés certains d'entre eux, s'appelle l'encapsulation des données. Intérêt de la démarche : puisqu'elles ont été déclarées privées, les coordonnées x et y d'un point ne peuvent être modifiées autrement que par un appel de la fonction placer sur ce point. Or, en prenant les précautions nécessaires lors de l'écriture de cette fonction (ce que nous avons noté « validation des valeurs de a et b ») le programmeur responsable de la classe Point peut garantir aux utilisateurs de cette classe que tous les objets crées auront toujours des coordonnées correctes. Autrement dit : chaque objet peut prendre soin de sa propre cohérence interne.

Autre avantage important : on pourra à tout moment changer l'implémentation (i.e. les détails internes) de la classe tout en ayant la certitude qu'il n'y aura rien à changer dans les programmes qui l'utilisent.

info Note. Dans une classe, les déclarations des membres peuvent se trouver dans un ordre quelconque, même lorsque ces membres se référencent mutuellement. Dans l'exemple précédent, le membre afficher mentionne les membres x et y, dont la définition se trouve après celle de afficher.
Jargon. On appelle
  • objet une donnée d'un type classe ou structure,
  • fonction membre un membre d'une classe qui est une fonction6,
  • donnée membre un membre qui est une variable7.
5En fait on peut aussi utiliser struct, voyez la section 2.2.5.

6Dans la plupart des langages orientés objets, les fonctions membres sont appelées méthodes.

7Dans beaucoup de langages orientés objets, les données membres sont appelées variables d'instance et aussi, sous certaines conditions, propriétés


II-B. Accès aux membres


II-B-1. Accès aux membres d'un objet

On accède aux membres des objets en C++ comme on accède aux membres des structures en C. Par exemple, à la suite de la définition de la classe Point donnée précédemment on peut déclarer des variables de cette classe en écrivant 8 :

Point a, b, *pt; 		// deux points et un pointeur de point
Dans un contexte oµu le droit de faire un tel accès est acquis (cf. section 2.2.3) l'accès aux membres du point a s'écrit :

a.x = 0; 				// un accès bien écrit au membre x du point a
d = a.distance(b); 		// un appel bien écrit de la fonction distance de l'objet a
Si on suppose que le pointeur pt a été initialisé, par exemple par une expression telle que

pt = new Point; 		// allocation dynamique d'un point
alors des accès analogues aux précédents s'écrivent :

pt->x = 0; 				// un accès bien écrit au membre x du point pointé par pt
d = pt->distance(b); 	// un appel de la fonction distance de l'objet pointé par pt
A propos de l'accès à un membre d'un objet, deux questions se posent. Il faut comprendre qu'elles sont tout à fait indépendantes l'une de l'autre :

  • l'accès est-il bien écrit ? c'est-à-dire, désigne-t-il bien le membre voulu de l'objet voulu ?
  • cet accès est-il légitime ? c'est-à-dire, dans le contexte oµu il est écrit, a-t-on le droit d'accès sur le membre
  • en question ? La question des droits d'accès est traitée à la section 2.2.3.
8Notez que, une fois la classe déclarée, il n'est pas obligatoire d'écrire class devant Point pour y faire référence.


II-B-2. Accès à ses propres membres, accès à soi-même

Quand des membres d'un objet apparaissent dans une expression écrite dans une fonction du même objet on dit que ce dernier fait un accès à ses propres membres. On a droit dans ce cas à une notation simplifiée : on écrit le membre tout seul, sans expliciter l'objet en question. C'est ce que nous avons fait dans les fonctions de la classe Point :

class Point {
	...
	void afficher() {
		cout << '(' << x << ',' << y << ')';
	}
	...
};
Dans la fonction afficher, les membres x et y dont il question sont ceux de l'objet à travers lequel on aura appelé cette fonction. Autrement dit, lors d'un appel comme

unPoint.afficher();
le corps de cette fonction sera équivalent à

cout << '(' << unPoint.x << ',' << unPoint.y << ')';
Accès à soi-même. Il arrive que dans une fonction membre d'un objet on doive faire référence à l'objet (tout entier) à travers lequel on a appelé la fonction. Il faut savoir que dans une fonction membre 9 on dispose de la pseudo variable this qui représente un pointeur vers l'objet en question. Par exemple, la fonction afficher peut s'écrire de manière équivalente, mais cela n'a aucun intérêt :

void afficher() {
	cout << '(' << this->x << ',' << this->y << ')';
}
Pour voir un exemple plus utile d'utilisation de this imaginons qu'on nous demande d'ajouter à la classe Point deux fonctions booléennes, une pour dire si deux points sont égaux, une autre pour dire si deux points sont le même objet. Dans les deux cas le deuxième point est donné par un pointeur :

class Point {
	...
	bool pointEgal(Point *pt) {
	return pt->x == x && pt->y == y;
	}
	bool memePoint(Point *pt) {
	return pt == this;
	}
	...
};	
9Sauf dans le cas d'une fonction membre statique, voir la section 2.8.


II-B-3. Membres publics et privés

Par défaut, les membres des classes sont privés. Les mots clés public et private permettent de modifier les droits d'accès des membres :

class nom {
	les membres déclarés ici sont privés
public:
	les membres déclarés ici sont publics
private:
	les membres déclarés ici sont privés
etc.
};
Les expressions public: et private: peuvent apparaitre un nombre quelconque de fois dans une classe. Les membres déclarés après private: (resp. public:) sont privés (resp. publics) jusqu'à la fin de la classe, ou jusqu'à la rencontre d'une expression public: (resp. private:).

Un membre public d'une classe peut être accédé partout oµu il est visible ; un membre privé ne peut être accédé que depuis une fonction membre de la classe (les notions de membre protégé, cf. section 4.2.1, et de classes et fonctions amies, cf. section 2.9, nuanceront cette affirmation).

Si p est une expression de type Point :
  • dans une fonction qui n'est pas membre ou amie de la classe Point, les expressions p.x ou p.y pourtant syntaxiquement correctes et sans ambiguijté, constituent des accès illégaux aux membres privés x et y de la classe Point,
  • les expressions p.afficher() ou p.placer(u, v) sont des accès légaux aux membres publics afficher et placer, qui se résolvent en des accès parfaitement légaux aux membres p.x et p.y.

II-B-4. Encapsulation au niveau de la classe

Les fonctions membres d'une classe ont le droit d'accéder à tous les membres de la classe : deux objets de la même classe ne peuvent rien se cacher. Par exemple, le programme suivant montre notre classe Point augmentée d'une fonction pour calculer la distance d'un point à un autre :

class Point {
public:
	void afficher() {
		cout << '(' << x << ',' << y << ')';
	}
	void placer(int a, int b) {
		validation des valeurs de a et b;
		x = a; y = b;
	}
	double distance(Point autrePoint) {
		int dx = x - autrePoint.x;
		int dy = y - autrePoint.y;
		return sqrt(dx * dx + dy * dy);
}
private:
	int x, y;
};
Lors d'un appel tel que p.distance(q) l'objet p accède aux membres privés x et y de l'objet q. On dit que C++ pratique l'encapsulation au niveau de la classe, non au niveau de l'objet.

On notera au passage que, contrairement à d'autres langages orientés objets, en C++ encapsuler n'est pas cacher mais interdire. Les usagers d'une classe voient les membres privés de cette dernière, mais ne peuvent pas les utiliser.


II-B-5. Structures

Une structure est la même chose qu'une classe mais, par défaut, les membres y sont publics. Sauf pour ce qui touche cette question, tout ce qui sera dit dans la suite à propos des classes s'appliquera donc aux structures :

struct nom {
	les membres déclarés ici sont publics
private:
	les membres déclarés ici sont privés
public:
	les membres déclarés ici sont publics
etc.
};

II-C. Définition des classes


II-C-1. Définition séparée et opérateur de résolution de portée

Tous les membres d'une classe doivent être au moins déclarés à l'intérieur de la formule classnom{...} ; qui constitue la déclaration de la classe.

Cependant, dans le cas des fonctions, aussi bien publiques que privées, on peut se limiter à n'écrire que leur en-tête à l'intérieur de la classe et définir le corps ailleurs, plus loin dans le même fichier ou bien dans un autre fichier.

Il faut alors un moyen pour indiquer qu'une définition de fonction, écrite en dehors de toute classe, est en réalité la définition d'une fonction membre d'une classe. Ce moyen est l'opérateur de résolution de portée, dont la syntaxe est

						NomDeClasse::
Par exemple, voici notre classe Point avec la fonction distance définie séparément :

class Point {
public:
	...
	double distance(Point autrePoint);
	...
}
Il faut alors, plus loin dans le même fichier ou bien dans un autre fichier, donner la définition de la fonction « promise » dans la classe Point. Cela s'écrit :

double Point::distance(Point autrePoint) {
	int dx = x - autrePoint.x;
	int dy = y - autrePoint.y;
	return sqrt(dx * dx + dy * dy);
};
Définir les fonctions membres à l'extérieur de la classe allège la définition de cette dernière et la rend plus compacte. Mais la question n'est pas qu'esthétique, il y a une différence de taille : les fonctions définies à l'intérieur d'une classe sont implicitement qualifiées « en ligne » (cf. section 1.4).

Conséquence : la plupart des fonctions membres seront définies séparément. Seules les fonctions courtes, rapides et fréquemment appelées mériteront d'être définies dans la classe.


II-C-2. Fichier d'en-tête et fichier d'implémentation

En programmation orientée objets, « programmer » c'est définir des classes. Le plus souvent ces classes sont destinées à être utilisées dans plusieurs programmes, présents et à venir10. Se pose alors la question : comment disposer le code d'une classe pour faciliter son utilisation ?

Voici comment on procède généralement :
  • les définitions des classes se trouvent dans des fichiers en-tête (fichiers « .h », « .hpp », etc.),
  • chacun des ces fichiers en-tête contient la définition d'une seule classe ou d'un groupe de classes intimement liées ; par exemple, la définition de notre classe Point pourrait constituer un fichier Point.h
  • les définitions des fonctions membres qui ne sont pas définies à l'intérieur de leurs classes sont écrites dans des fichiers sources (fichiers « .cpp » ou « .cp »),
  • aux programmeurs utilisateurs de ces classes sont distribués :
    • les fichiers « .h »
    • le fichiers objets résultant de la compilation des fichiers « .cpp »
Par exemple, voici les fichiers correspondants à notre classe Point (toujours très modeste) :

Fichier Point.h :
	class Point {
	public:
		void placer(int a, int b) {
			validation de a et b
			x = a; y = b;
	}
	double distance(Point autrePoint);
private:
	int x, y;
};

Fichier Point.cpp :
	#include "Point.h"
	#include <math.h>
	
	double Point::distance(Point autrePoint) {
		int dx = x - autrePoint.x;
		int dy = y - autrePoint.y;
		return sqrt(dx * dx + dy * dy);
}
La compilation du fichier Point.cpp produira un fichier objet (nommé généralement Point.o ou Point.obj).

Dans ces conditions, la « distribution » de la classe Point sera composée des deux fichiers Point.h et Point.obj, ce dernier ayant éventuellement été transformé en un fichier bibliothèque (nommé alors Point.lib ou quelque chose comme »ca). Bien entendu, tout programme utilisateur de la classe Point devra comporter la directive

	#include "Point.h"
et devra, une fois compilé, être relié au fichier Point.obj ou Point.lib.

10La réutilisabilité du code est une des motivations de la méthodologie orientée objets.


II-D. Constructeurs


II-D-1. Définition de constructeurs

Un constructeur d'une classe est une fonction membre spéciale qui :
  • a le même nom que la classe,
  • n'indique pas de type de retour,
  • ne contient pas d'instruction return.
Le rôle d'un constructeur est d'initialiser un objet, notamment en donnant des valeurs à ses données membres.

Le constructeur n'a pas à s'occuper de trouver l'espace pour l'objet ; il est appelé (immédiatement) après que cet espace ait été obtenu, et cela quelle que soit la sorte d'allocation qui a été faite : statique, automatique ou dynamique, cela ne regarde pas le constructeur. Exemple :

class Point {
public:
	Point(int a, int b) {
		validation des valeurs de a et b
		x = a; y = b;
	}
	... autres fonctions membres ...
private:
	int x, y;
};
Un constructeur de la classe est toujours appelé, explicitement (voir ci-dessous) ou implicitement, lorsqu'un objet de cette classe est créé, et en particulier chaque fois qu'une variable ayant cette classe pour type est définie.

C'est le couple définition de la variable + appel du constructeur qui constitue la réalisation en C++ du concept « création d'un objet ». L'intérêt pour le programmeur est évident : garantir que, dès leur introduction dans un programme, tous les objets sont garnis et cohérents, c'est-à-dire éviter les variables indéfinies, au contenu incertain.

Une classe peut posséder plusieurs constructeurs, qui doivent alors avoir des signatures différentes :

class Point {
public:
	Point(int a, int b) {
		validation de a et b
		x = a; y = b;
	}
	Point(int a) {
		validation de a
		x = a; y = 0;
	}
	Point() {
		x = y = 0;
	}
	...
private:
	int x, y;
};
L'emploi de paramètres avec des valeurs par défaut permet de grouper des constructeurs. La classe suivante possède les mêmes constructeurs que la précédente :

class Point {
public:
	Point(int a = 0, int b = 0) {
		validation de a et b
		x = a; y = b;
	}
	...
private:
	int x, y;
};
Comme les autres fonctions membres, les constructeurs peuvent être déclarés dans la classe et définis ailleurs. Ainsi, la classe précédente pourrait s'écrire également

class Point {
public:
	Point(int a = 0, int b = 0);
	...
private:
	int x, y;
};
et, ailleurs :

Point::Point(int a, int b) {
	validation de a et b
	x = a; y = b;
}
Deux remarques générales.
  1. Comme l'exemple ci-dessus le montre, lorsqu'une fonction fait l'objet d'une déclaration et d'une définition séparées, comme le constructeur Point, les éventuelles valeurs par défaut des argument concernent la déclaration, non la définition.
  2. Lorsqu'une fonction fait l'objet d'une déclaration et d'une définition séparées, les noms des arguments ne sont utiles que pour la définition. Ainsi, la déclaration du constructeur Point ci-dessus peut s'écrire également :

class Point {
		...
		Point(int = 0, int = 0);
		...
};

II-D-2. Appel des constructeurs

Un constructeur est toujours appelé lorsqu'un objet est crée, soit explicitement, soit implicitement. Les appels explicites peuvent être écrits sous deux formes :

Point a(3, 4);
Point b = Point(5, 6);
Dans le cas d'un constructeur avec un seul paramètre, on peut aussi adopter une forme qui rappelle l'initia- lisation des variables de types primitifs (à ce propos voir aussi la section 3.3.1) :

Point e = 7; // équivaut à : Point e = Point(7)
Un objet alloué dynamiquement est lui aussi toujours initialisé, au mois implicitement. Dans beaucoup de cas il peut, ou doit, être initialisé explicitement. Cela s'écrit :

Point *pt;
...
pt = new Point(1, 2);
Les constructeurs peuvent aussi être utilisés pour initialiser des objets temporaires, anonymes. En fait, chaque fois qu'un constructeur est appelé, un objet nouveau est crée, même si cela ne se passe pas à l'occasion de la définition d'une variable. Par exemple, deux objets sans nom, représentant les points (0,0) et (3,4), sont créés dans l'instruction suivante 11 :

cout << Point(0, 0).distance(Point(3, 4)) << "\n";
info Note. L'appel d'un constructeur dans une expression comportant un signe = peut prêter à confusion, à cause de sa ressemblance avec une affectation. Or, en C++, l'initialisation et l'affectation sont deux opérations distinctes, du moins lorsqu'elles concernent des variables d'un type classe : l'initialisation consiste à donner une première valeur à une variable au moment oµu elle commence à exister ; l'affectation consiste à remplacer la valeur courante d'une variable par une autre valeur ; les opérations mises en ¾uvre par le compilateur, constructeur dans un cas, opérateur d'affectation dans l'autre, ne sont pas les mêmes.
Comment distinguer le « = » d'une affectation de celui d'une initialisation ? Grossièrement, lorsque l'expres- sion commence par un type, il s'agit d'une définition et le signe = correspond à une initialisation. Exemple :

Point a = Point(1, 2); 		// Initialisation de a
Cette expression crée la variable a et l'initialise en rangeant dans a.x et a.y les valeurs 1 et 2. En revanche, lorsque l'expression ne commence pas par un type, il s'agit d'une affectation. Exemple :

Point a;
...
a = Point(1, 2); 			// Affectation de a
L'expression ci-dessus est une affectation ; elle crée un point anonyme de coordonnées (1,2) et le recopie sur la variable a en remplacement de la valeur courante de cette variable, construite peu avant. On arrive au même résultat que précédemment, mais au prix de deux initialisations et une affectation à la place d'une seule initialisation.

11Ces objets anonymes ne pouvant servir à rien d'autre dans ce programme, ils seront détruits lorsque cette instruction aura été exécutée


II-D-3. Constructeur par défaut

Le constructeur par défaut est un constructeur qui peut être appelé sans paramètres : ou bien il n'en a pas, ou bien tous ses paramètres ont des valeurs par défaut. Il joue un rôle remarquable, car il est appelé chaque fois qu'un objet est créé sans qu'il y ait appel explicite d'un constructeur, soit que le programmeur ne le juge pas utile, soit qu'il n'en a pas la possibilité :

Point x; 					// équivaut à : Point x = Point()
Point t[10]; 				// produit 10 appels de Point()
Point *p = new Point; 		// équivaut à : p = new Point()
Point *q = new Point[10]; 	// produit 10 appels