Préambule

Dans les semaines bloquées, nous avons été confrontés à quelques petits problèmes dont les solutions ne sont pas toujours complètement satisfaisantes.

(1) Tableau de taille variable

On veut disposer d'un tableau tableau dont la taille peut varier.

 
Code (a)
int   taille = 100;
float tableau[taille];
 
Code (b)
#define TAILLE_MAX 100
float tableau[TAILLE];
int taille; /* prend une valeur entre */
            /* 0 et TAILLE_MAX-1      */
 

L'exemple "naturel" (a) ne marche pas, la solution est donnée au (b).

(2) Passage de paramètres à une fonction

De la même manière, on voulait écrire une fonction pour saisir les différents éléments du tableau. On ne voulait pas forcément utiliser toute la place mémoire réservée pour le tableau. La taille du tableau doit être modifiable par la fonction.

On avait envie d'écrire une fonction telle que :

void saisirTableau(float tableau[], int taille);

Cette fonction ne donne pas le résultat désiré, il faut utiliser la fonction suivante :

int saisirTableau(float tableau[]) { 
 int taille;
   
 scanf("%d", &taille);
 /* saisie des "taille" éléments du tableau */ 
 ... 
return taille; 
}

L'appel de la fonction se fait naturellement par taille = saisirTableau(tableau);

(3) Échange de deux variables

On veut écrire une procédure d'échange de deux variables

void swap (int a, int b) {
 int c;
   
 c = a; 
 a = b; 
 b = c; 
}

Un petit programme nous montre rapidement qu'une telle fonction ne marche pas....

Le C passe les paramètres des fonctions par valeur (vu pendant les semaines bloquées), on ne travaille pas sur la variable fournie en paramètre mais sur une copie, c'est pour cela que les exemples (2) et (3) ne marchent pas. Pour travailler avec la variable elle-même, il faut la passer par adresse en utilisant des pointeurs.

1. Les Pointeurs


Le langage C permet de manipuler des adresses d'objets (structures ou variables). Le langage C autorise l'allocation dynamique de mémoire grâce aux fonctions système malloc() et free().

1.1. Adresse d'un objet

L'opérateur & appliqué à un objet de type quelconque fournit l'adresse de cet objet en mémoire.

Exemples :

int i, j;      définition des variables entières i et j
&i et &j       adresses de i et de j
int * p;       spécifie que le contenu de p est de type entier.
p est donc une adresse (ou un pointeur)
 
<type> * p;    déclare un pointeur sur le type <type>

L'opérateur d'indirection ou de déréférencement * appliqué à un pointeur donne le contenu (typé) de l'objet pointé.

Exemples :

int x, y, *p;
x=4;
p=&x;
printf("adresse de x %x", p);    /* %x : format hexadécimal */
printf("adresse de x %x, &x);    /* ligne équivalente à la précédente */
printf("contenu %d",*p); 
printf("contenu %d",x);          /* ligne équivalente à la précédente */
y=*p;                            /* "y=x" ou "y=4" */
(*p)++;                          /* incrémente le contenu de p */
printf("valeur de x est %d", x); /* valeur de x : 5 */

1.2. Les opérations sur les pointeurs

Affectation

L'affectation entre pointeurs n'est possible que pour des pointeurs de même type. Si ce n'est pas le cas, il faut utiliser l'opérateur de coercition ("cast"), dit aussi transtypage.

Rappel : transformation d'un réel en entier.

float x = 10.4;
int   i = (int) x;

Il existe un pointeur particulier : le pointeur qui ne pointe sur rien, le pointeur NULL. Ce pointeur ne peut être déréférencé sans provoquer une erreur.

float *f = NULL;
int   *g = (int *) 0;
  
printf("%d", *f);  /* est illicite, provoque une erreur */

En fait, la véritable définition de NULL est la suivante :

#define NULL (void *) 0

L'arithmétique sur les pointeurs

Les opérations autorisées sont les suivantes : incrémentation ("++"), décrémentation ("--"), addition ("+"), soustraction ("-"), affectations étendues ("+=" et "-="), comparaisons ("==" "!=" "<").

POINTEUR' = POINTEUR (adresse) + DEPLACEMENT

C'est une arithmétique intelligente : si on ajoute une valeur n à un pointeur p relatif à un objet de type <type>, p est augmenté en fait de n*sizeof(<type>).

Exemple :

float *f;
 
++f;   /* ou */
f++;   /* augmente f de 4 octets en fait    */
       /* si un float est codé sur 4 octets */

1.3. Les pointeurs et les tableaux

En C, le nom d'un tableau est un pointeur constant. C'est une adresse et plus précisément l'adresse du premier élément du tableau (d'indice 0).

