Aller au contenu principal

Programmation en C : cours, livres et liens divers.

Voici des liens menant vers des livres et cours de programmation en C en français :

Vous trouverez les livres suivants à la bibliothèque de la faculté :

  • Programmer en langage C, cours et exercices corrigés. Claude Delannoy. Cote : 000-005.13-35. Livre intéressant, qui présente le langage de façon pédagogique.
  • Exercices en langage C. Claude Delannoy. Cote : 000-005.13-15.
  • Méthodologie de la programmation en C Norme C99-API POSIX. Achille Braquelaire. Cote : 000-005-63. Présente toute la norme ISO de 1999 ainsi que l’API POSIX (interface avec les systèmes de type Unix). Très bon livre, très complet, mais ne convient pas aux débutants en programmation.
  • Initiation à l’algorithmique et à la programmation en C : Cours avec 129 exercices corrigés. Rémy Malgouyres, Rita Zrour, Fabien Feschet. Cote : 000-005-119.

Informatique2 ST

Voici les séries de TD de ce deuxième semestre :

et voilà les séries de TP :

Je vous rappelle que si vous avez besoin de mon aide concernant les cours, les TD ou les TP d’Informatique2, je suis disponible tous les mardis de 11h30 à 13h au département d’Informatique au 2e étage (demandez M. Brikci).

Informatique1 L1-ST

Voici en fichiers PDF les diapositives des premiers cours et le TD1 du module  Informatique1 pour les étudiants en 1re année de licence ST :

Remarque : L’examen portera sur les 4 premiers cours et tout le TD1, c’est-à-dire le contenu des six fichiers ci-dessus.

Erreurs de programmation en C courantes

Cet article est une traduction du texte Common C Programming Errors par Dr Paul Carter.
Dr Carter m’a gentiment autorisé à faire cette traduction et la publier ici ; qu’il en soit remercié.
Merci de me signaler toute erreur ou remarque dans les commentaires.


Table des matières

1. Introduction

2. Erreurs de débutants
2.1 Oublier de mettre un break dans une instruction switch
2.2 Utiliser = au lieu de ==
2.3 Erreurs de scanf()
2.3.1 Oublier de mettre un & à un argument
2.3.2 Utiliser le mauvais format pour l’opérande
2.4 Taille des tableaux
2.5 Division entière
2.6 Erreurs de boucles
2.7 Ne pas utiliser les prototypes
2.8 Ne pas initialiser les pointeurs

3. Erreurs de chaînes de caractères
3.1 Confondre les constantes caractère et chaîne de caractères
3.2 Comparer des chaînes avec ==
3.3 Chaînes sans zéro terminal
3.4 Ne pas laisser la place pour le zéro terminal

4. Erreurs d’Entrées/Sorties
4.1 Utilisation incorrecte de fgetc(), etc.
4.2 Utilisation incorrecte de feof()
4.3 Laisser des caractères dans le tampon d’entrée
4.4 Utiliser la fonction gets()

5. Remerciements


1. Introduction

Ce document dresse une liste des erreurs de programmation en C courantes que l’auteur voit maintes et maintes fois. Des solutions à ces erreurs y sont aussi présentées.

Une autre excellente ressource est la C FAQ (NdT : ou sa version française). Gimpel Software a aussi une liste de bugs difficiles à détecter en C/C++ qui peut être utile.

2. Erreurs de débutants

Ce sont des erreurs que les étudiants débutant en C font souvent. Toutefois, les professionnels les font encore parfois eux aussi !

2.1 Oublier de mettre un break dans une instruction switch

Rappelez-vous que le C ne sort pas d’une instruction switch quand un case est rencontré. Par exemple :

    int x = 2;
    switch(x) {
    case 2:
      printf("Deux\n");
    case 3:
      printf("Trois\n");
    }

affiche en sortie :

    Deux
    Trois

Mettez un break pour sortir du switch :

    int x = 2;
    switch(x) {
    case 2:
      printf("Deux\n");
      break;
    case 3:
      printf("Trois\n");
      break;   /* pas nécessaire, mais utile si des case supplémentaires sont ajoutés plus tard */
    }

2.2 Utiliser = au lieu de ==

L’opérateur = du C est utilisé exclusivement pour l’affectation et renvoie la valeur affectée. L’opérateur == est utilisé exclusivement pour la comparaison et renvoie une valeur entière (0 pour faux, 1 pour vrai). À cause de ces valeurs de retour, le compilateur C ne signale souvent pas d’erreur quand = est utilisé alors que l’on voulait en réalité un ==. Par exemple :

    int x = 5;
    if ( x = 6 )
      printf("x égale 6\n");

Ce code affiche en sortie x égale 6 ! Pourquoi ? L’affectation dans le if met 6 dans x et renvoie la valeur 6 au if. Puisque 6 n’est pas 0, cela est interprété comme vrai.

Une façon de faire en sorte que le compilateur trouve ce genre d’erreurs est de mettre toute constante (ou toute expression r-value) du côté gauche. Alors, si un = est utilisé, ce sera une erreur :

if ( 6 = x)

2.3 Erreurs de scanf()

Il y a deux types d’erreurs de scanf() courantes :

2.3.1 Oublier de mettre un & à un argument

scanf() doit avoir l’adresse de la variable pour stocker l’entrée dedans. Cela veut dire que souvent l’opérateur d’adresse & (« et commercial » ou « esperluette ») est nécessaire pour calculer les adresses. Voici un exemple :

    int x;
    char * st = malloc(31);

    scanf("%d", &x);   /* & nécessaire pour passer l'adresse à scanf() */
    scanf("%30s", st); /* PAS DE & ici, st pointe lui-même sur la variable ! */

La dernière ligne ci-dessus montre que parfois il est correct de ne pas mettre de & !

2.3.2 Utiliser le mauvais format pour l’opérande

Les compilateurs C ne vérifient pas que le format correct est utilisé pour les arguments d’un appel à scanf(). Les erreurs les plus courantes sont d’utiliser le format %f pour des double (qui doivent utiliser le format %lf) et de confondre %c et %s pour les caractères et les chaînes.

2.4 Taille des tableaux

Les tableaux en C commencent toujours à l’indice 0. Ce qui veut dire qu’un tableau de 10 entiers défini par :

    int a[10];

a des indices valides de 0 à 9, pas 10 ! Il est très courant que les étudiants dépassent d’une case la fin d’un tableau. Cela peut conduire à un comportement imprévisible du programme.

