Claude Delannoy

Programmer

en

langage C

Cours et exercices corrigés

5e édition 2009

Groupe Eyrolles

61, bd Saint-Germain

75240 Paris cedex 05

www.editions-eyrolles.com

La cinquième édition du présent ouvrage est parue en 2009 sous l’ISBN 978-2-212-11825-4.

À l’occasion de ce quatrième tirage, elle bénéficie d’une nouvelle couverture.

Le texte reste inchangé, à l’exception de quelques références à la norme ISO C11 publiée en 2011

En application de la loi du 11 mars 1957, il est interdit de reproduire intégralement ou partiellement le présent ouvrage, sur quelque support que ce soit, sans autorisation de l’éditeur ou du Centre Français d’Exploitation du Droit de Copie, 20, rue des Grands-Augustins, 75006 Paris.

ISBN 978-2-212-11825-4

© Groupe Eyrolles

Le format ePub a été préparé par Isako www.isako.com à partir de l'édition papier du même ouvrage.

Table

Couverture

Titre

Licence

Table

Avant-propos

Remarque concernant cette nouvelle édition :

Chapitre 1 - Généralités sur le langage C

1 Présentation par l’exemple de quelques instructions du langage C

1.1 Un exemple de programme en langage C

1.2 Structure d’un programme en langage C

1.3 Déclarations

1.4 Pour écrire des informations : la fonction printf

1.5 Pour faire une répétition : l’instruction for

1.6 Pour lire des informations : la fonction scanf

1.7 Pour faire des choix : l’instruction if

1.8 Les directives à destination du préprocesseur

1.9 Un second exemple de programme

2 Quelques règles d’écriture

2.1 Les identificateurs

2.2 Les mots-clés

2.3 Les séparateurs

2.4 Le format libre

2.5 Les commentaires

3 Création d’un programme en langage C

3.1 L’édition du programme

3.2 La compilation

3.3 L’édition de liens

3.4 Les fichiers en-tête

Chapitre 2 - Les types de base du langage C

1 La notion de type

2 Les types entiers

2.1 Leur représentation en mémoire

2.2 Les différents types d’entiers

2.3 Notation des constantes entières

3 Les types flottants

3.1 Les différents types et leur représentation en mémoire

3.2 Notation des constantes flottantes

4 Les types caractères

4.1 La notion de caractère en langage...

Avant-propos

Le langage C a été créé en 1972 par Denis Ritchie avec un objectif relativement limité : écrire un système d’exploitation (UNIX). Mais ses qualités opérationnelles l’ont très vite fait adopter par une large communauté de programmeurs.

Une première définition de ce langage est apparue en 1978 avec l’ouvrage de Kernighan et Ritchie The C programming language. Mais ce langage a continué d’évoluer après cette date à travers les différents compilateurs qui ont vu le jour. Son succès international a contribué à sa normalisation, d’abord par l’ANSI (American National Standard Institute), puis par l’ISO (International Standards Organization), plus récemment en 1993 par le CEN (Comité européen de normalisation) et enfin, en 1994, par l’AFNOR. En fait, et fort heureusement, toutes ces normes sont identiques, et l’usage veut qu’on parle de « C ANSI » ou de « C norme ANSI ».

La norme ANSI élargit, sans la contredire, la première définition de Kernighan et Ritchie. Outre la spécification de la syntaxe du langage, elle a le mérite de fournir la description d’un ensemble de fonctions qu’on doit trouver associées à tout compilateur C sous forme d’une bibliothèque standard. En revanche, compte tenu de son arrivée tardive, cette norme a cherché à « préserver l’existant », en acceptant systématiquement les anciens programmes. Elle n’a donc pas pu supprimer certaines formulations quelque peu désuètes ou redondantes. Par exemple, la première définition de Kernighan et Ritchie prévoit qu’on déclare une fonction en précisant uniquement le type de son résultat. La norme autorise qu’on la déclare sous forme d’un « prototype » (qui précise en plus le type de ses arguments) mais ne l’impose pas. Notez toutefois que le prototype deviendra obligatoire en C++.

Cet ouvrage a été conçu comme un cours de programmation en langage C. Suivant notre démarche habituelle, héritée de notre expérience de l’enseignement, nous présentons toujours les notions fondamentales sur un ou plusieurs exemples avant d’en donner plus formellement la portée générale. Souvent constitués de programmes complets, ces exemples permettent l’autoexpérimentation.

La plupart des chapitres de cet ouvrage proposent des exercices que nous vous conseillons de résoudre d’abord sur papier, en comparant vos solutions avec celles fournies en fin de volume et en réfléchissant sur les différences de rédaction qui ne manqueront pas d’apparaître. Ils serviront à la fois à contrôler les connaissances acquises et à les appliquer à des situations variées.