Exemple :

int tab[20];
int *p;
 
p=&tab[0]; /* <=> p=tab; */
 
p++;       /* p pointe désormais sur le deuxième élément du tableau */
tab++;     /* n'est pas possible car tab est un pointeur constant   */

Dans ce cas, p et tab désignent le même emplacement mémoire. *p et tab[0] donnent la même valeur.

En C, tab[i] et *(tab+i) sont deux écritures équivalentes.

Il faut être rigoureux sur la valeur de l'indice sous peine d'avoir le message "core dump" (à cause d'une erreur dite de débordement).

Exemple : Utilisation des chaînes de caractères, codage de la fonction strcpy() de <string.h>.

char ch1[80], ch2[80];
char *s1, *s2;
 
/* lecture de la chaîne de caractères ch1 */
scanf("%s", ch1); /* passage par adresse, ch1 est déjà une adresse*/
 
s1=ch1;
s2=&ch2[0]; /* deux manières de faire la même chose */

Version longue
while(*s1!=0) {
*s2 = *s1;
++s1;
s2++;
}
Version courte
while (*s2++=*s1++);

Aller plus loin : si l'on veut déclarer un pointeur constant, on pourra utiliser l'écriture suivante :

int const * p_constant;

1.4. Pointeurs sur les chaînes de caractères et tableaux de caractères

Il existe deux façons de déclarer des chaînes de caractères : seul le stockage en mémoire est différent (dans le deuxième cas, on stocke non seulement la chaîne mais aussi le pointeur)

char   tabchar[]  = "chaîne";
char * ptrchaine  = "une autre";

Exemple : Copie de chaînes de caractères

 
void strcpy(char *d, char *s) {
int i=0;
 
while ((d[i]=s[i])!='\0')
i++;
}
void strcpy(char *d, char *s) {
while ((*d++=*s++)!='\0') ;
}


 
void strcpy(char *d, char *s) {
while (*s==*t) {
d++;
s++;
}
}
void strcpy(char *d, char *s) {
while (*d++=*s++) ;
}


On modifie un pointeur sur une variable (s) et non pas le contenu pointé.

int strcmp(char *s, char *t) {
while (*s==*t) {
if (!*s) return 0;
s++;
t++; 
}
 
return *s - *t; 
/* pour que cela marche, il faut être sûr
que les char soient signés (dépend de la machine) */
/* return (signed)*s - (signed)*t; */
}

La fonction renvoie 0 si les chaînes sont égales, un nombre négatif si s<t et un nombre positif si s>t .

 
int strlen(char *s) {
char * p=s;
 
while(*p!='\0')
p++;
 
return p-s; 
}
int strlen(char *s) {
int register i=0;
 
while (*s++)
i++;
/* ou while (*(s+i++)); */
 
return i;
}

Le mot-clé register demande au compilateur de placer la variable dans un registre si cela est possible. Dans ce cas, sa manipulation est plus rapide. Cette optimisation est surtout utilisée sur les vieilles machines et les vieux compilateurs. Il y a évidemment d'autres fonctions dans la librairie <string.h>
char * strcat(s, cs) concatène cs à s et renvoie s modifié
. char * strstr(cs, ct) retourne la première occurrence de ct dans cs ou NULL si elle n'y figure pas.

1.5. Allocation dynamique de mémoire

Avec une instruction telle que : int tableau[1000]; on crée un tableau en mémoire. Cette allocation est dite statique car la taille est définitivement fixée à la compilation du programme.

On va voir les instructions d'allocation dynamique afin de mieux gérer la mémoire : on va demander au système de la mémoire quand on en a besoin (au cours de l'exécution du programme) et la lui restituer après.