2.5 Division entière

Contrairement au Pascal, le C utilise l’opérateur / pour les divisions réelle et entière. Il est important de comprendre comment le C détermine laquelle il fera. Si les deux opérandes sont d’un type entier, la division entière est utilisée, sinon la division réelle est utilisée. Par exemple :

double moitie = 1/2;

Ce code met dans moitie la valeur 0 pas 0.5 ! Pourquoi ? Parce que 1 et 2 sont des constantes entières. Pour corriger cela, changez au moins l’une d’elle en une constante réelle.

double moitie = 1.0/2;

Si les deux opérandes sont des variables entières et que vous voulez une division réelle, convertissez le type d’une des variables en double (ou float) par un transtypage (cast).

    int x = 5, y = 2;
    double d = ((double) x)/y;

2.6 Erreurs de boucles

En C, une boucle répète l’instruction qui vient juste après l’instruction de boucle. Le code suivant :

    int x = 5;
    while( x > 0 );
      x--;

est une boucle infinie. Pourquoi ? Le point-virgule après le while définit l’instruction à répéter comme étant l’instruction nulle (qui ne fait rien). Enlevez le point-virgule et la boucle fonctionnera comme prévu.

Une autre erreur courante avec les boucles est de faire une itération de trop ou une de moins. Vérifiez les conditions des boucles attentivement !

2.7 Ne pas utiliser les prototypes

Les prototypes indiquent au compilateur des caractéristiques importantes d’une fonction : le type de retour et les paramètres de la fonction. Si aucun prototype n’est donné, le compilateur suppose que la fonction retourne un int et peut prendre n’importe quel nombre de paramètres de n’importe quel type.

Une raison importante pour laquelle utiliser des prototypes est de permettre au compilateur de vérifier s’il y a des erreurs dans les listes d’arguments des appels de fonctions. Cependant, un prototype doit obligatoirement être utilisé si la fonction ne retourne pas un int. Par exemple, la fonction sqrt() retourne un double, pas un int. Le code suivant :

    double x = sqrt(2);

ne fonctionnera pas correctement si un prototype:

    double sqrt(double);

n’apparaît pas au-dessus de lui. Pourquoi ? Sans prototype, le compilateur C suppose que sqrt() retourne un int. Puisque la valeur retournée est stockée dans une variable de type double, le compilateur insère du code pour convertir la valeur en double. Cette conversion n’est pas requise et résultera en une valeur erronée.

La solution à ce problème est d’inclure le fichier d’en-tête C correct qui contient le prototype de sqrt(), à savoir math.h. Pour les fonctions que vous écrivez, vous devez soit placer le prototype en haut du fichier source soit créer un fichier d’en-tête et l’inclure.

2.8 Ne pas initialiser les pointeurs

Chaque fois que vous utilisez un pointeur, vous devriez être capable de répondre à la question : Quelle est la variable pointée par ce pointeur ? Si vous ne pouvez pas y répondre, c’est probablement qu’il ne pointe aucune variable. Ce type d’erreur va souvent résulter en une « erreur de segmentation/coredump » sous UNIX/Linux ou une erreur de protection générale sous Windows. (Sous le bon vieux DOS (beurk!), tout pouvait arriver !)

Voici un exemple de ce type d’erreur.

    #include <string.h>
    int main()
    {
      char * chn;   /* définit un pointeur sur un char ou un tableau de char */

      strcpy(chn, "abc");  /* sur quel tableau de char pointe chn ?? */
      return 0;
    }

Comment faire cela correctement ? Il faut soit utiliser un tableau soit allouer dynamiquement un tableau.

    #include <string.h>
    int main()
    {
      char chn[20];   /* définit un tableau de char */

      strcpy(chn, "abc");  /* chn pointe sur un tableau de char */
      return 0;
    }

ou

    #include <string.h>
    #include <stdlib.h>
    int main()
    {
      char *chn = malloc(20);   /* chn pointe sur un tableau alloué */

      strcpy(chn, "abc");  /* chn pointe sur un tableau de char */
      free(chn);                /* n'oubliez pas de désallouer une fois fini ! */
      return 0;
    }

En fait, la première solution est beaucoup plus préférable pour ce que fait ce code. Pourquoi ? L’allocation dynamique devrait seulement être utilisée quand elle est requise. Elle est plus lente et plus sujette aux erreurs que la simple définition d’un tableau normal.

3. Erreurs de chaînes de caractères

3.1 Confondre les constantes caractère et chaîne de caractères

C considère les constantes caractères et chaînes de caractères comme des choses très différentes. Les constantes caractères sont entourées d’apostrophes (guillemets simples) et les constantes chaînes sont entourées de guillemets (doubles). Les constantes chaînes agissent comme des pointeurs sur la véritable chaîne. Examinez le code suivant :

    char c = 'A';     /* correct */
    char c = "A";     /* erreur  */

La deuxième ligne affecte à la variable caractère c l’adresse d’une constante chaîne. Cela devrait générer une erreur du compilateur. Il devrait arriver la même chose si à un pointeur sur une chaîne est affectée une constante caractère :

    const char * chn = "A";     /* correct */
    const char * chn = 'A';     /* erreur  */

3.2 Comparer des chaînes avec ==

N’utilisez jamais l’opérateur == pour comparer la valeur de deux chaînes ! Les chaînes sont des tableaux de char. Le nom d’un tableau de caractères agit comme un pointeur sur la chaîne (exactement comme d’autres types de tableaux en C). Et alors ? Examinez le code suivant :

    char chn1[] = "abc";
    char chn2[] = "abc";
    if ( chn1 == chn2 )
      printf("Oui");
    else
      printf("Non");

Ce code affiche en sortie Non. Pourquoi ? Parce que l’opérateur == compare les valeurs des pointeurs chn1 et chn2, pas les données sur lesquelles ils pointent. La manière correcte de comparer deux valeurs chaînes est d’utiliser la fonction strcmp() de la bibliothèque (assurez-vous d’avoir inclus string.h). Si l’instruction if ci-dessus est remplacée par ce qui suit:

    if ( strcmp(chn1,chn2) == 0 )
      printf("Oui");
    else
      printf("Non");

alors le code affichera en sortie Oui. Pour des raisons similaires, n’utilisez pas les autres opérateurs relationnels (<, > etc.) avec les chaînes non plus. Utilisez strcmp() ici aussi.

