Contact Me
antibuginformatique@yahoo.ca

français

 Home   Services   Downloads   Writings 

Les pointeurs - Quelques notions de base


Patrick Lavoie
anti-bug informatique
5 mai 2002
Copyright © 2002


Pointeur

Un pointeur n'est simplement qu'une variable qui pointe sur un emplacement dans la mémoire. La valeur qu'il contient, c'est l'adresse mémoire.

Allocation

Une règle générale concernant l'allocation et la déallocation de la mémoire est que celui qui alloue de la mémoire est également celui qui libère la mémoire allouée. Si par exemple un objet alloue de la mémoire pour la retourner à l'appelant, seulement cet objet devrait pouvoir libérer la mémoire et fournir une méthode.

class foo
{
public :
    foo()
    {
        m_memory = new char[ 10 ];
    }

    ~foo()
    {
        delete m_memory;
    }

    foo* CreateObject()
    {
        return new foo;
    }

    void ReleaseObject( foo* object )
    {
        delete object;
    }

private :
    char*   m_memory;
};

void main()
{
    foo _myFoo;

    foo* _newFoo = _myFoo.CreateObject();
    _myFoo.ReleaseObject();
}

Dans l'exemple précédant, on peut voir que la méthode ReleaseObject libère la mémoire allouée par CreateObject. En procédant de cette manière, on peut facilement décider d'utiliser un autre mécanisme d'allocation de mémoire sans affecter le reste du programme.

