Security-X

Forum Security-X => Programmation => Cours => Discussion démarrée par: XmichouX le septembre 27, 2011, 15:41:25

Titre: Chaînes de caractères
Posté par: XmichouX le septembre 27, 2011, 15:41:25
Dans les sujets précédents, nous avons vu tout ce qui était relatif aux pointeurs et tableaux dynamiques/statiques. Je pars donc du principe que vous êtes familiarisés avec les pointeurs, tableaux & compagnie.

Dans ce chapitre, nous allons en voir une application concrète à travers la manipulation des chaînes de caractères.
En C, aucun type ne correspond à "chaîne de caractères". Il n'y a que des caractères de type char, stockés sur un octet (plus petite unité adressable dans la mémoire).

#include <stdio.h>

int main () {
char lettre = 'X';
printf ("La lettre est : %c\n", lettre);
return 1;
}

On notera que char lettre = 'X'; équivaut à  char lettre = 88;.
On sait bien-sûr que la mémoire ne stocke que des nombres (des 1 et des 0). Il a donc fallu pouvoir représenter les lettres par des nombres (voir ici (http://forum.security-x.fr/cours-17/(os)-bases-et-codification-de-caracteres-!-a-lire-en-premier-!/)). Le code ASCII correspondant à la lettre X est donc 88. En mémoire, il y a donc 88 (binaire), ce qui est interprété par 'X' lors de l'affichage.

a) Initialisation

Pour créer une chaîne de caractères, il faut donc ... créer un tableau de caractères. Ce n'est pas plus compliqué que ça.

#include <stdio.h>

int main () {
char maChaine[] = "ChaIne de caracteres";
printf ("Voici ma variable : %s\n", maChaine);
}

%s (%String) permet de spécifier qu'on passe en paramètre une chaîne de caractères. On passe à printf l'adresse du tableau, c'est à dire maChaine (et pas &maChaine). Nous avons vu dans le chapitre sur les pointeurs qu'un tableau se comportait comme un pointeur sur son premier élément.

Mais comment fait printf pour savoir jusqu'où lire, jusqu'où la chaîne s'arrête ? La réponse est simple.
Lorsqu'on crée une chaîne de caractères, un caractère null ('\0') ou String Zero est ajouté à la fin du mot.  Lorsque printf arrive à ce caractère, il sait alors qu'il doit s'arrêter de lire. '\0' est donc le caractère qui représente la fin d'une chaîne de caractères.
En effet, lorsqu'on affiche la taille du tableau statique avec l'opérateur sizeof, on obtient la taille de la chaîne + 1.

#include <stdio.h>

int main () {
char maChaine[] = "test";
printf ("Taille de ma chaine : %d\n", sizeof(maChaine));
}

Si l'on affiche nous-même le tableau de caractères, on aperçoit bien ce caractère nul.

#include <stdio.h>

int main () {
char maChaine[] = "test";
int i;
for (i = 0; i < 5; i++) {
printf ("%d ", maChaine[i]);
}
}

On affiche la valeur décimale de chaque élément, et non le caractère, sinon, on ne voit pas le caractère nul, puisqu'il est nul....

On obtient :

Citer
116 101 115 116 0

Ce qui correspond à : test\0.

Si on souhaite lire soi-même sa chaîne de caractères, il suffit d'utiliser une boucle comme celle-ci :

int cpt;
for (i = 0; maChaine[cpt] != '\0'; i++) {
printf ("%c", maChaine[cpt]);
}

Notons que si l'on met maChaine[cpt] != 0, cela revient exactement au même, puisque '\0' = 0.
Il est nécessaire de terminer la chaîne par ce caractère NULL, sinon on ne sait pas où la lecture s'arrêtera ce qui risque de provoquer une erreur de segmentation (accès à une zone mémoire ne nous appartenant pas).

La déclaration d'une chaîne de caractères comme tableau est moins pénible que pour un tableau de valeur, puisqu'on peut directement mettre toute la chaîne entre guillemets. Mais si on le souhaite, on peut aussi initialiser le tableau de caractères comme ça :

char chaine[] = {'t', 'e', 's', 't', '\0'};
Mais bon, je ne vois aucun intérêt à faire ça. De plus, on risque d'oublier le '\0' en faisant de cette manière.
Lorsqu'on déclare une chaîne de cette manière : char ch[] = "tata", le nombre d'éléments du tableau est calculé automatiquement par le compilateur, mais il reste possible de préciser la taille que l'on souhaite entre les crochets.
Notons que si l'on précise une taille inférieure à (taille du mot + 1), le mot sera coupé et le caractère nul ne sera pas inséré (peut dépendre du compilateur). Il est donc fortement déconseillé de préciser une taille lorsqu'on initialise une donnée.

On peut également initialiser une chaîne de cette manière :

const char *chaine = "0F56e2-frd5D3-7dv86e";
Dans ce cas, la chaîne sera stockée dans une zone en lecture seule (section .rodata/rdata). On ne peut donc pas la modifier.

Enfin, on peut créer une chaîne sans l'initialiser :

char chaine[50];
Cette fois-ci, il faut bien préciser la taille de la chaîne pour que le compilateur sache combien de mémoire allouer. Bien-sûr, on peut toujours utiliser l'allocation dynamique si besoin, comme pour un tableau classique. C'est exactement pareil.

La seule différence est le type du tableau, mais tout le fonctionnement est identique.

b) Manipulation de chaînes

Des fonctions en C existent déjà pour opérer de nombreuses opérations sur les chaînes de caractères : recherche, comparaison, concaténation et j'en passe.
Pour  cela, il faut inclure la librairie string.h.

#include <string.h>
Une des fonctions les plus utilisées est certainement strcmp (const char* ch1, const char* ch2) qui retourne 0 si les deux chaines sont égales. Je vous conseille de faire des tests de programme pour utiliser les fonctions principales de string.h (Google is your friend).

Il est aussi intéressant d'essayer de coder soi-même certaines de ces fonctions.

Par exemple, on peut imaginer une fonction de ce type pour comparer deux chaînes :

int comparerChaines (const char* ch1, const char* ch2) {
if (ch1 == NULL || ch2 == NULL)
return -2;
int i;
for (i = 0; *(ch1+i) != 0 || *(ch2+i) != 0; i++) {
if (*(ch1+i) != *(ch2+i))
return -1;
}
return 0;
}

Ou bien :

int comparerChaines (const char* ch1, const char* ch2) {
if (ch1 == NULL || ch2 == NULL)
return -2;

int i;
for (i = 0; *(ch1+i) != 0 && *(ch2+i) != 0 && *(ch1+i) == *(ch2+i); i++);
return *(ch1+i) - *(ch2+i);
}

Je vous laisse vous pencher sur ces deux fonctions, sachant que la deuxième est plus proche de la vraie fonction strcmp (car elle retourne la différence entre la première et la deuxième chaîne).

c) Saisie d'une chaîne

Trois flux (fichiers) sont ouverts à l'ouverture de votre programme : le flux d'entrée (stdin), le flux de sortie (stdout) et le flux d'erreur (stderr).
Ces trois flux sont tamponnés (ou bufferisés). Une mémoire tampon est un espace de mémoire servant à stocker des données temporairement. Voir ici (http://fr.wikipedia.org/wiki/M%C3%A9moire_tampon). Ces flux sont en C de type FILE *, FILE étant une structure de données.
Note : Chaque fichier ouvert par un processus se voit attribuer un descripteur de fichier (un entier).
Chacun d'eux est indexé dans une table relative au processus appelée table des descripteurs de fichier.
Les descripteurs de fichier correspondant aux flux stdin, stdout et stderr sont respectivement 0,1 et 2.

On les retrouve notamment en bash/batch.

Le flux d'entrée permet de manipuler des saisies de l'utilisateur, le flux de sortie permet d'afficher du texte, et le flux d'erreur permet de stocker tout ce qui est messages d'erreur.

Lorsque vous tapez une saisie dans un programme, le caractère de fin de ligne ('\n') permet de signaler la fin de la saisie.

De manière générale, une fonction de saisie émet un appel système qui génère une interruption. Le programme est suspendu, l'utilisateur peut pendant ce temps-là saisir des données qui sont copiées dans le tampon d'entrée standard. Lorsque l'utilisateur finit par taper entrée, un caractère de nouvelle ligne ('\n') est placé à la fin du tampon et l'exécution du programme reprend.
La fonction de saisie copie alors une partie ou toutes les données du tampon d'entrée dans l'endroit spécifié en ajoutant un caractère nul (fin de chaîne) et les supprime ensuite du tampon. Le caractère de fin de chaîne n'est pas présent dans le tampon, c'est la fonction de saisie qui le rajoute !

Scanf permet également de recevoir une chaîne de caractères :

char entree[50];

printf ("Tapez une chaine !\n");
scanf ("%s", entree);

printf ("La chaine tapee est %s\n", entree);

Scanf va lire dans le tampon d'entrée jusqu'à rencontrer un caractère "blanc" : espace, tabulation ou retour à la ligne. La fonction va copier dans la chaîne de destination tous les caractères lus jusqu'à ce caractère blanc, non compris, et va ajouter un caractère de fin de chaîne. Scanf va ensuite supprimer les données lues. La suite (à partir du caractère blanc) va donc rester dans le tampon.
Cependant, si l'on fait plusieurs scanf à la suite et qu'on affiche les résultats, on va voir que ces caractères blancs n'apparaissent nulle part.

Il semble donc que scanf, lors de sa lecture dans le tampon :
- Ignore ces caractères blancs (si existants) jusqu'à arriver à un caractère non blanc (lettres, chiffres, ...),
- Stoppe sa lecture avant le prochain caractère blanc. Notons qu'il y en a forcément un : la nouvelle ligne (Line Feed).

Les données restantes se trouvant dans le tampon d'entrée (si vous avez par exemple mis un espace), au prochain appel de scanf, celui-ci se servira directement dans le tampon et ne prendra pas de saisie de l'utilisateur !
En effet, chaque fonction de saisie (caractère, chaîne ...) demande une saisie de l'utilisateur uniquement si le tampon d'entrée est vide, à exception près pour scanf() si le tampon contient seulement des caractères "blancs".
Si vous avez bien suivi, quelque soit le scénario, il reste systématiquement (au moins) le '\n' dans le tampon d'entrée après appel à scanf().
De manière générale, il reste toujours au moins un caractère dans le tampon.

Pour éviter que scanf() ne relise dans le tampon pour une prochaine saisie sans même demander à l'utilisateur de taper quelque chose, on peut vider le tampon d'entrée soi-même (nous le verrons juste après avec fgets()).

Mais il y a un aspect encore plus grave. Si vous tapez plus de caractères que ce que vous avez alloué pour stocker la saisie et sans espace/tabulation, scanf va tout de même copier toutes les données du tampon d'entrée (jusqu'à la nouvelle ligne non comprise) dans votre tableau, ce qui va provoquer un débordement de mémoire/tampon (buffer overflow). Ceci peut faire planter votre programme et supprimer d'autres données en mémoire.

Scanf peut être utilisé correctement, mais c'est assez compliqué, voir ici (http://xrenault.developpez.com/tutoriels/c/scanf/).

Pour pallier cette difficulté, on peut utiliser la fonction fgets (localisé dans stdio.h) dont voici le prototype :

Citer
char *fgets( char *str, int num, FILE *stream );

Le stream (flux) à indiquer est stdin. Cette fonction permet de spécifier la taille de la chaîne qu'on souhaite copier ('\0' compris).

#include <stdio.h>

#define N 10

int main () {
char entree[N+1]; // +1 pour stocker le '\0'

printf ("Tapez une chaine !\n");
fgets (entree, N+1, stdin);
printf ("La chaine tapee est %s\n", entree);
}

À la différence de scanf(), fgets() va arrêter sa lecture au nombre de caractères spécifiés -1 ( puisqu'il a joute un '\0' à la fin de la chaîne), sauf s'il rencontre un caractère de nouvelle ligne avant, auquel cas, il s'arrête et copie le caractère de nouvelle ligne dans la chaîne de destination.
Dans le cas où fgets() rencontre le caractère de nouvelle ligne et que le nombre de caractères lu est inférieur ou égal au nombre de caractères spécifiés -1, le tampon d'entrée devient totalement vide : fgets() a lu l'intégralité du tampon.
Sinon, il reste des données dans le tampon.

Maintenant, ce qui est embêtant, c'est qu'on peut se retrouver avec le retour à la ligne dans la chaîne destination, ce qui gâche l'affichage.

Il y a deux possibilités :
- Ou bien le caractère '\n' est présent dans la chaîne : le tampon est donc vide.
- Ou bien il ne l'est pas : le tampon n'est donc pas vide.

On peut utiliser la fonction standard strchr dont voici le prototype :

Citer
char * strchr (char * str, int character);

La fonction recherche le caractère de type int (on peut passer un caractère, le cast est implicite) dans la chaîne pointée par le pointeur transmis et retourne un char * : pointeur sur le caractère trouvé ou NULL si non trouvé.
Une fois trouvé, il suffit de remplacer le retour à la ligne par un '\0' pour ne pas gêner l'affichage.

char* position = strchr (entree, '\n');
if (position != NULL)
*position = '\0';

Il faut tester votre pointeur, car si la saisie est de la longueur exacte requise ou trop longue, il n'y aura pas de '\n' dans votre chaîne (celui-ci se trouvant alors toujours dans le tampon) et le programme plantera, car vous tenterez d'accéder au contenu d'un pointeur nul ('\n' se trouvant dans le tampon).

Si l'utilisateur a tapé une saisie trop longue, le reste n'est pas supprimé du tampon d'entrée.
Or, si on se trouve dans ce cas-là, la prochaine saisie demandée ne se fera pas, et fgets() se servira directement dans les restes du tampon.

Il faut donc vider le tampon après chaque saisie :
- si la saisie est de longueur N (N+1-1) : seul le '\n' se trouve dans le tampon
- si la saisie est de longueur > N : une partie de la saisie et le '\n' se trouvent dans le tampon.

Sous Windows, il suffit de faire :

fflush(stdin);
Mais si on veut un code portable, on peut utiliser ceci :

int c = 0;
while (c != '\n' && c != EOF){
        c = getchar ();
}

Le principe, c'est donc de lire dans le tampon tant que le caractère est différent de la fin de ligne ou de la fin de fichier.
Si le tampon est vide, appeler cette fonction demander une saisie à l'utilisateur, ce que nous ne souhaitons pas. Mais puisqu'avec scanf(), le tampon n'est jamais vide après, on peut l'utiliser de manière systématique.

getchar() (= getc (stdin)) est une fonction qui demande la saisie d'un caractère. Si le buffer est vide, une saisie est requise, sinon elle se sert directement dans le buffer. En fait, on pourrait dire qu'une fonction de saisie se sert dans le tampon d'entrée : si celui-ci est vide, une interruption est réalisée permettant la saisie de la part de l'utilisateur, sinon, la fonction se sert directement dans ce dernier.
Ici, getchar() se lance jusqu'à rencontrer le caractère '\n' (fin de saisie) ou EOF (fin de fichier), car on peut aussi lire un fichier octet par octet, ligne par ligne ... Avec cette méthode, on vide donc le buffer si celui-ci n'est pas vide, sinon cette méthode demande une saisie (car le buffer est vide).
Si l'utilisateur a tapé une chaîne d'une taille inférieure à N, ce code va donc demander une saisie à l'utilisateur (le buffer étant vide), ce qui est gênant.
Il faut donc vider le buffer uniquement si la chaîne est trop longue ou égale à N (si le caractère '\n' n'est pas trouvé).

Voici un code général qui peut être utilisé :

#include <stdio.h>
#include <string.h>

#define LONGUEUR 10

void viderTampon();
void lireSaisie (char*, int);

int main () {
char entree[LONGUEUR+1];

printf ("Tapez une chaine de 10 caractères !\n");
lireSaisie(entree, sizeof(entree));

printf ("La chaine est : %s\n", entree);
}

void viderTampon() {
int c = 0;
    while (c != '\n' && c != EOF) {
        c = getchar();
    }
}

void lireSaisie (char* destination, int longueur) {
if (fgets (destination, longueur, stdin) != NULL) {
char *position = strchr (destination, '\n');
if (position != NULL)
*position = '\0';
else
viderTampon();
} else
viderTampon();
}

d) Arguments de la fonction Main

On peut enfin y venir. Le prototype de la fonction main est le suivant :

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

Note : *argv[] == **argv

Le premier argument est argc, qui indique le nombre d'arguments passés au programme. Enfin, le deuxième paramètre est un pointeur (rappel : une fonction prend en paramètre un pointeur, jamais un tableau) sur un tableau de pointeurs de caractères (voir dernier schéma du sujet sur les pointeurs/tableaux)

Chaque pointeur du tableau, pointé par argv, pointe sur un tableau de caractères, c'est-à-dire, une chaîne de caractères. Autrement dit, argv est un pointeur sur un tableau de chaînes de caractères.

Il est donc très facile de lire les arguments du Main :

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

int main (int argc, char **argv) {
int i;
if (argc > 1) {
for (i = 0; i < argc; i++) {
printf ("Argument %i : %s\n", i+1, *(argv+i));
}
} else {
printf ("Pas d'arguments !\n");
}
}

Le premier argument est toujours le nom de votre fichier (tapé dans la console).
Pour afficher chaque argument, j'ai mis %s -> *(argv+i), ce qui est égal à argv[ i]. Puisque pour lire une chaîne, il suffit de passer l'adresse de la chaîne à printf, il suffit de parcourir le tableau de pointeurs, et de fournir chaque pointeur (en augmentant i, pour accéder à l'argument qui suit).
Printf s'occupe ensuite d'accéder à chaque tableau de caractères dont l'adresse est contenu dans le pointeur et de le lire.
Titre: Re : Chaînes de caractères
Posté par: igor51 le juillet 18, 2012, 22:02:09
UP !