CM #2 : Programmes en C
Objectifs
Dans ce cours, nous allons définir
  • Ecrire un programme C
  • Utiliser les entrées-sorties avec printf et scanf
  • Rediriger les entrées-sorties standard

Syntaxe général d’un programme

Un programme simple comprend:

  • une liste optionnelle d’inclusions de fichiers (qui contiennent des déclarations de fonctions)
  • une liste optionnelle de déclarations de types, de structures, de variables de fonctions
  • une fonction main, unique et obligatoire qui est le point d’entrée du programme (i.e. quel que soit le programme, l’exécution commence forcément à la fonction main).

En C, un exemple de programme simple:

#include <stdio.h> // Nécessaire pour imprimer quelque chose à l'écran

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

En C, main est une fonction qui fournit un code de retour:

  • qui est de type int (entier)
  • qui, par convention, vaut 0 si tout s’est bien passé
  • qui est exploitable dans le terminal: il est possible d’afficher sa valeur via la commande echo $?
jdequidt@weppes:~$ clang -Wall -o simple simple.c
jdequidt@weppes:~$ .a/out
bonjour !
jdequidt@weppes:~$ echo $?
0
jdequidt@weppes:~$
\end{verbatim}

Anatomie d’un exemple

Voici un exemple de programme simple:

#include <stdio.h>

#define N 125

int main ()
{
    int somme = 0;
    int i     = N;

    while (0 >= i)
    {
        somme += i;
        i--;
    }

    printf ("La somme des entiers de 1 à %d est %d\n", N, somme);

    return 0;
}

Sur ce programme, un certain nombre de choses sont importantes:

  • main: voir précédemment, il est obligatoire
  • une constante N est définie avant le main
  • la fonction printf qui permet d’écrire à l’écran. Cependant ça n’est pas une fonction de base de C et doit par conséquent sa définition doit être incluse pour que le compilateur puisse compiler le programme. L’inclusion se fait via #include <stdio.h>, stdio.h (standard input-output) étant un fichier qui centralise les fonctions / types / variables utiles pour récupérer des données au clavier et afficher à l’écran. La fonction printf est détaillée dans la section suivante.

Entrées - sorties

printf et scanf sont deux fonctions que vous utiliserez énormément. La première permet d’afficher messages et données à l’écran tandis que la deuxième permet de demander des informations à l’utilisateur (et donc de récupérer ce qui est entré au clavier).

printf

La syntaxe générale de printf est un peu complexe et donc nous ne la détaillerons pas tout de suite. Son utilisation sur un programme simple est la suivante:

#include <stdio.h> // Obligatoire !

int main ()
{
    // Format simple
    printf ("Ceci est un message simple.");
    printf ("Ceci est un message avec retour à la ligne.\n");
    printf ("\tCeci est un message avec une tabulation.");

    // Format avec arguments entiers
    int x = 2, y = 3, z;
    printf ("\nMessage: (x = %d).\n", x);
    printf ("Message: (x = %d, y = %d).\n", x, y);
    printf ("Message: (x = %d, y = %d, somme = %d).\n", x, y, x + y);
    printf ("\nMessage: (z = %d).\n", z); // ERREUR classique ;)

    // Format avec autres arguments
    float  f = 0.1f;
    double d = 0.1;
    char   c = 'a';
    printf ("%c - %s - %f - %lf\n", c, "bonjour !", f, d);

    return 0;
}

Ce programme est compilé puis exécuté:

jdequidt@weppes:~$ clang -Wall -c print.c
jdequidt@weppes:~$ ./a.out
Ceci est un message simple.Ceci est un message avec retour à la ligne.
  Ceci est un message avec une tabulation.
Message: (x = 2).
Message: (x = 2, y = 3).
Message: (x = 2, y = 3, somme = 5).

Message: (z = 1570477600).
a - bonjour ! - 0.100000 - 0.100000
jdequidt@weppes:~$

Les %d, %c, %f… sont des chaînes de formatage et permettent d’afficher les valeurs de vos variables, elles commencent nécessairement par le caractère %. La liste complète des chaînes de formatage se trouve ci-dessous:

%d /* entier signé décimal */
%i /* entier signé décimal */
%u /* entier non-signé décimal */
%o /* entier non-signé octal, sans préfixe 0 */
%x /* entier non-signé hexadécimal sans préfixe 0x, 013245679abcdef */
%X /* entier non-signé hexadécimal sans préfixe 0x, 0123456789ABCDEF */

%c /* caractère */
%s /* chaîne de caractères terminée par \0 */

%f /* double notation [-]mm.dd */
%e /* double notation scientifique [-]m.dde[+/-]xx */
%E /* double notation scientifique [-]m.ddE[+/-]xx */
%g /* double notation automatique %e (si exposant < -4) ou %f */
%G /* double notation automatique %E (si exposant < -4) ou %f */

%p /* pointeur */
%% /* pour afficher le caractère % */
Remarque
Il n'est pas nécessaire de mémoriser toutes les chaînes de formatage. Par contre il est important de se rappeler de deux choses:
  • il doit y avoir autant de chaînes de formatage que de variables que l'on veut afficher (même si elles ont toutes le même type).
  • Toutes les données sont stockées en binaire et les opérations se font en binaire, la conversion n'est faite que pour l'affichage en utilisant les chaînes de formatage.

