Initiation au langage C

Jessee Michaël C. Edouard

Ce tutoriel va vous apprendre les concepts de base du langage C. Il n'est ni une référence ni une définition du langage mais a été écrit dans le but d'aider le lecteur à le prendre en main avant d'étudier les concepts avancés de ce langage. Bonne lecture.

Commentez cet article : Commentez

- Titre : Initiation au langage C

- Auteur : Jessee Michaël C. Edouard

- Parution : 24 mars 2008

- Mise à jour : 13 juin 2010

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2008 Melem. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. Historique

- I-B. Caractéristiques du langage C

- I-B-1. Universalité

- I-B-2. Concision, souplesse

- I-B-3. Puissance

- I-B-4. Portabilité

- I-B-5. Omniprésence

- I-C. A propos de ce document

- I-D. Remerciements

- II. Instruction, programme et fonction

- II-A. Structure générale d'un programme C

- II-A-1. Un premier programme

- II-A-2. Les commentaires

- II-A-3. Sensibilité à la casse

- II-A-4. Le format libre

- II-B. Du fichier source à l'exécutable

- II-B-1. La compilation

- II-B-2. Applications consoles

- II-B-3. Manipulations avec Visual C++ et Code::Blocks

- II-B-3-a. Avec Visual C++ 6

- II-B-3-b. Avec Visual Studio .NET

- II-B-3-c. Avec Code::Blocks 1.0

- II-C. Les fonctions

- II-C-1. Introduction

- II-C-2. Exemple avec une fonction "mathématique"

- II-C-3. Exemple avec une "procédure"

- II-C-4. Remarques

- II-D. Les macros

- II-D-1. Le préprocesseur

- II-D-2. Définition

- II-E. Exercices

- II-E-1. La lettre X

- II-E-2. Périmètre d'un rectangle

- III. Expressions et instructions

- III-A. Introduction aux types de données du langage C

- III-A-1. Les types de base du langage C

- III-A-2. Règle d'écriture des constantes littérales

- Nombres entiers

- Nombres flottants

- Caractères et chaînes de caractères

- III-A-3. Spécification de format dans printf

- III-A-3-a. Généralités

- III-A-3-b. Les "options"

- III-A-3-c. Le type entier

- III-A-3-d. Le type flottant

- III-A-4. Les variables et les constantes

- III-A-5. Définition de nouveaux types

- III-B. Les pointeurs

- III-B-1. Définition

- III-B-2. Saisir des données tapées au clavier avec la fonction scanf

- III-B-3. Exemple de permutation des contenus de deux variables

- III-C. Les expressions

- III-C-1. Introduction

- III-C-2. lvalue et rvalue

- III-C-3. Opérations usuelles

- III-C-3-a. Les opérateurs arithmétiques courants

- III-C-3-b. Les opérateurs de comparaison

- III-C-3-c. Les opérateurs logiques

- III-C-4. L'opérateur virgule

- III-C-5. Taille des données. L'opérateur sizeof

- III-C-6. Les opérateurs d'incrémentation et de décrémentation

- III-C-7. Expressions conditionnelles

- III-C-8. Autres opérateurs d'affectation

- III-C-9. Ordre de priorité des opérateurs

- III-D. Considérations liées à la représentation binaire

- III-D-1. Généralités

- III-D-2. Les caractères

- III-D-3. Dépassement de capacité

- III-E. La conversion de type

- III-E-1. Conversion implicite

- III-E-2. Conversion explicite (cast)

- III-F. Les instructions

- III-F-1. Introduction

- III-F-2. Bloc d'instructions

- III-F-3. L'instruction if

- III-F-4. L'instruction do

- III-F-5. L'instruction while

- III-F-6. L'instruction for

- III-F-7. Les instructions switch et case

- III-F-8. L'instruction break

- III-F-9. L'instruction continue

- III-F-10. L'instruction return

- III-F-11. L'instruction vide

- III-G. Exercices

- III-G-1. Valeur absolue

- III-G-2. Moyenne

- III-G-3. L'heure dans une minute

- III-G-4. Rectangle

- III-G-5. Triangle isocèle

- III-G-6. Somme

- IV. Tableaux, pointeurs et chaînes de caractères

- IV-A. Les tableaux

- IV-A-1. Définition

- IV-A-2. Initialisation

- IV-A-3. Création d'un type « tableau »

- IV-A-4. Les tableaux à plusieurs dimensions

- IV-A-5. Calculer le nombre d'éléments d'un tableau

- IV-B. Les pointeurs

- IV-B-1. Les tableaux et les pointeurs

- IV-B-2. L'arithmétique des pointeurs

- IV-B-3. Pointeurs constants et pointeurs sur constante

- IV-B-4. Pointeurs génériques

- IV-B-5. Exemple avec un tableau à plusieurs dimensions

- IV-B-6. Passage d'un tableau en argument d'une fonction

- IV-C. Les chaînes de caractères

- IV-C-1. Chaîne de caractères

- IV-C-2. Longueur d'une chaîne

- IV-C-3. Représentation des chaînes de caractères en langage C

- IV-C-4. Les fonctions de manipulation de chaîne

- strcpy, strncpy

- strcat, strncat

- strlen

- strcmp, strncmp

- IV-C-5. Fusion de chaînes littérales

- IV-D. Exercices

- IV-D-1. Recherche dans un tableau

- IV-D-2. Calcul de la moyenne

- IV-D-3. Manipulation de chaînes

- V. Les entrées/sorties en langage C

- V-A. Introduction

- V-B. Les fichiers

- V-C. Les entrée et sortie standards

- V-D. Exemple : lire un caractère, puis l'afficher

- V-E. Saisir une chaîne de caractères

- V-F. Lire une ligne avec fgets

- V-G. Mécanisme des entrées/sorties en langage C

- V-G-1. Le tamponnage

- V-G-1-a. Les tampons d'entrée/sortie

- V-G-1-b. Les modes de tamponnage

- V-G-2. Lire de manière sûre des données sur l'entrée standard

- VI. L'allocation dynamique de mémoire

- VI-A. Les fonctions malloc et free

- VI-B. La fonction realloc

- VI-C. Exercices

- VI-C-1. Calcul de la moyenne (version 2)

- VI-C-2. Recherche dans un tableau (version 2)

- VII. Solutions des exercices

- VII-A. La lettre X (II-E-1)

- VII-B. Périmètre d'un rectangle (II-E-2)

- VII-C. Valeur absolue (III-G-1)

- VII-D. Moyenne (III-G-2)

- VII-E. L'heure dans une minute (III-G-3)

- VII-F. Rectangle (III-G-4)

- VII-G. Triangle isocèle (III-G-5)

- VII-H. Somme (III-G-6)

- VII-I. Recherche dans un tableau (IV-D-1)

- VII-J. Calcul de la moyenne (IV-D-2)

- VII-K. Manipulation de chaînes (IV-D-3)

- VII-L. Calcul de la moyenne (version 2) (VI-C-1)

- VII-M. Recherche dans un tableau (version 2) (VI-C-2)

- VIII. Conclusion

I - Introduction

I-A - Historique

L'histoire du langage C est intimement liée à celle du système d'exploitation UNIX. En 1969, Ken Thompson, qui travaillait pour Bell Laboratories, mit au point sur un DEC PDP-7 un système d'exploitation semblable à Multics mais plus simple et plus modeste. Ce système reçut par la suite le nom d'Unics (Uniplexed Information and Computing System) qui ne tarda pas à être définitivement changé en UNIX.

