Premiers programmes en C

Objectifs
L'objectif de ce TP est d'écrire vos premiers programmes en C pour comprendre les différentes étapes (écriture, compilation, exécution) et interpréter les messages d'erreurs.

Exemple de base

Écrire avec l’éditeur de votre choix le programme suivant (appeler le base.c):

#include <stdio.h>

int main ()
{
    printf ("Hello World !\n");
    return 0;
}

Dans un terminal, taper la commande suivante:

gcc base.c

Normalement vous devez obtenir aucun message (cela signifie que tout s’est bien passé), la compilation a généré un programme dont le nom est a.out (il s’agit du nom par défaut). Vous pouvez donc exécuter le programme compilé via la commande qui donne:

Hello World !

À partir de ce premier exemple, un certain nombre de choses sont à retenir. La démarche usuelle est de:

  1. écrire le code C dans un éditeur
  2. le compiler (gcc), s’il y a des erreurs on retourne à l’étape 1
  3. on l’exécute (./a.out)

Le code C

Sans (trop) rentrer dans les détails de la programmation en C, voici quelques explications #include <stdio.h> est une directive du pré-processeur (elles commencent toutes par #). Ici cela signifie que l’on inclut à notre fichier source, le contenu du fichier <stdio.h>: il s’agit d’un fichier qui contient les fonctions usuelles d’entrée-sortie (saisie au clavier, affichage dans la console…). Sans elle, la fonction printf n’est pas utilisable.

Dans votre fichier base.c, retirer la première ligne. Re-compiler le fichier. Quelle est l’erreur obtenue ? Remettre la directive #include <stdio.h> en début de fichier et constater en tapant gcc -E base.c que votre fichier a été enrichi du fichier stdio.h.

int main()

est le point d’entrée de votre programme, il s’agit de la première fonction qui sera exécutée par votre programme (même si elle se situe en fin de fichier). Un programme doit forcément contenir une fonction main.

printf("Hello World !\n");

printf est la fonction d’affichage du langage C.

return 0; signifie que la fonction main renvoie la valeur 0.

il est possible d’avoir la valeur de retour de votre programme en tapant

echo $?

dans un terminal. Modifier l’entier après return dans votre programme C, compiler, exécuter et vérifier que la valeur de retour est bien modifiée.

La compilation

Il existe de nombreux compilateurs C: ceux disponibles à Polytech Lille sont gcc et clang. Les différences entre gcc et clang sont détaillées à l’adresse https://clang.llvm.org/comparison.html. Pour un IMA3, il est intéressant de constater que les messages d’erreurs sont plus ou moins simples à lire, comme par exemple:

int main ()
{
    printf ("Hello World !\n");
    return 0;
}

donne avec gcc:

base.c: In function ‘main’:
base.c:4:5: warning: implicit declaration of function ‘printf’ [-Wimplicit-function-declaration]
     printf("Hello World ! \n");
     ^~~~~~
base.c:4:5: warning: incompatible implicit declaration of built-in function ‘printf’
base.c:4:5: note: include ‘<stdio.h>’ or provide a declaration of ‘printf’
base.c:1:1:
+#include <stdio.h>

base_e.c:4:5:
     printf("Hello World ! \n");
     ^~~~~~

avec clang:

base.c:4:5: warning: implicitly declaring library function 'printf' with type 'int (const char *, ...)' [-Wimplicit-function-declaration]
    printf("Hello World ! \n");
    ^
base.c:4:5: note: include the header <stdio.h> or explicitly provide a declaration for 'printf'
1 warning generated.

même si les deux donnent la même information importante (include stdio.h), l’un est plus verbeux que l’autre. Cependant les différences entre les compilateurs peuvent être beaucoup plus importantes. Soit par exemple le fichier suivant (compil.c):

#include <stdio.h>

void loop_example ()
{
    int i;
    while (i < 10)
    {
        printf ("%s(%d) -- Hello World (%i)! \n", __func__, __LINE__, i);
        i++;
    }
    printf ("i == %i\n", i);
}

int main ()
{
    int i;
    while (i < 10)
    {
        printf ("%s(%d) -- Hello World (%i)! \n", __func__, __LINE__, i);
        i++;
    }
    printf ("i == %i\n", i);
    loop_example ();
    return 0;
}

Sans trop rentrer dans les détails, on peut remarquer que la suite d’instructions suivante est recopiée deux fois

int i;
while (i < 10)
{
    printf("%s(%d) -- Hello World (%i)! \n", __func__, __LINE__, i);
    i++;
}
printf("i == %i\n", i);

la première fois dans le main est la seconde fois dans une fonction qui s’appelle loop_example. Compiler le programme et exécuter le. Vous devez normalement constater que l’affichage n’est pas celui attendu:

