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

  • L’héritage

  • Les pointeurs et le polymorphisme

  • 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 :

../_images/img1.png

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 :

../_images/img2.png

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;
   T1(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 :

../_images/error.PNG

Chaînage 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 chaîner les constructeurs en appelant un constructeur à partir d’un autre constructeur. Voici un exemple :

struct Point
{
        int x;
        int y;

        Point() : Point(0,0) // chaînage 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

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

Un mot sur l’héritage

Dans les années 90, la programmation orientée objet est en plein essor. Comme le dit Stroustrup dans sa vidéo d’introduction au C++, chaque chose peut être représentée par une classe. Modéliser le monde en créant une classe par type d’objets est assez simple et intuitif. Cependant, aujourd’hui, on revient sur cette approche notamment dans le monde du jeu vidéo.

Par exemple, pour un jeu de rôle, on peut créer une classe Magicien, une classe Barbare et une classe Elfe. Cependant, si l’on veut gérer un magicien elfe, cette modélisation est insuffisante. On poussera la POO jusqu’à pouvoir fusionner différentes classes pour en faire de nouvelles (mécanisme d’héritage multiple). Nous pourrons ainsi construire un classe HumainMagicien et un classe ElfeMagicien. Nous sommes sauvés ! Peut-être, mais pas pour longtemps…

En effet, si l’on prend une photo à un instant t de nos personnages, le modèle objet que l’on a conçu fonctionne. Mais si l’on regarde ces personnages évoluer sur plusieurs heures de jeu, le modèle objet risque d’être mis à l’épreuve. En effet, un personnage HumainMagicien peut perdre ses pouvoirs magiques. Comment traiter ce cas ? Une astuce consiste à mettre ses points de magie à 0 pour éviter qu’il fasse de la magie. Mais si on peut modéliser un Humain par un HumainMagicien en mettant sa capacité de magie à 0, alors à quoi sert la classe Humain ? Bref, construire une hiérarchie de classes n’est pas forcément une bonne idée dans ce contexte. Avec l’expérience, il s’avère finalement plus simple et plus flexible de gérer une classe Personnage avec une liste de capacités pouvant évoluer au fil du temps.

La logique est la même pour les objets du jeu. Il semble incongrue de créer une classe Epée, puis Tonneau et encore Charette ! Vu la quantité d’objets présents dans un jeu MMORPG, cela va générer plusieurs milliers de classes ! Apparaît ici encore la tendance d’avoir une classe générique comme ElementDuJeu pouvant modéliser les différents objets présents dans le jeu. Chaque objet ElementDuJeu aura comme paramètres : une géométrie, une texture et une liste de clefs/valeurs décrivant ses caractéristiques.

Avec cette approche, nous avons maintenant deux classes assez générales : Personnage et ElementDuJeu. Faut-il encore aller plus loin en ne faisant plus qu’une seule classe ? Nous vous laissons réfléchir à cette question !