Mémoire dynamique (S4)

Suivant

Les variables dimensionnées sont des variables regroupées sous un même nom auxquelles on accède par un numéro appelé indice. Ces variables sont pratiques toutefois elles souffrent d'une limitation. 

Imaginons un programme dans lequel l'utilisateur peut entrer des valeurs pour que le programme les traite ensuite.

A priori on va imaginer d'utiliser une variable dimensionnée pour stocker les données. Mais si nous avons prévu une variable de 10 entiers on ne pourra en entrer 11. On peut se dire que l'on va dimensionner "plus large" de façon à ne pas être bloqué. Mais outre le fait que l'on va gaspiller de la mémoire si finalement on n'utilise pas tous les éléments il y a aussi que malgré tout on est limité au nombre de valeurs prévues par le programmeur.

L'idéal serait de pouvoir faire ceci :

printf("Combien d'elements : ");

scanf("%d",&n);

int A[n] ;

Mais cela est impossible ! Lors de la phase de compilation, le programme n'est pas en cours d'exécution, par conséquent la valeur de  n n'est pas connue ! Le compilateur ne peut donc pas réserver la mémoire pour la variable A…

En utilisant la mémoire dynamiquement, il est possible de palier cette difficulté. L'idée est de demander de la mémoire lorsque l'on en a besoin : i.e. pendant que le programme s’exécute. L'affectation de cette mémoire (son type, son utilisation) est au choix du programmeur.

Pour demander de la mémoire, le langage C met à disposition plusieurs fonctions contenues dans la bibliothèque stdlib.h. Nous n'en utiliserons que deux : malloc et free.

Réserver de la mémoire : malloc.

La syntaxe est simple : on précise en paramètre de la fonction le nombre d'octets que l'on désire. Par exemple pour avoir 100 octets on écrit : malloc( 100 ).

Si l'on désire non pas un nombre d'octets, mais un nombre d'éléments identiques, par exemple 100 variables de type float, on utilise la fonction sizeof qui retourne la taille en octets d'une variable. On écrit alors pour 100 float : malloc( 100*sizeof( float ) )

Pour gérer cette mémoire on utilise un pointeur. Pour un tableau de 100 float le pointeur sera de type float *.

La fonction malloc retourne une adresse non typée, elle est de type void *, il faut donc transtyper l'adresse retournée par la fonction malloc avant de l'affecter à un pointeur de type float *.

Variables dynamique à une dimension.

Ainsi, si l'on veut réserver n entiers, n étant lu au clavier on écrira :

int i, n, *p;

printf("Entrez n : ");
scanf("%d",&n);
p = (
int * ) malloc( n*sizeof( int ) ) ;
...

A partir de ce point, p pointe vers une zone mémoire mise à disposition du programme, exactement comme dans le cas d'une variable dimensionnée. On peut, d'ailleurs, l'utiliser ainsi (et en L3 je le conseille fortement).

for ( i=0 ; i<10 ; i++ )
    p[ i ] = i;

Libération de la mémoire :

La mémoire dynamique est partagée entre tous les programmes en cours d'exécution sur l'ordinateur. Aussi, afin de ne pas perturber les autres applications, une fois que l'on n'a plus besoin d'une variable dynamique, on libère la zone de mémoire en question à l'aide de l'instruction free.

free ( p ) ; // free est une fonction décrite dans stdlib.h

Variables dynamique à deux dimensions.

Très souvent on parle de vecteur pour les variables à une dimension et de matrice pour les variables a deux dimensions, c'est la terminologie que nous utiliserons ici.

Il y a deux façons de voir une matrice : soit comme un seul bloc de mémoire dans lequel on s'arrange pour calculer une adresse unique pour chaque élément de la "matrice", soit comme une collection de vecteurs.

Dans la vision bloc de mémoire, une matrice est pointée par un pointeur p. Si la matrice comporte L lignes alors l'élément se trouvant ligne i et colonne j a pour adresse :

p +i*L + j // -> équivaut à &p[i][j] 

Cette méthode fonctionne, mais est peu lisible. 

Modéliser une matrice comme une collection de vecteurs peut être fait à l'aide de typedef (ainsi que c'est expliqué ici) une matrice est une collection de vecteurs : un vecteur de vecteurs.

Chaque vecteur est une variable dimensionnée à une dimension (c'est donc une adresse) qui peut être gérée par un pointeur. Par conséquent, gérer une variable à deux dimensions nécessite un vecteur capable de stocker des pointeurs. 

Ce vecteur de pointeur géré dynamiquement conduit à l'utilisation d'un pointeur de pointeur. On appelle ce type de variables un Handler. 

Par exemple pour un pointeur de pointeur vers des entiers on écrit :

int **p ;
// p est un pointeur vers des pointeur de type int *

-> Notez les deux étoiles : p est de type int **.

Il est possible de dimensionner un vecteur de n pointeurs vers de int à l'aide de malloc.

p = ( int ** ) malloc( n*sizeof( int * ) ) ;

Notez le transtypage int ** puisque c'est le type de p.

On veut un pointeur de pointeurs : les éléments vers lesquels p pointe sont des pointeurs. Le type de l'argument de l'instruction sizeof est par conséquent int *

Pour une variable de n lignes et m colonnes, on crée pour chaque élément pointé par p un vecteur de m éléments.

p[ 0 ] = ( int *) malloc( m*sizeof( int ) ) ;

p[
n-1 ] = ( int *) malloc( m*sizeof( int ) ) ;

Cela peut être géré par une boucle. Une fois la mémoire réservée on peut utiliser la variable à deux dimensions p : 

p[ i ][ j ] ou *( *( p + i ) + j ) 

la première syntaxe étant quand même plus claire.

Cela peut donner par exemple le programme suivant :

 1 /*
 2  * Exemple de tableau dynamique a deux dimensions 
 3 
*/
 4
 5 #include<stdio.h>
 6
#include<stdlib.h>
 7
 8 int main( void )
 9 {
 double **P ;
10    
int i,j;
11
12
13    P = ( double ** )malloc( 3*sizeof( double * ) ); // 3 lignes
14    for ( i = 0 ; i < 3 ; i++ )
15       P[i] = (
double * )malloc( 4*sizeof( double ) ) ;
16       // 4 éléments par ligne
17       for (i = 0 ; i < 3 ; i++ )
18           
for ( j = 0 ; j < 4 ; j++ )
19               P[i][j] = i+j;
20 
21 
22    for (i = 0 ; i < 3 ; i++ )
23    {    
for ( j = 0 ; j < 4 ; j++ )
24             printf(
"%lf\t", P[i][j] ) ); // %lf pour des double.
25         printf( "\n" );
26    }
27 
28    for ( i = 0 ; i < 3 ; i++ )
29       free( P[i] );
30 
31    free( P ) ;
32 
33    return 0;
34 }

Notez que dans ce cas précis on n'est absolument pas obligé d'avoir le même nombre d'éléments sur chaque ligne :

14 for ( i = 0 ; i < 3 ; i++ )
15     P[i] = ( double * )malloc( (3-i)*sizeof( double ) ) ;
16     // 3,2,1 elts par ligne

Dans ce cas, la ligne P[ 0 ] a 3 colonnes, P[ 1 ] en a 2, P[ 0 ], une. La mémoire dynamique offre donc beaucoup plus de souplesse que la mémoire statique.

L3 EEA & ISS 2019-21 / p. castelan