• Aucun résultat trouvé

Mesures, copies et comparaisons de chaînes

Dans le document Programmation système en C sous (Page 197-200)

int i;

fprintf (stdout, "bloc_1 = ");

for (i = 0; i < lg ; i ++)

fprintf (stdout, "%02d ", bloc_1 [i]);

fprintf (stdout, "\n");

fprintf (stdout, "bloc_2 = ");

for (i = 0; i < lg ; i ++)

fprintf (stdout, "%02d ", bloc_2 [i]);

fprintf (stdout, "\n");

fprintf (stdout, "memcmp (bloc_1, bloc_2, %d) = %d\n", lg, memcmp (bloc_1, bloc_2, lg));

fprintf (stdout, "\n");

}

int main (void) {

unsigned char bloc_1 [4] = { 0x01, 0x02, 0x03, 0x04 );

unsigned char bloc_2 [4] = { 0x01, 0x02, 0x08, 0x04 );

unsigned char bloc_3 [4] = { 0x01, 0x00, 0x03, 0x04 );

affiche_resultats (bloc_1, bloc_1, 4);

affiche_resultats (bloc_1, bloc_2, 4);

affiche_resultats (bloc_1, bloc_3, 4);

return (0);

}

ATTENTION memcmp( ) renvoie le signe du résultat de la soustraction des premiers octets différents, pas la valeur même de la différence.

$ ./exemple_memcmp bloc_1 = 01 02 03 04 bloc_2 = 01 02 03 04

memcmp (bloc_1, bloc_2 4) = 0 bloc_1 = 01 02 03 04

bloc_2 = 01 02 08 04

memcmp (bloc_1, bloc_2, 4) = -1 bloc_1 = 01 02 03 04

bloc_2 = 01 00 03 04

memcmp (bloc_1, bloc_2, 4) = 1

$

Il faut être très prudent avec les comparaisons de blocs de mémoire. En effet, on aurait tendance, à tort, à utiliser cette routine pour comparer des structures par exemple. Mais le compilateur insère fréquemment des octets de remplissage dans les structures ou dans les unions pour optimiser l'alignement des divers champs. Ces octets de remplissage n'ont pas de valeurs précisément définies et peuvent varier entre deux structures dont les membres sont par ailleurs égaux. On ne pourra donc pas utiliser memcmp( ) pour comparer autre chose que des données binaires «brutes» où chaque octet a une signification précise.

Comme toujours, il existe une version obsolète de cette routine provenant de BSD, bcmp(

), qui est similaire à memcmp( ) :

int bcmp (const void * bloc_1, const void * bloc_2, int taille)

Nous nous intéresserons aux routines permettant de rechercher des sous-blocs de données au sein d'une zone de mémoire dans la section sur les recherches au coeur d'une chaîne.

Mesures, copies et comparaisons de chaînes

Avec la bibliothèque C standard, les chaînes sont représentées par une table de caractères terminée par un caractère nul permettant d'en marquer la fin. Lorsqu'on déclare une chaîne ainsi

char * chaine = "Seize caractères";

le compilateur crée une zone de données statique initialisée, avec dix-sept caractères:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 S e i z e c a r a c t è r e s \0 La fonction strlen( ) renvoie la longueur d'une chaîne, sans compter le caractère « \0»

final. Cette fonction est déclarée dans <string.h> ainsi : size_t strlen (const char * chaine);

Pour poursuivre notre exemple, strlen("Seize aractères") renvoie 16 puisque le caractère nul de fin n'est pas compté. Comme les tableaux sont accessibles en C à partir de l'élément d'indice 0, on retrouve toujours :

chaine [strlen (chaine)] = '\0'.

Lorsqu'on alloue dynamiquement la mémoire pour une chaîne, on la stocke dans un pointeur de type char *. Il importe à ce moment de ne pas oublier la place nécessaire pour le caractère nul final. La fonction strcpy( ) permet de copier une chaîne dans une autre. Il faut avoir alloué suffisamment de place dans la chaîne réceptrice. Le prototype de strcpy( ) est le suivant :

char * strcpy (char * destination, const char * origine);

La bonne méthode pour allouer la mémoire indispensable à la réception d'une copie d'une chaîne est la suivante :

char * nouvelle;

if ((nouvelle = (char *) malloc (strlen (originale) + 1)) != NULL) strcpy (nouvelle, originale);

else

perror ("malloc");

