Chapitre 2 : Type d'une variable, entiers, caractères, réels.

2.1 Précisions sur la notion de type

2.1.1 Type d'une variable

Nous avons déjà vu que pour déclarer une variable nombre permettant de stocker un entier, on écrit la déclaration suivante :

   int nombre;

Cette déclaration est composée de deux mots. Le mot int est appelé le type de cette variable et le mot nombre est son identifiant. Dans le cas présent, le type de la variable nombre est un entier, mais nous allons voir qu'il existe d'autres types et que l'on peut créer des variables permettant de stocker des nombres réels, des caractères et bien d'autres choses que nous verrons par la suite.

Une variable peut être considérée comme une boîte dans laquelle on peut stocker différentes choses. De la même manière qu'une petite boîte ne peut contenir d'objets trop gros, ou qu'une boîte en carton n'est pas adaptée pour contenir de l'eau, chaque variable est prévue pour stocker des valeurs d'un type bien précis et ne dépassant pas une certaine taille.

La variable nombre que nous avons déclarée plus haut est de type int. Elle est donc prévue pour stocker des entiers et ne pourra par exemple pas stocker de nombres à virgule. On verra également qu'il y a une limite à la valeur des entiers que l'on peut y stocker : on ne pourra pas par exemple y stocker une valeur comme cinq milliards.

Une variable correspond en effet à une zone de mémoire que l'on réserve pour stocker sa valeur. Plus un nombre a de chiffres, plus il faut de place pour l'écrire, donc de mémoire pour le retenir. Plus une variable utilise de mémoire, plus elle pourra contenir de valeurs différentes.

Lorsque l'on déclare une variable, on doit indiquer son type, qui définit :

Dans ce chapitre, nous étudierons trois types :

Pour déclarer une variable, on écrit son type, suivi de son identifiant et d'un point virgule.

Voici quelques exemples de déclarations de variables :

   int unEntier;
   double unNombreAVirgule;
   char unCaractere;
   int unAutreEntier = 0;

2.1.2 Déclarations multiples

Jusqu'à présent, lorsque nous avions besoin de plusieurs variables, nous écrivions autant de déclarations, chacune sur une ligne, par exemple :

   int nbLignes;
   int nbColonnes;
   int ligne;

Lorsque plusieurs des variables que l'on déclare sont de même type, il est possible de les déclarer sur une même ligne, sans répéter le type :

Pour déclarer plusieurs variables d'un même type, on écrit d'abord le type, puis les identifiants des variables déclarées, séparés par des virgules.

On peut ainsi déclarer les trois variables en une ligne :

   int nbLignes, nbColonnes, ligne;

On a vu au premier chapitre qu'il était possible d'affecter une valeur à une variable dès sa déclaration. C'est également possible lors des déclarations multiples :

   int ligne = 0;
   int colonne = 0;

On peut écrire ces deux déclarations avec affectations en une ligne :

   int ligne = 0, colonne = 0;

Il est cependant important de garder en tête le fait que la clarté du code est la priorité. Déclarer de trop nombreuses variables sur une même ligne peut nuire à cette clarté.

2.1.3 Notion de mot clé

Depuis le début de ce cours, nous avons vu un certain nombre de mots utilisés dans les programmes C :

Les identifiants de variables sont choisis par le programmeur et ne font pas partie du langage C lui-même. Il en est de même pour les instructions scanf et printf, qui sont des fonctions externes, dont nous étudierons plus tard le principe.

Les deux autres catégories de mots font par contre partie du langage C en tant que tel. Ce sont ce que l'on appelle des mots clés, dont nous verrons de nombreux autres exemples. Les mots clés étant réservés pour le langage lui-même, ils ne peuvent être utilisés comme identifiants, par exemple dans les déclarations de variable.

Par exemple, le code suivant est invalide :

#include <stdio.h>

int main()
{
    int simple, double;
    scanf("%d", &simple);
    double = 2*simple;
    printf("Le double de %d est %d\n", simple, double);
    return 0;
}

Ce programme provoquera l'erreur de compilation suivante :

