• Aucun résultat trouvé

ADRESSE ET VALEUR D’ UNE VARIABLE Objectifs du cours : Programmation en C (2 partie) : notions de base sur les pointeurs COURS

N/A
N/A
Protected

Academic year: 2022

Partager "ADRESSE ET VALEUR D’ UNE VARIABLE Objectifs du cours : Programmation en C (2 partie) : notions de base sur les pointeurs COURS"

Copied!
10
0
0

Texte intégral

(1)

Extrait du référentiel : BTS Systèmes Numériques option A (Informatique et Réseaux) Niveau(x) S4. Développement logiciel

S4.4. Programmation procédurale Manipulations de données (« quoi ») en pseudo-langage et/ou en langage C

Transcription d’algorithmes (« comment ») en pseudo-langage et/ou en langage C

Développement de programmes « console » avec gestion des arguments de la ligne de commande

4 4

3

Objectifs du cours :

- Adresse et valeur d’une variable - Les pointeurs

- Exercices

Toute variable manipulée dans un programme est stockée quelque part en mémoire.

Cette mémoire est constituée d’octets qui sont identifiés de manière univoque par un numéro qu’on appelle adresse.

Pour retrouver une variable, il suffit donc de connaître l’adresse de l’octet où elle est stockée (ou, s’il s’agit d’une variable qui recouvre plusieurs octets contigus, l’adresse du premier de ces octets). Pour des raisons évidentes de lisibilité, on désigne souvent les variables par des identificateurs, et non par leur adresse. C’est le compilateur qui fait alors le lien entre

l’identificateur d’une variable et son adresse en mémoire. Toutefois, il est parfois très pratique de manipuler directement une variable par son adresse.

ADRESSE ET VALEUR D’UNE VARIABLE

On appelle Lvalue (Left value) une variable pouvant être placée à gauche d’un opérateur d’affectation. Une Lvalue est caractérisée par :

 son adresse, c’est-à-dire l’adresse mémoire à partir de laquelle la variable est stockée ;

(2)

 sa valeur, c’est-à-dire ce qui est stocké à cette adresse.

Exemple : int i, j;

i = 3;

j = i;

Si le système a placé la variable i à l’adresse 0x48318360 en mémoire, et la variable j à l’adresse 0x48318364, on a :

Variable Adresse Valeur

i 0x48318360 3

j 0x48318364 3

Représentation simplifiée

Deux variables différentes ont des adresses différentes. L’affectation i = j; n’opère que sur les valeurs des variables. Les variables i et j étant de type int, elles sont stockées sur 2 ou 4 octets selon la machine.

Nous considèrerons ici et pour la suite qu’une variable typée int sera stockée en mémoire sur 4 octets.

Ainsi la valeur de i est stockée sur les octets d’adresse 0x48318360 à 0x48318363.

L’adresse d’une variable étant un numéro d’octet en mémoire, il s’agit d’un entier quelque soit le type considéré. Le format interne de cet entier (16 bits, 32 bits ou 64 bits) dépend des architectures.

L’opérateur & permet d’accéder à l’adresse d’une variable. Toutefois &i n’est pas une Lvalue mais une constante : on ne peut pas faire figurer &i à gauche d’un opérateur d’affectation.

Pour pouvoir manipuler des adresses, nous allons recourir à un nouveau type : les pointeurs.

LES POINTEURS

Si l’utilisation des références peut être implicite (c’est par exemple le cas lorsque vous manipulez des variables), il est des cas où elle doit être explicite. C’est à cela que servent les pointeurs : ce sont des variables dont le contenu est une adresse mémoire (une référence, donc).

Adresse Contenu

0x38318366

0x38318367 1A

0x38318368 9E

0x38318369 31

0x3831836A 38

0x38319E1A

Représentation succincte d’une zone mémoire (en hexadécimale et en little-endian)

(3)

Comme vous pouvez le voir, le contenu à l’adresse 0x38318367 est lui-même une adresse est référence le contenu situé à l’adresse 0x38319E1A.

Le pointeur est une variable dont son contenu est interprété comme une adresse mémoire. Donc, un pointeur est une variable contenant une adresse.

L’intérêt majeur : possibilité à une fonction ou procédure de « travailler » sur l’emplacement mémoire d’origine d’une variable.

Une mémoire est vue comme une succession de cases adjacentes. Ces cases contenant des informations qui sont soit des données, soit des adresses, soit un programme. Pour pouvoir se repérer dans ces cases chacune possède une adresse unique.

Un pointeur est un groupe de cases souvent quatre pouvant contenir une adresse. Sur un système dit 32 bits (4 octets), les adresses mémoire sont également codées sur 4 octets (donc 4 cases mémoire nécessaires pour stocker une adresse).

DÉCLARATION ET INITIALISATION

Pour différencier une variable de type pointeur d’une variable « ordinaire », le langage C/C++

stipule de déclarer une variable de type pointeur en lui ajoutant le symbole *. La syntaxe pour déclarer un pointeur est la suivante :

type *nom_du_pointeur;

Exemple :

