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 :

Question sur pointeur


Sujet :

C++

  1. #1
    Membre habitué
    Question sur pointeur
    Bonjour à tous,
    Je comprends le fonctionnement des pointeurs mais j'ai du mal à savoir quand les utiliser notamment sur ce petit exemple avec un tableau :
    Code :Sélectionner tout -Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    cout << "Saisir la taille du tableau ";
    			int sizeT;
    			cin >> sizeT;
    			int tab[sizeT];

    écris comme ça, çà ne marche pas, il faut le déclarer avec un pointeur :
    Code :Sélectionner tout -Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    cout << "Saisir la taille du tableau ";
    			int sizeT;
    			cin >> sizeT;
    		//	int tab[sizeT];
    			int* t = new int[sizeT];

    Pour quelle(s) raison(s) ? si quelqu'un pouvait m'expliquer MERCI

  2. #2
    Expert éminent sénior
    Salut,
    Quand tu déclares un tableau sous la forme de
    Code :Sélectionner tout -Visualiser dans une fenêtre à part
    1
    2
    3
    4
    void foo(){
        int tab[7];
        /* ... */
    }

    Ca revient à créer un tableau de taille fixe et connue à la compilation et donc à dire au compilateur quelque chose comme:
    je veux une donnée de type int, qui sera connue dans ma fonction sous le nom de tab et qui me permet de gérer les entiers par groupe de [7]
    Cette demande va avoir pour résultat le fait de que compilateur va "réserver" un espace mémoire dans la fonction correspondant à [7] fois la taille d'un int.

    Seulement, le compilateur a besoin de connaitre exactement la taille que le tableau prendra en mémoire, afin de pouvoir faire les différents calculs permettant de le positionner correctement sur la pile.

    Et, pour cela, le compilateur doit connaître -- au moment où il génère le code binaire de ton exécutable -- la taille du tableau (*).

    De plus, cette manière de travailler aura pour effet, vu que le tableau sera créé sur la pile (comprend: sans avoir recours à l'allocation dynamique de la mémoire), de soumettre le tableau aux règles strictes de durée de vie des données pour lesquelles on n'a pas recours à l'allocation dynamique de la mémoire, et donc, le tableau ne sera connu que dans la portée dans laquelle il sera déclaré.

    Le code suivant devrait te permettre de comprendre le principe:
    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
    code foo(){
        std::string str{"Hello "}; // str est construite ici, et est disponible pour toute la fonction
        int a{5}; // a est construit ici, et est disponible pour toute la fonction
        if(condition){ // les accolades crées une "protée secondaire"
            std::string autre{"Salut "}; // dans laquelle on peut créer de nouvelles variables
            int b{6};
            /* je peux aussi bien manipuler str et a que
             * autre et b ici, car les quatre données sont accessibles
             */
        } /* b est "détruite" en premier
           * autre est détruite en second
           * car les données pour lesquelles nous n'avons pas recours
           * à l'allocation dynamique de la mémoire sont détruites dans 
           * l'ordre inverse de leur déclaration
           */
        int tab[7];   // tab est créé ici et contient la place pour représenter 7 int
        a*=6;         // OK: a existe encore et peut être manipulée
        str+="World!";// OK: str existe encore et peut être manipulée
        // autre +="le monde";  PAS OK: autre n'existe plus ici 
    }/* tab est détruit ici en premier, vu que c'est la dernière donnée à avoir été déclarée
      * a est détruit ensuite
      * et, enfin, str est détuite ici
      */

    (*) Notes que tu n'est pas forcément obligé de fournir la taille sous la forme d'un nombre: si tu t'arranges pour déclarer une constante de compilation qui représente la taille de ton tableau, cela fonctionne aussi, par exemple
    Code :Sélectionner tout -Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    static const int TAILLE{7};
    /* nous pourrions d'ailleurs utiliser (depuis C++11) le mot clé constexpr sous la forme de
    static constexpr int TAILLE{7};
    */
    void foo(){
        int tab[TAILLE]; /* TAILLE étant une constante de compilation qui vaut 7,
                          * le compilateur sera content malgré tout <img src="images/smilies/icon_biggrin.gif" border="0" alt="" title=":D" class="inlineimg" />
                          */
    }


    Tout cela est très bien, mais cela t'oblige à savoir exactement le nombre d'éléments que tu veux pouvoir gérer avec ton tableau à la compilation, c'est à dire, bien longtemps avant que tu ne décide de lancer ton programme.

    Si le nombre d'éléments que ton programme devra gérer dépend du choix de l'utilisateur de ton programme, tu ne pourra donc pas utiliser ce système, et tu devras recourir à ce que l'on appelle l'allocation dynamique de la mémoire.

    En gros, cela revient à demander au système d'exploitation de nous trouver "quelque part" dans toute la mémoire disponible pour "l'ensemble du système" un "espace suffisant" que pour nous permettre d'y représenter "le nombre souhaité" d'éléments l'un à la suite de l'autre.

    Cela se fait, en C++, à l'aide de l'opérateur new Type lorsque l'on ne souhaite avoir qu'un seul élément, ou à l'aide de l'opérateurnew Type[taille] si l'on souhaite pouvoir représenter taile élément de type Type.

    Le système d'exploitation nous répondra alors en renvoyant une valeur numérique entière (généralement non signée) correspondant à l'adresse mémoire à laquelle se trouve la donnée de type Type que l'appel à new aura construite (le (pseudo)constructeur aura été appelé pour la donnée.

    Evidemment, lorsque l'on demande de créer plusieurs données de type Type l'adresse mémoire qui nous sera renvoyée sera celle de la première variable de type Type qui aura été créée, et nous pourrons la considérer comme étant le premier élément d'un tableau contigus d'éléments de type Type.

    Seulement, il y a une dernière chose à prendre en compte: lorsque tu fais appel à l'opérateur new (ou new[]), tu indique explicitement au compilateur que tu souhaites te charger personnellement de gérer la mémoire allouée à la donnée donnée créée, et, plus précisément, que tu souhaites pouvoir déterminer toi même le moment où cette mémoire pourra être libérée.

    Tu devras donc être sur d'appeler l'opérateur delete (si tu as utilisé l'opérateur [c]new[/] pour allouer la mémoire pour un seul élément) ou l'opérateur delete[](si tu as utilisé l'opérateur new[] pour allouer la mémoire pour un tableau d'éléments) sur ton pointeur au plus tard avant de perdre toute référence à la mémoire allouée.

    C'est à dire qu'il faudra travailler sous une forme proche de
    Code :Sélectionner tout -Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void foo(){
        cout << "Saisir la taille du tableau ";
        int sizeT;
        std::cin >>sizeT; // une vérification de validité serait la bienvenue ici
        int * tab= new int[sizeT];
        /* tu peux manipuler tab comme s'il s'agissait d'un tableau contenant sizeT éléments ici
         */
        delete[] tab; // sans oublier de libérer la mémoire pour tab
    }/* car tab sera oublié dés que l'exécution passera par 
      * l'accolade fermante de la fonction
      */

    ou sous une forme proche de
    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
    int * foo(){
        cout << "Saisir la taille du tableau ";
        int sizeT;
        std::cin >>sizeT; // une vérification de validité serait la bienvenue ici
        int * tab= new int[sizeT];
        /* tu peux manipuler tab comme s'il s'agissait d'un tableau contenant sizeT éléments ici
         */
        return tab;
    }
    int main(){
        int * result = foo(); // récupère l'adresse mémoire correspondant à tab dans foo()
     
        /* tu peux manipuler result comme s'il s'agissait d'un tableau contenant sizeT éléments ici
         * (à ceci près que tu ne sais plus ce que vaut sizeT)
         */
     
        delete[] result; // sans oublier de libérer la mémoire pour result (AKA tab, au niveau de foo)
    }


    (Note au passage que ce code "de bisounours" ne prend absolument pas en compte le fait qu'une exception pourrait être lancée, ce qui complique en pratique encore bien les choses )

    Mon grain de sel final

    Je crois que, si tu as réussi à suivre cette intervention jusqu'ici, les félicitations sont sans doute de rigueur.

    Seulement, voilà: bien que les explications données soient correctes et (je l'espère) compréhensibles, maintenant que tu sais tout cela, tu peux te dépêcher de l'oublier ou, à tout le moins, de ranger ces indications dans l'un des coins les plus poussiéreux de ta mémoire

    Car le fait est que, en C++, nous allons tout faire pour n'avoir à manipuler des pointeurs et l'allocation dynamique de la mémoire (et surtout, pour veiller à ce que la mémoire allouée soit libérée "en temps et en heure") que lorsque l'on n'a vraiment pas d'autre choix.

    Ainsi, si tu veux créer un tableau dont la taille risque de varier durant l'exécution, tu auras très largement intérêt à utiliser la classe std::vector, qui est prévue pour cela et qui s'occupera de gérer la mémoire "toute seule, comme une grande".

    Si bien que, au final, nous limiterons notre utilisation de l'allocation dynamique de la mémoire au seul cas pour lequel nous devrons réellement y recourir, à savoir, pour la création d'instances de classes polymorphes ou, de manière générale, ayant sémantique d'entité.

    Et encore, le recours à l'allocation dynamique de la mémoire sera malgré tout limité au stricte minimum (comprend: nous préférerons éviter d'y avoir recours s'il n'est pas absolument indispensable pour manipuler des classes ayant sémantique d'entité).

    Enfin, il faut savoir que la principale difficulté, lorsque nous avons recours à l'allocation dynamique de la mémoire, consiste à s'assurer que la mémoire soit correctement libérée au "meilleur moment possible".

    Depuis C++11 (la "nouvelle norme" concernant le C++, finalisée en 2011) nous disposons de pointeurs intelligents qui veilleront à notre place à ce que la mémoire allouée soit correctement libérée.

    Ces pointeurs intelligents sont répartis en deux grandes catégories:
    • les std::unique_ptr, que je te conseille d'utiliser "par défaut", à moins que tu ne soit dans une situation dans laquelle ils ne soient vraiment pas utilisables et
    • le couple std::shared_ptr + std::weak_ptr que je te conseille de n'utiliser que si std::unique_ptr ne fait vraiment pas l'affaire
    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

  3. #3
    Membre habitué
    Réponse à koala01
    Bjr et merci pour ta réponse. J'aurais besoin de précisions pour bien comprendre.
    Tu dis :

    Et, pour cela, le compilateur doit connaître -- au moment où il génère le code binaire de ton exécutable -- la taille du tableau (*).
    Pourquoi tu mets "*" dans la parenthèse en référence à un pointeur ?
    tu dis aussi

    (comprend: sans avoir recours à l'allocation dynamique de la mémoire)
    donc avoir recours à l'allocation dynamique de la mémoire c'est utiliser les pointeurs ?

    Merci et à +

  4. #4
    Membre actif
    Salut;

    «Écrivez nous de quoi vous avez besoin, on vous expliquera comment vous en passer !»; Coluche.

    Dans le premier exemple qui a été exposé, je note la possible tentative d'utiliser une VLA (Variable-Length Array ou Tableau de longueur variable).
    C'est autorisé en C (C99...) et à la discrétion du compilateur, par contre c'est interdit en C++, ceci au profit des conteneurs, std::vector, std::string.

    Voici un code potentiellement valable en C :

    Code C :Sélectionner tout -Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <stdio.h> 
    #include <stdlib.h> 
    #include <time.h>  
     
    int main ()
    {
      srand(time(NULL));
      size_t rsize= rand() % 10 + 2;
      /*rsize: longueur aléatoire obtenue à l’exécution*/
      int arr[rsize];
      arr[0]= 17;
      arr[1]= 33;
      for (size_t i= 0; i < rsize; ++i)
          printf("%d ", arr[i]);
    }


    Voici une équivalence en C++ (C++11...) :

    Code C++ :Sélectionner tout -Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <iostream>
    #include <vector>
     
    int main()
    {
        std::vector<int> v;
        v.push_back(17);
        v.push_back(33);
        for (auto n : v) 
            std::cout<< n<< ' ';
    }


    Ces deux codes peuvent être exécutés et comparés sur onlinegdb:
    https://onlinegdb.com/HJz8zkqDD
    https://onlinegdb.com/rki5QkcPw

    Pour ce qui est des pointeurs, tant que les bonnes habitudes du C++ ne sont pas ancrées, mieux vaut ne pas fleurter avec; enfin si l'on tient à sa jambe

    «C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do it blows your whole leg off.»; Stroustrup.

    Il faut faire preuve de patience en fait, car s'y attaquer trop tôt et dans le contexte des tableaux, ça perturbe plus que ça aide.

  5. #5
    Expert éminent sénior
    Citation Envoyé par xeron33 Voir le message
    Pourquoi tu mets "*" dans la parenthèse en référence à un pointeur ?
    tu dis aussi
    ¨Pour faire référence à l'autre endroit de mon intervention où il y a un (*)

    Parce que cette partie (le deuxième endroit où il y a un (*) ) est en relation directe avec le premier endroit où tu vois un (*), mais que j'avais autre chose à dire avant d'en parler

    donc avoir recours à l'allocation dynamique de la mémoire c'est utiliser les pointeurs ?
    HeHe...


    Il semblerait que tu aies beaucoup moins bien compris le principe des pointeurs que ce que tu ne te plais à le croire

    Car le principe du pointeur, c'est d'être une valeur numérique entière, généralement non signée, qui représente l'adresse mémoire à laquelle on (espère) trouvera une donnée ("passant éventuellement pour être")du type indiqué.

    Donc, oui, le fait d'avoir recours à l'allocation dynamique de la mémoire nous oblige fatalement à manipuler des pointeurs (ou des pointeurs intelligents), vu que l'allocation dynamique de la mémoire nous renverra **forcément** ... l'adresse mémoire à laquelle débute l'espace réservé lors de l'allocation dynamique.

    Cependant, il reste malgré tout possible d'utiliser des pointeurs sans ** forcément ** avoir recours à l'allocation dynamique de la mémoire (il "suffit" pour cela de prendre "l'adresse mémoire" de "quelque chose", à l'aide de l'opérateur & pour se retrouver à manipuler un pointeur ).
    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 habitué
    Réponse à koala01
    Merci tu dis :

    int a{5};
    Je ne connaissais pas cette façon d'écrire ça représente quoi exactement un tableau ?
    Merci

  7. #7
    Membre habitué
    réponse à kaitlyn
    Merci pour ton intervention tu dis ;

    par contre c'est interdit en C++, ceci au profit des conteneurs, std::vector, std::string.
    Je ne savais pas que le conteneur string était de longueur variable, autre chose que peut on mettre dans vector ?
    Merci

  8. #8
    Expert éminent sénior
    Citation Envoyé par xeron33 Voir le message

    Je ne connaissais pas cette façon d'écrire ça représente quoi exactement un tableau ?
    Merci
    non, c'est la nouvelle manière de définir les données lors de leur déclaration. C'est une manière qui s'adapte en théorie à n'importe quel type.
    Dans mon exemple, c'est exactement similaire à
    Code :Sélectionner tout -Visualiser dans une fenêtre à part
    int a = 5;

    Citation Envoyé par xeron33 Voir le message

    Je ne savais pas que le conteneur string était de longueur variable,
    Comme quoi, on en apprend tous les jours
    autre chose que peut on mettre dans vector ?
    Pour faire simple: à peu près n'importe quel type de donnée, qu'il s'agisse de types primitifs (comme des int, des float ou autres), ou des types de données plus complexes, y compris des classes perso
    Il faut juste veiller à ce que les données soient soit copiables, soit copiables par déplacement.
    Allez, un peu de lecture Et, si tu préfères en francais (attention, c'est une traduction automatique, qui risque donc d'être quelque peu "surprenante" ), cela se passe ici
    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

###raw>template_hook.ano_emploi###