Classe et constructeurs
Choix pédagogiques
Le temps étant limité, nous n’abordons pas les aspects suivant :
Les niveaux de visibilité (public/protected/private)
Les accesseurs (getter / setter)
Le mot clef static
Les variables et les fonctions de classe
Pour rappel, les structures en C++ correspondent à des classes dont tous les membres sont d’accès public.
Dans le chapitre sur la compilation séparée, nous verrons comment séparer la définition et le corps d’une fonction membre.
Modélisation objets
Utilité
Si nous choisissions de représenter deux magiciens en utilisant différentes variables et fonctions, nous obtenons un code de la forme suivante :
int MAGE_PV;
int MAGE2_ PMagie;
int MAGE_ PMagie;
int MAGE2_PV;
int MAGE2_PiecesOR;
int MAGE_PiecesOR;
void Magicien1BouleDeFeu(...) {...}
void Magicien2BouleDeFeu(...) {...}
void BaisserGrilleChateau() {...}
void Magicien1Incantation(...) {...}
void Magicien2Incantation(...) {...}
void MonstreAttaque() {...}
Les membres du premier et du second magicien se retrouvent mélangés au même niveau dans une longue liste de variables et de fonctions. Il devient intéressant de regrouper les caractéristiques de chaque magicien afin d’améliorer la structuration et la lisibilité de votre programme. Ainsi, nous utilisons une structure pour regrouper toutes les informations au même endroit :
struct Magicien
{
int PV;
int PMagie;
int PiecesOR;
void BouleDeFeu() {...}
void Incantation() {...}
};
Instance
On utilise souvent le terme instance comme synonyme du mot objet. L’expression instancier une classe signifie créer un objet de cette classe. La création de cet objet s’appelle l’instanciation.
Les constructeurs
Les constructeurs sont des fonctions dédiées à l’initialisation d’une instance. Chaque constructeur est associée à une liste d’arguments différents proposant différentes manières d’initialiser une instance. Par exemple, un point dans le plan peut être construit à partir :
de deux valeurs (x,y)
d’un angle θ et d’un rayon r
d’un autre point
Parmi tous les constructeurs, il existe un constructeur spécial appelé constructeur par défaut (default constructor). Il correspond au constructeur appelé sans passer d’arguments.
Quelques repères
Conceptuellement, les classes sont des types au même titre que int ou string. Ainsi une classe est avant-tout une description / un modèle comme les autres types. En effet, le type int sert à préciser qu’une variable prend une certaine place en mémoire et qu’elle représente un nombre entier. Lorsque l’on écrit int total; l’objet créé existe en mémoire, le type int joue uniquement le rôle de modèle.
La classe sert à décrire un objet. Prenons le cas d’une pièce mécanique, un boulon par exemple, qui représente notre objet. Il existe un plan sur papier décrivant ce boulon. Ce plan correspond à la notion de classe. Ainsi, ce plan nous informe sur la taille du boulon, sa forme et sa matière. Mais ce plan n’est pas une pièce mécanique, c’est une description de cette pièce. Chaque boulon fabriqué à partir de ce plan respecte l’ensemble des caractéristiques énoncées dans le plan.
La classe est unique, ses objets sont multiples Grâce à ce plan, qui est unique, on peut fabriquer zéro ou plusieurs boulons.
Les objets sont indépendants les uns des autres. Les boulons vont avoir des existences indépendantes. Certains finiront dans un moteur de voiture, d’autres dans un avion. Certains mis dans des conditions difficiles vont se casser rapidement. D’autres vont rouiller, d’autres vont rester dans le stock…
Les objets peuvent avoir des attributs propres et ces attributs doivent être décrits dans leur classe. Par exemple, prenons un modèle de voiture comme la Peugeot 206 (voiture la plus répandue en France). Ce modèle est édité en proposant plusieurs couleurs de carrosserie et plusieurs types de moteur. Ainsi une 206 noire et une 206 blanche, représentent deux objets de couleur différente. Cependant ces deux voitures appartiennent à un même modèle de Peugeot 206. Il en va de même pour une version diesel ou essence.
Dans le langage courant, les notions d’objet et de classe sont implicites. Lorsque vous dites « je vais acheter une C3 », on parle ici du modèle de voiture (la classe). Lorsque vous dites « j’ai vendu ma C3 », on parle ici d’un objet : la voiture que vous avez possédée pendant plusieurs années.
Les objets peuvent avoir des méthodes. Les objets ne se limitent pas à des choses inertes comme les boulons. Par exemple, une voiture dispose de ses propres méthodes : démarrer, klaxonner, activer le chauffage, allumer les feux de route, activer l’essuie-glace…
Ne mélangez pas objet et classe. Lorsqu’une voiture active son chauffage, c’est la voiture en question (l’objet) qui effectue l’action. Ce n’est pas la classe voiture !
Terminologie
Cette section doit être relue plusieurs fois car elle présente une terminologie très répandue mais difficile à appréhender.
Variable et fonction d’instance
Nous utilisons le vocabulaire suivant :
On parle de variable/attribut/champ d’instance pour désigner le paramètre d’un objet.
On parle de fonction/méthode d’instance pour désigner une action effectuée par un objet.
Variable et fonction membres
Un membre de classe (class member) correspond à tout élément se trouvant dans le bloc de code décrivant une classe. On parle de donnée membre (data member) et de fonction/méthode membre (member function).
Exemples de membres d’une classe :
Variables et les fonctions d’instance
Les constructeurs
Les variables et les fonctions de classe (hors programme).
Avertissement
Petit rappel, les constructeurs ne sont pas des méthodes d’instance. D’une part, ils ne sont pas appelés depuis un objet. D’autre part, un constructeur intervient avant que l’objet existe.
Quizzz
Une classe peut avoir plusieurs constructeurs.
Une classe a toujours un constructeur.
Si le développeur ne déclare pas de constructeur sans argument alors le compilateur en ajoute un automatiquement.
Une classe permet de créer plusieurs objets de mêmes caractéristiques.
Tous les objets d’une même classe possèdent les mêmes champs et les mêmes méthodes d’instance.
Tous les champs des objets d’une même classe possèdent des valeurs identiques.
Variable d’instance est un synonyme de champ d’instance.
Les champs et les méthodes d’instance font parti des membres d’instance d’une classe.
Une classe doit contenir des champs d’instance.
Une classe doit contenir des méthodes d’instance.
Un constructeur est une fonction membre ayant pour type de retour void.
Le constructeur par défaut est le constructeur pouvant être appelé sans donner d’arguments.
Les notions de classe et de type sont similaires.
Créer une classe pour chaque élément d’un jeu permet d’obtenir une modélisation objet de qualité.
Dans un programme, je peux avoir 1 classe et 0 objet.
Instance et objet sont deux notions différentes.
On ne peut instancier qu’un seul objet à partir d’une classe.
Les constructeurs sont des fonctions d’instance.
Mise en place en C++
Syntaxe
La définition des champs et des méthodes d’instance se fait dans le corps de la structure comme habituellement :
struct Magicien
{
int PiecesOr;
int PointDeMagie;
int PointDeVie;
void BouleDeFeu()
{
if (PointDeMagie < 10) return;
PointDeMagie -= 10;
std::cout << "Boule de feu";
}
void SortDeSoin()
{
if (PointDeMagie < 5) return;
PointDeMagie -= 5;
PointDeVie += 50;
std::cout << "Je vais mieux !";
}
};
L’ordre de déclaration des membres d’une structure n’a pas d’importance. Par convention, on déclare les champs d’instance en premier, puis les méthodes d’instance. Dans le corps d’une fonction d’instance, pour modifier une variable d’instance il suffit d’écrire son nom. Ainsi dans notre exemple, nous écrivons : PointDeMagie -= 5; et PointDeVie += 50;.
Opérateur d’accès
Depuis le nom d’une instance, l'accès aux champs et méthodes d’instance, se fait à travers l’utilisation de l’opérateur d’accès (dot operator) :
Syntaxe
main()
{
NomStructure NomInstance; // appel du constructeur par défaut
NomInstance.NomDuChamp // accès à un champ d'instance
NomInstance.NomMethode(parametre1, parametre2, ...) // appel d'une méthode d'instance
}
Exemple:
main()
{
Voiture V;
V.Demarre();
int puissance = V.puissance;
}
Indépendance entre objets
Prenons l’exemple d’une structure compteur :
struct Compteur
{
int Value;
};
Créons deux instances de la structure Compteur :
Compteur c1;
Compteur c2;
Deux zones mémoires sont réservées pour stocker chaque instance. Les noms c1 et c2 sont associés à chaque objet. On peut représenter la mémoire de la façon suivante :
Si on exécute l’instruction:
c1.Value = 10;
L’opérateur d’accès aux membres .
appliqué au nom c1 indique que c’est le champ Value de cet objet qui va être modifié
pour la valeur 10. Le 2ème objet ne subit aucune modification et la mémoire devient :
Exercices
Si je change la valeur d’un champ d’instance, l’objet n’est plus considéré comme appartenant à sa classe d’origine.
Une fois l’objet créé,je ne peux plus modifier ses variables d’instance
Pour modifier un champ d’instance j’utilise le signe . après avoir écrit le nom de la classe.
Le signe . correspond à l’opérateur d’accès aux membres.
Constructeurs
D’une manière générale, les constructeurs doivent respecter cette règle :
REGLE : Un constructeur est une méthode spéciale sans type de retour et avec un nom identique à celui de sa classe.
Pour des raisons de performance, la convention suivante a été choisie :
CONVENTION : Les variables d’instance, si elles ne sont pas initialisées dans le code ont des valeurs indéterminées.
Syntaxe
NomStructure(Type1 Param1, Type1 Param2,...) {...}
Exemple :
struct Point
{
int x;
int y;
Point() // constructeur sans argument
{
x = 0;
y = 0;
}
Point(int xx, int yy) // constructeur paramétrique
{
x = x ;
y = y ;
}
};
La syntaxe utilisée pour appeler chacun de ces constructeurs est :
Syntaxe
Point P; // appel du constructeur sans argument
Point P(); // appel du constructeur sans argument
Point P(10,20); // appel du constructeur paramétrique
Constructeur par défaut
Parmi tous les constructeurs, il existe un constructeur spécial appelé constructeur par défaut (default constructor). Il correspond au constructeur appelé sans passer d’arguments. L’appel de ce constructeur est parfois implicite, notamment lorsque vous écrivez :
Point P;
Comment se fait-il que nous avons pu instancier des structures sans créer de constructeur par défaut ? Si aucun constructeur n’est fourni par la programmeur, dans ce cas précis, le compilateur C++ en fournit un d’office. Par contre, si vous écrivez un constructeur paramétrique mais pas de constructeur par défaut, alors, du point de vue du C++, c’est une erreur. Voici un exemple :
#include <iostream>
using namespace std;
struct T1
{
int x;
};
struct T2
{
int x;
T2(int a, int b) {} // constructeur paramétrique
};
int main()
{
T1 a; // aucun constructeur paramétrique => le compilateur en fournit un par défaut
T2 b; // <<== appel implicite du constructeur par défaut
// 1 ctr paramétrique présent mais 0 constructeur par défaut => ERREUR
}
Message du compilateur :
Chaînage horizontal des constructeurs
Lorsqu’une classe contient plusieurs constructeurs, le contenu de ces derniers est parfois similaire. Pour éviter de réécrire plusieurs fois le même code, on peut depuis un constructeur appeler un autre constructeur de la même structure (chaînage horizontal). Voici un exemple :
struct Point
{
int x;
int y;
Point() : Point(0,0) // chaînage horizontal vers le constructeur paramétrique
{
std::cout << "Je peux faire d'autres actions ici";
}
Point(int xx, int yy) // constructeur paramétrique
{
x = xx ;
y = yy ;
}
};
Note
Le constructeur chaîné est appelé avant le traitement du constructeur actuel.
Ainsi lorsque nous écrivons :
Point P;
Voici le déroulé des différentes appels :
Appel du constructeur sans argument Point()
Chaînage vers le constructeur Point(0,0);
Exécution de x = 0 et y = 0
Retour au constructeur sans argument
Exécution de son bloc de code : cout << « Je peux faire d’au…
Fin
Initialisation des variables d’instance
Plusieurs options sont possibles :
Initialisation dans le corps du constructeur
struct Point
{
int x;
int y;
Point()
{
x = y = 0;
}
}
Initialisation lors des définitions
struct Point
{
int x = 0;
int y = 0;
};
Initialisation par chaînage horizontal
struct test
{
int b;
int & r;
test(int &i) : r(i), b(i) // int & r = i; et int b = i;
{
std::cout << "Je peux faire d'autres actions ici";
}
};
Note
L’initialisation par chaînage est le seul moyen connu pour initialiser un champ référence.
Note
On trouve une syntaxe équivalente utilisant des accolades : b{i}.
struct B
{
int i;
int d;
bool b;
B(int ii, int dd)
{
b = (ii == 3);
i = ii;
d = dd;
}
B(int i) : B(i, i * 2)
{}
};
int main()
{
B test(1);
}
Donnez les trois valeurs des champs de l’objet test après son instanciation :
i
d
b