Le langage C

Nous allons tremper un premier orteil dans le célèbre langage C, encore largement utilisé aujourd'hui pour ses performances (par exemple pour la programmation des microcontrôleurs en robotique). Ce billet donne une introduction au langage pour nous familiariser avec les concepts essentiels, qui sont communs à la plupart des langages de programmation.

Le langage C a été créé par Dennis Ritchie en parallèle de l'invention du système d'exploitation Unix (ancêtre de Linux) avec Ken Thompson et Brian Kernighan. Depuis sa création, le C a connu un essor fulgurant, ne prenant que relativement de l'âge grâce à sa simplicité et à ses hautes performances qui en font un langage de choix par exemple pour les applications embarquées (robots, voitures, ...). En 2021, le langage est encore actif, avec un prochain standard C2x prévu pour 2023.

Ce billet couvre les concepts majeurs du langage C pour coder vos propres applications et jeux vidéos, en mode console pour commencer. Il nous faudra pour cela un bon compilateur, par gcc sur Linux, qui s'installe directement sous Ubuntu par sudo apt install gcc. Le compilateur nous permettra plus tard de profiter des bibliothèques graphiques comme SDL ou OpenGL.

Premier programme

Commencons par notre premier programme : afficher le message "Hello World!", un grand classique en programmation ! Ce programme est un mythe, les grands programmeurs sont également passés par là, vous ne pouvez donc pas y échapper ;) Examinons le code source suivant :

#include <stdio.h>

int main() {
   printf("Hello World !\n");
   printf("Appuyez sur [ENTREE] pour continuer.\n");
   getchar();
   return 0;
}

La première ligne du programme est indispensable : elle sert à "inclure" des fonctions de base ("stdio" pour "standard input/output", entrées et sorties standard) pour les utiliser dans notre programme. Une fonction, dans ce langage, est assez proche d'une fonction mathématique : c'est une suite d'acions finies sur les données passées en argument. Ici int main() déclare la fonction principale, le point de départ du programme. En C, on indique le début et la fin d'une fonction par des accolades '{' et '}'. La fonction main (pour "main function" soit "fonction principale" en anglais) se termine donc après la dernière instruction return 0; avant l'accolade fermante. Le contenu entre les accolades s'appelle un bloc de code.

Syntaxe du langage

À la fin de chaque ligne, nous mettons des points-virgules : ils sont indispensables. En effet, le C interprête les points-virgules comme la fin d'une instruction et il ne tient pas compte des espaces ou des retours à la ligne. La fonction printf sert à écrire du texte : elle prend en argument une chaîne de caractère (du texte entre guillemets) suivie d'un nombre indéfini de variables. Pour écrire du texte à l'écran, on entre donc "printf", on ouvre des parenthèses et des guillemets, puis on entre le texte, après quoi on referme les guillemets et les parenthèses pour terminer la ligne par un point-virgule. Il existe aussi des caractères spéciaux que nous pouvons pour utiliser entre guillemets, par exemple :

Caractère Effet
'\n' Retour à la ligne (entrée)
'\t' Tabulation

Nous reviendrons sur ces caractères spéciaux plus loin. Un programme se termine quand toutes les instructions de la fonction principale ont été exécutées. Donc, pour l'instant, le programme se ferme dès que le texte est affiché, et on a pas le temps de voir. Il font donc faire une "pause" une fois le texte affichée. On écrit donc une phrase indiquant qu'il faut appuyer sur une touche pour terminer le programme, puis on entre l'instruction getchar(). Cette instruction attend que l'utilisateur appuie sur une touche : plus tard, nous récupèrerons la valeur de cette touche. Pour l'instant, elle sert juste à faire une pause jusqu'à ce qu'une touche soit pressée. On termine toujours la fonction principale par return 0;, qui sert à indiquer au système que tout s'est déroulé sans problème.

