Notions de pointeurs en C PRO-1027 Programmation Scientifique en C Notions de pointeurs en C Sujets Notions de base sur les pointeurs Pointeurs et Tableaux (matrices) Arithmétiques sur les pointeurs Chaînes de caractères Tableaux à deux dimensions Tableaux de pointeurs Allocation de mémoire Références: http://www.cplusplus.com/doc/tutorial/pointers.html http://www.cs.cf.ac.uk/Dave/C/node10.html http://pweb.netcom.com/~tjensen/ptr/pointers.htm http://www.commentcamarche.net/c/cpoint.php3 http://www.iu.hio.no/~mark/CTutorial/CTutorial.html
Notions de base des pointeurs Si nous voulons permuter le contenu de deux variables entières x et y, nous permuterons ces variable à l’aide d’une fonction "swap(x,y)" qui permet la permutation des variables x et y (les paramètres). La version suivante de cette fonction ne fonctionne pas car même si on modifie x et y, les nouvelles valeurs permutées ne sont pas retransmises à la fin de la fonction: void swap(int x, int y) { int temp; temp=x; x=y; y=temp; }
Notions de base des pointeurs Version fonctionnelle: void swap(int *px, int *py) { int temp; temp=*px; *px=*py; *py=temp; } Appel à la procédure swap(): int x=3, y=5; swap(&x,&y); // &x: & retourne l'adresse de la variable x // x=5 et y=3 au retour du swaP()
Notions de base des pointeurs La plupart des langages de programmation offrent la possibilité d'accéder aux données dans la mémoire de l'ordinateur à l'aide de pointeurs, Un pointeur est une variable affectée à la valeur d’un adresse. En C, les pointeurs jouent un rôle primordial dans la définition de fonctions Sachant que le passage des paramètres en C se fait toujours par valeur, Les pointeurs permettent de passer une référence sur un objet à une fonction appelée. Le pointeur permet donc de changer le contenu de variables déclarées dans d'autres fonctions. Ainsi le traitement de tableaux et de chaînes de caractères dans des fonctions serait impossible sans l'utilisation de pointeurs.
Notions de base des pointeurs Modes d’adressage Adressage direct : Dans la programmation, nous utilisons des variables pour stocker des informations. La valeur d'une variable se trouve à un endroit spécifique dans la mémoire interne (RAM) de l'ordinateur. Le nom de la variable nous permet alors d'accéder directement à cette valeur. Donc l’adressage direct permet l’accès au contenu d'une variable par le nom de la variable. Adressage indirect : Accès au contenu d'une variable, en passant par un pointeur qui contient l'adresse de la variable. Donc l’accès à une valeur d’une variable se fait par l’accès préalable au contenu d’un pointeur.
Notions de base des pointeurs Un pointeur est une variable qui contient l'adresse d'une autre variable. En langage C, chaque pointeur est limité à un type de données. Il peut contenir l'adresse d'une variable simple de ce type ou l'adresse d'une composante d'un tableau de ce type. Si un pointeur P contient l'adresse d'une variable A, on dit que 'P pointe sur A'. Un pointeur est une variable qui peut 'pointer' sur différentes adresses. Le pointeur pouvant être affecté à différentes adresses. Le nom d'une variable reste quant à lui toujours lié à la même adresse en RAM.
Notions de base des pointeurs Opérateurs sur les pointeurs &<var>: opérateur unaire qui retourne l’adresse de la variable var. int N; printf("Entrez un nombre entier : "); scanf("%d", &N); // scanf() prend une adresse comme deuxieme argument *<p>: opérateur unaire qui retourne le contenu pointer par p un pointeur. int A=10, B; int *P; P = &A; B = *P; *P = 20; A 10 A 20 3 B 10 2 1 P &A 1 2 3
Notions de base des pointeurs Déclaration des pointeurs <Type> *<NomPointeur> : déclare un pointeur <NomPointeur> qui être affecté des adresses de variables du type <Type> Une déclaration comme: int *P ; Le pointeur P est du type int *, donc P est un pointeur sur des objets int P contient donc l'adresse d‘objet de type int Lors de la déclaration d'un pointeur en C, ce pointeur est lié explicitement à un type de données. Donc la variable pointeur P déclarée comme pointeur sur des int ne peut pas recevoir l'adresse d'une variable d'un autre type que int.
Notions de base des pointeurs Opérations sur les pointeurs Les opérateurs * et & ont la même priorité que les autres opérateurs unaires (la négation !, l'incrémentation ++, la décrémentation --). Dans une même expression, les opérateurs unaires *, &, !, ++, -- sont évalués de droite à gauche. Si un pointeur P pointe sur une variable X, alors *P peut être utilisé partout où on peut écrire explicitement X. Si nous avons: P = &X; Y = X+1; // Y = *P + 1; X = X+10; // *P = *P + 10; X += 2; // *P +=2; ++X; // ++(*P); ou ++*P; X++; // (*P)++
Notions de base des pointeurs Valeur initiales La valeur numérique 0 (zéro, NULL) est utilisée pour indiquer qu'un pointeur ne pointe 'nulle part‘, sur rien. int *P ; P = 0 ; Les pointeurs sont aussi des variables et peuvent être utilisés comme telles. Soit P1 et P2 deux pointeurs sur des int, alors l'affectation P1 = P2; copie le contenu de P2 vers P1. P1 pointe alors sur le même objet que P2. Après les instructions suivantes : int A=20, B; // variable int . &A adresse de A int *P ; // P est un pointeur sur des int P = &A ; B = *P; // Contenu de A affecte a B
Notions de base des pointeurs Exercise main() { int A = 1, B = 2, C = 3; int *P1, *P2; P1=&A; // P1 pointe sur A P2=&C; // P2 pointe sur C *P1=(*P2)++; // A = 3 C = 4 P1=P2; // P1 pointe sur C P2=&B; // P2 pointe sur B *P1-=*P2; // C = 2 ++*P2; // B = 3 *P1*=*P2; // C = 6 A=++*P2**P1; // A = 24 P1=&A; // P1 pointe sur A *P2=*P1/=*P2; // B = 6 return 0; }
Pointeurs et tableaux int tableau[100]; &tableau[0] et tableau sont une seule et même adresse. Donc le nom d'un tableau est un pointeur constant sur le premier élément du tableau. int A[10], B; int *P ; P = A ; // P pointe sur A[0], P = &A[0]; equivalent B = *(P+1); // P+1 pointe sur le contenu de A[1] B = *(P+2); // P+2 pointe sur le contenu de A[2] ... B= *(P+i); // P+i pointe sur le contenu de A[i]
Pointeurs et tableaux ………………… float A[20]; float X ; float *P ; P = A ; // P pointe sur l’element A[0] X = *(P+9) ; // P+9 pointe sur l’element A[9] A représente l'adresse de l’élément A[0], *(A+1) pointe sur le contenu de A[1] *(A+2) pointe sur le contenu de A[2] ... *(A+i) pointe sur le contenu de A[i] ………………… A[0] *(A) A[19] *(A+19)
Pointeurs et tableaux Il existe une différence fondamentale entre une variable pointeur et le nom d'un tableau : Un pointeur est une variable, sa valeur peut alors changée, des opérations arithmétiques sur ces pointeurs peuvent être effectuées, comme l’affectation P = A ou des incréments P++ ou toutes autres opérations arithmétiques. Le nom associé à un tableau est une constante, donc des opérations arithmétiques comme A = P ou A++ sont illégales.
Exemple tableau VS pointeur Pointeurs et tableaux Exemple tableau VS pointeur main() { int T[10] = {-3, 4, 0, -7, 3, 8, 0, -1, 4, -9}; int POS[10]; // int * Tptr, *Pptr; int I,J; /* indices courants dans T et POS */ // Tptr = T; Pptr = POS; for (J=0,I=0; I<10; I++) if (T[I]>0) // (*(Tptr+I) > 0) POS[J] = T[I]; // *(Pptr+J) = *(Tptr+I) J++; } return 0;
Arithmétiques sur les pointeurs Affectation d’un pointeur Soient P1 et P2 deux pointeurs sur le même type de données, alors l'instruction P1 = P2 ; fait pointer P1 sur le même objet que P2. Addition et soustraction d’une valeur entière Si le pointeur P pointe sur l'élément A[i] d'un tableau, alors P+n pointe sur A[i+n] et P-n pointe sur A[i-n]. n correspond au nombre d’emplacements du type de donnée pointé qu’il faut se déplacer à partir de P pour atteindre l’emplacement recherché (P+n). Ce déplacement (décalage) correspond à un saut de n*sizeof(type de donnée) octets dans la mémoire RAM. Si P pointe sur des objets float, P+4 pointe alors sur le cinquième élément de type float, donc 16 octets à partir du début du vecteur de float pointé par P.
Arithmétiques sur les pointeurs Incrément/Décrément d’un pointeur Si P pointe sur l'élément A[i] d'un tableau, alors après l'instruction : P = &A[i]; P++ ; // P pointe sur A[i+1] P+= n ; // P pointe sur A[i+n] P-- ; // P pointe sur A[i-1] P-= n ; // P pointe sur A[i-n]
Arithmétiques sur les pointeurs Rappels importants: L'addition, la soustraction, l'incrémentation et la décrémentation sur les pointeurs sont seulement définies à l'intérieur d'un tableau. Si l'adresse formée par le pointeur et l'indice sort du domaine du tableau, alors le résultat n'est pas défini et peut causer des erreurs d’accès mémoire difficiles à détecter. Ces opérations arithmétiques sont valables également pour les pointeurs qui pointent vers des variables indépendantes, i.e., ne faisant pas partie d’un tableau. À titre d’exemple, si P pointe sur un élément de type quelconque, alors P+1 pointe sur l’octet qui suit immédiatement cet élément. Exemple : si P pointe sur un short int alors P+1 sera égale à l’adresse référencée par P plus 2 (puisque la taille d’un short int est 2 octets).
Arithmétiques sur les pointeurs Exemples d’utilisations d’opérations arithmétiques int A[10] ; int *P ; P = A+9 ; // P pointe sur le dixieme element de A P = A+11 ; // P pointe au-dela du vecteur A de 8 octets P = A-1 ; // P pointe 4 octets avant le debut de A
Arithmétiques sur les pointeurs Comparaisons de pointeurs (opérateurs relationnels) On peut comparer deux pointeurs par <, >, <=, >=, ==, !=. La comparaison de deux pointeurs qui pointent dans le même tableau est équivalente à la comparaison des indices correspondants. (Si les pointeurs ne pointent pas dans le même tableau, alors le résultat est donné par leurs positions relatives dans la mémoire).
Arithmétiques sur les pointeurs Autre exemple int A[] = {12, 23, 34, 45, 56, 67, 78, 89, 90}; int *P, *P2, B; P = A; B = *P+2; // B = 14 = A[0] + 2 B = *(P+2) + 2; // B = 36 = A[2] + 2 P2 = P+1; // P2 pointe sur A[1] P2 = &A[4]-3; // P2 pointe sur A[1] P2 = A+3 ; // P2 pointe sur A[3] B = &A[7]-P; // B = 28 P2 = P+(*P-10) ; // P2 pointe sur A[2] B = *(P+*(P+8)-A[7]); // B = 23;
Pointeurs (chaînes de caractères) Initialisation de pointeur sur des chaînes de caractères Une chaîne de caractères est stockée dans un tableau linéaire de "char". La chaîne de caractères est complétée par le caractère NULL, le zéro binaire ('\0'). char x[]="abcd"; // x est un tableau de 5 caractères, // x[4]=='\0' et aussi x[4]==0 Un pointeur de char peut être initialisé lors de sa déclaration si on lui affecte l'adresse d'une chaîne de caractères constante : char *B = "PRO1027" ;
Pointeurs (chaînes de caractères) Initialisation de pointeur sur des chaînes de caractères Il existe une différence importante entre les deux déclarations : char A[] = "PRO1027"; /* un tableau */ char *B = "PRO1027"; /* un pointeur */ A est un tableau qui a exactement la grandeur pour contenir la chaîne de caractères et le caractère de fin '\0'. Les caractères de la chaîne peuvent être changés, mais le nom A va toujours pointer sur la même adresse en mémoire. B est un pointeur qui pointe sur une chaîne de caractères constante stockée quelque part en mémoire. Le pointeur peut être modifié et pointer sur autre chose. La chaîne constante peut être lue, copiée ou affichée, mais pas modifiée.
Pointeurs (chaînes de caractères) Initialisation de pointeur sur des chaînes de caractères char *A = "Chaîne courte" ; char *B = "Chaîne plus longue"; A = B; // A pointe sur la meme chaine que B Les tableaux de caractères permettront de déclarer des chaînes de caractères que nous voulons modifier. Les pointeurs de char seront utilisés pour manipuler des chaînes de caractères constantes (dont le contenu ne change pas).
Tableaux à deux dimensions Un tableau M à deux dimensions peut être définis: int M[4][10] = { { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, {10,11,12,13,14,15,16,17,18,19}, {20,21,22,23,24,25,26,27,28,29}, {30,31,32,33,34,35,36,37,38,39} }; Le nom du tableau M représente l'adresse du premier élément du tableau et pointe sur le tableau M[0] qui a la valeur {0,1,2,3,4,5,6,7,8,9}. M[0] pointe sur la rangée 0. L'expression (M+1) est l'adresse du deuxième élément du tableau M et pointe sur M[1] qui a la valeur {10,11,12,13,14,15,16,17,18,19}. M[1] pointe sur la rangée 1. L'arithmétique des pointeurs qui respecte automatiquement les dimensions des éléments conclut logiquement que M+i désigne l'adresse du tableau M[i].
Tableaux à deux dimensions Exemple pointeur VS tableau 2D int M[4][10] = { { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, {10,11,12,13,14,15,16,17,18,19}, {20,21,22,23,24,25,26,27,28,29}, {30,31,32,33,34,35,36,37,38,39}}; int *P, B; P = (int *)M; /* conversion forcée */ B = *(P+(2*10) + 2); // B = M[2][2] Sachant que M est stockée ligne par ligne, il est maintenant possible traiter M à l'aide du pointeur P comme un tableau unidimensionnel de dimension 4*10.
Tableaux à deux dimensions Exemple pointeur VS tableau 2D ( des éléments de M) int M[4][10] = { { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, {10,11,12,13,14,15,16,17,18,19}, {20,21,22,23,24,25,26,27,28,29}, {30,31,32,33,34,35,36,37,38,39}}; int *P, som=0; P = (int *)M; for(int i=0;i<4;i++) for(int j=0;j<10;j++) som += M[i][j]; // P[i*10+j] ou *(P+(i*10)+j)
Tableaux à deux dimensions Pour référencer un tableau 2D à l'aide de pointeurs, nous avons besoin de quatre données : L'adresse du premier élément du tableau converti dans le type correspondant aux éléments du tableau La longueur d'une ligne réservée en mémoire Le nombre d'éléments effectivement utilisés dans une ligne Le nombre de lignes effectivement utilisées
Tableaux de pointeurs Déclaration: <Type> *<NomTableau>[<N>] déclare un tableau <NomTableau> de <N> pointeurs sur des données de type <Type>. double *A[10]; déclare un tableau de 10 pointeurs sur des objets de type double dont les adresses et les valeurs ne sont pas encore définies. Les tableaux de pointeurs sont souvent utilisés pour mémoriser de façon économique des chaînes de caractères de différentes longueurs. Ex: Le tableaux char *argv[] passé en argument à la fonction main() d’un programme C.
Tableaux de pointeurs Nous pouvons initialiser les pointeurs d'un tableau sur char par les adresses de chaînes de caractères constantes. char *MOIS[] = {"janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"}; déclare un tableau MOIS[] de 12 pointeurs de char. Chacun des pointeurs est initialisé avec l'adresse de l'une des 12 chaînes de caractères. On peut afficher ces 12 chaînes de caractères en fournissant les adresses contenues dans le tableau MOIS aux fonction d’affichage printf() ou puts() : int i; for (i=0; i<12; i++) printf("%s\n", MOIS[i]) ; // puts(MOIS[i])
Tableaux de pointeurs Comme MOIS[i] est un pointeur de char, on peut afficher les premières lettres des mois en utilisant l'opérateur * 'contenu de' : int i; for (i=0; i<12; i++) printf("%c\n", *MOIS[i]); L'expression MOIS[i]+j correspond à la jième lettre de la iième chaîne. for (i=0; i<12; i++) printf("%c\n",*(MOIS[i]+2));
Tableaux de pointeurs Formatage des sorties avec printf()
Tableaux de pointeurs Formatage des sorties avec printf()
Tableaux de pointeurs Formatage des sorties avec printf()
Allocation dynamique Lorsque nous ne connaissons pas d’avance la dimension des tableaux que nous devons utiliser dans notre application, Nous devons trouver des moyens pour réserver et libérer de la mémoire au fur et à mesure que nous en avons besoin. Nous parlons alors de l'allocation dynamique de la mémoire. Déclaration statique de données Chaque variable dans un programme utilise un certain nombre d'octets en mémoire. Les exemples présentés utilisaient un mode de réservation de mémoire automatique appliquée lors des déclarations des données. Dans tous ces cas, le nombre d'octets à réserver était déjà connu pendant la compilation. Ce qui correspond à la déclaration statique des variables. float A, B, C; // 3 X 4 octets short D[10][20]; // 10 X 20 X 2 octets char E[] = {"PRO1027"}; // 8 octets char F[][10] = {"un", "deux", "trois", "quatre"}; // 4 X 10 octets
Allocation dynamique Déclaration de pointeurs double *G ; // machine 32 bits, 4 octets char *H ; // machine 32 bits, 4 octets float *I[10] ; // machine 32 bits, 10 X 4 octets La fonction malloc() de la bibliothèque <stdlib.h> nous aide à localiser et à réserver de la mémoire au cours d'un programme. malloc( <N> ) : retourne l'adresse d'un bloc en mémoire de <N> octets libres ou la valeur zéro s'il n'y a pas assez de mémoire. Supposons que nous avons besoin d'un bloc de mémoire pour une image de niveaux de gris (0..255) de 1024X1024 unsigned char. unsigned char *img; img = malloc(1024*1024) ; // retourne un bloc de 1024X1024 octets Si nous voulons réserver de la mémoire pour des données d'un type dont la grandeur varie d'une machine à l'autre, nous avons besoin de la grandeur effective d'une donnée de ce type. L'opérateur sizeof() nous aide alors à préserver la portabilité du programme.
Allocation dynamique sizeof <var> fournit la grandeur de la variable <var> sizeof <const> fournit la grandeur de la constante <const> sizeof (<type>) fournit la grandeur pour un objet du type <type> Exemples: short A[10]; char B[5][10]; printf(“\n valeur retourne = %d”,sizeof(A); // 10 X 2 printf(“\n valeur retourne = %d”,sizeof(B); // 5 X 10 X 1 printf(“\n valeur retourne = %d”,sizeof(“PRO1027" ); // 8 printf(“\n valeur retourne = %d”,sizeof(float ); // 4 printf(“\n valeur retourne = %d”,sizeof(double ); // 8
Allocation dynamique Libération de la mémoire Quand un bloc de mémoire que nous avons réservé à l'aide de malloc() n’est plus utilisé, nous pouvons alors le libérer à l'aide de la fonction free() de la bibliothèque <stdlib.h>. free( <Pointeur> ) Cette fonction libère le bloc de mémoire désigné par le pointeur <Pointeur>. La fonction free() peut aboutir à un désastre si on essaie de libérer de la mémoire qui n'a pas été allouée par malloc(). La fonction free() ne change pas la valeur (adresse) du pointeur; il est donc conseillé d'affecter la valeur zéro au pointeur immédiatement après avoir libéré le bloc de mémoire qui y était attaché. Si nous ne libérons pas explicitement la mémoire à l'aide de la fonction free(), alors elle est libérée automatiquement à la fin du programme. MAIS entre temps, un programme qui ne libère pas sa mémoire inutilisée peut occasionner des fuites de mémoire (memory leaks).