3.3 Chaînes sans zéro terminal

C considère qu’une chaîne est un tableau de caractères avec un caractère nul terminal. Ce caractère nul a une valeur ASCII de 0 et peut être représenté simplement par 0 ou

'\0'

Cette valeur est utilisée pour marquer la fin des données significatives dans la chaîne. Si cette valeur manque, plusieurs fonctions de la bibliothèque string du C continueront à traiter les données qui suivent la fin des données significatives et souvent en dépassant la fin du tableau de caractères lui-même jusqu’à ce qu’elles finissent par trouver un octet zéro dans la mémoire !

La plupart des fonctions de la bibliothèque string du C qui créent des chaînes vont toujours les terminer proprement par un caractère nul. Certaines ne le font pas (comme strncpy() ). Assurez-vous de lire leurs descriptions attentivement.

3.4 Ne pas laisser la place pour le zéro terminal

Une chaîne en C doit avoir un zéro terminal (caractère nul de terminaison) à la fin des données significatives dans la chaîne. Une erreur courante est de ne pas allouer d’espace pour ce caractère supplémentaire. Par exemple, la chaîne définie ci-dessous

    char chn[30];

n’a de place que pour 29 (pas 30) caractères de données véritables seulement, puisqu’un caractère nul doit être présent après le dernier caractère de données.

Cela peut aussi être un problème avec l’allocation dynamique. Voici la manière correcte d’allouer une chaîne à la taille exacte nécessaire pour contenir une copie d’une autre chaîne :

    char * chn_copie = malloc( strlen(chn_orig) + 1);
    strcpy(chn_copie, chn_orig);

L’erreur courante est d’oublier d’ajouter un à la valeur de retour de strlen(). La fonction strlen() retourne le compte des caractères de la donnée, qui n’incluent pas le zéro terminal.

Ce type d’erreur peut être très difficile à détecter. Il peut ne causer aucun problème, ou seulement en causer dans des cas extrêmes. Dans le cas d’une allocation dynamique, il peut altérer le tas (la zone de la mémoire du programme utilisée pour l’allocation dynamique) et faire échouer la prochaine opération de tas (malloc(), free(), etc.).

4. Erreurs d’Entrées/Sorties

4.1 Utilisation incorrecte de fgetc(), etc.

Les fonctions fgetc(), getc() et getchar() renvoient toutes une valeur entière en retour. Par exemple, le prototype de fgetc() est :

    int fgetc( FILE * );

Parfois cette valeur entière est vraiment un simple caractère, mais il y a un cas très important dans lequel la valeur de retour n’est pas un caractère !

Quelle est cette valeur ? EOF. Une idée fausse courante chez les étudiants est que les fichiers ont un caractère EOF spécial à la fin (NdT : EOF=End Of File=Fin de fichier). Il n’y a pas de caractère spécial enregistré à la fin d’un fichier. EOF est un code d’erreur entier retourné par une fonction. Voici la mauvaise manière d’utiliser fgetc() :

    int count_line_size( FILE * fp )
    {
      char ch;
      int  cnt = 0;

      while( (ch = fgetc(fp)) != EOF && ch != '\n')
        cnt++;
      return cnt;
    }

Qu’est-ce qui ne va pas ici ? Le problème réside dans la condition de la boucle while. Afin de mieux voir, voici la boucle réécrite pour montrer ce que le C fera en coulisses.

    while( (int) ( ch = (char) fgetc(fp) ) != EOF && ch != '\n')
      cnt++;

Le type de la valeur de retour de fgetc(fp) est converti en char pour stocker le résultat dans ch. Ensuite, la valeur de ch doit être reconvertie en int à nouveau pour la comparer à EOF. Et alors ? Convertir une valeur du type int au type char puis à nouveau en int peut ne pas redonner la valeur de type int originale. Ce qui veut dire dans l’exemple ci-dessus que si fgetc() renvoie en retour la valeur EOF, la conversion peut modifier la valeur de façon que la comparaison ultérieure avec EOF soit fausse.

Quelle est la solution ? Rendre la variable ch de type int comme ceci :

    int count_line_size( FILE * fp )
    {
      int ch;
      int  cnt = 0;

      while( (ch = fgetc(fp)) != EOF && ch != '\n')
        cnt++;
      return cnt;
    }

Maintenant, la seule conversion de type cachée est dans la deuxième comparaison.

    while( (ch = fgetc(fp)) != EOF &&  ch != ((int) '\n') )
      cnt++;

Cette conversion n’a pas d’effet néfaste du tout ! Ainsi, la morale de tout cela est : utilisez toujours une variable de type int pour stocker le résultat de fgetc(), getc() et getchar().

4.2 Utilisation incorrecte de feof()

Il y a une méprise très largement répandue sur la façon de fonctionner de la fonction feof() du C. Beaucoup de programmeurs l’utilisent comme la fonction eof() du Pascal. Cependant, la fonction du C fonctionne différemment !

Quelle est la différence ? La fonction du Pascal renvoie vrai si la prochaine lecture va échouer à cause de la fin de fichier. La fonction du C renvoie vrai si la dernière fonction a échoué. Voici un exemple d’une mauvaise utilisation de feof() :

    #include <stdio.h>
    int main()
    {
      FILE * fp = fopen("test.txt", "r");
      char ligne[100];

      while( ! feof(fp) ) {
        fgets(ligne, sizeof(ligne), fp);
        fputs(ligne, stdout);
      }
      fclose(fp);
      return 0;
    }

Ce programme affichera en sortie la dernière ligne du fichier en entrée deux fois. Pourquoi ? Après que la dernière ligne est lue en entrée et affichée en sortie, feof() va toujours renvoyer 0 (faux) et la boucle va continuer. Le prochain fgets() échoue et ainsi la variable ligne portant le contenu de la dernière ligne n’est pas modifiée et est encore affichée en sortie. Après cela, feof() va renvoyer vrai (puisque fgets() a échoué) et la boucle se termine.

Comment réparer cela ? Voici une manière de faire :

    #include <stdio.h>
    int main()
    {
      FILE * fp = fopen("test.txt", "r");
      char ligne[100];

      while( 1 ) {
        fgets(ligne, sizeof(ligne), fp);
        if ( feof(fp) )    /* voir s'il y a EOF juste après fgets() */
          break;
        fputs(ligne, stdout);
      }
      fclose(fp);
      return 0;
    }