L'instruction return indique la valeur de retour d'une fonction, c'est à dire les données qu'elle associe aux arguments passés, comme en mathématiques où f(x)=1/x\def\bfA{\boldsymbol{A}} \def\bfB{\boldsymbol{B}} \def\bfC{\boldsymbol{C}} \def\bfD{\boldsymbol{D}} \def\bfE{\boldsymbol{E}} \def\bfF{\boldsymbol{F}} \def\bfG{\boldsymbol{G}} \def\bfH{\boldsymbol{H}} \def\bfI{\boldsymbol{I}} \def\bfJ{\boldsymbol{J}} \def\bfK{\boldsymbol{K}} \def\bfL{\boldsymbol{L}} \def\bfM{\boldsymbol{M}} \def\bfN{\boldsymbol{N}} \def\bfO{\boldsymbol{O}} \def\bfP{\boldsymbol{P}} \def\bfQ{\boldsymbol{Q}} \def\bfR{\boldsymbol{R}} \def\bfS{\boldsymbol{S}} \def\bfT{\boldsymbol{T}} \def\bfU{\boldsymbol{U}} \def\bfV{\boldsymbol{V}} \def\bfW{\boldsymbol{W}} \def\bfX{\boldsymbol{X}} \def\bfY{\boldsymbol{Y}} \def\bfZ{\boldsymbol{Z}} \def\bfalpha{\boldsymbol{\alpha}} \def\bfa{\boldsymbol{a}} \def\bfbeta{\boldsymbol{\beta}} \def\bfb{\boldsymbol{b}} \def\bfcd{\dot{\bfc}} \def\bfchi{\boldsymbol{\chi}} \def\bfc{\boldsymbol{c}} \def\bfd{\boldsymbol{d}} \def\bfe{\boldsymbol{e}} \def\bff{\boldsymbol{f}} \def\bfgamma{\boldsymbol{\gamma}} \def\bfg{\boldsymbol{g}} \def\bfh{\boldsymbol{h}} \def\bfi{\boldsymbol{i}} \def\bfj{\boldsymbol{j}} \def\bfk{\boldsymbol{k}} \def\bflambda{\boldsymbol{\lambda}} \def\bfl{\boldsymbol{l}} \def\bfm{\boldsymbol{m}} \def\bfn{\boldsymbol{n}} \def\bfomega{\boldsymbol{\omega}} \def\bfone{\boldsymbol{1}} \def\bfo{\boldsymbol{o}} \def\bfpdd{\ddot{\bfp}} \def\bfpd{\dot{\bfp}} \def\bfphi{\boldsymbol{\phi}} \def\bfp{\boldsymbol{p}} \def\bfq{\boldsymbol{q}} \def\bfr{\boldsymbol{r}} \def\bfsigma{\boldsymbol{\sigma}} \def\bfs{\boldsymbol{s}} \def\bftau{\boldsymbol{\tau}} \def\bftheta{\boldsymbol{\theta}} \def\bft{\boldsymbol{t}} \def\bfu{\boldsymbol{u}} \def\bfv{\boldsymbol{v}} \def\bfw{\boldsymbol{w}} \def\bfxi{\boldsymbol{\xi}} \def\bfx{\boldsymbol{x}} \def\bfy{\boldsymbol{y}} \def\bfzero{\boldsymbol{0}} \def\bfz{\boldsymbol{z}} \def\defeq{\stackrel{\mathrm{def}}{=}} \def\p{\boldsymbol{p}} \def\qdd{\ddot{\bfq}} \def\qd{\dot{\bfq}} \def\q{\boldsymbol{q}} \def\xd{\dot{x}} \def\yd{\dot{y}} \def\zd{\dot{z}} f(x) = 1/x a pour valeur de retour l'inverse de x. On ferme l'accolade pour terminer notre fonction main. Allez-y, compilez et admirez votre premier programme. C'est un début qui nous mènera loin :) Sachez cependant que le C ne supporte aucun accent en mode console (il faut pour cela effectuer un petit détournement que nous ne verrons pas ici). Commentaires

Dans vos fichiers sources, tout caractère est considéré comme du code C, à moins de préciser au compilateur le contraire. En fait, il est possible de "commenter" certaines parties du fichier source, ce qui veut dire que le texte choisi sera ignoré par le compilateur. Cela sert à donner des indications aux autres développeurs, mais aussi à se rappeler rapidemment l'utilité de telle ou telle portion de code. Par exemple, les sources fournis dans ce cours sont tous commentés afin de faciliter la compréhension du lecteur ;)

Commentaires

Pour commenter du code en C, il existe deux moyens. Le premier est celui recommandé pour rester compatible avec les anciennes versions du langage : tout code situé entre /* et */ dans le code est ignoré. Attention : le début d'un commentaire ne peut être donné qu'en-dehors de guillemets. Ce type de commentaire est "multi-ligne", car tout un paragraphe peut être commenté.

/*
 * Exemple de commentaire
 * multi-ligne en C
 */

Par ailleurs, il existe une seconde manière de commenter du code : tout texte situé après les caractères // sera ignoré pour toute la ligne, ce qui signifie que le commentaire s'étendra du double-slash jusqu'à la fin de la ligne. Idem, le double-slash doit être placé en-dehors de guillemets.

// Exemple de commentaire sur une ligne

Blocs

Le code C est organisé en blocs, un bloc étant le code compris entre une accolade ouvrante { et une accolade fermante }. Tout code entre ces deux caractères fait partie du bloc. Ainsi, les instructions d'une fonction comme la fonction principale sont comprises dans un bloc, limité par les accolades ouvrante et fermante. Mais la notion de bloc ne se limite pas à ces accolades, car les blocs peuvent être "imbriqués" : ainsi, nous pouvons insérer un bloc dans un autre, obtenant un code du style :

int main() {
    {
        {
            // instructions
        }
        {
            // instructions
        }
    }
    {
        // instructions
    }
}

Attention : les blocs sont ouverts et fermés dans le même ordre, c'est-à-dire qu'un bloc ouvert dans un bloc sera égalemment fermé dans celui-ci. On peut considérer un bloc comme "descendant" d'un autre s'il est ouvert (et donc fermé) dans ce dernier, qui est alors son "parent". Cette notion de parentée est importante, entre autre, afin de disposer de variables, comme nous allons le voir plus loin. Les fonctions et certaines autres instructions du langage nécessitent égalemment un bloc contenant le code associé.

Variables

Nous allons poursuivre notre exploration du langage par les variables. Là encore, le concept est assez proche des variables mathématiques. En C cependant, les variables sont "typées", c'est à dire qu'une variable ne peut contenir qu'un certain type de données, les types étant les nombres entiers, flottants, décimaux, les caractères ou encore les booléens (vrai/faux). Plus précisément, les variables sont des entitées nommées auxquelles nous pouvons associer les valeurs que nous voulons, tant que nous respectons leur type. Ces valeurs peuvent évoluer, d'où le nom "variable", à l'opposé des "constantes" qui sont des valeurs invariables dans votre code. Par exemple, 1 et 8.42 sont des constantes. Voici un bref aperçu des types de variables standards :