test.c:5: parse error before `double'

On a en effet tenté de déclarer une variable double dont le nom est un mot clé, puisqu'il s'agit d'un type.

2.2 Notation en base binaire

Lorsque nous manipulons des nombres, nous les écrivons sous la forme d'une suite de chiffres, où chaque chiffre peut prendre dix valeurs possibles, de 0 à 9. La valeur d'un nombre à quatre chiffres est alors égale à 1000 (103) fois le premier chiffre, plus 100 (102) fois le deuxième chiffre, plus 10 (101) fois le troisème, plus 1 (100) fois le quatrième. On multiplie chaque chiffre par la base (10) puissance la position du chiffre en partant de la droite, et en commençant à 0. C'est ce que l'on appelle la base 10, puisqu'elle se base sur dix chiffres différents. La raison pour laquelle nous comptons de cette manière est en grande partie due au fait que cela nous permet de compter facilement sur nos 10 doigts.

Le nombre 5432, écrit en base 10, vaut ainsi 5 * 1000 + 4 * 100 + 3 * 10 + 2 * 1.

Un ordinateur n'ayant pas de doigts, compter en base 10 (dite base décimale) ne lui est pas particulièrement adapté. La base 2, composée des chiffres 0 et 1, l'est par contre beaucoup plus. Pour un appareil électrique, on peut en effet considérer deux états élémentaires possibles : pas de courant (0), ou du courant (1). C'est donc en base 2 que la machine manipule tous les nombres. La valeur d'un nombre en base 2, dite base binaire, se calcule selon le même principe que la valeur d'un nombre en base 10 : la valeur d'un nombre à quatre chiffres, est de 8 (23) fois le premier chiffre, plus 4 (22) fois le deuxième, plus 2 (21) fois le troisième, plus 1 (20) fois le quatrième chiffre.

Par exemple, le nombre en base binaire 101011 est égal à :

1 * 25 + 0 * 24 + 1 * 23 + 0 * 22 + 1 * 21 + 1 * 20.

Soit :

1 * 32 + 0 * 16 + 1 * 8 + 0 * 4 + 1 * 2 + 1 * 1

Soit (on l'affiche en base 10 pour nous) :

32 + 8 + 2 + 1 = 43

Exercice : traduire à la main les nombres en base 2 qui suivent, en nombres en base 10.

10
101
111
10001
1010101
100100100

Solution :

Pour calculer plus rapidement, on peut inscrire les nombres dans un tableau, avec en colonne les puissances de 2 correspondant à la position de chaque chiffre :

2561286432168421Total
0000000102
0000001014 + 1 = 5
0000001114 + 2 + 1 = 7
00001000116 + 1 = 17
00101010164 + 16 + 4 + 1 = 85
100100100256 + 32 + 4 = 292


Pour convertir un nombre de la base 10 à la base 2, on commence par rechercher la plus grande puissance de 2 qui soit inférieure au nombre. Par exemple, si notre nombre est 19, la plus grande puissance de 2 inférieure à 19 est 16 (2 puissance 4). On soustrait cette valeur à notre nombre : 19 - 16 = 3, et on recommence l'opération avec le reste, ici 3. Chaque puissance de deux ainsi obtenue correspond à la position d'un chiffre 1 dans la notation binaire du nombre.

Pour notre exemple, on a obtenu 19 = 16 + 2 + 1, ce qui donne le nombre en base 2 : 10011.

Pour se faciliter la tâche, il est bon d'avoir devant soi une liste des premières puissances de 2, et leur rang. Les connaître par coeur est d'ailleurs très souvent utile en programmation.

Exercice : traduire à la main les nombres en base 10 qui suivent, en nombres en base 2.

6
14
23
64
71

Solution :

6 = 4 + 2 -> 110
14 = 8 + 4 + 2 -> 1110
23 = 16 + 4 + 2 + 1 -> 10111
64 = 64 -> 1000000
71 = 64 + 4 + 2 + 1 -> 1000111

2.3 Précisions sur les entiers

2.3.1 Représentation binaire du type int

Toutes les variables que nous avons utilisées pour l'instant étaient de type int : des entiers. Comme nous l'avons vu dans la section précédente, il existe une limite aux valeurs que l'on peut stocker dans une variable. Dans le cas des entiers, cette limite n'est cependant pas toujours la même et dépend de la machine sur laquelle votre programme va s'exécuter.

Les entiers sont stockés sur la machine sous la forme de nombres en base binaire. Il y a cependant une limite sur le nombre de chiffres que l'on peut stocker dans une variable de type int. Cette limite est de 32 chiffres sur les machines récentes, mais peut être de 16 sur d'autres. On considérera pour l'instant que la limite est de 32, puisque c'est très probablement le cas sur la machine que vous utilisez. La mémoire utilisée pour stocker une variable de type int est donc de 32 chiffres binaires, que l'on appelle bits. On dit ainsi que l'on manipule des entiers 32 bits. Cela correspond également à quatre octets, un octet correspondant à huit bits.

Il existe une différence entre la notation binaire que nous avons vue dans la section précédente et le stockage des nombres dans une variable de 32 bits. Cette différence porte sur la manière de stocker les nombres négatifs. En base binaire "classique", on écrit un nombre négatif en base 2 de la même manière qu'en base 10 : en mettant un signe - devant. Le nombre -9 s'écrit ainsi -1001. Or, dans une variable entière sur 32 bits, on dispose de 32 chiffres pour stocker un nombre et de rien d'autre. On doit donc utiliser ces chiffres eux-mêmes pour déterminer si le nombre est négatif ou non.

Pour comprendre comment les nombres négatifs sont stockés, prenons l'exemple d'un compteur kilométrique d'une voiture (suffisamment ancienne pour que ce compteur soit mécanique), composé de 8 chiffres. Lorsque l'on avance de 100m, le chiffre de droite tourne d'un cran, et passe au chiffre suivant. S'il passe de 9 à 0, le chiffre suivant tourne d'un cran également, etc.

Supposons maintenant qu'il soit possible de le faire remonter en arrière. Que se passe-t-il lorsque tous les chiffres sont à 0 et que l'on le fait tourner à l'envers ? Votre compteur aura probablement tous ses chiffres qui repasseront à 9. La valeur "-1" sera alors représentée par l'affichage : 99999999 (tous les chiffres à 9).

Comment déterminer si ce nombre 99999999 représente la valeur -1, ou la très grande valeur positive que nous lisons ? Il n'y a pas de recette miracle : il faut décider une fois pour toutes que les nombres supérieurs à une certaine valeur seront des nombres négatifs. On peut par exemple dire que tous les nombres au dessus de 50000000 doivent être considérés comme négatifs. Le plus grand nombre positif pouvant être représenté est alors 49999999. La valeur 50000000 représente le nombre -50000000, puisque 50000000 + 50000000 = 100000000, soit 00000000 sur notre compteur à 8 chiffres.

Ceci est exactement le principe que l'on applique pour stocker des entiers négatifs dans une variable de 32 bits. La valeur -1 est représentée par le nombre 11111111111111111111111111111111 (32 bits à 1), la valeur -2 par 11111111111111111111111111111110, etc. On définit que les nombres doivent être considérés comme négatifs à partir de la valeur : 10000000000000000000000000000000. Le plus grand entier que l'on peut stocker est alors : 01111111111111111111111111111111 (un zéro, suivi de 31 uns).

Si l'on traduit ces nombres en base 10, on en déduit que la plus grande valeur que l'on peut stocker sur un entier 32 bits est de 231-1, c'est à dire 2147483647 (un peu plus de deux milliards). La valeur la plus petite que l'on puisse stocker est -231, soit -2147483648.

Exercice : on a évoqué le fait que sur certaines machines, une variable de type int est stockée sur 16 bits seulement. Quelles sont à votre avis les valeurs minimales et maximales d'une variable int sur ces machines ?


Solution : En appliquant le même principe, on définit que les nombres négatifs sont les nombres à partir de 1000000000000000 (un 1 suivi de 15 zéros). Le plus grand nombre (positif) est donc 215 - 1 soit 32767, et le plus petit nombre est -215, soit -32768.


Exercice : déterminer un critère simple pour déterminer à partir de sa notation binaire 32 bits, si un nombre est positif ou négatif.


Solution : Les nombres négatifs étant ceux à partir de 10000000000000000000000000000000, il suffit de regarder le premier bit de la valeur. S'il est à 1, c'est un nombre négatif, s'il est à 0, c'est un nombre positif.


2.3.2 Dépassements de capacité

Nous venons de voir qu'il y a une limite aux valeurs que peut prendre un entier. Que se passe-t-il lorsque l'on dépasse cette limite ? Pour le voir, testez le programme suivant et essayez de comprendre le résultat.

#include <stdio.h>

int main()
{
   int grandeValeur = 2147483647;
   printf("Valeur de départ : %d\n", grandeValeur);
   grandeValeur++;
   printf("Après incrémentation : %d\n", grandeValeur);
   grandeValeur--;
   printf("Après décrémentation : %d\n", grandeValeur);
   return 0;
}

Le programme devrait afficher le texte suivant :

Valeur de départ : 2147483647
Après incrémentation : -2147483648
Après décrémentation : 2147483647

La valeur initiale 2147483647 correspond à la plus grande valeur positive que peut contenir une variable int. Elle correspond à la valeur binaire 01111111111111111111111111111111. Si on l'incrémente de 1, on obtient en binaire, 10000000000000000000000000000000, qui est la toute première valeur négative que l'on peut représenter. On a en fait "fait le tour" des valeurs possibles.

On aurait pu "faire le tour" de nombreuses manières différentes. Une multiplication du nombre initial par 5 aurait ainsi donné la valeur 2147483643, après deux dépassements de capacité en une même opération.

La machine ne fait aucune vérification de dépassement de la capacité d'un entier. Lorsque vous écrivez un programme, vous devez donc systématiquement vérifier vous-mêmes que les valeurs que vous manipulez ne risquent pas de dépasser les valeurs maximales qu'un int permet de gérer. Si vous ne le faites pas, vous risquez de rencontrer des erreurs d'exécution difficiles à détecter.

2.3.3 Division entière et modulo

Nous avons déjà évoqué la notion de division entière au premier chapitre, en citant rapidement l'opérateur correspondant '/'. La division entière est une opération qui fournit le résultat de la division de deux nombres, en conservant uniquement la partie entière du résultat, c'est à dire en ne considérant pas les décimales. On l'appelle aussi division euclidienne (du nom de son inventeur, Euclide). Essayez par exemple le programme suivant :

#include <stdio.h>

int main()
{
   int nb1 = 11;
   int nb2 = 4;
   int resultat;
   resultat = nb1 / nb2;
   printf("%d divisé par %d donne %d\n", nb1, nb2, resultat);
   return 0;
}

Le résultat est 2. En effet, même si le résultat exact de la division de 10 par 4 est de 2.5, on ne garde que la partie entière 2. Notez qu'il ne s'agit pas d'une valeur arrondie à l'entier le plus proche : la division entière de 11 par 4 donnera aussi 2, alors que la division exacte de 11 par 4 donne 2.75.

Dans le cas où l'un des nombres de la divisions est négatif, le résultat est bien sûr négatif, et est égal à l'opposé de ce qu'il serait avec uniquement des nombres positifs. La division entière de 10 par -4 donne ainsi -2. On remarque ici que le résultat n'est pas non plus un "résultat arrondi à l'entier inférieur le plus proche", puisque dans le cas des nombres négatifs, la valeur entière du résultat est supérieure ou égale au résultat de la division exacte.

Une autre opération sur les entiers, que nous n'avons pas encore vue, est ce que l'on appelle le modulo, qui est représenté par l'opérateur '%' et qui permet de calculer le reste de la division entière de deux nombres. Par exemple l'expression 11 % 4, que l'on lit "11 modulo 4", vaut 3, puisque 10 divisé par 4 donne 2 et qu'il reste 3 : 11 = 4 * 2 + 3.

Exercice : calculez à la main les valeurs des expressions suivantes :

   3 / 2
   3 % 2
   14 % 5
   20 % 5
   15788 / 10
   15788 % 10
   15788 % 2

Solution :

   3 / 2 = 1
   3 % 2 = 1
   14 % 5 = 4 car 14 = 5 * 2 + 4
   20 % 5 = 0 car 20 = 5 * 4
   15788 / 10 = 1578
   15788 % 10 = 8 car 15788 = 1578 * 10 + 8
   15788 % 2 = 0 car 15788 est disible par 2, donc donne un reste nul

Exercice : écrivez un programme qui lit un nombre en entrée et qui affiche "pair" si le nombre est pair, et "impair" sinon.


On peut remarquer qu'un nombre positif modulo un nombre K est toujours compris entre 0 et K - 1. Par contre, dans le cas d'un nombre négatif modulo un nombre positif K, le résultat sera un nombre négatif ou nul, compris entre - K + 1 et 0. Par exemple, l'expression -11 % 4 vaut -3. Le modulo d'un entier par un nombre K négatif est égal au modulo de cet entier par la valeur absolue de K. 10 % -4 est ainsi égal à 10 % 4 et vaut donc 2.

Exercice : écrivez un programme qui lit un nombre de secondes entier au clavier et qui affiche cette durée en jours, heures, minutes et secondes au format "%d jours, %d heures, %d minutes et %d secondes".


2.4 Les caractères

2.4.1 Déclaration et notation

Nous avons vu au premier chapitre, qu'avec l'instruction printf, nous pouvions afficher du texte, sous la forme de chaînes de caractères : des caractères placés entre guillemets : "du texte". Nous verrons plus tard comment créer et manipuler des variables permettant de stocker des chaînes de caractères, mais voyons tout d'abord comment stocker un caractère à la fois.

Pour manipuler un caractère seul, on n'utilise pas de guillemets, mais des apostrophes : 'A', '%', '@', '9', 'é', ' ' (espace), sont quelques exemples de caractères. On peut stocker un tel caractère dans une variable de type char :

   char unCaractere = 'A';

2.4.2 Affichage et lecture au clavier

Pour afficher un caractère, on utilise la même instruction que pour un entier : printf. Le principe est le même, mais on doit cette fois utiliser le texte "%c" au lieu de "%d" :

   char unCaractere = 'A';
   printf("%c", unCaractere);

Il existe des caractères dits de contrôle, qui ne sont pas affichés sous la forme d'un simple caractère, mais peuvent par exemple influencer la mise en page du texte. On en a déjà vu plusieurs, comme le retour à la ligne, que l'on note '\n', ou la tabulation, notée '\t'. Ces caractères ne pouvant pas être représentés sous la forme d'une simple lettre, on utilise un caractère d'échappement : \ (anti-slash), suivi d'une lettre, pour les identifier.

Certains autres caractères demandent également d'utiliser cette notation. Par exemple si l'on souhaite afficher le caractère apostrophe, on ne peut pas se contenter de le placer entre apostrophes : ''' serait ambigu. On utilise donc ici aussi le caractère d'échappement et note l'apostrophe '\''. De même, pour afficher le caractère d'échappement, on doit utiliser '\\'. Ces notations fonctionnent à l'intérieur d'une chaîne entre guillemets, aussi bien que pour un caractère seul entre apostrophes.

Par exemple, si l'on veut afficher le texte "Le caractère " s'appelle guillemet.", on peut écrire le code suivant :

   char guillemet = '\"';
   printf("Le caractère %c s'appelle guillemet.", guillemet);

Mais on peut aussi simplement écrire :

   printf("Le caractère \" s'appelle guillemet.");

Pour lire un caractère au clavier, on peut utiliser scanf :

   char unCaractere;
   scanf("%c", &unCaractere);

Comme dans le cas de la lecture d'un entier, il faut que l'utilisateur appuie sur la touche entrée pour qu'un caractère puisse être lu par scanf. On ne peut donc pas lire les caractères au fur et à mesure où ils sont tapés, en utilisant cette instruction. Nous verrons plus tard qu'il existe d'autres manières de lire les caractères.

Si l'utilisateur ne tape pas de caractère avant d'appuyer sur entrée, le premier caractère lu sera le caractère de retour à la ligne : '\n'. Si vous exécutez votre programme sous le système d'exploitation windows, ce sont en fait deux caractères de contrôle qui sont "tapés" lorsque l'on appuie sur entrée : le caractère '\r', puis le caractère '\n'. Le caractère '\r', appelé retour de chariot indique que le curseur doit être placé tout à gauche de la ligne. Le caractère '\n' indique qu'il faut passer à la ligne suivante.

Une fois que l'on a lu un caractère, on peut aussi l'afficher, ou effectuer diverses opérations dessus. On peut par exemple tester s'il est égal à une valeur donnée en utilisant l'opérateur "==", ou différent d'une valeur, grâce à l'opérateur "!=" :

   char unCaractere;
   scanf("%c", &unCaractere);
   if (unCaractere != 'A')
      printf("Ce caractère n'est pas un A\n");
   else
      printf("Ce caractère est un A\n");

Exercice : écrivez un programme qui lit une ligne de texte en entrée et affiche cette ligne verticalement, c'est à dire en plaçant un caractère par ligne. Indication : la ligne se termine lorsque le caractère '\n' est lu.

Remarque : il n'est pas nécessaire de stocker toute la ligne avant de commencer à afficher le résultat. Comme dans tous les exercices qui suivent (et la plupart des exercices du site), on peut afficher le résultat au fur et à mesure de la lecture des caractères de l'entrée par scanf.


Solution :

#include <stdio.h>

int main()
{
   char caractereLu = ' ';
   while (caractereLu != '\n')
   {
      scanf("%c", &caractereLu);
      printf("%c\n", caractereLu);
   }
   return 0;
}

En exécutant ce programme, vous verrez que rien n'est affiché tant que l'on appuie pas sur entrée. Le système d'exploitation transmet en fait le texte au programme ligne par ligne, et l'instruction scanf reste en attente tant qu'on n'a pas validé la ligne. Chaque appel à scanf ne lit cependant qu'un caractère.


Exercice : écrivez un programme qui lit une ligne en entrée et affiche le nombre de caractères de cette ligne. Attention : il ne faut pas compter le caractère '\r', s'il apparaît.


Exercice : écrivez un programme qui lit une ligne tapée au clavier et l'affiche cette ligne en remplaçant tous les espaces par le caractère "_".


2.4.3 Stockage en mémoire, représentation binaire et code ASCII

De même que pour les variables de type int, les variables de type char utilisent un espace mémoire limité et ont donc un nombre limité de valeurs possibles :

Une variable char est stockée sur un octet, soit 8 bits.

Exercice : donnez la nombre exact de valeurs différentes possibles d'une variable de type char.


Solution : une variable de 8 bits peut représenter 28 valeurs différentes, soit 256.


Une variable char peut dans certains cas être manipulée comme un nombre. On pourra par exemple écrire "char unCaractere = 65;". Lorsqu'il est manipulé comme un nombre, un char peut prendre des valeurs positives, aussi bien que négatives. Chaque nombre possible correspond à un caractère. Par exemple, le nombre 65 correspond au caractère 'A'.

Exercice : sachant que le principe de représentation des nombres négatifs dans une variable char est le même que pour une variable int, déterminer les valeurs minimales et maximales que peut prendre un char, lorsqu'il est vu comme un nombre.


Solution : on considère que les nombres sur 8 bits sont négatifs à partir de 10000000, soit 27, ou 128. Un char peut donc prendre des valeurs allant de -128 à 127.


Avec 256 valeurs possibles, on peut stocker sans problèmes tous les caractères minuscules, majuscules, accentués, chiffres, etc. Cette limite ne permet cependant pas de gérer tous les caractères qui existent. Par exemple, on ne pourra pas stocker de caractères chinois dans une variable de type char, puisqu'il existe de l'ordre de 50000 caractères chinois différents. On a donc dû faire un choix des différents caractères que l'on peut représenter.

Suivant la langue que l'on utilise, ou le type de machine, ce choix peut être différent. Il existe cependant un choix standard, qui définit que quelle que soit la machine, un certain nombre de caractères seront toujours disponibles, et seront toujours stockés de la même manière. Ce choix standard est ce que l'on appelle la table ASCII (American Standard Code for Information Interchange). Elle définit précisément les caractères correspondant aux 128 premières valeurs possibles (de 0 à 127).

La table ASCII contient toutes les lettres minuscules, majuscules, chiffres, les signes de ponctuation, un certain nombre de caractères spéciaux (#, $, *, etc.) et des caractères de contrôle. Les caractères accentués n'en font par contre pas partie et sont stockés parmi les 128 possibilités restantes, qui peuvent prendre des valeurs différentes suivant le pays ou la machine.

2.4.4 Calculs et comparaisons

Les caractères étant stockés comme des nombres, ils peuvent être manipulés comme tels. On peut ainsi effectuer des additions sur des caractères, des comparaisons, des incrémentations, etc. Pour que ces calculs aient un sens, il est nécessaire d'avoir en tête un certain nombre de points relatifs aux codes ASCII des caractères les plus utilisés :

Remarque : Pour plus de détails sur le code ASCII, et en particulier la description complète des 128 caractères, vous pouvez consulter l'article correspondant sur Wikipedia.

Ces propriétés permettent par exemple de tester facilement si une lettre majuscule se trouve après une position donnée dans l'alphabet. Voici un exemple de code qui l'utilise :

#include <stdio.h>

int main()
{
   char caractereLu;
   scanf("%c", &caractereLu);
   if (caractereLu < 'A')
      printf("Ce caractère n'est pas une lettre majuscule\n");
   else if (caractereLu > 'Z')
      printf("Ce caractère n'est pas une lettre majuscule\n");
   else if (caractereLu <= 'M')
      printf("Ce caractère est dans la 1ère moitié de l'alphabet\n");
   else
      printf("Ce caractère est dans la 2ème moitié de l'alphabet\n");
   return 0;
}

Remarque : nous verrons dans un chapitre ultérieur qu'il est possible de regrouper plusieurs tests, afin d'éviter de répéter exactement la même instruction comme on l'a fait ici. On se contentera de les répéter, en attendant.

Exercice : écrivez un programme qui lit un caractère en entrée, et affiche "minuscule" si le caractère est une lettre minuscule non accentuée, "majuscule" si c'est une majuscule, et "autre" si ce n'est aucun des deux. Vous pouvez vous servir du fait que dans la table ASCII, les lettres majuscules se trouvent avant les lettres minuscules.


Exercice : écrivez un programme qui affiche tous les caractères de l'alphabet, en majuscules, avec un espace entre chaque caractère. Vous devez utiliser une boucle et non écrire la chaîne directement dans le programme.


2.5 Les nombres décimaux

2.5.1 Déclaration, notations classique et scientifique

Tous les nombres que nous avons manipulés jusqu'à présent étaient des entiers. Il est possible de faire de nombreuses choses uniquement avec des entiers, mais lorsque l'on fait des calculs, on souhaite généralement pouvoir manipuler des nombres décimaux. Il est possible de les manipuler en langage C, avec le type double. Nous verrons cependant que c'est un type de données à manipuler avec précaution.

Pour déclarer une variable permettant de stocker un nombre décimal, on utilise le type double, avec la syntaxe suivante :

   double nombreDecimal;

Une fois une variable déclarée, on peut bien sûr y stocker des valeurs. On utilise pour cela l'opérateur d'affectation "=", suivi de la valeur du nombre décimal. On écrit cependant ce nombre avec un point et non une virgule. Ceci vient simplement du fait que c'est comme cela qu'on écrit les nombres décimaux en anglais. Par exemple pour stocker la valeur 2,5 dans notre variable, on écrira :

   nombreAVirgule = 2.5;

Dans le cas où le nombre décimal que vous voulez exprimer ne comporte aucune décimale, comme par exemple la valeur 1, il est important de le représenter tout de même avec un point décimal, pour qu'il ne soit pas considéré comme un entier. Par exemple l'expression 10 / 4 vaudra 2, alors que l'expression 10. / 4. vaudra 2.5. On peut également ajouter un 0 après le point, pour avoir une apparence plus naturelle : 4.0 .

Il est également possible d'exprimer un nombre décimal en notation scientifique, c'est-à dire en le donnant sous la forme de deux nombres : le premier est appelé la mantisse et est un nombre décimal compris entre 1 et 10 (exclu). Le deuxième est l'exposant : la puissance de 10 par laquelle on doit multiplier la mantisse pour obtenir le nombre décimal que l'on souhaite représenter. Les deux nombres sont séparés par le caractère 'e'

Par exemple, le nombre 234.567 s'écrit 2.34567e2, car 234.567 est égal à 2.34567 multiplié par 102. On écrit alors :

   nombreAVirgule = 2.34567e2;

On peut également avoir un exposant négatif, qui correspond à une puissance de 10 négative. Par exemple, 0.1 s'écrit 1e-1, 0.01 s'écrit 1e-2, etc. On peut considérer la valeur de l'exposant comme étant le nombre de chiffres duquel on doit déplacer le point décimal (équivalent de la virgule en français). On déplace le point vers la droite pour un exposant négatif et vers la gauche pour un exposant positif, en ajoutant des 0 si nécessaire.

Voici quelques autres exemples de nombres représentés en notation scientifique :

   0.15 : 1.5e-1
   1000000000 (un milliard) : 1e9
   4230000000 : 4.23e9
   -0.0001243 : -1.243e-4

L'un des intérêts de la notation scientifique est qu'elle permet de représenter simplement de très grands nombres, ou de très petits nombres. Par exemple, si l'on veut écrire un milliard de milliards de milliards, soit un 1 suivi de 27 zéros, on peut écrire tout simplement : 1e27.

Exercice : représentez les nombres suivants en notation scientifique :

   200.0
   0.024
   436789000.0

Solution :

   200.0 : 2e2
   0.024 : 2.4e-2
   436789000. : 4.36789e8

Exercice : représentez les nombres suivants en notation classique (non scientifique) :

   2.45e6
   1.23e-3
   4e4

Solution :

   2.45e6 : 2450000.0
   1.23e-3 : 0.00123
   4e4 : 40000.0

2.5.2 Affichage et lecture au clavier

Pour afficher un nombre décimal à l'écran, on utilise une fois encore l'instruction printf, cette fois avec la chaîne de caractères "%lf". Voici un exemple :

#include <stdio.h>

int main()
{
   double nombreDecimal = 425.21;
   printf("Le nombre est : %lf\n", nombreDecimal);
   return 0;
}

Il est également possible d'afficher le nombre en notation scientifique, en utilisant la chaîne "%e".

De même, pour lire un nombre décimal au clavier, on utilisera scanf, avec également la chaîne "%lf". Le nombre entré au clavier doit être au même format que ce que l'on a décrit plus haut, en notation avec un point décimal ou scientifique. Essayez par exemple le programme suivant :

#include <stdio.h>

int main()
{
   double nombreDecimal;
   scanf("%lf", &nombreDecimal);
   printf("Valeur : %lf\n", nombreDecimal);
   return 0;
}

2.5.3 Stockage en mémoire, limites et précision

Comme dans le cas des entiers ou des caractères, les variables décimales sont stockées avec une place limitée en mémoire, qui est de 64 bits pour le type double. La manière dont ces bits sont utilisés est cependant assez différente de celle utilisée pour les entiers et caractères. Les nombres sont en fait stockés en notation scientifique, c'est à dire qu'un certain nombre de bits sont destinés à stocker les chiffres de la mantisse, alors que les bits restants sont utilisés pour stocker l'exposant. On dit ainsi qu'on stocke les nombres en virgule flottante, puisque l'exposant permet de placer la virgule à la position que l'on souhaite (par opposition à un stockage où l'on aurait un nombre limité de chiffres avant, et après la virgule).

Une conséquence de cette manière de stocker les nombres décimaux, est que l'on ne peut pas simplement définir une valeur minimale et une valeur maximale, avec toutes les valeurs entre les deux possibles. Déterminer précisément l'ensemble des valeurs possibles d'une variable de type double n'est pas simple, et nous nous contenterons donc d'approximations générales. De plus, les limites peuvent varier suivant la machine utilisée. Voici cependant les limites que l'on a en général :

Ces valeurs permettent d'avoir une idée de la précision des nombres que l'on peut stocker dans une variable de type double. Il faut surtout garder en tête qu'il n'est pas toujours possible de stocker exactement la valeur que l'on souhaite. D'une part il n'est pas possible de stocker des valeurs en dehors des limites citées ci-dessus, d'autre part, il faut savoir que tout nombre stocké dans un double n'est qu'une approximation d'un nombre, avec une certaine précision.

Supposons par exemple que vous vouliez stocker la valeur 1/3 (un tiers). Cette valeur s'écrit en base 10 sous la forme 0.333333333... avec une infinité de 3. Il n'est donc pas possible de la stocker exactement, avec un nombre limité de chiffres. Dans le cas d'un double, c'est le cas aussi de valeurs comme 1/10. En effet, si 1/10 peut s'écrire exactement 0.1 en base 10, son écriture en base binaire demande une infinité de chiffres, comme 1/3 demandait une infinité de chiffres en base 10.

Nous n'entrerons pas plus en détail dans les subtilités du stockage des double, dans ce cours. L'important est simplement de garder en tête le fait que l'on ne manipule pas de valeurs exactes. Exécuter le code suivant vous permettra de vous en rendre compte concrètement :

#include <stdio.h>

int main()
{
   double valeur;

   valeur = 100000000000.1;
   printf("Valeur : %lf\n", valeur);
   return 0;
}

Le texte affiché sera probablement :

Valeur : 100000000000.100006

La raison en est que la valeur exacte 100000000000.1 n'est pas représentable avec une variable double, et que 100000000000.100006 est la valeur représentable qui en est la plus proche. On peut ainsi représenter les valeurs suivantes, mais aucune valeur intermédiaire :

100000000000.099991
100000000000.100006
100000000000.100021

2.5.3 Calculs et comparaisons de nombre décimaux.

La plupart des opérateurs que l'on a utilisés pour les nombres entiers fonctionne aussi pour les nombres décimaux. L'opérateur '/' ne représente plus la division entière, mais la division au sens plus général. L'opérateur modulo '%' n'existe par contre pas pour les variables de type double. Il est possible de faire des calculs de modulo avec des nombre décimaux, mais ils ne se font pas avec cet opérateur.

Les autres opérateurs fonctionnent de la même manière que pour les entiers, à ceci près que le résultat n'est jamais qu'une approximation, contrairement aux entiers pour lesquels l'on obtient des valeurs exactes tant que l'on ne dépasse pas les limites des valeurs possibles.

Il y a deux manières de dépasser les limites des valeurs possibles, avec des nombres décimaux. L'une consiste à dépasser la valeur maximale que l'on puisse stocker dans un double, l'autre consiste à dépasser la précision possible avec ce type. Voyons tout d'abord ce qui se passe dans le premier cas :

#include <stdio.h>

int main()
{
   double nombre = 1e308;
   printf("Le nombre vaut %e\n", nombre);
   nombre *= 10.0;
   printf("Le nombre vaut %e\n", nombre);
   nombre /= 10.0;
   printf("Le nombre vaut %e\n", nombre);
   return 0;
}

Voici ce que ce programme affiche :

Le nombre vaut 1.000000e+308
Le nombre vaut inf
Le nombre vaut inf

Le texte "inf" signifie "infini". Le type double permet en effet de représenter l'infini, et de l'utiliser dans les calculs. On obtient la valeur "inf" lorsque l'on dépasse la valeur maximale possible, d'une manière ou d'une autre. La valeur "inf" peut ensuite être utilisée dans les calculs. On peut ensuite enlever ou ajouter n'importe quelle valeur à "inf", on obtiendra toujours "inf". On obtient ainsi "inf" sur la troisième ligne de sortie de notre programme, après avoir divisé "inf" par 10. Il existe également une valeur représentant l'infini négatif : "-inf". Une autre manière d'obtenir la valeur "inf" est de diviser un nombre par 0.

Voyons maintenant le deuxième cas : celui où l'on dépasse la précision du type double.

#include <stdio.h>

int main()
{
   double nombre = 1e20;
   nombre++;
   if (nombre != 1e20)
      printf("Le nombre a changé\n");
   else
      printf("Le nombre est le même\n");
   return 0;
}

Ce programme affichera "Le nombre est le même", bien qu'on ait ajouté 1 à notre nombre. Le problème vient du fait que le nombre 1e20 + 1.0 demanderait une précision de 21 chiffres pour être représenté (un 1, 19 zéros, puis à nouveau un 1). Lorsque l'on fait des calculs dont l'effet demande plus de précision que le type double ne peut le gérer, les calculs n'ont tout simplement pas d'effet.

Un autre point important concernant la manipulation du type double est que comme les valeurs ne sont jamais que des approximations, la notion d'égalité n'a pas vraiment de sens. Essayez le code suivant, pour vous en rendre compte.

#include <stdio.h>

int main()
{
   if (0.1 + 0.2 != 0.3)
      printf("la somme est inexacte\n");
   return 0;
}

Le résultat de ce programme peut dépendre de la machine, mais sur notre serveur d'évaluation par exemple, vous obtiendrez probablement le résultat suivant :

la somme est inexacte

Il ne s'agit nullement d'une erreur de notre serveur, mais est simplement dû au fait que l'on travaille avec des approximations. Il n'est pas possible de stocker dans un double, la valeur exacte de 0.1, 0.2, ou 0.3. La valeur approchée que l'on obtient en ajoutant 0.1 et 0.2 n'est donc pas nécessairement la même que celle de 0.3. Les valeurs sont très proches, mais pas forcément exactement identiques. On peut donc énoncer la règle suivante :

Il ne faut jamais utiliser les opérateurs de comparaison "==" ou "!=" pour tester la valeur de variables de type double.

Pour tester effectivement si deux valeurs sont quasiment les mêmes, il faudra ainsi s'assurer que leur différence ne dépasse pas un certain seuil, en valeur absolue.


2.6 Mélange de types

2.6.1 Conversion implicite du type

Les variables de type int, char et double ont beau représenter des valeurs assez différentes, elles ne sont pas entièrement incompatibles. On a déjà vu qu'une variable de type char pouvait être vue comme un entier et on a par exemple écrit :

   char caractere = 65;

Lorsque l'on écrit cela, c'est en fait une valeur de type int que l'on fournit (65), et que l'on demande de stocker dans notre variable char. La valeur int 65 est en quelque sorte convertie en sa valeur char 'A', avant d'être stockée dans la variable. C'est ce que l'on appelle une conversion implicite de type. On n'a pas spécifiquement indiqué que l'on voulait transformer le type d'une valeur, mais c'est une conséquence obligée de ce que l'on a écrit.

On peut effectuer des conversions implicites entre tous les types de variables, de la même manière. On peut par exemple convertir (implicitement) une valeur caractère en une valeur entière. Essayez le programme suivant pour comprendre ce qui se passe :

#include <stdio.h>

int main()
{
   char caractereLu;
   int codeAscii;
   scanf("%c", &caractereLu);
   codeAscii = caractereLu;
   printf("Le code ASCII de ce caractère est : %d\n", codeAscii);
   return 0;
}

Si vous tapez 'A' puis entrée, le programme affichera :

Le code ASCII de ce caractère est : 65

L'instruction codeAscii = caractereLu est ici une nouvelle conversion implicite de type : on convertit le contenu d'une variable de type char en type int, pour le stocker dans l'autre variable.

Lorsque l'on convertit un caractère en entier, c'est simplement la valeur numérique du caractère qui est obtenue. L'information est toujours exactement la même, elle est simplement vue comme étant d'un type différent. C'est différent si on effectue l'opération inverse. Regardez par exemple le programme suivant :

#include <stdio.h>

int main()
{
   int valeurInt = 342;
   char valeurChar;
   valeurChar = valeurInt;
   valeurInt = valeurChar;
   printf("L'entier vaut maintenant : %d\n", valeurInt);
   return 0;
}

Ce programme affichera le texte suivant :

L'entier vaut maintenant : 86

La raison est que lors de l'exécution de l'instruction valeurChar = valeurInt;, la valeur de la variable valeurInt n'a pas pu être conservée : 342 est supérieur à 127, qui est la plus grande valeur numérique pouvant être stockée dans une variable char. La valeur qui a été stockée est en fait 342 % 128 (342 = 128 * 2 + 86). Lorsque l'on reconvertit la valeur caractère en valeur de type int avec l'instruction valeurInt = valeurChar;, on ne retrouve ainsi pas la valeur initiale, mais 86 : de l'information a été perdue. Bien sûr, ceci ne se produit que si la valeur entière dépasse les limites du type char.

De la même manière, il est possible de convertir une valeur entière en valeur décimale. Lorsque l'on écrit :

   double valeurDecimale = 42;
On a écrit la valeur 42 sans point décimal, ce qui signifie qu'on la considère comme un entier. Cette valeur est alors implicitement convertie en valeur du type double, soit 42.0, avant d'être stockée dans la variable. Une variable de type double pouvant stocker sans problèmes des nombres allant jusqu'à plus de 2 milliards, avec suffisamment de précision pour conserver tous les chiffres, on ne perd pas d'information lorsque l'on convertit un int en double. La valeur stockée n'est plus "exactement" la valeur entière fournie, mais c'en est une approximation très très proche.

On peut finalement convertir une variable de type double en entier :

   int valeurEntiere = 42.54;

Dans ce cas, c'est la partie entière du nombre décimal qui sera stockée dans l'entier, à savoir 42. Les décimales seront perdues à jamais. Bien sûr, on perd également de l'information si l'on dépasse la valeur maximale pouvant être stockée dans un int.

Les conversions entre double et char fonctionnent selon le même principe que les conversions entre double et int.

Exercice : écrivez un programme qui lit un nombre décimal au clavier et qui affiche la valeur de ce nombre, arrondie à l'entier le plus proche. Dans le cas où la partie décimale du nombre est égale à 0.5, on arrondira à l'entier supérieur. Par exemple, pour 4.5, on devra afficher 5.


Exercice : écrivez un programme qui lit un nombre décimal au clavier et qui calcule la valeur de ce nombre arrondie au centième inférieur, puis l'affiche.


2.6.2 Conversion explicite du type

Dans les exercices de la section précédente, nous avons utilisé des variables intermédiaires de différents types, pour profiter des possibilités des conversions. Nous allons maintenant voir comment on peut s'en passer, avec les conversions explicites.

Supposons que nous souhaitions obtenir la partie entière d'une variable double, mais garder le résultat dans cette même variable. On peut écrire :

   double valeurDecimale = 43.245;
   int valeurEntiere = valeurDecimale;
   valeurDecimale = valeurEntiere;

Le fait de passer par une variable de type int a ainsi forcé la conversion, donc le calcul de la partie entière. Il est cependant possible de faire la même chose plus simplement, de la manière suivante :

   double valeurDecimale = 43.245;
   valeurDecimale = (int)valeurDecimale;

Dans cet exemple, on a converti explicitement le contenu de la variable valeurDecimale en un entier (int), avant de stocker le résultat dans la variable de type double. La règle appliquée est la suivante :

Pour convertir explicitement une valeur d'un type donné en une valeur d'un nouveau type, on place le nom du nouveau type entre parenthèses, devant la valeur à convertir.

Exercice : refaire le deuxième exercice de la section précédente, en utilisant cette fois la conversion explicite de type, pour obtenir un programme plus court : écrivez un programme qui lit un nombre décimal au clavier et qui calcule la valeur de ce nombre arrondie au centième inférieur, puis l'affiche.


2.7 Erreurs

Essayez de compiler le programme suivant :

#include <stdio.h>

int main()
{
   int test = 42.5 % 2;
   return 0;
}

Vous obtiendrez une erreur dans ce genre :

test.c:5: invalid operands to binary %

Cela signifie "Opérandes invalides pour l'opérateur binaire % ", ce qui veut dire que les opérandes de l'opérateur % (les expressions que vous avez placées de chaque côté du %) ne sont pas valides. En l'occurence, le problème est qu'ils n'ont pas le bon type : l'opérateur % ne fonctionne pas avec des double, or l'opérande de gauche, 42.5 est un double. Aucune conversion implicite ne peut se faire dans ce cas. Vous devez écrire explicitement (int)42.5 si vous voulez qu'une conversion se fasse.


© Aflo Informatique , 2003-2008