Toutefois, ce n’est pas la meilleure manière. Il n’y a aucune raison véritable d’utiliser feof() du tout. Les fonctions d’entrées du C renvoient des valeurs qui peuvent être utilisées pour voir s’il y a fin de fichier. Par exemple, fgets renvoie le pointeur NULL en cas de fin de fichier. Voici une meilleure version du programme :

    #include <stdio.h>
    int main()
    {
      FILE * fp = fopen("test.txt", "r");
      char ligne[100];

      while( fgets(ligne, sizeof(ligne), fp) != NULL )
        fputs(ligne, stdout);
      fclose(fp);
      return 0;
    }

L’auteur attend toujours de voir un étudiant utiliser la fonction feof() correctement !

Entre parenthèses, cette discussion s’applique également à C++ et Java. La méthode eof() d’un istream fonctionne exactement comme le feof() du C.

4.3 Laisser des caractères dans le tampon d’entrée

Les fonctions d’entrées (et de sorties) du C mettent en tampon les données. La mise en tampon consiste à stocker les données en mémoire et ne lire (ou écrire) les données des (ou dans les) périphériques d’entrée/sortie que lorsque c’est nécessaire. La lecture et l’écriture de données en grands blocs est beaucoup plus efficace qu’un octet (ou caractère) à la fois. Souvent la mise en tampon n’a pas d’effet sur la programmation.

Il y a une situation où la mise en tampon est visible, ce sont les entrées utilisant scanf(). Le clavier est en général mis en tampon ligne par ligne. Cela veut dire que chaque ligne d’entrée est stockée dans un tampon. Des problèmes peuvent apparaître quand un programme ne traite pas toutes les données dans une ligne avant de vouloir traiter la prochaine ligne d’entrée. Par exemple, examinez le code suivant :

    int x;
    char chn[31];

    printf("Entrez un entier : ");
    scanf("%d", &x);
    printf("Entrez une ligne de texte : ");
    fgets(chn, 31, stdin);

Le fgets() ne lira pas la ligne de texte qui est tapée en entrée. Au lieu de cela, il va probablement seulement lire une ligne vide. En fait, le programme n’attendra même pas d’entrée pour l’appel à fgets(). Pourquoi ? L’appel à scanf() lit les caractères dont il a besoin qui représentent le nombre entier lu en entrée, mais il laisse le '\n' dans le tampon d’entrée. Le fgets() commence alors à lire les données du tampon d’entrée. Il trouve un '\n' et s’arrête sans avoir besoin de plus d’entrées du clavier.

Quelle est la solution ? Une méthode simple est de lire et jeter tous les caractères venant du tampon d’entrée jusqu’à un '\n' après l’appel à scanf(). Puisque c’est une chose qui pourra être utilisée dans beaucoup de situations, il est logique d’en faire une fonction. Voici une fonction qui fait exactement cela :

    /* fonction vider_ligne
    *  Cette fonction lit et jette tous caractères restants
    *  de la ligne d'entrée courante d'un fichier.
    *  Paramètres :
    *     fp - pointeur sur un fichier FILE dont on lit les caractères
    *  Précondition:
    *     fp pointe sur un fichier ouvert
    *  Postcondition:
    *     le fichier référencé par fp est positionné à la fin 
    *     de la ligne suivante ou la fin du fichier.
    */
    void vider_ligne( FILE * fp )
    {
      int ch;

      while( (ch = fgetc(fp)) != EOF && ch != '\n' )
        /* corps nul */;
    }

Voici le code précédent réparé en utilisant la fonction ci-dessus :

    int x;
    char chn[31];

    printf("Entrez un entier : ");
    scanf("%d", &x);
    vider_ligne(stdin);
    printf("Entrez une ligne de texte : ");
    fgets(chn, 31, stdin);

Une solution incorrecte est d’utiliser ce qui suit :

    fflush(stdin);

Cette instruction sera compilée mais son comportement est indéfini par la norme ANSI du C. La fonction fflush() est seulement faite pour être utilisée sur des flux ouvert en sortie, pas en entrée. Cette méthode semble fonctionner avec certains compilateurs C, mais n’est pas du tout portable ! Par conséquent elle ne doit pas être utilisée.

4.4 Utiliser la fonction gets()

N’utilisez pas cette fonction ! Elle ne sait pas combien de caractères peuvent être stockés sans danger dans la chaîne qui lui est transmise. Ainsi, si trop de caractères sont lus, le contenu de la mémoire sera endommagé. Beaucoup de bugs de sécurité ont été exploités sur Internet en utilisant cela ! Utilisez la fonction fgets() plutôt (en lisant depuis stdin). Mais souvenez-vous que contrairement à gets(), fgets() n’élimine pas le \n final de l’entrée.

L’utilisation des fonctions scanf() peut également être dangereuse. Le format %s peut écraser la chaîne de destination. Toutefois, il peut être utilisé sans risque en spécifiant une largeur. Par exemple, le format %20s ne va pas lire plus de 20 caractères.

5. Remerciements

L’auteur désire remercier Stefan Ledent pour avoir suggéré la section « Ne pas laisser la place pour le zéro terminal ».

Copyright © 2008 Paul Carter. Tous droits réservés.
Traduction : Amine Brikci-Nigassa 2013.

Programmation en C sous UN*X – TP N°1

1. Un petit rien

Pour commencer, nous allons écrire, compiler et exécuter le plus petit programme en C possible.
Ouvrez le terminal.
Tapez la commande suivante, tout en minuscules (le C comme Unix sont sensibles à la casse) :

echo "main(){}">rikiki.c

Ça y est, félicitations : vous avez écrit un programme en C ! Et vous l’avez enregistré dans le fichier « rikiki.c » !
Si si ! Vérifiez : affichez le contenu du répertoire courant avec la commande :

ls

Vous voyez le fichier rikiki.c ? C’est votre programme source.

echo est une commande du shell Unix qui, comme son nom l’indique, renvoie en écho le texte que vous lui avez crié donné en paramètre. Renvoie vers où ? vers la sortie standard, c’est-à-dire normalement vers l’écran, mais ici la sortie standard a été redirigée (détournée) vers le fichier rikiki.c avec l’opérateur de redirection >.

Si vous n’êtes toujours pas convaincu, affichez le contenu du fichier avec la commande :

cat rikiki.c

vous devriez retrouver votre programme :

main(){}