Type Description Bornes
bool Variable booléenne, "vraie" ou "fausse", 1 ou 0. true ou false
char Désigne un caractère, peut être utilisé comme entier (8 bits) [-128; 127]
unsigned char Désigne un caractère, peut être utilisé comme entier positif (8 bits) [0; 255]
short int Nombre entier (16 bits) [-32 768 ; 32 767]
unsigned short int Nombre entier positif (16 bits) [0 ; 65 535]
int Nombre entier (integer value = nombre entier) (32 bits) [-2^31 ; (2^31-1)]
unsigned int Nombre entier (32 bits) [0 ; (2^32-1)]
long Entier long, peut prendre des valeurs plus importantes qu'un int. Dépend du compilateur (32 bits) [-2^31 ; (2^31-1)]
unsigned long Entier long, peut prendre des valeurs plus importantes qu'un int. Dépend du compilateur (32 bits) [0 ; (2^32-1)]
float Réel à virgule flottante (32 bits) [3.4e-38 ; 3.4e+38]
double Réel à virgule flottante (64 bits) [1.7e-308 ; 1.7e+308]
long double Réel à virgule flottante (80 bits) [3.4e-4932 ; 1.1e+4932]

Initialisation

Vous remarquerez que les types du C ont des bornes, c'est à dire que les variables d'un type précis ne peuvent prendre que les valeurs comprisent entre la borne inférieure et la borne supérieure du type. A noter aussi que vous pouvez ajouter signed ou unsigned avant le type de votre variable : signed est l'option par défaut, et indique que celle-ci sera signée, c'est-à-dire qu'elle pourra prendre des valeurs négatives et positives. A l'inverse, une variable non-signée est toujours positive, ce qui lui permet d'accepter des valeurs extrèmes plus élevées. Par exemple, une variable de type unsigned int pourra prendre des valeurs de [0; 10^32]. La syntaxe pour déclarer une variable est la suivante : type nom_de_variable;. Ainsi, voici comment créer une variable 'dix' :

int dix;

La variable dix ainsi déclarée, on peut lui affecter n'importe quelles valeurs entières comprises entre les bornes du type 'int'. Il est possible de déclarer plusieures variables d'un même type sur une même ligne de code, en séparant leurs noms par des virgules. Toutes les variables disposent de leurs propres opérateurs, c'est-à-dire des symboles permettant de modifier la variable ; ainsi, '=' est appelé "opérateur d'égalité", et permet d'affecter une valeur à une variable. Nous pouvons affecter une valeur à une variable n'importe quand, y compris dès sa déclaration :

int dix = 10, onze;
dix = 11;  // le compilateur ne nous le reprochera pas ;)
onze = dix;
dix = 10;

Opérations sur les variables

Nous avons désormais des variables, reste à les faire évoluer. Pour ce faire, le C propose des opérations primaires, correspondants aux principales opérations mathématiques (addition, soustraction...) jusqu'à des théories plus complexes (voir l'en-tête math.h). Plus tard, vous serez à même d'écrire vos propres fonctions pour manipuler ces variables, mais pour l'instant l'essentiel est d'apprendre à utiliser les opérateurs dits binaires. Ceux-ci se nomment ainsi car ils nécessitent deux paramètres, l'un étant votre variable, l'autre la valeur à manipuler. Par exemple, variable = 2; utilise l'opérateur binaire '=', qui affecte la valeur 2 à votre variable. Rien de bien difficile à ce niveau, les principales opérations arithmétiques s'utilisant de la même manière que dans la vie courrante. Voici la liste des principaux opérateurs binaires, suivie d'un code source d'exemple :