Si nous souhaitons créer un pointeur sur int (c’est-à-dire un pointeur pouvant stocker l’adresse d’un élément de type int) et que nous voulons le nommer « ptr », nous devons écrire ceci.

int *ptr;/* ptr est une variable qui contiendra une adresse à laquelle est stockée un entier 32 bits */

Un pointeur est toujours typé. Autrement dit, vous aurez toujours un pointeur sur (ou vers) un élément d’un certain type (int, double, char, ...).

Un pointeur, comme une variable, ne possède pas de valeur par défaut, il est donc important de l’initialiser pour éviter d’éventuels problèmes. Pour ce faire, il est nécessaire de recourir à l’opérateur d’adressage (ou de référencement) : & qui permet d’obtenir l’adresse d’un élément.

Ce dernier se place derrière l’élément dont l’adresse souhaite être obtenue.

Exemples : int a = 10;

int *p;

p = &a;

ou

int a = 10;

int *p = &a;

(4)

Faites attention à ne pas mélanger différents types de pointeurs ! Un pointeur sur int n’est pas le même qu’un pointeur sur long ou qu’un pointeur sur double. De même, n’affectez l’adresse d’un objet qu’à un pointeur du même type.

Exemples : int a;

double b;

int *p1 = &b; /* Faux */

int *p2 = &a; /* Correct */

double *p3 = p; /* Faux */

Nous vous vu que, le plus souvent, les fonctions retournaient une valeur particulière en cas d’erreur. Qu’en est-il pour la valeur que retourne un pointeur ? Existe-t-il une valeur spéciale qui puisse représenter une erreur ?

Il existe un cas particulier : le pointeur nul. Un pointeur nul est tout simplement un pointeur contenant une adresse invalide. Cette adresse invalide dépend de votre système d’exploitation, mais elle est la même pour tous les pointeurs nuls. Ainsi, deux pointeurs nuls ont une valeur égale.

Pour obtenir cette adresse invalide, il vous suffit de convertir explicitement zéro vers le type de pointeur voulu. Ainsi, le pointeur suivant est un pointeur nul.

int *p = NULL; /* un pointeur nul */

Afin de clarifier un peu les codes sources, il existe une constante définie dans l’en- tête <stddef.h> : NULL. Celle-ci peut être utilisée partout où un pointeur nul est attendu.

INDIRECTION (OU DÉFÉRENCEMENT)

Maintenant que nous savons récupérer l’adresse d’un élément et l’affecter à un pointeur, voyons le plus intéressant : accéder à cet objet ou le modifier via le pointeur. Pour y parvenir, nous avons besoin de l’opérateur d’indirection (ou de déréférencement) : *.

L’opérateur d’indirection attend un pointeur comme opérande et se place juste derrière celui-ci.

Une fois appliqué, ce dernier nous donne accès à la valeur de l’élément référencé par le pointeur, aussi bien pour la lire que pour la modifier.

Exemple : int a = 10;

int *p = &a;

printf("a = %d\n", *p);

Résultat : a = 10

Dans l’exemple ci-dessus, nous accédons à la valeur de la variable a via le pointeur p. À présent, modifions la valeur de la variable a à l’aide du pointeur p.

(5)

Exemple : int a = 10;

int *p = &a;

*p = 20

printf("a = %d\n", *p);

Résultat : a = 20 EXERCICES

Soit les deux programmes partiels en C ci-dessous : /* programme 1 */

int main() {

int i = 3, j = 6;

int *p1, *p2;

p1 = &i;

p2 = &j;

*p1 = *p2;

}

et

/* programme 2 */

int main() {

int i = 3, j = 6;

int *p1, *p2;

p1 = &i;

p2 = &j;

p1 = p2;

}

Question 1

Complétez la configuration ci-dessous en vous arrêtant juste avant la dernière affectation de chacun des programmes.

Variable Adresse Valeur i 0x48318360

j 0x48318364

p1 0x48318384

p2 0x48318392

(6)

Question 2

Complétez la configuration ci-dessous après l’affectation *p1 = *p2; du premier programme.

Variable Adresse Valeur i 0x48318360

j 0x48318364

p1 0x48318384

p2 0x48318392

Question 3

Complétez la configuration ci-dessous après l’affectation p1 = p2; du deuxième programme.

Variable Adresse Valeur i 0x48318360

j 0x48318364

p1 0x48318384

p2 0x48318392

PASSAGE DE POINTEURS EN ARGUMENTS DE FONCTION

Exemple :

#include <stdio.h>

void test(int *pa, int *pb) {

*pa = 10;

*pb = 20;

}

int main(void) {

int a;

int b;

int *pa = &a;

int *pb = &b;

test(&a, &b);

(7)

test(pa, pb);

printf("a = %d, b = %d\n", a, b);

printf("a = %d, b = %d\n", *pa, *pb);

return 0;

}

Résultat :

a = 10, b = 20 a = 10, b = 20

On peut remarquer que les appels test(&a, &b) et test(pa, pb) réalisent la même opération.

POINTEUR DE POINTEUR