A l'époque, le seul langage vraiment adapté à l'écritute de systèmes d'exploitation était le langage d'assemblage. Ken Thompson développa alors un langage de plus haut niveau (c'est-à-dire plus proche du langage humain), le langage B (dont le nom provient de BCPL, un sous-ensemble du langage CPL, lui-même dérivé de l'Algol, un langage qui fut populaire à l'époque), pour faciliter l'écriture d'UNIX. C'était un langage faiblement typé (un langage non typé, par opposition à un langage typé, est un langage qui manipule les objets sous leur forme binaire, sans notion de type (caractère, entier, réel, ...)) et trop dépendant du PDP-7 pour permettre de porter UNIX sur d'autres machines. Alors Denis Ritchie et Brian Kernighan, de Bell Laboratories également, améliorèrent le langage B pour donner naissance au langage C. En 1973, UNIX fut réécrit entièrement en langage C. Pendant 5 ans, le langage C fut limité à l'usage interne de Bell jusqu'au jour ou Brian Kernighan et Denis Ritchie publièrent une première définition du langage dans un ouvrage intitulé The C Programming Language. Ce fut le début d'une révolution dans le monde de l'informatique.

Grâce à sa puissance, le langage C devint rapidement très populaire et en 1983, l'ANSI (American National Standards Institute) décida de le normaliser en ajoutant également quelques modifications et améliorations, ce qui donna naissance en 1989 au langage tel que nous le connaissons aujourd'hui. L'année suivante, le langage C a été adopté par l'ISO (International Organization for Standardization) et actuellement, par l'ISO et l'IEC (International Electrotechnical Commission), et a connu au fil du temps certaines révisions (dans l'immense majorité des cas toujours compatibles avec les versions antérieures) dont les plus importantes sont celles de 1990 (adoption par l'ISO), de 1995 (amélioration du support du développement de programmes internationnaux) et de 1999 (ajout d'un nombre important de nouvelles fonctionnalités). La version de 1999 est cependant actuellement encore peu diffusée c'est pourquoi dans ce document, nous nous placerons dans l'apprentissage des versions de 1990 et 1995.

I-B - Caractéristiques du langage C

I-B-1 - Universalité

Langage de programmation par excellence, le C n'est pas confiné à un domaine particulier d'applications. En effet, le C est utilisé dans l'écriture

- de systèmes d'exploitations (comme Windows, UNIX et Linux) ou de machines virtuelles (JVMs, Runtimes et Frameworks .NET, logiciels de virtualisation, etc.)

- de logiciels de calcul scientifique, de modélisation mathématique ou de CFAO (Matlab, R, Labview, Scilab, etc.)

- de bases de données (MySQL, Oracle, etc.)

- d'applications en réseau (applications intranet, internet, etc.)

- de jeux vidéo (notamment avec OpenGL ou la SDL et/ou les différents moteurs (de jeu, physique ou 3D) associés))

- d'assembleurs, compilateurs, débogueurs, interpréteurs, de logiciels utilitaires et dans bien d'autres domaines encore.

Oui, le C permet tout simplement de tout faire, ce qui lui a valu, à juste titre, l'appelation courante "the God's programming language" (le langage de programmation de Dieu). A cause de son caractère proche du langage de la machine, le C est cependant peu productif, ce qui signifie qu'il faut souvent écrire beaucoup pour faire peu. C'est donc le prix à payer pour l'utilisation de ce langage surpuissant !

I-B-2 - Concision, souplesse

C'est un langage concis, très expressif, et les programmes écrits dans ce langage sont très compacts grâce à un jeu d'opérateurs puissant.

I-B-3 - Puissance

Le C est un langage de haut niveau mais qui permet d'effectuer facilement des opérations de bas niveau et d'accéder aux fonctionnalités du système, ce qui est la plupart du temps impossible ou difficile à réaliser dans les autres langages de haut niveau. C'est d'ailleurs ce qui fait l'originalité du C, et aussi la raison pour laquelle il est autant utilisé dans les domaines où la performance est cruciale comme la programmation système, le calcul numérique ou encore l'informatique embarquée.

I-B-4 - Portabilité

C'est un langage qui ne dépend d'aucune plateforme matérielle ou logicielle, c'est-à-dire qui est entièrement portable. De plus, de par sa simplicité, écrire un compilateur C pour un processeur donné n'est pas beaucoup plus compliqué que d'écrire un assembleur pour ce même processeur. Ainsi, là où l'on dispose d'un assembleur pour programmer, on dispose aussi généralement d'un compilateur C, d'où l'on dit également que le C est un "assembleur portable".

I-B-5 - Omniprésence

La popularité du langage mais surtout l'élégance des programmes écrits en C est telle que son style et sa syntaxe ont influencé de nombreux langages :

- C++ et Objective C sont directement descendus du C (on les considère souvent comme des extensions du C)

- C++ a influencé Java

- Java a influencé JavaScript

- PHP est un mélange de C et de langage de shell-scripting sous Linux

- C# est principalement un mix de C++ et de Java, deux langages de style C, avec quelques influences fonctionnelles d'autres langages comme Delphi et Visual Basic (le # de C# vient-il dit-on de deux "++" superposés)

- Et on risque d'y passer la journée ...

Connaître le C est donc très clairement un atout majeur pour quiconque désire apprendre un de ces langages.

I-C - A propos de ce document

Ce document contient des cours et des exercices. Chaque fois que vous tombez sur des exercices, traitez-les avant de passer au chapitre ou au paragraphe suivant.

I-D - Remerciements

J'adresse un remerciement tout particulier à JagV12 pour sa relecture très attentive de cet article ainsi qu'à Franck.h pour ses conseils et son soutien.

II - Instruction, programme et fonction

II-A - Structure générale d'un programme C

II-A-1 - Un premier programme

Nous allons écrire un programme qui affiche Hello, world en langage C.

hello.c

#include <stdio.h>

int main()
{
printf("Hello, world\n");
return 0;
}

Voici la sortie de ce programme (c'est-à-dire, ce que nous, utilisateurs, verrons affiché sur le périphérique de sortie standard) :

Hello, world

Le caractère '\n' dans la chaîne de caractères "Hello, world\n" représente le caractère de fin de ligne. Il permet d'insérer une nouvelle ligne (new line).

En langage C, la « brique » qui permet de créer un programme est la fonction. Un programme écrit en C est donc constitué d'une ou plusieurs fonctions, une fonction étant généralement composée d'une ou plusieurs instructions, chaque instruction élémentaire devant se terminer par un point-virgule.

printf est une fonction qui permet d'afficher du texte sur la sortie standard, par défaut l'écran. main est également une fonction, c'est celle qui sera automatiquement appelée à l'exécution. On l'appelle le point d'entrée du programme ou encore la fonction principale. Ici, elle est seulement composée de deux instructions :

1. printf("Hello, world\n");

2. return 0;

Selon la norme officielle du langage C, main est une fonction qui doit retourner un entier (int). Chez de nombreux systèmes (dont Windows et UNIX), cet entier est appelé le code d'erreur de l'application. En langage C, bien que cela ne soit pas forcément le cas pour le système d'exploitation, on retourne 0 pour dire que tout s'est bien passé.

Le langage C impose (à quelques exceptions près que nous verrons plus bas) qu'une fonction doit avoir été déclarée avant de pouvoir être utilisé (nous verrons plus tard ce que c'est qu'une déclaration). Dans notre exemple, puisque nous utilisons la fonction printf, nous devons tout d'abord la déclarer. Le moyen le plus simple de le faire est d'inclure le fichier stdio.h, qui contient entre autres la déclaration de cette fonction, à l'aide de la directive include :

1. #include <stdio.h>

II-A-2 - Les commentaires

On peut insérer des commentaires n'importe où dans du code C à l'aide des délimiteurs /* et */. Les commentaires permettent de rendre les programmes plus lisibles. Voici un exemple d'utilisation de commentaires :

hello.c

#include <stdio.h>

int main()
{
/* Ce programme affiche "Hello, world" */
printf("Hello, world\n");
return 0;
}

II-A-3 - Sensibilité à la casse

Le C est sensible à la casse : il fait la distinction entre les majuscules et les minuscules. Printf et printf sont deux choses complètement différentes et qui n'ont strictement rien à voir ...

II-A-4 - Le format libre

En langage C, les espaces (y compris les tabulations et les retours à la ligne) peuvent être utilisées à volonté (ou pas du tout) avant ou après un séparateur (# < > ( ) { } ; …). On traduit ce fait en disant que le C est un langage au format libre. Une chaîne ("...") doit cependant toujours tenir sur une seule ligne (nous reviendrons sur ce point plus tard). Ainsi, notre programme qui affiche "Hello, world" aurait également pu s'écrire :

1. #   include    <stdio.h>

2. int

3. main ( )

4. {   printf    (

5. "Hello, world\n"

6. );return

8. 0

10. ;}

Dans certains cas, les espaces peuvent cependant être utilisés de manière précise. Par exemple, avec :

1. #include < stdio.h >

Vous demandez d'inclure le fichier <espace>stdio.h<espace> (qui a de fortes chances de ne pas exister, ce qui génèrerait des erreurs à la compilation ...) et non le fichier stdio.h !

II-B - Du fichier source à l'exécutable

II-B-1 - La compilation

Avant de définir la compilation, il y a deux expressions qu'il faut déjà avoir défini. Il s'agit de "fichier source" et "fichier exécutable".

- Un fichier source est un fichier contenant, dans le cas qui nous intéresse, des lignes de code écrits en C. Le code contenu dans le fichier source est appelé du code source. Les fichiers sources C portent généralement l'extension ".c" (par exemple : hello.c).

- Un fichier exécutable est un fichier contenant, entre autres, du code directement exécutable par le processeur. Ce code est appelé code machine. L'extension portée par les fichiers exécutables varient selon le système. Sous Windows par exemple, ils portent généralement l'extension ".exe" (par exemple : hello.exe). Sous Linux, ils ne portent généralement pas d'extension.

La compilation c'est, en première approximation, le processus pendant lequel le fichier source (qui ne contient que du texte) est transformé en fichier exécutable (qui contient du code compréhensible par le processeur). Une définition plus précise sera donnée dans les cours plus avancés.

Puisque les programmes C doivent être compilés avant de pouvoir être exécutés, on dit que le C est un langage compilé. Le programme qui effectue la compilation est appelé compilateur.

Nombreux programmeurs utilisent cependant un EDI (Environnement de Développement Intégré) au lieu d'un simple compilateur pour compiler leurs programmes. Un EDI est un logiciel qui intègre un éditeur de texte pour taper les codes sources, un compilateur pour les traduire en exécutable ainsi que d'autres outils aidant à la mise au point et à la distribution des programmes. Il existe de nombreux EDIs pour le langage C. Certains sont gratuits (Code::Blocks Studio, Visual C++ Express, etc.), d'autres payants (CodeGear RAD Studio, Microsoft Visual Studio, etc.). Téléchargez ou achetez et installez le logiciel qui vous convient donc avant de continuer. Pour les manipulations pratiques, nous supposerons dans la suite que nous travaillons sous Windows, mais que cela ne vous empêche de lire ce tutoriel même si vous utilisez un autre système.

II-B-2 - Applications consoles

Le C voulant être un langage entièrement portable, la création d'interfaces graphiques n'est pas supportée en standard par le langage. En effet, la notion d'interface graphique n'existe pas forcément dans tous les systèmes. Dans les systèmes proposant un environnement graphique, comme Windows par exemple, les applications qui en sont dépourvues sont souvent appelées applications consoles. Ces applications, quand elles sont lancées, s'exécutent dans une fenêtre (la fameuse "console") dans laquelle l'utilisateur n'utilise que le clavier pour commander le programme). Dans ce tutoriel, c'est toujours ce type d'applications que allons développer.

II-B-3 - Manipulations avec Visual C++ et Code::Blocks

II-B-3-a - Avec Visual C++ 6

1. Lancez Visual C++.

2. Créez un projet d'application console que vous allez nommer helloworld en choisissant File > New > Project > Win32 Console Application. Un dossier nommé helloworld est alors créé. Ce sera votre répertoire par défaut. Deux fichiers, entre autres, sont également créés : helloworld.prj (votre projet) et helloworld.dsw (votre espace de travail).

3. Choisissez An empty project pour qu'aucun fichier ne soit automatiquement ajouté à notre projet.

4. Ajoutez au projet un nouveau fichier que vous allez nommer hello.c avec la commande Project > Add to Project > New > C++ source file. Nommez-le bien hello.c car si vous omettez l'extension, VC6 va automatiquement ajouter l'extension .cpp et vous aurez donc un fichier source C++ (hello.cpp) au lieu d'un fichier source C, à moins bien sûr que c'est justement ce que vous cherchiez ...

5. Dans l'explorateur de projet (normalement c'est sur votre gauche), cliquez sur File View pour afficher une vue des fichiers qui constituent votre projet. Ouvrez ensuite le dossier Source Files puis double cliquez sur hello.c pour l'ouvrir dans l'éditeur.

6. Saisissez maintenant le code puis compilez avec la commande Build > Build helloworld.exe (F7). Le fichier helloworld\Debug\helloworld.exe est alors créé.

7. Pour tester votre programme sans quitter VC, choisissez Build > Execute helloworld.exe (Ctrl + F5). Le message de type "Appuyez sur une touche pour continuer" ne fait évidemment pas partie de votre programme. C'est juste un message provenant de votre EDI pour éviter la fermeture immédiate de la console à la fin de l'exécution du programme, ce qui ne vous aurait pas permis de "contempler" sa sortie.

II-B-3-b - Avec Visual Studio .NET

La procédure est presque les même que sous VC6. Créez un nouveau projet d'application Windows (Win32 Project) puis dans l'étape Applications Settings de l'assistant, choissez Console Application puis Empty Project. Compilez puis testez.

II-B-3-c - Avec Code::Blocks 1.0

1. Lancez Code::Blocks.

2. Créez un nouveau projet d'application console que vous aller nommer helloworld en choisissant File > New Project > Console Application. Enregistrez-le dans un répertoire de votre choix qui sera alors votre répertoire par défaut. Avant de valider, cochez l'option Do not create any files afin qu'aucun fichier ne soit automatiquement ajouté à notre projet. Une fois que vous avez validé, un fichier helloworld.cbp (votre projet) sera créé dans le répertoire de votre projet.

3. Créez un nouveau fichier que vous allez nommer hello.c avec la commande File > New File. Acceptez que ce fichier soit ajouté au projet.

4. Saisissez maintenant le code puis compilez avec la commande Build > Build (Ctrl + F9). Le fichier helloworld.exe est alors créé.

5. Pour tester votre programme sans quitter Code::Blocks, choisissez Build > Run (Ctrl + F10). Le message de type "Appuyez sur une touche pour continuer" ne fait évidemment pas partie de votre programme. C'est juste un message provenant de votre EDI pour éviter la fermeture immédiate de la console à la fin de l'exécution du programme, ce qui ne vous aurait pas permis de "contempler" sa sortie.

II-C - Les fonctions

II-C-1 - Introduction

Comme nous l'avons déjà dit plus haut, les fonctions sont les briques avec lesquelles on construit un programme en langage C. La vie d'un programmeur C se résume donc à créer puis utiliser des fonctions. Vous allez adorer ! Nous allons donc maintenant voir de plus près ce que c'est qu'une fonction.

II-C-2 - Exemple avec une fonction "mathématique"

En mathématiques, on définit une fonction comme suit :

1. f(x) = x² - 3

Cela signifie que f est une fonction qui reçoit en argument un réel x et qui retourne un réel : x^2 - 3.

Ecrivons une fonction C que nous appellerons f, qui reçoit en argument un entier x et qui retourne également un entier : x^2 - 3.

1. int f(int x)

2. {

3. return x*x - 3;

4. }

Le code ci-dessus constitue ce qu'on appelle la définition ou encore l'implémentation de la fonction f. Voyons maintenant un exemple d'utilisation de cette fonction.

1. #include <stdio.h>

3. int f(int); /* declaration de la fonction f */

5. int main()

6. {

7. int x = 4;

8. printf("f(%d) = %d\n", x, f(x));

9. return 0;

10. }

12. int f(int x)

13. {

14. return x*x - 3;

15. }

La ligne :

1. int f(int);

tient lieu de déclaration de la fonction f. Sans le point-virgule, on a ce qu'on appelle le prototype ou la signature de la fonction f. Ici, le prototype (la signature) indique que "f est une fonction qui nécessite en argument un int (int f(int)) et qui retourne un int (int f(int))". Un prototype suivi d'un point-virgule constitue ce qu'on appelle une déclaration (mais ce n'est pas la seule forme possible de déclaration d'une fonction).

Attention ! Nous avons vu jusqu'ici qu'une instruction élémentaire se termine par un point-virgule. Cela ne signifie en aucun cas que tout ce qui se termine par un point-virgule est donc une « instruction ». Une déclaration est une déclaration, pas une instruction. Nous définirons ce que c'est exactement une instruction encore plus loin.

Le %d dans la chaîne passée en premier argument de printf est ce qu'on appelle un spécificateur de format. Il renseigne sur la manière dont nous souhaitons afficher le texte. Ici, on veut afficher les nombres 4 et 13 (f(4)). Nous disons donc à printf d'utiliser le format « nombre entier » (%d) pour les afficher. Le premier %d correspond au format que nous voulons utiliser pour afficher x et le deuxième pour f(x). Nous y reviendrons un peu plus loin.

Sachez également que la variable x dans la fonction main n'a absolument rien à voir avec la variable x en paramètre de la fonction f. Chaque fonction peut avoir ses propres variables et ignore complètement ce qui se passe chez les autres fonctions. On vient ainsi d'introduire le concept de "variables locales" que nous étudierons également plus tard.

II-C-3 - Exemple avec une "procédure"

Certains langages de programmation (Pascal, VB (Visual Basic), etc.) font la distinction entre "fonction" et "procédure". En langage C et ses dérivés, il n'y a pas cette distinction. Ce que chez certains langages on appelle procédure est en effet ni plus ni moins qu'une fonction qui effectue un traitement mais qui ne retourne pas de valeur, c'est-à-dire une fonction qui ne retourne rien. En langage C, ce "rien" est indiqué par le mot-clé void (terme anglais signifiant "vide"). Voici un exemple de fonction qui ne requiert aucun argument et qui ne retourne rien : une fonction qui affiche "Bonjour." 3 fois (en 3 lignes).

1. #include <stdio.h>

3. void Bonjour3Fois(void);

5. int main()

6. {

7. /* Ce programme affiche Bonjour 6 fois */

8. Bonjour3Fois();

9. Bonjour3Fois();

10. return 0;

11. }

13. void Bonjour3Fois(void)

14. {

15. printf("Bonjour.\n");

16. printf("Bonjour.\n");

17. printf("Bonjour.\n");

18. }

II-C-4 - Remarques

Dans une déclaration on peut :

- Mettre le nom des arguments de la fonction (bon uniquement pour la déco et rien d'autre). Par exemple :

exemple de déclaration d'une fonction

int Surface(int Longueur, int largeur);

- Ne pas préciser les arguments de la fonction (déconseillé). Dans ce cas, il faut aller à la définition de la fonction pour connaître les arguments qu'elle requiert. Attention ! Ne pas préciser les arguments d'une fonction n'est pas la même chose que déclarer une fonction qui ne requiert aucun argument. Exemple :

exemple de déclaration incomplète d'une fonction (déconseillé)

int Surface(); /* Surface est une fonction, point. Voir sa definition pour plus de details. */

exemple de déclaration d'une fonction qui ne requiert aucun argument

int Surface(void);

De plus on peut :

- Ne pas déclarer une fonction retournant un int (déconseillé).

- La déclaration d'une fonction n'est nécessaire que lorsque son utilisation précède sa définition. En effet, une fois définie, la fonction est entièrement connue et donc n'a plus besoin d'être déclarée. Il est cependant toujours conseillé de ne définir une fonction qu'après son utilisation (ce qui requiert donc une déclaration) ne serait-ce que pour la lisibilité du programme (en effet c'est le programme qu'on veut voir à première vue, pas les petits détails).

Dans une définition:

- On peut ne pas préciser le type de retour d'une fonction (déconseillé). Dans ce cas celle-ci est supposée retourner un int.

- Si la fonction a déjà été déclarée à l'aide d'une déclaration complète (c'est-à-dire avec prototype), une paire de parenthèses vide signifie que la fonction n'accepte aucun argument. Sinon, les parenthèses vides signifient que les arguments requis par la fonction ne sont pas renseignés. Cette manière de définir une fonction est à éviter sauf pour des fonctions qui ne sont jamais appelées depuis le programme comme main() par exemple.

II-D - Les macros

II-D-1 - Le préprocesseur

Avant d'être effectivement compilés, les fichiers sources C sont traités par un préprocesseur qui résout certaines directives qui lui sont données comme l'inclusion de fichier par exemple. Le préprocesseur, quoi qu'étant un programme à priori indépendant du compilateur, est un élément indispensable du langage.

Une directive donnée au préprocesseur commence toujours par #. Nous avons déjà rencontré la directive include qui permet d'inclure un fichier. La directive define permet de définir des macros.

II-D-2 - Définition

Une macro, dans sa forme la plus simple, se définit de la manière suivante :

1. #define <macro> <le texte de remplacement>

Par exemple :

1. #define N 10

commande au préprocesseur de remplacer toutes les occurrences de le lettre N par 10. Bien sûr il n'y a pas que les lettres et les nombres qu'on peut utiliser, rien ne nous empêche par exemple d'écrire :

1. #define PLUS +

Ainsi, toutes les occurrences de PLUS seront remplacés par +.

Dans certaines circonstances, les macros peuvent également avantageusement remplacer les fonctions. Par exemple :

1. #define carre(x) x * x

Dans ce cas, une expression telle que

1. carre(3)

sera remplacé par :

1. 3 * 3

Si cela semble fonctionner à première vue, voyons un peu ce qu'on obtiendrait avec :

1. carre(1 + 1)

Et bien, on obtiendrait :

1. 1 + 1 * 1 + 1

soit 1 + 1 + 1 soit 3 car la multiplication est prioritaire par rapport à l'addition. Nous verrons plus tard l'ordre de priorité des opérateurs. C'est pourquoi on utilise généralement beaucoup de parenthèses dans le corps d'une macro. Ainsi, dans l'exemple précédent, on aurait du par exemple écrire :

1. #define carre(a) ((a) * (a))

Ce qui nous donnerait pour carre(1 + 1) par exemple :

1. ((1 + 1) * (1 + 1))

II-E - Exercices

II-E-1 - La lettre X

Ecrire un programme qui dessine la lettre dans une grille de dimensions 5 x 5 en utilisant des espaces et le caractère '*'. Cela signifie qu'on veut avoir comme sortie :

*   *
* *
*
* *
*   *

II-E-2 - Périmètre d'un rectangle

Ecrire une fonction qui permet d'avoir le périmètre d'un rectangle de longueur et largeur données en arguments et utiliser cette fonction dans un programme arbitraire pour afficher le périmètre d'un rectangle de dimensions 100 x 60. Exemple de sortie :

Le perimetre d'un rectangle de longueur 100 et de largeur 60 est 320.

III - Expressions et instructions

III-A - Introduction aux types de données du langage C

III-A-1 - Les types de base du langage C

Au niveau du processeur, toutes les données sont représentées sous leur forme binaire et la notion de type n'a pas de sens. Cette notion n'a été introduite que par les langages de haut niveau dans le but de rendre les programmes plus rationnels et structurés. Mais même parmi les langages de haut niveau, on distingue ceux qui sont dits fortement typés (qui offrent une grande variété de types), comme le Pascal par exemple, de ceux qui sont dits faiblement ou moyennement typés (et plus proches de la machine) comme le C par exemple. Le langage C ne dispose que de 4 types de base :

Type C

Type correspondant

char

caractère (entier de petite taille)

int

entier

float

nombre flottant (réel) en simple précision

double

nombre flottant (réel) en double précision

Devant char ou int, on peut mettre le modificateur signed ou unsigned selon que l'on veut avoir un entier signé (par défaut) ou non signé. Par exemple :

1. char ch;

2. unsigned char c;

3. unsigned int n; /* ou tout simplement : unsigned n */

La plus petite valeur possible que l'on puisse affecter à une variable de type entier non signé est 0 alors que les entiers signés acceptent les valeurs négatives.

Devant int, on peut mettre également short ou long auxquels cas on obtiendrait un entier court (short int ou tout simplement short) respectivement un entier long (long int ou tout simplement long). Voici des exemples de déclarations valides :

1. int n = 10, m = 5;

2. short a, b, c;

3. long x, y, z = -1;

4. unsigned long p = 2;

long peut être également mis devant double, le type résultant est alors long double (quadruple précision).

III-A-2 - Règle d'écriture des constantes littérales

Nombres entiers

Toute constante littérale « pure » de type entier (ex : 1, -3, 60, 40, -20, ...) est considérée par le langage comme étant de type int.

Pour expliciter qu'une constante littérale de type entier est de type unsigned, il suffit d'ajouter à la constante le suffixe u ou U. Par exemple : 2u, 30u, 40U, 50U, ...

De même, il suffit d'ajouter le suffixe l ou L pour expliciter qu'une constante littérale de type entier est de type long (on pourra utiliser le suffixe UL par exemple pour unsigned long).

Une constante littérale de type entier peut également s'écrire en octal (base 8) ou en hexadécimal (base 16). L'écriture en hexa est évidement beaucoup plus utilisée.

Une constante littérale écrite en octal doit être précédée de 0 (zéro). Par exemple : 012, 020, 030UL, etc.

Une constante littérale écrite en hexadécimal doit commencer par 0x (zéro x). Par exemple 0x30, 0x41, 0x61, 0xFFL, etc.

Nombres flottants

Toute constante littérale « pure » de type flottant (ex : 0.5, -1.2, ...) est considérée comme étant de type double.

Le suffixe f ou F permet d'expliciter un float. Attention, 1f n'est pas valide car 1 est une constante entière. Par contre 1.0f est tout à fait correcte. Le suffixe l ou L permet d'expliciter un long double.

Une constante littérale de type flottant est constituée, dans cet ordre :

- d'un signe (+ ou -)

- d'une suite de chiffres décimaux : la partie entière

- d'un point : le séparateur décimal

- d'une suite de chiffres décimaux : la partie décimale

- d'une des deux lettres e ou E : symbole de la puissance de 10 (notation scientifique)

- d'un signe (+ ou -)

- d'une suite de chiffres décimaux : la puissance de 10

Par exemple, les constantes littérales suivantes représentent bien des nombres flottants : 1.0, -1.1f, 1.6E-19, 6.02e23L, 0.5 3e8

Caractères et chaînes de caractères

Les caractères sont placés entre simple quottes (ex : 'A', 'b', 'c', ...) et les chaînes de caractères entre double quottes (ex : "Bonjour", "Au revoir", ...). Certains caractères sont spéciaux et il faut utiliser la technique dite d'échappement pour les utiliser. En langage C, le caractère d'échappement est le caractère \. Exemples :

Caractère

Valeur

'\t'

Le caractère 'tabulation'

'\r'

Le caractère 'retour chariot'

'\n'

Le caractère 'fin de ligne'

'\\'

Le caractère 'antislash'

III-A-3 - Spécification de format dans printf

III-A-3-a - Généralités

Voici la liste des codes format que nous utiliserons les plus souvent par la suite :

Code format

Utilisation

c

Afficher un caractère

d

Afficher un int

u

Afficher un unsigned int

x, X

Afficher un entier dans le format hexadécimal

f

Afficher un float ou un double en notation décimale

e

Afficher un float ou un double en notation scientifique avec un petit e

E

Afficher un float ou un double en notation scientifique avec un grand E

g, G

Afficher un float ou un double (utilise le format le plus adapté)

%

Afficher le caractère '%'

Lorsqu'on affiche une donnée de type flottant dans le format "scientifique", il n'y a qu'un chiffre, ni moins ni plus, devant le point décimal (et ce chiffre est 0 si et seulement si la donnée vaut 0), le nombre de chiffres après ce point est variable et le nombre de chiffres utilisés pour représenter l'exposant dépend de l'implémentation mais il y en a toujours au moins deux.

D'autre part :

- h devant d ou u indique que l'argument est un short

- l devant d, u, x ou X indique que l'argument est de type long

- L devant f, e, E ou g indique que l'argument est de type long double

La spécification de format dans printf va cependant beaucoup plus loin qu'une simple spécification de type. En règle générale, une spécification de format a la structure suivante : %[options]specificateur_de_type. Les "options" ont été mis entre crochets car elles sont toujours facultatives. Les options disponibles dépendent du code de conversion (le "spécificateur de type") utilisé. Nous n'allons pas toutes les présenter (la documentation de référence est là pour ça) mais allons quand même voir quelques options que nous aurons assez souvent l'occasion d'utiliser.

Enfin, pour afficher un caractère, il vaut mieux utiliser putchar (ex : putchar('*')) qui est plus simple plutôt que printf.

III-A-3-b - Les "options"

Une spécification de format dans printf a le plus souvent (cas pratique) la structure suivante :

%[[+][-][0]][gabarit][.precision][modificateur_de_type]specificateur_de_type

Les drapeaux + et 0 n'ont de sens que pour les données à imprimer sous forme numérique.

Découvrons le rôle des paramètres les plus utiles pour les types les plus utilisés.

III-A-3-c - Le type entier

Pour le type entier (code de conversion d, u, x ou X) :

- h devant le spécificateur de type indique que l'argument attendu est de type short et l indique que l'argument est de type long.

- La précision indique le nombre maximum de caractères à utiliser pour imprimer la donnée.

- Le gabarit indique le nombre minimum de caractères à utiliser.

- Le drapeau 0 indique qu'il faut ajouter autant de 0 qu'il en faut avant le premier chiffre significatif de sorte que tout l'espace reservé au texte (défini par le gabarit) soit occupé.

- Le drapeau - indique que le texte doit être aligné à gauche. L'alignement par défaut est donc bien évidemment à droite.

- Le drapeau + indique qu'on veut que le signe du nombre soit toujours affiché, même si le nombre est positif ou nul.

Voici un programme proposant quelques applications :

1. #include <stdio.h>

3. int main()

4. {

5. int n = 1234;

7. printf("+------------+\n");

8. printf("|%12d|\n", n); /* 12 caracteres minimum, alignement a droite */

9. printf("+------------+\n");

10. printf("|%-12d|\n", n); /* 12 caracteres minimum, alignement a gauche */

11. printf("+------------+\n");

12. printf("|%012d|\n", n); /* 12 caracteres minimum, caractere de remplissage : '0' */

13. printf("+------------+\n");

15. /* Affichage obligatoire du signe */

17. printf("|%+12d|\n", n);

18. printf("+------------+\n");

19. printf("|%+-12d|\n", n);

20. printf("+------------+\n");

21. printf("|%+012d|\n", n);

22. printf("+------------+\n");

24. return 0;

25. }

Voici la sortie de ce programme :

+------------+
|        1234|
+------------+
|1234        |
+------------+
|000000001234|
+------------+
|       +1234|
+------------+
|+1234       |
+------------+
|+00000001234|
+------------+

III-A-3-d - Le type flottant

Pour le type flottant (code de conversion f, e, E, g ou G) :

- L devant le spécificateur de type indique que l'argument attendu est de type long double.

- La précision indique le nombre de caractères à afficher après le point décimal pour les codes f, e et E (la valeur par défaut est 6) et le nombre maximum de chiffres significatifs pour les codes g et G.

- Le gabarit indique le nombre minimum de caractères à utiliser.

- Le drapeau 0 indique qu'il faut ajouter autant de 0 qu'il en faut avant le premier chiffre significatif de sorte que tout l'espace reservé au texte (défini par le gabarit) soit occupé.

- Le drapeau - indique que le texte doit être aligné à gauche. L'alignement par défaut est donc bien évidemment à droite.

- Le drapeau + indique qu'on veut que le signe du nombre soit toujours affiché, même si le nombre est positif ou nul.

Voici un programme proposant quelques applications :

1. #include <stdio.h>

3. int main()

4. {

5. double x = 12.34;

7. /* Affichage de flottant avec 4 chiffres apres le point decimal. */

9. printf("%.4f\n", x);

10. printf("%.4e\n", x);

12. /* Affichage dans le format le plus approprie. */

14. printf("%g\n", x);

16. return 0;

17. }

Voici la sortie de ce programme (en supposant que l'implémentation utilise 3 chiffres pour imprimer l'exposant) :

12.3400
1.2340e+001
12.34

III-A-4 - Les variables et les constantes

Si vous avez déjà fait un peu de math dans votre vie, vous devriez connaître ce que c'est qu'une variable. La notion est pratiquement la même en programmation. La déclaration d'une variable en langage C, dans sa forme la plus simple, se fait de la manière suivante :

1. <type> variable;

Par exemple :

1. int n;

Voici d'autres exemples beaucoup plus généralistes :

1. int a, b, c;

2. int i = 0, j, n = 10;

3. double x, y, z = 0.5;

Une déclaration de variable de la forme :

1. int n = 0;

est appelée une déclaration avec initialisation. Ce n'est pas la même chose que :

1. int n;

2. n = 0;

qui est une déclaration suivie d'une instruction d'affectation. Pour vous en convaincre rapidement, une variable constante (ou varaible en lecture seule) est une variable qui ne peut être qu'initialisée puis lue (la valeur d'une constante ne peut pas être modifiée). On obtient une constante en ajoutant le mot-clé const devant une déclaration. Par exemple :

1. const int n = 10;

On ne pourra jamais écrire :

1. n = <quoi que ce soit>;

Le mot-clé const être placé avant ou après le type ou encore avant le nom de la variable. Voici deux exemples :

1. const int n = 10;

2. int const m = 20;

III-A-5 - Définition de nouveaux types

Le C dispose d'un mécanisme très puissant permettant au programmeur de créer de nouveaux types de données en utilisant le mot clé typedef. Par exemple :

1. typedef int ENTIER;

Définit le type ENTIER comme n'étant autre que le type int. Rien ne nous empêche donc désormais d'écrire par exemple :

1. ENTIER a, b;

Bien que dans ce cas, un simple #define aurait pu suffire, il est toujours recommandé d'utiliser typedef qui est beaucoup plus sûr.

III-B - Les pointeurs

III-B-1 - Définition

Comme nous le savons très bien, l'endroit où s'exécute un programme est la mémoire donc toutes les données du programme (les variables, les fonctions, ...) se trouvent en mémoire. Le langage C dispose d'un opérateur - & - permettant de récupérer l'adresse en mémoire d'une variable ou d'une fonction quelconque. Par exemple, si n est une variable, &n désigne l'adresse de n.

Le C dispose également d'un opérateur - * - permettant d'accéder au contenu de la mémoire dont l'adresse est donnée. Par exemple, supposons qu'on ait :

1. int n;

Alors les instructions suivantes sont strictement identiques.

1. n = 10;

1. *( &n ) = 10;

Un pointeur (ou une variable de type pointeur) est une variable destinée à recevoir une adresse. On dit alors qu'elle pointe sur un emplacement mémoire. L'accès au contenu de la mémoire se fait par l'intermédiaire de l'opérateur *.

Les pointeurs en langage C sont typés et obéissent à l'arithmétique des pointeurs que nous verrons un peu plus loin. Supposons que l'on veuille créer une variable - p - destinée à recevoir l'adresse d'une variable de type int. p s'utilisera alors de la façon suivante :

1. p = &n;

2. ...

3. *p = 5;

4. ...

*p est un int. Vous suivez ? Alors comment devrait-on déclarer le pointeur ? Si vous avez bien suivi, vous auriez répondu :

1. int *p;

Et de plus, si vous avez très bien suivi, vous auriez certainement ajouté : et cela est strictement équivalent à :

1. int * p;

Car en C, les espaces sont totalement gratuites ! Donc voilà, le type d'un pointeur sur int est int *. Mais cela peut être moins évident dans certaines déclarations. Par exemple, dans :

1. /* cas 1 */

2. int * p1, p2, p3;

seul p1 est de type int * ! Le reste, p2 et p3, sont de type int. Si vous voulez obtenir 3 pointeurs, vous devrez utilisez la syntaxe suivante :

1. /* cas 2 */

2. int *p1, *p2, *p3;

Ou avec un typedef :

1. typedef int * PINT;

2. PINT p1, p2, p3;

Par contre cela n'aurait pas marché si on avait défini PINT à l'aide d'un #define car cela nous amènerait au cas 1.

III-B-2 - Saisir des données tapées au clavier avec la fonction scanf

On va encore faire un petit exercice de logique, plutôt de bon sens.

Si nous voulons afficher un entier par exemple, que doit-on fournir à la fonction d'affichage (printf par exemple) : la valeur de l'entier que nous voulons afficher ou l'adresse de la variable contenant le nombre que nous voulons afficher ?

La bonne réponse est : l'entier que nous voulons afficher bien sûr, autrement dit sa valeur. Personne n'a demandé son adresse !

Maintenant, si nous voulons demander à l'utilisateur (celui qui utilise notre programme) de taper un nombre puis ranger le nombre ainsi tapé dans une variable, que devons-nous fournir à la fonction permettant la saisie : l'adresse de la variable dans laquelle nous souhaitons ranger le nombre que l'utilisateur a entré, ou la valeur que contient actuellement cette variable ?

La bonne réponse est bien sûr : l'adresse de la variable dans laquelle nous souhaitons ranger le nombre entré

La fonction scanf, déclarée dans le fichier stdio.h, permet de lire des données formatées sur l'entrée standard, par défaut le clavier.

Voici un programme qui demande à l'utilisateur d'entrer 2 nombres puis affiche leur somme :

1. #include <stdio.h>

3. int main()

4. {

5. int a, b, c;

7. printf("Ce programme calcule la somme de 2 nombres.\n");

9. printf("Entrez la valeur de a : ");

10. scanf("%d", &a);

12. printf("Entrez la valeur de b : ");

13. scanf("%d", &b);

15. c = a + b;

16. printf("%d + %d = %d\n", a, b, c);

18. return 0;

19. }

Mais faites gaffe avec la fonction scanf ! "%d" par exemple n'a rien à voir avec " %d ". En effet dans ce dernier cas, le format attendu par la fonction est :

1. <un espace> <un entier> <puis un autre espace>

Où espace désigne en fait n'importe quel caractère ou série de caractères blancs (espace, tabulation, etc.). Voici un exemple beaucoup plus convaincant :

1. #include <stdio.h>

3. int main()

4. {

5. int mon_age = 23, votre_age;

7. printf("Bonjour, j'ai %d ans. Et vous? ", mon_age);

8. scanf("%d ans", &votre_age);

9. printf("Ah! vous avez %d ans?\n", votre_age);

11. return 0;

12. }

L'utilisateur devra alors taper par exemple :

1. 22 ans

Peu importe le nombre de caractères blancs entre 22 et ans.

C'est ce qu'on appelle une saisie formatée. scanf retourne le nombre de conversions de format réussies. La valeur de retour de scanf permet donc déjà de voir si l'utilisateur a bien respecté le format attendu ou non. Dans notre exemple, il n'y a qu'une seule conversion de format à faire (%d). Si l'utilisateur respecte bien le format, scanf retournera 1.

Les fonctions telles que scanf sont plutôt destinées à être utilisées pour lire des données provenant d'un programme sûr (par l'intermédiaire d'un fichier par exemple), pas celles provenant d'un humain, qui sont sujettes à l'erreur. Les codes format utilisés dans scanf sont à peu près les mêmes que dans printf, sauf pour les flottants notamment.

Code format

Utilisation

f, e, g

float

lf, le, lg

double

Lf, Le, Lg

long double

Voici un programme qui permet de calculer le volume d'un cône droit à base circulaire selon la formule : V = 1/3 * (B * h) où B est la surface de base soit pour une base circulaire : B = PI*R^2, où R est le rayon de la base.

1. #include <stdio.h>

3. double Volume(double r_base, double hauteur);

5. int main()

6. {

7. double R, h, V;

9. printf("Ce programme calcule le volume d'un cone.\n");

11. printf("Entrez le rayon de la base : ");

12. scanf("%lf", &R);

14. printf("Entrez la hauteur du cone : ");

15. scanf("%lf", &h);

17. V = Volume(R, h);

18. printf("Le volume du cone est : %f", V);

20. return 0;

21. }

23. double Volume(double r_base, double hauteur)

24. {

25. return (3.14 * r_base * r_base * hauteur) / 3;

26. }

III-B-3 - Exemple de permutation des contenus de deux variables

Voici un programme simple qui permute le contenu de deux variables.

1. include <stdio.h>

3. int main()

4. {

5. int a = 10, b = 20, c;

7. /* on sauvegarde quelque part le contenu de a */

8. c = a;

10. /* on met le contenu de b dans a */

11. a = b;

13. /* puis le contenu de a que nous avons sauvegardé dans c dans b */

14. b = c;

16. /* affichons maintenant a et b */

17. printf("a = %d\nb = %d\n", a, b);

19. return 0;

20. }

Maintenant, réécrivons le même programme en utilisant une fonction. Cette fonction doit donc pouvoir localiser les variables en mémoire autrement dit nous devons passer à cette fonction les adresses des variables dont on veut permuter le contenu.

1. #include <stdio.h>

3. void permuter(int * addr_a, int * addr_b);

5. int main()

6. {

7. int a = 10, b = 20;

9. permuter(&a, &b);

10. printf("a = %d\nb = %d\n", a, b);

12. return 0;

13. }

15. void permuter(int * addr_a , int * addr_b)

16. /***************\

17. * addr_a <-- &a *

18. * addr_b <-- &b *

19. \***************/

20. {

21. int c;

23. c = *addr_a;

24. *addr_a = *addr_b;

25. *addr_b = c;

26. }

III-C - Les expressions

III-C-1 - Introduction

En langage C, la notion d'expression est beaucoup plus riche que dans n'importe quel autre langage. De ce fait en donner une définition est longue et nous allons donc plutôt l'introduire (comme nous l'avons d'ailleurs toujours fait) de manière intuitive. Pour cela, analysons l'instruction suivante :

1. c = a + b;

Dans cette instruction (où nous supposons que les 3 variables sont tous de type int), il y a tout d'abord évaluation de a + b, puis affection du résultat dans c. On dit que :

1. a + b

est une expression, plus précisément une expression de type int (car a et b sont de type int) et dont la valeur est la somme de a et b (a + b), et que :

1. c = a + b;

est une instruction (remarquez bien le point-virgule).

L'expression a + b est composée de deux expressions à savoir a et b. On dit alors que c'est une expression complexe. En langage C, les opérateurs permettent de construire des expressions complexes. Dans notre exemple, l'opérateur d'addition à savoir + nous a permis de construire une expression complexe, a + b, à partir des expressions simples a et b.

Or en langage C, = est également un opérateur, l'opérateur d'affectation, et permet donc de construire des expressions complexes. Ainsi, dans notre exemple :

1. c = a + b

est une expression (remarquez bien l'absence de point-virgule, sinon ce serait une instruction) et dont la valeur et le type sont celle et celui de l'expression situé à gauche à savoir c.

III-C-2 - lvalue et rvalue

Une lvalue (de left value) est tout ce qui peut figurer à gauche de l'opérateur d'affectation. Une variable, par exemple, est une lvalue. Si p est un pointeur (disons vers int), p est une lvalue (en effet un pointeur est une variable comme toutes les autres) et *p est également une lvalue.

Une rvalue (de right value) est tout ce qui peut figurer à droite de l'opérateur d'affectation. Une variable, une constante littérale, l'adresse d'une variable en sont des exemples. Si a est une variable, disons de type int, alors a est une lvalue tandis que a, -a ou a + 1 par exemple sont des rvalues. Cet exemple nous montre bien qu'une variable est à la fois une lvalue et une rvalue.

Une rvalue possède une valeur mais pas une adresse. Ainsi par exemple : a étant une lvalue, &a a bien un sens (désigne l'adresse mémoire de la variable a). Par contre, une expression insensée telle que &(-a) ou &(a + 1) sera rejetée (avec plaisir) par le compilateur.

III-C-3 - Opérations usuelles

III-C-3-a - Les opérateurs arithmétiques courants

Les opérateurs arithmétiques courants +, -, * et / existent en langage C. Toutefois, la division entière est un tout petit peu délicate. En effet, si a et b sont des entiers, a / b vaut le quotient de a et b c'est-à-dire par exemple, 29 / 5 vaut 5. Le reste d'une division entière s'obtient avec l'opérateur modulo %, c'est-à-dire, en reprenant l'exemple précédent, 29 % 5 vaut 4.

III-C-3-b - Les opérateurs de comparaison

Les opérateurs de comparaison sont

Opérateur

Rôle

<

Inférieur à

>

Supérieur à

==

Egal à

<=

Inférieur ou égal à

>=

Supérieur ou égal à

!=

Différent de

Par exemple, l'expression :

1. 1 < 1000

est vraie. Vraie ? Et que vaut « vrai » au juste ? Quel est le type d'une expression « vraie » ? En langage C, la valeur d'une expression « vraie » est non nulle et celle d'une expression « fausse » zéro. Et réciproquement, toute valeur non nulle (24, -107, 2.9, ...) peut être interprétée comme vrai et zéro comme faux. Par exemple :

1. int prop;

2. prop = (1 < 1000); /* alors prop = VRAI */

III-C-3-c - Les opérateurs logiques

On peut construire des expressions logiques complexes à l'aide des opérateurs

Opérateur

Rôle

&&

ET

||

OU

!

NON

Par exemple :

1. int prop1, prop2, prop_ou, prop_et, prop_vrai;

2. prop1 = (1 < 1000);

3. prop2 = (2 == -6);

4. prop_ou = prop1 || prop2; /* VRAI, car prop1 est VRAI */

5. prop_et = prop1 && prop2; /* FAUX, car prop2 est FAUX */

6. prop_vrai = prop1 && !prop_2 /* VRAI car prop1 et !prop2 sont VRAI */

Les opérateurs logiques jouissent des propriétés suivantes :

- Dans une opération ET, l'évaluation se fait de gauche à droite. Si l'expression à gauche de l'opérateur est fausse, l'expression à droite ne sera plus évaluée car on sait déjà que le résultat de l'opération sera toujours FAUX.

- Dans une opération OU, l'évaluation se fait de gauche à droite. Si l'expression à gauche de l'opérateur est vrai, l'expression à droite ne sera plus évaluée car on sait déjà que le résultat de l'opération sera toujours VRAI.

III-C-4 - L'opérateur virgule

On peut séparer plusieurs expressions à l'aide de l'opérateur virgule. Le résultat est une expression dont la valeur est celle de l'expression la plus à droite. L'expression est évaluée de gauche à droite. Par exemple, l'expression :

1. (a = -5, b = 12, c = a + b) * 2

vaut 14.

III-C-5 - Taille des données. L'opérateur sizeof

La taille d'une donnée désigne la taille, en octets, que celle-ci occupe en mémoire. Par extension de cette définition, la taille d'un type de données désigne la taille d'une donnée de ce type. Attention ! octet désigne ici, par abus de langage, la taille d'un élément de mémoire sur la machine cible (la machine abstraite), c'est-à-dire la taille d'une case mémoire (qui vaut 8 bits dans la plupart des architectures actuelles), et non un groupe de 8 bits. En langage C, un octet (une case mémoire) est représenté par un char. La taille d'un char n'est donc pas forcément 8 bits, même si c'est le cas dans de nombreuses architectures, mais dépendante de la machine. La norme requiert toutefois qu'un char doit faire au moins 8 bits et que la macro CHAR_BIT, déclarée dans limits.h, indique la taille exacte d'un char sur la machine cible.

Le C dispose d'un opérateur, sizeof, permettant de connaître la taille, en octets, d'une donnée ou d'un type de données. La taille d'un char vaut donc évidemment 1 puisqu'un char représente un octet. Par ailleurs, il ne peut y avoir de type dont la taille n'est pas multiple de celle d'un char. Le type de la valeur retournée par l'opérateur sizeof est size_t, déclaré dans stddef.h, qui est inclus par de nombreux fichiers d'en-tête dont stdio.h.

Comme nous l'avons déjà dit plus haut, la taille des données est dépendante de la machine cible. En langage C, la taille des données n'est donc pas fixée. Néanmoins la norme stipule qu'on doit avoir :

1. sizeof (char) <= sizeof (short) <= sizeof (int) <= sizeofd(long)

Sur un processeur Intel (x86) 32 bits par exemple, un char fait 8 bits, un short 16 bits, et les int et les long 32 bits.

III-C-6 - Les opérateurs d'incrémentation et de décrémentation

Il s'agit des opérateurs ++ (opérateur d'incrémentation) et -- (opérateur de décrémentation). Ils existent sous forme préfixée ou postfixée. L'opérande doit être une lvalue de type numérique ou pointeur. Soient i et j deux variables de type int :

1. j = i++;

est équivalent à :

1. j = i;

2. i = i + 1;

On voit donc bien que la valeur de i++ est celle de i avant l'évaluation effective de l'expression (post incrémentation).

Inversement :

1. j = ++i;

est équivalent à :

1. i = i + 1;

2. j = i;

Où l'on voit bien que la valeur de ++i est celle de i après l'évaluation effective de l'expression (pré incrémentation).

L'opérateur -- s'utilise de la même manière que ++ mais l'opérande est cette fois-ci décrémentée.

III-C-7 - Expressions conditionnelles

Une expression conditionnelle est une expression dont la valeur dépend d'une condition. L'expression :

1. p ? a : b

vaut a si p est vrai et b si p est faux.

III-C-8 - Autres opérateurs d'affectation

Ce sont les opérateurs : +=, -=, *=, /=, ...

1. x += a;

par exemple est équivalent à :

1. x = x + a;

III-C-9 - Ordre de priorité des opérateurs

Les opérateurs sont classés par ordre de priorité. Voici les opérateurs que nous avons étudiés jusqu'ici classés dans cet ordre. Opérateur Associativité

Opérateur

Associativité

Parenthèses

de gauche à droite

! ++ -- - (signe) sizeof

de gauche à droite

* / %

de gauche à droite

+ -

de gauche à droite

< <= > >=

de gauche à droite

== !=

de gauche à droite

& (adresse de)

de gauche à droite

&&

de gauche à droite

||

de gauche à droite

Opérateurs d'affectation (= += ...)

de droite à gauche

,

de gauche à droite

Ce n'est pas parce que cet ordre existe qu'il faut le retenir par coeur. Pour du code lisible, il est même conseillé de ne pas trop en tenir compte et d'utiliser des parenthèses dans les situations ambiguës.

III-D - Considérations liées à la représentation binaire

III-D-1 - Généralités

L'ordinateur traite les données uniquement sous forme numérique. A l'intérieur de l'ordinateur, ces nombres sont représentés en binaire. La manière de représenter une donnée dans la mémoire de l'ordinateur est appelée codage. Pour bien comprendre le C, vous devez connaître comment les données sont représentées en mémoire, c'est-à-dire sous leur forme binaire. Voir à ce sujet votre cours de numérique.

III-D-2 - Les caractères

La représentation numérique des caractères définit ce qu'on appelle un jeu de caractères. Par exemple, dans le jeu de caractères ASCII (American Standard Code for Information Interchange) qui est un jeu de caractères qui n'utilise que 7 bits et qui est à la base de nombreux codes populaires de nos jours, le caractère 'A' est représenté par le code 65, le caractère 'a' par 97 et '0' par 48. Hélas, même ASCII n'est pas adopté par le langage C. En effet si le C dépendait d'un jeu de caractères particulier, il ne serait alors pas totalement portable. Le standard définit néanmoins un certain nombre de caractères que tout environnement compatible avec le C doit posséder parmi lesquels les 26 lettres de l'alphabet latin (donc en fait 52 puisqu'on différencie les majuscules et les minuscules), les 10 chiffres décimaux, les caractères # < > ( ) etc. Le programmeur (mais pas le compilateur) n'a pas besoin de connaître comment ces caractères sont représentés dans le jeu de caractères de l'environnement. Le standard ne définit donc pas un jeu de caractères mais seulement un ensemble de caractères que chaque environnement compatible est libre d'implémenter à sa façon (plus éventuellement les caractères spécifiques à cet environnement). La seule contrainte imposée est que leur valeur doit pouvoir tenir dans un char.

Concernant la technique d'échappement, sachez également qu'on peut insérer du code octal (commençant par 0) ou hexadécimal (commençant par x) après le caractère d'échappement \ pour obtenir un caractère dont le code dans le jeu de caractères est donné. L'hexadécimal est de loin le plus utilisé. Par exemple : '\x30', '\x41', '\x61', ... Et enfin pour les caractères de code 0, 1, ... jusqu'à 7, on peut utiliser les raccourcis '\0', '\1', ... '\7'.

III-D-3 - Dépassement de capacité

Le dépassement de capacité a lieu lorsqu'on tente d'affecter à une lvalue une valeur plus grande que ce qu'elle peut contenir. Par exemple, en affectant une valeur sur 32 bits à une variable ne pouvant contenir que 16 bits.

III-E - La conversion de type

III-E-1 - Conversion implicite

En langage C, des règles de conversion dite implicite s'appliquent aux données qui composent une expression complexe lorsqu'ils ne sont pas de même type (entier avec un flottant, entier court avec entier long, entier signé avec un entier non signé, etc.). Par exemple, dans l'expression :

1. 'A' + 2

'A' est de type char et 2 de type int. Dans ce cas, 'A' est tout d'abord converti en int avant que l'expression ne soit évaluée. Le résultat de l'opération est de type int (car un int + un int donne un int). Ici, il vaut 67 (65 + 2). En fait, les char et les short sont toujours systématiquement convertis en int c'est-à-dire que dans l'addition de deux char par exemple, tous deux sont tout d'abord convertis en int avant d'être additionnés, et le résultat est un int (pas un char). Un unsigned char sera converti en unsigned int, et ainsi de suite.

En règle générale : le type le plus « faible » est convertit dans le type le plus « fort ». Par exemple, les entiers sont plus faibles que les flottants donc 1 mélangé à un flottant par exemple sera tout d'abord converti en 1.0 avant que l'opération n'ait effectivement lieu.

III-E-2 - Conversion explicite (cast)

Il suffit de préciser le type de destination entre parenthèses devant l'expression à convertir. Par exemple :

1. float f;

2. f = (float)3.1416;

Dans cet exemple, on a converti explicitement 3.1416, qui est de type double, en float. Lorsqu'on affecte un flottant à un entier, seule la partie entière, si elle peut être représentée, est retenue.

III-F - Les instructions

III-F-1 - Introduction

L'instruction est la plus petite unité qui compose une fonction. Le seul type d'instruction que nous avons vu jusqu'ici est l'expression-instruction qui n'est autre qu'une expression suivie d'un point-virgule. Comprendre les notions d'expression et d'instruction est indispensable pour bien comprendre et maîtriser la syntaxe du langage C. Nous allons maintenant généraliser la notion d'instruction en langage C.

III-F-2 - Bloc d'instructions

Un bloc, abusivement appelé bloc d'instructions, ou plus précisément une instruction-bloc consiste en un groupe de déclarations et d'instructions (qui peuvent être également des blocs). Un bloc est délimité par des accolades. Une fonction est toujours constituée d'une instruction-bloc.

Une variable déclarée à l'intérieur d'un bloc est locale à (c'est-à-dire n'est connue qu'à l'intérieur de) ce bloc et masque toute variable de même nom déclarée à l'extérieur de celui-ci (une variable globale est une variable déclarée à l'extérieur de toute fonction et est connue partout dans le fichier source). D'autre part, une déclaration doit toujours se faire en début de bloc. Les arguments effectifs d'une fonction sont également des variables locales à cette fonction.

Voici un exemple de programme (présentant peu d'intérêt je le conçois) comportant plusieurs blocs :

1. #include <stdio.h>

3. int main()

4. {

5. printf("Les instructions en langage C\n");

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

8. {

9. /* Un bloc d'instructions */

10. printf("**********\n");

11. printf("* Bloc 1 *\n");

12. printf("**********\n");

13. }

15. putchar('\n');

17. {

18. /* Un autre bloc */

19. printf("**********\n");

20. printf("* Bloc 2 *\n");

21. printf("**********\n");

23. {

24. /* Encore un autre bloc */

25. printf("**********\n");

26. printf("* Bloc 3 *\n");

27. printf("**********\n");

28. }

29. }

31. return 0;

32. }

Retenez donc bien ceci : un bloc constitue bien une et une seule instruction. Bien que cette instruction soit en fait un bloc d'instructions (et de déclarations), il est conseillé de la voir comme une et une seule instruction plutôt que comme un bloc d'instructions. Une instruction-bloc ne se termine pas par un point-virgule. Conclusion : Il est faux de dire qu'une instruction doit toujours se terminer par un point-virgule. Le point-virgule marque tout simplement la fin de certaines instructions, dont les expressions-instructions.

III-F-3 - L'instruction if

Permet d'effectuer des choix conditionnels. La syntaxe de l'instruction est la suivante :

1. if ( <expression> )

2. <une et une seule instruction>

3. else

4. <une et une seule instruction>

Une instruction if peut ne pas comporter de else. Lorsqu'on a plusieurs instructions if imbriquées, un else se rapporte toujours au dernier if suivi d'une et une seule instruction. Par exemple : écrivons un programme qui compare deux nombres.

1. #include <stdio.h>

3. int main()

4. {

5. int a, b;

7. printf("Ce programme compare deux nombres.\n");

9. printf("Entrez la valeur de a : ");

10. scanf("%d", &a);

12. printf("Entrez la valeur de b : ");

13. scanf("%d", &b);

15. if (a < b)

16. printf("a est plus petit que b.\n");

17. else

18. if (a > b)

19. printf("a est plus grand que b.\n");

20. else

21. printf("a est egal a b.\n");

23. return 0;

24. }

III-F-4 - L'instruction do

Permet d'effectuer une boucle. La syntaxe de l'instruction est la suivante :

1. do

2. <une et une seule instruction>

3. while ( <expression> );

L'instruction do permet d'exécuter une instruction tant que <expression> est vraie. Le test est fait après chaque exécution de l'instruction. Voici un programme qui affiche 10 fois Bonjour.

1. #include <stdio.h>

3. int main()

4. {

5. int nb_lignes_affichees = 0;

7. do

8. {

9. printf("Bonjour.\n");

10. nb_lignes_affichees++;

11. }

12. while (nb_lignes_affichees < 10);

14. return 0;

15. }

III-F-5 - L'instruction while

Permet d'effectuer une boucle. La syntaxe de l'instruction est la suivante :

1. while ( <expression> )

2. <une et une seule instruction>

L'instruction while permet d'exécuter une instruction tant que <expression> est vraie. Le test est fait avant chaque exécution de l'instruction. Donc si la condition (<expression>) est fausse dès le départ, la boucle ne sera pas exécutée.

III-F-6 - L'instruction for

Permet d'effectuer une boucle. La syntaxe de l'instruction est la suivante :

1. for ( <init> ; <condition> ; <step>)

2. <instruction>

Elle est pratiquement identique à :

1. <init>;

2. while ( <condition> )

3. {

4. <instruction>

5. <step>

6. }

Par exemple, écrivons un programme qui affiche la table de multiplication par 5.

1. #include <stdio.h>

3. int main()

4. {

5. int n;

7. for(n = 0; n <= 10; n++)

8. printf("5 x - -\n", n, 5 * n);

10. return 0;

11. }

III-F-7 - Les instructions switch et case

Ces instructions permettent d'éviter des instructions if trop imbriquées comme illustré par l'exemple suivant :

1. #include <stdio.h>

3. int main()

4. {

5. int n;

7. printf("Entrez un nombre entier : ");

8. scanf("%d", &n);

10. switch(n)

11. {

12. case 0:

13. printf("Cas de 0.\n");

14. break;

16. case 1:

17. printf("Cas de 1.\n");

18. break;

20. case 2: case 3:

21. printf("Cas de 2 ou 3.\n");

22. break;

24. case 4:

25. printf("Cas de 4.\n");

26. break;

28. default:

29. printf("Cas inconnu.\n");

30. }

32. return 0;

33. }

III-F-8 - L'instruction break

Permet de sortir immédiatement d'une boucle ou d'un switch. La syntaxe de cette instruction est :

1. break;

III-F-9 - L'instruction continue

Dans une boucle, permet de passer immédiatement à l'itération suivante. Par exemple, modifions le programme table de multiplication de telle sorte qu'on affiche rien pour n = 4 ou n = 6.

1. #include <stdio.h>

3. int main()

4. {

5. int n;

7. for(n = 0; n <= 10; n++)

8. {

9. if ((n == 4) || (n == 6))

10. continue;

12. printf("5 x %2d %2d\n", n, 5 * n);

13. }

15. return 0;

16. }

III-F-10 - L'instruction return

Permet de terminer une fonction. La syntaxe de cette instruction est la suivante :

1. return <expression>; /* termine la fonction et retourne <expression> */

ou :

1. return; /* termine la fonction sans spécifier de valeur de retour */

III-F-11 - L'instruction vide

Une instruction peut être vide. Par exemple :

1. /* Voici une instruction vide * / ;

2. /* Voici une autre instruction vide */ ;

3. { /* Encore une instruction vide */ }

III-G - Exercices

III-G-1 - Valeur absolue

Ecrire un programme qui demande à l'utilisateur d'entrer un nombre et qui affiche ensuite la "valeur absolue" de ce nombre. Il y a plusieurs façons de définir la valeur absolue d'un nombre x mais nous utiliserons la suivante : c'est x si x est supérieur ou égal à 0 et -x si x est inférieur à 0. La valeur absolue d'un nombre est donc un nombre toujours positif ou nul. Voici un exemple d'exécution :

Ce programme permet de determiner la valeur absolue d'un nombre.
Entrez un nombre : -6
La valeur absolue de -6 est 6.
Merci d'avoir utilise ce programme. A bientot !

III-G-2 - Moyenne

Ecrire un programme qui demande à l'utilisateur d'entrer une note. La note doit être comprise entre 0 et 20 sinon le programme affichera "Cette note n'est pas valide" puis se terminera. Après que l'utilisateur ait entré une note valide, si la note est supérieure ou égale à 10, le programme affichera "Vous avez eu la moyenne." sinon, il affichera "Vous n'avez pas eu la moyenne.". Voici quelques exemples d'exécution :

Ce programme permet de determiner si vous avez eu la moyenne ou non.
Entrez votre note (0 a 20) : 12
Vous avez eu la moyenne.
Merci d'avoir utilise ce programme. A bientot !

Ce programme permet de determiner si vous avez eu la moyenne ou non.
Entrez votre note (0 a 20) : 22
Cette note n'est pas valide.
Merci d'avoir utilise ce programme. A bientot !

III-G-3 - L'heure dans une minute

Ecrire un programme qui demande à l'utilisateur d'entrer l'heure et la minute actuelles et qui affiche ensuite l'heure et la minute qu'il fera une minute après. Voici quelques exemples d'exécution :

Ce programme permet de determiner l'heure qu'il sera une minute plus tard.
Entrez l'heure actuelle : 14
Entrez la minute actuelle : 38
Dans une minute il sera : 14:39.
Merci d'avoir utilise ce programme. A bientot !

Ce programme permet de determiner l'heure qu'il sera une minute plus tard.
Entrez l'heure actuelle : 23
Entrez la minute actuelle : 59
Dans une minute il sera : 00:00.
Merci d'avoir utilise ce programme. A bientot !

Pour simplifier l'exercice, on supposera que les valeurs entrées par l'utilisateur seront toujours correctes.

III-G-4 - Rectangle

a. Ecrire une fonction affiche_car qui reçoit en arguments un caractère c et un entier n - void affiche_car(char c, int n) - et qui imprime ce caractère n fois. Utilisez cette fonction pour écrire un programme qui demande à l'utilisateur d'entrer un nombre n puis qui affiche une ligne de longueur n en utilisant le caractère '*', c'est-à-dire une ligne composée de n '*'. n doit être compris entre 0 et 20. Voici un exemple d'exécution :

Ce programme dessine une ligne.
Entrez la longueur de la ligne (0 a 20) : 10
**********
Merci d'avoir utilise ce programme. A bientot !

b. En utilisant la fonction affiche_car, écrire un programme qui demande à l'utilisateur d'entrer deux nombres L et h puis qui dessine un rectangle de longueur L et de hauteur h à l'aide du caractère '*'. L et h doivent être compris entre 0 et 20. Voici un exemple d'exécution :

Ce programme dessine un rectangle plein.
Entrez la longueur du rectangle (0 a 20) : 10
Entrez la hauteur du rectangle (0 a 20) : 4
**********
**********
**********
**********
Merci d'avoir utilise ce programme. A bientot !

c. Reprendre l'exercice b en dessinant cette fois ci un rectanle creux. Voici un exemple d'exécution :

Ce programme dessine un rectangle creux.
Entrez la longueur du rectangle (0 a 20) : 10
Entrez la hauteur du rectangle (0 a 20) : 4
**********
*        *
*        *
**********
Merci d'avoir utilise ce programme. A bientot !

III-G-5 - Triangle isocèle

a. Ecrire un programme qui demande à l'utilisateur d'entrer un nombre h puis qui dessine un triangle isocèle de hauteur h et de surface minimale à l'aide du caractère '*'. h doit être compris entre 0 et 20. b. Afficher après le dessin le nombre d'étoiles affichées. Voici un exemple d'exécution :

Ce programme dessine un triangle.
Entrez la hauteur du triangle (0 a 20) : 4
*
***
*****
*******
Il y a 16 etoile(s) affichee(s).
Merci d'avoir utilise ce programme. A bientot !

III-G-6 - Somme

Ecrire un programme qui demande à l'utilisateur d'entrer au plus 10 nombres puis qui affiche la somme des nombres entrés. L'utilisateur peut à tout moment terminer sa liste en entrant 0. Voici quelques exemples d'utilisation :

Ce programme permet de calculer une somme.
Entrez la liste des nombres a additionner (terminez en entrant 0).
1
3
5
7
0
Le total est : 16.
Merci d'avoir utilise ce programme. A bientot !

Ce programme permet de calculer une somme.
Entrez la liste des nombres a additionner (terminez en entrant 0).
1
2
3
4
5
6
7
8
9
10
Le total est : 55.
Merci d'avoir utilise ce programme. A bientot !

IV - Tableaux, pointeurs et chaînes de caractères

IV-A - Les tableaux

IV-A-1 - Définition

Un tableau est un regroupement d'une ou plusieurs données de même type contigus en mémoire. L'accès à un élément du tableau se fait par un système d'indice, l'indice du premier élément étant 0. Par exemple :

1. int t[10];

déclare un tableau de 10 éléments (de type int) dont le nom est t. Les éléments du tableau vont donc de t[0], t[1], t[2] ... à t[9]. t est une variable de type tableau, plus précisément (dans notre cas), une variable de type tableau de 10 int (int [10]). Les éléments du tableau sont des int. Ils peuvent être utilisés comme n'importe quelle variable de type int.

IV-A-2 - Initialisation

On peut initialiser un tableau à l'aide des accolades. Par exemple :

1. int t[10] = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90};

Bien évidemment, on n'est pas obligé d'initialiser tous les éléments, on aurait donc pu par exemple nous arrêter après le 5ème élément, et dans ce cas les autres éléments du tableau seront automatiquement initialisés à 0. Attention ! une variable locale non initialisée contient « n'importe quoi », pas 0 !

Lorsqu'on déclare un tableau avec initialisation, on peut ne pas spécifier le nombre d'éléments car le compilateur le calculera automatiquement. Ainsi, la déclaration :

1. int t[] = {0, 10, 20, 30};

est strictement identique à :

1. int t[4] = {0, 10, 20, 30};

IV-A-3 - Création d'un type « tableau »

Tout d'abord, étudions un peu la logique du mot-clé typedef. Pour cela, supposons que l'on veuille pouvoir utiliser indifféremment les formules suivantes pour déclarer un entier :

1. int x;

et

1. ENTIER x;

Dans la première forme, on remplace x par ENTIER et on la fait précéder du mot-clé typedef, ce qui nous donne :

1. typedef int ENTIER;

Concrètement, cela signifie : un ENTIER est tout ce qui suit le mot int dans une déclaration. On peut également formuler une phrase similaire (genre : un TABLEAU est tout ce qui est précédé du mot int et suivi de [10] dans sa déclaration) pour définir un type TABLEAU mais il y a plus malin :

On remplace t par TABLEAU dans sa déclaration puis on ajoute devant le mot-clé typedef, exactement comme ce qu'on a fait avec x et ENTIER. Ainsi, le type tableau de 10 int se définit de la manière suivante :

1. typedef int TABLEAU[10];

Désormais :

1. int t[10];

et :

1. TABLEAU t;

sont strictement équivalents.

IV-A-4 - Les tableaux à plusieurs dimensions

On peut également créer un tableau à plusieurs dimensions. Par exemple :

1. int t[10][3];

Un tableau à plusieurs dimensions n'est en fait rien d'autre qu'un tableau (tableau à une dimension) dont les éléments sont des tableaux. Comme dans le cas des tableaux à une dimension, le type des éléments du tableau doit être parfaitement connu. Ainsi dans notre exemple, t est un tableau de 10 tableaux de 3 int, ou pour vous aider à y voir plus clair :

1. typedef int TRIPLET[3];

2. TRIPLET t[10];

Les éléments de t vont de t[0] à t[9], chacun étant un tableau de 3 int.

On peut bien entendu créer des tableaux à 3 dimensions, 4, 5, 6, ...

On peut également initialiser un tableau à plusieurs dimensions. Par exemple :

1. int t[3][4] = { {0, 1, 2, 3},

2. {4, 5, 6, 7},

3. {8, 9, 10, 11} };

Qu'on aurait également pu tout simplement écrire :

1. int t[][4] = { {0, 1, 2, 3},

2. {4, 5, 6, 7},

3. {8, 9, 10, 11} };

IV-A-5 - Calculer le nombre d'éléments d'un tableau

La taille d'un tableau est évidemment le nombre d'éléments du tableau multiplié par la taille de chaque élément. Ainsi, le nombre d'éléments dans un tableau est égal à sa taille divisée par la taille d'un élément. On utilise alors généralement la formule sizeof(t) / sizeof(t[0]) pour connaître le nombre d'éléments d'un tableau t. La macro définie ci-dessous permet de calculer la taille d'un tableau :

1. #define COUNT(t) (sizeof(t) / sizeof(t[0]))

IV-B - Les pointeurs

IV-B-1 - Les tableaux et les pointeurs

Pour nous fixer les idées, considérons le tableau t suivant :

1. char t[10];

Mais les règles que nous allons établir ici s'appliquent à n'importe quel type de tableau, y compris les tableaux à plusieurs dimensions.

Définissons ensuite le type TABLEAU par :

1. typedef char TABLEAU[10];

Mais avant d'aller plus loin, j'aimerais déjà préciser que les tableaux et les pointeurs n'ont rien de commun, à part peut-être le fait qu'on peut pointer sur n'importe quel élément d'un tableau (ou sur un tableau ...), tout comme on peut pointer sur n'importe quoi. Mais il y a quand même une chose qui lie les deux notions, c'est que : lorsqu'elle n'est pas utilisée en unique argument de sizeof ou en unique argument de l'opérateur & ("adresse de"), une expression de type tableau est toujours convertie par le compilateur en l'adresse de son premier élément. Cela signifie, dans ces conditions, que si t est un tableau de disons 10 éléments, l'écriture t est strictement équivalente à &t[0], donc t + 1 (qui est strictement équivalent à &(t[0]) + 1) est strictement équivalent à &t[1] et ainsi de suite. Ainsi :

1. t[5] = '*';

est strictement équivalent à :

1. *(t + 5) = '*';

Et ainsi de suite.

Dans la pratique, on utilise un pointeur sur un élément du tableau, généralement le premier. Cela permet d'accéder à n'importe quel élément du tableau par simple calcul d'adresse. En effet, comme nous l'avons dit plus haut : t + 1 est équivalent à &(t[1]), t + 2 à &(t[2]), etc.

Voici un exemple qui montre une manière de parcourir un tableau :

1. #include <stdio.h>

3. #define COUNT(t) (sizeof(t) / sizeof(t[0]))

5. void Affiche(int * p, size_t nbElements);

7. int main()

8. {

9. int t[10] = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90};

11. Affiche(t, COUNT(t));

13. return 0;

14. }

16. void Affiche(int * p, size_t nbElements)

17. {

18. size_t i;

20. for(i = 0; i < nbElements; i++)

21. printf("%d\n", p[i]);

22. }

IV-B-2 - L'arithmétique des pointeurs

L'arithmétique des pointeurs est née des faits que nous avons établis précédemment. En effet si p pointe sur un élément d'un tableau, p + 1 doit pointer sur l'élément suivant. Donc si la taille de chaque élément du tableau est par exemple de 4, p + 1 déplace le pointeur de 4 octets (où se trouve l'élément suivant) et non de un.