Opérateur Résultat
= Affecte une valeur à une variable. Celle-ci doit être à gauche de l'opérateur.
+ Retourne la somme des deux paramètres.
- Retourne la différence des deux paramètres.
* Retourne le produit des deux paramètres.
/ Retourne le quotient des deux paramètres.
% Retourne le modulo des deux paramètres (c'est-à-dire le reste de la division euclidienne du premier par le second).
unsigned int x;
x = 2;                 // affecte la valeur 2 à x
x = 2 + 2;             // après cette instruction x vaut 4
x = (2 + 1) * (4 / 2); // maintenant x vaut 6
x = x - 2;             // après cette instruction x vaut 4 à nouveau
x = 8 / x;             // maintenant x vaut 2

Ces opérations s'effectuent exactement de la même manière qu'en mathématiques, et ne présentent donc pas de réelle difficulté. Idem pour les parenthèses. Comme vous pouvez le remarquer, nous avons également utilisé x à droite de l'opérateur '=' : dans ce cas, c'est la valeur de x qui est évaluée, et la dernière opération de l'exemple ci-dessus revient au même que d'écrire x = (8/4); puisque la valeur de x avant l'opération était 4. Il peut être fastidieux de toujours avoir à utiliser l'opérateur d'égalité pour modifier une variable, aussi existe-t-il d'autres opérateurs permettant de clarifier et d'alléger la rédaction du code.

Incrémentation et décrémentation

Le sens de ces deux termes est simple : incrémenter une variable de K signifie ajouter la valeur K à la variable, tandis que décrémenter une variable de K signifie soustraire la valeur K à cette variable. Il existe des opérateurs unaires (c'est-à-dire qui ne prennent qu'un seul paramètre) qui incrémentent ou décrémentent de 1, par exemple l'opérateur ++ : l'instruction foo++; incrémente la variable foo de 1. Cet opérateur est dit unaire car il ne prend qu'un seul paramètre : la variable à incrémenter. A l'opposée, il existe l'opérateur '--'. Ce sont les seuls opérateurs unaires qui seront abordés dans ce cours, mais ils sont relativement important pour pouvoir utiliser des boucles.

int i = 12;              // variable i = 12
printf("i = %d\n" , i);  // affichage de i
i++;                     // maintenant i = 13
printf("i = %d" , i);    // affichage de i

Voici le résultat à l'écran de ce bloc de code :

i = 12
i = 13

Dans notre exemple i++; est une post-incrémentation. Nous aurions aussi pu écrire i--; pour une post-décrémentation. Cette syntaxe vous évite d'écrire à chaque fois i = (i + 1);, ce qui est déjà un gain de temps considérable. Il existe aussi des opérateurs de pré-décrémentation et de pré-incrémentation. Leur intérêt intervient au niveau de l'évaluation : nous avons vu plus haut que la valeur d'une variable est évaluée si, par exemple, elle se situe à droite de l'opérateur d'égalité. Utiliser une post-incrémentation dans une portion de code où la variable est évaluée ajoutera 1 à celle-ci une fois qu'elle aura été évaluée, tandis qu'utiliser une pré-incrémentation ajoutera 1 à la variable avant l'évaluation. Le code suivant illustre ce cas de figure :

unsigned int i = 1, x;

x = (i++);
/* Ici, x = 1 et i = 2 */

x = (++i);
/* Ici, x = 3 et i = 3 */

Il existe d'autres opérateurs d'incrémentation et de décrémentation, binaire ceux-ci : il s'agit de +=, -=, *= et /=, qui correspondent aux quatre opérations arithmétiques de base. La syntaxe à utiliser est la même que pour l'opérateur =, c'est-à-dire la variable à gauche et la valeur à droite :

i += 5; /* iden que i = (i+5); */
i -= 5; /* idem que i = (i-5); */

i = 2;
i *= (++i);
/* Ici, i = 9 */

Maîtriser ces principaux opérateurs est essentiel, d'une part pour se familiariser avec la syntaxe du langage, mais surtout parceque vous les utiliserez à outrance.

Affichage

Vous pouvez avoir besoin de consulter le contenu de vos variables durant le programme. Plus tard, vous serez à même de les manipuler à l'écran ou dans un fichier, mais pour l'instant le moyen le plus simple et d'utiliser la fonction printf :

printf("La variable dix est égale à %d", dix);

Le flag %d est utilisé pour appeler une variable entière dans printf() : vous pouvez taper ceci à l'intérieur des guillemets, et devez indiquer par la suite (après la virgule) les variables associées, dans l'ordre d'apparition des flags dans le texte. Voici les différents "flags" disponnibles, par type de variable :

%d int
%c char
%f double
%e float
%ld long
%s char* : chaîne de caractères

Exemple :

/* Affichage de variables */
unsigned int var1 = 8, var2 = 2;
char lettre = 'x';
double var3 = 10.0;
printf("%c = (%d + %d) = %f", lettre, var1, var2, var3);

Résultat :

x = (8 + 2) = 10.000000

Vous remarquerez l'affichage de la variable de type 'double'. Les variables double contiennent plusieurs chiffres après la virgule. En C, vous n'avez pas d'autres choix, pour le moment, que de laisser ceci tel quel. Les variables de ce type ne peuvent en effet s'écrire avec printf qu'en décimal (six chiffres après la virgule) ou en exponentiel (puissance de 10). Laissons donc cela de côté pour le moment, nous y reviendrons un peu plus tard.

Chaînes de caractères

Les chaînes de caractères représentent une suite de caractères accessible via une seule variable. Elles reposent sur les concepts liés des tableaux et des pointeurs, que nous n'aborderons pas encore. Néanmoins, nous avons jugé qu'il est plus intéressant pour un débutant de manipuler du texte rapidement, aussi seront-elles introduites dès maintenant et expliquées en profondeur plus loin. Une chaîne de caractère se définit comme une variable de type char dont, à la déclaration, on suffixe le nom par une paire de crochets contenant le nombre de caractères à stocker. Ainsi, l'instruction char nom[20 + 1]; déclare une variable de type "tableau de caractères" dont le nom est nom et qui peut contenir 21 caractères.

Sur ces 21 caractères, seuls 20 seront accessibles car le dernier caractère d'une chaîne est toujours le caractère '0', soit l'entier 0. Cela indique au langage la fin de chaîne. Ceci fait, on peut utiliser la variable via son nom (sans les crochets) dans le code, mais les opérateurs sur les variables vus plus haut, comme '=', ne fonctionnent pas : ainsi, nom = "Robert";, avec nom tableau de caractères, ne fonctionnera absolument pas. Pour ce faire, on utilisera des fonctions standards qui seront vues plus loin, le but de cet encart étant simplement de vous familiariser avec cette syntaxe que vous verrez plus amplement avec les tableaux.

Écrivons notre deuxième programme complet. Le code suivant permet d'écrire en sortie le nom, l'age et la taille d'une personne stockés dans des variables. Plus tard, nous utiliserons les fonctions d'interaction avec l'utilisateur pour qu'il entre ses propres paramètres. Ne pas oublier l'instruction getchar(); avant le return 0; de la fonction principale.

#include <stdio.h>

/* Fonction principale */
int main()
{
   /* Variables utilisees */
   int age;
   double taille;
   char nom[20 + 1];

   /* Affectation des valeurs*/
   age = 41;
   taille = 1.75;

   /* La fonction strcpy(variable, "texte");
      affecte la valeur "texte" à une chaîne
      de caractères. Elle ajoute le 0 de fin
      de chaîne automatiquement */
   strcpy(nom, "Billou");

   /* Affichage du resultat */
   printf("Billou a %d ans et mesure %f metres.", nom, age, taille);

   /* Fin d'execution */
   getchar();
   return 0;
}

Ce programme donne :

Billou a 41 ans et mesure 1.750000 metres._

Nous n'avons pas utilisé de variable pour stocker le nom "Billou" car il s'agit d'une chaîne de caractères, et que ce concept recquiert la maîtrise des tableaux et des pointeurs. Néanmoins, vous en savez désormais assez sur les variables pour pouvoir les utiliser dans vos premiers programmes.

Conditions

Un des points forts du langage C est qu'il permet une gestion puissante des boucles et des conditions, et les variables vont ici prendre une place essentielle. De façon schématique, le langage comporte des instructions conditionnelles :

si (condition)
    faire ceci;
sinon, et si (condition)
    faire cela;
sinon
    faire ça;

Mais aussi des boucles comme :

pendant que (condition)
    faire ceci;

Ces deux concepts de programmation, désormais communs à tous les langages, ont constitué une révolution à l'époque de leur création. Vous devrez maîtriser les diverses instructions qui suivent sur le bout des doigts, car elles constituent la base même de l'interactivité de tout logiciel :

  • Les conditions if/else
  • Les conditions switch
  • Les boucles while
  • Les boucles for

Conditions if

L'instruction if est la plus commune des instructions conditionnelles, c'est-à-dire les instructions qui n'exécutent une portion de code que si une ou plusieures conditions sont vérifiées. Elle s'emploie de la façon suivante :

if (condition) {
    // instructions
}

La condition est une valeur booléenne, c'est-à-dire true (vrai) ou false (faux), oui ou non, 1 ou 0. Cette valeur peut être celle d'une variable ou la valeur de retour d'une fonction, mais la plupart du temps elle est le résultat de l'évaluation d'une condition, utilisant les opérateurs de comparaison et les opérateurs logiques. Voici un exemple d'utilisation de cette instruction :

if (taille == 2.20) {
    printf("Vous etes grand !");
}

On fournit la condition de l'instruction entre les parenthèses. Ici, si la variable taille est égale à 2.20, on affichera la chaîne de caractères "Vous etes grand !". Sinon, cette portion de code ne sera pas exécutée. A noter le double-égal : nous n'affectons pas une nouvelle valeur à taille, mais nous testons sa valeur, ce qui explique l'utilisation d'un symbole différent pour éviter toute confusion : l'opérateur logique d'égalité ==. Une instruction if sera exécuté si les valeurs passé en conditions prennent la valeur booléenne vraie.

Opérateurs de comparaison

Voici les opérateurs de comparaison disponibles en C :

Opérateur Condition Exemple
== Egalité de deux valeurs (foo == 1)
!= Inégalité, c'est-à-dire vérifier que les deux valeurs sont différentes (variable != 1)
< Infériorité d'une valeur par rapport à une autre (variable < 8)
< Infériorité ou égalité d'une valeur par / à une autre (variable <= 8)
> Supériorité d'une valeur par / à une autre (variable1 > variable2)
> Supériorité ou égalité d'une valeur par / à une autre (variable >= 6)

En utilisant ceux-ci, il est possible de comparer des valeurs entre elles (le terme valeur comprend aussi bien les constantes que les variables). Il est bien entendu possible de tester plusieures conditions en même temps : pour ce faire, il est préférable de mettre chaque expression (condition) entre parenthèses, et d'associer les expressions entre elles en utilisant les opérateurs logiques que voici :

Opérateurs logiques

Opérateur Effet Exemple
&& Et logique : test vrai si et seulement si les deux conditions sont vraies (i == 1 && j == 2)
|| Ou logique : test vrai si l'une ou l'autre des deux conditions est vraie (i == 1 || i == 3)
! Non logique : test vrai si et seulement si la condition est fausse (i == 1 && !(j == 2))

Conditions else

Il est possible d'ajouter d'autres d'instructions à la suite d'une instruction if, qui ne seront exécutées que si les instructions if précédentes ne l'ont pas étées. Pour ce faire, on peut utiliser :

if (condition1) {
    // instructions exécutées seulement si condition1 est vraie
} else if (condition2) {
    // instructions exécutées seulement si condition1 est fausse et
    // condition2 est vraie
} else {
    // instructions exécutées seulement si toutes les autres conditions
    // (ici condition1 et condition2) sont fausses
}

Dans qui consiste en une seconde instruction if qui ne sera évaluée que si l'instruction if précédente n'a pas été exécutée. Cette syntaxe permet de tester certaines possibilités uniquement si les précédentes sont fausses, ce qui optimise le code car dans certains cas ce n'est pas la peine de tester ces valeurs en fonction des tests précédents. On peut aussi utiliser else, dont les instructions ne seront exécutées que si toutes les instructions if..else if précédentes ne l'ont pas étées. On obtient ainsi des tests type si..ou si..sinon, syntaxe pratique et performante. Il est possible d'omettre les accolades si une instructions if, else if ou else ne comporte qu'une seule instruction, comme suit : if (x == 1) x = 0;. Ainsi, considérons l'exemple suivant :

if (taille > 2.0) {
    printf("Vous mesurez plus de 2 m.\n");
} else if (taille > 1.8) {
    printf("Vous mesurez entre 1,6 m et 1,8 m.\n");
} else if (taille > 1.6) {
    printf("Vous mesurez entre 1,6 m et 1,8 m.\n");
} else if (taille > 1.4) {
    printf("Vous mesurez entre 1,4 m et 1,6 m.\n");
} else /* taille <= 1.4 */ {
    printf("Vous mesurez moins de 1,4 m.\n");
}

Cet exemple illustre l'intérêt de telles constructions : nous n'avons pas besoin de tester si la taille de l'utilisateur est supérieure à 1,6 m si l'on sait déjà qu'elle est supérieure à 1,8 m et qu'on a déjà exécuté les instructions associées. Ainsi, dès que la taille de l'utilisateur aura été située, les conditions suivantes ne seront pas testées, ce qui rend le code plus clair (sachant que les conditions précédentes sont fausses, nous n'avons pas à tester les deux bornes de taille à nouveau) mais aussi plus performant puisque nous effectuons moins de tests. Afin de clarifier un peu les choses, voici un autre programme d'exemple plus évolué utilisant des conditions à la chaîne :