main(19) -- Hello World (0)!
main(19) -- Hello World (1)!
main(19) -- Hello World (2)!
main(19) -- Hello World (3)!
main(19) -- Hello World (4)!
main(19) -- Hello World (5)!
main(19) -- Hello World (6)!
main(19) -- Hello World (7)!
main(19) -- Hello World (8)!
main(19) -- Hello World (9)!
i == 10
i == 32752

D’une part parce que les messages Hello World de la fonction ne sont pas affichés et d’autre part parce que la valeur de i est incohérente… et en plus elle change à chaque exécution (faites l’essai pour vérifier). C’est lié au fait que la variable i n’est pas initialisée et donc elle peut prendre n’importe quelle valeur1. Pourtant cette erreur n’est ni détectée par gcc, ni par clang (à vérifier par vous-mêmes). Cependant si l’on ajoute les options de détection des alertes -W -Wall à clang via la commande

clang -W -Wall compil.c

on obtient les messages suivants:

compil.c:6:12: warning: variable 'i' is uninitialized when used here [-Wuninitialized]
    while (i < 10)
           ^
compil.c:5:10: note: initialize the variable 'i' to silence this warning
    int i;
         ^
          = 0
compil.c:17:12: warning: variable 'i' is uninitialized when used here [-Wuninitialized]
    while (i < 10)
           ^
compil.c:16:10: note: initialize the variable 'i' to silence this warning
    int i;
         ^
          = 0
2 warnings generated.

Le fait que i ne soit pas initialisé est cette-fois ci bien indiqué (et ce pour les deux lignes concernées). Vérfier que malgré les options -W -Wall, gcc ne détecte pas ces alertes. Corriger le programme et vérifier que l’exécution est conforme à ce qui est attendu à savoir

main(19) -- Hello World (0)!
main(19) -- Hello World (1)!
main(19) -- Hello World (2)!
main(19) -- Hello World (3)!
main(19) -- Hello World (4)!
main(19) -- Hello World (5)!
main(19) -- Hello World (6)!
main(19) -- Hello World (7)!
main(19) -- Hello World (8)!
main(19) -- Hello World (9)!
i == 10
loop_example(8) -- Hello World (0)!
loop_example(8) -- Hello World (1)!
loop_example(8) -- Hello World (2)!
loop_example(8) -- Hello World (3)!
loop_example(8) -- Hello World (4)!
loop_example(8) -- Hello World (5)!
loop_example(8) -- Hello World (6)!
loop_example(8) -- Hello World (7)!
loop_example(8) -- Hello World (8)!
loop_example(8) -- Hello World (9)!
i == 10

Vous pouvez constater qu’il est possible d’afficher la fonction courante d’un programme via __func__ et de la ligne du programme via __LINE__.

Enfin quel que soit le compilateur chois, il est possible de modifier le nom du programme générée en utilisant l’option -o. Par exemple si on souhaite que notre programme s’appelle edinfo, il suffit de taper (cela fonctionne aussi avec gcc)

clang -W -Wall compil.c -o edinfo
Ce qu'il faut retenir
  • Compiler toujours vos programmes avec les options de détections -W -Wall
  • Il faut toujours initialiser vos variables avant de les utiliser

Les redirections

De la même manière que pour les commandes Linux, vous pouvez rediriger les entrées-sorties. Par exemple en utilisant > suivi d’un nom de fichier, tous les messages affichés par votre programme seront écrits dans le fichier et non dans le console. Essayer avec le fichier compil.c.

Pour la redirection en entrée, écrire / compiler / exécuter le programme suivant:

#include <stdio.h> // également obligatoire pour scanf

int main ()
{
    int   x;
    char  c;
    float f;

    printf ("Donnez un entier, un caractère et un réel :\n");
    scanf ("%d %c %f", &x, &c, &f); // x vaut la valeur entrée au clavier

    printf ("Valeurs entrées: %d %c %f\n", x, c, f);

    return 0;
}

Maintenant écrire dans un fichier (test_scanf.txt par exemple), les valeurs 5 a 3.14 Exécuter le programme via ./a.out < test_scanf.txt. Constater que vous n’avez plus besoin d’entrer de valeurs au clavier mais que tout est lu directement dans le fichier.

Votre premier programme

À partir des programmes précédents et de l’aide éventuelle sur printf (man 3 printf) et sur scanf (man 3 scanf), écrire un programme qui lit 10 nombres au clavier (ou dans un fichier via une redirection) et qui en affiche la moyenne à l’écran (ou dans un fichier via une redirection). Regarder les erreurs générées quand le format n’est pas bon (par exemple un caractère à la place d’un nombre) ou quand le nombre d’éléments attendus n’est pas bon (moins de 10 valeurs).

  1. le premier qui trouve pourquoi i est correctement initialisé à 0 dans le main gagne un bonus