Partage
  • Partager sur Facebook
  • Partager sur Twitter

Passage par valeur, par adresse, par référence ?

Sujet résolu
    18 février 2018 à 17:05:23

    Bonjour,

    on m'a dit qu'il est possible de passer un objet a une méthode de 3 manières différentes : par valeur, par adresse ou par référence.

    Je me suis donc renseigné sur ces 3 méthodes mais au final je me retrouve encore plus perdu qu'avant :euh:

    Quelqu'un pourrais t il m'expliquer la différence entres celle-ci (avec un exemple si possible) ainsi que le rôle de constructeur de copie dans celle ci ?

    -
    Edité par Tenkai188 18 février 2018 à 17:06:39

    • Partager sur Facebook
    • Partager sur Twitter
      18 février 2018 à 18:11:48

      Salut,

      De manière générale, lorsque tu crée une fonction proche de

      void foo(int i){
          /* ... */
      }

      (quelque soit le type de i, même si j'ai utilisé un int pour l'occasion), on dit que l'on transmet le paramètre par copie, parce que, si la fonction vient à modifier le paramètre, elle le fera sur... une copie de la variable qui a été transmise par la fonction appelante.

      Essayes le code qui suit pour t'en persuader:

      void foo(int value){
          value+=3;
          std::cout<<"Dans foo j'ajoute 3 à value; value vaut maitnenant"<<value<<"\n";
      }
      int main(){
          int value{5};
          std::cout<<"Avant l'appel de foo, value vaut "<<value<<"\n";
          foo(value);
          std::cout<<"Apres l'appel de foo, value vaut "<<value<<"\n";
          return 0;
      }

      qui te proposera un affichage proche de

      Avant l'appel de foo, value vaut 5
      Dans foo j'ajoute 3 a value; value vaut maintenant 8
      Apres l'appel de foo, value vaut 5

      Comme tu peux le constater, bien que le paramètre de foo porte exactement le même nom que la variable qui a servi d'argument dans la fonction main, il s'agit de variables totalement différentes: la fonction foo ne modifie pas la valeur de value qui est utilisée par la fonction main.

      Ca, c'est le cas "de base".  Maintenant, on peut transmettre les paramètres par référence, et les choses vont changer un tout petit peu.

      Une référence, c'est ce que l'on appelle un alias sur une donnée. Cet alias, quand il sera utilisé, utilisera systématiquement ... la variable à laquelle il fait référence. On indique que l'on veut transmettre une référence en utilisant l'esperluette & entre le type et le nom du paramètre, soit sous une forme proche de

      /* bar prend un élément de type int par REFERENCE */
      void bar(int & ref){
          /...*/
      }

      Comme ref fait... référence à la variable qui est transmise en argument dans la fonction appelante, bar va avoir exactement le même résultat que si... le code de bar avait directement été appelé dans la fonction appelante.  Cette fonction (bar) va donc modifier durablement la valeur de la variable de main qui lui a été transmise.  Observes:

      void bar(int &ref){
          ref+=3;
          std::cout<<"Dans bar j'ajoute 3 à ref; ref vaut maitnenant"<<ref<<"\n";
      }
      
      int main(){
          int value{5};
          std::cout<<"Avant l'appel de bar, value vaut "<<value<<"\n";
          bar(value);
          std::cout<<"Apres l'appel de bar, value vaut "<<value<<"\n";
          return 0;
      }

      qui te proposera un affichage proche de

      Avant l'appel de bar, value vaut 5
      Dans bar j'ajoute 3 a ref; ref vaut maintenant 8
      Apres l'appel de bar, value vaut 8

      Comme tu peux le constater, malgré le fait que le nom soit différent (value dans main et ref dans bar), la fonction bar a réellement travaillé avec la donnée qui appartenait à la fonction main ;)

      La troisième solution est la transmission par adresse: il faut savoir que toutes les données que tu peux manipuler sont forcément représentées "quelque part" dans la mémoire de ton ordinateur.  Pour que ton ordinateur puisse accéder à ces données, il faut qu'il puisse savoir "où aller chercher la donnée qui l'intéresse".

      Chaque case mémoire est donc identifiable par ce que l'on appelle une "adresse", un peu comme les appartements d'un immeuble HLM dont chacun est identifiable par un numéro qui lui est spécifique.

      Il faut d'ailleurs savoir que c'est le seul mode de fonctionnement interne dont dispose l'ordinateur: quand on lui demande d'accéder à une donnée particulière, on lui demande en réalité d'accéder ... au contenu d'une adresse mémoire spécifique ;)

      On appelle généralement la donnée qui représente l'adresse à laquelle se trouve une donnée spécifique un pointeur.  Pour obtenir l'adresse d'une donnée, on peut utiliser le symbole esperluette & (de nouveau) juste avant le nom de la donnée existante, et, pour représenter le fait que l'on utilise l'adresse d'une donnée, on utilise le symbole étoile * dans la déclaration, entre le type de la donnée et le nom du pointeur.

      De même, pour accéder à une donnée qui se trouve à une adresse particulière, nous ferons précéder le nom du pointeur du symbole *

      Ainsi, une fonction proche de

      /* doIt s'attend à recevoir un pointeur sur un entier
       * ou, si tu préfères, l'adresse mémoire à laquelle elle
       * devrait trouver une donnée de type int
       */
      void doIt(int * ptr){
          /* ... */
      }

      s'attend à recevoir... l'adresse à laquelle elle pourra trouver une donnée de type int.

      Bien sur, vu que l'on travaille désormais avec une adresse mémoire, le résultat sera exactement le même que si on avait utilisé une référence, mais le code pour y parvenir devra malgré tout changer un peu... Observes:

      void doIt(int * ptr){
          /* si j'utilise juste ptr, je vais changer l'adresse...
           * ce n'est pas ce que je veux: je veux changer la valeur
           * de ce qui se trouve à  l'adresse représentée par ptr
           */
          *ptr += 3;
          /* de même, je ne veux pas afficher l'adresse de la variable
           * mais la valeur qui se trouve à l'adresse  indiquée
           */
          std::cout<<"Dans doIt j'ajoute 3 à ce qui se trouve à l'adresse\n
                   <<"representee par ptr; *ptr vaut maitnenant"<<*ptr<<"\n";
      }
      
      int main(){
          int value{5};
          std::cout<<"Avant l'appel de doIt, value vaut "<<value<<"\n";
          doIt(value);
          std::cout<<"Apres l'appel de doIt, value vaut "<<value<<"\n";
          return 0;
      }

      qui occasionnera un affichage proche de

      Avant l'appel de doIt, value vaut 5
      Dans doIt j'ajoute 3 à ce qui se trouve à l'adresse
      representee par ptr; *ptr vaut maitnenant 8
      Apres l'appel de doIt, value vaut 8

      Voilà pour ce qui concerne les bases du passage par paramètre.

      Il faut savoir que:

      • l'utilisation de pointeurs pose énormément de problèmes, et que l'on n'y aura donc recours que si l'on n'a pas d'autres choix
      • l'utilisation de référence présente pas mal d'avantage, tant sur l'utilisation de pointeurs, que du point de vue des performances, mais, sur ce point, je t'invite à t'intéresser à cette discussion ;)
      • Partager sur Facebook
      • Partager sur Twitter
      Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
        19 février 2018 à 15:40:05

        Merci pour cette explication koala01 grace à toi j'y vois beaucoup plus clair :)

        j'ai cependant encore quelques questions :

        1. Concernant le constructeur de copie: il semble logiquement nécessaire pour le passage par valeur mais l'est il aussi pour le passage par référence et par adresse ?

        2. J'ai vu que lors d'un passage d'objet par référence on précise qu'il s'agit d'une constante

        void resultatSomme(const Nombre &, const Nombre&);//exemple

        Cela est il aussi nécessaire pour le passage par adresse ?

        Merci encore pour ton aide :)

        • Partager sur Facebook
        • Partager sur Twitter
          19 février 2018 à 17:11:04

          1) Concernant le constructeur de copie: il semble logiquement nécessaire pour le passage par valeur mais l'est il aussi pour le passage par référence et par adresse ?

          C'est une erreur de croire qu'un passage par valeur implique forcement un constructeur de copie. Il y a aussi le constructeur de déplacement.

          Le principal but de la référence est de ne pas copier, mais de référencer une variable, donc pas d'appel au constructeur de copie. La seule exception est la référence constante qui peut créer des objets temporaires en appelant explicitement un constructeur. Ce n'est plus une copie, mais un appel de constructeur préalablement définie. En gros tous les paramètres prenant un std::string const& et dans lesquels on fournit un chaîne C ("abc") font une référence temporaire.

          Les pointeurs sont un type de base et par conséquent les règles de base s'appliquent dessus: un pointeur passé par valeur est copié. Mais on ne copie qu'un pointeur, pas les données pointées.

          2) Tout dépend du but recherché. Si on ne veut pas modifier la valeur, alors le const s'impose.

          -
          Edité par jo_link_noir 19 février 2018 à 17:11:30

          • Partager sur Facebook
          • Partager sur Twitter
            19 février 2018 à 17:44:19

            Merci cela répond à mes dernières questions :)

            Je passe le sujet en résolu.

            Encore merci a vous 2 :)

            • Partager sur Facebook
            • Partager sur Twitter

            Passage par valeur, par adresse, par référence ?

            × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
            × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
            • Editeur
            • Markdown