#include <stdio.h>

/* Fonction principale */
int main() {
    /* Variables utilisees */
    unsigned int age = 21;
    double taille = 1.28;
    bool adulte = false;

    /* Test de l'age */
    if (age >= 18) {
      printf("Vous etes adulte ");
      adulte = true;
    } else {
      printf("Vous etes un enfant ");
    }

    /* Evaluation de la taille */
    if (taille > 2.0) {
        printf("et vous mesurez plus de 2 m.\n");
    } else if (taille > 1.8) {
        printf("et vous mesurez entre 1,6 m et 1,8 m.\n");
    } else if (taille > 1.6) {
        printf("et vous mesurez entre 1,6 m et 1,8 m.\n");
    } else if (taille > 1.4) {
        printf("et vous mesurez entre 1,4 m et 1,6 m.\n");
    } else if (adulte) {
        printf("et vous mesurez moins de 1,4 m.\n");
    } else /* !adulte && taille <= 1.4 */ {
        printf("mais vous etes encore petiot ;)\n");
    }

   /* Fin d'exécution */
   getchar();
   return 0;
}

Après compilation, ce programme donne :

Vous etes adulte et vous mesurez moins de 1,4 m._

A noter l'utilisation d'une variable booléenne adulte, qui ne peut prendre que les valeurs true ou false : si l'age est supérieur à 18 ans, nous mettons cette variable à true (vrai) ; plus tard, lors de l'évaluation de la taille, on ne se permet de souligner la petite taille de la personne que si elle est adulte, la condition testée étant la valeur de la variable. On aurait aussi pu écrire else if (adulte == true), mais la variable étant déjà booléenne ce n'est pas nécessaire. Nous ne savons pas encore acquérir les valeurs de age et taille par l'utilisateur du programme, mais nous y arrivons bientôt ;)

