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 :

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