Les fonctions mémoire sont décrites dans la librairie standard <stdlib.h>.

Allocation mémoire
void *  malloc(size_t taille);
void *  calloc(size_t nb_elem, size_t taille);
void * realloc(void * pointeur, size_t nouvelle_taille);

Restitution mémoire
void free(void * pointeur);

malloc() réserve en mémoire un espace de la taille spécifiée (en octets) et renvoie un pointeur sur cette zone. La fonction renvoie NULL si l'allocation est impossible.

calloc() fait le même chose que malloc(). La zone mémoire est en plus initialisée à zéro.

realloc() permet de changer la taille mémoire d'un bloc précédemment alloué (désignée par pointeur). Prenons l'exemple où l'on a besoin de plus de mémoire, trois cas se présentent :

Si on demande moins de mémoire, le bloc initial est tronqué.

free() libère l'espace mémoire réservé avec les fonctions ci-dessus.

Remarque : void * est le pointeur "universel" : un tel pointeur peut pointer sur n'importe quoi. Pour pouvoir l'utiliser, il faut le transtyper. La prudence est donc de rigueur !

Exemple : Allocation d'un tableau d'entier de taille choisie

int   taille;
int * tableau;
 
printf("Entrer la nouvelle taille ? ");
scanf("%d", &taille);
 
tableau = (int *) malloc (taille*sizeof(int)); /* allocation mémoire*/
 
if (tableau==NULL) {
printf("allocation mémoire impossible");
} else {
/* l'allocation mémoire a eu lieu */
/*    ... traitement voulu ...    */ 
 
 free(tableau); /*rendu mémoire après utilisation*/
}

1.6. Les tableaux de pointeurs.

Les pointeurs sont de variables comme les autres : on peut donc faire un tableau de pointeurs comme on peut faire un tableau d'entiers.

int * tableau[80]; /* crée un tableau de pointeurs sur des entiers */
                   /* de taille 80 */

1.7. Tableaux multidimensionnels

int matrice[2][4] déclare une matrice (statiquement) de deux lignes et de quatre colonnes (c'est juste une convention de nommage). L'indexation commence à 0. L'élément situé en deuxième ligne et troisième colonne est matrice[1][2].

Un tableau statique à deux dimensions est vu en mémoire comme un tableau de tableau : chaque ligne est stockée après l'autre. Ainsi l'indice le plus à droite varie plus vite que ceux de gauche. Une conséquence de ce stockage est que, dans les prototypes, on peut omettre la première dimension mais pas la deuxième car elle est nécessaire pour l'indexation.

Exemple :

void f (int m[][4]);

Au niveau des pointeurs, on a les équivalences suivantes matrice[1][2] ou *(mat[1]+2) ou *(*(mat+1)+2).

Comment déclare-t'on un vrai tableau multidimensionnel dynamiquement ?

On va créer un tableau à 3 lignes et 4 colonnes de différentes manières :

tableau t1
 
tableau t2

1.8. Pointeurs de pointeurs de pointeurs .....

Imaginons le cas où l'on veuille écrire une fonction d'échange de pointeurs sur des entiers. Voilà deux solutions

Code (a)
typedef int * pint;
void swap (pint* a, pint* b) {
pint c = *a;
 
*a = *b;
*b = c;
}
Code (b)
void swap (int** a, int** b) {
int * c = *a;
 
*a = *b;
*b = c;
}

Le typedef est décrit précisément à la section concernant les structures.

Rappel : Priorité des opérateurs

Voici le tableau de priorité des opérateurs par ordre décroissant avec leur sens d'évaluation (de gauche à droite ou de droite à gauche)

Priorité des opérateurs (ordre décroissant)
 () fonction ou parenthèses, []
->
 
. et ->
->
 
(cast)sizeof & adresse * indirection - négation ! ~ ++ --
<-
 
* / %
->
 
+ -
->
 
<< >>
->
 
> >= < <=
->
 
== !=
->
 
& bit
->
 
^
->
 
|
->
 
&&
->
 
||
->
 
?:
<-
 
= += -= *= /= %= >>= <<= &= ^= |=^
<-
 
, séquence
<-
 
Valid XHTML 1.0!