Conditions switch

L'instruction switch est une autre instruction de branchement, plus utilisée pour tester les valeurs possibles d'une variable. Sa syntaxe est différente, mais relativement simple. Elle s'utilise de la façon suivante : switch (variable) { case valeur: { instructions; break; } default: { instructions; break; }}. switch peut aussi servir à tester n'importe quel valeur, comme un retour de fonction ou une valeur booléenne (variable, condition...) Chaque test de valeur se rédige sous la forme case valeur: {instructions; break;}, et il est possible de prévoir un cas par défaut, comportant les instructions exécutées si aucun autre test n'a été vrai. Les accolades après les tests ne sont pas nécessaires. Voici un exemple d'utilisation :

switch (variable) {
   case 1:
      printf("OK");
      break;
   case 2:
      printf("FALSE");
      break;
   default:
      printf("?");
      break;
}

Ici, si la variable variable est égale à 1 on affiche "OK", et "FALSE" si elle est égale à 2. Si elle n'est égale ni à 1 ni à 2, c'est-à-dire à aucune des possibilitées prévues, on affiche "?". A noter l'obligation du mot clef "break" après une condition remplie : si ce mot-clef était ommis, les instructions des tests suivants seraient aussi exécutées. L'opérateur ternaire

Plus complexe mais très utile, l'opérateur ternaire permet d'écrire de façon concise ce qui aurait put être une longue suite d'instructions if. Il se définit ainsi : (condition) ? oui : non; Si la condition condition est remplie, le code oui après le ? est évalué. Sinon, ce sera le cas du code non après les deux points ':'. Cet opérateur peut s'employer avec des variables comme avec des instructions. Voici quelques exemples concrets illustrant son utilisation :

/* Instructions : */
(age >= 18)
    ? puts("Vous etes adulte.")
    : puts("Vous n'etes pas adulte");

/* Valeurs : */
puts ((age >= 18)
    ? "Vous etes adulte."
    : "Vous n'etes pas adulte.");

Dans le premier cas, la première instruction puts (équivalente à printf) est exécutée si l'age est supérieur ou égal à 20, faute de quoi la seconde sera exécutée. Dans le second cas, ce ne sont plus les instructions qui sont soumies à condition mais les valeurs à afficher : idem, en fonction de l'age, une chaîne de caractères constante sera affichée plutôt qu'une autre. L'avantage de cet opérateur est qu'il peut être inclus dans n'importe quelle portion de code, même au milieu d'un appel de fonction (comme ci-dessus). Il vous sera sans doute utile, mais attention car la relecture du code n'en devient que plus ardue, ce qui est source de bugs.

Boucles

Nous allons maintenant voir le cas des boucles. Il s'agit d'un concept essentiel en programmation. Une boucle est un ensemble d'instructions qui s'ont exécutées répétitivement jusqu'à ce qu'une condition soit remplie, ou qu'on quitte la boucle via une instruction spécifique.

Boucles while

Le premier cas de boucle étudé sera celui de while, qui peut être utilisée seule ou avec le mot-clef do. Nous verrons les deux possibilités. La première syntaxe est la boucle do-while :

int cpt = 0;
do {
   ++cpt;
   printf("cpt = %d\n", cpt);
} while (cpt < 5);

La seconde est la boucle while :

int cpt = 0;
while (cpt < 5) {
   ++cpt;
   printf("cpt = %d\n", cpt);
}

Résultat dans les deux cas :

cpt = 1
cpt = 2
cpt = 3
cpt = 4
cpt = 5

