Class & constructors ******************** .. include:: ../BoutonGoogleTrad.rst 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 : .. code-block:: cpp 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 : .. code-block:: cpp struct Wizard { int HP; int MP; int Gold; void Fireball() { ... } void Chant() { ... } }; Instance -------- On utilise souvent le terme **instance** (:cppterm:`ìnstance`) comme synonyme du mot objet. L'expression **instancier une classe** (:cppterm:`instantiate a class`) signifie créer un objet de cette classe. La création de cet objet s'appelle **l'instanciation** (:cppterm:`instantiation`). Les constructeurs ----------------- Les **constructeurs** (:cppterm:`constructors`) 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 :cpp:`(x,y)` * d'un angle :cpp:`θ` et d'un rayon :cpp:`r` * d'un autre point Parmi tous les constructeurs, il existe un constructeur spécial appelé **constructeur par défaut** (:cppterm:`default constructor`) `voir ici `_ 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 :cpp:`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 :cpp:`int total;` l'objet créé existe en mémoire, le type :cpp:`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 ------------------------------- Dans le cadre de la programmation objet, vous rencontrez la terminologie suivante : * On parle de **variable/attribut/champ d'instance** [instance variable, instance attribute, instance field], pour désigner le paramètre d'un objet. * On parle de **fonction/méthode d'instance** [instance method, instance function] pour désigner une action effectuée par un objet. Le C++ utilise sa propre terminologie qui suit la même logique : * variable/attribut d'instance :cppterm:`data member` * fonction/méthode d'instance :cppterm:`member function` Un **membre de classe** (:cppterm:`class member`) (`voir cette page `_) correspond à tout élément se trouvant dans le bloc de code décrivant une classe. .. warning:: Les membres de classe représentent tous les éléments présents dans le bloc de code de la classe, c'est une définition très englobante. Les membres de classe inclut les variables et fonctions d'instance mais aussi d'autres éléments comme les constructeurs par exemple. .. note:: 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 ====== .. quiz:: POO :title: Programmation Orientée Objet * :quiz:`{"type":"TF","answer":"T"}` Une classe peut avoir plusieurs constructeurs. * :quiz:`{"type":"TF","answer":"T"}` Une classe a toujours un constructeur. * :quiz:`{"type":"TF","answer":"F"}` Si le développeur ne déclare pas de constructeur sans argument alors le compilateur en ajoute un automatiquement. * :quiz:`{"type":"TF","answer":"T"}` Une classe permet de créer plusieurs objets de mêmes caractéristiques. * :quiz:`{"type":"TF","answer":"T"}` Tous les objets d'une même classe possèdent les mêmes champs et les mêmes méthodes d'instance. * :quiz:`{"type":"TF","answer":"F"}` Tous les champs des objets d'une même classe possèdent des valeurs identiques. * :quiz:`{"type":"TF","answer":"T"}` Variable d'instance est un synonyme de champ d'instance. * :quiz:`{"type":"TF","answer":"T"}` Les champs et les méthodes d'instance font parti des membres d'instance d'une classe. * :quiz:`{"type":"TF","answer":"F"}` Une classe doit contenir des champs d'instance. * :quiz:`{"type":"TF","answer":"F"}` Une classe doit contenir des méthodes d'instance. * :quiz:`{"type":"TF","answer":"F"}` Un constructeur est une fonction membre ayant pour type de retour void. * :quiz:`{"type":"TF","answer":"T"}` Le constructeur par défaut est le constructeur pouvant être appelé sans donner d'arguments. * :quiz:`{"type":"TF","answer":"T"}` Les notions de classe et de type sont similaires. * :quiz:`{"type":"TF","answer":"F"}` Créer une classe pour chaque élément d'un jeu permet d'obtenir une modélisation objet de qualité. * :quiz:`{"type":"TF","answer":"T"}` Dans un programme, je peux avoir 1 classe et 0 objet. * :quiz:`{"type":"TF","answer":"F"}` Instance et objet sont deux notions différentes. * :quiz:`{"type":"TF","answer":"F"}` On ne peut instancier qu'un seul objet à partir d'une classe. * :quiz:`{"type":"TF","answer":"F"}` Les constructeurs sont des fonctions d'instance. .. code-block:: cpp struct Vector2D { double x,y; void print() {...} }; * :quiz:`{"type":"TF","answer":"T"}` La variable :cpp:`x` est un :cpp:`data member`. * :quiz:`{"type":"TF","answer":"T"}` La fonction :cpp:`print()` est une :cpp:`member function`. * :quiz:`{"type":"TF","answer":"T"}` La fonction :cpp:`print()` et la variable :cpp:`x` sont des :cpp:`class members`. 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 : .. code-block:: cpp 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 : :cpp:`PointDeMagie -= 5;` et :cpp:`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** (:cppterm:`dot operator`) : .. admonition:: Syntaxe .. code-block:: cpp 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: .. code-block:: cpp int main() { Car c; c.Start(); int power = c.power; } Indépendance entre objets ------------------------- Prenons l'exemple d'une structure compteur : .. code-block:: cpp struct Counter { int Value; }; Créons deux instances de la structure *Compteur* : .. code-block:: cpp Counter c1; Counter c2; Deux zones mémoires sont réservées pour stocker chaque instance. Les noms :cpp:`c1` et :cpp:`c2` sont associés à chaque objet. On peut représenter la mémoire de la façon suivante : .. image:: img1.png :align: center :scale: 40 % Si on exécute l'instruction: .. code-block:: cpp c1.Value = 10; L'opérateur d'accès aux membres :cpp:`.` appliqué au nom :cpp:`c1` indique que c'est le champ :cpp:`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 : .. image:: img2.png :align: center :scale: 40% Exercices --------- .. quiz:: qcmClasse3 :title: Membres d'instance * :quiz:`{"type":"TF","answer":"F"}` Si je change la valeur d'un champ d'instance, l'objet n'est plus considéré comme appartenant à sa classe d'origine. * :quiz:`{"type":"TF","answer":"F"}` Une fois l'objet créé,je ne peux plus modifier ses variables d'instance * :quiz:`{"type":"TF","answer":"F"}` Pour modifier un champ d'instance j'utilise le signe . après avoir écrit le nom de la classe. * :quiz:`{"type":"TF","answer":"T"}` 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 : .. panels:: :column: col-lg-10 p-2 **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 : .. panels:: :column: col-lg-10 p-2 Les variables d'instance, si elles ne sont pas initialisées dans le code, ont des valeurs indéterminées. .. admonition:: Syntax .. code-block:: cpp StructName(Type1 param1, Type2 param2, ...) { ... } Exemple : .. code-block:: cpp 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 : .. admonition:: Syntax .. code-block:: cpp 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** :cppterm:`(default constructor)` `voir ici `_. Il correspond au constructeur appelé sans passer d'arguments. L'appel de ce constructeur est parfois implicite, notamment lorsque vous écrivez : .. code-block:: cpp Point P; Les constructeurs avec arguments s'appellent des constructeurs paramétriques :cppterm:`(parametrized constructors)`. .. panels:: :column: col-lg-10 p-2 **REGLE** : **Si aucun constructeur n'est fourni par le programmeur**, le compilateur C++ fournit un constructeur par défaut de manière implicite. A l'inverse, si un constructeur paramètrique est présent, il faut fournir un constructeur par défaut de manière explicite. .. code-block:: cpp #include 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 : .. image:: error.PNG :scale: 160% Délégation 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 (:cppterm:`constructor delegation`). Voici un exemple : .. code-block:: cpp struct Point { int x; int y; Point() : Point(0,0) // 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, on peut effectuer des traitements supplémentaires si besoin. Initialisation des variables d'instance ======================================= Plusieurs options sont possibles : Initialisation dans le corps du constructeur -------------------------------------------- .. code-block:: cpp struct Point { int x; int y; Point() // default constructor { x = y = 0; } }; Initialisation lors des définitions ----------------------------------- .. code-block:: cpp struct Point { int x = 0; int y = 0; }; Liste d'initialisations des membres ----------------------------------- Il est possible d'initialiser des variables membres avant l'exécution du bloc du constructeur. On parle de :cppterm:`data member initializer`. .. code-block:: cpp 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:: La liste d'initialisation est le seul moyen connu pour initialiser un champ référence. .. note:: On trouve une syntaxe équivalente utilisant des accolades : :cpp:`b{i}`. .. quiz:: classConstructorsDelegation :title: Delegating Constructor .. code-block:: cpp 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`` :quiz:`{"type":"FB","answer":"1"}` * ``d`` :quiz:`{"type":"FB","answer":"2"}` * ``b`` :quiz:`{"type":"FB","answer":"false"}` .. quiz:: classConstructorsDelegation1 :title: Quizzz Cette écriture : .. code-block:: test() : test(0) ... correspond à * :quiz:`{"type":"TF","answer":"F"}` Un :cppterm:`constructor binding` * :quiz:`{"type":"TF","answer":"F"}` Un :cppterm:`data member initializer` * :quiz:`{"type":"TF","answer":"T"}` Un :cppterm:`constructor delegation` Préparation à l'examen ====================== Il est vivement recommandé de ne pas déléguer ces exercices à l'IA car ils seront repris lors des examens écrits. Exercice 1 ---------- * Créez un nouveau fichier .cpp * Codez les fonctionnalités demandées ci-dessous et validez votre code avec les tests donnés ci-dessous La classe V2 : * Créez une structure :cpp:`V2` stockant deux valeurs entières * Utilisez uniquement des valeurs entières * Ecrivez un constructeur par défaut qui initialise la position à :cpp:`(0,0)` * Ecrivez son constructeur paramétrique * Ecrivez l'opérateur :cpp:`+` entre deux objets :cpp:`V2` sous forme de fonction externe à la classe * Syntaxe : :cpp:`V2 operator+(const V2& v1, const V2& v2)` * Ecrivez l'opérateur :cpp:`*` d'un :cpp:`V2` par un entier sous forme de fonction externe à la classe * Syntaxe : :cpp:`V2 operator* (const V2& v1, int factor)` * Validez les tests ci-dessous * Effectuez le test suivant : * Donnez deux :cpp:`V2 a` et :cpp:`b` et un entier :cpp:`k` * Calculez le résultat de l'expression : :cpp:`(a+b)\*k` et stocker le dans un :cpp:`V2` * Calculez le résultat de l'expression : :cpp:`(a\*k+b\*k)` et stocker le dans un :cpp:`V2` * Comparez les deux valeurs .. code-block:: cpp 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 ---------- * Copiez-collez votre classe :cpp:`V2` dans votre nouveau fichier cpp * Codez les fonctionnalités demandées ci-dessous, il est fortement recommandé d'écrire vos propres tests La classe Rectangle : * Créez une structure Rectangle * Ajoutez des champs : :cpp:`Size (V2)` et position :cpp:`P (V2)` * Ajoutez un constructeur paramétrique qui initialise la taille et la position de l’objet à partir de deux :cpp:`V2` passées en paramètres. * Ajoutez une méthode d’instance :cpp:`Area()` qui retourne l’aire du rectangle. * Dans la fonction :cpp:`main()` : * Créez une nouvelle instance de la structure Rectangle de dimension 20 par 30 à la position :cpp:`(15,20)`. * Affichez les valeurs des champs de l’objet créé. * Affichez l’aire de l’objet créé en appelant la méthode :cpp:`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 : :cpp:`width (int)` et :cpp:`pos (V2)` * Vous devez utiliser le chaînage des constructeurs * Le corps de ce constructeur ne doit contenir aucune instruction * Ajoutez une méthode :cpp:`resizeDouble()` qui multiplie la taille du rectangle par 2 * Utilisez votre opérateur :cpp:`*` dans la fonction :cpp:`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 :cpp:`Translate(...)` qui déplace la position * Utilisez votre opérateur :cpp:`+` dans la fonction :cpp:`Translate()` * Effectuez un test