Au même titre que n’importe quel autre objet, un pointeur a lui aussi une adresse. Dès lors, il est possible de créer un objet pointant sur ce pointeur : un pointeur de pointeur.

Exemple : int a = 10;

int *pa = &a;

int **pp = &pa;

Celui-ci s’utilise de la même manière qu’un pointeur si ce n’est qu’il est possible d’opérer deux indirections : une pour atteindre le pointeur référencé et une seconde pour atteindre la variable sur laquelle pointe le premier pointeur.

#include <stdio.h>

int main(void) {

int a = 10;

int *pa = &a;

int **pp = &pa;

printf("a = %d\n", **pp);

return 0;

}

Résultat : a = 10

Ceci peut continuer à l’infini pour concevoir des pointeurs de pointeur de pointeur de pointeur de…

LE TYPE VOID

Le mot-clé void permet d’indiquer qu’une fonction n’utilise aucun paramètre et/ou ne retourne aucune valeur.

(8)

void est en fait un type, au même titre que int ou double.

En fait, il s’agit d’un type dit « incomplet », c’est à dire que la taille de ce dernier n’est pas calculable et qu’il n’est pas utilisable dans des expressions. Quel est l’intérêt de la chose ? Il permet de créer des pointeurs « génériques » (ou « universels »).

En effet, nous venons de voir qu’un pointeur devait toujours être typé. Cependant, cela peut devenir gênant si vous souhaitez créer une fonction qui doit pouvoir travailler avec n’importe quel type de pointeur. C’est ici que le type void intervient : un pointeur sur void est considéré comme un pointeur générique, ce qui signifie qu’il peut référencer n’importe quel type d’élément.

En conséquence, il est possible d’affecter n’importe quelle adresse d’élément à un pointeur sur void et d’affecter un pointeur sur void à n’importe quel autre pointeur (et inversement).

Exemple : int a;

double b;

void *p;

double *r;

p = &a; /* Correct */

p = &b; /* Correct */

r = p; /* Correct */

Il est possible d’afficher une adresse à l’aide de l’indicateur de conversion p de la fonction printf().

Ce dernier attend en argument un pointeur sur void. Vous voyez ici l’intérêt d’un pointeur générique : un seul indicateur suffit pour afficher tous les types de pointeurs.

int a;

int *p = &a;

printf("%p == %p\n", (void *)&a, (void *)p);

Si vous souhaitez connaître l’adresse invalide de votre système : printf("%p\n", (void *)0); /* Comme ceci */

printf("%p\n", (void *)NULL); /* Ou comme cela */

Vous devriez trouver le plus souvent : 0000 0000.

EXERCICES EXERCICE N°1

Soit le code en C partiel ci-dessous : int main()

{

int A = 1;

(9)

int B = 2;

int C = 3;

int *P1, *P2;

P1=&A;

P2=&C;

*P1=(*P2)++; /* incrémentation postfix : affectation puis incrémentation */

P1=P2;

P2=&B;

*P1-=*P2; /* diminue de */

++*P2; /* incrémentation */

*P1*=*P2;

A=++*P2**P1;

P1=&A;

*P2=*P1/=*P2;

return 0;

}

Question

Complétez le tableau ci-dessous :

A B C P1 P2

Init 1 2 3 / /

P1=&A 1 2 3 &A / P2=&C

*P1=(*P2)++

P1=P2

P2=&B

*P1-=*P2

++*P2

*P1*=*P2

A=++*P2**P1

P1=&A

*P2=*P1/=*P2

(10)

EXERCICE N°2 Question

Programmez une fonction nommée « echange », dont le rôle est d’échanger la valeur de deux variables de type int. Autrement dit, la valeur de la variable « a » doit devenir celle de

« b » et la valeur de « b », celle de « a ».

Références

Documents relatifs

Deux ETCD (Équipement Terminal d’un Circuit de Données) qui adaptent les données issues de l’ETTD au support de transmission (modulation, codage) et gèrent la

Chaque station peut émettre, dès qu’elle le désire (méthode aléatoire), ce qui implique un risque de conflit d’accès avec les autres stations et des procédures de résolution

Chaque fichier objet est incomplet, insuffisant pour être exécuté, car il contient des appels de fonctions ou des références à des variables qui ne sont pas définies dans le

La déclaration d’une fonction ou d’une procédure consiste à donner le type de la fonction, son nom, et le type des arguments reçus en paramètres, tout cela en dehors de la fonction

- les membres publics de la classe de base sont accessibles par tous, fonctions membres et objets de la classe de base et de la classe dérivée. - les membres protégés de la classe

Vous pourriez écrire une suite de if ou différentes entrées d’un switch qui, suivant le nombre entré, appelleraient une fois printf() , puis deux fois, puis trois fois, etc.

Comme nous pouvons le voir, bien que la condition soit fausse (pour rappel, une valeur nulle correspond à une valeur fausse), le corps de la boucle est exécuté une fois puisque

e coursename : afficher le nom d’un professeur le plus ancien souhaitant enseigner le cours coursename f filename : recopie la liste chaînée qui en résulte dans le