Le principe d'une boucle est d'exécuter un ensemble d'instructions jusqu'à ce qu'une ou plusieures conditions soient validées. Le test de ces conditions est appelé "test de sortie", et selon la syntaxe utilisée il est effectué avant ou après avoir exécuté les instructions. Dans le premier cas, comme le laisse suggérer la syntaxe du code, les instructions sont exécutées avant le test de sortie, ce qui signifie au moins une fois, à l'inverse du second où le test sera effectué en premier. Il existe un mot-clef permettant de sortir d'une boucle à partir de n'importe laquelle de ses instructions : break; La plupart du temps, il est utilisé au sein des instructions de la boucle pour quitter celle-ci si une ou plusieures conditions sont validées.

Par ailleurs, il existe un autre mot-clef, continue;, qui permet de "revenir" au début de la boucle après avoir exécuté le test de sortie. Les boucles while et do..while sont les plus accessibles du langage. Elles sont souvent utilisées comme "boucles principales" d'un programme, c'est à dire une boucle dans la fonction principale qui se répète tant que l'utilisateur n'a pas demandé à quitter. Il existe cependant un autre type de boucles, plus complet : les boucles for.

Boucles for

Les boucles for sont un type de boucle un peu plus complexe que les boucles while, car elles ne se contentent pas de répéter une suite d'instructions soumises à condition. On peut les rapprocher de l'utilisation du symbole mathématique Sigma : en effet, la principale utilisation de telles boucles est le parcours d'une suite d'éléments. Pour ce faire, on leur fournit trois paramètres, qui peuvent tous trois être omis en fonction des besoins. En premier lieu, on peut déclarer des variables spécifiques à la boucle, c'est-à-dire qu'elles ne seront disponnible que dans le bloc d'instructions de la boucle. La plupart du temps, ces variables sont des compteurs sur le nombre d'éléments de la liste. Le second paramètre est la condition de poursuite. Le troisième paramètre constitue une suite d'instructions à exécuter en fin de boucle, qui représente la plupart du temps une mise-à-jour de l'élément courrant de la liste. Voici le schéma syntaxique d'utilisation des boucles for :

for (déclaration de variables; condition; instructions de fin de boucle) {
    // instructions
}

Sans les premiers et troisièmes paramètres, une telle boucle est similaire à une boucle while. Cette syntaxe particulière est néanmoins très pratique, surtout dans le cas où l'on parcours une liste d'éléments, car elle permet de séparer les instructions pour chaque élément du code de parcours. Ainsi, si l'on veut reproduire l'exemple présenté ci-avant pour les boucles while, il suffit de déclarer une variable compteur cpt spécifique à la boucle, et d'incrémenter cpt en fin de boucle. Le code obtenu est bien plus clair et concis que le précédent, et le code d'affichage est distinct du code de "parcours" qui compte le nombre d'éléments.

for (int cpt = 1; cpt <= 5; ++cpt) {
   printf("cpt = %d\n", cpt);
}

Le résultat est strictement le même que précédemment, mais l'écriture en est plus concise. Quelques détails diffèrent : on donne la valeur 1 à cpt plutôt que 0 car l'instruction ++cpt; n'est plus effectuée au début mais à la fin de la boucle. Aussi, on utilise <= plutôt que < car while vérifiait la condition de poursuite en fin de boucle, tandis que for le fait au début. L'utilité de la boucle de l'exemple ci-dessus est donc d'effectuer 5 fois les instructions entre accolade, la variable cpt servant de compteur. Il vous sera impossible d'utiliser cpt en-dehors de la boucle, mais vous pouvez toujours la déclarer dans un bloc parent.

Interactions avec l'utilisateur

Nous connaissons maintenant les bases pour faire un programme autonome, suivant une liste d'instructions données, mais nous ne savons pas encore interagir avec l'utilisateur, c'est-à-dire lui demander des informations avant d'agir en conséquence. Nous allons apprendre ci-dessous les principales fonctions d'entrée/sortie qui répondent à ce besoin :

  • La fonction getchar
  • La fonction fgets
  • La fonction puts
  • La fonction scanf

Fonction getchar

Comme son nom l'indique, cette fonction demande à l'utilisateur d'entrer un caractère quelconque, lettre ou chiffre. getchar() retourne ce caractère, de la même manière que votre fonction main() retourne la valeur 0. Ainsi, évaluer getchar() revient à évaluer sa valeur de retour, soit le caractère tapé par l'utilisateur. On peut donc utiliser l'opérateur '=' pour affecter ce caractère à une variable :

caractere = getchar();

La valeur de la variable caractere de type char sera celle du caractère tapé par l'utilisateur. Attention : afin de valider son caractère, l'utilisateur appuyera sur [Entrée]. Or, entrée est considéré comme un autre caractère, 'n', que vous avez déjà utilisé. Ainsi, les prochaines fois que vous utiliserez getchar, il vous faudra effectuer deux appels : un appel seul, pour passer cette entrée, et un appel affectant la valeur du caractère à votre variable. Cette fonction peut servir à toutes sortes de traitements de caractères, mais n'est souvent utilisée que pour demander à l'utilisateur de taper [Entrée].

Fonction fgets

La fonction getchar ne permet de lire qu'un seul caractère à la fois. Afin d'y pallier, nous allons utiliser une autre fonction, fgets, qui lit non pas un caractère mais une chaîne de caractères (on pourra la stocker dans un tableau comme précédemment). Demandons son nom à l'utilisateur de notre programme, et stockons le dans une variable char nom[20 + 1]; qui peut contenir jusqu'à vingt caractères. Nous pouvons procéder comme suit :