Ce programme est en langage C. Pour que la machine le comprenne et l’exécute, il faut le traduire en langage machine. Pour cela, compilez-le avec la commande :

gcc rikiki.c

GCC (GNU Compiler Collection) est un compilateur qui comprend plusieurs langages. Comme le nom du fichier donné en paramètre se termine par .c (attention ! il faut qu’il se termine par .c en minuscule), il va considérer que le programme est en C et va le compiler (le traduire) en langage machine dans un fichier exécutable binaire.

Par défaut, le nom du fichier exécutable obtenu est a.out (assembler output).

GCC ne renvoie aucun message si tout se passe bien, sinon il peut afficher des erreurs ou des avertissements.
Vérifiez avec la commande ls que a.out a bien été créé par GCC.

Pour savoir ce que fait le programme, il faut demander au système de l’exécuter. Pour cela, il suffit de taper le nom du fichier exécutable précédé du chemin où il se trouve. Comme il est dans le répertoire courant (symbolisé par un point . sous Unix) il suffit de taper la commande :

./a.out

Le résultat de l’exécution doit donner… rien du tout !

En effet, ce premier programme n’effectue aucun affichage (et rien d’autre non plus d’ailleurs). Mais il nous a permis de voir les étapes de la compilation et de l’exécution.

Examinons-le :

main(){}

Il ne comporte qu’une ligne, qui est en fait une définition de fonction.
Le nom de la fonction est main.
Le corps de la fonction est vide : il n’y a rien dans le bloc d’instructions délimité par les accolades { (début de bloc) et } (fin de bloc).

La définition de la fonction main() est obligatoire dans tout programme en C. C’est la fonction principale, qui est le point de départ du programme et dont les instructions sont exécutées en premier.

Si nous voulons que notre programme fasse quelque chose, il faut donc mettre au moins une instruction dans le bloc du corps de main() entre les accolades.

2. Hello, world!

Nous allons créer le fichier texte hello.c qui contiendra le programme source de notre deuxième exemple. Dans le terminal, tapez la commande :

kwrite hello.c &

L’éditeur Kwrite s’ouvre dans une fenêtre.
Si cela ne fonctionne pas, c’est que Kwrite n’est pas installé, essayez alors avec Gedit, en remplaçant la commande kwrite ci-dessus par gedit

(Remarque : le & à la fin de la commande est une astuce pour pouvoir entrer d’autres commandes dans le terminal après sans avoir à fermer la fenêtre de l’éditeur)

Tapez dans la fenêtre de l’éditeur de texte le programme suivant (sans les numéros des lignes bien sûr) :

main()
{
    puts("Hello, world!");
}

Enregistrez votre programme (Ctrl+S ou bouton Enregistrer) puis revenez au terminal et compilez le programme avec :

gcc hello.c

Exécutez le programme avec :

./a.out

… et admirez le résultat ! Si tout s’est bien passé, votre programme a affiché :

Hello, world!

Il a ensuite terminé son exécution et a rendu la main à l’interpréteur de commandes du terminal.

Observons encore une fois notre programme ci-dessus.
Remarquez que nous avons mis les accolades chacune seule sur une ligne. Le choix de ce style permet de veiller à ce que chaque accolade ouvrante ait une accolade fermante correspondante qui soit alignée avec elle. Quand on a beaucoup de blocs imbriqués, c’est très utile.
Le C n’impose pas de style particulier pour la présentation, mais il est conseillé de ne pas abuser de cette liberté pour écrire du code incompréhensible. Le programme doit rester bien lisible pour être maintenable.

La seule différence entre le premier programme et le deuxième est donc l’instruction de la ligne 3. Cette instruction est construite grâce à une expression et un point-virgule. La plupart des instructions en C se terminent par un point-virgule. Ce n’est pas un séparateur d’instructions (comme en Pascal) mais il fait partie de l’instruction. La plus petite instruction en C, l’instruction vide, est constituée d’un simple point-virgule (nous aurons l’occasion de l’utiliser dans les boucles notamment).

L’expression qui constitue (avec le point-virgule) l’instruction de la ligne 3 est un appel à une fonction. On appelle une fonction en écrivant son nom suivi de parenthèses. Si la fonction nécessite des paramètres, on les met entre les parenthèses en les séparant par des virgules.

La fonction appelée ici est puts(), qui est une fonction de la bibliothèque standard stdio (standard input/output) qui permet d’afficher sur la sortie standard (ici c’est l’écran) le contenu de la chaîne de caractères qui lui est donnée en argument.

Remarquons qu’une chaîne de caractères s’écrit en C avec des guillemets " autour.

Il existe une autre fonction de la bibliothèque stdio qui permet d’afficher à l’écran : printf()
Modifiez votre programme de cette façon :

main()
{
    puts("Hello, world!");
    printf("Salam alikoum !");
}

Compilez-le. Cette fois-ci, le compilateur vous donne un message.

hello.c: In function ‘main’:
hello.c:4:5: attention : incompatible implicit declaration of built-in function ‘printf’ [enabled by default]

C’est un avertissement (warning). Les avertissements ressemblent à des erreurs mais ne sont pas fatals : ils n’empêchent pas la compilation de se terminer. Essayez d’exécuter votre programme, vous verrez qu’il fonctionne bien.

Attention, ne négligez pas les avertissements : très souvent, ils montrent un défaut dans votre programme qui peut vous causer de gros ennuis par la suite.

Mais avant de nous en occuper, nous allons d’abord régler un petit problème : les programmes que nous compilons s’appellent tous a.out, et vous avez dû remarquer que le dernier écrase toujours le précédent. En fait, la syntaxe de la commande gcc donnée précédemment est incomplète. On ne l’utilise jamais comme cela (sauf si on est vraiment pressé).

Voici la syntaxe qu’il est conseillé plutôt d’utiliser. Essayez-la :

gcc hello.c -o hello -Wall -g -std=c99

- l’option -o permet de préciser le nom du fichier exécutable à obtenir : il vaut mieux avoir un exécutable qui s’appelle hello que a.out
- l’option -Wall (attention, W majuscule) permet d’activer tous les avertissements : ils sont vraiment très utiles.
- l’option -g permet d’ajouter des informations pour la détection des erreurs (exemple : si vous écrivez printF au lieu de printf, elle permet de préciser le numéro de la ligne où se trouve l’erreur).
- l’option -std=c99 permet d’utiliser la norme de 1999 plutôt que celle de 1990.