Nous avons cherché à privilégier tout particulièrement la clarté et la progressivité de l’exposé. Dans cet esprit, nous avons systématiquement évité les « références avant », ce qui, le cas échéant, autorise une étude séquentielle ; de même, les points les plus techniques ne sont exposés qu’une fois les bases du langage bien établies (une présentation prématurée serait perçue comme un bruit de fond masquant le fondamental).

D’une manière générale, notre fil conducteur est ce qu’on pourrait appeler le « C moderne », c’est-à-dire non pas la norme ANSI pure et dure, mais plutôt l’esprit de la norme dans ce qu’elle a de positif. Nous pensons ainsi forger chez le lecteur de bonnes habitudes de programmation en C et, par la même occasion, nous lui facilitons son entrée future dans le monde du C++.

Enfin, outre son caractère didactique, nous avons doté cet ouvrage d’une organisation appropriée à une recherche rapide d’information :

ses chapitres sont fortement structurés : la table des matières, fort détaillée, offre de nombreux points d’entrée,

au fil du texte, des encadrés viennent récapituler la syntaxe des différentes instructions,

une annexe fournit la description des fonctions les plus usitées de la bibliothèque standard (il s’agit souvent d’une reprise d’informations déjà présentées dans le texte),

un index détaillé permet une recherche sur un point précis ; il comporte également, associé à chaque nom de fonction standard, le nom du fichier en-tête (.h) correspondant.

Remarque concernant cette nouvelle édition :

L’ISO a publié en 1999, sous la référence ISO/IEC 9899:1999, une extension de la norme du langage C, plus connue sous l’acronyme C99. Bien qu’ancienne, celle-ci est loin d’être implémentée dans sa totalité par tous les compilateurs. Dans cette nouvelle édition :

la mention C ANSI continue à désigner l’ancienne norme, souvent baptisée C89 ou C90 ;

lorsque cela s’est avéré justifié, nous avons précisé les nouveautés introduites par la norme...

Chapitre 1

Généralités sur le langage C

Dans ce chapitre, nous vous proposons une première approche d’un programme en langage C, basée sur deux exemples commentés. Vous y découvrirez (pour l’instant, de façon encore informelle) comment s’expriment les instructions de base (déclaration, affectation, lecture et écriture), ainsi que deux des structures fondamentales (boucle avec compteur, choix).

Nous dégagerons ensuite quelques règles générales concernant l’écriture d’un programme. Enfin, nous vous montrerons comment s’organise le développement d’un programme en vous rappelant ce que sont l’édition, la compilation, l’édition de liens et l’exécution.

1 Présentation par l’exemple de quelques instructions du langage C

1.1 Un exemple de programme en langage C

Voici un exemple de programme en langage C, accompagné d’un exemple d’exécution. Avant d’en lire les explications qui suivent, essayez d’en percevoir plus ou moins le fonctionnement.

#include <stdio.h>
#include <math.h>
#define NFOIS 5

main()
{ int i ;
  float x ;
  float racx ;

  printf ("Bonjour\n") ;
  printf ("Je vais vous calculer %d racines carrées\n", NFOIS) ;

  for (i=0 ; i<NFOIS ; i++)
    { printf ("Donnez un nombre : ") ;
      scanf ("%f", &x)  ;
      if (x < 0.0)
       printf ("Le nombre %f ne possède pas de racine carrée\n", x) ;
       else
        { racx = sqrt (x) ;
          printf ("Le nombre %f a pour racine carrée : %f\n", x, racx) ;
        }
    }
  printf ("Travail terminé - Au revoir") ;
}

Bonjour
Je vais vous calculer 5 racines carrées
Donnez un nombre : 4
Le nombre 4.000000 a pour racine carrée : 2.000000
Donnez un nombre : 2
Le nombre 2.000000 a pour racine carrée : 1.414214
Donnez un nombre : -3
Le nombre -3.000000 ne possède pas de racine carrée
Donnez un nombre : 5.8
Le nombre 5.800000 a pour racine carrée : 2.408319
Donnez un nombre : 12.58
Le nombre 12.580000 a pour racine carrée : 3.546829
Travail terminé - Au revoir

Nous reviendrons un peu plus loin sur le rôle des trois premières lignes. Pour l’instant, admettez simplement que le symbole NFOIS est équivalent à la valeur 5.

1.2 Structure d’un programme en langage C

La ligne :

main()

se nomme un « en-tête ». Elle précise que ce qui sera décrit à sa suite est en fait le programme principal (main). Lorsque nous aborderons l’écriture des fonctions en C, nous verrons que celles-ci possèdent également un tel en-tête ; ainsi, en C, le programme principal apparaîtra en fait comme une fonction dont le nom (main) est imposé.

Le programme (principal) proprement dit vient à la suite de cet en-tête. Il est délimité par les accolades « { » et « } ». On dit que les instructions situées entre ces accolades forment un « bloc »...