#include <stdio.h>

/* Fonction principale */
int main() {
   /* Tableau de caractère */
   char nom[20 + 1];

   /* Message de demande : */
   printf("Entrez votre nom :\n");

   /* Lecture dans la variable `nom`,
    * 20 caractères maximum,
    * depuis l'entrée standard `stdin` (clavier) */
   fgets(nom, 20, stdin);

   /* Affichage de la chaîne avec le flag `%s` */
   printf("Votre nom est %s.\n", nom);

   /* Fin d'exécution, petit getchar pour attendre */
   getchar();
   return 0;
}

Mettons que vous entriez "Robert". Le binaire exécuté donne :

Entrez votre nom :
Robert
Votre nom est Robert._

En-dehors des chaînes de caractères, fgets permet aussi de lire des entiers ou des réels à virgule flottante. Néanmoins, l'utilisation de scanf lui est préférable dans ces cas (voir plus loin). Dans l'exemple ci-dessus, nous demandons à fgets de lire vingt caractères au maximum : il s'agit d'une sécurité pour éviter les erreurs de segmentation ("segmentation faults" ou "segfaults") dues aux débordements de mémoire. Sans ce test (ou si par exemple nous exécutons fgets(nom, 42, stdin); alors que nom ne contient que vingt caractères), si l'utilisateur entre d'avantage de caractères que la variable nom ne peut en stocker, le programme tentera d'écrire en mémoire dans une zone en-dehors de la variable, écrasant soit d'autres variables soit du code exécutable.

Fonction puts

Rien de bien compliqué ici : la fonction puts se content d'écrire le contenu d'une variable en sortie. Son temps d'exécution est meilleur que celui de printf, mais elle ne permet aucune mise-en-forme. Elle peut être bénéfique en termes de performances si elle est utilisée dans une portion de code appelée très fréquemment. Dans le cas contraire, on lui préfèrera printf.

puts(variable);

Fonction scanf

De même que puts va de paire avec fgets, le fonctionnement de scanf est similaire à celui de printf, mais son rôle est de lire des informations en entrée. Pour ce faire, cette fonction prend en paramètres une chaîne de caractères comportant les flags représentant les variables à lire, identiques à ceux de printf. Ceci fait, les variables doivent être spécifiées après les guillemets, mais d'une manière différente que pour printf : en effet, scanf() doit connaître non pas le nom de la variable mais son adresse, c'est à dire la zone en mémoire où est stockée la variable. On obtient l'adresse d'une variable en préfixant son nom d'une esperluette & mais vous verrez ce concept plus en détail en abordant les pointeurs. Les adresses des variables doivent être séparées par des virgules et dans le même ordre d'apparition que les flags.

Dans l'ensemble, scanf s'utilise comme suit :

scanf(flags, &variable...;)

Ainsi, pour lire en entrée une variable entière, on peut employer le code suivant :

unsigned int entier;
scanf("%d", &entier);

Précision : pour lire une chaîne de caractères, il ne faut pas utiliser le & préfixant le nom de la variable, car une telle variable est en réalité un pointeur sur la zone mémoire comportant les caractères. Vous verrez ce point plus en détail en abordant tableaux et pointeurs.

Lire une chaîne de caractères

char nom[40 + 1];
scanf("%s", nom);

Voici donc pour les principales fonctions d'Entrée/Sortie utiles aux débutants en C. Il s'agit d'un petit encart au parcours plus théorique d'apprentissage du langage pour permettre aux néophytes de coder rapidement de petites applications interactives, et ce avant d'aborder des concepts plus complexes.

Conclusion

Ceci n'est pas une fin mais un début : arrivés ici, vous commencez à maîtriser le langage C et les premiers concepts à connaître. Libre à vous maintenant d'exploiter ces constructions (boucles, conditions, entrées, sorties) pour coder des programmes de plus en plus complexes et interactifs.

Pour aller plus loin

Il existe en France deux excellents moyens d'apprendre la programmation : l'association France-IOI et le concours national Prologin. Je vous les recommande chaudement et en particulier si vous êtes au collège ou au lycée ! Pour ma part, j'ai mieux appris (plus, plus facilement et de façon beaucoup plus marrante) aux stages d'entraînement France-IOI qu'à n'importe laquelle des autres institutions où j'ai étudié par la suite. Quinze ans plus tard, je m'estime encore incroyablement chanceux de les avoir découverts et reconnaissant envers nos entraîneurs de l'époque Mathias Hiron et Arthur Charguéraud :)

Voici un dernier code source d'exemple pour la route :

personne.c

#include <stdio.h>

/* Fonction principale */
int main() {
   /* Variables locales */
   int age;
   double taille;
   char nom[20 + 1];

   /* Acquisition du nom */
   printf("Entrez votre nom :\n");
   fgets(nom, 20, stdin);

   /* Acquisition de l'age */
   printf("Entrez votre age :\n");
   scanf("%d", &age);

   /* Acquisition de la taille */
   printf("Entrez votre taile :\n");
   scanf("%lf", &taille);

   /* Messages en sortie */
   printf("Bonjour %s !\n\n");
   printf("Vous avez %d ans et vous mesurez %f metres.", nom, age, taille);
   printf("J'en sais des choses non ?\n");

   /* Fin d'exécution */
   getchar();
   return 0;
}

Sur ce, bon courage dans vos aventures informatiques à venir !

Discussion

Feel free to post a comment by e-mail using the form below. Your e-mail address will not be disclosed.

📝 You can use Markdown with $\LaTeX$ formulas in your comment.

By clicking the button below, you agree to the publication of your comment on this page.

Opens your e-mail client.