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.
int taille = 100;
float tableau[taille];
#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 */
while(*s1!=0) {
*s2 = *s1;
++s1;
s2++;
}
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>
.
void * malloc(size_t
taille);
void * calloc(size_t
nb_elem, size_t taille);
void * realloc(void
* pointeur, size_t nouvelle_taille);
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 :
- Il n'est pas possible d'avoir un bloc mémoire contigü de la taille demandée : l'allocation est impossible. Le bloc initial n'est pas changé !
- Il reste un espace mémoire suffisant après le bloc donné en paramètre pour constituer le bloc demandé : le complément de mémoire nous est alors attribué.
- Il n'y a pas assez de mémoire juste après le bloc mémoire déjà alloué pour former un bloc continu mais le système dispose de l'espace à un autre endroit, le bloc initial est déplacé...
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 :
type * t1;
on triche un peu, on alloue un bloc global et on accède aux éléments par une formule typei*n+j
.type * t3[];
un tableau de ligne ...type ** t2;
un vrai tableau !!!


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
typedef int * pint;
void swap (pint* a, pint* b) {
pint c = *a;
*a = *b;
*b = c;
}
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)
() fonction ou parenthèses, []
->
. et ->
->
(cast)sizeof & adresse * indirection - négation ! ~ ++ --
<-
* / %
->
+ -
->
<< >>
->
> >= < <=
->
== !=
->
& bit
->
^
->
|
->
&&
->
||
->
?:
<-
= += -= *= /= %= >>= <<= &= ^= |=^
<-
, séquence
<-