Modificateurs

Remarque
Il n'est absolument pas nécessaire de retenir cette partie du cours. Il faut juste se rappeler de son existence pour retrouver rapidement les informations si besoin.

Il est également possible de modifier la mise en forme de l’affichage avec un certain nombre de modificateurs et la syntaxe générale devient: %(fl)(ent)(.)(pre)(long)type où type est l’une des valeurs indiquées précédemment (d, i, u…). Les modificateurs sont indiqués entre parenthèses car ils sont optionnels.

  • fl: permet de modifier l’alignement: - impose une justification à gauche; + ajout un signe + pour les nombres positifs, un espace pour les nombres sans signe; 0 permet d’ajouter des 0 en préfixe; # active la forme secondaire pour l’affichage (préfixe sur les formes octales et hexadécimales…)
  • ent est un entier qui indique en caractères la taille minimum du formatage
  • pre est un entier qui indique la précision (i.e. le nombre de chiffres après la virgule)
  • long est un modificateur de longueur, vaut soit h (pour les types short) ou l (pour les types long ou double).

Voici quelques exemples:

#include <stdio.h>

int main ()
{
    printf ("%5d\n", 123);    // 5 chiffres mini
    printf ("%*d\n", 8, 123); // 8 chiffres mini

    printf ("-------------------\n");

    printf ("%+4d\n", 123);  // 4 chiffres mini et signe
    printf ("%+4d\n", -456); // 4 chiffres mini et signe
    printf ("%+4d\n", 0);    // 4 chiffres mini et signe
    printf ("%04d\n", 0);    // 4 chiffres mini et remplissage avec 0

    printf ("-------------------\n");

    printf ("%x\n", 123);  // affichage octal
    printf ("%#x\n", 123); // affichage octal, prefixe 0
    printf ("%o\n", 123);  // affichage hexa
    printf ("%#o\n", 123); // affichage hexa, prefixe 0x

    printf ("-------------------\n");

    printf ("%f\n", 123.456789);
    printf ("%5.2f\n", 123.456789); // 2 chiffres
    // après la virgule (5 chiffres mini)

    return 0;
}

Programme que l’on compile puis que l’on exécute.

jdequidt@weppes:~$ clang -Wall -c print2.c
jdequidt@weppes:~$ ./a.out
  123
     123
***
+123
-456
  +0
0000
***
7b
0x7b
173
0173
***
123.456789
123.46
jdequidt@weppes:~$

scanf

La syntaxe est proche de printf puisqu’elle utilise également les chaînes de formatage. Ci-dessous, un programme qui illustre son utilisation:

#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;
}

Ce programme est compilé puis exécuté:

jdequidt@weppes:~$ clang -c scan.c -Wall
jdequidt@weppes:~$ ./a.out
Donnez un entier, un caractère et un réel :
4 f 8.956
Valeurs entrées: 4 f 8.956000
jdequidt@weppes:~$

Plusieurs choses importantes sont à retenir:

Important
Chacune des variables est précédée par un &. Cette syntaxe correspond à un pointeur et sera vue ultérieurement. L'utilisation de scanf n'impose pas de connaître les pointeurs mais il faut penser à ajouter ce caractère.

En général, le compilateur vous indique qu’il y a une erreur possible via un warning. Dans l’exemple précédent, en supprimant le & devant le f de l’appel à scanf, le compilateur affiche le message suivant:

jdequidt@weppes:~$ clang -c scan.c -Wall
scan.c:10:16: warning: format specifies type 'float *' but the
argument has type 'double' [-Wformat]
  scanf("%d %c %f", &x, &c, f); // x vaut la valeur entrée au clavier
                     ~^           ~
