Les tableaux dynamiques et les hashs

Aujourd'hui, nous allons nous intéresser à deux types de données terriblement pratiques proposés par la Glib-2.0. Ce sont les tableaux dynamiques (empruntés à des langages «haut niveau») et les hashs (empruntés à Perl).

Les tableaux dynamiques

Les tableaux dynamiques de la Glib-2.0 allient le meilleur des tableaux statiques du C et des listes chaînées. La contrepartie est une faible surconsommation de la mémoire et une légère lourdeur syntaxique.

On utilisera un tableau dynamique lorsque le nombre d'éléments que ce tableau devra contenir n'est pas connu au moment de la création du tableau. Les éléments d'un tableau dynamique peuvent être de n'importe quel type mais doivent tous avoir la même taille.

On crée un tel tableau à l'aide de la fonction suivante:
GArray *g_array_new(gboolean fin_par_NULL,
gboolean mise_a_zero,
guint taille_element);
Le paramètre taille_element représente la taille d'un élément du tableau, en octets. fin_par_NULL indique si l'on souhaite que le tableau soit automatiquement terminé par un élément nul, c'est-à-dire une zone mémoire de taille_element octets, tous égaux à zéro ; dans ce cas, il faut mettre fin_par_NULL à TRUE. Dans le cas contraire (si fin_par_NULL vaut FALSE), le seul moyen de connaître la taille du tableau sera de consulter le champ len de la structure GArray. Le paramètre mise_a_zero, s'il vaut TRUE, indique que l'on souhaite que tous les éléments soient initialisés à zéro lors de leur création. La fonction g_array_new() renvoie un pointeur sur le tableau nouvellement créé. Ce tableau est initialement vide et aucune place mémoire n'est réservée pour y stocker des éléments. C'est généralement ce que l'on veut. Cependant, si, au moment de la création du tableau, on sait déjà qu'il va devoir contenir plusieurs milliers d'éléments, il est possible d'optimiser un peu les choses. En effet, plutôt que de partir d'un tableau vide et de le remplir par la suite en ajoutant les éléments un à un (ce qui est lent, puisque la Glib-2.0 devra réserver de la mémoire pour chacun des éléments introduits), il est possible de réserver de la mémoire pour un certain nombre d'éléments au moment de la création du tableau. Pour ceci, il suffit d'utiliser la fonction suivante:
GArray *g_array_sized_new(gboolean fin_par_NULL,
gboolean mise_a_zero,
guint taille_element,
guint pre_reserves);