Même une fonction aussi simple que strlen( ) peut parfois poser des problèmes. En effet, l'implémentation d'une telle routine nécessite de balayer toute la chaîne jusqu'à rencontrer un caractère nul, puis de renvoyer le nombre de caractères parcourus. La véritable implémentation est optimisée en assembleur dans la bibliothèque GlibC, mais on peut quand même en donner un équivalent fourni par Kernighan et Ritchie :

size_t

strlen (const char * s) {

char *p=s;

while (* p != '\0') p++;

return (p - s);

}

En fait, on renvoie ici la différence arithmétique entre le pointeur sur le caractère nul et le pointeur initial. Un problème grave peut se poser si on ne trouve pas de caractère nul.

Imaginons le traitement de chaînes relativement grandes provenant d'un fichier de texte par exemple. Malheureusement, l'utilisateur s'est trompé lorsqu'il nous a fourni le nom du fichier de texte à lire et il nous a transmis un fichier constitué de données binaires, une image graphique par exemple, ne contenant — comble de malheur — aucun zéro. Que se passe-t-il alors ? La fonction strlen( ) va

parcourir toute la zone de mémoire à la recherche d'un zéro et, n'en trouvant pas, va déborder sur la suite de la mémoire. Il est possible que la page suivante ne soit pas attribuée, et le programme va recevoir subitement un signal SIGSEGV qui va le tuer.

On peut avancer qu'il suffit, avant d'appeler strlen( ), d'écrire de force un caractère nul à une distance arbitraire, suffisamment grande pour correspondre à la plus grande chaîne qu'on puisse traiter. Dans la plupart des cas, c'est effectivement suffisant, mais nous n'avons pas toujours un accès en écriture sur la page mémoire de la chaîne à lire, la projection d'un fichier par exemple peut avoir uniquement l'autorisation PROT_READ.

