IX. Pointeurs et fonctions▲
IX-A. Objectifs▲
Dans ce chapitre, nous manipulerons deux notions essentielles du langage C : les pointeurs et les fonctions…
IX-B. Binaire, octets…▲
IX-B-1. Binaire▲
L'ordinateur ne « comprend » que des 0 et des 1. On dit qu'il travaille en base 2 ou binaire (tout simplement parce que ce système de numération est particulièrement adapté à la technologie électronique utilisée pour construire les ordinateurs). Au contraire des machines, les humains préfèrent généralement la base 10, c'est-à-dire le système décimal.
La table 8.1 donne l'écriture binaire et décimale de quelques nombres.
Binaire | Décimal |
0 | 0 |
1 | 1 |
10 | 2 |
11 | 3 |
100 | 4 |
101 | 5 |
110 | 6 |
111 | 7 |
1000 | 8 |
1001 | 9 |
1010 | 10 |
… | … |
11111111 | 255 |
Nous voyons dans cette table que le nombre 7 est donc représenté par trois symboles « 1 » qui se suivent : 111.
Chacun de ces « 1 » et « 0 » s'appelle un bit (contraction de l'anglais binary digit : chiffre binaire). Par exemple, le nombre « 1010001 » est composé de 7 bits.
Un ensemble de huit bits consécutifs (chacun pouvant prendre la valeur 0 ou 1), s'appelle un octet (byte en anglais).
- 11110000 représente un octet
- 10101010 représente un octet
- 100011101 ne représente pas un octet
IX-B-2. Compter en base 2, 3, 10 ou 16▲
Vous avez l'habitude de compter en base 10 (décimal). Pour cela, vous utilisez dix chiffres de 0 à 9.
Pour compter en base 2 (binaire), on utilisera deux chiffres : 0 et 1.
Pour compter en base 3, on utilisera 3 chiffres : 0, 1 et 2.
La base 16 (base hexadécimale) est très utilisée en informatique et notamment en langage assembleur. Mais compter en hexadécimal nécessite d'avoir 16 chiffres ! La solution est de compléter les chiffres habituels de la base 10 par des lettres de l'alphabet comme le montre le tableau 8.2.
Base 16 | Base 10 |
0 | 0 |
1 | 1 |
2 | 2 |
3 | 3 |
4 | 4 |
5 | 5 |
6 | 6 |
7 | 7 |
8 | 8 |
9 | 9 |
A | 10 |
B | 11 |
C | 12 |
D | 13 |
E | 14 |
F | 15 |
Dans la pratique, afin de ne pas faire de confusion entre le nombre 10 (en décimal) et ce même nombre en hexadécimal, on précède ce dernier par le préfixe « 0x » :
int
i1=
10
;
int
i2=
0x10
;
printf
(
"
i1=%d i2=%d
"
,i1,i2);
Ce programme affichera i1=10 i2=16.
IX-C. Variables : pointeurs et valeurs▲
IX-C-1. Variables et mémoire▲
Une variable est une zone mémoire disponible pour y ranger des informations. Par exemple, dans le cas d'une variable car déclarée par char car ;, la zone mémoire disponible sera de 1 octet (exactement une case mémoire). En revanche, une variable de type int utilisera au moins 4 octets (la valeur exacte dépend du compilateur), c'est-à-dire 4 cases mémoires.
À présent, représentons‐nous la mémoire comme une unique et longue « rue » remplie de maisons. Chaque case mémoire est une maison. Chaque maison porte un numéro, c'est ce qui permet de définir son adresse postale. Cette adresse est unique pour chaque maison dans la rue. De manière analogue, pour une case mémoire, on parlera d'adresse mémoire.
#include <stdio.h>
int
main (
) {
char
c=
'
A
'
;
printf (
"
c contient %c et est stocké à %p
\n
"
,c,&
c);
return
0
;
}
L'exécution nous donne, par exemple :
c contient A et est stocké à 0xbf8288a3
Le format « %p » permet d'afficher une adresse en hexadécimal.
Vous pouvez essayer le programme précédent, mais vous n'aurez probablement pas la même adresse mémoire que dans l'exemple.
IX-C-2. Pointeurs▲
Pour manipuler les adresses mémoire des variables, on utilise des variables d'un type spécial : le type « pointeur ». Une variable de type pointeur est déclarée ainsi :
type*
variable;
L'espace après * peut être omis ou placé avant, peu importe.
C'est le signe * qui indique que nous avons affaire à un pointeur. L'indication qui précède * renseigne le compilateur sur le type de case pointée, et comment se comporteront certaines opérations arithmétiques(17). Si v est un pointeur vers un int, et que nous désirons modifier l'entier pointé, nous écrirons :
*
v =
valeur;
*v peut être interprété comme « contenu de l'adresse mémoire pointée par v ».
#include <stdio.h>
int
main (
) {
char
car=
'
C
'
;
char
*
ptr_car; /* Variable de type pointeur */
printf
(
"
Avant, le caractère est : %c
\n
"
,car);
ptr_car =
&
car; /* ptr_car = adresse de car */
*
ptr_car =
'
E
'
; /* on modifie le contenu de l'adresse mémoire */
printf
(
"
Après, le caractère est : %c
\n\n
"
,car);
printf
(
"
Cette modification est due à :
\n
"
);
printf
(
"
Adresse de car : %p
\n
"
,&
car);
printf
(
"
Valeur de ptr_car : %p
\n
"
,ptr_car);
return
0
;
}
ptr_car = &car ; | ptr_car contient l'adresse postale de Monsieur car |
*ptr_car = 'E'; | On entre chez Monsieur car et on y dépose le caractère E |
printf ("%c",car) ; | On va regarder ce qu'il y a chez Monsieur car pour l'afficher à l'écran |
Table 8.3 - Pointeurs et valeurs
On supposera que la variable car est stockée à l'adresse 0x0100 ; la variable ptr_car est stockée à l'adresse 0x0110.
Reprenons les deux premières lignes d'exécution du programme :
char
car=
'
C
'
;
char
*
ptr_car;
La mémoire ressemble alors à ceci :
Nom de la variable | car | ptr_car |
Contenu mémoire | C | ? |
Adresse mémoire (0x) | 100 | 110 |
Nous mettons « ? » comme contenu de ptr_car car cette variable n'a pas encore été initialisée. Sa valeur est donc indéterminée.
Le programme se poursuit par :
printf
(
"
Avant, le caractère est : %c
\n
"
,car);
ptr_car =
&
car; /* ptr_car = adresse de car */
Il y aura donc affichage à l'écran de Avant, le caractère est : Cpuis la mémoire sera modifiée comme ceci :
Nom de la variable | car | ptr_car |
Contenu mémoire | C | 100 |
Adresse mémoire (0x) | 100 | 110 |
ptr_carn'est donc qu'une variable, elle contient la valeur 100 (l'adresse de la variable car).
Finalement, la ligne :
*
ptr_car =
'
E
'
; /* on modifie le contenu de l'adresse mémoire */
conduit à modifier la mémoire comme suit :
Nom de la variable | car | ptr_car |
Contenu mémoire | E | 100 |
Adresse mémoire (0x) | 100 | 110 |
Prenez le temps de comprendre tout ce qui précède avant de passer à la suite.
IX-C-3. Exercice d'application▲
Exercice n°8.1 — Intelligent
Réalisez un programme équivalent qui change une valeur numérique (int) de 10 à 35.
#include <stdio.h>
int
main (
) {
char
car=
'
C
'
;
char
*
ptr_car=
NULL
;
printf
(
"
Avant, le caractère est : %c
\n
"
,car);
ptr_car =
&
car; /* ptr_car = adresse de car */
*
ptr_car =
'
E
'
; /* on modifie le contenu de l'adresse mémoire */
printf
(
"
\n
Après le caractère est : %c
\n
"
,car);/*on a modifié car*/
return
0
;
}
La constante NULL vaut 0. Or un programme n'a pas le droit d'accéder à l'adresse 0. Dans la pratique, écrire prt_car=NULL signifie que l'adresse mémoire pointée est pour l'instant invalide.
On peut se demander à quoi servent les pointeurs ! En effet, ceci a l'air compliqué alors que l'on pourrait faire bien plus simple.
Ainsi tout le travail effectué par le programme précédent se résume au code suivant :
#include <stdio.h>
int
main (
) {
char
car=
'
C
'
;
printf
(
"
Avant, le caractère est : %c
\n
"
,car);
car=
'
E
'
;
printf
(
"
\n
Après le caractère est : %c
\n
"
,car);/*on a modifié car*/
return
0
;
}
Nous verrons par la suite que dans certains cas, on ne peut pas se passer des pointeurs, notamment pour certaines manipulations au sein des fonctions…
IX-D. Fonctions▲
Une fonction est un petit bloc de programme qui à l'image d'une industrie va créer, faire ou modifier quelque chose. Un bloc de programme est mis sous la forme d'une fonction si celui‐ci est utilisé plusieurs fois dans un programme ou simplement pour une question de clarté. De la même manière que nous avions défini la fonction main, une fonction se définit de la façon suivante :
<type de sortie> <nom de la fonction> (<paramètres d'appels>) {
Déclaration des variables internes à la fonction
Corps de la fonction
Retour
}
Comme indiqué précédemment, il ne faut pas mettre les < et >, qui ne sont là que pour faciliter la lecture.
Voici un exemple de fonction qui renvoie le maximum de deux nombres :
int
maximum (
int
valeur1, int
valeur2) {
int
max;
if
(
valeur1<
valeur2)
max=
valeur2;
else
max=
valeur1;
return
max;
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
#include <stdio.h>
int
maximum (
int
valeur1, int
valeur2) {
int
max;
if
(
valeur1<
valeur2)
max=
valeur2;
else
max=
valeur1;
return
max;
}
int
main (
) {
int
i1,i2;
printf
(
"
entrez 1 valeur:
"
);
scanf
(
"
%d
"
,&
i1);
printf
(
"
entrez 1 valeur:
"
);
scanf
(
"
%d
"
,&
i2);
printf
(
"
max des 2 valeurs :%d
\n
"
,maximum
(
i1,i2));
return
0
;
}
- Dans la pratique (et en résumant un peu), quand vous tapez ./programme pour lancer votre programme, l'ordinateur exécute la fonction main qui se trouve à la ligne 12. L'ordinateur demande donc ensuite d'entrer deux valeurs qui seront stockées dans i1 et i2.
- Supposons que l'utilisateur ait entré les valeurs 111 et 222.
- Arrivé à l'exécution de la ligne 18, la machine doit appeler la fonction printf, et pour cela, chacun des paramètres de la fonction doit être évalué.
- L'exécution passe alors à la fonction maximum, ligne 3. À ce niveau, on peut comprendre intuitivement que la variable valeur1 prend la valeur de i1 (c'est‐à‐dire 111) et la valeur valeur2 prend la valeur de i2 (c'est‐à‐dire 222), du fait de l'appel de maximum(i1,i2).
- La fonction maximum se déroule. Après avoir passé les lignes 4 à 8, la valeur de max sera de 222.
- À la ligne 9 on sort donc de la fonction maximum pour revenir à l'appel fait par printf de la ligne 18. Tous les paramètres de la fonction ayant été évalués (maximum(i1,i2) a été évalué à 222), printf est exécuté et dans le format, « %d » est remplacé par 222.
- Le programme se termine.
IX-E. Type void▲
Le mot void signifie vide. Le type void est notamment utilisé comme type de sortie pour les fonctions qui ne retournent aucun résultat (qu'on appelle aussi procédures).
Exemple :
/* Fonction affichant un caractère */
void
affiche_car (
char
car) {
printf (
"
%c
"
,car);
}
#include <stdio.h>
/* Fonction affichant un caractère */
void
affiche_car (
char
car) {
printf (
"
%c
"
,car);
}
int
main (
) {
char
c;
int
i;
printf
(
"
Veuillez entrer un caractère:
"
);
scanf
(
"
%c
"
,&
c);
for
(
i=
0
; i<
100
; i++
)
affiche_car
(
c);
return
0
;
}
IX-F. Variables globales et locales▲
Les variables déclarées dans les fonctions sont dites locales. Il existe aussi les variables dites globales qui sont déclarées en‐dehors de toute fonction (y compris le main()).
Les variables globales sont modifiables et accessibles par toutes les fonctions sans avoir besoin de les passer en paramètres. Il est de ce fait extrêmement dangereux d'utiliser des variables globales.
Les variables locales ne sont modifiables et accessibles que dans la fonction où elles sont déclarées. Pour les modifier ou les utiliser dans une autre fonction, il est nécessaire de les passer en paramètres.
IX-F-1. Variables locales▲
#include <stdio.h>
int
carre (
int
val) {
int
v =
0
; /* Variable locale */
v =
val *
val;
return
v;
}
int
main (
) {
int
val_retour =
0
; /* Variable locale */
val_retour =
carre (
2
);
printf
(
"
Le carré de 2 est : %d
\n
"
, val_retour);
return
0
;
}
La variable val_retourde main et la variable v de carre sont toutes deux des variables locales. Le passage des valeurs se fait par copie. Lors du return v, on peut imaginer que c'est le contenu de vqui est renvoyé, puis stocké dans val_retour. La variable v est détruite lorsqu'on revient dans main.
IX-F-2. Variables globales▲
#include <stdio.h>
int
val =
0
;
int
val_retour =
0
;
void
carre (
) {
val_retour =
val *
val;
}
int
main (
) {
val =
2
;
carre (
);
printf
(
"
Le carré de 2 est %d
\n
"
, val_retour);
return
0
;
}
Les variables val et val_retoursont des variables globales. On constate que le programme devient rapidement illisible et difficile à vérifier…
Le conseil à retenir est de ne pas utiliser de variable(s) globale(s) lorsqu'il est possible de s'en passer.
IX-F-3. Utilisation et modification de données dans les fonctions▲
L'appel d'une fonction peut s'effectuer à l'aide de paramètres.
Ces paramètres figurent dans les parenthèses de la ligne de titre de la fonction.
#include <stdio.h>
void
affiche_Nombre (
int
no) {
no=
no+
1
;
printf (
"
Le nombre no est : %d
\n
"
,no);
}
int
main (
){
int
a=
12
;
affiche_Nombre (
a);
printf
(
"
Le nombre a est : %d
\n
"
,a);
return
0
;
}
Dans le cas qui précède, lors de l'appel de fonction, c'est le contenu de a(c'est-à-dire 12) qui est envoyé dans la fonction, et qui se retrouve donc stocké dans no. Le nombre 12 existe donc en deux exemplaires : une fois dans la variable a de main et une fois dans la variable no de affiche_Nombre. Puis on ajoute 1 dans no (mais pas dans a), et on l'affiche. On voit donc apparaître :
Le nombre no est : 13
La procédure se termine alors et la variable no est détruite. On revient dans la fonction main qui affiche le contenu de a qui vaut encore 12… On voit donc apparaître :
Le nombre a est : 12
Il faut donc retenir que les paramètres d'une fonction sont passés par valeur et que modifier ces paramètres ne modifie que des copies des variables d'origine.
Pour pouvoir modifier les variables d'origine, il ne faut plus passer les paramètres par valeur, mais par adresse, en utilisant les pointeurs, comme c'est le cas dans l'exemple qui suit.
#include <stdio.h>
void
avance_Position (
int
*
pointeur_int) {
*
pointeur_int =
*
pointeur_int +
2
;
}
int
main (
) {
int
x=
0
;
printf
(
"
Position de départ : %d
\n
"
,x);
avance_Position (&
x);
printf
(
"
Nouvelle position : %d
\n
"
,x);
return
0
;
}
Imaginons que pointeur_int se trouve en mémoire à l'adresse 20 et x à l'adresse 10 :
Nom de la variable | x | pointeur_int |
Contenu mémoire | ||
Adresse mémoire (0x) | 10 | 20 |
À présent, déroulons le programme principal :
- int x=0; : déclaration et initialisation de x à 0
- Avance_Position (&x); : appel de la fonction Avance_Position (&x); avec la valeur 10 (l'adresse de x) en paramètre
Table 8.8 - Stockage de variables (e) Nom de la variable x pointeur_int Contenu mémoire 0 10 Adresse mémoire (0x) 10 20 - void Avance_Position (int* pointeur_int) : on lance cette fonction et pointeur_int vaut donc 10
- * pointeur_int = (* pointeur_int) + 2; : (*pointeur_int) pointe sur la variable x. L'ancienne valeur de x va être écrasée par sa nouvelle valeur : 0+2
- printf("Nouvelle position :%d",x); : on se retrouve avec 2 comme valeur.
Nous devons utiliser des pointeurs pour pouvoir modifier certaines variables en dehors de l'endroit où elles sont déclarées. On dit généralement qu'une fonction n'est pas capable de modifier ses arguments. L'usage des pointeurs devient dans ce cas nécessaire… Ainsi, le programme suivant affichera 2 et non 4.
#include <stdio.h>
void
calcule_double (
int
x){
x=
x+
x;
}
int
main (
) {
int
i=
2
;
calcule_double
(
i);
printf
(
"
i vaut à présent :%d
"
,i); /* il vaut toujours 2 !!! */
return
0
;
}
Pour rendre le programme fonctionnel en utilisant les pointeurs, il faudrait écrire :
#include <stdio.h>
void
calcule_double (
int
*
p_i){
*
p_i=*
p_i+*
p_i;
}
int
main (
) {
int
i=
2
;
calcule_double
(&
i);
printf
(
"
i vaut à présent :%d
"
,i);
return
0
;
}
Néanmoins, la bonne pratique (celle qui donne les programmes les plus simples à comprendre et les plus faciles à déboguer) serait d'écrire :
#include <stdio.h>
int
calcule_double (
int
a){
a=
a+
a;
return
a;
}
int
main (
) {
int
i=
2
;
i=
calcule_double
(
i);
printf
(
"
i vaut à présent :%d
"
,i);
return
0
;
}
Observez attentivement les trois exemples qui précèdent et soyez sûr d'avoir bien compris pourquoi le premier affiche 2 alors que les deux suivants affichent 4.
Exercice n°8.2 — Encore un
Modifiez le programme précédent afin d'avoir à disposition une fonction qui prend en paramètre un pointeur vers un entier et modifie l'entier pointé en lui ajoutant 1.
int
*
x;
*
x++
;
augmentera l'adresse de x (on va chez le voisin) et non sa valeur. Pour cela il faut écrire :
(*
x)++
;
Ceci est une faute courante, prenez garde …
Exercice n°8.3 — Mon beau sapin
Reprenez le programme du sapin de Noël du chapitre précédent. Écrivez deux fonctions :
- ramure (int clignes) qui dessine la ramure du sapin sur clignes de hauteur,
- tronc (int pos_t) qui dessine le tronc en position pos_t (pos_t blancs avant le tronc).
Puis utilisez ces deux fonctions pour dessiner le sapin sur n lignes.
IX-F-4. Prototypes des fonctions▲
Dans la pratique, lorsqu'on souhaite écrire un programme qui contient de nombreuses fonctions, on essaie de présenter le code de manière propre et structurée. Ainsi, plutôt que cette version :
#include <stdio.h>
int
max (
int
x, int
y) {
if
(
x<
y)
return
y;
else
return
x;
}
int
min (
int
x, int
y) {
if
(
x<
y)
return
x;
else
return
y;
}
int
main (
) {
int
i1,i2;
i1=
123
;
i2=
1267
;
printf
(
"
max : %d
\n
"
,max
(
i1,i2));
printf
(
"
min : %d
\n
"
,min
(
i1,i2));
return
0
;
}
on lui préfèrera la version suivante où les prototypes des fonctions disponibles figurent en début de code (un peu comme une table des matières) :
#include <stdio.h>
/* PROTOTYPES : */
int
max (
int
x, int
y);
int
min (
int
x, int
y);
int
max (
int
x, int
y) {
if
(
x<
y)
return
y;
else
return
x;
}
int
min (
int
x, int
y) {
if
(
x<
y)
return
x;
else
return
y;
}
int
main (
) {
int
i1,i2;
i1=
123
;
i2=
1267
;
printf
(
"
max : %d
\n
"
,max
(
i1,i2));
printf
(
"
min : %d
\n
"
,min
(
i1,i2));
return
0
;
}
L'intérêt de faire figurer les prototypes en début de programme, en plus d'augmenter la clarté du code, sera vu par la suite.
IX-G. Corrigés des exercices du chapitre▲
Corrigé de l'exercice n°8.1 — Intelligent
#include <stdio.h>
int
main (
) {
int
val =
10
;
int
*
ptr_val;
printf (
"
Avant le nombre est : %d
\n
"
,val);
ptr_val =
&
val;
*
ptr_val =
35
;
printf (
"
Après le nombre est : %d
\n
"
,val);
return
0
;
}
Corrigé de l'exercice n°8.2 — Encore un
#include <stdio.h>
void
ajoute_un (
int
*
pointeur_int) {
*
pointeur_int =
*
pointeur_int +
1
;
}
int
main (
) {
int
i=
10
;
printf (
"
i=%d
\n
"
,i);
ajoute_un (&
i);
printf (
"
i=%d
\n
"
,i);
return
0
;
}
Corrigé de l'exercice n°8.3 — Mon beau sapin
#include <stdio.h>
#include <stdlib.h>
// Dessin de la ramure du sapin
void
ramure (
int
clignes) {
int
i=
0
, j=
0
;
for
(
i=
1
; i<=
clignes; i++
) {
for
(
j=
0
; j<
clignes-
i; j++
)
printf
(
"
*
"
);
for
(
j=
1
; j<=
(
i*
2
-
1
); j++
)
printf
(
"
"
);
printf
(
"
\n
"
);
}
}
// Dessin du tronc du sapin
void
tronc (
int
pos_t) {
int
i=
0
, j=
0
;
for
(
j=
1
; j<=
3
; j++
) {
for
(
i=
1
; i<=
pos_t; i++
)
printf (
"
"
);
printf (
"
@@@
\n
"
);
}
}
int
main (
) {
int
nb_lig =
15
;
ramure (
nb_lig);
tronc (
nb_lig -
2
);
return
0
;
}
IX-H. À retenir▲
IX-H-1. Les différentes bases de numération▲
Il est souhaitable de retenir que :
- compter en base 2 (binaire) revient à n'utiliser que des 0 et des 1 ;
- un octet est formé de 8 bits : 11110101;
- la base 10 est celle qu'on emploie tous les jours;
- en base 16 (base hexadécimale), des lettres sont utilisées en plus des 10 chiffres (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F).
IX-H-2. Pointeur▲
Un pointeur est une variable faite pour contenir une adresse mémoire, souvent l'adresse d'une autre variable.
IX-H-3. Structure d'un programme C▲
Pour finir, voici deux programmes qui reprennent l'essentiel de ce qui a été vu :
#include <stdio.h>
int
main (
) {
int
i=
100
;
int
*
pointeur_sur_i=
NULL
;
pointeur_sur_i=&
i;
*
pointeur_sur_i=
200
;
printf (
"
i vaut à présent %d
\n
"
,i);
return
0
;
}
Le programme précédent revient à stocker 200 dans la variable i.
#include <stdio.h>
/* Prototypage */
void
avance_Position (
int
*
x);
void
avance_Position (
int
*
x) {
(*
x)+=
2
;
}
int
main (
) {
int
i=
0
;
int
x=
0
;
printf
(
"
Position de départ : %d
\n
"
,x);
for
(
i =
0
; i<
5
; i++
) {
avance_Position (&
x);
printf (
"
Nouvelle position : %d
\n
"
,x);
}
return
0
;
}