Annonce

Réduire
Aucune annonce.

Les Pointeurs en Langage C

Réduire
X
 
  • Filtre
  • Heure
  • Afficher
Tout nettoyer
nouveaux messages

  • Tutoriel Les Pointeurs en Langage C

    Cet article s'adresse aux débutants dans le C, ainsi qu'à ceux qui connaissent déjà les pointeurs mais qui aimeraient clarifier certaines choses à ce sujet.

    Les pointeurs constituent LA base fondamentale du C. C'est le concept clé de ce langage ; si vous ne le maîtrisez pas, vous serez vite limités dans vos programmes écrits en C.

    Petite parenthèse : Cet article ne traite pas l'allocation dynamique de mémoire (avec malloc() ).

    Sommaire

    Les pointeurs
    La mémoire et les adresses
    Pointeurs, variables, et types
    Intérêts des pointeurs
    Tableaux et chaînes de caractères
    Tableaux
    Chaînes de caractères
    Tableau de pointeur

    1. Les pointeurs

    Entrons dans le vif du sujet. Qu'est-ce qu'un pointeur ?

    Définition : Un pointeur est une variable contenant une adresse mémoire.
    C'est aussi simple que cela. Mais nous pouvons disserter sur cette définition pendant des heures...

    1.1. La mémoire et les adresses

    Commençons par voir le concept fondamental qui est celui de la mémoire. Quand un programme s'éxécute, le processeur commence par le lire et copier son code dans la RAM, autrement dit la mémoire. Le programme va se voir réserver un bout de mémoire durant son éxécution, qui va être séparé en sections. Pour faire simple, une section comportera son code binaire, une autre les données qu'il utilise. En réalité, les données sont elles-même séparées en plusieurs parties, mais nous verrons cela plus tard.

    Pour se repérer dans la mémoire, on utilise les adresses. Une adresse est (la quasi-totalité du temps) notée en héxadécimal, et occupe 4 octets. C'est la position d'un octet précis par rapport au début de la mémoire, comme pour les offsets dans les fichiers.
    Les variables étant stockées dans la mémoire, eles se trouvent donc à une certaine adresse. On distinguera alors la valeur d'une variable, et son adresse.

    1. 2. Pointeurs, variables et types

    Comme nous l'avons vu, un pointeur est une variable dans laquelle est stockée l'adresse mémoire d'une autre variable. On dit alors que le pointeur "pointe vers" la variable. Plus clairement, si p_a contient l'adresse de a,alors on dit que p_a pointe vers a. Rien de compliqué là dedans, il s'agit juste de vocabulaire.

    Une variable occupe une certaine place en mémoire, selon son type. Par exemple, le char occupe 1 octet, le int 4.
    Vous pouvez obtenir la taille (en octets) d'une variable ou d'un type à l'aide de la fonction sizeof(type ou variable) . Par exemple, regardons le programme suivant :

    Code:
    int main()
    {
    printf("La taille d'un char est %d octet(s)",sizeof(char));
    getchar();
    }
    A l'exécution, celui-ci sort :

    La taille d'un char est 1 octet(s)

    Petite remarque : la taille d'un type peut dépendre de la machine que vous utilisez, notemment pour les 'int' et 'long'.

    Nous avons dit qu'un pointeur pointait vers une variable. Il faut donc prendre en compte le type de cette variable. De manière plus claire, si vous déclarez un int a, et que vous placez l'adresse de a dans un pointeur p_a, il vous faudra spécifier que p_a pointe vers un int. En effet, si jamais vous déclarez p_a comme pointeur poitant vers un char, et si vous réutilisez p_a, le processeur ne prendra que le 1er octet de a... et toute l'exécution de votre programme s'en trouvera faussée.

    Voyons la pratique. Un pointeur se déclare presque comme les variables. Voila la syntaxe :

    type * nom_pointeur;

    Ceci déclare le pointeur de nom 'nom_pointeur'. Il faut alors l'initialiser, c'est à dire lui donner une valeur (l'adresse d'une autre variable). Car il y a une chose TRES importante en C : Généralement, quand vous déclarez une variable, celle-ci contient déja une valeur. En fait, "déclarer une variable" signifie pour le processeur "réserver de la mémoire pour cette variable". Mais il ne lui donne aucune valeur, même pas zéro. En fait, cela dépend du compilateur, mais pour faire simple, et comme nous sommes des programmeurs "propres", nous supposerons que non. Le processeur alloue donc de la mémoire pour cette variable, mais garde la valeur qui se trouvait déja à l'emplacemement où la variable a été allouée. Par conséquent, son contenu est assez aléatoire, il dépend de l'état de la mémoire avant l'éxécution du programme. Alors pour éviter les mauvaises surprises, initialisez toujours vos variables !

    Pour nous aider, le C nous offre deux opérateurs. L'un, &, permet d'obtenir l'adresse d'une variable. L'autre, *, à ne pas confondre avec le * de déclaration, permet d'obtenir le contenu pointé par un pointeur. Le & se place devant une variable ; le * devant un pointeur.

    Exemple :

    Code:
    int main()
    {
    int a = 2;
    printf("%p\n", (void *)&a); //le %p permet d'afficher en héxadécimal
    int b = 3;
    int * p_b;
    *p_b = b; //p_b pointe alors vers b, donc 3
    printf("p_b = : %p\n",p_b);
    printf("*p_b = %d", (int) *p_b);
    getchar();
    }
    Cela affiche par exemple :

    adresse de a : 22ff74
    p_b = : 22ffa8
    *p_b = 3

    Mais nous avons pourtant vu tout à l'heure qu'une adresse était codée sur 4 octets. C'est vrai, mais ici, les adresses réelles sont 0022ff74 et 0022ffa8 ; les 0 ont été supprimés pendant leur traitement par printf().

    Remarque : il est conseiller de faire précéder les noms de vos pointeurs par des "p" ou "p_" , comme l'exemple le montre; cela permet de ne pas se tromper et de savoir directement que c'est un pointeur.

    Exercice : Dans le main() , déclarez une variable a sans l'initialiser, puis un pointeur p_a pointant vers a. Puis donnez à a la valeur 5, sans passer direrctement par a (sans écrire "a = 5" ) . Affichez enfin l'adresse de a et son contenu, ainsi que le contenu de p_a. Ce petit exercice est corrigé en bas de page.

    Il est conseillé de bien s'entraîner les pointeurs ainsi qu'avec les opérateurs * et &. D'ailleurs, voici un petit un tuyau, si cela peut vous aider à mieux comprendre.
    La déclaration d'un pointeur doit être lue à l'envers !

    En effet, c'est simple : il suffit de lire de droite à gauche. int * p_i; est lu comme ceci : "p_i est un pointeur (*) vers un entier (int)". Cela peut faciliter la lecture du code, surtout quand il y a plusieurs * qui se suivent...

    Attention ! Certains, partisans du moindre effort, voudront déclarer deux pointeurs comme ceci :

    int * p_a , p_b;

    Dommage ! Cela ne marche pas. Le compilateur ne vous dira rien sur cette ligne, parce qu'il n'y a pas d'erreur de syntaxe ; par contre vous aurez à coup sur des erreurs dans la suite. Pourquoi ? Tout simplement parce que l'étoile * doit être répétée, elle ne fait pas partie du type. Ici, p_a sera un pointeur, mais p_b sera un entier ! En gros, pour avoir 2 pointeurs, il faut écrire :

    int * p_a ,* p_b;

    Idem pour les prototypes des fonctions ; nous verrons cela plus bas.
    Il est aussi possible (et même conseillé) de déclarer une variable (ou un pointeur) par ligne, comme ceci :

    int *p_a;
    int *p_b;

    C'est plus clair, il n'y a aucun risque de confusion.

    Enfin, maintenant que nous avons vu tout cela, selon vous, que renvoit sizeof( char * ) ?
    La réponse est : 4. Un pointeur contient une adresse, une adresse est codée sur 4 octets donc on en déduit qu'un pointeur (quelque soit son type) occupera toujours 4 octets en mémoire.

    1.3. Intérêt des pointeurs

    Après tout, quel est l'intérêt des pointeurs ? En réalité, ils en ont beaucoup. Regardez plutôt :

    Code:
    void incremente(int a)
    {
    a++;
    }
    int 
    main()
    {
    int b = 5; 
    incremente(b);
    printf("b = %d",b);
    getchar();
    }
    Compilez et observez. Non, vous ne rêvez pas :

    b = 5

    Pourquoi ? Parce que, lors d'un appel à une fonction, celle-ci reçoit une copie des arguments, et non les originaux. C'est la copie de b qui a été incrémentée ici, et cela n'a rien changé pour la variable originale b qui reste égale à 5...

    Pour pouvoir malgré tout modifier b par incremente(), il faut modifier la fonction pour qu'elle utilise non pas la valeur mais l'adresse de l'argument qu'on lui passe :

    Code:
    void 
    incremente(int * p_a)
    {
    (*p_a)++;
    }
    int main()
    {
    int b = 5;
    incremente(&b);
    printf("b = %d",b);
    getch();
    }
    Comme on a modifié le prototype (int * p_a), on a modifié également l'appel en prenant en compte le fait que l'argument est un pointeur avec &b.

    Là encore, danger ! Notez la nécessité des parenthèses dans (*p_a)++! En effet, l'opérateur ++ est prioritaire par rapport à * ; il s'éxécute avant. Donc si vous mettez *p_a++ , vous allez incrémenter p_a, c'est à dire l'adresse que contient le pointeur (il y a décallage du pointeur). Puis * retournera la valeur de l'adresse ainsi incrémentée : vous allez lire le contenu de l'adresse mémoire du voisin. Alors que nous voulons prendre d'abord le contenu pointé puis l'incrémenter. Ce n'est donc pas du tout la même chose, donc n'oubliez pas les parenthèses !

    A retenir : pour modifier une variable dans une fonction, il faut impérativement connaître l'adresse de cette variable dans la mémoire. Implicitement, quand vous faites b = 5 , le processeur prend l'adresse de b, et fixe à 5 le contenu pointé par cette adresse.

    Le deuxième avantage, nous ne l'avons pas encore abordé : c'est le fait que'un pointeur peut pointer non pas une variable mais un ensemble de variables. On appelle cela un tableau.

    2. Tableaux et chaînes de caractères

    2.1. Tableaux

    Définition : Un tableau est une zone de la mémoire constituée de plusieurs variables de même type.

    C'est la définition exacte d'un tableau. Pourtant, certains préfèrent généralement considérer un tableau comme un pointeur vers un espace mémoire comportant des variables de même type. En fait, un tableau est l'espace mémoire lui-même et non pas un pointeur vers celui-ci. Mais en pratique, on utilise les tableaux comme des pointeurs, donc vous pouvez garder à l'esprit la dernière définition sans oublier la première.

    Vous êtes sans doute familiarisés avec les tableaux dans d'autres langages. les variables consécutives du tableau sont répérées par leur indice, c'est à dire leur numéro à partir du début du tableau. La première variable du tableau a pour indice 0. Les deux caractéristiques d'un tableau sont sa taille et le type des variables qui le constituent. Profitons-en pour introduire le terme de buffer. Un buffer est juste un synonyme qui est couramment utilisé pour désigner un tableau. Dans la suite, nous jonglerons sans doute entre ces termes, ne soyez donc pas étonnés.

    Voici comment se déclare un tableau :

    type tableau[taille];

    La taille du tableau est exprimée en octets. J'attire votre attention sur ce que j'ai dit précédemment : si vous déclarez un tableau de taille n, le 1er indice étant 0, le dernier élément de votre tableau aura pour indice n-1 ! Attention à ne pas écrire en dehors de votre tableau, vous provoqueriez des erreurs. Pas forcément très graves à priori, mais sachez qu'il est possible de les exploiter. Ces failles sont plus communément appelées les débordements de tampons ou les "buffer overflows". Ce genre de vulnérabilités provient d'un manque d'attention du programmeur et peut conduire au détournement de l'éxécution d'un programme, permettant très souvent l'éxécution de code arbitraire sur la machine compromise...

    Une fois le tableau déclaré, chaque variable du tableau est désignée par son indice entre crochets ; la k-ième variable étant tableau[k-1]. Si vous faites un sizeof(tableau) , cela vous retournera non pas 4, car un tableau n'est pas exactement un pointeur, mais la taille que prend en mémoire le tableau, c'est à dire le nombre de variables multiplié par la taille du type de chaque variable. Voici un exemple de tableau :

    Code:
    int main()
    {
    int buffer[5];
    int i;
    buffer[0] = 5;
    buffer[1] = 4;
    buffer[2] = 3;
    buffer[3] = 2;
    buffer[4] = 1;
    printf("buffer occupe %d octets et contient les elements suivants :\n",sizeof(buffer));
    for(i = 0;i<5;i++)
    {
    printf("%d\n",buffer[i]);
    }
    getch();
    }
    Ce qui retourne :

    buffer occupe 20 octets et contient les elements suivants :

    5
    4
    3
    2
    1

    On constate que la taille de buffer est bien le nombre de ses éléments (5) multiplié par la taille de chaque variable (4 octets pour un int) donc 20 au total. On va bien de 0 à 4 dans les indices, que ce soit pour déclarer les valeurs et pour les lire : comme i reste strictement inférieur à 5, sa valeur maxi est 4.

    Remarquez que l'on aurait pu n'avoir qu'une ligne d'initialisation du tableau : int buffer[5] = {5,4,3,2,1};. D'ailleurs, il n'est alors même plus nécessaire de préciser la taille du tableau, int buffer[] = {5,4,3,2,1}; fonctionne parfaitement.

    2.2. Chaînes de caractères

    Maintenant que vous connaissez les tableaux, voyons un cas particulier : les tableaux de char, plus comunément appelées les chaines de caractères.

    Définition : Une chaîne de caractères est un tableau de char dont la denrière variable est un zéro.

    Le zéro en question est le caractère ASCII nul, aussi noté '\0' ou 0 en décimal. N'oubliez jamais la présence de ce zéro. Pourquoi ? Parce que si vous voulez afficher la chaîne de caractères que vous avez initialiser, le processeur lira votre tableau jusqu'à ce qu'il rencontre le zéro. S'il n'y en a pas, il continuera. Jusqu'à ce qu'il en trouve un ou jusqu'à ce que le programme plante en tentant d'accéder à une zone de la mémoire où il n'a pas le droit d'aller...

    En pratique, la déclaration des chaines de caractères se fait plus simplement que les tableaux.

    Code:
    int main()
    {
    char buffer[] = "abc";
    int i;
    for(i = 0;i {
    printf("%d\n",buffer[i]);
    }
    getch();
    }
    On obtient ceci :

    97
    98
    99
    0

    On peut noter alors une chose : le programme rajoutte tout seul le zéro terminal. 97, 98 et 99 sont les codes ascii décimaux de a, b et c. Ce n'est pas compliqué..

    Pourtant, ça peut le devenir. En C, quand vous écrivez une chaine du style "chaine", le processeur sous-entend l'adresse de cette chaine. En fait, dans l'instruction char buffer[] = "abc";, il y a deux étapes. D'abord :

    char buffer[4];

    où le processeur calcule la taille de la chaîne et alloue un buffer.

    Puis :

    buffer = "abc";

    où le processeur initialise le buffer et fait correspondre son adresse avec celle de la chaîne.

    Attention ! Un caractère se note entre guillements simples (ou apostrophes ' ) comme ceci : 'a' ; alors qu'une chaîne de caractères est entre guillemets doubles " ! 'a' représente le caractère a ou le code ASCII 97, tandis que "a" désigne l'adresse de la chaîne de caractères contenant le caractère 'a'...

    Dernière chose concernant les tableaux : pour les initialiser, vous pouvez utiliser l'instruction memset(). Elle prend en argument le tableau (ou une adresse mémoire, comme vous préférez) , la valeur à donner à tous les éléments du tableau, et sa taille. Elle peut s'utiliser comme ceci : memset(buffer,0,5); dans notre exemple précédent.
    En fait d'une manière plus générale, memset(a, v, n); copie la valeur v à l'adresse mémoire a, et ceci n fois en se décallant vers les adresses hautes.
    Pour copier des tableaux, utilisez memcpy(adresse destination, adresse source, taille). En effet, l'opérateur = ne vous sera d'aucune utilité si vous souhaitez duppliquer des tableaux. Car buffer1 = buffer2 ne copie pas buffer2 dans buffer1 mais fais pointer buffer1 vers le même espace mémoire que buffer2. Les deux tableaux pointent donc vers le même espace mémoire...

    2.3. Tableaux de pointeurs

    Définition : Un tableau de pointeurs est un tableau dont les variables sont des pointeurs.

    Les pointeurs en question peuvent pointer vers des variables, où, plus généralement, vers d'autres tableaux. Dans ce dernier cas, le tableau de départ est un tableau à deux dimensions ! Notez bien que les sous-tableaux peuvent être de différentes tailles.
    Le tableau de pointeurs le plus utilisé est peut-être "argv", le tableau qui va contenir le nom du programme ainsi que ses arguments. C'est un tableau de chaines de caractères, si vous préférez.

    On déclare les tableaux de pointeurs comme les tableaux. Si jamais nous voulons déclarer un tableau de de n pointeurs poitant chacun vers un tableau de taille m, autrement dit un tableau multidimensionnel de n lignes et m colonnes, nous écritons :

    type tableau[n][m];

    Une autre méthode est de déclarer le tableau avec deux * en tant que pointeur vers pointeurs :

    type ** tableau;

    Mais dans ce cas la déclaration est un peu plus compliquée, pusiqu'il faudra déclarer chaque sous-tableau...

    Conclusion : Pointeurs vers pointeurs, pointeurs vers tableaux de pointeurs...
    Non, rassurez-vous, nous n'allons pas voir ces cas là. Non pas parceque c'est trop compliqué, au contraire : vous connaissez déja tout sur les pointeurs. Après, votre imagination fera le reste.

    Les possibilités avec les pointeurs sont illimités, comme vous l'aurez certainement compris. Nous avons juste vu les tableaux, les tableaux de pointeurs et les chaînes de caractères, mais ce sont les principales bases Pour conclure, on peut étendre le sujet des tableaux sur une notion importante : Il s'agit des structures. Une structure, pour faire simple, est un tableau mais dont les variables peuvent être de types différents. Leur emploi est très fréquent en C mais leur utilisation ne se fait pas avec la même syntaxe que le C. Peut-être verrons-nous cela dans un prochain article...

    Sur ce, bon coding !

    Corrigé de l'exercice :

    Code:
    int main()
    {
    int a;
    int * p_a = &a;
    *p_a = 5;
    printf("Adresse de a : %p ; Contenu de a : %d ; Contenu de p_a : %p",&a,a,p_a);
    getchar();
    }
    Cela donne :

    Adresse de a : 22ff74

    Contenu de a : 5

    Contenu de p_a : 22ff74

    On constatera bien entendu que l'adresse de a est la même que l'adresse contenu dans p_a, puisque p_a pointe vers a.

    cred:trance
    Dernière modification par SAKAROV, 26 juillet 2013, 21h13.
    sigpic

    Cyprium Download Link

    Plus j'étudie plus j'me rends compte que je n'sais rien.

    †|
Chargement...
X