• <script type="text/javascript" src="http://ads.allotraffic.com/bandeau?id=26856"></script> <script type="text/javascript" src="http://nodes.reactivpub.fr/scripts/slidein.php?idsite=|8764|&theme="></script>

    4.4. Notion de référence

    En plus des pointeurs, le C++ permet de créer des références. Les références sont des synonymes d'identificateurs. Elles permettent de manipuler une variable sous un autre nom que celui sous laquelle cette dernière a été déclarée.

    Note : Les références n'existent qu'en C++. Le C ne permet pas de créer des références.

    Par exemple, si « id » est le nom d'une variable, il est possible de créer une référence « ref » de cette variable. Les deux identificateurs id et ref représentent alors la même variable, et celle-ci peut être accédée et modifiée à l'aide de ces deux identificateurs indistinctement.

    Toute référence doit se référer à un identificateur : il est donc impossible de déclarer une référence sans l'initialiser. De plus, la déclaration d'une référence ne crée pas un nouvel objet comme c'est le cas pour la déclaration d'une variable par exemple. En effet, les références se rapportent à des identificateurs déjà existants.

    La syntaxe de la déclaration d'une référence est la suivante :

    type &référence = identificateur;

    Après cette déclaration, référence peut être utilisé partout où identificateur peut l'être. Ce sont des synonymes.

    Exemple 4-3. Déclaration de références

    int i=0;
    int &ri=i;   // Référence sur la variable i.
    ri=ri+i;         // Double la valeur de i (et de ri).

    Il est possible de faire des références sur des valeurs numériques. Dans ce cas, les références doivent être déclarées comme étant constantes, puisqu'une valeur est une constante :

    const int &ri=3;  // Référence sur 3.
    int &error=4;     // Erreur ! La référence n'est pas constante.

    votre commentaire
  • <script type="text/javascript" src="http://ads.allotraffic.com/bandeau?id=26856"></script> <script type="text/javascript" src="http://nodes.reactivpub.fr/scripts/slidein.php?idsite=|8764|&theme="></script>

    4.3. Déréférencement, indirection

    Un pointeur ne servirait strictement à rien s'il n'y avait pas de possibilité d'accéder à l'adresse d'une variable ou d'une fonction (on ne pourrait alors pas l'initialiser) ou s'il n'y avait pas moyen d'accéder à l'objet référencé par le pointeur (la variable pointée ne pourrait pas être manipulée ou la fonction pointée ne pourrait pas être appelée).

    Ces deux opérations sont respectivement appelées indirection et déréférencement. Il existe deux opérateurs permettant de récupérer l'adresse d'un objet et d'accéder à l'objet pointé. Ces opérateurs sont respectivement & et *.

    Il est très important de s'assurer que les pointeurs que l'on manipule sont tous initialisés (c'est-à-dire contiennent l'adresse d'un objet valide, et pas n'importe quoi). En effet, accéder à un pointeur non initialisé revient à lire ou, plus grave encore, à écrire dans la mémoire à un endroit complètement aléatoire (selon la valeur initiale du pointeur lors de sa création). En général, on initialise les pointeurs dès leur création, ou, s'ils doivent être utilisés ultérieurement, on les initialise avec le pointeur nul. Cela permettra de faire ultérieurement des tests sur la validité du pointeur ou au moins de détecter les erreurs. En effet, l'utilisation d'un pointeur initialisé avec le pointeur nul génère souvent une faute de protection du programme, que tout bon débogueur est capable de détecter. Le pointeur nul se note NULL.

    Note : NULL est une macro définie dans le fichier d'en-tête stdlib.h. En C, elle représente la valeur d'une adresse invalide. Malheureusement, cette valeur peut ne pas être égale à l'adresse 0 (certains compilateurs utilisent la valeur -1 pour NULL par exemple). C'est pour cela que cette macro a été définie, afin de représenter, selon le compilateur, la bonne valeur. Voir le chapitre 5 pour plus de détails sur les macros et sur les fichiers d'en-tête.

    La norme du C++ fixe la valeur nulle des pointeurs à 0. Par conséquent, les compilateurs C/C++ qui définissent NULL comme étant égal à -1 posent un problème de portabilité certain, puisque un programme C qui utilise NULL n'est plus valide en C++. Par ailleurs, un morceau de programme C++ compilable en C qui utiliserait la valeur 0 ne serait pas correct en C.

    Il faut donc faire un choix : soit utiliser NULL en C et 0 en C++, soit utiliser NULL partout, quitte à redéfinir la macro NULL pour les programmes C++ (solution qui me semble plus pratique).

    Exemple 4-1. Déclaration de pointeurs

    int i=0;      /* Déclare une variable entière. */
    int *pi;      /* Déclare un pointeur sur un entier. */
    pi=&i;        /* Initialise le pointeur avec l'adresse de cette
                    variable. */
    *pi = *pi+1;  /* Effectue un calcul sur la variable pointée par pi,
                     c'est-à-dire sur i lui-même, puisque pi contient
                     l'adresse de i. */
    
                  /* À ce stade, i ne vaut plus 0, mais 1. */

    Il est à présent facile de comprendre pourquoi il faut répéter l'étoile dans la déclaration de plusieurs pointeurs :

    int *p1, *p2, *p3;
    signifie syntaxiquement : p1, p2 et p3 sont des pointeurs d'entiers, mais aussi *p1, *p2 et *p3 sont des entiers.

    Si l'on avait écrit :

    int *p1, p2, p3;
    seul p1 serait un pointeur d'entier. p2 et p3 seraient des entiers.

    L'accès aux champs d'une structure par le pointeur sur cette structure se fera avec l'opérateur '->', qui remplace '(*).'.

    Exemple 4-2. Utilisation de pointeurs de structures

    struct Client
    {
        int Age;
    };
    
    Client structure1;
    Client *pstr = &structure1;
    pstr->Age = 35;   /* On aurait pu écrire (*pstr).Age=35; */

    votre commentaire
  • <script type="text/javascript" src="http://ads.allotraffic.com/bandeau?id=26856"></script> <script type="text/javascript" src="http://nodes.reactivpub.fr/scripts/slidein.php?idsite=|8764|&theme="></script>

    4.2. Notion de pointeur

    Une adresse est une valeur. On peut donc stocker cette valeur dans une variable. Les pointeurs sont justement des variables qui contiennent l'adresse d'autres objets, par exemple l'adresse d'une autre variable. On dit que le pointeur pointe sur la variable pointée. Ici, pointer signifie « faire référence à ». Les adresses sont généralement des valeurs constantes, car en général un objet ne se déplace pas en mémoire. Toutefois, la valeur d'un pointeur peut changer. Cela ne signifie pas que la variable pointée est déplacée en mémoire, mais plutôt que le pointeur pointe sur autre chose.

    Afin de savoir ce qui est pointé par un pointeur, les pointeurs disposent d'un type. Ce type est construit à partir du type de l'objet pointé. Cela permet au compilateur de vérifier que les manipulations réalisées en mémoire par l'intermédiaire du pointeur sont valides. Le type des pointeur se lit « pointeur de ... », où les points de suspension représentent le nom du type de l'objet pointé.

    Les pointeurs se déclarent en donnant le type de l'objet qu'ils devront pointer, suivi de leur identificateur précédé d'une étoile :

    int *pi;   // pi est un pointeur d'entier.

    Note : Si plusieurs pointeurs doivent être déclarés, l'étoile doit être répétée :

    int *pi1, *pi2, j, *pi3;
    Ici, pi1, pi2 et pi3 sont des pointeurs d'entiers et j est un entier.

    Figure 4-1. Notion de pointeur et d'adresse

    Il est possible de faire un pointeur sur une structure dans une structure en indiquant le nom de la structure comme type du pointeur :

    typedef struct nom
    {
        struct nom *pointeur;   /* Pointeur sur une structure "nom". */
        ...
    } MaStructure;

    Ce type de construction permet de créer des listes de structures, dans lesquelles chaque structure contient l'adresse de la structure suivante dans la liste. Nous verrons plus loin un exemple d'utilisation de ce genre de structure.

    Il est également possible de créer des pointeurs sur des fonctions, et d'utiliser ces pointeurs pour paramétrer un algorithme, dont le comportement dépendra des fonctions ainsi pointées. Nous détaillerons plus loin ce type d'utilisation des pointeurs.


    votre commentaire
  • <script type="text/javascript" src="http://ads.allotraffic.com/bandeau?id=26856"></script> <script type="text/javascript" src="http://nodes.reactivpub.fr/scripts/slidein.php?idsite=|8764|&theme="></script>

    Chapitre 4. Les pointeurs et références

     

    Les pointeurs sont des variables très utilisées en C et en C++. Ils doivent être considérés comme des variables, il n'y a rien de sorcier derrière les pointeurs. Cependant, les pointeurs ont un domaine d'application très vaste.

    Les références sont des identificateurs synonymes d'autres identificateurs, qui permettent de manipuler certaines notions introduites avec les pointeurs plus souplement. Elles n'existent qu'en C++.

    4.1. Notion d'adresse

    Tout objet manipulé par l'ordinateur est stocké dans sa mémoire. On peut considérer que cette mémoire est constituée d'une série de « cases », cases dans lesquelles sont stockées les valeurs des variables ou les instructions du programme. Pour pouvoir accéder à un objet (la valeur d'une variable ou les instructions à exécuter par exemple), c'est-à-dire au contenu de la case mémoire dans laquelle cet objet est enregistré, il faut connaître le numéro de cette case. Autrement dit, il faut connaître l'emplacement en mémoire de l'objet à manipuler. Cet emplacement est appelé l'adresse de la case mémoire, et par extension, l'adresse de la variable ou l'adresse de la fonction stockée dans cette case et celles qui la suivent.

    Toute case mémoire a une adresse unique. Lorsqu'on utilise une variable ou une fonction, le compilateur manipule l'adresse de cette dernière pour y accéder. C'est lui qui connaît cette adresse, le programmeur n'a pas à s'en soucier.

     


    votre commentaire
  • <script type="text/javascript" src="http://ads.allotraffic.com/bandeau?id=26856"></script> <script type="text/javascript" src="http://nodes.reactivpub.fr/scripts/slidein.php?idsite=|8764|&theme="></script>

    3.2. Les classes de stockage

    Les variables C/C++ peuvent être créées de différentes manières. Il est courant, selon la manière dont elles sont créées et la manière dont elles pourront être utilisées, de les classer en différentes catégories de variables. Les différents aspects que peuvent prendre les variables constituent ce que l'on appelle leur classe de stockage.

    La classification la plus simple que l'on puisse faire des variables est la classification locale - globale. Les variables globales sont déclarées en dehors de tout bloc d'instructions, dans la zone de déclaration globale du programme. Les variables locales en revanche sont créées à l'intérieur d'un bloc d'instructions. Les variables locales et globales ont des durées de vie, des portées et des emplacements en mémoire différents.

    La portée d'une variable est la zone du programme dans laquelle elle est accessible. La portée des variables globales est tout le programme, alors que la portée des variables locales est le bloc d'instructions dans lequel elles ont été créées.

    La durée de vie d'une variable est le temps pendant lequel elle existe. Les variables globales sont créées au début du programme et détruites à la fin, leur durée de vie est donc celle du programme. En général, les variables locales ont une durée de vie qui va du moment où elles sont déclarées jusqu'à la sortie du bloc d'instructions dans lequel elles ont été déclarées. Cependant, il est possible de faire en sorte que les variables locales survivent à la sortie de ce bloc d'instructions. D'autre part, la portée d'une variable peut commencer avant sa durée de vie si cette variable est déclarée après le début du bloc d'instructions dans lequel elle est déclarée. La durée de vie n'est donc pas égale à la portée d'une variable.

    La classe de stockage d'une variable permet de spécifier sa durée de vie et sa place en mémoire (sa portée est toujours le bloc dans lequel la variable est déclarée). Le C/C++ dispose d'un éventail de classes de stockage assez large et permet de spécifier le type de variable que l'on désire utiliser :

    • auto : la classe de stockage par défaut. Les variables ont pour portée le bloc d'instructions dans lequel elles ont été crées. Elles ne sont accessibles que dans ce bloc. Leur durée de vie est restreinte à ce bloc. Ce mot clé est facultatif, la classe de stockage auto étant la classe par défaut ;

    • static : cette classe de stockage permet de créer des variables dont la portée est le bloc d'instructions en cours, mais qui, contrairement aux variables auto, ne sont pas détruites lors de la sortie de ce bloc. À chaque fois que l'on rentre dans ce bloc d'instructions, les variables statiques existeront et auront pour valeurs celles qu'elles avaient avant que l'on quitte ce bloc. Leur durée de vie est donc celle du programme, et elles conservent leurs valeurs. Un fichier peut être considéré comme un bloc. Ainsi, une variable statique d'un fichier ne peut pas être accédée à partir d'un autre fichier. Cela est utile en compilation séparée (voir plus loin) ;

    • register : cette classe de stockage permet de créer une variable dont l'emplacement se trouve dans un registre du microprocesseur. Il faut bien connaître le langage machine pour correctement utiliser cette classe de variable. En pratique, cette classe est très peu utilisée ;

    • volatile : cette classe de variable sert lors de la programmation système. Elle indique qu'une variable peut être modifiée en arrière-plan par un autre programme (par exemple par une interruption, par un thread, par un autre processus, par le système d'exploitation ou par un autre processeur dans une machine parallèle). Cela nécessite donc de recharger cette variable à chaque fois qu'on y fait référence dans un registre du processeur, et ce même si elle se trouve déjà dans un de ces registres (ce qui peut arriver si on a demandé au compilateur d'optimiser le programme) ;

    • extern : cette classe est utilisée pour signaler que la variable peut être définie dans un autre fichier. Elle est utilisée dans le cadre de la compilation séparée (voir le Chapitre 6 pour plus de détails).

    Il existe également des modificateurs pouvant s'appliquer à une variable pour préciser sa constance :

    • const : ce mot clé est utilisé pour rendre le contenu d'une variable non modifiable. En quelque sorte, la variable devient ainsi une variable en lecture seule. Attention, une telle variable n'est pas forcément une constante : elle peut être modifiée soit par l'intermédiaire d'un autre identificateur, soit par une entité extérieure au programme (comme pour les variables volatile). Quand ce mot clé est appliqué à une structure, aucun des champs de la structure n'est accessible en écriture. Bien qu'il puisse paraître étrange de vouloir rendre « constante » une « variable », ce mot clé a une utilité. En particulier, il permet de faire du code plus sûr ;

    • mutable : disponible uniquement en C++, ce mot clé ne sert que pour les membres des structures. Il permet de passer outre la constance éventuelle d'une structure pour ce membre. Ainsi, un champ de structure déclaré mutable peut être modifié même si la structure est déclarée const.

    Pour déclarer une classe de stockage particulière, il suffit de faire précéder ou suivre le type de la variable par l'un des mots clés auto, static, register, etc. On n'a le droit de n'utiliser que les classes de stockage non contradictoires. Par exemple, register et extern sont incompatibles, de même que register et volatile, et const et mutable. Par contre, static et const, de même que const et volatile, peuvent être utilisées simultanément.

    Exemple 3-13. Déclaration d'une variable locale statique

    int appels(void)
    {
        static int n = 0;
        return n = n+1;
    }

    Cette fonction mémorise le nombre d'appels qui lui ont été faits dans la variable n et renvoie ce nombre. En revanche, la fonction suivante :

    int appels(void)
    {
        int n = 0;
        return n =n + 1;
    }
    renverra toujours 1. En effet, la variable n est créée, initialisée, incrémentée et détruite à chaque appel. Elle ne survit pas à la fin de l'instruction return.

    Exemple 3-14. Déclaration d'une variable constante

    const int i=3;

    i prend la valeur 3 et ne peut plus être modifiée.

    Les variables globales qui sont définies sans le mot clé const sont traitées par le compilateur comme des variables de classe de stockage extern par défaut. Ces variables sont donc accessibles à partir de tous les fichiers du programme. En revanche, cette règle n'est pas valide pour les variables définies avec le mot clé const. Ces variables sont automatiquement déclarées static par le compilateur, ce qui signifie qu'elles ne sont accessibles que dans le fichier dans lequel elles ont été déclarées. Pour les rendre accessibles aux autres fichiers, il faut impérativement les déclarer avec le mot clé extern avant de les définir.

    Exemple 3-15. Déclaration de constante externes

    int i = 12;          /* i est accessible de tous les fichiers. */
    const int j = 11;    /* Synonyme de "static const int j = 11;". */
    
    extern const int k;  /* Déclare d'abord la variable k... */
    const int k = 12;    /* puis donne la définition. */

    Notez que toutes les variables définies avec le mot clé const doivent être initialisées lors de leur définition. En effet, on ne peut pas modifier la valeur des variables const, elles doivent donc avoir une valeur initiale. Enfin, les variables statiques non initialisées prennent la valeur nulle.

    Les mots clés const et volatile demandent au compilateur de réaliser des vérifications additionnelles lors de l'emploi des variables qui ont ces classes de stockage. En effet, le C/C++ assure qu'il est interdit de modifier (du moins sans magouiller) une variable de classe de stockage const, et il assure également que toutes les références à une variable de classe de stockage volatile se feront sans optimisations dangereuses. Ces vérifications sont basées sur le type des variables manipulées. Dans le cas des types de base, ces vérifications sont simples et de compréhension immédiate. Ainsi, les lignes de code suivantes :

    const int i=3;
    int j=2;
    
    i=j;   /* Illégal : i est de type const int. */
    génèrent une erreur parce qu'on ne peut pas affecter une valeur de type int à une variable de type const int.

    En revanche, pour les types complexes (pointeurs et références en particulier), les mécanismes de vérifications sont plus fins. Nous verrons quels sont les problèmes soulevés par l'emploi des mots clés const et volatile avec les pointeurs et les références dans le chapitre traitant des pointeurs.

    Enfin, en C++ uniquement, le mot clé mutable permet de rendre un champ de structure const accessible en écriture :

    Exemple 3-16. Utilisation du mot clé mutable

    struct A
    {
        int i;           // Non modifiable si A est const.
        mutable int j;   // Toujours modifiable.
    };
    
    const A a={1, 1};    // i et j valent 1.
    
    int main(void)
    {
        a.i=2;           // ERREUR ! a est de type const A !
        a.j=2;           // Correct : j est mutable.
        return 0;
    }

    votre commentaire