Les messages du compilateur sont plus nombreux. L’option -Wall a activé l’affichage de plusieurs avertissements :

hello.c:1:1: attention : return type defaults to ‘int’ [-Wreturn-type]
hello.c: In function ‘main’:
hello.c:3:5: attention : implicit declaration of function ‘puts’ [-Wimplicit-function-declaration]
hello.c:4:5: attention : implicit declaration of function ‘printf’ [-Wimplicit-function-declaration]
hello.c:4:5: attention : incompatible implicit declaration of built-in function ‘printf’ [enabled by default]

Il ne faut pas oublier que même si le programme est compilé et fonctionne, ces avertissements dénotent des erreurs de programmation.

Trois des avertissements concernent les fonctions puts et printf. Le compilateur se plaint car il n’a pas trouvé la déclaration de ces fonctions et les a donc déclarées de façon implicite (par défaut). Le compilateur a corrigé l’erreur mais nous avons eu de la chance que ces fonctions soient si simples, cela aurait pu être plus grave avec d’autres fonctions. La solution est d’inclure dans notre programme les déclarations que l’on trouve dans un fichier fait pour cela. En effet, toutes les fonctions de la bibliothèque stdio se trouvent prêtes dans le fichier d’en-tête stdio.h, qu’il suffit d’inclure en ajoutant au début de notre programme :

#include <stdio.h>

Cette ligne est une directive du préprocesseur. Avant de compiler un programme, le compilateur appelle toujours le préprocesseur qui va remplacer cette ligne par le contenu du fichier d’en-tête stdio.h. Ainsi, les déclarations de printf() et puts() seront incluses dans notre programme.
Essayons donc de compiler notre nouveau programme :

#include <stdio.h>
main()
{
    puts("Hello, world!");
    printf("Salam alikoum !");
}

Le premier avertissement de tout à l’heure est toujours là. Il concerne notre fonction main() elle-même. Il indique que la fonction main() retourne par défaut un entier (int). En effet, nous n’avons pas indiqué quel était le type de retour de main() et dans la norme du C, c’est une faute, même si elle est corrigée par défaut par le compilateur. Dans la norme, il y a seulement deux façons de définir main(), la première est comme suit (nous parlerons plus tard de la deuxième) :

int main(void)

(suivie bien sûr du corps de la fonction entre accolades)
int est le type de retour (entier) et void indique qu’il n’y a pas de paramètres.

Quel est cet entier retourné par main() ? C’est le code de sortie du programme : un nombre qui est renvoyé au système d’exploitation pour qu’il sache si le programme s’est terminé sans erreur. Sous Unix et la plupart des systèmes, le code doit être zéro quand le programme se termine correctement et un entier non nul sinon. Mais ce n’est pas le cas pour tous les systèmes. Il existe des constantes pour le code de sortie dans le fichier d’en-tête stdlib.h : ce sont EXIT_SUCCESS et EXIT_FAILURE.

Voici donc notre programme, corrigé selon la norme, pour qu’il se compile sans avertissements :

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    puts("Hello, world!");
    printf("Salam alikoum !\n");
    return EXIT_SUCCESS;
}

Remarquez que l’on y a ajouté aussi le code de caractère \n à la fin de la deuxième chaîne : cela permet de retourner à la ligne, car puts() le fait automatiquement mais pas printf(). puts() est plus approprié que printf pour afficher une chaîne de caractères mais printf() permet de combiner plusieurs types à l’affichage.

3. Calcul

/*
 *  Licence informatique 2e année - 2012-2013
 *  TP - Calcul
 *  Fichier source : calcul.c
 *  Compiler avec :
 *  gcc -Wall -o calcul calcul.c -g -std=c99
 *  Auteur : Amine "nh2" Brikci-Nigassa
 */
 
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    puts("--- Calcul ---");
    printf("1er nombre : ");
    int a;           // déclaration de l'entier a
    scanf("%d",&a);  // lecture de a
    printf("2e nombre (non nul) : ");
    int b;
    scanf("%d",&b);
    if (b == 0) {    // test d'égalité : double égal
        puts("Erreur !! 2e nombre nul !");
        return EXIT_FAILURE;  // sortie du programme
    } else {
        int somme = a + b;    // déclaration + affectation (égal)
        printf("%d + %d = %d\n", a, b, somme);
        int produit = a * b;
        printf("%d x %d = %d\n", a, b, produit);
        int quotient = a / b;
        printf("%d : %d = %d\n", a, b, quotient);
        int reste = a % b;
        printf("%d = %d x %d + %d\n", a, b, quotient, reste);        
        return EXIT_SUCCESS;
    }
}

Essayez ce programme.

Les commentaires sont délimités soit par /* et */ soit par // et la fin de la ligne.

Remarquez la syntaxe utilisée pour la déclaration des variables (type nom;). Contrairement au Pascal, les déclarations peuvent être mélangées aux instructions (depuis la norme de 1999). Souvent, on préfère déclarer une variable au dernier moment, juste avant de l’utiliser.

La fonction scanf() permet de lire l’entrée standard (ici le clavier). On utilise le code format %d pour des entiers en base 10. La variable à lire est précédée du symbole & qui signifie « adresse mémoire de » (scanf() ne reçoit pas la variable mais son adresse en mémoire, pour pouvoir en modifier le contenu).

La syntaxe de if est la suivante :

if (condition)
    instruction_si_vrai;
else
    instruction_si_faux;

La condition doit obligatoirement être entourée de parenthèses.
N’oubliez pas les points-virgules, ils font partie des instructions.

Exercice

Écrivez un programme en C permettant de calculer les racines réelles et complexes d’une équation du second degré à une inconnue.
Indications :
- Le type à utiliser pour déclarer des réels est float ou double. On préfère généralement le type double.
- Le code format pour la lecture d’un réel avec scanf() est %f pour un float et %lf pour un double.
- Le code format pour l’écriture d’un réel avec printf() est %f ou %g pour les deux types. On préférera %g dans cet exercice, mais essayez d’abord avec %f.
- La fonction racine carrée est sqrt() et elle est déclarée dans le fichier d’en-tête math.h. Attention : pour que la bibliothèque math soit utilisée à la compilation (à l’édition de liens), il faut ajouter l’option -lm à la commande gcc.
- L’opérateur logique ET s’écrit && en C
- L’opérateur logique OU s’écrit || en C
- L’opérateur logique NON s’écrit ! en C
- Les opérateurs de comparaison inférieur, supérieur, inf. ou égal, sup. ou égal, égal et différent s’écrivent respectivement < > <= >= == !=