Chapitre 2

Les types de base

du langage C

Les types char, int et float que nous avons déjà rencontrés sont souvent dits « scalaires » ou « simples », car, à un instant donné, une variable d’un tel type contient une seule valeur. Ils s’opposent aux types « structurés » (on dit aussi « agrégés ») qui correspondent à des variables qui, à un instant donné, contiennent plusieurs valeurs (de même type ou non). Ici, nous étudierons en détail ce que l’on appelle les types de base du langage C ; il s’agit des types scalaires à partir desquels pourront être construits tous les autres, dits « types dérivés », qu’il s’agisse :

de types structurés comme les tableaux, les structures ou les unions,

d’autres types simples comme les pointeurs ou les énumérations.

Auparavant, cependant, nous vous proposons de faire un bref rappel concernant la manière dont l’information est représentée dans un ordinateur et la notion de type qui en découle.

1 La notion de type

La mémoire centrale est un ensemble de positions binaires nommées bits. Les bits sont regroupés en octets (8 bits), et chaque octet est repéré par ce qu’on nomme son adresse.

L’ordinateur, compte tenu de sa technologie actuelle, ne sait représenter et traiter que des informations exprimées sous forme binaire. Toute information, quelle que soit sa nature, devra être codée sous cette forme. Dans ces conditions, on voit qu’il ne suffit pas de connaître le contenu d’un emplacement de la mémoire (d’un ou de plusieurs octets) pour...

Chapitre 3

Les opérateurs et les

expressions en langage C

1 L’originalité des notions d’opérateur et d’expression en langage C

Le langage C est certainement l’un des langages les plus fournis en opérateurs. Cette richesse se manifeste tout d’abord au niveau des opérateurs classiques (arithmétiques, relationnels, logiques) ou moins classiques (manipulations de bits). Mais, de surcroît, le C dispose d’un important éventail d’opérateurs originaux d’affectation et d’incrémentation.

Ce dernier aspect nécessite une explication. En effet, dans la plupart des langages, on trouve, comme en C :

d’une part, des expressions formées (entre autres) à l’aide d’opérateurs,

d’autre part, des instructions pouvant éventuellement faire intervenir des expressions, comme, par exemple, l’instruction d’affectation :

y = a * x +b ;

ou encore l’instruction d’affichage :

printf ("valeur %d", n + 2*p) ;

dans laquelle apparaît l’expression n + 2 * p.

Mais, généralement, dans les langages autres que C, l’expression possède une valeur mais ne réalise aucune action, en particulier aucune affectation d’une valeur à une variable. Au contraire, l’affectation y réalise une affectation d’une valeur à une variable mais ne possède pas de valeur. On a affaire à deux notions parfaitement disjointes. En langage C, il en va différemment puisque :

d’une part, les (nouveaux) opérateurs d’incrémentation pourront non seulement intervenir au sein d’une expression (laquelle, au bout du compte, possédera une valeur), mais également agir sur le contenu de variables. Ainsi, l’expression (car, comme nous le verrons, il s’agit bien d’une expression en C) :

++i

réalisera une action, à savoir : augmenter la valeur i de 1 ; en même temps, elle aura une valeur, à savoir celle de i après incrémentation.

d’autre part, une affectation apparemment classique telle que :

i = 5

pourra, à son tour, être considérée comme une expression (ici, de valeur 5). D’ailleurs, en C, l’affectation (=) est un opérateur. Par exemple, la notation suivante :

k = i = 5

représente une expression en C (ce n’est pas encore une instruction - nous y reviendrons). Elle sera interprétée comme :

k = (i = 5)

Autrement dit, elle affectera à i la valeur 5 puis elle affectera à k la valeur de l’expression i = 5, c’est-à-dire 5.

En fait, en C, les notions d’expression et d’instruction sont étroitement liées puisque la principale instruction de ce langage est une expression terminée par un point-virgule. On la nomme souvent « instruction expression ». Voici des exemples de telles instructions qui reprennent les expressions évoquées ci-dessus :

++i ;
i = 5 ;
k = i = 5 ;

Les deux premières ont l’allure d’une affectation telle qu’on la rencontre classiquement dans la plupart des autres langages. Notez que, dans les deux cas, il y a évaluation d’une expression (++i ou i=5) dont la valeur est finalement inutilisée. Dans le dernier cas, la valeur de l’expression i=5, c’est-à-dire 5, est à son tour affectée à k ; par contre, la valeur finale de l’expression complète est, là encore, inutilisée.

Ce chapitre vous présente la plupart des opérateurs du C ainsi que les règles de priorité et de conversion de type qui interviennent dans les évaluations des expressions. Les (quelques) autres opérateurs concernent essentiellement les pointeurs, l’accès aux tableaux et aux structures et les manipulations de bits. Ils seront exposés dans la suite de cet ouvrage.

