Classe et constructeurs ✱

  • Your native language: with Chrome, right-click on the page and select « Translate into … »

  • English version:

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 WIZARD1_HP;
int WIZARD2_MP;
int WIZARD1_MP;
int WIZARD2_HP;
int WIZARD2_Gold;
int WIZARD1_Gold;

void Wizard1Fireball(...)    { ... }
void Wizard2Fireball(...)    { ... }
void LowerCastleGate()       { ... }
void Wizard1Chant(...)       { ... }
void Wizard2Chant(...)       { ... }
void MonsterAttack()         { ... }

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 Wizard
{
        int HP;
        int MP;
        int Gold;

        void Fireball()   { ... }
        void Chant()      { ... }
};

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 Wizard
{
        int Gold;
        int ManaPoints;
        int HealthPoints;

        void Fireball()
        {
                if (ManaPoints < 10) return;
                ManaPoints -= 10;
                std::cout << "Fireball!";
        }

        void HealSpell()
        {
                if (ManaPoints < 5) return;
                ManaPoints -= 5;
                HealthPoints += 50;
                std::cout << "I feel better!";
        }

};

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

int main()
{
        StructName instanceName;                            // call to the default constructor

        instanceName.fieldName                              // access to an instance field

        instanceName.methodName(param1, param2, ...)        // call to an instance method
}

Exemple:

int main()
{
        Car c;
        c.Start();
        int power = c.power;
}

Indépendance entre objets

Prenons l’exemple d’une structure compteur :

struct Counter
{
        int Value;
};

Créons deux instances de la structure Compteur :

Counter c1;
Counter 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.

Syntax

StructName(Type1 param1, Type2 param2, ...) { ... }

Exemple :

struct Point
{
        int x;
        int y;

        Point() // default constructor
        {
                x = 0;
                y = 0;
        }

        Point(int xx, int yy)  // parameterized constructor
        {
                x = xx;
                y = yy;
        }
};

La syntaxe utilisée pour appeler chacun de ces constructeurs est :

Syntax

Point p;         // call to the default constructor
Point p();       // call to the default constructor ⚠ looks like a function declaration
Point p(10,20);  // call to the parameterized constructor

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) {}  // parameterized constructor
};

int main()
{
   T1 a;   // no parameterized constructor => compiler provides a default one

   T2 b;   // <<== implicit call to the default constructor
           // 1 parameterized constructor present but no default constructor => ERROR
}

Message du compilateur :

../_images/error.PNG

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) // horizontal delegation to the parameterized constructor
        {
                std::cout << "I can do other actions here";
        }

        Point(int xx, int yy)  // parameterized constructor
        {
                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()   // default constructor
        {
                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; and int b = i;
        {
                std::cout << "I can do other actions here";
        }
};

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

Travail à rendre sur Github Classroom

Exercice 1

  • Créez un fichier nommé 3_struct_V2.cpp

  • Codez les fonctionnalités demandées ci-dessous et validez les tests donnés ci-dessous

  • Uploadez le fichier sur votre github

La classe V2 :

  • Créez tout d’abord une structure V2 stockant deux valeurs entières

    • Utilisez uniquement des valeurs entières

    • Ecrivez un constructeur par défaut qui initialise la position à (0,0)

    • Ecrivez son constructeur paramétrique

  • Ecrivez l’opérateur + entre deux objets V2 sous forme de fonction externe à la classe

    • Syntaxe : V2 operator+(const V2& v1, const V2& v2)

  • Ecrivez l’opérateur * d’un V2 par un entier sous forme de fonction externe à la classe

    • Syntaxe : V2 operator* (const V2& v1, int factor)

  • Validez les tests ci-dessous

  • Effectuez le test suivant :

    • Donnez deux V2 a et b et un entier k

    • Calculez le résultat de l’expression : (a+b)*k et stocker le dans un V2

    • Calculez le résultat de l’expression : (a*k+b*k) et stocker le dans un V2

    • Comparez les deux valeurs

int main()
{
        int t = 1;

        // 1) Default ctor -> (0,0)
        V2 d;
        if (d.x != 0 || d.y != 0) cout << "FAIL test 1";

        // 2) Parametric ctor -> (3,-2)
        V2 p(3, -2);
        if (p.x != 3 || p.y != -2) cout << "FAIL test 2";

        // 3) operator+ basic: (1,2)+(3,4)=(4,6)
        V2 a(1, 2), b(3, 4);
        V2 sum = a + b;
        if (sum.x != 4 || sum.y != 6) cout << "FAIL test 3";

        // 4) operator* scaling: (1,2)*4=(4,8)
        V2 s1 = a * 4;
        if (s1.x != 4 || s1.y != 8) cout << "FAIL test 4";

        // 5) Commutativity of +
        V2 ab1 = a + b, ab2 = b + a;
        if (ab1.x != ab2.x || ab1.y != ab2.y) cout << "FAIL test 5";

        return 0;
}

Exercice 2

  • Créez un fichier nommé 3_struct_rectangle.cpp

  • Copiez-collez votre classe V2 dans ce programme

  • Codez les fonctionnalités et les tests demandées

  • Uploadez le fichier sur votre github

La classe Rectangle :

  • Créez une structure Rectangle

    • Ajoutez des champs : Size (V2) et position P (V2)

  • Ajoutez un constructeur paramétrique qui initialise la taille et la position de l’objet à partir de deux V2 passées en paramètres.

  • Ajoutez une méthode d’instance Area() qui retourne l’aire du rectangle.

  • Dans la fonction main() :

    • Créez une nouvelle instance de la structure Rectangle de dimension 20 par 30 à la position (15,20).

    • Affichez les valeurs des champs de l’objet créé.

    • Affichez l’aire de l’objet créé en appelant la méthode Area().

    • Exécutez votre programme et vérifiez que tout fonctionne correctement.

  • Ajoutez un constructeur permettant de créer un carré à partir de 2 arguments : width (int) et pos (V2)

    • Vous devez utiliser le chaînage des constructeurs

    • Le corps de ce constructeur ne doit contenir aucune instruction

  • Ajoutez une méthode resizeDouble() qui multiplie la taille du rectangle par 2

    • Utilisez votre opérateur * dans la fonction resizeDouble()

    • Créez un carré en utilisant le nouveau constructeur

    • Doublez sa taille

    • Affichez les dimensions obtenues pour les vérifier

  • Créez une méthode Translate(…) qui déplace la position

    • Utilisez votre opérateur + dans la fonction Translate()

    • Effectuez un test