Outils d'utilisateurs

Outils du Site


poo

Différences

Cette page vous donne les différences entre la révision choisie et la version actuelle de la page.

Lien vers cette vue

poo [2016/02/20 11:00]
winjerome
poo [2019/12/27 19:58] (Version actuelle)
gbdivers créée
Ligne 1: Ligne 1:
  
-^ Chapitre précédent ^ [[programmez_avec_le_langage_c|Sommaire principal]] ^ Chapitre suivant ^ +<note warning>Ce cours n'est plus à jour, il est préférable de ne pas le suivreJe vous recommande le cours sur Zeste de Savoir https://zestedesavoir.com/tutoriels/822/la-programmation-en-c-moderne/.</note>
- +
-__ const correcteness des fonction membres __  +
- +
-====== Créer des classes ====== +
- +
-__ rappel sur la sémantique de valeur, à quoi cela correspond __ +
- +
-Deux aspects : structurer les données et leur appliquer des traitements. Dans les chapitres précédents, vu la partie traitement : l'algorithmique. La POO vise à fournir une méthode pour structurer les données. +
- +
-===== Classe et objet ===== +
- +
-Classe = type défini dans le code, objet = ce qui apparaît en mémoire dans le code.   +
-Objet = l'instanciation d'une classe. Il peut y avoir plusieurs objets qui sont instanciés à partir d'une classe. +
- +
-La syntaxe, pour une classe qui ne fait rien (vide) : +
- +
-<code cpp> +
-class A { // une classe +
-};  +
- +
-A a1;     // un premier objet, qui s'appelle "a1" et de type "A" +
-A a2;     // un second objet, qui s'appelle "a2" et de type "A" +
-</code> +
- +
-(Attention au point-virgule après les accolades). Il est possible d'utiliser également le mot clé ''struct'' (la différence sera expliquée ensuite). Dans les deux cas, cela permet de créer une classe. +
- +
-<code cpp> +
-struct B {  +
-}; +
-</code> +
- +
-On peut avoir plusieurs objets instanciés à partir d'une même classe, mais on ne peut pas avoir deux classes avec le même nom (tout comme on ne pouvait pas avoir deux variables avec le même nom). +
- +
-En pratique, une classe est un type. Par exemple, vous avez déjà vu des exemples de classe : string, vector, array. Leur utilisation est identique à n'importe quel type de la bibliothèque standard. +
- +
-Le nom des classes suit les mêmes règles (caractères autorisés) que les noms de variables ou de fonctions. +
- +
-<note info> +
-Il est classique lorsque l'on écrit des codes d'exemples de donner des noms bateaux aux noms. Vous avez déjà vu par exemple ''i'', ''j'', ''k'' pour nommer des entiers, ''s'' ou ''str'' pour des chaînes, ''f()'', ''g()'', ''h()'', ''foo()'', ''bar()'' pour des fonctions. Pour les classes, on donne souvent une lettre majuscule : ''A'', ''B'', ''C'', etc. +
- +
-Ces choix de nom ne sont bien sûr pas à utiliser dans un vrai projet, uniquement pour des exemples ou poser une question sur un forum. +
-</note> +
- +
-Comme il n'y a pas de différence entre les classes de la lib standard et vos propres classes, vous pouvez les utiliser n'importe où, comme vous le faisiez pour les autres types. +
- +
-<code cpp> +
-class A {};       // pour définir un type (A est une classe) +
- +
-A a {};           // pour définir une variable (a est un objet) +
-void f(A a) {}    // comme paramètre de fonction +
-A g() {}          // comme paramètre de retour de fonction +
- +
-f(a);             // comme argument de fonction +
-a = g();          // comme résultat de fonction +
-auto a = f();     // avec auto +
- +
-template<typename T> +
-void h(T t) {}    // fonction template +
- +
-h<A>();           // comme argument template de fonction +
-std::vector<A> v; // comme argument template d'une classe +
-</code> +
- +
-===== Les membres d'une classe ===== +
- +
-Une classe rassemble des variables et des fonctions. S'appellent variables membres ou attributs et fonctions membres ou méthodes. Déclare membres de la même façon que d'habitude : +
- +
-<code cpp> +
-struct A { +
- +
-    int i {}; +
-     +
-    void f() { +
-        cout << "appel de f()" << endl; +
-    } +
-     +
-}; +
-</code> +
- +
-Pour les appeler, il faut créer un objet puis utiliser ses membres (comme vous l'avez fait pour les classes de la lib standard) : +
- +
-<code cpp> +
-A a {};              // un objet de type A +
- +
-a.i = 123;           // modification de i +
-cout << a.i << endl; // utilisation de i +
- +
-a.f();               // utilisation de f() +
-</code> +
- +
-Les membres d'une classe ne sont pas au même niveau (on parle de portée) que les fonctions libres, il n'y a pas de conflit entre les noms et il est donc possible de donner le même nom à une fonction libre et une fonction membre. +
- +
-<code cpp> +
-void f() {} // fonction libre +
- +
-struct A { +
-    void f() {}  // fonction membre +
-}; +
- +
-int main() { +
-    f();    // appel de la fonction libre +
-     +
-    A a {}; +
-    a.f();  // appel de la fonction membre +
-+
-</code> +
- +
-Vous avez déjà rencontré cette situation avec les fonctions ''begin'' et ''end'' par exemple. Pour rappel, ces fonctions peuvent s'appeler comme des fonctions libres ou des fonctions membres : +
- +
-<code cpp> +
-vector<int> v {}; +
- +
-begin(v);  // fonction libre +
-v.begin(); // fonction membre +
-</code> +
- +
-Pour déclarer de telles fonctions, il faut simplement créer une fonction membre et une fonction libre qui prend un paramètre : +
- +
-<code cpp> +
-struct A { +
-    void f() const { ... }  // la fonction membre +
-}; +
- +
-void f(A const& a) {  // on utilise une référence sur l'objet, pour ne pas le copier +
-    a.f();            // on appelle la fonction membre +
-+
- +
-A a {}; +
-a.f(); +
-f(a); +
-</code> +
- +
-Les deux fonctions font la même chose. +
- +
-Définitions : +
- +
-  * déclaration : on dit au compilateur qu'un identifiant existe (''A'', ''i'', etc.) ; +
-  * définition : on dit au compilateur à quoi correspond à un identifiant : +
-  * implémentation : on dit au compilateur comment on fait. +
- +
-Exemple : +
- +
-<code cpp> +
-void f(); // déclaration (on dit que "f" existe) et définition (on dit que "f" est +
-          // une fonction, qui ne prend aucun paramètre et retourne rien) +
-      +
-void f() { ... } // implémentation +
- +
-class A;         // déclaration de +
- +
-class A {        // définition de A +
-    int i {}; +
-    void f(); +
-}; +
- +
-void A::f() {     // implémentation de A::f +
-}; +
-</code> +
- +
-<note>**Type incomplet** +
- +
-Un type est complet lorsqu'il est entièrement défini. Un type qui est simplement déclaré et pas encore défini est incomplet par exemple. Pour les fonctions et classes, on a vu comment séparer déclaration et définition. +
- +
-Pour une variable, possible aussi de séparer, en utilisant le mot-clé ''extern'' (cf ailleurs). Utile avec des libs. +
- +
-<code cpp> +
-extern int a; // déclaration +
- +
-a += 1; // erreur, a est simplement déclaré (i.e. le compilateur sait que l'identifiant "a"  +
-        // existe, mais il ne sait pas à quoi cela correspond) +
-         +
-int a {}; // ok, définition +
-</code> +
-</note> +
- +
-ODR (//one definition rule//) : on peut avoir plusieurs déclarations, mais une seule définition  __ //header guard// : éviter plusieurs définitions d'une même classe __ +
- +
-<code cpp> +
-class A {}; +
-class A {}; // erreur, double définition +
- +
- +
-class B; +
-class B; // ok, double déclaration +
-</code> +
- +
-Par contre, avant d'utiliser il faut que cela soit défini : +
- +
-<code cpp> +
-class A; +
-class A; // ok, double déclaration +
- +
-A a {}; // erreur, non défini +
- +
-class A {}; +
-A a {}; // ok, défini +
-</code> +
- +
- +
-déclaration anticipée +
- +
-<code cpp> +
-class A { +
-    B b {}; // erreur, B n'est pas connu à ce niveau +
-}; +
- +
-class B { +
-}; +
-</code> +
- +
-<code cpp> +
-class B; // déclaration anticipée de B +
- +
-class A { +
-    B b {}; // ok, le compilateur sait que B existe (même  +
-            // s'il ne sait pas à quoi cela correspond) +
-}; +
- +
-class B { +
-}; +
-</code> +
- +
-__problématique de double inclusion de classe dans plusieurs fichiers __ +
- +
- +
-===== Visibilité des membres d'une classe ===== +
- +
-Trois types de visibilité : +
- +
-  * ''public'' (publique) : le membre est visible depuis l'extérieur de la classe ; +
-  * ''private'' (privé) : le membre n'est pas visible depuis l'extérieur de la classe ; +
-  * ''protected'' (protégé) : le membre n'est visible que depuis les classes dérivées. +
- +
-Le troisième cas est lié à la notion d'héritage, sera vu dans les classes à sémantique d'entité. +
- +
-Par exemple, une classe ''A'' avec un membre ''f()'' ou ''i''. Pour appeler le membre, on utilise l'opérateur. (Comme déjà fait avec ''begin'' et ''end'' par exemple) : +
- +
-<code cpp> +
-A a {};  // définit a +
-a.i = 0; // accès au membre i +
-a.f();   // accès au membre f() +
-</code> +
- +
-Bien faire attention aux notions de variables et types. ''A'' est un type, il permet de déclarer une variable. ''a'' est une variable, on peut appeler ''.'' dessus. +
- +
-Dans ce code, on utilise la classe ''A'', on est l'extérieur de la classe. Comme on a accès aux membres, on a donc un accès public. Avec un membre privé, essayer d'accéder depuis l'extérieur produit une erreur du compilateur : +
- +
-<code> +
-main.cpp:9:7: error: 'i' is a private member of 'A' +
-    a.i = 123; +
-      ^ +
-main.cpp:2:9: note: implicitly declared private here +
-    int i {}; +
-        ^ +
-main.cpp:10:7: error: 'f' is a private member of 'A' +
-    a.f(); +
-      ^ +
-main.cpp:3:10: note: implicitly declared private here +
-    void f() {} +
-         ^ +
-2 errors generated. +
-</code> +
- +
-On définit la visibilité des membres en utilisant les mots-clés ''public'', ''private'' et ''protected''. Par exemple : +
- +
-<code cpp> +
-class A { +
-public: +
-    int i {}; +
-    void foo() {} +
-private: +
-    int j {}; +
-    void g() {} +
-}; +
-</code> +
- +
-La déclaration de visibilité s'applique tant qu'aucun autre identificateur n'est spécifié. Donc dans ce code, ''i'' et ''f'' sont publics et ''j'' et ''g'' sont privés. +
- +
-<code cpp> +
-A a {}; +
-a.i = 123; // ok +
-a.f();     // ok +
-a.j = 123; // erreur +
-a.g();     // erreur +
-</code> +
- +
-Par défaut, le mot-clé ''class'' crée une classe avec des membres en visibilité privée, on peut omettre le ''private'' s'il est en premier. Donc écrire : +
- +
-<code cpp> +
-class A { +
-private: +
-    int i {}; +
-    void foo() {} +
-}; +
-</code> +
- +
-est équivalent à : +
- +
-<code cpp> +
-class A { +
-    int i {}; +
-    void foo() {} +
-}; +
-</code> +
- +
-Le mot-clé ''struct'' est similaire à ''class'' et permet de définir une classe. La seule différence est que ''struct'' a une visibilité publique par défaut : +
- +
-<code cpp> +
-class A { +
-    int i {}; +
-}; +
- +
-struct B { +
-    int i {}; +
-}; +
- +
-A a {}; +
-a.i = 123; // erreur, i est privé avec class si public n'est pas spécifié +
- +
- +
-B b {}; +
-b.i = 123; // ok, i est public avec struct par défaut +
-</code> +
- +
-<note>**Accesseurs** +
- +
-Des accesseurs sont des fonctions membres spécifiques, qui permettent de lire et modifier des variables membres. Cela permet de mettre les variables membres en ''private'' et contrôler leur accès. +
- +
-<code cpp> +
-// sans accesseurs +
-class A { +
-public: +
-    int m_i {}; +
-}; +
- +
-// avec accesseurs +
-class B { +
-private: +
-    int m_i {}; +
-public: +
-    int getI() const { return m_i } +
-    void setI(int i) { m_i = i; } +
-}; +
-</code> +
- +
- +
-On les appelle souvent //getter// et //setter//. +
- +
-À première vue, semble respecter encapsulation, mais vision des classes comme ensemble de données et pas comme un prestataire de services. Analogie : si on avait un classe Portefeuille, première approche correspond à "voila mon portefeuille, sers-toi", la seconde à "voila X euros". La seconde est beaucoup plus sécurisée... +
-</note> +
- +
-===== portée, statique et espace de noms ===== +
- +
-On peut remarquer que l'on a des syntaxes similaires. Un seul identifiant par portée. Un récapitulatif : +
- +
-<code cpp> +
-// déclaration fonction +
-void f();  // fonction libre dans la portée globale :: +
- +
-namespace myspace { +
-void f();  // fonction libre dans la portée myspace +
-+
- +
-class A { +
-    void f(); // fonction membre +
-    static g(); // fonction membre static +
-}; +
- +
- +
-// utilisation +
- +
-int main() { +
-    f(); // fonction libre de :: +
-    ::f(); // autre syntaxe, avec portée globale explicite +
-     +
-    myspace::f(); // libre de myspace +
-     +
-    A a {}; +
-    a.f(); // membre non statique +
-     +
-    A::g(); // membre static +
-+
-</code> +
- +
-Portée : espace de noms (global ou user), classe, fonction dans lequel une identifiant est défini. Utilisation de l'opérateur de portée :: +
- +
-**Namelookup et signature de fonction** +
- +
-Quand on rencontre l'utilisation d'un identifiant, comment trouver la fonction qui correspond ? Template, spécialisation template, surcharge, multiple définition (qu'est-ce qui rentre dans la signature ?)  +
- +
- +
- +
-===== Encapsulation ===== +
- +
-On a vu que les classes que vous déclarez sont identiques à celles de la lib standard. La réciproque est vraie : les classes de la lib standard sont identiques au code que vous pouvez écrire. Elles sont écrites en C++, avec la même syntaxe que vous utilisez (des exercices à la fin du cours proposent de réécrire ces classes). +
- +
-En particulier, il est tout à fait possible d'aller regarder le code C++ des classes et fonctions de la lib standard pour voir comment elles sont implémentées. +
- +
-Mais la question importante est : avez-vous eu besoin de connaître le code de ces classes et fonctions pour les utiliser ? +
- +
-La réponse est bien sûr non (heureusement, comme vous les utilisez depuis le début du cours, si vous ne pouviez pas les utiliser sans voir leur code, vous seriez un peu bloqué). Pour utiliser une classe et une fonction, vous avez simplement besoin : +
- +
-  * de connaître son nom et ce qu'elle fait ; +
-  * de connaître la liste de ses paramètres. +
- +
-Si une classe ou fonction est correctement conçue, il est généralement possible de savoir ce qu'elle fait et le rôle de chaque paramètre, rien qu'avec leur nom. Si on regarde par exemple la fonction [[http://en.cppreference.com/w/cpp/algorithm/sort|std::sort]] : +
- +
-<code cpp> +
-template< class RandomIt > +
-void sort( RandomIt first, RandomIt last ); +
-</code> +
- +
-Le nom signifie ''tri'', on comprend qu'elle permet de trier quelque chose. Cette fonction prend deux paramètres, de type ''RandomIt'', qui s'appellent ''first'' et ''last''. On comprend donc que cette fonction permet de trier une collection entre un premier élément et un dernier. +
- +
-Pour ''RandomIt'', il faut bien sûr savoir ce qu'est un [[http://en.cppreference.com/w/cpp/concept/RandomAccessIterator|RandomAccessIterator]] pour savoir à quoi cela correspond. Mais (normalement) vous savez que cela correspond à des itérateurs, par exemple dans vector. +
- +
-L'ensemble des informations que l'on donne sur une classe ou une fonction et qui permet de les utiliser s'appelle l'interface d'une classe. Celle-ci contient les noms des classes, fonctions et paramètres que les utilisateurs peuvent utiliser, ainsi que la documentation. +
- +
-En pratique, cela signifie que pour utiliser une classe, vous pouvez écrire : +
- +
-__ doivent déjà connaître la différence entre définition et implémentation ? dans le chapitre sur les fonctions ? __ +
- +
-<code cpp> +
-void f();    // définition d'une fonction libre +
- +
-struct A { +
-    int f(); // définition d'une fonction membre +
-     +
-}; +
-</code> +
- +
-Le code de la fonction n'est pas nécessaire pour comprendre comment utiliser cette classe. Le code permettant cela s'appelle la définition (d'une classe ou d'une fonction). Le bloc de code {} est remplacé par '';'' +
- +
-Bien sûr, à un moment donné, il faut donner le code correspondant à la fonction. Ce code s'appelle l'implémentation de la fonction. Pour implémenter une fonction membre, il faut indiquer la classe correspondante, en utilisant l'opérateur de portée ''::'' : +
- +
-<code cpp> +
-// implémentation d'une fonction libre +
-void f() { +
-    ... +
-+
- +
-// implémentation d'une fonction membre +
-void A::f() { +
-    ... +
-+
-</code> +
- +
-On va même pouvoir aller plus loin et séparer les définitions et implémentations dans deux fichiers séparés. Le premier dans un .h et second dans un .cpp. Compilation de .cpp et inclusion de .h. +
- +
-Remarque : sauf template +
- +
-On peut remarquer un avantage très intéressant de cette séparation : on peut modifier l'implémentation, sans que cela impacte la définition. Cela implique donc que l'on peut modifier le code interne d'une classe ou d'une fonction sans avoir besoin de modifier le code qui l'utilise. +
- +
-Pour être concret, si on écrit une fonction d'incrémentation, on pourra écrire : +
- +
-<code cpp> +
-template<typename T> +
-void next(T & value) { +
-    value += 1; +
-+
- +
-int i {}; +
-next(i); +
- +
-auto it = begin(v); +
-next(it); +
-</code> +
- +
-Par la suite, on réalise que ce code ne fonctionne pas avec les itérateurs de std::list (qui n'est pas un RandomIterator, mais un [[http://en.cppreference.com/w/cpp/concept/BidirectionalIterator|BidirectionalIterator]], qui ne propose pas l'opérateur += ).  +
- +
-On peut alors corriger le code et utiliser l'opérateur ++ : +
- +
-<code cpp> +
-template<typename T> +
-void next(T & value) { +
-    ++value; +
-+
-</code> +
- +
-Le code précédent continue de fonctionner (le code est maintenable) et on peut à présent utiliser std::list (le code est évolutif). +
- +
-La séparation entre définition et implémentation s'appelle l'encapsulation. Ce principe permet de gagner en maintenabilité et évolutivité du code. Plus les classes et fonctions seront correctement encapsulées, pour votre code gagnera en qualité. +
- +
-Il ne sera pas toujours possible de séparer correctement définition et implémentation. Ce n'est pas grave, il faut juste être conscient que cela aura un impact sur la maintenabilité et l'évolutivité. +
- +
-Une erreur classique est d'exposer les détails internes d'une classe. Par exemple, si vous avez une classe qui contient un vector et que vous voulez pouvoir modifier les éléments de ce vector, vous pouvez écrire:  +
- +
-<code cpp> +
-struct A { +
-    vector<int> v {}; +
-}; +
- +
-// ou équivalent, un accesseur : +
- +
-class B { +
-    vector<int> v {}; +
-public: +
-    vector<int> & get_v { return v; } +
-}; +
-</code> +
- +
-On expose vector vers l'extérieur, il fait partie de l'interface. Si un jour on utilisait un std::list, le code utilisateur ne sera pas forcément correct. +
- +
-Possible d'éviter cela en ne mettant pas vector en interface. Par exemple, on peut proposer des fonctions ''begin'' et ''end'', comme pour les conteneurs (il faut fournir les versions ''const'' et non ''const''. __ Pourquoi ?  Comment savoir ce qu'il faut fournir ? __) : +
- +
-<code cpp> +
-class A { +
-    Container  v {}; +
-public: +
-    using container = vector<int>; +
-    using iterator = container ::iterator; +
-    using const_iterator = container ::iterator; +
-     +
-    iterator       begin()       { return begin(v); } +
-    const_iterator begin() const { return begin(v); } +
-    iterator       end()         { return end(v); } +
-    const_iterator end() const   { return end(v); } +
-}; +
-</code> +
- +
-Autre solution, design pattern visiteur, i.e. prendre une fonction à appliquer sur chaque élément : +
- +
-<code cpp> +
-class A { +
-    vector<int> v {}; +
-public: +
-    template<typename Function> +
-    void apply(Function f) { +
-        std::for_each(begin(v), end(v), f); +
-    } +
-}; +
-</code> +
- +
-===== Principe de responsabilité unique ===== +
- +
-Faire une seule chose et le faire bien +
- +
-===== Classe template ===== +
- +
-De la même manière que l'on a pu définir des fonctions template, il est possible de définir des classes template. Un exemple de classe template, c'est vector ou array. +
- +
-La déclaration d'un classe template est similaire à une fonction, il faut ajouter template avec la liste des paramètres template : +
- +
-<code cpp> +
-template<typename T> +
-class A { +
-}; +
-</code> +
- +
-Les paramètres template peuvent être utilisés dans l'ensemble de la classe, pour la déclaration d'une variable membre ou comme paramètre de fonction membre. +
- +
-<code cpp> +
-template<typename T> +
-struct A { +
-    T value {}; +
-    T f(T param); +
-}; +
-</code> +
- +
-__ séparation implémentation et définition __ +
- +
-Pour instancier une classe template, il faut spécifier les arguments templates. Contrairement aux fonctions template, le compilateur ne peut pas déduire les arguments template à partir des arguments de fonction. +
- +
-<code cpp> +
-A<int> a_int {}; +
-A<double> a_double {}; +
-</code> +
- +
-^ Chapitre précédent ^ [[programmez_avec_le_langage_c|Sommaire principal]] ^ Chapitre suivant ^ +
- +
-{{tagCours C++}}+
poo.1455962431.txt.gz · Dernière modification: 2016/02/20 11:00 (modification externe)