2 Les opérateurs arithmétiques en C

2.1 Présentation des opérateurs

Comme tous les langages, C dispose d’opérateurs classiques « binaires » (c’est-à-dire portant sur deux « opérandes »), à savoir l’addition (+), la soustraction (-), la multiplication (*) et la division (/), ainsi que d’un opérateur « unaire » (c’est-à-dire ne portant que sur un seul opérande) correspondant à l’opposé noté - (comme dans -n ou dans -x+y).

Les opérateurs binaires ne sont a priori définis que pour deux opérandes ayant le même type parmi : int, long int, float, double, long double et ils fournissent un résultat de même type que leurs opérandes.

Remarque

En machine, il n’existe, par exemple, que des additions de deux entiers de même taille ou de flottants de même taille. Il n’existe pas d’addition d’un entier et d’un flottant ou de deux flottants de taille différente.

Mais nous verrons, dans le paragraphe 2, que, par le jeu des conversions implicites, le compilateur saura leur donner une signification :

soit lorsqu’ils porteront sur des opérateurs de type différent,

soit lorsqu’ils porteront sur des opérandes de type char ou short.

De plus, il existe un opérateur de modulo noté % qui ne peut porter que...

Chapitre 4

Les entrées-sorties

conversationnelles

Jusqu’ici, nous avons utilisé de façon intuitive les fonctions printf et scanf pour afficher des informations à l’écran ou pour en lire au clavier. Nous vous proposons maintenant d’étudier en détail les différentes possibilités de ces fonctions, ce qui nous permettra de répondre à des questions telles que :

quelles sont les écritures autorisées pour des nombres fournis en données ? Que se passe-t-il lorsque l’utilisateur ne les respecte pas ?

comment organiser les données lorsque l’on mélange les types numériques et les types caractères ?

que se produit-il lorsque, en réponse à scanf, on fournit trop ou trop peu d’informations ?

comment agir sur la présentation des informations à l’écran ?

Nous nous limiterons ici à ce que nous avons appelé les « entrées-sorties conversationnelles ». Plus tard, nous verrons que ces mêmes fonctions (moyennant la présence d’un argument supplémentaire) permettent également d’échanger des informations avec des fichiers.

En ce qui concerne la lecture au clavier, nous serons amené à mettre en évidence certaines lacunes de scanf en matière de comportement lors de réponses incorrectes et à vous fournir quelques idées sur la manière d’y remédier.

1 Les possibilités de la fonction printf

Nous avons déjà vu que le premier argument de printf est une chaîne de caractères qui spécifie à la fois :

des caractères à afficher tels quels,

des codes de format repérés par %. Un code de conversion (tel que c, d ou f) y précise le type de l’information à afficher.

D’une manière générale, il existe d’autres caractères de conversion soit pour d’autres types de valeurs, soit pour agir sur la précision de l’information que l’on affiche. De plus, un code de format peut contenir des informations complémentaires agissant sur le cadrage, le gabarit ou la précision. Ici, nous nous limiterons aux possibilités les plus usitées de printf . Nous avons toutefois mentionné le code de conversion relatif aux chaînes (qui ne seront abordées que dans le chapitre 8) et les entiers non signés (chapitre 13). Sachez cependant que le paragraphe 1.2 de l’annexe vous en fournit un panorama complet.

1.1 Les principaux codes de conversion

c

char : caractère affiché « en clair » (convient aussi à short ou à int compte tenu des conversions systématiques)

d

int (convient aussi à char ou à int, compte tenu des conversions systématiques)

u

unsigned int (convient aussi à unsigned char ou à unsigned short, compte tenu des conversions systématiques)

ld

long

lu

unsigned long

f

