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 :

Copie d'objet en passant un objet et en retournant son adresse ?


Sujet :

C++

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre du Club
    Homme Profil pro
    Inscrit en
    Mai 2012
    Messages
    7
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations forums :
    Inscription : Mai 2012
    Messages : 7
    Par défaut Copie d'objet en passant un objet et en retournant son adresse ?
    Bonjour à tous,
    Je m'excuse pour cette question bête : je viens de m'apercevoir que j'avais une grosse lacune dans la matière, en renvoyant une adresse d'un objet.

    Pour l'exemple, j'ai une classe Personnage qui a de base un nom et une vie.

    Si j'ai deux personnages jean et hector qui ont chacun 10 points de vie
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     
    Personnage *jean = new Personnage("jean",10);
    Personnage *hector = new Personnage("hector",10);
    puis que je fais (1)
    et que j'enlève de la vie à hector, jean perdra la vie aussi, puisque jean est devenu hector, ok.

    Maintenant avec la fonction suivante :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    Personnage* Personnage::getPersonnage(Personnage p) {
    	return &p;
    }
    // pas de static, là n'est pas le problème
    Si, à la place de (1) je fais :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
     
    jean = jean->getPersonnage(*hector);
    et que je diminue la vie de hector, jean prends le nom de hector, mais ses points restent échangés. J'avoue ne pas comprendre pourquoi.
    J'ai deux autres fonctions :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     
    Personnage* Personnage::getPersonnage2(Personnage* p) { return p; }
    Personnage* Personnage::getPersonnage3(Personnage& p) { return &p; }
    et là, si je fais :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
     
    jean = jean->getPersonnage2(hector);
    // ou
    jean = jean->getPersonnage3(*hector);
    jean devient hector, je comprends puisqu'on passe l'objet lui-même en paramètre.

    Si une âme charitable voulait bien m'expliquer le pourquoi du comment avec getPersonnage,
    Merci par avance.

    Jean-Eude

    Edit & PS : comme dit dans le titre du post, cela réalise-t'il une copie ? Si c'est le cas, est-ce bien fait ?

  2. #2
    Inactif  


    Homme Profil pro
    Doctorant sécurité informatique — Diplômé master Droit/Économie/Gestion
    Inscrit en
    Décembre 2011
    Messages
    9 026
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 32
    Localisation : France, Loire (Rhône Alpes)

    Informations professionnelles :
    Activité : Doctorant sécurité informatique — Diplômé master Droit/Économie/Gestion
    Secteur : Enseignement

    Informations forums :
    Inscription : Décembre 2011
    Messages : 9 026
    Par défaut
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    Personnage* Personnage::getPersonnage(Personnage p) { //ici tu as une copie
    	return &p; //tu retournes un pointeur vers une variable locale /!\
    }

  3. #3
    Membre du Club
    Homme Profil pro
    Inscrit en
    Mai 2012
    Messages
    7
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations forums :
    Inscription : Mai 2012
    Messages : 7
    Par défaut
    Haha shame on me je n'y ai même pas pensé.
    Merci !

  4. #4
    Membre Expert Avatar de Trademark
    Profil pro
    Inscrit en
    Février 2009
    Messages
    762
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Février 2009
    Messages : 762
    Par défaut
    EDIT : et mon roman alors ?

    Bonjour,

    je vais reprendre tes méthodes getPersonnage pour expliquer ce que ça fait, et puis après j'expliquerai la manière commune de faire.

    Tout d'abord soyons sur que la notion de pointeur est bien définie pour toi (si tu viens de Java où il faut mettre des new partout, il va falloir changer ta méthodologie .

    Un pointeur est une case mémoire de 8 octets (souvent) qui contient une adresse. Cette case mémoire a également une adresse, comme toutes cases mémoires. Un pointeur est de la forme <type>* variable ; et on alloue la donnée pointée par un pointeur sur le tas, tandis que les autres variables sont allouées sur la pile. Un pointeur contient une adresse mémoire, et à cette adresse mémoire contient une donnée du type que tu as choisis (à priori Personnage). Pour accéder à cette case mémoire tu fais *pointeur, le '*' te dirigeant sur la case pointée par le pointeur. Une fois que tu as alloué une variable avec un new, tu as la responsabilité de libérer la mémoire occupé par ce pointeur.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    Personnage *jean = new Personnage("jean",10);
    Personnage *hector = new Personnage("hector",10);
    jean = hector
    Dans ce premier exemple, tu as alloué jean et hector sur le tas. Par conséquent tu peux deviner que jean et hector sont des cases mémoires contenant une adresse. Maintenant, sacre bleu, tu affectes hector à jean, et donc tu perds l'adresse pointée par jean, et tu ne pourras plus accéder à cette zone mémoire allouée, et par conséquent la libérer (avec l'opérateur delete, dans ce cas tu ferais : [inlinecode]delete jean;[/inlinecode]). Maintenant l'état de jean et hector est clair: il existe deux cases mémoires distinctes pour jean et hector MAIS elles contiennent toutes les deux la même adresse et pointent donc vers la même donnée.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    jean = jean->getPersonnage(*hector);
    Personnage* Personnage::getPersonnage(Personnage p) { return &p; }
    Ici, c'est un peu différent que précédemment car il y a une copie. Tu passes hector en argument, mais pas son adresse mémoire, mais la donnée que contient hector, que tu obtiens en faisant *hector. Ta méthode reçoit un personnage, or en C++ (et en C) tous passage de paramètre s'effectue par valeur. Par conséquent tu copies *hector dans p.
    p est maintenant une variable allouée sur la pile car il n'y a pas eu de "new" lors de la copie.

    Allouée sur la pile, ça veut dire quoi ? En bref, ça veut dire que toutes les variables qui seront allouées sur la pile ne seront accessible et valides que dans le contexte actuel, le contexte actuel est délimité par des '{}'. Donc dans ton code, p sera seulement accessible dans {return &p; }. Ça semble logique.

    Le problème c'est que tu retournes l'adresse de p. Le retour est autorisé car en définitive, il ne s'agit que d'une variable pointeur de 8 octets pointant sur Personnage, ce qui est exactement ce que le type de retour suggère. Mais tu retournes l'adresse d'une variable locale, rappelons-le, délimitée par des {}. Lorsque tu quittes ta fonction, tu quittes le contexte actuel et toutes les variables déclarées dans le contexte actuel sont supprimées !

    Dans ton main (ou la fonction appelante), tu te retrouves avec une adresse invalide, car supprimée.

    Pourquoi ça marche quand même ?

    En fait, c'est parce que la mémoire n'est pas "nettoyée", donc ta variable p qui se trouvait à l'adresse x pendant l'appel de fonction, y est toujours après l'appel. Mais la prochaine fois que tu vas appeler une fonctions, tu vas écraser la valeur de cette variable et tu tenteras d'accéder à une adresse invalide en faisant *jean. Car l'adresse contenue dans jean qui est supposée pointée vers une zone mémoire "Personnage" ne contient plus des données relatives à personnage. À partir de ce moment, des choses incohérentes vont se produire, comme ne plus avoir un bonne âge ou un bon nom. Et finalement tu tenteras d'accéder à une zone mémoire invalide et le programme te lancera bravement une "segfault".

    Donc, ne jamais retourné de variable local, n'utiliser les variables allouées sur la pile que dans leur contexte !

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    jean = jean->getPersonnage2(hector);
    Personnage* Personnage::getPersonnage2(Personnage* p) { return p; }
    Après toutes ces explications, tu commences surement à te douter de la supercherie de ce code. Si non, c'est peut-être le moment de faire une pause et de réfléchir.

    Je t'explique, tu passes par valeur l'adresse mémoire contenue dans hector à "getPersonnage2", elle est passée par copie dans p. La règle est toujours la même, p est une variable locale. Mais c'est une variable pointeur, donc la zone mémoire pointée par p n'est pas locale (car allouée avec un new). Tu retournes ensuite la valeur contenue dans p, et affectes l'adresse de hector à jean.

    C'est exactement pareil que jean = hector. Il y a une fuite mémoire, l'adresse de jean est perdue !

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    jean = jean->getPersonnage3(*hector);
    Personnage* Personnage::getPersonnage3(Personnage& p) { return &p; }
    Ici, il n'y a pas copie (en réalité si, mais on s'abstiendra de ce détail), tu peux considérer que "p" est en fait un "alias", soit un autre nom pour désigner exactement la même chose que "*hector". Donc p est *hector. Donc tu retournes l'adresse de p, qui est en fait l'adresse de "*hector" soit &(*hector), soit en réalité hector car l'adresse de la zone pointée par hector est bien la donnée contenue dans hector (ouf).

    Donc ici, contrairement au cas 2 ("getPersonnage1"), il n'y a pas de risque d'incohérence ou de segfault mais par contre, tu auras une fuite de mémoire comme dans le cas 1 et 3.


    Bon ceci dit, la manière courante est de faire un constructeur de copie. C'est-à-dire un constructeur Personnage qui prend une référence sur un autre Personnage et qui affecte un par un les nouveaux champs.

    Mais avant tout, je pense que tu fais un amalgame entre pointeur et variables locales. Normalement, à ton niveau, tu dois pouvoir te passer des pointeurs et donc ne pas faire de new ou delete. Utilise simplement des variables locales que tu passes par référence. Si tu dois retourner une de ces variables, envisage de retourner une copie (créée automatiquement en ne mettant pas la petite étoile devant Personnage par exemple). Ou alors si l'objet est trop "gros" en terme de mémoire, utilise un new. Dans ce dernier cas, il ne faudra pas que tu oublies d'utiliser le delete associé.

    Finalement quand tu seras plus expérimenté et que tu devras utiliser des pointeurs, utilise des smart pointeur. Ce sont des pointeurs qui libèrent leur mémoire tout seul.

  5. #5
    Membre du Club
    Homme Profil pro
    Inscrit en
    Mai 2012
    Messages
    7
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations forums :
    Inscription : Mai 2012
    Messages : 7
    Par défaut
    Yeah, merci pour cette explication.
    Je viens de Haskell, c'est un autre univers parallèle .
    J'ai fait un peu de C mais sans plus. Je suis donc passé à côté de plein de choses, c'est pourquoi je me mets maintenant pleinement au cpp.
    J'ai vu comme tu le mentionnes qu'il faut faire des delete, sinon c'est la catastrophe .
    Pour le tas et la pile, c'est très clair. Quant à ton explication du pourquoi çà marche quand même, je t'en remercie je l'ignorais.
    C'est vrai que si j'avais continué et eu un segmentation fault çà m'aurait conduit à faire des recherches supplémentaires...
    Pour getPersonnage2, je n'ai même pas réfléchi sur le moment, c'est vrai que c'est pas beau. D'orénavant je veillerai à faire tout le temps des delete.
    Il est vrai que j'ai fait un peu de java et avoir le garbage collector qui enlève tous les objets inutilisés est bien pratique.

    Quand tu parles de la copie qui s'effectue quand même lors du passage par référence, peux-tu compléter ?

    PS : pour les pointeurs, les références et tout le toutim il faut que je m'y mette pour pouvoir comprendre à fond, alors autant le faire dès le début

  6. #6
    Membre émérite

    Profil pro
    Inscrit en
    Décembre 2008
    Messages
    533
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Décembre 2008
    Messages : 533
    Par défaut
    Citation Envoyé par Trademark Voir le message
    Un pointeur est de la forme <type>* variable ; et on alloue un pointeur sur le tas, tandis que les autres variables sont allouées sur la pile.
    Probablement une étourderie, mais le pointeur est bien alloué sur la pile, tandis que son contenu est effectivement l'adresse d'un objet alloué sur le tas.
    Beau pavé, sinon

  7. #7
    Membre Expert Avatar de Trademark
    Profil pro
    Inscrit en
    Février 2009
    Messages
    762
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Février 2009
    Messages : 762
    Par défaut
    Probablement une étourderie, mais le pointeur est bien alloué sur la pile, tandis que son contenu est effectivement l'adresse d'un objet alloué sur le tas.
    merci, c'est corrigé

    Quand tu parles de la copie qui s'effectue quand même lors du passage par référence, peux-tu compléter ?
    En fait, ça ne te servira pas à grand chose de le savoir, mais une référence est un pointeur caché. Sauf que tu ne peux pas l'utiliser comme un pointeur et le déréférencer, c'est un peu plus propre. Mais il ne faut pas t'embrouiller avec ça, pense qu'une référence est un alias de nom, et ça sera bien.

    Mais comme je t'ai dit, il faut que tu évites les pointeurs, peut-être qu'en C c'est inévitable de les utiliser mais c'est loin d'être le cas en C++ (bon sauf les pointeurs cachés par les références mais ça il ne faut pas en tenir compte, car tu n'auras pas de perte de mémoire à cause de ça).

    Sinon je ne sais pas sur quelle plateforme tu travailles mais tu peux utiliser un utilitaire très pratique pour détecter les fuites de mémoire et autre comportement suspect qui s'appelle Valgrind, en console tu fais:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    valgrind ./monprogramme
    Et tu auras un tas d'info.

  8. #8
    Inactif  


    Homme Profil pro
    Doctorant sécurité informatique — Diplômé master Droit/Économie/Gestion
    Inscrit en
    Décembre 2011
    Messages
    9 026
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 32
    Localisation : France, Loire (Rhône Alpes)

    Informations professionnelles :
    Activité : Doctorant sécurité informatique — Diplômé master Droit/Économie/Gestion
    Secteur : Enseignement

    Informations forums :
    Inscription : Décembre 2011
    Messages : 9 026
    Par défaut
    Citation Envoyé par Trademark Voir le message
    Mais comme je t'ai dit, il faut que tu évites les pointeurs, peut-être qu'en C c'est inévitable de les utiliser mais c'est loin d'être le cas en C++

    C'est plutôt le contraire, en C++ on en bouffe pas mal de pointeurs, il faut essayer d'utiliser le plus de références/pointeurs possibles pour éviter au maximum les copies inutiles et gagner en performance.

    Après on utilisera surtout les références pour éviter les copies inutiles et les pointeurs pour les allocations dynamiques et lorsqu'on veut pouvoir changer l'objet pointé.

    Sinon, je ne vois pas pourquoi le fait de passer par une référence plutôt qu'un pointeur

    "[on] n'aura[it] pas de perte de mémoire à cause de ça"

    Une référence ne se différencie d'un pointeur que par son utilisation :
    une référence est un pointeur qui s'utilise comme la variable qu'il pointe.

  9. #9
    Membre émérite

    Profil pro
    Inscrit en
    Décembre 2008
    Messages
    533
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Décembre 2008
    Messages : 533
    Par défaut
    Citation Envoyé par Neckara Voir le message
    C'est plutôt le contraire, en C++ on en bouffe pas mal de pointeurs, il faut essayer d'utiliser le plus de références/pointeurs possibles pour éviter au maximum les copies inutiles et gagner en performance.
    Je crois que ce qu'il veut dire, c'est que les pointeurs doivent être enfouis le plus profondément possible dans l'implémentation, et que leur cycle de vie via new/delete doit être lié à celui d'une variable de la pile (ex: shared pointer).

  10. #10
    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,
    Citation Envoyé par Neckara Voir le message
    C'est plutôt le contraire, en C++ on en bouffe pas mal de pointeurs, il faut essayer d'utiliser le plus de références/pointeurs possibles pour éviter au maximum les copies inutiles et gagner en performance.

    Après on utilisera surtout les références pour éviter les copies inutiles et les pointeurs pour les allocations dynamiques et lorsqu'on veut pouvoir changer l'objet pointé.

    Sinon, je ne vois pas pourquoi le fait de passer par une référence plutôt qu'un pointeur

    "[on] n'aura[it] pas de perte de mémoire à cause de ça"

    Une référence ne se différencie d'un pointeur que par son utilisation :
    une référence est un pointeur qui s'utilise comme la variable qu'il pointe.
    En fait, la différence majeure entre une référence et un pointeur est... l'obligation d'existence de l'objet sous jascent:

    Lorsque tu utilises une référence, tu es sur qu'il existe bien un objet référencé, quitte à ce que ce soit une variable anonyme temporaire (dans le cas d'une référence constante transmise en argument), alors qu'un pointeur peut contenir l'adresse NULL (nullptr en C++ 11) qui correspond à une adresse facilement testable et connue comme étant invalide.

    De plus, une référence permet d'assurer beaucoup plus facilement la const correctness : le fait que l'on ne pourra invoquer que des fonctions s'étant engagée à ne pas modifier l'objet si celui ci doit etre constant.

    Le conseil qu'il est donc bon de donner en C++ est dés lors proche de "utilises les références (de préférence constante si l'objet ne peut pas etre modifié) autant que possible et les pointeurs quand tu n'as pas d'autres choix".

    En effet, si tu crées une classe qui doit faire référence à un élément "parent", mais qu'il peut ne pas y avoir de parent, tu n'auras pas le choix: il faudra bien que tu utilises un pointeur (dont tu devras vérifier la validité à chaque utilisation, pour etre sur qu'il y a bel et bien un parent )

    De même, si une fonction doit renvoyer un objet d'un type (faisant partie d'une hiérarchie de classe) particulier déterminé à l'exécution, tu n'auras pas d'autre choix que de passer par un pointeur et par l'allocation dynamique de la mémoire (idéalement en encapsulant le tout dans un pointeur intelligent, si tu disposes de C++11) continue d'exister une fois le retour de fonction effectué.

    Par contre, dés que tu as la certitude qu'un objet existe (par exemple, qu'un pointeur contient bel et bien une adresse valide à laquelle se trouve effectivement un objet du type indiqué (ou d'un type dérivé du type indiqué) ), il n'est franchement pas inutile de passer tout de suite une référence sur l'objet plutot qu'un pointeur sur celui-ci:

    Non seulement, les comportements polymorphes s'effectuent très bien depuis les références, mais tu as la certitude (pour autant que l'adresse contenue par le pointeur soit effectivement valide) que l'objet existe et existera tout au long du traitement, mais, en plus, cela simplifie aussi la syntaxe d'utilisation et te permet de manipuler ton objet exactement comme... s'il s'agissait de l'objet lui-même .
    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

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

Discussions similaires

  1. Réponses: 9
    Dernier message: 31/03/2011, 16h32
  2. Coordonnées d'un objet Shape passant à 0 après création
    Par Palca dans le forum ActionScript 3
    Réponses: 3
    Dernier message: 23/04/2009, 20h12
  3. Réponses: 9
    Dernier message: 08/01/2009, 18h08
  4. Réponses: 15
    Dernier message: 23/06/2006, 13h57

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