De même, puisque l'on devrait avoir (p + 1) - p = 1 et non 4, la différence entre deux adresses donne le nombre d'éléments entre ces adresses et non le nombre d'octets entre ces adresses. Le type d'une telle expression est ptrdiff_t, qui est défini dans le fichier stddef.h.

Et enfin, l'écriture p[i] est strictement équivalente à *(p + i).

Cela montre à quel point le typage des pointeurs est important. Cependant, il existe des pointeurs dits génériques capables de pointer sur n'importe quoi. Ainsi, la conversion d'un pointeur générique en un pointeur d'un autre type par exemple ne requiert aucun cast et vice versa.

IV-B-3 - Pointeurs constants et pointeurs sur constante

L'utilisation du mot-clé const avec les pointeurs est au début un peu délicate. En effet, en C on a ce qu'on appelle des pointeurs constants et des pointeurs sur constante (et donc aussi des pointeurs contants sur constante ...). Il faut bien savoir les différencier.

- Pointeur constant : Le pointeur, qui est une variable comme toute les autres, est déclaré const.

- Pointeur sur constante : C'est l'objet pointé qui est constant.

Dans le deuxième cas, l'objet pointé n'est pas nécessairement une constante. Seulement, on ne pourra pas le modifier via le pointeur (qui considère que l'objet est une constante). De même, on peut pointer sur une mémoire en lecture seule avec n'importe quel pointeur (et non nécessairement un pointeur sur constante) mais cela ne signifie pas que la mémoire devient désormais accessible en écriture (puisque la mémoire pointée est en lecture seule. Ce n'est pas en pointant là-dessus avec quoi que ce soit qu'on pourra changer cela.). Si le pointeur n'est pas un pointeur sur contante alors qu'il pointe sur une constante, le compilateur acceptera évidemment de modifier le contenu de la mémoire pointée (puisque l'écriture est syntaxiquement correcte), mais le problème se manifestera à l'exécution, lorsque le programme tentera de modifier le contenu de la zone en lecture seule.

Comme int * est le type d'un pointeur sur int, un pointeur constant sur un int est de type

1. int * const

Si on avait placé le mot-clé const avant int *, on obtiendrait :

1. const int *

qui correspond plutôt, contrairement à ce qu'on attendait, au type pointeur sur const int (pointeur sur constante) ! Voici deux illustrations de l'utilisation de const avec les pointeurs :

1. int n, m;

2. int * const p = &n;

3. *p = 10;

4. /* p = &m : INTERDIT ! p est une constante ! */

1. int n, m;

2. int const * p = &n; /* Ou const int * p = &n; */

3. p = &m;

4. /* *p = 10 : INTERDIT ! *p est une constante ! */

IV-B-4 - Pointeurs génériques

Le type des pointeurs génériques est void *. Comme ces pointeurs sont génériques, la taille des données pointées est inconnue et l'arithmétique des pointeurs ne s'applique donc pas à eux. De même, puisque la taille des données pointées est inconnue, l'opérateur d'indirection * ne peut être utilisé avec ces pointeurs, un cast est alors obligatoire. Par exemple :

1. int n;

2. void * p;

4. p = &n;

5. *((int *)p) = 10; /* p etant desormais vu comme un int *, on peut alors lui appliquer l'operateur *. */

Etant donné que la taille de toute donnée est multiple de celle d'un char, le type char * peut être également utilisé en tant que pointeur universel. En effet, une variable de type char * est un pointeur sur octet autrement dit peut pointer n'importe quoi. Cela s'avère pratique des fois (lorsqu'on veut lire le contenu d'une mémoire octet par octet par exemple) mais dans la plupart des cas, il vaut mieux toujours utiliser les pointeurs génériques. Par exemple, la conversion d'une adresse de type différent en char * et vice versa nécessite toujours un cast, ce qui n'est pas le cas avec les pointeurs génériques.

Dans printf, le spécificateur de format %p permet d'imprimer une adresse (void *) dans le format utilisé par le système.

Et pour terminer, il existe une macro à savoir NULL, définie dans stddef.h, permettant d'indiquer q'un pointeur ne pointe nulle part. Son intérêt est donc de permettre de tester la validité d'un pointeur et il est conseillé de toujours initialiser un pointeur à NULL.

IV-B-5 - Exemple avec un tableau à plusieurs dimensions

Soit :

1. int t[10][3];

Définissons le type TRIPLET par :

1. typedef int TRIPLET[3];

De façon à avoir :

1. TRIPLET t[10];

En dehors du cas sizeof et opérateur & ("adresse de"), t représente l'adresse de t[0] (qui est un TRIPLET) donc l'adresse d'un TRIPLET. En faisant t + 1, on se déplace donc d'un TRIPLET soit de 3 int.

D'autre part, t peut être vu comme un tableau de 30 int (3 * 10 = 30) puisque les éléments d'un tableau sont toujours contigus en mémoire. On peut donc accéder à n'importe quel élément de t à l'aide d'un pointeur sur int.

Soit p un pointeur sur int et faisons :

1. p = (int *)t;

On a alors, numériquement, les équivalences suivantes :

t

p

t + 1

p + 3

t + 2

p + 6

...

t + 9

p + 27

Prenons alors à présent, le 3ème TRIPLET de t soit t[2].

Puisque le premier élément de t[2] se trouve à l'adresse t + 2 soit p + 6, deuxième se trouve en p + 6 +1 et le troisième et dernier en p + 6 + 2. Après cet entier, on se retrouve au premier élément de t[3], en p + 9.

En conclusion, pour un tableau déclaré :

1. <type> t[N][M];

on a la formule :

1. t[i][j] = *(p + M*i + j) /* ou encore p[M*i + j] */

Où évidemment : p = (int *)t.

Et on peut bien sur étendre cette formule pour n'importe quelle dimension.

IV-B-6 - Passage d'un tableau en argument d'une fonction

Nous avons déjà vu que le passage d'un tableau en argument d'une fonction se fait tout simplement en passant l'adresse de son premier élément. Sachez également que, en argument d'une fonction,

1. <type> <identificateur>[]

est strictement équivalent à :

1. <type> * <identificateur>

Alors que dans une « simple » déclaration, la première déclare un tableau (qui doit être impérativement initialisé) et la deuxième un pointeur. On peut également préciser le nombre d'éléments du tableau mais cette information sera complètement ignorée par le compilateur. Elle n'a donc d'intérêt que pour la documentation.

IV-C - Les chaînes de caractères

IV-C-1 - Chaîne de caractères

Par définition, une chaîne de caractères, ou tout simplement : chaîne, est une suite finie de caractères. Par exemple, "Bonjour", "3000", "Salut !", "EN 4", ... sont des chaînes de caractères. En langage C, une chaîne de caractères littérale s'écrit entre double quottes, exactement comme dans les exemples donnés ci-dessus.

IV-C-2 - Longueur d'une chaîne

La longueur d'une chaîne est le nombre de caractères qu'elle comporte. Par exemple, la chaîne "Bonjour" comporte 7 caractères ('B', 'o', 'n', 'j', 'o', 'u' et 'r'). Sa longueur est donc 7. En langage C, la fonction strlen, déclarée dans le fichier string.h, permet d'obtenir la longueur d'une chaîne passée en argument. Ainsi, strlen("Bonjour") vaut 7.

IV-C-3 - Représentation des chaînes de caractères en langage C

Comme nous l'avons déjà mentionné plus haut, les chaîne de caractères littérales s'écrivent en langage C entre double quottes. En fait, le langage C ne dispose pas vraiment de type chaîne de caractères. Une chaîne est tout simplement représentée à l'aide d'un tableau de caractères.

Cependant, les fonctions manipulant des chaînes doivent être capables de détecter la fin d'une chaîne donnée. Autrement dit, toute chaîne de caractères doit se terminer par un caractère indiquant la fin de la chaîne. Ce caractère est le caractère '\0' et est appelé le caractère nul ou encore caractère de fin de chaîne. Son code ASCII est 0. Ainsi la chaîne "Bonjour" est en fait un tableau de caractères dont les éléments sont 'B', 'o', 'n', 'j', 'o', 'u', 'r', '\0', autrement dit un tableau de 8 caractères et on a donc "Bonjour"[0] = 'B', "Bonjour"[1] = 'o', "Bonjour"[2] = 'n', ... "Bonjour"[7] = '\0'. Toutefois, la mémoire utilisée pour stocker la chaîne peut être accessible qu'en lecture, le il n'est donc pas portable de tenter de la modifier.

Les fonctions de manipulation de chaîne de la bibliothèque standard du langage C sont principalement déclarées dans le fichier string.h. Voici un exemple d'utilisation d'une de ces fonctions.

1. #include <stdio.h>

2. #include <string.h>

4. int main()

5. {

6. char t[50];

8. strcpy(t, "Hello, world!");

9. printf("%s\n", t);

11. return 0;

12. }

Dans cet exemple, la chaîne t ne peut contenir tout au plus que 50 caractères, caractère de fin de chaîne inclus. Autrement dit t ne peut que contenir 49 caractères « normaux » car il faut toujours réserver une place pour le caractère de fin de chaîne : '\0'. On peut aussi bien sûr initialiser une chaîne au moment de sa déclaration, par exemple :

1. char s[50] = "Bonjour";

Qui est strictement équivalente à :

1. char s[50] = { 'B', 'o', 'n', 'j', 'o', 'u', 'r', '\0'};

Puisque, vu d'un pointeur, la valeur d'une expression littérale de type chaîne n'est autre que l'adresse de son premier élément, on peut utiliser un simple pointeur pour manipuler une chaîne. Par exemple :

1. char * p = "Bonjour";

Dans ce cas, p pointe sur le premier élément de la chaîne "Bonjour". Or, comme nous l'avons déjà dit plus haut, la mémoire allouée pour la chaîne "Bonjour" est en lecture seule donc on ne peut pas écrire par exemple :

1. p[2] = '*'; /* Interdit */

Avec un tableau, ce n'est pas l'adresse en mémoire de la chaîne qui est stockée, mais les caractères de la chaîne, copiés caractère par caractère. La mémoire utilisée par le tableau étant indépendante de celle utilisée par la chaîne source, on peut faire ce qu'on veut de notre tableau. La fonction strcpy permet de copier une chaîne vers un autre emplacement mémoire.

Le paragraphe suivant discute des fonctions de manipulation de chaînes en langage C.

IV-C-4 - Les fonctions de manipulation de chaîne

strcpy, strncpy

1. #include <stdio.h>

2. #include <string.h>

4. int main()

5. {

6. char t1[50], t2[50];

8. strcpy(t1, "Hello, world!");

9. strcpy(t2, "*************");

10. strncpy(t1, t2, 3);

11. printf("%s\n", t1);

13. return 0;

14. }

Attention ! Si t1 n'est pas assez grand pour pouvoir contenir la chaîne à copier, vous aurez un débordement de tampon (buffer overflow). Un tampon (ou buffer) est tout simplement une zone de la mémoire utilisée par un programme pour stocker temporairement des données. Par exemple, t1 est un buffer de 50 octets. Il est donc de la responsabilité du programmeur de ne pas lui passer n'importe quoi ! En effet en C, le compilateur suppose que le programmeur sait ce qu'il fait !

La fonction strncpy s'utilise de la même manière que strcpy. Le troisième argument indique le nombre de caractères à copier. Aucun caractère de fin de chaîne n'est automatiquement ajouté.

strcat, strncat

1. #include <stdio.h>

2. #include <string.h>

4. int main()

5. {

6. char t[50];

8. strcpy(t, "Hello, world");

9. strcat(t, " from");

10. strcat(t, " strcpy");

11. strcat(t, " and strcat");

12. printf("%s\n", t);

14. return 0;

15. }

strlen

Retourne le nombre de caractères d'une chaîne.

strcmp, strncmp

On n'utilise pas l'opérateur == pour comparer des chaînes car ce n'est pas les adresses qu'on veut comparer mais le contenu mémoire. La fonction strcmp compare deux chaînes de caractères et retourne :

- zéro si les chaînes sont identiques

- un nombre négatif si la première est "inférieure" (du point de vue lexicographique) à la seconde

- et un nombre positif si la première est "supérieure" (du même point de vue ...) à la seconde

Ainsi, à titre d'exemple, dans l'expression

1. strcmp("clandestin", "clavier")

La fonction retourne un nombre négatif car, 'n' étant plus petit que 'v' (dans le jeu de caractères ASCII, ça n'a rien à voir avec le langage C), "clandestin" est plus petit que "clavier".

IV-C-5 - Fusion de chaînes littérales

Le C permet de fusionner deux chaînes littérales en les plaçant simplement côte à côte. Cela se révèle particulièrement utile lorsqu'on doit composer une chaîne et que cette dernière soit trop longue a écrire sur une seule ligne. Par exemple :

1. #include <stdio.h>

3. int main()

4. {

5. printf( "Voici une chaine de caracteres particulierement longue, tres longue, "

6. "tellement longue (oui, longue !) qu'il a fallu la decouper en deux !" );

7. return 0;

8. }

IV-D - Exercices

IV-D-1 - Recherche dans un tableau

Nous nous proposons d'écrire un programme qui permet de trouver toutes les occurrences d'un nombre entré par l'utilisateur parmi un un ensemble de nombres précédemment entrés. L'utilisateur entrera dix nombres (que vous placerez dans un tableau), ensuite il entrera le nombre à rechercher et le programme devra afficher toutes les occurrences de ce nombre s'il y en a ainsi que le nombre d'occurrences trouvées. Voici un exemple d'exécution :

Ce programme permet de trouver toutes les occurrences d'un nombre.
Entrez 10 nombres :
t[0] : 2
t[1] : -1
t[2] : 3
t[3] : 28
t[4] : 3
t[5] : -8
t[6] : 9
t[7] : 40
t[8] : -1
t[9] : 8
Entrez le nombre a rechercher : 3
t[2]
t[4]
2 occurrence(s) trouvee(s).
Merci d'avoir utilise ce programme. A bientot !

IV-D-2 - Calcul de la moyenne

Nous nous proposons d'écrire un programme permettant de calculer la moyenne d'un élève. L'utilisateur entrera 5 "notes". Une note est constituée d'une note sur 20 (de type entier) et d'un coefficient compris entre 1 et 5 (entier également). La note définitive pour une matière est la note sur 20 multipliée par le coefficient. La moyenne de l'étudiant est égale au total des notes définitives divisé par la somme des coefficients.

On créera un tableau t de 5 tableaux de 3 entiers, c'est-à-dire int t[5][3]. Les 5 éléments de t sont destinés à accueillir les 5 notes. Pour chaque note t[i], t[i][0] contiendra la note sur 20, t[i][1] le coefficient et t[i][2] la note définitive. Le programme demandera à l'utilisateur d'entrer les notes puis affichera la moyenne de l'élève avec 2 chiffres après la virgule et les calculs intermédiaires. Voici un exemple d'exécution :

Ce programme permet de calculer votre moyenne scolaire.
Note 1 (0 a 20) : 14
Coef 1 (1 a 5)  : 5
Note 2 (0 a 20) : 10
Coef 2 (1 a 5)  : 5
Note 3 (0 a 20) : 16
Coef 3 (1 a 5)  : 3
Note 4 (0 a 20) : 8
Coef 4 (1 a 5)  : 1
Note 5 (0 a 20) : 12
Coef 5 (1 a 5)  : 1
+------+------+----------+
| Note | Coef | Note Def |
+------+------+----------+
|   14 |    5 |       70 |
+------+------+----------+
|   10 |    5 |       50 |
+------+------+----------+
|   16 |    3 |       48 |
+------+------+----------+
|    8 |    1 |        8 |
+------+------+----------+
|   12 |    1 |       12 |
+------+------+----------+
| Tot. |   15 |      188 |
+------+-----------------+
| Moy. |           12.53 |
+------+-----------------+
Merci d'avoir utilise ce programme. A bientot !

IV-D-3 - Manipulation de chaînes

Ecrire les fonctions de manipulation de chaînes suivantes :

- str_cpy, de rôle et d'utilisation semblables à la fonction strcpy de la bibliothèque standard

- str_len, de rôle et d'utilisation semblalbles à la fonction strlen de la bibliothèque standard

- str_equals, qui reçoit en arguments de chaînes de caractères et qui retourne VRAI si elles sont égales et FAUX dans le cas contraire

V - Les entrées/sorties en langage C

V-A - Introduction

Les entrées/sorties (E/S) ne font pas vraiment partie du langage C car ces opérations sont dépendantes du système. Cela signifie que pour réaliser des opérations d'entrée/sortie en C, il faut en principe passer par les fonctionnalités offertes par le système. Néanmoins sa bibliothèque standard est fournie avec des fonctions permettant d'effectuer de telles opérations afin de faciliter l'écriture de code portable. Les fonctions et types de données liées aux entrées/sorties sont principalement déclarés dans le fichier stdio.h (standard input/output).

V-B - Les fichiers

Les entrées/sorties en langage C se font par l'intermédiaire d'entités logiques, appelés flux, qui représentent des objets externes au programme, appelés fichiers. Un fichier peut être ouvert en lecture, auquel cas il est censé nous fournir des données (c'est-à-dire être lu) ou ouvert en écriture, auquel cas il est destiné à recevoir des données provenant du programme. Un fichier peut être à la fois ouvert en lecture et en écriture. Une fois qu'un fichier est ouvert, un flux lui est associé. Un flux d'entrée est un flux associé à un fichier ouvert en lecture et un flux de sortie un flux associé à un fichier ouvert un écriture. Tous les fichiers ouverts doivent être fermés avant la fin du programme.

Lorsque les données échangées entre le programme et le fichier sont de type texte, la nécessité de définir ce qu'on appelle une ligne est primordiale. En langage C, une ligne est une suite de caractères terminée par le caractère de fin de ligne (inclus) : '\n'. Par exemple, lorsqu'on effectue des saisies au clavier, une ligne correspond à une suite de caractères terminée par ENTREE. Puisque la touche ENTREE termine une ligne, le caractère généré par l'appui de cette touche est donc, en C standard, le caractère de fin de ligne soit '\n'.

V-C - Les entrée et sortie standards

Lorsque le système exécute un programme, trois fichiers sont automatiquement ouverts :

- l'entrée standard par défaut le clavier

- la sortie standard, par défaut l'écran (ou la console)

- et l'erreur standard, par défaut associé à l'écran (ou la console)

Respectivement associés aux flux stdin, stdout et stderr. Ils sont automatiquement fermés avant la fin du programme.

Chez la majorité des systèmes, dont Windows et UNIX, l'utilisateur peut rediriger les entrée et sortie standards vers un autre fichier à l'aide des symboles < et >. Par exemple, exécutez le programme qui affiche « Hello, world » à partir de l'interpréteur de commandes à l'aide de la commande suivante (nous supposons que nous sommes sous Windows et que le programme s'appelle hello.exe) :

1. hello > sortie.txt

et vous verrez que le message sera imprimé dans le fichier sortie.txt et non à l'écran. Si le fichier n'existe pas, il sera créé. S'il existe déjà, son ancien contenu sera effacé.

Un deuxième et dernier exemple : écrire un programme qui affiche Hello, world en écrivant explicitement sur stdout, autrement dit avec un printf beaucoup plus explicite. On utilisera alors la fonction fprintf qui permet d'écrire du texte sur un flux de sortie, dans notre cas : stdout, ce qui nous donne :

1. #include <stdio.h>

3. int main()

4. {

5. fprintf(stdout, "Hello, world\n");

6. return 0;

7. }

V-D - Exemple : lire un caractère, puis l'afficher

La macro getc permet de lire un caractère sur un flux d'entrée. La macro putc permet d'écrire un caractère sur un flux de sortie. Voici un programme simple qui montre comment utiliser les macros getc et putc :

1. #include <stdio.h>

3. int main()

4. {

5. int c; /* le caractere */

7. printf("Veuillez taper un caractere : ");

8. c = getc(stdin);

10. printf("Vous avez tape : ");

11. putc(c, stdout);

13. return 0;

14. }

Vous vous demandez certainement la raison pour laquelle on a utilisé int plutôt que char dans la déclaration de c. Et bien tout simplement parce que getc retourne un int (de même putc attend un argument de type int). Mais justement : Pourquoi ? Et bien parce que getc doit pouvoir non seulement retourner le caractère lu (un char) mais aussi une valeur qui ne doit pas être un char pour signaler qu'aucun caractère n'a pu être lu. Cette valeur est EOF. Elle est définie dans le fichier stdio.h. Dans ces conditions, il est clair qu'on peut utiliser tout sauf un char comme type de retour de getc.

Un des cas les plus fréquents où getc retourne EOF est lorsqu'on a rencontré la fin du fichier. La fin d'un fichier est un point situé au-delà du dernier caractère de ce fichier (si le fichier est vide, le début et la fin du fichier sont donc confondus). On dit qu'on a rencontré la fin d'un fichier après avoir encore tenté de lire dans ce fichier alors qu'on se trouve déjà à la fin, pas juste après avoir lu le dernier caractère. Lorsque stdin est associé au clavier, la notion de fin de fichier perd à priori son sens car l'utilisateur peut très bien taper n'importe quoi à n'importe quel moment. Cependant l'environnement d'exécution (le système d'exploitation) offre généralement un moyen de spécifier qu'on n'a plus aucun caractère à fournir (concrètement, pour nous, cela signifie que getc va retourner EOF). Sous Windows par exemple, il suffit de taper en début de ligne la combinaison de touches Ctrl + Z (héritée du DOS) puis de valider par ENTREE. Evidemment, tout recommence à zéro à la prochaine opération de lecture.

Les macros getchar et putchar s'utilisent comme getc et putc sauf qu'elles n'opèrent que sur stdin, respectivement stdout. Elles sont définies dans stdio.h comme suit :

1. #define getchar() getc(stdin)

2. #define putchar(c) putc(c, stdout)

Et enfin fgetc est une fonction qui fait la même chose que getc (qui peut être en fait une fonction ou une macro ...). De même fputc est une fonction qui fait la même chose que putc.

V-E - Saisir une chaîne de caractères

Il suffit de lire les caractères présents sur le flux d'entrée (dans notre cas : stdin) jusqu'à ce que l'on ait atteint la fin du fichier ou le caractère de fin de ligne. Nous devrons fournir en arguments de la fonction l'adresse du tampon destiné à contenir la chaîne de caractère saisie et la taille de ce tampon pour supprimer le risque de débordement de tampon.

1. #include <stdio.h>

3. char * saisir_chaine(char * lpBuffer, size_t nBufSize);

5. int main()

6. {

7. char lpBuffer[20];

9. printf("Entrez une chaine de caracteres : ");

10. saisir_chaine(lpBuffer, sizeof(lpBuffer));

12. printf("Vous avez tape : %s\n", lpBuffer);

14. return 0;

15. }

17. char * saisir_chaine(char * lpBuffer, size_t nBufSize)

18. {

19. size_t nbCar = 0;

20. int c;

22. c = getchar();

23. while (nbCar < nBufSize - 1 && c != EOF && c != '\n')

24. {

25. lpBuffer[nbCar] = (char)c;

26. nbCar++;

27. c = getchar();

28. }

30. lpBuffer[nbCar] = '\0';

32. return lpBuffer;

33. }

La fonction scanf permet également de saisir une chaîne de caractères ne comportant aucun espace (espace, tabulation, etc.) grâce au spécificateur de format %s. Elle va donc arrêter la lecture à la rencontre d'un espace (mais avant d'effectuer la lecture, elle va d'abord avancer jusqu'au premier caractère qui n'est pas un espace). scanf ajoute enfin le caractère de fin de chaîne. Le gabarit permet d'indiquer le nombre maximum de caractères à lire (caractère de fin de chaîne non compris). Lorsqu'on utilise scanf avec le spécificateur %s (qui demande de lire une chaîne sans espace), il ne faut jamais oublier de spécifier également le nombre maximum de caractères à lire (à mettre juste devant le s) sinon le programme sera ouvert aux attaques par débordement de tampon. Voici un exemple qui montre l'utilisation de scanf avec le spécicateur de format %s :

1. #include <stdio.h>

3. int main()

4. {

5. char lpBuffer[20];

7. printf("Entrez une chaine de caracteres : ");

8. scanf("%19s", lpBuffer);

10. printf("Vous avez tape : %s\n", lpBuffer);

12. return 0;

13. }

Et enfin, il existe également une fonction, gets, déclarée dans stdio.h, qui permet de lire une chaîne de caractères sur stdin. Cependant cette fonction est à proscrire car elle ne permet pas de spécifier la taille du tampon qui va recevoir la chaîne lue.

V-F - Lire une ligne avec fgets

La fonction fgets permet de lire une ligne (c'est-à-dire y compris le '\n') sur un flux d'entrée et de placer les caractères lus dans un buffer. Cette fonction ajoute ensuite le caractère '\0'. Exemple :

1. #include <stdio.h>

3. int main()

4. {

5. char lpBuffer[20];

7. printf("Entrez une chaine de caracteres : ");

8. fgets(lpBuffer, sizeof(lpBuffer), stdin);

10. printf("Vous avez tape : %s", lpBuffer);

12. return 0;

13. }

Dans cet exemple, deux cas peuvent se présenter :

- l'utilisateur entre une chaîne comportant 18 caractères tout au plus puis valide le tout par ENTREE, alors tous les caractères de la ligne, y compris le caractère de fin de ligne, sont copiés dans lpBuffer puis le caractère de fin de chaîne est ajouté

- l'utilisateur entre une chaîne comportant plus de 18 caractères (c'est-à-dire >= 19) puis valide le tout par ENTREE, alors seuls les 19 premiers caractères sont copiés vers lpBuffer puis le caractère de fin de chaîne est ajouté

V-G - Mécanisme des entrées/sorties en langage C

V-G-1 - Le tamponnage

V-G-1-a - Les tampons d'entrée/sortie

En langage C, les entrées/sorties sont par défaut bufférisées, c'est-à-dire que les données à lire (respectivement à écrire) ne sont pas directement lues (respectivement écrites) mais sont tout d'abord placées dans un tampon (buffer) associé au fichier. La preuve, vous avez certainement remarqué par exemple que lorsque vous entrez des données pour la première fois à l'aide du clavier, ces données ne seront lues qu'une fois que vous aurez appuyé sur la touche ENTREE. Ensuite, toutes les opérations de lecture qui suivent se feront immédiatement tant que le caractère '\n' est encore présent dans le tampon de lecture, c'est-à-dire tant qu'il n'a pas été encore lu. Lorsque le caractère '\n' n'est plus présent dans le buffer, vous devrez à nouveau appuyer sur ENTREE pour valider la saisie, et ainsi de suite.

Les opérations d'écriture sont moins compliquées, mais il y a quand même quelque chose dont il serait totalement injuste de ne pas en parler. Comme nous l'avons déjà dit plus haut, les entrées/sorties sont par défaut bufferisées c'est-à-dire passent par un tampon. Dans le cas d'une opération d'écriture, il peut arriver que l'on souhaite à un certain moment forcer l'écriture physique des données présentes dans le tampon sans attendre que le système se décide enfin de le faire. Dans ce cas, on utilisera la fonction fflush. Nous verrons dans le paragraphe suivant un exemple d'utilisation de cette fonction.

V-G-1-b - Les modes de tamponnage

Le langage C permet de spécifier le mode de tamponnage à utiliser avec un flux donné à l'aide de la fonction setvbuf. Elle doit être appelée avant toute utilisation du fichier.

Il existe 3 modes de tamponnage des entrées/sorties :

- Pas de tamponnage (_IONBF (no buffering)), dans lequel le flux n'est associé à aucun tampon. Les données sont directement écrites sur ou lues depuis le fichier.

- Tamponnage par lignes (_IOLBF (line buffering)), le mode par défaut (d'après la norme), dans lequel le flux est associé à un tampon vidé que lorsqu'il est plein, lorsque le caractère de fin de ligne a été envoyé, lorsqu'une opération de lecture sur un flux en mode "Pas de tampon" a été efectuée ou lorsqu'une opération de lecture sur un flux en mode "Tamponnage par lignes" nécessite son vidage. Par exemple, en pratique, lorsque stdout est dans ce mode, toute demande de lecture sur stdin provoquera l'écriture physique des caractères encore dans le tampon. Cela permet d'avoir les questions afichées à l'écran avant que l'utilisateur puisse entrer la réponse.

- Tamponnage strict (_IOFBF (full buffering)), dans lequel le flux associé à un tampon vidé que lorsqu'il est plein.

Dans tous les cas, la fermeture d'un fichier ouvert en écriture entraîne également l'écriture des caractères qui sont encore dans le tampon, s'il y en a (ce qui a bien évidemment lieu avant sa fermeture).

Le prototype de la fonction setvbuf est le suivant :

1. int setvbuf(FILE * f, char * buf, int mode, size_t size);

L'argument mode indique évidemment le mode à utiliser (_IONBF, _IOLBF ou _IOFBF) et l'argument size la taille du tampon (buf) à associer au flux. Si buf vaut NULL, un tampon de taille size sera alloué et associé au fichier. Les arguments buf et size sont évidemment ignorés lorsque mode vaut _IONBF.

Voici un exemple d'utilisation de cette fonction :

1. #include <stdio.h>

3. #define N 1

5. void loop(unsigned long n);

7. int main()

8. {

9. setvbuf(stdout, NULL, _IOFBF, 8);

11. printf("a\n"); /* Contenu du buffer : [a\n]. */

12. loop(N); /* Faire une quelconque longue boucle */

14. /* Le buffer n'est pas encore plein, rien ne s'affichera donc sur la sortie standard. */

16. printf("b\n"); /* Contenu du buffer : [a\nb\n]. */

17. fflush(stdout); /* Vider le buffer. */

19. /* Le buffer n'est pas encore plein mais fflush a ete appelee. On aura donc :   */

20. /* - Sur la sortie standard : + [a\nb\n].                                       */

21. /* - Dans le buffer : [] (rien).                                                */

23. loop(N); /* Faire une quelconque longue boucle */

25. printf("azertyuiop\n");

27. /* On aura :                                          */

28. /* - Dans le buffer : [azertyui].                     */

29. /* Le buffer est plein, le vidage s'impose. On aura : */

30. /* - Sur la sortie standard : + [azertui].            */

31. /* - Dans le buffer : [] (rien).                      */

32. /* Il reste encore les carateres [op\n]. On aura :    */

33. /* - Dans le buffer : [op\n].                         */

35. loop(N); /* Faire une quelconque longue boucle */

37. return 0;

39. /* Au dela de l'accolade : Fin du programme.                                           */

40. /* Tous les fichiers encore ouverts (dont la sortie standard) seront fermes. On aura : */

41. /* - Sur la sortie standard : + [op\n].                                                */

42. /* - Dans le buffer : [] (rien).                                                       */

43. }

45. void loop(unsigned long n)

46. {

47. unsigned long i, j, end = 100000000;

49. for(i = 0; i < n; i++)

50. for(j = 0; j < end; j++);

51. }

V-G-2 - Lire de manière sûre des données sur l'entrée standard

Tout d'abord, analysons le tout petit programme suivant :

1. #include <stdio.h>

3. int main()

4. {

5. char nom[12], prenom[12];

7. printf("Entrez votre nom : ");

8. fgets(nom, sizeof(nom), stdin);

10. printf("Entrez votre prenom : ");

11. fgets(prenom, sizeof(prenom), stdin);

13. printf("Votre nom est : %s", nom);

14. printf("Et votre prenom : %s", prenom);

16. return 0;

17. }

Dans ce programme, si l'utilisateur entre un nom comportant moins de 10 caractères puis valide par ENTREE, alors tous les caractères rentrent dans nom et le programme se déroule bien comme prévu. Par contre si l'utilisateur entre un nom comportant plus de 10 caractères, seuls les 11 premiers caractères seront copiés dans nom et des caractères sont donc encore présents dans le buffer du clavier. Donc, à la lecture du prénom, les caractères encore présents dans le buffer seront immédiatement lus sans que l'utilisateur n'ait pu entrer quoi que ce soit. Voici un deuxième exemple :

1. #include <stdio.h>

3. int main()

4. {

5. int n;

6. char c;

8. printf("Entrez un nombre (entier) : ");

9. scanf("%d", &n);

11. printf("Entrez un caractere : ");

12. scanf("%c", &c);

14. printf("Le nombre que vous ave entre est : %d\n", n);

15. printf("Le caractere que vous ave entre est : %c\n", c);

17. return 0;

18. }

Lorsqu'on demande à scanf de lire un nombre, elle va déplacer le pointeur jusqu'au premier caractère non blanc, lire tant qu'elle doit lire les caractères pouvant figurer dans l'expression d'un nombre, puis s'arrêter à la rencontre d'un caractère invalide (espace ou lettre par exemple).

Donc dans l'exemple ci-dessus, la lecture du caractère se fera sans l'intervention de l'utilisateur à cause de la présence du caractère '\n' (qui sera alors le caractère lu) due à la touche ENTREE frappée pendant la saisie du nombre.

Ces exemples nous montrent bien que d'une manière générale, il faut toujours vider le buffer du clavier après chaque saisie, sauf si celui-ci est déjà vide bien sûr. Pour vider le buffer du clavier, il suffit de manger tous les caractères présents dans le buffer jusqu'à ce qu'on ait rencontré le caractère de fin de ligne ou atteint la fin du fichier. A titre d'exemple, voici une version améliorée (avec vidage du tampon d'entrée après lecture) de notre fonction saisir_chaine :

1. char * saisir_chaine(char * lpBuffer, int nBufSize)

2. {

3. char * ret = fgets(lpBuffer, nBufSize, stdin);

5. if (ret != NULL)

6. {

7. char * p = lpBuffer + strlen(lpBuffer) - 1;

8. if (*p == '\n')

9. *p = '\0'; /* on ecrase le '\n' */

10. else

11. {

12. /* On vide le tampon de lecture du flux stdin */

13. int c;

15. do

16. c = getchar();

17. while (c != EOF && c != '\n');

18. }

19. }

21. return ret;

22. }

VI - L'allocation dynamique de mémoire

VI-A - Les fonctions malloc et free

L'intérêt d'allouer dynamiquement de la mémoire se ressent lorsqu'on veut créer un tableau dont la taille dont nous avons besoin n'est connue qu'à l'exécution par exemple. On utilise généralement les fonctions malloc et free.

1. int t[10];

2. ...

3. /* FIN */

Peut être remplacé par :

1. int * p;

3. p = malloc(10 * sizeof(int));

4. ...

5. free(p); /* libérer la mémoire lorsqu'on n'en a plus besoin */

6. /* FIN */

Les fonctions malloc et free sont déclarées dans le fichier stdlib.h. malloc retourne NULL en cas d'échec. Voici un exemple qui illustre une bonne manière de les utiliser :

1. #include <stdio.h>

2. #include <stdlib.h>

4. int main()

5. {

6. int * p;

8. /* Creation d'un tableau assez grand pour contenir 10 entiers */

9. p = malloc(10 * sizeof(int));

11. if (p != NULL)

12. {

13. printf("Succes de l'operation.\n");

14. p[0] = 1;

15. printf("p[0] = %d\n", p[0]);

16. free(p); /* Destruction du tableau. */

17. }

18. else

19. printf("Le tableau n'a pas pu etre cree.\n");

21. return 0;

22. }

VI-B - La fonction realloc

La fonction realloc :

1. void * realloc(void * memblock, size_t newsize);

permet de « redimensionner » une mémoire allouée dynamiquement (par malloc par exemple). Si memblock vaut NULL, realloc se comporte comme malloc. En cas de réussite, cette fonction retourne alors l'adresse de la nouvelle mémoire, sinon la valeur NULL est retournée et la mémoire pointée par memblock reste inchangée.

1. #include <stdio.h>

2. #include <stdlib.h>

4. int main()

5. {

6. int * p = malloc(10 * sizeof(int));

8. if (p != NULL)

9. {

10. /* Sauver l'ancienne valeur de p au cas ou realloc echoue. */

11. int * q = p;

12. /* Redimensionner le tableau. */

13. p = realloc(p, 20 * sizeof(int));

15. if (p != NULL)

16. {

17. printf("Succes de l'operation.\n");

18. p[0] = 1;

19. printf("p[0] = %d\n", p[0]);

20. free(p);

21. }

22. else

23. {

24. printf("Le tableau n'a pas pu etre redimensionne.\n");

25. free(q);

26. }

27. }

28. else

29. printf("Le tableau n'a pas pu etre cree.\n");

31. return 0;

32. }

VI-C - Exercices

VI-C-1 - Calcul de la moyenne (version 2)

Reprendre l'exercice IV-D-2 en demandant cette fois-ci à l'utilisateur le nombre de notes qu'il désire entrer (limité à exactement 5 dans l'ancien sujet). Voici un exemple d'exécution :

Ce programme permet de calculer votre moyenne scolaire.
Entrez le nombre de notes que vous voulez entrer : 5
Note 1 (0 a 20) : 14
Coef 1 (1 a 5)  : 5
Note 2 (0 a 20) : 10
Coef 2 (1 a 5)  : 5
Note 3 (0 a 20) : 16
Coef 3 (1 a 5)  : 3
Note 4 (0 a 20) : 8
Coef 4 (1 a 5)  : 1
Note 5 (0 a 20) : 12
Coef 5 (1 a 5)  : 1
+------+------+----------+
| Note | Coef | Note Def |
+------+------+----------+
|   14 |    5 |       70 |
+------+------+----------+
|   10 |    5 |       50 |
+------+------+----------+
|   16 |    3 |       48 |
+------+------+----------+
|    8 |    1 |        8 |
+------+------+----------+
|   12 |    1 |       12 |
+------+------+----------+
| Tot. |   15 |      188 |
+------+-----------------+
| Moy. |           12.53 |
+------+-----------------+
Merci d'avoir utilise ce programme. A bientot !

VI-C-2 - Recherche dans un tableau (version 2)

Reprenez l'exercice IV-D-1 en remplaçant les nombres par des chaînes de caractères. L'utilisateur entrera en premier lieu le nombre de chaînes qu'il va entrer.

Aucune chaîne ne doit excéder 20 caractères mais la mémoire que vous utiliserez pour stocker chaque chaîne entrée doit être juste assez grande pour la contenir. Autrement dit, votre objectif sera de minimiser la quantité de mémoire utilisée. Voici un exemple d'exécution :

Ce programme permet de trouver toutes les occurrences d'une chaine.
Combien de chaines voulez-vous entrer ? 10
Entrez 10 chaines de caracteres (20 caracteres par chaine au plus) :
t[0] : programmation
t[1] : langage
t[2] : C
t[3] : ANSI
t[4] : ISO
t[5] : IEC
t[6] : programmation
t[7] : programme
t[8] : code
t[9] : programme
Entrez la chaine a rechercher : programmation
t[0]
t[6]
2 occurrence(s) trouvee(s).
Merci d'avoir utilise ce programme. A bientot !

VII - Solutions des exercices

VII-A - La lettre X (II-E-1)

Voici la solution la plus évidente et la plus lisible :

1. #include <stdio.h>

3. int main()

4. {

5. printf("*   *\n");

6. printf(" * * \n");

7. printf("  *  \n");

8. printf(" * * \n");

9. printf("*   *\n");

10. return 0;

11. }

On peut cependant remarquer que les espaces après le dernier * de chaque ligne sont inutiles. De plus, rien ne nous oblige à faire un appel à printf pour chaque ligne. On peut faire tout le dessin avec un seul appel à cette fonction. Voici donc une autre solution, un peu moins éviddente que la première :

1. #include <stdio.h>

3. int main()

4. {

5. printf("*   *\n * *\n  *\n * *\n*   *\n");

6. return 0;

7. }

VII-B - Périmètre d'un rectangle (II-E-2)

1. #include <stdio.h>

3. #define LONGUEUR 100

4. #define HAUTEUR 60

6. int perimetre(int L, int h);

8. int main()

9. {

10. printf(

11. "Le perimetre d'un rectangle de longueur %d et de hauteur %d est %d.\n",

12. LONGUEUR, HAUTEUR, perimetre(LONGUEUR, HAUTEUR)

13. );

14. return 0;

15. }

17. int perimetre(int L, int h)

18. {

19. return 2 * (L + h);

20. }

Ayez l'habitude de représenter les valeurs figées par des macros. Cela permet d'avoir un code à la fois lisible et facilement maintenable : si un jour la spécification change, il suffit de changer les définitions des macros et non plusieurs parties du programme.

VII-C - Valeur absolue (III-G-1)

Il y a plusieurs façons d'y arriver. Voici deux exemples :

1. #include <stdio.h>

3. int main()

4. {

5. double x, y;

7. printf("Ce programme permet de determiner la valeur absolue d'un nombre.\n");

8. printf("Entrez un nombre : ");

9. scanf("%lf", &x);

11. if (x >= 0)

12. y = x;

13. else

14. y = -x;

16. printf("La valeur absolue de %f est : %f\n", x, y);

18. printf("Merci d'avoir utilise ce programme. A bientot !\n");

20. return 0;

21. }

1. #include <stdio.h>

3. int main()

4. {

5. double x;

7. printf("Ce programme permet de determiner la valeur absolue d'un nombre.\n");

8. printf("Entrez un nombre : ");

9. scanf("%lf", &x);

11. printf("La valeur absolue de %f est : %f\n", x, (x >= 0 ? x : -x));

13. printf("Merci d'avoir utilise ce programme. A bientot !\n");

15. return 0;

16. }

VII-D - Moyenne (III-G-2)

1. #include <stdio.h>

3. int main()

4. {

5. int note;

7. printf("Ce programme permet de determiner si vous avez eu la moyenne ou non.\n");

8. printf("Entrez votre note (0 a 20) : ");

9. scanf("%d", &note);

11. if (0 <= note && note <= 20)

12. {

13. if (note >= 10)

14. printf("Vous avez eu la moyenne.\n");

15. else

16. printf("Vous n'avez pas eu la moyenne.\n");

17. }

18. else

19. printf("Cette note n'est pas valide.\n");

21. printf("Merci d'avoir utilise ce programme. A bientot !\n");

23. return 0;

24. }

VII-E - L'heure dans une minute (III-G-3)

1. #include <stdio.h>

3. int main()

4. {

5. int h, m;

7. printf("Ce programme permet de determiner l'heure qu'il sera une minute plus tard.\n");

8. printf("Entrez l'heure actuelle : ");

9. scanf("%d", &h);

10. printf("Entrez la minute actuelle : ");

11. scanf("%d", &m);

13. m++;

15. if (m == 60)

16. {

17. m = 0;

18. h++;

20. if (h == 24)

21. h = 0;

22. }

24. printf("Dans une minute il sera : %02d:%02d\n", h, m);

26. printf("Merci d'avoir utilise ce programme. A bientot !\n");

28. return 0;

29. }

VII-F - Rectangle (III-G-4)

Le programme a est complet. Pour les programmes b et c, seule la fonction main() est donnée (les autres parties du programme restant inchangées par rapport à a).

a.

1. #include <stdio.h>

3. void affiche_car(char c, int n);

5. int main()

6. {

7. int L;

9. printf("Ce programme dessine une ligne.\n");

10. printf("Entrez la longueur de la ligne (0 a 20) : ");

11. scanf("%d", &L);

13. if (0 <= L && L <= 20)

14. {

15. affiche_car('*', L);

16. putchar('\n');

17. }

18. else

19. printf("Cette valeur est invalide.\n");

21. printf("Merci d'avoir utilise ce programme. A bientot !\n");

23. return 0;

24. }

26. void affiche_car(char c, int n)

27. {

28. if (c > 0 && n > 0)

29. {

30. int i; /* i : nombres de caracteres deja affiches */

32. for(i = 0; i < n; i++) /* on arrete des que i a atteint n */

33. putchar(c);

34. }

35. }

Vous ne vous attendiez certainement pas à ce que la valeur de c soit testée avant de le passer à putchar. La raison est tout simplement que l'argument attendu par putchar est un int (qui doit avoir une valeur positive) et non un char. Comme char peut faire référence à signed char, il vaut mieux tester la valeur du caractère reçu avant de faire quoi que ce soit avec. putchar sera étudiée dans plus de détails dans le chapitre V.

b.

1. int main()

2. {

3. int L;

5. printf("Ce programme dessine un rectangle plein.\n");

6. printf("Entrez la longueur du rectangle (0 a 20) : ");

7. scanf("%d", &L);

9. if (0 <= L && L <= 20)

10. {

11. int h;

13. printf("Entrez la hauteur du rectangle (0 a 20) : ");

14. scanf("%d", &h);

16. if (0 <= h && h <= 20)

17. {

18. /* On dessine h lignes (lignes 1 a h) */

20. int i; /* i : numero de la ligne en cours */

22. for(i = 1; i <= h; i++)

23. {

24. affiche_car('*', L);

25. putchar('\n');

26. }

27. }

28. else

29. printf("Cette valeur est invalide.\n");

30. }

31. else

32. printf("Cette valeur est invalide.\n");

34. printf("Merci d'avoir utilise ce programme. A bientot !\n");

36. return 0;

37. }

c.

1. int main()

2. {

3. int L;

5. printf("Ce programme dessine un rectangle creux.\n");

6. printf("Entrez la longueur du rectangle (0 a 20) : ");

7. scanf("%d", &L);

9. if (0 <= L && L <= 20)

10. {

11. int h;

13. printf("Entrez la hauteur du rectangle (0 a 20) : ");

14. scanf("%d", &h);

16. if (0 <= h && h <= 20)

17. {

18. int i;

20. for(i = 1; i <= h; i++)

21. {

22. /* Pour les lignes 1 et h, dessiner une ligne continue */

23. if (i == 1 || i == h)

24. affiche_car('*', L);

25. else

26. {

27. /* Pour le reste, seules les extreimites sont visibles */

28. putchar('*');

29. affiche_car(' ', L - 2);

30. putchar('*');

31. }

33. putchar('\n');

34. }

35. }

36. else

37. printf("Cette valeur est invalide.\n");

38. }

39. else

40. printf("Cette valeur est invalide.\n");

42. printf("Merci d'avoir utilise ce programme. A bientot !\n");

44. return 0;

45. }

VII-G - Triangle isocèle (III-G-5)

Ici aussi, seule la fonction main() est donnée pour les mêmes raisons que dans VII-F.

a. Il faut juste remarquer qu'à la ligne i (i allant de 1 à h), il y a h - i espaces suivi de 1 + 2 * (i - 1) étoiles.

1. int main()

2. {

3. int h;

5. printf("Ce programme dessine un triangle.\n");

6. printf("Entrez la hauteur du triangle (0 a 20) : ");

7. scanf("%d", &h);

9. if (0 <= h && h <= 20)

10. {

11. int i;

13. for(i = 1; i <= h; i++)

14. {

15. affiche_car(' ', h - i);

16. affiche_car('*', 2 * i - 1);

17. putchar('\n');

18. }

19. }

20. else

21. printf("Cette valeur est invalide.\n");

23. printf("Merci d'avoir utilise ce programme. A bientot !\n");

25. return 0;

26. }

b.

1. int main()

2. {

3. int h;

5. printf("Ce programme dessine un triangle.\n");

6. printf("Entrez la hauteur du triangle (0 a 20) : ");

7. scanf("%d", &h);

9. if (0 <= h && h <= 20)

10. {

11. int i, n, nb_etoiles; /* nb_etoiles : nombre d'etoiles deja affichees */

13. for(i = 1, nb_etoiles = 0; i <= h; i++)

14. {

15. n = h - i;

16. affiche_car(' ', n);

18. n = 2 * i - 1;

19. affiche_car('*', n);

21. nb_etoiles += n;

23. putchar('\n');

24. }

26. printf("Il y a %d etoile(s) affichee(s).\n", nb_etoiles);

27. }

28. else

29. printf("Cette valeur est invalide.\n");

31. printf("Merci d'avoir utilise ce programme. A bientot !\n");

33. return 0;

34. }

VII-H - Somme (III-G-6)

1. #include <stdio.h>

3. int main()

4. {

5. int n, saisis = 0, fini = 0, total = 0;

7. printf("Ce programme permet de calculer une somme.\n");

8. printf("Entrez la liste des nombres a additionner (terminez en entrant 0).\n");

10. while (!fini)

11. {

12. scanf("%d", &n);

13. saisis++;

14. total += n;

15. fini = (saisis == 10 || n == 0);

16. }

18. printf("Le total est : %d\n", total);

20. printf("Merci d'avoir utilise ce programme. A bientot !\n");

22. return 0;

23. }

VII-I - Recherche dans un tableau (IV-D-1)

1. #include <stdio.h>

3. int main()

4. {

5. int t[10], i, n, nb_occurrences = 0;

7. printf("Ce programme permet de trouver toutes les occurrences d'un nombre.\n");

8. printf("Entrez 10 nombres :\n");

10. for(i = 0; i < 10; i++)

11. {

12. printf("t[%d] : ", i);

13. scanf("%d", t + i);

14. }

16. printf("Entrez le nombre a rechercher : ");

17. scanf("%d", &n);

19. for(i = 0; i < 10; i++)

20. {

21. if (t[i] == n)

22. {

23. printf("t[%d]\n", i);

24. nb_occurrences++;

25. }

26. }

28. printf("%d occurrence(s) trouvee(s)\n", nb_occurrences);

30. printf("Merci d'avoir utilise ce programme. A bientot !\n");

32. return 0;

33. }

VII-J - Calcul de la moyenne (IV-D-2)

1. #include <stdio.h>

3. int main()

4. {

5. int t[5][3], i, total_coefs = 0, total_notes = 0;

6. double moyenne;

8. printf("Ce programme permet de calculer votre moyenne scolaire.\n");

10. for(i = 0; i < 5; i++)

11. {

12. /* Saisie des notes */

14. printf("Note %d : ", i + 1);

15. scanf("%d", &(t[i][0]));

16. printf("Coef %d : ", i + 1);

17. scanf("%d", &(t[i][1]));

18. t[i][2] = t[i][0] * t[i][1];

20. /* Calcul des totaux */

22. total_notes += t[i][2];

23. total_coefs += t[i][1];

24. }

26. moyenne = ((double)total_notes) / total_coefs;

28. /* Affichage du resultat */

30. printf("+------+------+----------+\n");

31. printf("| Note | Coef | Note Def |\n");

32. printf("+------+------+----------+\n");

34. for(i = 0; i < 5; i++)

35. {

36. printf("| %4d | %4d | %8d |\n", t[i][0], t[i][1], t[i][2]);

37. printf("+------+------+----------+\n");

38. }

40. printf("| Tot. | %4d | %8d |\n", total_coefs, total_notes);

41. printf("+------+-----------------+\n");

42. printf("| Moy. | %15.2f |\n", moyenne);

43. printf("+------+-----------------+\n");

45. printf("Merci d'avoir utilise ce programme. A bientot !\n");

47. return 0;

48. }

VII-K - Manipulation de chaînes (IV-D-3)

1. char * str_cpy(char * dest, const char * source)

2. {

3. int i;

5. for(i = 0; source[i] != '\0'; i++)

6. dest[i] = source[i];

8. dest[i] = '\0';

10. return dest;

11. }

13. size_t str_len(const char * t)

14. {

15. size_t len;

17. for(len = 0; t[len] != '\0'; len++)

18. /* On ne fait rien, on laisse seulement boucler */ ;

20. return len;

21. }

23. int str_equals(const char * s1, const char * s2)

24. {

25. int i = 0, equals = 1;

27. while (equals && s1[i] != '\0' && s2[i] != '\0')

28. {

29. if (s1[i] != s2[i])

30. equals = 0;

31. i++;

32. }

34. if (equals && (s1[i] != '\0' || s2[i] != '\0'))

35. equals = 0;

37. return equals;

38. }

Remarquez bien que nous avons choisi char * comme type de retour de str_cpy et non void. La valeur retournée n'est autre que la valeur du pointeur dest. Le prototype de str_cpy est strictement le même que celui de la fonction standard strcpy. Cela permet d'écrire du code compact lorsqu'on a envie, par exemple :

1. char s[50];

2. strcat(strcpy(s, "Bonjour"), " tout le monde !");

Le deuxième argument de la fonction (s2) est déclaré const char * et non simplement char *. En effet, les données pointées par s2 doivent être uniquement lues par la fonction, cette dernière ne doit pas avoir le droit de les modifier. L'ajout du qualificateur const ici est donc plus que recommandé. De plus, cela a un aspect documentaire : la présence ou l'abscence de const permet aux utilisateurs d'une fonction de savoir quelles données pourraient être modifiées par la fonction et quelles données seront justes lues.

VII-L - Calcul de la moyenne (version 2) (VI-C-1)

1. #include <stdio.h>

2. #include <stdlib.h>

4. typedef int NOTE[3];

6. int main()

7. {

8. int n;

10. printf("Ce programme permet de calculer votre moyenne scolaire.\n");

11. printf("Entrez le nombre de notes que vous voulez entrer : ");

12. scanf("%d", &n);

14. if (n > 0)

15. {

16. NOTE * t = malloc(n * sizeof(NOTE));

18. if (t != NULL)

19. {

20. int i, total_coefs = 0, total_notes = 0;

21. double moyenne;

23. for(i = 0; i < n; i++)

24. {

25. printf("Note %d : ", i + 1);

26. scanf("%d", &(t[i][0]));

27. printf("Coef %d : ", i + 1);

28. scanf("%d", &(t[i][1]));

29. t[i][2] = t[i][0] * t[i][1];

30. total_notes += t[i][2];

31. total_coefs += t[i][1];

32. }

34. moyenne = ((double)total_notes) / total_coefs;

36. printf("+------+------+----------+\n");

37. printf("| Note | Coef | Note Def |\n");

38. printf("+------+------+----------+\n");

40. for(i = 0; i < n; i++)

41. {

42. printf("| %4d | %4d | %8d |\n", t[i][0], t[i][1], t[i][2]);

43. printf("+------+------+----------+\n");

44. }

46. free(t);

48. printf("| Tot. | %4d | %8d |\n", total_coefs, total_notes);

49. printf("+------+-----------------+\n");

50. printf("| Moy. | %15.2f |\n", moyenne);

51. printf("+------+-----------------+\n");

52. }

53. else

54. printf("Le tableau n'a pas pu etre cree.\n");

55. }

56. else

57. printf("Cette valeur est invalide.\n");

59. printf("Merci d'avoir utilise ce programme. A bientot !\n");

61. return 0;

62. }

VII-M - Recherche dans un tableau (version 2) (VI-C-2)

1. #include <stdio.h>

2. #include <stdlib.h>

3. #include <string.h>

5. typedef char * PCHAR;

7. size_t get_string(char * lpBuffer, int nBufSize);

9. int main()

10. {

11. int n;

12. char s[21];

14. printf("Ce programme permet de trouver toutes les occurrences d'une chaine.\n");

15. printf("Combien de chaines voulez-vous entrer ? ");

16. scanf("%d", &n);

17. get_string(s, sizeof(s)); /* Pour purger stdin */

19. if (n > 0)

20. {

21. PCHAR * t = malloc(n * sizeof(PCHAR));

23. if (t != NULL)

24. {

25. int i, nb_occurrences = 0, succes = 1;

27. printf("Entrez %d chaines de caracteres (20 caracteres par chaine au plus) :\n", n);

29. for(i = 0; succes && i < n; i++)

30. {

31. size_t len;

33. printf("t[%d] : ", i);

34. len = get_string(s, sizeof(s));

35. t[i] = malloc(len + 1);

37. if (t[i] != NULL)

38. strcpy(t[i], s);

39. else

40. {

41. int j;

43. for(j = 0; j < i; j++)

44. free(t[j]);

46. succes = 0;

48. printf("Memoire insuffisante pour continuer.\n");

49. }

50. }

52. if (succes)

53. {

54. printf("Entrez la chaine a rechercher : ");

55. get_string(s, sizeof(s));

57. for(i = 0; i < n; i++)

58. {

59. if (strcmp(t[i], s) == 0)

60. {

61. printf("t[%d]\n", i);

62. nb_occurrences++;

63. }

65. free(t[i]);

66. }

68. printf("%d occurrence(s) trouvee(s)\n", nb_occurrences);

69. }

71. free(t);

72. }

73. else

74. printf("Le tableau n'a pas pu etre cree.\n");

75. }

77. printf("Merci d'avoir utilise ce programme. A bientot !\n");

79. return 0;

80. }

82. size_t get_string(char * lpBuffer, int nBufSize)

83. {

84. size_t len = 0;

86. if (fgets(lpBuffer, nBufSize, stdin) != NULL)

87. {

88. char * p;

89. len = strlen(lpBuffer);

91. p = lpBuffer + len - 1;

92. if (*p == '\n')

93. {

94. *p = '\0'; /* on ecrase le '\n' */

95. len--;

96. }

97. else

98. {

99. /* On vide le tampon de lecture du flux stdin */

100. int c;

102. do

103. c = getchar();

104. while (c != EOF && c != '\n');

105. }

106. }

108. return len;

109. }

VIII - Conclusion

Ce tutoriel vous a présenté les éléments indispensables pour bien démarrer avec le langage C. Pour maîtriser un langage (et la programmation), il ne faut cependant pas s'arrêter à une bonne compréhension, il faut aussi acquérir de l'expérience. Améliorer les exemples et les solutions des exercices, se fixer des projets, sont des moyens très efficaces pour obtenir cette expérience et ensuite passer à la suite du cours. Alors, à vos projets !