1 warning generated.
jdequidt@weppes:~$
Important
La chaîne de caractères (entre les ") impose la manière dont l'utilisateur doit fournir les données. Ici, elles sont simplement séparées par un espace. Tout autre format donné par l'utilisateur induit un résultat non-conforme.

Par exemple, si l’utilisateur utilise la virgule , comme séparateur alors le programme ne fonctionne pas comme attendu:

jdequidt@weppes:~$ ./a.out
Donnez un entier, un caractère et un réel :
0,c,4.6
Valeurs entrées: 0 , 8234257612800.000000
jdequidt@weppes:~$

Les caractères et scanf

Soit le programme suivant qui demande 3 caractères à l’utilisateur:

#include <stdio.h>

int main ()
{
    char x, y, z;

    printf ("Donnez 1 caractère :\n");
    scanf ("%c", &x);
    printf ("Donnez 1 caractère :\n");
    scanf ("%c", &y);
    printf ("Donnez 1 caractère :\n");
    scanf ("%c", &z);

    printf ("Valeurs entrées: x = %c, y = %c, z = %c\n", x, y, z);

    return 0;
}

que l’on compile puis exécute:

jdequidt@weppes:~$ clang -Wall 007_scanf_buffer.c
jdequidt@weppes:~$ ./a.out
Donnez 1 caractère :
r
Donnez 1 caractère :
Donnez 1 caractère :
y
Valeurs entrées: x = r, y =
, z = y
jdequidt@weppes:~$

On constate que le programme ne fonctionne pas correctement: la demande du deuxième caractère est zappée (même si le message du printf est affiché à l’écran) et du coup on ne peut rentrer que deux valeurs. Ceci s’explique par le fait que le retour à la ligne \n est un caractère (code ASCII égal à 10) et il est par conséquent scanné par le deuxième appel de scanf ce qui est confirmé par le fait que dans le terminal on voit y = puis un retour à la ligne. Par conséquent il faut être particulièrement vigilant lorsque l’on scanne des caractères. Une solution (pas très élégante) est de modifier les deux derniers appels à scanf en ajoutant un retour à la ligne (i.e. scanf("\n%c", &y);).

Ce problème est présent uniquement lorsque l’on scanne des caractères. Les espaces et retours à la ligne sont automatiquement supprimés par scanf lorsque l’on scanne des données d’autres types.

Redirections

Les redirections sont des moyens simples de rediriger des données depuis/vers des fichiers. Les 3 entrées/sorties standard sont:

  • stdin (0): entrée standard qui correspond à ce qui est tapé au clavier
  • stdout (1): sortie standard qui correspond à ce qui est affiché à l’écran
  • stderr (2): erreur standard qui affiche les message d’erreurs (par défaut à l’écran)

La syntaxe générale d’une redirection est la suivante: supposons l’existence de commande qui est une commande système ou un programme.

  • commande < fichier permet de rediriger l’entrée standard. Ceci signifie que les valeurs que tape habituellement l’utilisateur au clavier seront directement lues dans fichier.
  • commande > fichier permet de rediriger la sortie standard. Tous les messages habituellement affichés à l’écran seront écrits dans fichier. Attention: si fichier existait déjà, son contenu est écrasé.
  • commande >> fichier permet de rediriger la sortie standard. Tous les messages habituellement affichés à l’écran seront écrits dans fichier. Attention: si fichier existait déjà, les messages de commande sont ajoutés à la fin du fichier.
  • commande 2> fichier permet de rediriger l’erreur standard, son fonctionnement est le même que pour la sortie standard.

Bien entendu ces commandes peuvent être combinées. Reprenons le programme de la section précédente. Soit fichier1 qui contient 9 z 3.14156, il est possible de faire:

jdequidt@weppes:~$ clang -c scan.c -Wall
jdequidt@weppes:~$ ./a.out < fichier1 > fichier2
jdequidt@weppes:~$

On remarque que l’exécution de notre programme avec les redirections n’affiche absolument rien à l’écran. On peut regarder le contenu de fichier2 pour vérifier que la redirection a bien fonctionné:

jdequidt@weppes:~$ cat fichier2
Donnez un entier, un caractère et un réel :
Valeurs entrées: 9 z 3.14156
jdequidt@weppes:~$
Remarque
la commande cat permet, entre autres, d'afficher le contenu d'un fichier. Vous pouvez également utiliser la commande less.

Précisions sur les entrées-sorties

Note
Le cas des entrées-sorties sera abordée de manière plus complète en cours de Programmation Avancée (S6) et en Systèmes (S7). Ici, un problème courant est mis en évidence et une solution simple est proposée.

Les entrées-sorties ne sont pas immédiates mais sont stockées dans un tampon (buffer). En effet les opérations qui envoient des données depuis / vers un périphérique vers / depuis la mémoire de l’ordinateur sont très coûteuses en temps de calcul. Par conséquent, les instructions du style printf et scanf sont exécutées et leurs résultats sont stockés dans le tampon jusqu’à ce que celui soit plein. Une fois plein, le tampon est vidé vers l’écran (dans le cas d’un printf) ou depuis le clavier (dans le cas d’un scanf). Ceci permet de minimiser les échanges entre mémoires et périphériques.

#include <stdio.h>
#include <unistd.h> // pour utiliser sleep

int main ()
{
    int i = 0;
    while (i < 10)
    {
        printf ("%c", '.');
        fflush (stdout);
        sleep (1); // le programme s'endort 1 seconde
        i++;
    }
    printf ("\n");

    return 0;
}

Un moyen simple de constater la présence de tampons est de compiler et d’exécuter le programme ci-dessus dont le principe est très simple. On souhaite afficher un point '.' à l’écran toutes les secondes. Pourtant à l’exécution on constate que le programme ne fonctionne pas de manière attendue: rien n’est affiché à l’écran pendant 10 secondes puis les 10 '.' sont affichés d’une traite. Ceci s’explique par le fait que les caractères sont stockés dans le tampon et comme il n’est pas plein, il n’est pas transféré à l’écran. Quand le programme se termine, tous les tampons sont vidés et donc l’affichage à l’écran est effectué. Il est possible de forcer le vidage des tampons d’entrées-sorties avec la fonction fflush. Par exemple fflush(stdout) permet de vider le tampon de la sortie standard et fflush(stdin) permet de vider le tampon de l’entrée standard.