Des pointeurs et des hommes (5)

Previous

Vous commencez j’en suis sûr, à mieux comprendre les tableaux. Aujourd’hui, je vous propose de découvrir les tableaux dynamiques, et au travers d’eux des fonctions qui vont vous accompagner durant toute votre apprentissage du C : malloc et free.

Vous êtes prêts ? Prenez un café, installez vous confortablement, c’est reparti pour un nouveau chapitre !

Tableaux statiques et tableaux dynamiques

Jusqu’ici, nous avons utilisé la syntaxe suivante pour déclarer un tableau :

<type> tableau[taille] ;

Par exemple :

int tab[42] ;

Ce tableau est ce que l’on appelle un tableau statique, car sa taille est censée être connue au moment de la compilation. Dans les premières versions du C, il était ainsi impossible d’utiliser une variable pour donner la taille d’un tableau.

Ceci va changer avec la norme C99, qui va introduire cette possibilité avec les “variable length arrays”, ou VLA. Cette norme vise à rendre plus souple l’utilisation des tableaux en autorisant des codes ayant cette forme :

#include <stdio.h>

int 
main() {
	int A ;
	scanf("%d", &A) ;
	int tab[A] ;
	return 0 ;
}

Comme vous pouvez le voir, on utilise ici une variable pour spécifier la taille du tableau !

Néanmoins, si cela est parfois pratique, il existe quelques contraintes lors de l’utilisation d’un VLA. Par exemple, il n’est pas possible de déclarer un tel tableau en tant que variable globale, ni avec le mot clé “static” car le standard est clair à ce sujet :

6.7.6.2 Array declarators

	[...]

    2 If an identifier is declared as having a variably modified type, 
	it shall be an ordinary identifier (as defined in 6.2.3), have no linkage, 
	and have either block scope or function prototype scope. If an identifier 
	is declared to be an object with static or thread storage duration, it 
	shall not have a variable length array type.

Ceci n’est pas très surprenant : cela vient tout simplement du fait que les tailles des sections data et bss, où sont stockées les variables globales, doivent être connues dès le chargement de l’exécutable.

De plus, utiliser un VLA provoque une complexification non négligeable de l’assembleur généré par notre compilateur :

$ gcc -g -o test main.c

$ objdump -S test
[...]
int main() {
    1145:       55                      push   %rbp
    1146:       48 89 e5                mov    %rsp,%rbp
    1149:       41 57                   push   %r15
    114b:       41 56                   push   %r14
    114d:       41 55                   push   %r13
    114f:       41 54                   push   %r12
    1151:       53                      push   %rbx
    1152:       48 83 ec 28             sub    $0x28,%rsp
    1156:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
    115d:       00 00
    115f:       48 89 45 c8             mov    %rax,-0x38(%rbp)
    1163:       31 c0                   xor    %eax,%eax
    1165:       48 89 e0                mov    %rsp,%rax
    1168:       48 89 c3                mov    %rax,%rbx
    int A ;
        scanf("%d", &A) ;
    116b:       48 8d 45 b4             lea    -0x4c(%rbp),%rax
    116f:       48 89 c6                mov    %rax,%rsi
    1172:       48 8d 3d 8b 0e 00 00    lea    0xe8b(%rip),%rdi        # 2004 <_IO_stdin_used+0x4>
    1179:       b8 00 00 00 00          mov    $0x0,%eax
    117e:       e8 bd fe ff ff          call   1040 <__isoc99_scanf@plt>
        int tab[A] ;
    1183:       8b 45 b4                mov    -0x4c(%rbp),%eax
    1186:       48 63 d0                movslq %eax,%rdx
    1189:       48 83 ea 01             sub    $0x1,%rdx
    118d:       48 89 55 b8             mov    %rdx,-0x48(%rbp)
    1191:       48 63 d0                movslq %eax,%rdx
    1194:       49 89 d6                mov    %rdx,%r14
    1197:       41 bf 00 00 00 00       mov    $0x0,%r15d
    119d:       48 63 d0                movslq %eax,%rdx
    11a0:       49 89 d4                mov    %rdx,%r12
    11a3:       41 bd 00 00 00 00       mov    $0x0,%r13d
    11a9:       48 98                   cltq
    11ab:       48 8d 14 85 00 00 00    lea    0x0(,%rax,4),%rdx
    11b2:       00
    11b3:       b8 10 00 00 00          mov    $0x10,%eax
    11b8:       48 83 e8 01             sub    $0x1,%rax
    11bc:       48 01 d0                add    %rdx,%rax
    11bf:       b9 10 00 00 00          mov    $0x10,%ecx
    11c4:       ba 00 00 00 00          mov    $0x0,%edx
    11c9:       48 f7 f1                div    %rcx
    11cc:       48 6b c0 10             imul   $0x10,%rax,%rax
    11d0:       48 29 c4                sub    %rax,%rsp
    11d3:       48 89 e0                mov    %rsp,%rax
    11d6:       48 83 c0 03             add    $0x3,%rax
    11da:       48 c1 e8 02             shr    $0x2,%rax
    11de:       48 c1 e0 02             shl    $0x2,%rax
    11e2:       48 89 45 c0             mov    %rax,-0x40(%rbp)

    return 0 ;
    11e6:       b8 00 00 00 00          mov    $0x0,%eax
    11eb:       48 89 dc                mov    %rbx,%rsp

}
[...]