EPST 2e année – TP6

Le TP N°6 va nous faire manipuler des chaînes de caractères dans un tableau, le tout alloué dynamiquement sur le tas.

Comme nous l’avons déjà vu, la particularité des chaînes de caractères en C est qu’elles correspondent en fait à une suite contiguë d’éléments de type char dont le dernier doit obligatoirement avoir la valeur zéro (caractère nul de fin de chaîne). Cela peut correspondre à un tableau statique (déclaré comme variable globale par exemple) ou automatique (variable locale) de caractères, ou à un bloc de mémoire alloué dynamiquement grâce à l’une des fonctions malloc(), calloc() ou realloc() puis libéré grâce à free().

Voici l’énoncé :

Il y a donc deux tableaux à construire : l’un pour les noms et l’autre pour les notes. Le deuxième est le plus simple, puisque le type float est un type scalaire. Une fois que l’utilisateur nous a indiqué le nombre d’éléments des tableaux, on peut allouer dynamiquement celui des notes. En utilisant une variable automatique de type tableau à longueur variable (VLA), nous pourrions déclarer :
float notes[nbetudiants];
Mais comme il est demandé d’utiliser une allocation dynamique sur le tas, nous ferons plutôt :
float * notes = calloc(nbetudiants, sizeof(*notes));

(… en cours d’écriture …)

EPST 2e année – TP5 Exercice 2

Nous continuons avec les fonctions d’allocation et libération dynamiques de la mémoire.

Énoncé

Solution de l’exercice II

Comme d’habitude, essayez de résoudre l’exercice par vous-même, et ensuite comparez votre solution avec la mienne ci-dessous.

/*
 *  EPST 2e année - 2012-2013
 *  TP5 - Allocation dynamique de la mémoire
 *  Exercice 2 : Concaténation de tableaux
 *  Fichier source : concatab.c
 *  Compiler avec :
 *  gcc -Wall -o concatab concatab.c -std=c99
 *  Auteur : Amine "nh2" Brikci-Nigassa
 */
 
#include <stdio.h>
#include <stdlib.h>

/* concat : concaténation de deux tableaux
 * Paramètres :
 *  -  tail1 : taille du 1er tableau 
 *  -  tab1 : 1er tableau de réels (double)
 *  -  tail2 : taille du 2e tableau
 *  -  tab2 : 2e tableau de réels
 * Résultat : retourne un pointeur sur un tableau alloué dynamiquement  
 *            contenant les éléments du 1er suivis de ceux du 2e
 * (Ne pas oublier de libérer la mémoire pointée par la valeur de retour)
 */
double * concat(int tail1, double tab1[tail1],
                int tail2, double tab2[tail2]){
 /* pour provoquer une erreur, essayer :
    double * p = calloc(1000000000000,sizeof(*p));
  */
    double * p = calloc(tail1+tail2,sizeof(*p));
    
    if (p)  // en cas d'erreur, p est NULL <=> 0 <=> faux
        for (int i = 0 ; i < tail1+tail2 ; ++i)
            p[i] = (i<tail1) ? tab1[i] : tab2[i-tail1] ;
    return p;
}

/* saisir : saisie des éléments d'un tableau
 * Paramètres :
 *  -  taille : nbre d'éléments du tableau
 *  -  tabl : tableau des réels (double) à saisir
 *  -  nom : nom du tableau
 */
void saisir(int taille, double tabl[taille], char * nom){
    printf("Entrez les éléments du tableau %s\n", nom);
    for (int i=0 ; i<taille ; ++i){
        printf("%s[%d] = ",nom,i);
        scanf("%lf",tabl+i);
    }
}

int main(void){
    int tail1, tail2;
    printf("Taille du tableau A : ");
    scanf("%d",&tail1);
    double tab1[tail1];
    saisir(tail1, tab1, "A"); 
    printf("Taille du tableau B : ");
    scanf("%d",&tail2);
    double tab2[tail2];
    saisir(tail2, tab2, "B"); 
    double * tab3 = concat(tail1, tab1, tail2, tab2);
    if (!tab3){
        perror("erreur d'allocation mémoire");
        return(EXIT_FAILURE);
    }
    puts("Le tableau résultant est :");
    for (int i=0 ; i<tail1+tail2 ; ++i)
        printf("C[%d] = %g%s",i,tab3[i],i<tail1+tail2-1?", ":"\n");
    free(tab3);
    return EXIT_SUCCESS;
}

On nous demande de commencer par la saisie de deux tableaux dans le « programme principal » (la fonction main()). J’ai choisi d’utiliser des tableaux à longueur variable (VLA), qui, rappelons-nous (cf. TP4), permettent de faire une allocation dynamique automatiquement : la taille de chaque tableau est déterminée à l’exécution ; on peut donc la demander à l’utilisateur.

Nous avons la chance d’utiliser un compilateur récent qui autorise les VLA (qui n’étaient pas dans la norme du C avant 1999), cela évite d’utiliser des pointeurs (ils ne sont pas méchants mais ils peuvent être fatigants parfois :-). Cela permet surtout de ne pas s’occuper de la libération de la mémoire : l’allocation est automatique, et la libération aussi ; nous n’en sommes pas responsables puisque nous n’avons fait que déclarer un tableau et c’est le compilateur qui s’est occupé de l’allocation.

Une autre différence entre l’allocation des VLA et celle obtenue par les fonctions de la famille de malloc() est que la deuxième est faite sur le tas, alors que la première est faite sur la pile, qui est le segment de la mémoire utilisé par les fonctions pour y stocker leurs paramètres et toutes les variables locales (et qui déborde souvent quand on abuse de la récursivité). Wikipédia a un article qui explique cela clairement.
Si on veut qu’une fonction puisse allouer de la mémoire dynamiquement qui sera utilisée par le programme après qu’elle se termine, on ne peut pas le faire sur la pile (ni avec une déclaration locale habituelle ni avec des VLA) car par définition, les variables qui s’y trouvent seront libérées (dépilées) à la sortie de la fonction. D’où l’utilité de l’allocation sur le tas dans ces cas.