double ou float (compte tenu des conversions systématiques float -> double...

Chapitre 5

Les instructions de contrôle

A priori, dans un programme, les instructions sont exécutées séquentiellement, c’est-à-dire dans l’ordre où elles apparaissent. Or la puissance et le « comportement intelligent » d’un programme proviennent essentiellement :

de la possibilité d’effectuer des choix, de se comporter différemment suivant les circonstances (celles-ci pouvant être, par exemple, une réponse de l’utilisateur, un résultat de calcul...),

de la possibilité d’effectuer des boucles, autrement dit de répéter plusieurs fois un ensemble donné d’instructions.

Tous les langages disposent d’instructions, nommées instructions de contrôle, permettant de réaliser ces choix ou ces boucles. Suivant le cas, celles-ci peuvent être :

basées essentiellement sur la notion de branchement (conditionnel ou inconditionnel) ; c’était le cas, par exemple, des premiers Basic,

ou, au contraire, traduire fidèlement les structures fondamentales de la programmation structurée ; cela était le cas, par exemple, du langage Pascal bien que, en toute rigueur, ce dernier dispose d’une instruction de branchement inconditionnel GOTO.

Sur ce point, le langage C est quelque peu hybride. En effet d’une part, il dispose d’instructions structurées permettant de réaliser :

des choix : instructions if...else et switch,

des boucles : instructions do...while, while et for.

Mais, d’autre part, la notion de branchement n’en est pas totalement absente puisque, comme nous le verrons :

il dispose d’instructions de branchement inconditionnel : goto, break et continue,

l’instruction switch est en fait intermédiaire entre un choix multiple parfaitement structuré (comme dans Pascal) et un aiguillage multiple (comme dans Fortran).

Ce sont ces différentes instructions de contrôle du langage C que nous nous proposons d’étudier dans ce chapitre.

1 L’instruction if

Nous avons déjà rencontré des exemples d’instruction if et nous avons vu que cette dernière pouvait éventuellement faire intervenir un bloc. Précisons donc tout d’abord ce qu’est un bloc d’une manière générale.

1.1 Blocs d’instructions

Un bloc est une suite d’instructions placées entre { et }. Les instructions figurant dans un bloc sont absolument quelconques. Il peut s’agir aussi bien d’instructions simples (terminées par un point-virgule) que d’instructions structurées (choix, boucles) lesquelles peuvent alors à leur tour renfermer d’autres blocs.

Rappelons qu’en C, la notion d’instruction est en quelque sorte récursive. Dans la description de la syntaxe des différentes instructions, nous serons souvent amené à mentionner ce terme d’instruction. Comme nous l’avons déjà noté, celui-ci désignera toujours n’importe quelle instruction C : simple, structurée ou un bloc.

Un bloc peut se réduire à une seule instruction, voire être vide. Voici deux exemples de blocs corrects :

{ }
{ i = 1 ; }

Le second bloc ne présente aucun intérêt en pratique puisqu’il pourra toujours être remplacé par l’instruction simple qu’il contient.

En revanche, nous verrons que le premier bloc (lequel pourrait a...

Chapitre 6

La programmation modulaire

et les fonctions

Comme tous les langages, C permet de découper un programme en plusieurs parties nommées souvent « modules ». Cette programmation dite modulaire se justifie pour de multiples raisons :

Un programme écrit d’un seul tenant devient difficile à comprendre dès qu’il dépasse une ou deux pages de texte. Une écriture modulaire permet de le scinder en plusieurs parties et de regrouper dans le programme principal les instructions en décrivant les enchaînements. Chacune de ces parties peut d’ailleurs, si nécessaire, être décomposée à son tour en modules plus élémentaires ; ce processus de décomposition pouvant être répété autant de fois que nécessaire, comme le préconisent les méthodes de programmation structurée.

La programmation modulaire permet d’éviter des séquences d’instructions répétitives, et cela d’autant plus que la notion d’argument permet de paramétrer certains modules.

La programmation modulaire permet le partage d’outils communs qu’il suffit d’avoir écrits et mis au point une seule fois. Cet aspect sera d’autant plus marqué que C autorise effectivement la compilation séparée de tels modules.

1 La fonction : la seule sorte de module existant en C

Dans certains langages, on trouve deux sortes de modules, à savoir :

Les fonctions, assez proches de la notion mathématique correspondante. Notamment, une fonction dispose d’arguments (en C, comme dans la plupart des autres langages, une fonction peut ne comporter aucun argument) qui correspondent à des informations qui lui sont transmises et elle fournit un unique résultat scalaire (simple) ; désigné par le nom même de la fonction, ce dernier peut apparaître dans une expression. On dit d’ailleurs que la fonction possède une valeur et qu’un appel de fonction est assimilable à une expression.

Les procédures (terme Pascal) ou sous-programmes (terme Fortran ou Basic) qui élargissent la notion de fonction. La procédure ne possède plus de valeur à proprement parler et son appel ne peut plus apparaître au sein d’une expression. Par contre, elle dispose toujours d’arguments. Parmi ces derniers, certains peuvent, comme pour la fonction, correspondre à des informations qui lui sont transmises. Mais d’autres, contrairement à ce qui se passe pour la fonction, peuvent correspondre à des informations qu’elle produit en retour de son appel. De plus, une procédure peut réaliser une action, par exemple afficher un message (en fait, dans la plupart des langages, la fonction peut quand même réaliser une action, bien que ce ne soit pas là sa vocation).

En C, il n’existe qu’une seule sorte de module, nommé fonction (il en ira de même en C++ et en Java, langage dont la syntaxe est proche de celle de C). Ce terme, quelque peu abusif, pourrait laisser croire que les modules du C sont moins généraux que ceux des autres langages. Or il n’en est rien, bien au contraire ! Certes, la fonction pourra y être utilisée comme dans d’autres langages, c’est-à-dire recevoir des arguments et fournir un résultat scalaire qu’on utilisera dans une expression, comme, par exemple, dans :

y = sqrt(x)+3 ;

Mais, en C, la fonction pourra prendre des aspects différents, pouvant complètement dénaturer l’idée qu’on se fait d’une fonction. Par exemple :

La valeur d’une fonction pourra très bien ne pas être utilisée ; c’est ce qui se passe fréquemment lorsque vous utilisez printf ou scanf. Bien entendu, cela n’a d’intérêt que parce que de telles fonctions réalisent une action (ce qui, dans d’autres langages, serait réservée aux sous-programmes ou procédures).

Une fonction pourra ne fournir aucune valeur.

Une fonction pourra fournir un résultat non scalaire (nous n’en parlerons toutefois que dans le chapitre consacré aux structures).

Une fonction pourra modifier les valeurs de certains de ses arguments (il vous faudra toutefois attendre d’avoir étudié les pointeurs pour voir par quel mécanisme elle y parviendra).

Ainsi, donc, malgré son nom, en C, la fonction pourra jouer un rôle aussi général que la procédure ou le sous-programme des autres langages.

Par ailleurs, nous verrons qu’en C plusieurs fonctions peuvent partager des informations, autrement que par passage d’arguments. Nous retrouverons la notion classique de « variables globales » (en Basic, toutes les variables sont globales, de sorte qu’on ne le dit pas - en Fortran, ces variables globales sont rangées dans des « COMMON »).

Enfin, l’un des atouts du langage C réside dans la possibilité de compilation séparée. Celle-ci permet de découper le programme source en plusieurs parties, chacune de ces parties pouvant comporter une ou plusieurs fonctions. Certains auteurs emploient parfois le mot « module » pour désigner chacune de ces parties (stockées dans un fichier) ; dans ce cas, ce terme de module devient synonyme de fichier source. Cela facilite considérablement le développement et la mise au point de grosses applications. Cette possibilité crée naturellement quelques contraintes supplémentaires, notamment au niveau des variables globales que l’on souhaite partager entre différentes parties du programme source (c’est d’ailleurs ce qui justifiera l’existence de la déclaration extern).

Pour garder une certaine progressivité dans notre exposé, nous supposerons tout d’abord que nous avons affaire à un programme source d’un seul tenant (ce qui ne nécessite donc pas de compilation séparée). Nous présenterons ainsi la structure générale d’une fonction, les notions...

Chapitre 7

Les tableaux et les pointeurs

Comme tous les langages, C permet d’utiliser des tableaux. On nomme ainsi un ensemble d’éléments de même type désignés par un identificateur unique ; chaque élément est repéré par un indice précisant sa position au sein de l’ensemble.

Par ailleurs, le langage C dispose de pointeurs, c’est-à-dire de variables destinées à contenir des adresses d’autres objets (variables, fonctions...).

A priori, ces deux notions de tableaux et de pointeurs peuvent paraître fort éloignées l’une de l’autre. Toutefois, il se trouve qu’en C un lien indirect existe entre ces deux notions, à savoir qu’un identificateur de tableau est une « constante pointeur ». Cela peut se répercuter dans le traitement des tableaux, notamment lorsque ceux-ci sont transmis en argument de l’appel d’une fonction.

C’est ce qui justifie que ces deux notions soient regroupées dans un seul chapitre.

1 Les tableaux à un indice

1.1 Exemple d’utilisation d’un tableau en C

Supposons que nous souhaitions déterminer, à partir de vingt notes d’élèves (fournies en données), combien d’entre elles sont supérieures à la moyenne de la classe.

S’il ne s’agissait que de calculer simplement la moyenne de ces notes, il nous suffirait d’en calculer la somme, en les cumulant dans une variable, au fur et à mesure de leur lecture. Mais, ici, il nous faut à nouveau pouvoir consulter les notes pour déterminer combien d’entre elles sont supérieures à la moyenne ainsi obtenue. Il est donc nécessaire de pouvoir mémoriser ces vingt notes.

Pour ce faire, il paraît peu raisonnable de prévoir vingt variables scalaires différentes (méthode qui, de toute manière, serait difficilement transposable à un nombre important de notes).

Le tableau va nous offrir une solution convenable à ce problème, comme le montre le programme suivant.

Exemple d’utilisation d’un tableau

#include <stdio.h>
main()
{  int i, som, nbm ;
   float moy ;
   int t[20] ;

   for (i=0 ; i<20 ; i++)
      { printf ("donnez la note numéro %d : ", i+1) ;
        scanf ("%d", &t[i]) ;
      }
   for (i=0, som=0 ; i<20 ; i++)  som += t[i] ;
   moy = som / 20 ;
   printf ("\n\n moyenne de la classe : %f\n", moy) ;
   for (i=0, nbm=0 ; i<20 ; i++ )
       if (t[i] > moy) nbm++ ;
   printf ("%d élèves ont plus de cette moyenne", nbm) ;
}

La déclaration :

int t[20]

réserve l’emplacement pour 20 éléments de type int. Chaque élément est repéré par sa position dans le tableau, nommée indice. Conventionnellement, en langage C, la première position porte le numéro 0. Ici, donc, nos indices vont de 0 à 19. Le premier élément du tableau sera désigné par t[0], le troisième par t[2], le dernier par t[19].

Plus généralement, une notation telle que t[i] désigne un élément dont la position dans le tableau est fournie par la valeur de i. Elle joue le même rôle qu’une variable scalaire de type int.

La notation &t[i] désigne l’adresse de cet élément t[i] de même que &n désignait l’adresse de n.

1.2 Quelques règles

a) Les éléments de tableau

Un élément de tableau est une lvalue. Il peut donc apparaître à gauche d’un opérateur d’affectation comme dans :

t[2] = 5

Il peut aussi apparaître comme opérande d’un opérateur d’incrémentation, comme dans :

t[3]++             --t[i]

En revanche, il n’est pas possible, si t1 et t2 sont des tableaux d’entiers, d’écrire t1 = t2 ; en fait, le langage C n’offre aucune possibilité d’affectations globales de tableaux, comme c’était le cas, par exemple, en Pascal.

b) Les indices

