IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

 C++ Discussion :

Conception de classes et pointeurs


Sujet :

C++

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre éclairé
    Profil pro
    Étudiant
    Inscrit en
    Janvier 2008
    Messages
    253
    Détails du profil
    Informations personnelles :
    Localisation : France, Haute Savoie (Rhône Alpes)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Janvier 2008
    Messages : 253
    Par défaut Conception de classes et pointeurs
    Bonsoir à tous,

    J'ai trouvé un cas de mise en pratique de mes toutes petites connaissances en C++, c'est l'occasion pour moi de m'en servir et surtout de progresser dans la maitrise de ce langage.

    Plusieurs cours ont déjà été décortiqués mais j'ai encore du mal à faire des choix solides entre pointeurs et variables, allocation statique et allocation dynamique.
    Je souhaite surtout faire de l'objet, le plus rapidement possible.

    A première vue, et pour reproduire un comportement qui m'est familier avec des langages de bien plus haut niveau, je serais tenté de faire du "tout pointeur". Des attributs de mes classes aux paramètres qu'acceptent mes méthodes.
    Cependant, si mes attributs (privés) sont des pointeurs et que mes getters/setters acceptent aussi des pointeurs, on va vite se retrouver avec ce que je crois être un gros problème de fiabilité.

    Soit ce code :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
     
    class A {
      private: std::string *_attrib;
     
      public: std::string *getVal(void) const;
      public: void setVal (std::string *val);
    }
     
    std::string *A::getVal (void) const {
       return this->_attrib;
    }
    void A::setVal (std::string *val){
       this->_attrib = val;
    }
     
     
    main (){
       std::string str1 = "test...";
     
       A *obj = new A();
       obj->setVal(&str1);
     
       str1.erase(4, 3);
     
       cout << *obj->getVal() << endl; // Doit afficher "test" au lieu de "test..."
    }
    On parvient à modifier un élément qui est pourtant privé.

    J'hésite vraiment entre ce risque là (avec des liaisons parfois franchement sales) et l'économie de mémoire qui me semble être substantielle en utilisant des pointeurs.

    Par extension, on peut également se demander si en cas de clone de mon objet de type A, la copie de l'attribut doit être prise en charge explicitement où si la duplication est récursive (d'une autre côté, pourquoi cela serait-il le cas vu que le seul attribut est un pointeur?).

    En d'autres termes, j'ai du mal à trouver une règle générale qui me permettrait de dire si le code est propre ou non.

    Merci de m'en dire plus, bon week end

  2. #2
    Invité
    Invité(e)
    Par défaut
    hello,

    1: private et public:
    - Dans bisounours, si tu as un attribut private, tu ne devrais avoir aucun accesseur (get/set), ta variable devrait être manipulée qu'à l'intérieur de ta classe.
    - j'évite de parler héritage pour l'instant.
    - interdire l'accès direct au publique et en accorder l'accès via des getters/setters peut se justifier si tu veux faire un traitement lorsqu'on modifie cette variable. (par exemple, compter le nombre de fois qu'on y accède pour des logs)
    =>de manière générale, si tu peux te passer des getters/setters, passe-t-en, parce qu'àprès tu trimballes des dépendances sur tes classes (qui au passage ne t'offre aucune utilité à part stocker)

    2:services ou conteur de données:
    ya deux types de classes: celles qui stockent et celles qui servent.
    ex: classA{public:int a,b,c;}: ca stocke
    ex: classB{public:sayHello(){}}; ca sert (idem ya une valeur ajoutée).
    De manière générale encore, les classes qui stockent, tu duppliques leur valeur car tu t'en tapes.
    Pour les classes à sémantique d'entité, il n'y a pas de sens qu'elles soient duppliquées, (il faudrait que s'il y a deux classes duppliquées, elles aient la même durée de vie et le même comportement). Pour l'instant considère qu'elles sont pas copiables.

    3:pointeur ou mémoire:
    Tant que tu le peux passes-toi des pointeurs sinon faut que gères toi même la mémoire..
    Tu as besoin des pointeurs quand tu as besoin de faire de l'alloc dynamique (ex: un gros tableau, du polymorphisme).
    Sinon sers-toi de référence, ou de copie de données

    concernant les lois,
    pour getter/setter ya la loi de Déméter, le reste je sais pas.

  3. #3
    Membre éclairé
    Profil pro
    Étudiant
    Inscrit en
    Janvier 2008
    Messages
    253
    Détails du profil
    Informations personnelles :
    Localisation : France, Haute Savoie (Rhône Alpes)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Janvier 2008
    Messages : 253
    Par défaut
    Bonjour galérien69, merci pour cette réponse.

    Elle soulève de nouvelles interrogations chez moi

    Citation Envoyé par galerien69 Voir le message
    hello,

    1: private et public:
    - Dans bisounours, si tu as un attribut private, tu ne devrais avoir aucun accesseur (get/set), ta variable devrait être manipulée qu'à l'intérieur de ta classe.
    - interdire l'accès direct au publique et en accorder l'accès via des getters/setters peut se justifier si tu veux faire un traitement lorsqu'on modifie cette variable. (par exemple, compter le nombre de fois qu'on y accède pour des logs)
    =>de manière générale, si tu peux te passer des getters/setters, passe-t-en, parce qu'àprès tu trimballes des dépendances sur tes classes (qui au passage ne t'offre aucune utilité à part stocker)
    Voila un principe de l'objet que j'ignorais :s
    Je connais le principe de l'encapsulation qui m'impose private ou du moins protected pour chacun de mes attributs.
    Ceci dit, je fais des vérifications presque à chacun de mes set.

    - j'évite de parler héritage pour l'instant.
    On va éviter de mettre de l'argent là-dedans oui

    2:services ou conteur de données:
    ya deux types de classes: celles qui stockent et celles qui servent.
    ex: classA{public:int a,b,c;}: ca stocke
    ex: classB{public:sayHello(){}}; ca sert (idem ya une valeur ajoutée).
    De manière générale encore, les classes qui stockent, tu duppliques leur valeur car tu t'en tapes.
    Pour les classes à sémantique d'entité, il n'y a pas de sens qu'elles soient duppliquées, (il faudrait que s'il y a deux classes duppliquées, elles aient la même durée de vie et le même comportement). Pour l'instant considère qu'elles sont pas copiables.
    Pour ce passage, la structure et les interfaces de mes classes sont déjà connues, je ne reviendrai pas là-dessus.
    Quand je parlais de copie, il s'agit bien sur des instances de ces classes
    Le but est d'éviter les conflits lors d'accès concurrents qui peuvent survenir en cas d'utilisation presque abusive des pointeurs.

    3:pointeur ou mémoire:
    Tant que tu le peux passes-toi des pointeurs sinon faut que gères toi même la mémoire..
    Tu as besoin des pointeurs quand tu as besoin de faire de l'alloc dynamique (ex: un gros tableau, du polymorphisme).
    Sinon sers-toi de référence, ou de copie de données
    D'accord

    Sur la question particulière des attributs, est-il préférable de les mettre en référence où en copie dure?
    Le problème va être pour le 1er cas une gestion correcte de la concurrence, cf le cas que j'évoque dans mon 1er post.

  4. #4
    Invité
    Invité(e)
    Par défaut
    Sur la question particulière des attributs, est-il préférable de les mettre en référence où en copie dure?
    Aucun des deux.

    Le problème va être pour le 1er cas une gestion correcte de la concurrence, cf le cas que j'évoque dans mon 1er post.
    tu n 'évoques pas la concurrence dans ton 1er post.

    Il n'y a pas de règles. Seulement des bonnes pratiques pour les lignes générales. Il y a des cas ou tu auras comme attributs des pointeurs: ex: construction d'un arbre, utilisation de polymorphisme, optimisation mémoire,...
    Il y a des cas ou tu auras des valeurs copiées/copiables.
    type de base (int,string,..),
    Ca dépend ce que tu fais.

    Enfin pour les accès concurrents, c'est assez délicat, je laisse le soin à d'autres.

  5. #5
    Expert éminent
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 644
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 644
    Par défaut
    Salut,

    En gros, tu peux avoir deux sortes de classes :

    Les classes qui ont une sémantique de valeur, qui peuvent exister plusieurs fois avec des valeurs identiques (comme une classe point, ou couleur par exemple)

    Ces classes sont, classiquement :
    • constantes (si tu modifie la composante rouge d'une couleur, tu obtiens une autre couleur )
    • copiables (tu peux avoir deux instances présentant les même caractéristiques à deux adresses mémoire différentes)
    • assignables (tu peux décider de repeindre en bleu une pièce qui est en rouge à l'origine )
    • comparable, au minimum du point de vue de l'égalité
    • peu enclines à entrer dans une hiérarchie de classe (à hériter ou à servir de base à un héritage)

    Et il y a les classes qui ont sémantique d'entités, qui s'identifient, d'une manière ou d'une autre, de manière unique et non ambigüe (comme une classe "personne" dont chaque instance représente une personne particulière, ou une classe "compte bancaire" pour la même raison).

    Ces classes sont, classiquement:
    • non copiables (il ne faut pas qu'il existe deux instances d'un même compte bancaire, car, sinon, les retrait effectués au travers d'une instance ne seraient pas répercutés sur l'autre)
    • non assignables
    • modifiables(pour tout ce qui ne sert pas à l'identification unique et non ambigue du moins)
    • non comparables : tu ne peux comparer que certaines de leurs caractéristiques, comme le numéro du compte ou son solde, mais il n'y a aucun sens à comparer deux compte bancaires dans leur globalité
    • globalement adaptées à intervenir dans une relation d'héritage
    Pour ce qui est des accesseurs (getters) et des mutateurs :
    Il existe, comme l'a fait remarquer galerien69, la loi demeter qui dit que si un objet de type A manipule un objet de type B et que l'objet de type B manipule lui-même un objet de type C, tu ne devrais pas avoir à connaitre le type C pour pouvoir travailler avec le type A.

    A priori, tu ne devrais donc fournir un accesseur sur une donnée que si le fait de fournir l'accès à cette donnée fait partie des services que l'on est en droit d'attendre de la part de la classe :

    Par exemple, un compte bancaire a, très certainement, un membre "solde" qui représente le montant disponible sur le compte.

    Comme on s'attend à pouvoir connaitre ce montant, il est "sensé" de fournir un accesseur dessus

    Par contre, tout le monde est bien conscient qu'une voiture est composée, entre autres, d'un réservoir d'essence.

    Cependant, tout ce que l'utilisateur "habituel" d'une voiture doit savoir à son sujet tient dans "l'interface" que la voiture expose de ce réservoire : la trappe de remplissage, la jauge de niveau, les éventuelles indications permettant d'évaluer la distance que tu peux parcourir, etc: ta classe voiture va donc fournir une série de services relatifs à la présence du réservoir, mais il n'y a strictement aucune raison pour qu'elle donne directement l'accès à celui-ci

    En ce qu concerne les mutateurs, maintenant :

    Déjà, si tu n'expose pas un accesseur, il n'y a strictement aucune raison d'exposer un mutateur

    Et, si tu exposes un accesseur, la présence d'un mutateur se justifie très rarement :

    En effet, tu devrais préférer le fait d'exposer des comportement explicites, comme "ajouterAuSolde" ou "effectuerUnPayement", plutôt que d'exposer un mutateur "setSolde".

    La raison en est toute simple : si tu définis le mutateur "setSolde", cela obligera l'utilisateur à écrire un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    Montant solde = compte.getSolde();
    solde+= montantARajouter;
    /* ou 
    solde-= montantDuPayement;
    */
    compte.setSolde(solde);
    alors que cette logique devrait etre de la responsabilité de la classe CompteBancaire.

    D'autre part, le choix du nom d'un comportement ayant pour objectif de modifier un objet permet bien souvent de lever certaines ambiguités :
    Si tu as une classe MachinChose qui se positionne en utilisant la classe point, le fait d'utiliser le comportement move(distanceX, distanceY) voir moveTo(nouvellePosition)permet bien mieux de se rendre compte qu'il s'agit d'un mouvement et qu'il peut ne pas être "immédiat" (car il faut sans doute le temps de parcourir la distance indiquée) que ne permet de le faire un simple "setPosition"

    Pour ce qui est de l'utilisation de pointeurs ou de références : Si tu veux (ou que tu dois) éviter la copie d'un objet lorsque tu le passes comme paramètre à une fonction, transmet le par référence "chaque fois que c'est possible" et par pointeur "lorsque tu n'as vraiment pas le choix".

    Les seuls cas dans lesquels tu devrait recourir aux pointeurs comme arguments d'une fonction sont:
    1. Le besoin de pouvoir indiquer que l'élément n'existe pas, alors qu'il est impossible de fournir une valeur représentant "la non existence" de l'objet
    2. Le besoin de t'interfacer avec du code C, pour récupérer le premier élément d'un tableau
    Dans tous les autres cas, transmets tes arguments:
    • par référence, (éventuellement constante, si l'objet transmis à la fonction ne doit subir aucune modification) s'il ne s'agit pas d'un type primitif (en gros, dés que sizeof(TonType) est plus grand que sizeof(int)
    • par valeur s'il s'agit d'un type primitif (ou que sizeof(TonType est plus petit ou égal à sizeof(int) ) et que la valeur dans la fonction appelante ne doit pas être modifiée
    • par référence (non constante) si la fonction appelée doit modifier la valeur dans la fonction appelante.
    Note cependant que ce dernier cas aura pour résultat de rendre ta fonction "non pure" car elle a un effet de bord, et qu'elle ne pourra donc pas être utilisée dans un cadre multi threadé

    Enfin, pour ce qui est des différentes variables :
    préfères autant que possible l'utilisation de valeur
    Si tu ne peux pas utiliser des valeurs, préfères les référence "chaque fois que faire se peut" et n'utilises les pointeurs que "lorsque tu n'as vraiment pas le choix"

    Les cas dans lesquels tu n'as pas le choix et qu'il faudra utiliser des pointeurs sont:
    • Le fait de fournir une indication du contenant au contenu (ex : un pointeur "parent")
    • Le fait que la variable soit de type polymorphe (aka : soit dans une hiérarchie de classe ) et que
      • soit la variable est créée par une fonction tierce et tu dois en prendre la responsabilité de la durée de vie
      • soit tu n'est pas en mesure (pour des raisons de portées essentiellement) d'initialiser ta variable directement au moment où tu la décalre
    Note à ce sujet que l'on préférera bien souvent, surtout depuis la sortie de la nouvelle norme, avoir recours aux pointeurs intelligents (unique_ptr, shared_ptr ou weak_ptr) car il se manipulent par valeur ou par référence et qu'ils apportent d'énormes avantages en terme de gestion correcte de la durée de vie

    Enfin, pour ce qui est de l'allocation dynamique : Tu ne devrais avoir à écrire un new que dans le cas d'objets polymorphes

    Pour tout ce qui a trait à la gestion de collections d'objets, reportes toi aux classes prévues à cet effet
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  6. #6
    Membre éclairé
    Profil pro
    Étudiant
    Inscrit en
    Janvier 2008
    Messages
    253
    Détails du profil
    Informations personnelles :
    Localisation : France, Haute Savoie (Rhône Alpes)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Janvier 2008
    Messages : 253
    Par défaut
    Bonjour Galérien et Koala, vous m'avez gâté, merci

    Je vais tenter de faire honneur à ces réponses de bonne qualité!

    Citation Envoyé par koala01 Voir le message
    En gros, tu peux avoir deux sortes de classes :
    A voir les différents traits que tu cites pour chacune, j'ai clairement plus une approche d'entité même si un faible nombre de mes classes peut rentrer dans une logique de valeur.

    Si je comprends bien et pour en être tout a fait certain, dans le cas de classes d'entité je ne dois pas pouvoir faire ça:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     
    monEntite a;
    monEntite b = a; // Copie d'instance
    Par extension il me semble donc obligatoire de transmettre ces instances à une fonction par référence sans quoi le changement de scope provoquera une copie pourtant interdite. Oui?

    Cette copie peut-elle être interdite formellement où elle reste une bonne pratique à laquelle il faut s'astreindre?

    A priori, tu ne devrais donc fournir un accesseur sur une donnée que si le fait de fournir l'accès à cette donnée fait partie des services que l'on est en droit d'attendre de la part de la classe
    Certainement, on est tout à fait d'accord là dessus.
    L'encapsulation m'oblige à mettre mes attributs privés mais pas à fournir des accesseurs/mutateurs dessus. C'est selon les besoins et à ma discrétion, suivant les cas évoqués après dans ton message.

    Pour ce qui est de l'utilisation de pointeurs ou de références [...]
    Les seuls cas dans lesquels tu devrait recourir aux pointeurs comme arguments d'une fonction sont:
    1. Le besoin de pouvoir indiquer que l'élément n'existe pas, alors qu'il est impossible de fournir une valeur représentant "la non existence" de l'objet
    En fait c'était surtout ce point qui me servait d'élément central de réflexion.

    Je viens principalement du java et du PHP, où la différence valeur/référence n'est pas du tout la même qu'en C++ et est altéré par le côté haut niveau des deux langages.
    En PHP il est possible de définir certains paramètres à NULL sans avoir à overloader mes méthodes avec plusieurs signatures (c'est obligatoire en Java).
    En C++, utiliser un paramètre de type pointeur me permettrais de le mettre à NULL si il est optionnel en vérifiant scrupuleusement bien sur dans la fonction sa valeur.

    Si je résume : référence = paramètre obligatoire, pointeur = possibilité d’être NULL.

    J'ai bien compris le reste.

    Note cependant que ce dernier cas aura pour résultat de rendre ta fonction "non pure" car elle a un effet de bord, et qu'elle ne pourra donc pas être utilisée dans un cadre multi threadé
    Dans certains de mes cours d'algorithmique, on devait appeler ça des "paramètres de sortie" des fonctions.
    Justement là on aurait des accès concurrent potentiellement sur les mêmes instances.

    Note à ce sujet que l'on préférera bien souvent, surtout depuis la sortie de la nouvelle norme, avoir recours aux pointeurs intelligents (unique_ptr, shared_ptr ou weak_ptr) car il se manipulent par valeur ou par référence et qu'ils apportent d'énormes avantages en terme de gestion correcte de la durée de vie
    Très bien, c'est enregistré merci.

    Pour tout ce qui a trait à la gestion de collections d'objets, reportes toi aux classes prévues à cet effet
    Alors justement, question importante à mes yeux : les collections.

    Sachant qu'elles peuvent contenir des références plutôt que des valeurs, est-il recommandé de déclarer ces références comme constantes?
    Si on a accès à l'instance d'un membre de la collection, on pourrait éditer le contenu de la collection sans y avoir accès directement.
    Quoi que ce n'est pas exactement le contenu de la collection puisque la collection ne contient que des références. Whaaaaa c'est dur de savoir finalement

    Déclarer des collection de références ne servirait selon moi qu'à économiser de la mémoire, je n'ai pas d'autres usages en tête.


    Citation Envoyé par galerien69 Voir le message
    tu n 'évoques pas la concurrence dans ton 1er post.
    En fait je pensais évoquer la concurrence de l'accès sur str1 : il peut être atteint depuis l'intérieur de la classe A mais aussi depuis le main directement puisque c'est là qu'il a été instancié.

    Avec ces explications supplémentaire, ce cas peut être évité en déclarant _attrib en valeur. C'est logique d'un autre côté, étant privé, il ne peut exister que dans l'instance. Sinon il devient publique dès lors qu'on peut le modifier en dehors de l'instance.


    Bonne soirée et merci encore.

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. [POO] conception des classes
    Par poukill dans le forum C++
    Réponses: 229
    Dernier message: 19/07/2006, 08h28
  2. [conception] reprise classes mal pensées
    Par in dans le forum Général Java
    Réponses: 8
    Dernier message: 05/06/2006, 13h45
  3. Conception de classe
    Par Azharis dans le forum C++
    Réponses: 9
    Dernier message: 17/12/2005, 10h15
  4. Classe, pile, pointeurs et casse-tête!
    Par zazaraignée dans le forum Langage
    Réponses: 6
    Dernier message: 26/09/2005, 16h57
  5. probleme de conception de classe
    Par NhyMbuS dans le forum C++
    Réponses: 2
    Dernier message: 08/05/2005, 17h10

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo