IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Concepts avancés du langage C

Concepts avancés du langage C


précédentsommairesuivant

VI. Bibliothèque standard

VI-A. Fonctions à arguments en nombre variable

Le langage C permet de créer des fonctions dont le nombre d'arguments peut varier d'un appel à un autre c'est-à-dire n'est pas fixe. Nous avons déjà eu jusqu'ici l'occasion d'utiliser de telles fonctions comme par exemple printf et scanf. Maintenant nous allons voir comment en créer.

Une fonction qui accepte un nombre variable d'arguments doit au moins posséder un argument fixe, en effet même si la fonction peut recevoir un nombre variable d'arguments, elle doit quand même savoir à chaque appel combien d'arguments ont été passés. Par exemple, les fonctions printf et scanf comptent le nombre de spécificateurs de formats pour connaître le nombre d'arguments passés.

La bibliothèque standard du langage C est fournie avec des macros permettant d'accéder aux arguments « variables » de manière portable. Ces macros sont définies dans le fichier d'entête stdarg.h. Le principe est simple : à l'intérieur de la fonction, on crée une liste d'arguments (de type va_list). On initialise cette liste en pointant sur le dernier argument fixe à l'aide de la macro va_start. Puis on parcoure la liste à l'aide de la macro va_arg. Lorsqu'on en a terminé avec la liste, il faut appeler va_end.

Nous allons écrire une fonction qui calcule la moyenne des nombres passés en arguments. Le premier argument servira à préciser le nombre d'arguments variables passés.

 
Sélectionnez
#include <stdio.h>
#include <stdarg.h>

double moyenne(int N, ...);

int main()
{
    printf("moyenne = %f\n", moyenne(4, 10.0, 20.0, 30.0, 40.0));

    return 0;
}

double moyenne(int N, ...)
{
    double moy = 0.0;

    if (N > 0)
    {
        va_list args;
        int i;
        double xi, somme = 0.0;

        va_start(args, N);
        for(i = 0; i < N; i++)
        {
            xi = va_arg(args, double);
            somme += xi;
        }

        va_end(args);
        moy = somme / N;
    }

    return moy;
}

VI-B. Les paramètres régionaux

Les paramètres régionaux sont un ensemble de paramètres qui peuvent changer selon la localisation tels que le symbole décimal, le symbole monétaire, le symbole de groupement des chiffres, etc. Les fonctions, macros et types ayant un lien avec ces paramètres sont déclarés/définis dans le fichier locale.h. La fonction setlocale :

 
Sélectionnez
char * setlocale(int category, const char * locale);

permet de modifier les paramètres régionaux utilisés par le programme. Cette fonction retourne NULL en cas d'erreur. Par défaut, tous les programmes utilisent les paramètres standard du langage C, c'est-à-dire comme si :

 
Sélectionnez
setlocale(LC_ALL, "C");

avait été appelé.

Les valeurs utilisables pour l'argument category sont LC_ALL, LC_COLLATE, LC_CTYPE, LC_MONETARY, LC_NUMERIC, LC_TIME, plus d'autres éventuelles macros définies par l'implémentation. Pour l'argument locale, "C" indique d'utiliser les paramètres standard du C et "" les paramètres de l'environnement d'exécution. L'implémentation peut évidemment autoriser d'autres valeurs. La fonction localeconv :

 
Sélectionnez
struct lconv * localeconv();

permet de récupérer les valeurs des paramètres courants. Par exemple :

 
Sélectionnez
#include <stdio.h>
#include <locale.h>

int main()
{
    struct lconv * plconv;

    if (setlocale(LC_MONETARY, "") != NULL)
    {
        plconv = localeconv();
        printf("Le symbole monetaire est : %s\n", plconv->currency_symbol);
    }

    return 0;
}

VI-C. « Type » des caractères

Dans le fichier ctype.h sont déclarées des fonctions (ou macros) facilitant la manipulation des caractères. Ces fonctions (macros) attendent en argument un int puisqu'elles doivent aussi tester EOF. Leur comportement, à l'exception de isdigit et isxdigit, est dépendant de la valeur du jeu de paramètres LC_CTYPE.

Les fonctions suivantes permettent de tester le « type » d'un caractère et retourne VRAI (c'est-à-dire une valeur non nulle) lorsque le prédicat est vrai.

 
Sélectionnez
int isalpha(int c); /* c est une letrre */
int isdigit(int c); /* c est un chiffre decimal */
int isxdigit(int c); /* c est un chiffre hexadecimal */
int isalnum(int c); /* c est une lettre ou un chiffre decimal */
int islower(int c); /* c est une lettre minuscule */
int isupper(int c); /* c est une lettre majudcule */
int isspace(int c); /* c est un caractere d'espacement */
int ispunct(int c); /* c est une ponctuation */
int iscntrl(int c); /* c est un caractere de controle (DEL, ECHAP, ...) */
int isprint(int c); /* c est un caractere imprimable */
int isgraph(int c); /* isprint(c) && !isspace(c) */

Les fonctions toupper et tolower permettent de convertir, lorsque cela est possible, une lettre en majuscule respectivement en minuscule. Par exemple toupper('a') retourne 'A'.

VI-D. Les caractères larges

A la différence de la plupart des langages de haut niveau, le langage C traite les caractères comme de simples entiers (en fait un caractère est un entier) exactement de la même manière que le fait le processeur lui-même. Le type char n'est pas le seul type utilisé pour manipuler des caractères. Un caractère large (wide character) est un caractère plus large, en termes de taille, qu'un simple char, c'est-à-dire pouvant prendre plus de valeurs que ces derniers.

Une constante littérale de type caractère large s'écrit comme une constante de type char mais avec un préfixe L, par exemple : L'x'. De même, une constante chaîne constituée de caractères larges s'écrit comme une simple constante chaîne constituée de char mais avec le préfixe L, par exemple : L"Bonjour". Le standard définit un caractère large comme étant de type wchar_t, défini dans le fichier stddef.h.

Les fonctions impliquant des caractères ou chaînes de caractères en argument ou en valeur de retour ont donc toujours, à quelques très rares exceptions près, leur équivalent en wchar_t principalement déclarées dans wchar.h et wctype.h. Ainsi on a putchar/putwchar, getchar/getwchar, isalpha/iswalpha, toupper/towupper, strlen/wcslen, strcpy/wcscpy, printf/wprintf, scanf/wscanf, fgets/fgetws, etc. Par exemple :

 
Sélectionnez
#include <stdio.h>
#include <wchar.h>

int main()
{
    char c = 'a', *s = "Bonjour";
    wchar_t wc = 'a', *ws = L"Bonjour";

    printf("Voici un caractere simple : ");
    putchar(c);

    printf("\nEt voici un caractere large : ");
    putwchar(wc);

    printf("\n\n");

    printf("Voici une chaine de caracteres simples : %s\n", s);
    wprintf(L"Et voici une chaine de caracteres larges : %s\n", ws);

    return 0;
}

Un caractère multioctet (multibyte character) est un élément du jeu de caractères du système hôte ou, en termes plus simples, un ... caractère ! (Mais non nécessairement représentable par un char). On l'appelle multioctet puisqu'il peut être codé sur un (pourquoi pas ?) ou plusieurs octets. Un caractère multioctet peut donc être représenté par un char ou un tableau de char selon le cas. De même, une chaîne de caractères multioctets peut être représentée à l'aide d'un simple tableau de char. L'interprétation d'une chaîne de caractères multioctets est donc bien évidemment très dépendante du codage utilisé et ne rentre pas dans le cadre de ce tutoriel.

D'autre part, en fait, le type char a plutôt été désigné non seulement pour représenter un « octet » mais aussi n'importe caractère supporté par le langage lui-même (l'ensemble de ces caractères formant ce qu'on appelle le jeu de caractères de base), qui doit absolument tenir sur un octet, et non n'importe quel caractère du jeu de caractères du système hôte, qui est un sur ensemble du jeu de caractères de base, cette tâche ayant été plutôt confiée aux caractères larges qui sont donc tenus, si vous avez bien suivi, de pouvoir représenter n'importe quel caractère multioctet (... à leur manière !), alors qu'on recourrait éventuellement à un tableau si l'on utilisait le type char.

Il existe plusieurs fonctions dédiées à la manipulation des caractères multioctets, déclarées dans stdlib.h. Leur comportement dépend bien évidemment de la valeur du jeu de paramètres LC_CTYPE. En particulier, la fonction mbstowcs (multibyte string to wide char string) :

 
Sélectionnez
size_t mbstowcs(wchar_t * pwcs, const char * s, size_t nwcs);

permet de convertir une chaîne de caractères multioctets en une chaîne équivalente de caractères larges. La fonction wcstombs réalise l'inverse. Par exemple :

 
Sélectionnez
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define COUNT(t) (sizeof(t) / sizeof(t[0]))

int main()
{
    char s[] = "Hello, world!";
    wchar_t wcs[20];

    mbstowcs(wcs, s, COUNT(wcs));
    wprintf(L"%s\n", wcs);

    return 0;
}