Un indice peut prendre la forme de n’importe quelle expression arithmétique de type entier (ou caractère, compte tenu des règles de conversion systématique). Par exemple, si n, p, k et j sont de type int, ces notations sont correctes :

t[n-3]
t[3*p-2*k+j%l]

Il en va de même, si c1 et c2 sont de type char, de :

t[c1+3]
t[c2-c1]

c) La dimension d’un tableau

La dimension d’un tableau (son nombre d’éléments) ne peut être qu’une constante ou une expression constante...

Chapitre 8

Les chaînes de caractères

Certains langages (Java, Basic, anciennement Turbo Pascal) disposent d’un véritable type chaîne. Les variables d’un tel type sont destinées à recevoir des suites de caractères qui peuvent évoluer, à la fois en contenu et en longueur, au fil du déroulement du programme. Elles peuvent être manipulées d’une manière globale, en ce sens qu’une simple affectation permet de transférer le contenu d’une variable de ce type dans une autre variable de même type.

D’autres langages (Fortran, Pascal standard) ne disposent pas d’un tel type chaîne. Pour traiter de telles informations, il est alors nécessaire de travailler sur des tableaux de caractères dont la taille est nécessairement fixe (ce qui impose à la fois une longueur maximale aux chaînes et ce qui, du même coup, entraîne une perte de place mémoire). La manipulation de telles informations est obligatoirement réalisée caractère par caractère et il faut, de plus, prévoir le moyen de connaître la longueur courante de chaque chaîne.