Mais alors ? Faut-il les éviter ? Sommes nous condamnés à utiliser des tableaux de taille fixe ?

Certains (et mes profs en faisaient partie) vous diront que oui. Mais si vous avez l’habitude de me lire, vous savez sûrement que je n’aime pas les avis trop tranchés sur les choses.

Les VLA ont une utilité, mais il faut simplement avoir conscience de cette complexité et se demander avant de les utiliser si elle est un problème ou pas dans votre code. Et la question se résume bien souvent à “En ai-je vraiment besoin ici ?”. A vous de répondre à cette question au cas par cas … :)

Une autre façon pour le programmeur de faire des tableaux de taille variable, c’est d’utiliser la fonction “malloc”.

Les tableaux dynamiques : malloc et free

L’utilisation de malloc nécessite l’inclusion dans votre code de stdlib.h : elle fait partie de la librairie standard du C. Vous la retrouverez donc sur tous les OS, même les plus inavouables. Malloc permet justement de faire une requête à cet OS, et lui demander de nous réserver une certaine quantité de mémoire.

Ceci tombe à point car nous avons vu, dans le chapitre précédant de cette série, qu’un tableau n’était justement que cela : un espace mémoire dans lequel les valeurs sont stockées les unes derrière les autres. Voici un exemple simple de création et d’utilisation d’un tel tableau :

#include <stdio.h>
#include <stdlib.h>

int 
main() 
{
	int* tab ;
	int i ;

	/* Allocation du tableau */

	tab = malloc( 10*sizeof(int) ) ;

	/* Utilisation */

	for (i=0; i<10; i++) {
		tab[i] = 0 ;
	}

	/* Liberation memoire */

	free(tab) ;

	return 0;
}

La fonction malloc permet de réserver durant l’exécution du programme de la mémoire pour stocker nos valeurs. C’est ce qui explique d’ailleurs l’expression “tableaux dynamiques” qui est généralement utilisée pour désigner ce type de tableaux.

Première chose à observer ici : ce que nous demandons, nous devons le rendre. C’est pourquoi tout espace mémoire réservé à l’aide de “malloc” doit être libéré lorsque nous n’en avons plus besoin à l’aide de la fonction “free”.

Regardons maintenant en détail l’utilisation de malloc :

tab = malloc( 10*sizeof(int) ) ;

Malloc ne prend qu’un seul argument : la quantité de mémoire que nous désirons obtenir. Puisque nous souhaitons créer un tableau de 10 entiers, nous avons utilisé l’expression “10*sizeof(int)” soit, si vous préférez, “10 fois la taille en octets d’un entier”.

Comment ? Pourquoi ? Oui, un entier faisant 32 bits sur notre PC, nous aurions pu demander 40 Octets directement. Cela aurait parfaitement fonctionné et le compilo n’aurait même pas râlé. Mais nous sommes entre gens de bonne société ici, et nous respectons la portabilité ! Rien, dans la norme du C, ne vous dit que l’entier fait 4 octets … Et qui sait ? Je suis peut-être en train de recompiler votre code pour le faire tourner sur mon gaufrier hein ? :)

