Des pointeurs et des hommes (5)
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.