En langage C, il n’existe pas de véritable type chaîne, dans la mesure où l’on ne peut pas y déclarer des variables d’un tel type. En revanche, il existe une convention de représentation des chaînes. Celle-ci est utilisée à la fois :

par le compilateur pour représenter les chaînes constantes (notées entre doubles quotes) ;

par un certain nombre de fonctions qui permettent de réaliser :

les lectures ou écritures de chaînes ;

les traitements classiques tels que concaténation, recopie, comparaison, extraction de sous-chaîne, conversions...

Mais, comme il n’existe pas de variables de type chaîne, il faudra prévoir un emplacement pour accueillir ces informations. Un tableau de caractères pourra faire l’affaire. C’est d’ailleurs ce que nous utiliserons dans ce chapitre. Mais nous verrons plus tard comment créer dynamiquement des emplacements mémoire, lesquels seront alors repérés par des pointeurs.

1 Représentation des chaînes

1.1 La convention adoptée

En C, une chaîne de caractères est représentée par une suite d’octets correspondant à chacun de ses caractères (plus précisément à chacun de leurs codes), le tout étant terminé par un octet supplémentaire de code nul. Cela signifie que, d’une manière générale, une chaîne de n caractères occupe en mémoire un emplacement de n+1 octets.

1.2 Cas des chaînes constantes

C’est cette convention qu’utilise le compilateur pour représenter les « constantes chaîne » (sous-entendu que vous les introduisez dans vos programmes), sous des notations de la forme :

"bonjour"

De plus, une telle notation sera traduite par le compilateur en un pointeur (sur des éléments de type char) sur la zone mémoire correspondante.

Voici un programme illustrant ces deux particularités :

Convention de représentation des chaînes

#include <stdio.h>
main()
{  char * adr ;
   adr = "bonjour" ;
   while (*adr)
    { printf ("%c", *adr) ;
      adr++ ;
    }
}

 bonjour