Par cette même règle, on libère la mémoire de la même façon qu'elle a été allouée. De plus, pour libérer la mémoire allouée, si on n'utilise pas la bonne méthode de déallocation, on aura comme résultat de le mémoire corrompue ou une fuite de mémoire. Alors voici quelques équivalences :

  • new - delete (C++)
  • new[ ] - delete[ ] (C++)
  • malloc - free (C run-time)
  • calloc - free (C run-time)
  • GlobalAlloc - GlobalFree (Win32)
  • LocalAlloc - LocalFree (Win32)

  • En suivant ces règles, on évite plusieurs maux de têtes en recherchant des fuites de mémoire ou de la corruption de la mémoire. Par le fait même, on ouvre également la porte à la possibilité d'apporter des changements plus facilement, plus rapidement et surtout en minimisant le risque d'introduire des nouvelles erreurs.

    Assignation

    En assignant une valeur à un pointeur, on ne fait que déplacer l'endroit vers lequel il pointe dans la mémoire. Pour pouvoir accéder au contenu vers lequel in pointe, on doit le déréférencer. Pour déréférencer un pointeur, on ajoute un * devant le pointeur.

    int i = 10;
    int j = 7;
    int* pi = &i;       // pi pointe sur i
    int* pj = &j;       // pj pointe sur j
    
    *pi = 20;           // on déréférence pi pour accéder à son contenu. i = 20
    *pj = 14;           // on déréférence pj pour accéder à son contenu. j = 14
    pi = pj;            // pi pointe au même endroit que pj
    *pj = 5;            // j = 5, *pj = 5 et *pi = 5 parce que pi pointe sur pj
    

    Références

    Les références ont été introduites avec le C++. Les références jouent le même rôle que les pointeurs à l'exception que leur but est de simplifier la lecture et la compréhension du code. Une référence ne fait que référencer une adresse mémoire exactement comme un pointeur. Un pointeur peut être NULL tandis qu'une référence ne peut être NULL puisque la référence fonctionne comme un pointeur déréférencé et qu'on ne peut déréférencer un pointeur NULL. Une référence se lit comme une variable ordinaire, c'est-à-dire qu'on n'a pas besoin d'ajouter un * pour le déréférencer.

    int i = 5;
    int* pi = &i;
    int& ri = i;        // référence de ri sur i
    int& rj = *p;       // référence de rj sur le contenu de p
    *pi = 10;           // on doit déréférencer pi pour accéder à son contenu
    ri = 5;             // tandis qu'une référence fonctionne comme une variable
    rj = 10;            // ou un pointeur déjà déréférencé
    

    Où vont les symboles * et &?

    Ces symboles prennent différentes significations dépendant de l'endroit où ils sont situés. Le symbole & peut vouloir dire qu'on déclare une référence ou bien vouloir dire qu'on veut l'adresse mémoire d'une variable. Le symbole * peut vouloir dire qu'on veut déréférencer un pointeur ou qu'on veut déclarer un type de pointeur.

    int* pi = new int( 5 ); // * après le type indique un type de pointeur
    int i = *pi;            // * avant une variable indique qu'on déréférence
    
    int& ri = i;            // & après le type indique un type de référence
    int* pj = &i;           // & avant une variable indique l'adresse mémoire
    

    Arithmétique de pointeurs

    Les pointeurs permettent d'accéder et de parcourir la mémoire. Si on a un tableau de valeurs, on peut accéder à ses valeurs de deux manières. Une façon claire d'accéder aux valeurs est d'utiliser les symboles [ ].

    int _array[ 10 ];
    _array[ 3 ] = 10;
    

    Ce qui se passe en réalité pour accéder au quatrième élément du tableau, c'est qu'on prend l'adresse de base du tableau et qu'on lui additionne 4 fois la taille d'un élément.

    int _array[ 10 ];
    int* p = _array;
    p += 4;
    *p = 10;
    
    ou
    int _array[ 10 ];
    int* p = _array;
    *( p + 4 ) = 10;
    

    L'arithmétique de pointeur prend en compte la taille d'un élément. Alors incrémenter un pointeur de int de 1, augmente en réalité l'adresse mémoire sur laquelle il pointe de 4 parce que la taille d'un int est 4 octets. Tandis qu'un pointeur de char augmenterait seulement d'un octet en mémoire puisque qu'un char n'occupe qu'un seul octet.

    Si, par exemple, on a un tableau de deux doubles, voici sa représentation en mémoire :

    double _array[ 2 ];
    
    // p pointe à l'adresse mémoire 0x20 puisque _array se situe à 0x20.
    double* p = _array;
    
    // Ici, on fait pointer _element1 à l'adresse mémoire occupé par le premier
    // élément du tableau. Faire pointer sur le premier élément ou à l'espace
    // mémoire du tableau, c'est équivalent. Donc _element1 pointe aussi sur 0x20.
    // Notez bien le symbole & devant _array. Si on avait seulement écrit
    // _array[ 0 ], ça aurait indiqué le contenu du premier élément. En ajoutant
    // le symbole & devant _array, ça indique qu'on veut en fait l'adresse du
    // premier élément.
    double * _element1 = &_array[ 0 ];
    
    // Ici, _element2 pointe sur le deuxième élément du tableau. Il pointe donc
    // à l'adresse 0x28 puisque la taile d'un double est de 8 octets.
    double * _element2 = &_array[ 1 ];
    
    // On aurait aussi bien pu accéder au deuxième élément de cette manière.
    // _element2 pointe aussi sur 0x28. En additionnant 1, le pointeur
    // s'incrémente de 1 fois la taille d'un double, soit de 8 octets. Ici, on
    // n'a pas besoin d'ajouter le symbole & devant _array puisqu'on n'a pas
    // indiqué qu'on voulait le contenu d'un élément en utilisant les [].
    // _array est déjà un pointeur et on pointe sur l'élément suivant en lui
    // additionnant 1.
    _element2 = _array + 1;
    

    Cast C vs Cast C++

    En C++, il y a quatre types de cast. Il y a reinterpret_cast, static_cast, dynamic_cast et const_cast.

    L'utilité du const_cast se limite à éliminer le modificateur const.

    Le reinterpret_cast, convertit aveuglément n'importe quoi en n'importe quoi. Le compilateur n'effectue aucune validation sur la conversion. Il est donc très facile de mal utiliser reinterpret_cast et de causer des problèmes. Le reinterpret_cast est équivalent au cast C où on ne fait que mettre entre parenthèses le type vers lequel on veut convertir. Il est souvent utilisé pour convertir un pointeur quelconque en pointeur void ou en valeur int et vice-versa.

    Le static_cast, fait quelques validations au moment de la compilation. Il est donc plus sécuritaire que le reinterpret_cast. Il peut cependant laisser passer quelques erreurs qui ne peuvent qu'être détectées au moment de l'exécution.

    class Base {};
    class Derivee : public Base {};
    
    void foo( Base* pb, Derivee* pd )
    {
        // Correct parce pd est un pointeur sur Derivee et qu'il dérive de Base
        Base* pTmpB = static_cast< Base* >( pd );
    
        // Peut-être incorrect parce pb ne pointe pas nécessairement sur une
        // instance de Base. Ce genre de validation ne peut être fait qu'au moment
        // de l'exécution. 
        Derivee* pTmpD = static_cast< Derivee* >( pb );
    }
    

    Par contre, le dynamic_cast fait sa validation au moment de l'exécution. Alors dans l'exemple précédent, si on avait remplacé le second static_cast par un dynamic_cast, on aurait pu détecter si le pointeur pb pointait effectivement sur une instance de Derivee. Si dynamic_cast détecte une erreur, il retourne NULL. Avec le dynamic_cast on n'a donc jamais de pointeur invalide contrairement aux autre casts.






    Copyright © 1996-2017
    anti-bug informatique inc.
    All rights reserved.