Par ailleurs, la chaîne dont il est question peut aussi être un argument d'entrée d'une routine déclarée sous forme de const char *, donc non modifiable (même si le compilateur ne fournit qu'un avertissement et pas une erreur). La chaîne peut aussi être une constante statique dans un segment de données protégé en écriture. Par exemple, le code suivant déclenche une erreur de segmentation SIGSEGV :

void

modifie_chaine (char * chaine) {

chaine [0] = '10';

} int main (void) {

modifie_chaine ("abc");

return (0);

}

Pour éviter de déborder d'une chaîne en recherchant sa longueur, la bibliothèque GlibC offre une fonction strnlen( ) qui limite la portée de la recherche de la fin de chaîne. Elle prend tout simplement un argument supplémentaire pour indiquer la longueur maximale : size_t strnlen (const char chaine, size_t longueur_maxi);

Dans la documentation Gnu, cette fonction est indiquée comme étant équivalente à strlen (chaine) < longueur_maxi ? strlen (chaine) : longueur_maxi

mais elle n'est heureusement pas implémentée comme cela. Tout d'abord, il faudrait stocker dans une variable le retour de strlen( ) et ne pas la rappeler deux fois. Mais de surcroît, cette fonction n'arrangerait en rien notre problème si nous attendions le retour de strlen( ) pour limiter sa valeur. En réalité, strnlen( ) est implémentée en utilisant memchr( ), que nous verrons dans la prochaine section, et qui recherche la première occurrence du caractère nul jusqu'à une certaine distance limite.

Pour savoir si on a atteint ou non la longueur maximale, il suffit d'examiner le dernier caractère qui doit normalement être nul :

taille = strnlen (chaine, TAILLE_MAXI_SEGMENT);

if (chaine [taille] != '\0') {

/* prévenir l'utilisateur et recommencer la saisie */

fprintf (stderr, "La chaine fournie est trop longue \n");

return (ERREUR);

}

Lorsqu'on désire obtenir une copie d'une chaîne de caractères, il n'existe pas moins de huit variantes possibles dans la GlibC. La fonction la plus courante est bien entendu strcpy( ) déclarée ainsi :

char * strcpy (char * destination, const char * source);

Elle copie tous les caractères contenus de la chaîne source, y compris le 0 final, dans la chaîne destination, et renvoie un pointeur sur cette dernière. Aucune protection n'est fournie en ce qui concerne les risques de débordement de la chaîne source. Pour cela, il faut utiliser strncpy( ) :

char * strncpy (char * destination, const char * source, size_t taille_maxi);

Le comportement est le suivant :

• Si la chaîne source est plus courte que la taille maximale indiquée, elle est copiée dans la chaîne destination, puis l'espace restant de la chaîne destination est rempli avec des caractères nuls jusqu'à la taille maximale. Ceci sert lorsqu'on veut comparer des zones mémoire complètes, l'état des caractères inutilisés étant fixé.

• Si la chaîne source est plus longue que la taille maximale indiquée, on ne copie que cette dernière longueur. Aucun caractère nul n'est ajouté dans la chaîne destination.

Nous avons vu que cela peut être une situation à risque pour l'emploi ultérieur de strlen( );

Par exemple, strncpy( destination, "ABCDEFGH", 12) remplit la chaîne de destination ainsi:

A B C D E F G H \0 \0 \0 \0

Alors que strncpy(destination, "ABCDEFGH", 5) remplit la chaîne de destination ainsi

A B C D E sans qu'il y ait de caractère nul ajouté.

Le programmeur prudent pourra donc utiliser des routines du genre : char * destination;

size_t longueur;

if ((destination = (char *) malloc (LONGUEUR_MAXI_CHAINES + 1)) = NULL) { /* traitement d'erreur */

[...]

}

destination [LONGUEUR_MAXI_CHAINES] = '\0';

strncpy (destination, source, LONGUEUR_MAXI_CHAINES);

[...]

longueur = strnlen (destination, LONGUEUR_MAXI_CHAINES);

if (longueur == LONGUEUR_MAXI_CHAINES) { /* traitement d'erreur*/

[...]

}

Les fonctions stpcpy( ) et stpncpy( ) ont exactement la même syntaxe et la même signification que strcpy( ) et strncpy( ), mais elles renvoient un pointeur différent :

• stpcpy( ) renvoie un pointeur sur le caractère nul final de la chaîne destination.

• stpncpy( ) renvoie un pointeur sur le caractère situé dans la chaîne destination, immédiatement après le dernier caractère écrit, si la chaîne source est plus longue que la taille maximale indiquée.

• stpncpy( ) renvoie un pointeur sur le premier caractère nul écrit dans la chaîne destination si la chaîne source est plus courte que la taille maximale. La chaîne destination est dans ce cas complétée avec des zéros jusqu'à la longueur maximale, mais on renvoie un pointeur sur le premier caractère nul ajouté.

Ces fonctions sont disponibles dans la GlibC en tant qu'extensions Gnu, même s'il s'agit probablement de routines provenant du monde Dos. En renvoyant un pointeur sur la fin de la chaîne, elles permettent de faire des concaténations successives. Nous allons créer une fonction prenant en argument une chaîne destination, une longueur maximale, suivies d'un nombre quelconque de chaînes et d'un pointeur NULL final. Cette routine va concaténer toutes les chaînes transmises dans la chaîne destination, en surveillant qu'il n'y ait pas de déborde-ment, caractère nul final compris.

exemple_stpncpy.c : #define _GNU_SOURCE #include <stdarg.h>

#include <stdio.h>

#include <string.h>

void

concatenation (char * destination, size_t taille_maxi, ...) {

va_list arguments;

char * source;

char * retour;

size_t taille_chaine;

retour = destination;

taille_chaine = 0;

va_start (arguments, taille_maxi);

while (1) {

source = va_arg (arguments, char *);

if (source == NULL)

/* fin des arguments */

break;

retour = stpncpy (retour, source, taille_maxi - taille_chaine);

taille_chaine = retour - destination;

if (taille_chaine == taille_maxi) {

/* Ecraser le dernier caractère par un zéro */

retour --, * retour = '\0';

break;

}

}

va_end (arguments):

} int main (void) {

char chaine [20];

concatenation (chaine, 20, "123", "456", "7890", "1234", NULL);

fprintf (stdout, "%s\n", chaine);

concatenation (chaine, 20, "1234567890", "1234567890", "123", NULL);

fprintf (stdout, "%s\n", chaine);

return (0);

}

L'exécution nous permet de vérifier que notre concaténation fonctionne bien, tout en ne dépassant jamais la longueur maximale indiquée :

$ ./exemple_stpncpy 12345678901234 1234567890123456789

$

Il existe deux fonctions, strdup( ) et strndup( ), particulièrement pratiques, car elles assurent l'allocation mémoire nécessaire pour stocker la chaîne destination. Elles sont déclarées ainsi :

char * strdup (const char *chaine);

char * strndup (const char * chaine, size_t longueur);

Elles renvoient toutes deux un pointeur sur la copie nouvellement allouée de la chaîne, ou NULL en cas d'échec dans malloc( ). La fonction strndup( ) ne copie au plus que la longueur indiquée, y compris le caractère nul final. La chaîne renvoyée se termine donc toujours par un zéro. Bien entendu, il faut libérer les chaînes renvoyées en invoquant free( ) une fois qu'on a fini de les utiliser.

Deux fonctions supplémentaires existent en tant qu'extensions Gnu : strdupa( ) et strndupa( ). Elles se présentent exactement comme strdup( ) et strndup( ), mais la copie de chaîne est allouée dans la pile en utilisant la fonction alloca( ), et non malloc(

). Il ne faut donc pas tenter d'appeler free( ) avec le pointeur renvoyé, car l'espace occupé par la chaîne sera automatiquement libéré au retour de la fonction (ou lors d'un saut non local longjmp). Pour ces deux fonctions, on prendra donc les précautions qui s'imposent vis-à-vis de l'emploi de variables allouées dynamiquement dans la pile avec alloca( ), comme nous l'avons vu dans le chapitre traitant de la gestion de l'espace mémoire du processus.

Aucune des fonctions de copie que nous avons examinées ne permet de copier des chaînes se recouvrant partiellement. Il est pourtant utile de déplacer des parties d'une chaîne à l'intérieur d'elle-même. Cela permet par exemple d'éliminer les espaces en début de ligne.

La seule fonction acceptable pour cela est memmove( ), mais elle nous oblige à rechercher nous-même la fin de la chaîne. Nous verrons comment implémenter de manière assez performante une élimination des blancs en début et fin de chaîne, dans la prochaine section, car nous utiliserons les fonctions strchr( ) et strspn( ) que nous analyserons alors.

Il est également fréquent d'avoir besoin d'ajouter une portion de chaîne à la fin d'une autre. Par exemple, on prépare phrase par phrase un texte en fonction de divers paramètres, puis le texte est affiché ou transmis à une routine de sauvegarde, de présentation dans un composant d'interface graphique, etc. Plusieurs méthodes sont possibles pour concaténer des chaînes, à commencer par strcpy( ) qui utilise un pointeur sur la fin de la chaîne destination. Nous avons également écrit une routine de ce type dans l'exemple précédent, avec stpncpy( ). Il est toujours envisageable d'employer sprintf( ). La fonction dont on se sert le plus couramment est pourtant strcat( ), ainsi que son acolyte strncat( ) qui permet par précaution de limiter la longueur de la chaîne réceptrice. Leurs prototypes sont déclarés ainsi :

char * strcat (char * destination, const char * a_ajouter);

char * strncat (char * destination, const char * a_ajouter, size_t taille);

La taille indiquée dans l'appel de strnat( ) est celle de la portion qui peut être ajoutée à la chaîne destination. Avant l'appel de strnat( ), la chaîne destination doit donc disposer d'une taille totale valant au moins strlen(destination ) + taille + 1 (pour le caractère nul final). L'exemple suivant va nous permettre d'utiliser strnat( ) pour concaténer les arguments d'appel de la fonction, tout en limitant la taille totale. La valeur 20 est choisie arbitrairement pour imposer une limite volontairement basse.

exemple_strncat.c

#include <stdio.h>

#include <string.h>

#define LG_MAXI 32 /* 20 + 12, cf. plus bas */

int

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

int i;

int taille;

char chaine [LG_MAXI + 1];

strcpy (chaine, "Arguments : "); /* déjà 12 caractères */

for (i = 1; i < argc; i++) { taille = strlen (chaine);

strncat (chaine, argv [i], LG_MAXI - taille);

}

fprintf (stdout, "%s\n", chaine);

return (0);

}

Lors de l'exécution, la chaîne est bien limitée à 32 caractères (20 pour les arguments, et 12 pour l'affichage de «Arguments : »), auxquels s'ajoute un caractère nul que nous avons compté lors de la déclaration de la chaîne.

$ ./exemple_strncat 12345 678 90 1 Arguments : 12345678901

$ ./exemple_strncat 123456789 01 23 45678 Arguments : 123456789012345678

$ ./exemple_strncat 123456789 01 23 45678 90123 Arguments : 12345678901234567890

$

Dans le document Programmation système en C sous (Page 197-200)