VI-E. Recherche dans une chaîne

La fonction strchr

 
Sélectionnez
char * strchr(const char * s, int c);

retourne l'adresse de la première occurrence du caractère c dans la chaîne s ou NULL si aucune occurrence n'a été trouvée. La fonction strrchr (r pour reverse) fait aussi la même chose sauf qu'elle effectue la recherche dans le sens inverse (c'est-à-dire à partir de la fin).

La fonction strpbrk

 
Sélectionnez
char * strpbrk(const char * s, const char * cs);

retourne l'adresse de la première occurrence de n'importe caractère de la chaîne cs dans la chaîne s ou NULL si aucune occurrence n'a été trouvée.

La fonction strstr

 
Sélectionnez
char * strstr(const char * s, const char * substr);

retourne l'adresse de la première occurrence de la chaîne substr dans la chaîne s ou NULL si aucune occurrence n'a été trouvée.

La fonction strtok

 
Sélectionnez
char * strtok(const char * s, const char * separators);

utilisée en boucle, permet d'obtenir toutes les sous chaînes (tokens) de la chaîne s délimitées par n'importe quel des caractères de la chaîne separators. Si s n'est pas NULL, elle est copiée dans une variable locale statique à la fonction. Celle-ci retournera ensuite la première sous chaîne trouvée (en la terminant par '\0'). Si s est NULL et que la fonction a déjà été correctement initialisée, la sous chaîne suivante est retournée, sinon, le comportement est indéfini. Lorsque aucune sous chaîne n'a pu être trouvée, NULL est retournée. Bien entendu, on peut aussi changer la liste des séparateurs à chaque appel de la fonction. Par exemple :

 
Sélectionnez
#include <stdio.h>
#include <string.h>

int main()
{
    char lpBuffer[100], separators[] = " \t.,!?\n", *p;

    printf("Entrez une chaine de caracteres : ");
    fgets(lpBuffer, sizeof(lpBuffer), stdin);

    printf("\n");

    p = strtok(lpBuffer, separators);
    while(p != NULL)
    {
        printf("%s\n", p);
        p = strtok(NULL, separators);
    }

    return 0;
}

VI-F. Conversion des chaînes

Ces fonctions permettent de constituer une valeur numérique (entier ou flottant) à partir d'une chaîne de caractères. Elles sont déclarées dans le fichier stdlib.h. On distingue deux grandes familles de fonctions de conversion à savoir les fonctions atoxx et celle des fonctions strtoxx.

 
Sélectionnez
int atoi(const char * s);
long atol(const char * s);
double atof(const char * s);

Par exemple :

 
Sélectionnez
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int mille = atoi("1000");

    printf("mille = %d\n", mille);

    return 0;
}

Mais que ce passerait-il si on passait par exemple "vive le vent" au lieu de "1000" à la fonction ? Et bien, c'est justement là le problème avec les fonctions atoxx : elles ne sont pas capables de signaler qu'on leur a passé n'importe quoi. Dans un programme solide, il faut utiliser les fonctions strtoxx puis tester si la conversion s'est bien déroulée. Une conversion s'est bien déroulée si elle s'est arrêtée à la rencontre du caractère de fin de chaîne.

 
Sélectionnez
long strtol(const char * s, char * *p_stop, int base);
unsigned long strtoul(const char * s, char * *p_stop, int base);
double strtod(const char * s, char * *p_stop);

Un petit exemple :

 
Sélectionnez
#include <stdio.h>
#include <stdlib.h>

int main()
{
    char s[] = "FF";
    char * p_stop;
    int n, base = 16;

    n = (int)strtol(s, &p_stop, base);
    
    if (*p_stop == '\0') /* OK */
        printf("%s (base %d) = %d (base 10).\n", s, base, n);
    else
        printf("Erreur : %c n'est pas un chiffre.\n", *p_stop);

    return 0;
}