Le paramètre supplémentaire, pre_reserves, indique la taille mémoire qui devra être pré-réservée (en nombre d'éléments). Attention ! Le tableau créé est toujours vide, simplement l'insertion des pre_reserves premiers éléments se fera très rapidement.

Justement, voyons maintenant comment insérer des éléments dans un tableau dynamique. Il existe six fonctions pour faire cela, suivant le nombre d'éléments que l'on souhaite insérer et l'endroit de cette insertion dans le tableau. Ainsi, pour insérer un élément à la fin du tableau, on utilisera la fonction suivante:
GArray *g_array_append_val(GArray *tableau, element);
Vous remarquerez que l'element ajouté n'a pas de type précis. La fonction g_array_append_val() (qui est en réalité une macro, mais ne chipotons pas...) s'occupe elle-même de vérifier et d'adapter la taille du tableau avant de placer le nouvel element à la fin du tableau.
Pour insérer plusieurs éléments à la fin d'un tableau dynamique, on utilisera:
GArray *g_array_append_vals(GArray *tableau,
gconstpointer premier_element,
guint nb_elements);
Le paramètre premier_element est l'adresse du premier élément que l'on souhaite ajouter et nb_elements est le nombre d'éléments que l'on souhaite ajouter en une seule opération. Ces éléments doivent être consécutifs en mémoire, c'est-à-dire que l'adresse du deuxième élément doit être égale à l'adresse du premier élément, plus la taille d'un élément. g_array_append_vals() est donc très pratique pour copier un tableau (que ce soit un tableau statique du C standard ou un tableau dynamique de la Glib-2.0) vers une partie d'un tableau dynamique.

De façon similaire, les deux fonctions suivantes permettent d'ajouter un ou plusieurs élément(s) au début d'un tableau. Les autres éléments, présents avant l'insertion, sont décalés vers la fin du tableau.
GArray *g_array_prepend_val(GArray *tableau, element);
GArray *g_array_prepend_vals(GArray *tableau,
gconstpointer premier_element,
guint nb_elements);

Les deux fonctions suivantes, quant à elles, permettent l'insertion d'un ou plusieurs élément(s) au milieu d'un tableau, à partir d'un index donné:
GArray *g_array_insert_val(GArray *tableau,
guint index,
element);
GArray *g_array_insert_vals(GArray *tableau,
guint index,
gconstpointer premier_element,
guint nb_elements);

Bien évidemment, il est possible d'accéder à chacun des éléments du tableau à l'aide de la macro suivante:
g_array_index(tableau, type, index)
tableau est notre tableau dynamique (de type GArray *), type est le type des éléments du tableau, et index, le numéro de l'élément auquel on souhaite accéder.

Lorsque l'on n'a plus besoin du tableau, on peut libérer la mémoire qu'il utilisait à l'aide de la fonction :
gchar *g_array_free(GArray *tableau,
gboolean libere_donnee);
Le paramètre libere_donnee, s'il vaut TRUE, indique que l'on souhaite vraiment que la place occupée par les éléments du tableau soit libérée de la mémoire, sinon g_array_free() renvoie un pointeur sur le premier élément de cet espace mémoire qui peut alors être considéré comme un tableau statique, qui devra être libéré par un g_free().

Il est possible de redimensionner un tableau dynamique à tout moment à l'aide de la fonction suivante:
GArray *g_array_set_size(GArray *tableau,
guint longueur);
Le paramètre longueur indique la nouvelle taille du tableau en nombre d'éléments. Si la nouvelle taille est inférieure à l'ancienne, les éléments supplémentaires sont perdus, sinon des nouveaux éléments (nuls si le tableau a été créé avec mise_a_zero à TRUE) sont créés.

Il est également possible de supprimer un élément du tableau, à partir de son index, avec la fonction:
GArray *g_array_remove_index(GArray *tableau,
guint index);
Tous les éléments suivants l'élément supprimé sont décalés vers le début du tableau afin de garder la continuité et l'ordre des éléments. Cela peut être un peu lent si le tableau est très grand. Donc, si l'ordre des éléments n'est pas important, il vaut mieux utiliser la fonction suivante:
GArray *g_array_remove_index_fast(GArray *tableau,
guint index);
Avec cette fonction, l'élément supprimé est remplacé par le dernier élément du tableau, ce qui permet de «boucher le trou» très rapidement, sans décaler les autres éléments.

Enfin, une fois un tableau dynamique constitué, il est possible d'en trier les éléments avec:
void g_array_sort(GArray *tableau,
GCompareFunc fonction_comparaison);
Le paramètre fonction_comparaison doit être une fonction permettant de classer deux éléments. Son prototype doit être le suivant:
gint fonction_comparaison(gconstpointer a, gconstpointer b);
a et b sont deux pointeurs sur des éléments du tableau. Cette fonction doit renvoyer une valeur positive si a doit être classé avant b et une valeur négative dans le cas contraire.

Il est même possible d'utiliser une fonction de comparaison plus complexe si l'on trie le tableau avec la fonction suivante:
void g_array_sort_with_data(GArray *tableau,
GCompareDataFunc fonction_comparaison,
gpointer donnee_sup);
La fonction de comparaison doit alors avoir le prototype suivant:
gint fonction_comparaison(gconstpointer a,
gconstpointer b,
gpointer donnee_sup);
Le comportement de g_array_sort_with_data() est similaire à celui de g_array_sort(). Simplement, le paramètre supplémentaire donnee_sup est transmis à la fonction de comparaison. Cela peut être utile, par exemple, pour utiliser la même fonction de comparaison pour trier un tableau par ordre croissant et décroissant, comme nous le verrons dans notre exemple.

Les tableaux de pointeurs

Comme les tableaux de pointeurs sont très utilisés en C, la Glib-2.0 prévoit une interface simplifiée pour les tableaux dynamiques de pointeurs.

Par exemple, un tel tableau est créé avec la fonction suivante:
GPtrArray *g_ptr_array_new(void);

ou la suivante si l'on souhaite pré-réserver de la mémoire pour des pointeurs:
GPtrArray *g_ptr_array_sized_new(guint pre_reserves);

On ajoute simplement un pointeur dans un tel tableau à l'aide de la fonction qui suit:
void g_ptr_array_add(GPtrArray *tableau,
gpointer pointeur);
Cette fonction ajoute le pointeur à la fin du tableau. Il n'y a pas de fonction particulière permettant d'ajouter un nouveau pointeur au début ou au milieu du tableau ou encore pour ajouter plusieurs pointeurs d'un seul coup.

Comme pour les tableaux «normaux», on peut accéder à un pointeur particulier du tableau à partir de son index à l'aide de la macro suivante:
gpointer g_ptr_array_index(GPtrArray *tableau,
guint index);

De même, on peut libérer la place occupée par un tableau de pointeurs ou simplement changer sa taille à l'aide, respectivement, des deux fonctions suivantes:
gpointer *g_ptr_array_free(GPtrArray *tableau,
gboolean libere_donnee);
void g_ptr_array_set_size(GPtrArray *tableau,
gint longueur);

Pour enlever un pointeur du tableau, en connaissant son index, on retrouve les deux fonctions suivantes:
gpointer g_ptr_array_remove_index(GPtrArray *tableau,
guint index);
gpointer g_ptr_array_remove_index_fast(GPtrArray *tableau,
guint index);

Mais il est également possible de retirer un pointeur directement d'après sa valeur, à l'aide des deux fonctions suivantes:
gboolean g_ptr_array_remove(GPtrArray *tableau,
gpointer pointeur);
gboolean g_ptr_array_remove_fast(GPtrArray *tableau,
gpointer pointeur);
Ces deux fonctions se comportent de la même façon que les deux précédentes, mais elles suppriment la première occurrence du pointeur dans le tableau. Elles renvoient TRUE si le pointeur a été trouvé et correctement supprimé et FALSE dans le cas contraire.

Enfin, il est également possible de trier les tableaux de pointeurs, comme les autres types de tableaux à l'aide des deux fonctions suivantes:
void g_ptr_array_sort(GPtrArray *tableau,
GCompareFunc fonction_comparaison);
void g_ptr_array_sort_with_data(GPtrArray *tableau,
GCompareDataFunc fonction_comparaison,
gpointer donnee_sup);

Il existe aussi une interface simplifiée pour les tableaux dynamiques de guint8 (d'octets non signés). Les fonctions utilisées pour cela ressemblent grandement à celles des tableaux de pointeurs. Simplement, elles commencent par «g_byte_array_». Je ne les détaillerai pas ici puisque les tableaux d'octets sont bien souvent mieux représentés par des chaînes de caractères ou par les GStrings de la Glib-2.0, que nous étudierons prochainement.

Voyons maintenant comment utiliser les fonctions des tableaux dynamiques dans un exemple:
/* tableau.c */

#include <stdio.h>
#include <glib.h>


/* fonction de comparaison, par ordre croissant si
* donnee_sup est non NULL, par ordre décroissant
* si donnee_sup est NULL */
gint fonction_comparaison(gint *a, gint *b,
gpointer donnee_sup)

if (GPOINTER_TO_INT(donnee_sup))
return *a - *b;
else
return *b - *a;


/* Affiche tous les éléments du tableau passé en paramètre */
void affiche_tableau(GArray *tableau)

gint i;

for (i = 0 ; g_array_index(tableau, gint, i) ; i++)
printf("%d ", g_array_index(tableau, gint, i));
printf("n");



int main(int argc, char *argv[])

GArray *tableau;
/* quelques valeurs pour remplir notre tableau */
gint valeurs[] = 5, 6, 7, 8, 9, 10 ;
gint i;
/* Affichage du numéro de version de la glib */
printf("glib version : %d.%d.%dn",
GLIB_MAJOR_VERSION,
GLIB_MINOR_VERSION,
GLIB_MICRO_VERSION);

/* Un tableau d'entiers */
tableau = g_array_new(TRUE, TRUE, sizeof(gint));

/* on ajoute quelques valeurs une par une */
for (i = 1 ; i<40 ; i+=4)
g_array_append_val(tableau, i);
/* puis on ajoute d'autres valeurs en plein milieu */
g_array_insert_vals(tableau, 5, valeurs, 6);

/* Premier affichage */
affiche_tableau(tableau);

/* on trie par ordre décroissant */
g_array_sort_with_data(tableau,
(GCompareDataFunc)fonction_comparaison,
GINT_TO_POINTER(0));

/* on affiche de nouveau */
affiche_tableau(tableau);

/* tri par ordre croissant */
g_array_sort_with_data(tableau,
(GCompareDataFunc)fonction_comparaison,
GINT_TO_POINTER(1));

/* nouvel affichage */
affiche_tableau(tableau);

/* on supprime trois éléments */
g_array_remove_index_fast(tableau, 5);
g_array_remove_index(tableau, 6);
g_array_remove_index_fast(tableau, 7);

/* dernier affichage */
affiche_tableau(tableau);

/* libération de la mémoire */
g_array_free(tableau, TRUE);

return 0;

Compilez ce programme à l'aide de l'habituel:
gcc tableau.c -o tableau `pkg-config --cflags --libs glib-2.0` -O2 -g -Wall
Regardez bien ce qu'il affiche, et essayez de bien comprendre la dernière ligne affichée.

Les hashs

Les hashs, ou «tables de hashage», sont des structures permettant de stocker des données sans ordre particulier. Chaque donnée est associée à une clef qui doit permettre de retrouver la donnée. Cette clef est souvent une chaîne de caractères, mais elle peut être à peu près n'importe quoi. Il suffit simplement qu'elle soit suffisamment discriminante pour que l'on puisse retrouver la donnée sans ambiguïté.

Les hashs stockent les données dans une structure à deux niveaux afin de retrouver très rapidement une donnée en fonction de sa clef. Tout d'abord, la clef est passée au travers d'une «fonction de hashage» qui renvoie un nombre entier. Ce nombre indique dans quelle partie du hash se trouve la donnée. La recherche de la donnée peut alors se faire uniquement dans cette partie du hash, qui est normalement aussi petite que possible.

Pour créer un hash (vide), on utilise la fonction suivante:
GHashTable *g_hash_table_new(GHashFunc fonction_hashage,
GEqualFunc fonction_comparaison);
Les deux paramètres de g_hash_table_new() sont des pointeurs sur des fonctions. Le premier, fonction_hashage est la fonction de hashage du hash que l'on veut créer. Son prototype doit être le suivant:
guint fonction_hashage(gconstpointer clef);
Elle doit donc prendre en paramètre un pointeur quelconque, qui est la clef associée à une donnée, et renvoyer un entier dépendant de cette clef. Il n'y a pas de règle absolue pour écrire cette fonction. L'idéal serait qu'elle répartisse au mieux l'ensemble des clefs que le hash contiendra. Ainsi, si le hash doit contenir une centaine de clefs et que la fonction de hashage ne renvoie que dix nombres différents, l'idéal serait que dix clefs renvoient chacun des nombres. Dans la pratique, cela est quasiment impossible si l'on ne connaît pas à l'avance l'ensemble de toutes les clefs. En fait, le plus simple (et le plus sage) est d'utiliser des fonctions existantes que l'on trouve dans la littérature appropriée, ou directement dans la Glib-2.0. Par exemple, si nos clefs sont des chaînes de caractères, on peut utiliser la fonction prédéfinie g_str_hash() comme fonction de hashage. De même, pour des clefs numériques, on peut utiliser g_int_hash().

Le second paramètre de g_hash_table_new(), fonction_comparaison, est une fonction dont le prototype doit être le suivant:
gboolean fonction_comparaison(gconstpointer a,
gconstpointer b);
Cette fonction doit renvoyer TRUE si les deux clefs a et b sont égales, et FALSE sinon. Ici aussi, le plus simple est d'utiliser les fonctions fournies par la Glib-2.0, à savoir g_str_equal() pour des clefs alphanumériques et g_int_equal() pour des clefs numériques.

Il existe une autre fonction permettant de créer une table de hashage:
GHashTable *g_hash_table_new_full(GHashFunc fonction_hashage,
GEqualFunc fonction_comparaison,
GDestroyNotify fonction_destruction_clef,
GDestroyNotify fonction_destruction_valeur);
Les deux premiers paramètres ont le même sens pour la fonction g_hash_table_new(). Les deux derniers sont des fonctions qui seront appelées pour éventuellement libérer la mémoire utilisée respectivement par une clef et sa donnée associée lors du retrait de la donnée du hash ou lors de la destruction hash. Souvent, on n'a besoin de libérer la mémoire que pour la donnée (ou que pour la clef) ; on peut alors mettre l'un de ces paramètres à NULL.

Une fois la table de hashage créée à l'aide des fonctions précédentes, on peut commencer à y ajouter des données, par exemple à l'aide de la fonction suivante:
void g_hash_table_insert(GHashTable *hash,
gpointer clef,
gpointer donnee);
Les paramètres clef et donnee peuvent être de nature quelconque, mais comme indiqué précédemment, clef est souvent une chaîne de caractères ou un entier. Si la clef existe déjà dans le hash, sa donnée associée est remplacée par la donnee passée en paramètre ; dans ce cas, si fonction_destruction_valeur() a été définie lors de la création de la table de hashage, l'ancienne donnée est libérée à l'aide de cette fonction, et si fonction_destruction_clef() a été définie, le paramètre clef est libéré par cette fonction.

L'autre fonction permettant d'ajouter des données dans un hash est la suivante:
void g_hash_table_replace(GHashTable *hash,
gpointer clef,
gpointer donnee);
Cette fonction est très semblable à g_hash_table_insert(), sauf lorsque la clef donnée en paramètre est déjà dans le hash. Dans ce cas là, c'est l'ancienne clef qui est libérée avec fonction_destruction_clef(), et la nouvelle clef remplace l'ancienne. En règle générale, g_hash_table_insert() et g_hash_table_replace() sont équivalentes. Pour comprendre la différence entre ces deux fonctions, il faut garder à l'esprit que les clefs sont comparées à l'aide de la fonction fonction_comparaison, qui n'est pas forcément une fonction d'égalité stricte.

Pour retirer une donnée et sa clef associée d'une table de hashage, il suffit d'utiliser la fonction suivante:
gboolean g_hash_table_remove(GHashTable *hash,
gconstpointer clef);
Si la clef est trouvée dans le hash, cette fonction renvoie TRUE et la clef et la donnée sont éventuellement libérées grâce aux fonctions fonction_destruction_clef() et fonction_destruction_valeur(). Si la clef n'est pas trouvée, cette fonction renvoie FALSE.

Il est également possible de réaliser la même opération sans que la mémoire utilisée par la clef et la donnée ne soit libérée. C'est le rôle de la fonction suivante:
gboolean g_hash_table_steal(GHashTable *hash,
gconstpointer clef);
Cette fonction peut être utile si, par exemple, la donnée et la clef sont utilisées ailleurs dans le programme.

Bien entendu, il est possible de retrouver les données que l'on a placées dans une table de hashage. La façon la plus simple est en général d'utiliser la fonction suivante:
gpointer g_hash_table_lookup(GHashTable *hash,
gconstpointer clef);
Cette fonction renvoie tout simplement la donnée associée à la clef ou NULL si la clef n'est pas trouvée dans le hash.

Cependant, NULL peut être une donnée valide. Il devient alors impossible de savoir si une donnée dont la valeur est NULL a été trouvée ou si NULL est renvoyé pour indiquer que la donnée n'a pas été trouvée. Il faut alors utiliser la fonction suivante:
gboolean g_hash_table_lookup_extended(GHashTable *hash,
gconstpointer clef,
gpointer *clef_originale,
gpointer *donnee);
Le paramètre clef indique toujours la clef de la donnée que l'on recherche, mais cette fois-ci, la donnée est renvoyée dans le pointeur donnee. De plus, la clef associée à la donnée est renvoyée dans le pointeur clef_originale, ce qui peut être utile dans le cas où les clefs sont identiques dans le sens de la fonction fonction_comparaison(), alors qu'elles sont physiquement différentes. Enfin, cette fonction renvoie TRUE si la clef a été trouvée et FALSE sinon.

Comme pour la plupart des structures de données proposées par la Glib-2.0, il est possible d'appeler une fonction ayant pour paramètre chacune des données présentes dans le hash. Cela se fait à l'aide de la fonction suivante:
void g_hash_table_foreach(GHashTable *hash,
GHFunc fonction,
gpointer donnee_sup);
La fonction fonction est appelée pour chacune des données du hash sans ordre particulier. Cette fonction doit avoir le prototype suivant:
void fonction(gpointer clef,
gpointer donnee,
gpointer donnee_sup);
Les paramètres clef et donnee sont les clefs et données successives du hash et donnee_sup est la donnée supplémentaire passée comme troisième paramètre à la fonction g_hash_table_foreach().

Dans le même ordre d'idées, la fonction suivante permet de supprimer toutes ou seulement une partie des données d'une table de hashage:
guint g_hash_table_foreach_remove(GHashTable *hash,
GHRFunc fonction,
gpointer donnee_sup);
Un peu comme g_hash_table_foreach(), cette fonction appelle la fonction fonction pour chacune des paires clef/donnée du hash. Cette fonction doit avoir le prototype suivant:
gboolean fonction(gpointer clef,
gpointer donnee,
gpointer donnee_sup);
Les paramètres de cette fonction ont le même sens que pour la fonction de g_hash_table_foreach(). Si la valeur de retour de cette fonction est TRUE, la donnée et sa clef sont retirées du hash et les éventuelles fonctions de libération de mémoire sont appelées. Si la fonction renvoie FALSE, rien ne se passe, et le processus continue avec la donnée suivante.

De même que pour g_hash_table_remove(), il est possible d'effectuer la même opération sans appeler les fonctions de libération de mémoire avec la fonction suivante:
guint g_hash_table_foreach_steal(GHashTable *hash,
GHRFunc fonction,
gpointer donnee_sup);

Toutefois, pour retirer complètement toutes les données d'un hash, libérer la mémoire des données et des clefs, et détruire la table de hashage en une seule fois, le plus efficace sera d'utiliser la fonction suivante:
void g_hash_table_destroy(GHashTable *hash);

Enfin, la fonction suivante renvoie le nombre de données contenues dans un hash:
guint g_hash_table_size(GHashTable *hash);

Voyons maintenant comment utiliser toutes ces fonctions dans un programme exemple:

/* hash.c */

#include <stdio.h>
#include <glib.h>

void affiche_hash(gchar *clef, gchar *donnee, gchar *format)

printf(format, clef, donnee);


int main(int argc, char *argv[])

GHashTable *hash;
/* Affichage du numéro de version de la glib */
printf("glib version : %d.%d.%dn",
GLIB_MAJOR_VERSION,
GLIB_MINOR_VERSION,
GLIB_MICRO_VERSION);

/* on crée un hash dont les clefs seront alphanumériques */
hash = g_hash_table_new_full((GHashFunc)g_str_hash,
(GEqualFunc)g_str_equal,
(GDestroyNotify)g_free,
(GDestroyNotify)g_free);

/* on insère quelques couples clef/donnée */
g_hash_table_insert(hash, g_strdup("Monday"), g_strdup("Lundi"));
g_hash_table_insert(hash, g_strdup("Tuesday"), g_strdup("Mardi"));
g_hash_table_insert(hash, g_strdup("Wednesday"), g_strdup("Mercredi"));
g_hash_table_insert(hash, g_strdup("Thursday"), g_strdup("Jeudi"));
g_hash_table_insert(hash, g_strdup("Friday"), g_strdup("Vendredi"));
g_hash_table_insert(hash, g_strdup("Saturday"), g_strdup("Samedi"));
g_hash_table_insert(hash, g_strdup("Sunday"), g_strdup("Dimanche"));

/* On affiche le contenu du hash (dans le désordre !) */
g_hash_table_foreach(hash, (GHFunc)affiche_hash, "%s : %sn");

printf("Nombre de jours : %dn", g_hash_table_size(hash));

/* On récupère le "mardi" */
printf("mardi : %sn", g_hash_table_lookup(hash, "Tuesday"));

/* Et on libère toute la mémoire utilisée */
g_hash_table_destroy(hash);

return 0;


Compilez ce programme avec :
gcc hash.c -o hash `pkg-config --cflags --libs glib-2.0` -O2 -g -Wall


La prochaine fois

La prochaine fois, nous ferons un tour du côté des fonctions utilitaires de la Glib-2.0.

<dindinx@wanadoo.fr>
Créateur et coordinateur de Giram.