Pour que le programme soit assez clair et concis, la saisie des tableaux est faite avec la fonction saisir(), qui sera appelée deux fois dans main() : un appel pour chaque tableau, avec pour paramètres le tableau et sa taille (dans l’ordre inverse, rappelez-vous) et une chaîne de caractères contenant le nom du tableau.

Par contre, pour se familiariser avec elles, les fonctions d’allocation dynamique de la mémoire sont utilisées pour allouer le troisième tableau.
double * p = calloc(tail1+tail2, sizeof(*p));
L’instruction ci-dessus alloue un bloc de mémoire avec calloc() ayant une taille correspondant à (tail1+tail2) nombres. Elle est équivalente à la suivante :
double * p = malloc((tail1+tail2)*sizeof(*p));
bien que ces deux fonctions aient quand même une petite différence (vérifiez dans le manuel de malloc()) mis à part le nombre de paramètres : les valeurs contenues dans le bloc alloué par malloc() sont indéterminées, alors que calloc(), elle, s’occupe de les initialiser à zéro. Retenez cela, même si dans notre TP, cela n’a aucune importance (par contre cela a fait l’objet d’une question d’examen…).
L’opérateur unaire sizeof (bizarrement, ce n’est pas une fonction mais bien un opérateur du C, même s’il est représenté par un nom, pas par un symbole) donne une expression dont la valeur est la taille en octets du type de son opérande. On peut l’utiliser avec une expression ou un type (à entourer obligatoirement de parenthèses dans le 2e cas). Il pourrait sembler plus simple d’écrire :
double * p = calloc(tail1+tail2, sizeof(double));
Cela revient au même puisque le type de *p est justement double, mais beaucoup de programmeurs conseillent la première notation, pour deux raisons : on risque moins de se tromper avec la première et (surtout) si on change le type de p plus tard, dans la deuxième notation on risque d’oublier de changer l’opérande de sizeof, alors que dans la première il reste correct.

Pour vous exercer, vous pouvez vous amuser à inverser : utiliser les fonctions d’allocation sur le tas pour la saisie et un tableau à longueur variable pour le tableau du résultat. Mais attention à la position de vos déclarations. Remarquez ci-dessus : bien que l’allocation avec calloc() a été faite dans la fonction concat(), l’allocation automatique de tab1 et tab2 par contre se fait dans main(). La solution suivante ne fonctionnerait pas :

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

void concat(int tail1, double tab1[tail1],
            int tail2, double tab2[tail2],
                       double tab3[]     ){
    for (int i = 0 ; i < tail1+tail2 ; ++i)
        tab3[i] = (i<tail1) ? tab1[i] : tab2[i-tail1] ;
}

double * saisir(int taille, char * nom){
    printf("Entrez les éléments du tableau %s\n", nom);
    double tabl[taille];
    for (int i=0 ; i<taille ; ++i){
        printf("%s[%d] = ",nom,i);
        scanf("%lf",tabl+i);
    }
    return tabl;
}

int main(void){
    int tail1, tail2;
    printf("Taille du tableau A : ");
    scanf("%d",&tail1);
    double *tab1 = saisir(tail1, "A"); 
    printf("Taille du tableau B : ");
    scanf("%d",&tail2);
    double *tab2 = saisir(tail2, "B"); 
    double *tab3 = calloc(tail1+tail2,sizeof(*tab3));
    if (!tab3){
        perror("erreur d'allocation mémoire");
        return(EXIT_FAILURE);
    }
    concat(tail1, tab1, tail2, tab2, tab3);
    puts("Le tableau résultant est :");
    for (int i=0 ; i<tail1+tail2 ; ++i)
        printf("C[%d] = %g%s",i,tab3[i],i<tail1+tail2-1?", ":"\n");
    free(tab3);
    return EXIT_SUCCESS;
}

La fonction concat() ne pose aucun problème ici (rappelez-vous qu’un paramètre de type tableau est un pointeur en fait). Et puis la compilation se fera sans erreur. Vous aurez juste un petit avertissement (avec attention ou warning) sans importance…?

Vous pourrez exécuter ce programme mais vous aurez une surprise à la fin ; les valeurs obtenues dans le tableau tab3 seront aberrantes :

Taille du tableau A : 2
Entrez les éléments du tableau A
A[0] = 2.3
A[1] = 4.3
Taille du tableau B : 3
Entrez les éléments du tableau B
B[0] = 5.4
B[1] = 6.4
B[2] = 4.2
Le tableau résultant est :
C[0] = 3.95253e-323, C[1] = -4.57395e-42, C[2] = 3.95253e-323, C[3] = -4.57395e-42, C[4] = 6.7646e-316

Le message d’avertissement du compilateur était-il donc plus important que nous l’avions cru ? Que disait-il au fait ?

18:5: attention : cette fonction retourne l'adresse d'une variable locale [enabled by default]

La ligne 18 est celle du return tabl; de la fonction saisir(). L’avertissement confirme ce que j’ai dit plus haut : la variable tabl a été allouée dynamiquement, mais de façon automatique, sur la pile, pas sur le tas, avec le mécanisme des VLA, pas avec une fonction de type malloc(). Sa libération aussi se fait de façon automatique : elle est dépilée à la sortie de la fonction car c’était une variable locale. Son contenu est donc perdu et cela ne sert à rien de retourner son adresse !

Cette erreur n’est pas rare chez les débutants programmeurs, méfiez-vous, et puis surtout n’ignorez pas les avertissements du compilateur (et rappelez-vous de les activer tous avec l’option -Wall de gcc).

Dernier rappel : j’espère que vous avez remarqué l’appel à la fonction free() à la fin du programme. Souvenez-vous que vous devez toujours avoir autant de free() que de malloc(). Dans le programme « bugué » ci-dessus, on voit directement l’appel à calloc() mais attention aux allocations cachées : parfois elles peuvent être faites dans une fonction qui n’est pas visible, c’est pourquoi il est important de l’indiquer dans les commentaires de ce genre de fonctions et de dire qu’il faut libérer la mémoire allouée par elles. C’est ce que j’ai fait dans ma solution (la bonne) pour la fonction concat().

Encore une chose : n’oubliez pas de tester le succès de l’allocation dynamique en vérifiant si le pointeur renvoyé par malloc()/calloc()/realloc() n’est pas NULL. Essayez de remplacer tail1+tail2 par 1000000000000 dans l’appel à calloc() pour provoquer un échec d’allocation mémoire.

Suivre

Recevez les nouvelles publications par mail.