Pour créer des applications robustes, on n'effectue jamais de saisie à l'aide de scanf (et moins encore à l'aide de gets). On saisit tout d'abord une chaîne (à l'aide de fgets) puis éventuellement la convertir dans le type qu'on veut avoir.

VI-G. Fonctions de manipulation de chaîne « généralisées »

L'intérêt des fonctions memxxx (memcpy, memcmp, etc.) est de permettre d'utiliser n'importe quel pointeur sur un objet dans une fonction assez voisine des fonctions orientées chaînes telles que strcpy, strcmp, etc. Elles sont également déclarées dans le fichier string.h.

 
Sélectionnez
void * memcpy(void * p_dest, const void * p_src, size_t n_bytes_to_be_copied);
void * memmove(void * p_dest, const void * p_src, size_t n_bytes_to_be_moved);
void * memcmp(void * p_dest, const void * p_src, size_t n_bytes_to_be_compared);
void * memchr(const void * p_buffer, int c, size_t n_buffer_length);
void * memset(void * p_buffer, int c, size_t n_bytes_to_be_set);

La différence entre memcpy et memmove vient du fait que cette dernière peut être utilisée même si les deux buffers (src et dest) se chevauchent. Elle est généralement utilisée pour « glisser » (ou « déplacer ») des données d'où son nom. Par exemple, le programme suivant copie chaque octet du tableau t2 vers le tableau t1 (t1 doit donc être au moins aussi grand que t2).

 
Sélectionnez
#include <stdio.h>
#include <string.h>
int main()
{
    int t1[10], t2[] = {0, 10, 20, 30, 40};
    int i;

    memcpy(t1, t2, sizeof(t2));

    for(i = 0; i < 5; i++)
        printf("t1[%d] = %d\n", i, t1[i]);

    return 0;
}

VI-H. La date et l'heure

VI-H-1. Généralités

Les fonctions, macros et types de données standard permettant de travailler avec la date et l'heure sont déclarées/définies dans le fichier time.h.

VI-H-2. Connaître la date et l'heure courantes

La fonction permettant de connaître la date et l'heure calendaires (un "timestamp"), est time :

 
Sélectionnez
time_t time(time_t * p_time);

En cas d'erreur, (time_t)(-1) est retourné.

On peut donc récupérer la date et l'heure soit en stockant la valeur de retour de time dans une variable de type time_t, soit en passant en argument l'adresse d'une variable du même type. On peut même encore compliquer ...

Il est également important d'avoir à l'esprit que ce n'est qu'une simple et petite fonction, pas un sorcier ni un devin, et qu'elle ne peut donc retourner que ce que le système lui a fournit et rien de plus.

D'autre part, une valeur de type time_t c'est quand même difficile à décoder (en fait pas si difficile que ça mais bref ...) pour un humain. L'idéal pour nous c'est d'avoir une structure avec des champs comme heure, minute, seconde, etc. Heureusement pour nous, cette structure existe. Il s'agit de la structure struct tm. Oui, mais comme vous le savez bien, il y a d'un côté le temps local (en anglais Local Time Zone ou tout simplement local time) qui dépend de la localisation et de l'autre le Temps Universel (ou UTC pour Coordinated Universal Time). La structure struct tm est définie dans time.h comme suit :

 
Sélectionnez
struct tm {
    int tm_sec;
    int tm_min;
    int tm_hour; /* 0 - 23 */
    int tm_mday; /* 1 - 31 */
    int tm_mon; /* 0 - 11 (0 = janvier)*/
    int tm_year; /* annees ecoulees depuis 1900 */
    int tm_wday; /* 0 - 6 (0 = dimanche) */
    int tm_yday; /* 0 - 365 (0 = 1er janvier) */
    int tm_isdst; /* DST (Daylight-Saving Time) flag */
};

Le DST flag indique si les informations contenues dans la structure tiennent compte de l'heure d'été ou non. Sa valeur est 1 pour oui, 0 pour non et un nombre négatif si l'information n'est pas disponible.

La fonction localtime permet de convertir le temps calendaire (time_t) en temps local. La fonction gmtime (de Greenwich Meridian Time, l'ancienne appellation du Temps Universel) permet quant à elle de convertir le temps calendaire en Temps universel. Ces deux fonctions ont le même prototype.

 
Sélectionnez
struct tm * localtime(time_t * p_time);
struct tm * gmtime(time_t * p_time);

En cas d'erreur, NULL est retourné.

Voici un exemple de programme qui affiche la date et l'heure courantes :

 
Sélectionnez
#include <stdio.h>
#include <time.h>

int main()
{
    time_t t;
    struct tm * ti;
    const char * days[] = {"dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"};
    const char * months[] = {"janvier", "fevrier", "mars", "avril", "mai", "juin", "juillet", "aout", "septembre", "octobre", "novembre", "decembre"};

    t = time(NULL);
    ti = localtime(&t);
    printf("On est le %s %d %s %d\n", days[ti->tm_wday], ti->tm_mday, months[ti->tm_mon], ti->tm_year + 1900);
    printf("Et il est %02d:%02d:%02d\n", ti->tm_hour, ti->tm_min, ti->tm_sec);

    return 0;
}

On a également la fonction asctime qui permet de convertir facilement un temps représenté par une structure (struct tm) en une chaîne de caractères et la fonction ctime qui reçoit quant à elle un argument de type time_t. Dommage que le format utilisé soit anglo-saxon.

 
Sélectionnez
char * asctime(const struct tm * pTime);
char * ctime(const time_t * p_time);

Par exemple :

 
Sélectionnez
#include <stdio.h>
#include <time.h>

int main()
{
    time_t t = time(NULL);

    printf("Date is : %s\n", ctime(&t));

    return 0;
}

Il y a également la fonction strftime assez complexe pour être détaillée ici.

VI-H-3. Les conversions date <-> timestamp

Nous avons vu tout à l'heure les fonctions localtime et gmtime qui permettent de connaître les informations de date et heure à partir d'un temps calendaire. Il existe une fonction mktime qui réalise l'inverse, c'est-à-dire qui calcule le temps calendaire en fonction d'informations de date et heure locales. Lors d'un appel à mktime, les champs tm_wday et tm_yday sont ignorés. En effet, ils peuvent être calculés à partir des valeurs des autres champs.

Voici un exemple de programme qui demande à l'utilisateur d'entrer une date et qui affiche le jour de la semaine correspondant à cette date.

 
Sélectionnez
#include <stdio.h>
#include <time.h>
#include <string.h>

int main()
{
    struct tm ti;

    printf("Entrez une date au format jj/mm/aaaa : ");
    memset(&ti, 0, sizeof(ti));
    if (scanf("%d/%d/%d", &ti.tm_mday, &ti.tm_mon, &ti.tm_year) != 3)
        printf("Mauvais format.\n");
    else
    {
        time_t t;
        const char * days_tab[] = {"dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"};

        ti.tm_mon -= 1;
        ti.tm_year -= 1900;

        t = mktime(&ti);
        memcpy(&ti, localtime(&t), sizeof(struct tm));

        printf("C'est un %s.\n", days_tab[ti.tm_wday]);
    }

    return 0;
}

VI-H-4. Connaître le temps processeur d'un programme

Une autre fonction bien pratique de time.h est la fonction clock qui retourne le temps processeur utilisé par le programme depuis le début de son exécution (le point marquant le "début de l'exécution" d'un programme est défini par l'implémentation et non par le C), exprimé en tops, un top étant égal à 1/CLOCKS_PER_SEC seconde (il s'agit là d'une division de réels). Le prototype de cette fonction est :

 
Sélectionnez
clock_t clock(void);

clock_t étant évidemment un type arithmétique représentant le temps d'utilisation du processeur d'un programme depuis le début de son exécution. Une des multiples applications de cette fonction (mais qui n'est pas la meilleure ...) est de ne quitter une boucle (qui ne fait rien de spécial ...) que lorsqu'un certain temps s'est écoulé, cela permet de suspendre le programme pendant un intervalle de temps donné. Par exemple :

 
Sélectionnez
#include <stdio.h>
#include <time.h>

void wait(int ms);

int main()
{
    printf("Suspension du programme pendant 5s.\n");
    wait(5000);
    printf("Termine.\n");

    return 0;
}

void wait(int ms)
{
    clock_t start = clock();
    while ((clock() - start) * 1000 < ms * CLOCKS_PER_SEC) ;
}

VI-I. Les nombres pseudo-aléatoires

La bibliothèque standard du langage C est fournie avec des fonctions permettant de générer des nombres de façon pseudo aléatoire. Le générateur de ces nombres doit tout d'abord être initialisée avec une graine qui doit elle-même être plus ou moins aléatoire. Si la graine est constante, la même série de nombres va être générée chaque fois que le programme sera exécuté car le générateur sera alors toujours initialisé avec la même valeur. En ce qui nous concerne, pas la peine d'aller chercher sur Mars, le temps est une valeur qui change « tout le temps » ! Elle peut donc convenir dans la plupart des cas pour initialiser le générateur.

Les fonctions liées aux nombres aléatoires, déclarées dans stdlib.h sont :

 
Sélectionnez
void srand(unsigned int seed);
int rand(void);

La fonction srand permet d'initialiser le générateur. La fonction rand retourne à chaque appel un nombre pseudo aléatoire compris entre 0 et RAND_MAX (qui doit être au moins égal à 32767 quel que soit l'implémentation). Par exemple :

 
Sélectionnez
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main()
{
    int i;

    srand((unsigned)time(NULL));

    printf("Voici dix nombres tires au hasard compris entre 0 et 99.\n");

    for(i = 0; i < 10; i++)
        printf("%d\t", rand() % 100);

    return 0;
}

précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2008 Melem. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.