La déclaration :

char * adr ;

réserve simplement l’emplacement pour un pointeur sur un caractère (ou une suite de caractères). En ce qui concerne la constante :

"bonjour"

le compilateur a créé...

Chapitre 9

Les structures

et les énumérations

Nous avons déjà vu comment le tableau permettait de désigner sous un seul nom un ensemble de valeurs de même type, chacune d’entre elles étant repérée par un indice.

La structure, quant à elle, va nous permettre de désigner sous un seul nom un ensemble de valeurs pouvant être de types différents. L’accès à chaque élément de la structure (nommé champ) se fera, cette fois, non plus par une indication de position, mais par son nom au sein de la structure.

Quant au type énumération, il s’agit d’un cas particulier de type entier. Sa présentation (tardive) dans ce chapitre ne se justifie que parce que sa déclaration et son utilisation sont très proches de celles du type structure.

1 Déclaration d’une structure

Voyez tout d’abord cette déclaration :

struct enreg
        {  int numero ;
           int qte ;
           float prix ;
        } ;

Celle-ci définit un modèle de structure mais ne réserve pas de variables correspondant à cette structure. Ce modèle s’appelle ici enreg et il précise le nom et le type de chacun des champs constituant la structure (numero, qte et prix).

Une fois un tel modèle défini, nous pouvons déclarer des variables du type correspondant (souvent, nous parlerons de structure pour désigner une variable dont le type est un modèle de structure).

Par exemple :

struct enreg art1 ;

réserve un emplacement nommé art1 « de type enreg » destiné à contenir deux entiers et un flottant.

De manière semblable :

struct enreg art1, art2 ;

réserve deux emplacements art1 et art2 du type enreg.

Remarque

Bien que ce soit peu recommandé, sachez qu’il est possible de regrouper la définition du modèle de structure et la déclaration du type des variables dans une seule instruction comme dans cet exemple :

struct enreg
        {  int numero ;
           int qte ;
           float prix ;
        } art1, art2 ;

Dans ce dernier cas, il est même possible d’omettre le nom de modèle (enreg), à condition, bien sûr, que l’on n’ait pas à déclarer par la suite d’autres variables de ce type.

2 Utilisation d’une structure

En C, on peut utiliser une structure de deux manières :

en travaillant individuellement sur chacun de ses champs ;

en travaillant de manière...

Chapitre 11

La gestion dynamique

de la mémoire

Nous avons déjà eu l’occasion de faire la distinction entre les données statiques (variables globales ou locales statiques) et les données automatiques (variables locales). D’autre part, nous avons évoqué les possibilités d’allocation dynamique d’espace mémoire.

Cela signifie qu’en langage C un programme comporte en définitive trois types de données :

statiques ;

automatiques ;

dynamiques.

Les données statiques occupent un emplacement parfaitement défini lors de la compilation.

Les données automatiques, en revanche, n’ont pas une taille définie a priori. En effet, elles ne sont créées et détruites qu’au fur et à mesure de l’exécution du programme. Elles sont souvent gérées sous forme de ce que l’on nomme une pile (stack en anglais), laquelle croît ou décroît suivant les besoins du programme. Plus précisément, elle croît à chaque entrée dans une fonction pour faire place à toutes les variables locales nécessaires pendant la durée de vie de la fonction ; elle décroît d’autant à chaque sortie.

Les données dynamiques n’ont pas non plus de taille définie a priori. Leur création ou leur libération dépend, cette fois, de demandes explicites faites lors de l’exécution du programme. Leur gestion, qui ne saurait se faire à la manière d’une pile, est indépendante de celle des données automatiques. Plus précisément, elle se fait généralement dans ce que l’on nomme un tas (heap en anglais) dans lequel on cherche à allouer ou à libérer de l’espace en fonction des besoins.

...

Index

Symbols

! (opérateur) 35

!= (opérateur) 33

- (opérateur) 27

-- (opérateur) 39

#define 9, 23, 205, 206

#elif 213

#else 211

#endif 211

#ifdef 211

#ifndef 211

#include 9, 205

% (opérateur) 27

%= (opérateur) 42

& (opérateur) 127

&& (opérateur) 35

* (opérateur) 27, 127

*= (opérateur) 42

+ (opérateur) 27

++ (opérateur) 39

+= (opérateur) 42

/ (opérateur) 27

/= (opérateur) 42

< (opérateur) 33

<= (opérateur) 33

-= (opérateur) 42

== (opérateur) 33

-> (opérateur) 176

> (opérateur) 33

>= (opérateur) 33

|| (opérateur) 35

A

abs (math.h) 242

accès

direct 181, 185

séquentiel 181

acos (math.h) 239

affectation

conversions forcées 42

de pointeurs ...