Regardons maintenant la valeur renvoyée par malloc. Voici ce que dit le man :

void *malloc(size_t size);

Comme vous pouvez le constater, malloc renvoie un pointeur non typé, ou void*. Cela est normal : nous avons demandé une certaine quantité de mémoire, l’OS nous l’a affecté. Nous ne lui avons pas dit ce que nous comptions en faire. Cette adresse, nous souhaitons la stocker dans une variable, tab, de type int* pour pouvoir accéder aux entiers les uns après les autres.

Pour faire les choses bien, nous pourrions faire un cast : nous disons au compilateur que oui, le résultat de malloc, nous souhaitons bien le transformer en int* avant de le mettre dans notre variable. En pratique, ce n’est pas nécessaire car le void* sera automatiquement promu dans le bon type et cela pourrait masquer des erreurs ( par exemple si vous oubliez d’inclure stdlib.h ). On ne fait donc généralement pas de cast pour malloc.

Comme toutes les requêtes, notre demande de mémoire peut être refusée. C’est rare, mais parfois, malloc peut juste échouer. Là encore, la page de man nous indique la voie :

VALEUR RENVOYÉE
       Les fonctions malloc() et calloc() renvoient un pointeur vers la mémoire  allouée,  qui
       est  correctement  alignée  pour  n'importe quel type interne. Si elles échouent, elles
       renvoient NULL.

On ajoute donc habituellement un petit test pour vérifier la valeur de retour de malloc, et agir le cas échéant :

#include <stdio.h>
#include <stdlib.h>

int 
main() 
{
	int* tab ;
	int i ;

	tab = malloc( 10*sizeof(int) ) ;
	if ( tab == NULL ) {
		fprintf(stderr, "Malloc failed\n") ;
		return 1 ;
	}

	for (i=0; i<10; i++) {
		tab[i] = 0 ;
	}

	free(tab) ;

	return 0;
}

Leave free ( or die hard … )

Pour libérer la mémoire réservée par votre programme à l’aide de malloc, on utilise la fonction “free”. Son usage est relativement simple : il suffit de lui passer l’adresse que vous avez obtenu de malloc, et celle-ci s’occupera de la libération de la mémoire.

Il est à noter que la fonction free ne renvoie pas de valeur ni d’erreur : Le standard du C ( version C99 ici ) est très claire à ce sujet :

The free function causes the space pointed to by ptr to be deallocated, that is,
made available for further allocation. If ptr is a null pointer, no action 
occurs. Otherwise, if the argument does not match a pointer earlier returned by
the calloc, malloc, or realloc function, or if the space has been deallocated by
a call to free or realloc, the behavior is undefined.

Là où il faut être prudent, c’est que la valeur contenue dans la variable tab n’a pas été altérée par la fonction free. Notre tab contient toujours l’adresse, mais nous ne sommes plus censés l’utiliser puisque nous avons libéré cet espace mémoire. Pour éviter cette erreur très classique, il est recommandé de mettre ce pointeur à la valeur NULL afin d’éviter tout usage postérieur.

Et si je ne libère pas la mémoire ? C’est grave ?

Et bien … Pas tant que ça en pratique ! Car bien heureusement, l’OS veille en général sur vous ! Il s’occupera, à la mort de votre process, de s’assurer que la mémoire qui vous a été affectée est bien libérée. C’est en tout cas le cas pour tous les OS modernes que je connais !

Ceci étant dit, ce n’est pas parce qu’oublier un free ne déclenchera pas un massacre sanglant de chatons qu’il ne faut pas y prêter attention dans vos codes. Tout d’abord, cela reste en général un indicateur d’un code de bonne qualité. Ensuite, et c’est probablement plus important, vous risquez d’utiliser un malloc au sein d’une boucle, ce qui réservera de plus en plus de mémoire, jusqu’à souvent dépasser les ressources disponibles sur votre machine.

Bref, ne pas libérer la mémoire, c’est pas bien !

Conclusion

Chaleur oblige -Ce sont les joies du blogging au mois d’août- je vais en rester là pour aujourd’hui. Nous verrons dans le prochain article en quoi les tableaux dynamiques sont différents des tableaux statiques.

A bientôt !

Rancune.