Le C en 20 heures


précédentsommairesuivant

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. 

Table 8.1 - Équivalences entre l'écriture binaire et l'écriture décimale
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. 

Table 8.2 - Base 16 et base 10
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 » :

 
Sélectionnez
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.

Par exemple, ce petit programme va afficher l'adresse mémoire d'une variable de type caractère.
 
Sélectionnez
#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 :
 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
*v = valeur;

*v peut être interprété comme « contenu de l'adresse mémoire pointée par v  ».

Lisez le programme suivant ainsi que les explications associées.
 
Sélectionnez
#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;
}
Voici ce que cela donne avec notre rue bordée de maisons : 
Table 8.3 - Pointeurs et valeurs
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 :

 
Sélectionnez
char car='C';
char* ptr_car;

La mémoire ressemble alors à ceci : 

Table 8.4 - Stockage de variables (a)
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 :

 
Sélectionnez
 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 : C puis la mémoire sera modifiée comme ceci : 

Table 8.5 - Stockage de variables (b)
Nom de la variable car ptr_car
Contenu mémoire C 100
Adresse mémoire (0x) 100 110

ptr_car n'est donc qu'une variable, elle contient la valeur 100 (l'adresse de la variable car).

Finalement, la ligne :

 
Sélectionnez
*ptr_car = 'E';  /* on modifie le contenu de l'adresse mémoire */

conduit à modifier la mémoire comme suit : 

Table 8.6 - Stockage de variables (c)
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

Image non disponible Exercice n°8.1 — Intelligent

Réalisez un programme équivalent qui change une valeur numérique (int) de 10 à 35.

Notons que, dans la pratique, lorsque l'on déclare un pointeur, il est plus prudent de l'initialiser :
 
Sélectionnez
#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("\nAprè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 :

 
Sélectionnez
#include <stdio.h>
int main () {
   char car='C';
   printf("Avant, le caractère est : %c\n",car);
   car='E';
   printf("\nAprè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 :

 
Sélectionnez
<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 :

 
Sélectionnez
int maximum (int valeur1, int valeur2) {
   int max;
   if (valeur1<valeur2)
      max=valeur2;
   else
      max=valeur1;
   return max;
}
Le programme complet pourrait être le suivant :
 
Sélectionnez
  1. #include <stdio.h> 
  2.   
  3.  int maximum (int valeur1, int valeur2) { 
  4.      int max; 
  5.      if (valeur1<valeur2) 
  6.          max=valeur2; 
  7.      else 
  8.          max=valeur1; 
  9.      return max; 
  10. } 
  11.  
  12. int main () { 
  13.     int i1,i2; 
  14.     printf("entrez 1 valeur:"); 
  15.     scanf("%d",&i1); 
  16.     printf("entrez 1 valeur:"); 
  17.     scanf("%d",&i2); 
  18.     printf("max des 2 valeurs :%d\n",maximum(i1,i2)); 
  19.      return 0; 
  20. } 
  • 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 :

 
Sélectionnez
/* Fonction affichant un caractère */
void affiche_car (char car) {
   printf ("%c",car);
}
Exemple de programme complet :
 
Sélectionnez
#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

 
Sélectionnez
#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_retour de 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 v qui est renvoyé, puis stocké dans val_retour. La variable v est détruite lorsqu'on revient dans main.

IX-F-2. Variables globales

 
Sélectionnez
#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_retour sont 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.

Par exemple :
 
Sélectionnez
#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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
#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 : 

Table 8.7 - Stockage de variables (d)
Nom de la variable x pointeur_int
Contenu mémoire    
Adresse mémoire (0x) 10 20

À présent, déroulons le programme principal :

  1. int x=0; : déclaration et initialisation de x à 0
  2. 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 xpointeur_int
    Contenu mémoire010
    Adresse mémoire (0x)1020
  3. void Avance_Position (int* pointeur_int) : on lance cette fonction et pointeur_int vaut donc 10
  4. * 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
  5. 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.

 
Sélectionnez
#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 :

 
Sélectionnez
#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 :

 
Sélectionnez
#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.

Image non disponible 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.

Ce type d'opération sur les pointeurs est dangereux :
 
Sélectionnez
int* x;
*x++;

augmentera l'adresse de x (on va chez le voisin) et non sa valeur. Pour cela il faut écrire :
 
Sélectionnez
(*x)++;

Ceci est une faute courante, prenez garde …

Image non disponible 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 :

 
Sélectionnez
#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) :

 
Sélectionnez
#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

Image non disponible Corrigé de l'exercice n°8.1 — Intelligent

 
Sélectionnez
#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;
}

Image non disponible Corrigé de l'exercice n°8.2 — Encore un

 
Sélectionnez
#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;
}

Image non disponible Corrigé de l'exercice n°8.3 — Mon beau sapin

 
Sélectionnez
#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 :

 
Sélectionnez
#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.

 
Sélectionnez
#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;
}

précédentsommairesuivant
Ajouter 1 à un pointeur sur un int revient à ajouter 4 à l'adresse, alors qu'ajouter 1 à un pointeur sur un char revient à n'ajouter que 1 à l'adresse.

  

Licence Creative Commons
Le contenu de cet article est rédigé par Eric Berthomier et Daniel Schang et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2013 Developpez.com.