Cours Introduction au langage C

1 Introduction au langage C

Bernard Cassagne

Laboratoire de Genie Informatique

IMAG

Grenoble

novembre 1991

 

2 Avant-propos

Au sujet de ce manuel

Le but de ce manuel est de permettre a une personne sachant programmer, d’acquerir les elements fondamentaux du langage C. Ce manuel presente donc chaque notion selon une gradation des di cultes et ne cherche pas a ^etre exhaustif. Il comporte de nombreux exemples, ainsi que des exercices dont la solution se trouve dans le corps du texte, mais commence toujours sur une page di erente. Le lecteur peut donc au choix, ne lire les solutions qu’apres avoir programme sa solution personnelle, ou bien lire directement la solution comme si elle faisait partie du manuel.

Les notions supposees connues du lecteur

Les notions dont la connaissance est necessaire a la comprehension de ce manuel se trouvent ci-dessous, chaque terme etant accompagne d’une courte definition.

identificateur Sert a nommer les objets du langage.

effet de bord Modification de l’etat de la machine. Un effet de bord peut etre^ interne au programme (par exemple, modification de la valeur d’une variable) ou externe au programme (par exemple ecriture dans un chier). Toute partie de programme qui n’est pas declarative a pour but soit de calculer une valeur, soit de faire un effet de bord.

procedure Permet d’associer un nom a un traitement algorithmique. La procedure est la brique de base de la construction de programme. Le but d’une procedure est de realiser au moins un effet de bord (sinon elle ne sert a rien).

fonction Possede en commun avec la procedure d’associer un nom a un traitement algorithmique, mais la caracteristique d’une fonction est de delivrer une valeur utilisable dans une expression. Les langages de programmation permettent generalement aux fonctions, en plus du fait de retourner une valeur, de faire un ou plusieurs effets de bord.

Conventions syntaxiques

Les regles de grammaires suivront les conventions suivantes:

  1. les elements terminaux du langage seront ecrits dans une fonte a largeur constante, comme ceci: while.
  2. les elements non terminaux du langage seront ecrits en italique, comme ceci: in-struction.
  3. les regles de grammaires seront ecrites de la maniere suivante:

les parties gauches de regles seront seules sur leur ligne, cadrees a gauche et suivies du signe deux points (:).

les différentes parties droites possibles seront introduites par le signe ) et indentees sur la droite.

ex:

instruction :

) if ( expression

) if ( expression

) instruction1

) instruction1 else

instruction2

Ceci signi e qu’il y a deux manieres possibles de deriver le non-terminal in-struction. La premiere regle indique qu’on peut le deriver en

if ( expression ) instruction1

la deuxieme regle indique qu’on peut aussi le deriver en

if ( expression ) instruction1 else instruction2

une partie droite de regle pourra etre^ ecrite sur plusieurs lignes. Ceci permettra de re eter une maniere possible de mettre en page le fragment de programme C correspondant, de facon a obtenir une bonne lisibilite.

Sans en changer la signi cation, l’exemple precedent aurait pu ^etre ecrit:

instruction :

) if ( expression ) instruction1

) if ( expression ) instruction1

else instruction2

les elements optionnels d’une regle seront indiques en mettant le mot \option” (en italique et dans une fonte plus petite) a droite de l’element concerne.

Par exemple, la regle:

declareur-init :

) declareur initialiseur option

indique que declarateur-init peut se deriver soit en:

declareur initialiseur

soit en:

declareur

Chapter 1

Les bases

1.1 Le compilateur

Les compilateurs C font subir deux transformations aux programmes:

  1. un preprocesseur fait subir au texte des transformations d’ordre purement lexical,
  2. le compilateur proprement dit prend le texte genere par le preprocesseur et le traduit en instructions machine.

Le but du preprocesseur est de rendre des services du type traitement de macros et compila-tion conditionnelle. Initialement, il s’agissait de deux outils di erents, mais les possibilites du preprocesseur ont ete utilisees de maniere tellement extensive par les programmeurs, qu’on en est venu a considerer le preprocesseur comme partie integrante du compilateur. C’est cette derniere philosophie qui est retenue par l’ANSI dans sa proposition de norme pour le langage C.

1.2 Les types de base

1.2.1 les caracteres

Le mot cle designant les caracteres est char. char est le type dont l’ensemble des valeurs est l’ensemble des valeurs entieres formant le code des caracteres utilise sur la machine cible. Le type char peut donc ^etre considere comme un sous-ensemble du type entier.

Le code des caracteres utilise n’est pas de ni par le langage: c’est un choix d’implementation.

Cependant, dans la grande majorite des cas, le code utilise est le code dit ASCII.

1.2.2 Les entiers

Le mot cle designant les entiers est int.

Les entiers peuvent ^etre a ectes de deux types d’attributs : un attribut de precision et un attribut de representation.

Les attributs de precision sont short et long. Du point de vue de la precision, on peut donc avoir trois types d’entiers : les short int, les int et les long int. Les int sont implementes sur ce qui est un mot \naturel ” de la machine. Les long int sont implementes si possible plus grands que les int, sinon comme des int. Les short in sont implementes plus courts que les int, sinon comme des int. Les implementations classiques mettent les short int sur 16 bits, les long int sur 32 bits, et les int sur 16 ou 32 bits selon ce qui est le plus e cace.

L’attribut de representation est unsigned. Du point de vue de la representation, on peut donc avoir deux types d’entiers : les int et les unsigned int. Les int permettent de contenir des entiers signes, tres generalement en representation en complement a 2, bien que cela ne soit pas impose par le langage. Les unsigned int permettent de contenir des entiers non signes en representation binaire.

On peut combiner attribut de precision et attribut de representation et avoir par exemple un unsigned long int. En resume, on dispose donc de six types d’entiers: les int, les short int, les long int (tous trois signes) et les unsigned int, les unsigned short int et les unsigned long int.

1.2.3 Les ottants

Les ottants peuvent ^etre en simple ou en double precision. Le mot cle designant les ottants simple precision est float, et celui designant les ottants double precision est double. La precision e ectivement utilisee pour ces deux types depend de l’implementation.

1.3 Les constantes

1.3.1 Les constantes entieres

On dispose de 3 notations pour les constantes entieres: decimale, octale et hexadecimale.

Les contantes decimales s’ecrivent de la maniere usuelle (ex: 372). Les constantes octales doivent commencer par un zero (ex: 0477). Les constantes hexadecimales doivent commencer par 0x ou 0X et on peut utiliser les lettres majuscules aussi bien que les minuscules pour les chi res hexadecimaux.

ex:

0x5a2b

0X5a2b

0x5A2B

Si une constante entiere peut tenir sur un int, elle a le type int, sinon elle a le type long int.

On dispose d’une convention permettant d’indiquer au compilateur qu’une constante entiere doit avoir le type long int. Cette convention consiste a faire suivre la con-stante de la lettre l (en majuscule ou en minuscule). Les notations decimales, octales et hexadecimales peuvent ^etre utilisees avec cette convention.

ex :

0L

0456l

0x7affL

attention

Ces conventions ne respectent pas les conventions habituelles, puisque 010 devant ^etre interprete en octal, n’est pas egal a 10.

1.4. LES CHA^NES DE CARACTERES LITTERALES

1.3.2 Les constantes caracteres

La valeur d’une constante caractere est la valeur numerique du caractere dans le code de la machine.

Si le caractere dispose d’une representation imprimable, une constante caractere s’ecrit entouree du signe ‘. ex: ‘g’

En ce qui concerne les caracteres ne disposant pas de representation imprimable, C autorise une notation a l’aide d’un caractere d’escape qui est \ :

Certains d’entres eux, utilises tres frequemment, disposent d’une notation partic-uliere. Il s’agit des caracteres suivant:

new line

horizontal tabulation

back space

carriage return

form feed

back slash

single quote

‘\n’

‘\t’

‘\b’

‘\r’

‘\f’

‘\\’

‘\{”}

les autres disposent de la notation \x ou x est un nombre exprime en octal. ex:

‘\001’ SOH

‘\002’ STX

‘\003’ ETX

etc…

1.3.3 Les constantes ottantes

La notation utilisee est la notation classique par mantisse et exposant. Pour introduire l’exposant, on peut utiliser la lettre e sous forme minuscule ou majuscule. ex:

notation C notation mathematique

2.

2

.3

0:3

2.3

2:3

2e4

2 104

2.e4

2 104

.3e4

0:3 104

2.3e4

2:3 104

2.3e-4

2:3 10 4

Toute constante ottante est consideree comme etant de type ottant double precision.

1.4 Les cha^nes de caracteres litterales

Une cha^ne de caracteres litterale est une suite de caracteres entoures du signe “. ex:

“ceci est une chaine”

Toutes les notations de caracteres non imprimables de nies en 1.3.2 sont utilisables dans les cha^nes. ex:

“si on m’imprime,\nje serai sur\ntrois lignes a cause des deux new line”

Le caractere \ suivi d’un passage a la ligne suivante est ignore. Cela permet de faire tenir les longues cha^nes sur plusieurs lignes de source.

ex:

“ceci est une tres tres longue chaine que l’on fait tenir \ sur deux lignes de source”

Le compilateur rajoute a la n de chaque cha^ne un caractere mis a zero. (Le caractere dont la valeur est zero est appelle null dans le code ASCII). Cette convention de n de cha^ne est utilisee par les fonctions de la librairie standard.

1.5 constantes nommees

Il n’y a pas en C de possibilite de donner un nom a une constante. On peut cependant

realiser un e et equivalent gr^ace au preprocesseur. Lorsque le preprocesseur lit une ligne du type:

#define identi cateur reste-de-la-ligne

il remplace dans toute la suite du source, toute nouvelle occurence de identi cateur par reste-de-la-ligne.

Par exemple on peut ecrire:

#define pi 3.14159

et dans la suite du programme on pourra utiliser le nom pi pour designer la constante 3.14159.

1.6 Declarations de variables ayant un type de base

Une telle declaration se fait en faisant suivre le nom du type, par la liste des noms des variables.

ex :

int i;

int i,j;

short int k;

float f;

double d1,d2;

Il est possible de donner une valeur initiale aux variables ainsi declarees.

ex:

int i = 54;

int i = 34,j = 12;

1.7. LES OPERATEURS LES PLUS USUELS

1.7.1 l’a ectation

En C, l’a ectation est un operateur et non pas une instruction.

Syntaxe:

expression :

) lvalue = expression

Dans le jargon C, une lvalue est une expression qui doit delivrer une variable (par opposition a une constante). Une lvalue peut ^etre par exemple une variable, un

element de tableau, mais pas une constante. Cette notion permet d’exprimer dans la grammaire l’impossibilite d’ecrire des choses du genre 1 = i qui n’ont pas de sens.

Exemples d’a ectation:

i = 3

f = 3.4

i = j + 1

Semantique:

L’operateur a deux e ets:

  1. un e et de bord consistant a a ecter la valeur de expression a la variable designee par la lvalue
  2. l’operateur delivre la valeur ainsi a ectee, valeur qui pourra donc ^etre utilisee dans une expression englobant l’a ectation.

ex :

i = (j = k) + 1

La valeur de k est a ectee a j et cette valeur est le resultat de l’expression (j = k), on y ajoute 1 et le resultat est a ecte a i.

Restrictions:

La lvalue doit designer une variable ayant un type de base. Il n’est donc pas possible d’a ecter un tableau a un tableau, par exemple.

Conversions de type:

Lorsque la valeur de l’expression est a ectee a la lvalue, la valeur est eventuellement convertie dans le type de la lvalue. On peut par exemple a ecter a un ottant, la valeur d’une expression entiere.

1.7.2 L’addition

Syntaxe: expression :

) expression1 + expression2

Semantique:

Les deux expressions sont evaluees , l’addition realisee, et la valeur obtenue est la valeur de l’expression d’addition.

L’ordre dans lequel les deux expressions sont evaluees, n’est pas determine. Si ex-pression1 et expression2 font des e ets de bords, on n’est pas assure de l’ordre dans lequel ils se feront.

Apres evaluation des expressions, il peut y avoir conversion de type de l’un des operandes, de maniere a permettre l’addition. On pourra par exemple faire la somme d’une expression delivrant un reel et d’une expression delivrant un entier.

1.7.3 La soustraction

Syntaxe:

L’operateur peut etre^ utilise de maniere unaire ou binaire:

expression :

  • – expression

) expression1 – expression2

Semantique:

Les deux expressions sont evaluees, la soustraction realisee, et la valeur obtenue est la valeur de l’expression soustraction. (La semantique de – expression est celle de 0

  • expression).

Les m^emes remarques concernant l’ordre d’evaluation des operandes ainsi que les eventuelles conversions de type faites au sujet de l’addition s’appliquent a la sous-traction.

1.7.4 La multiplication

Syntaxe: expression :

) expression1 * expression2

Semantique:

Les deux expressions sont evaluees, la multiplication realisee, et la valeur obtenue est la valeur de l’expression multiplicative.

Les m^emes remarques concernant l’ordre d’evaluation des operandes ainsi que les eventuelles conversions de type faites au sujet de l’addition s’appliquent a la multi-plication.

1.7.5 La division

Syntaxe: expression :

) expression1 / expression2

Semantique:

Contrairement a d’autres langages, le langage C ne dispose que d’une seule notation

pour designer deux operateurs di erents: le signe / designe a la fois la division entiere et la division reelle.

Si expression1 et expression2 delivrent deux valeurs entieres, alors il s’agit d’une division entiere. Si l’une des deux expressions au moins delivre une valeur reelle, il s’agit d’une division reelle.

Dans le cas de la division entiere, si les deux operandes sont positifs, l’arrondi se fait vers zero, mais si au moins un des deux operandes est negatif, la facon dont se

fait l’arrondi depend de l’implementation (mais il est generalement fait zero). ex:

13 / 2 delivre la valeur 6 et -13 / 2 ou 13 / -2 peuvent delivrer -6 ou -7 mais le resultat sera generalement -6.

Dans le cas de la division reelle, les remarques concernant l’ordre d’evaluation des operandes ainsi que les eventuelles conversions de type faites au sujet de l’addition

s’appliquent egalement.

1.7.6 L’operateur modulo

Syntaxe: expression :

) expression1 % expression2 Semantique:

Les deux expressions sont evaluees et doivent delivrer une valeur de on evalue le reste de la division entiere de expression1 par expression2 obtenue est la valeur de l’expression modulo.

type entier, et la valeur

Si au moins un des deux operandes est negatif, le signe du reste depend de l’implementation, mais il est generalement pris du m^eme signe que le dividende. ex : 13 % 2 delivre 1 -13 % 2 delivre generalement -1

  1. % -2 delivre generalement 1

Les choix d’implementation faits pour les operateurs division entiere et modulo doivent etre^ coherents, en ce sens que l’expression b * (a / b) + a % b doit avoir pour valeur a.

1.7.7 Les operateurs de comparaison

Syntaxe:

expression :

) expression1 operateur expression2 ou operateur peut ^etre l’un des symboles suivants:

operateur semantique

  • strictement superieur
    • strictement inferieur

>=

superieur ou egal

<=

inferieur ou egal

==

egal

!=

di erent

Semantique:

Les deux expressions sont evaluees puis comparees, la valeur rendue est de type int et vaut 1 si la condition est vraie, et 0 sinon.

On remarquera que le type du resultat est int, car le type booleen n’existe pas.

1.8 Les instructions les plus usuelles

1.8.1 Instruction expression

Syntaxe: instruction :

      • expression ;

Semantique:

L’expression est evaluee, et sa valeur est ignoree. Ceci n’a donc de sens que si

l’expression realise un e et de bord. Dans la majorite des cas, il s’agira d’une expression d’a ectation.

ex:

i = j + 1;

Remarque

D’apres la syntaxe, on voit qu’il est parfaitement valide d’ecrire l’instruction

i + 1;

mais ceci ne faisant aucun e et de bord, cette instruction n’a aucune utilite.

1.8.2 Instruction composee

Syntaxe: instruction :

  • {

liste-de-declarationsoption

liste-d’instructions

}

Semantique:

le but de l’instruction composee est double, elle permet:

  1. de grouper un ensemble d’instructions en lui donnant la forme syntaxique d’une seule instruction.
  2. de declarer des variables qui ne seront accessible qu’a l’interieur de l’instruction composee (structure classique de ‘blocs’).

Remarques sur la syntaxe

  1. il n’y a pas de separateur dans liste-d’instructions. Les points virgules sont des terminateurs pour les instructions qui sont des expressions.
  2. Les accollades jouent le r^ole des mots cles begin et end que l’on trouve dans certains langages (Pascal, PL/1, etc..)

1.8.3 instruction if

Syntaxe: instruction :

) if ( expression ) if ( expression

) instruction1

) instruction1 else

instruction2

Semantique:

expression est evaluee, si la valeur rendue est non nulle, on execute instruction1, sinon on execute instruction2 si elle existe.

Remarques sur la syntaxe

  1. Attention au fait que expression doit etre^ parenthesee.
  2. La partie then de l’instruction n’est pas introduite par un mot cle: pas de then comme dans certains langages.
  3. Lorsqu’il y a une possible ambiguite sur l’instruction if dont depend une partie else, l’ambiguite est levee en faisant dependre le else de l’instruction if la plus proche.

Par exemple, dans le cas de :

if (a > b) if (c < d) u = v; else i = j;

le else sera celui du if (c < d). Si on voulait qu’il en soit autrement, il faudrait ecrire:

if (a > b)

{

if (c < d) u = v;

}

else i = j;

Remarques sur la semantique

Etant donne que l’instruction if teste l’egalite a zero de expression, celle-ci n’est pas

necessairement une expression de comparaison. Toute expression delivrant une valeur pouvant ^etre comparee a zero est valide.

Exemples d’instructions if

ex:

if (a > b) max = a; else max = b;

if (x > y)

{

/*

liste d’instructions

*/

}

else

{

/*

liste d’instructions

*/

}

if (a)

/*

equivalent a if (a != 0)

*/

{

}

 

Chapter 2

Fonctions et procedures

2.1 De nition d’une fonction

Syntaxe:

de nition-de-fonction :

) type identi cateur ( liste-d’identi cateursoption ) liste-de-declarations1 option

{

liste-de-declarations2 option liste-d’instructions

}

Semantique:

type est le type de la valeur rendue par la fonction, identi cateur est le nom de la fonction, liste-d’identi cateurs est la liste des noms des parametres formels, et la

liste-de-declarations1 est la liste des declarations des parametres formels permettant d’indiquer le type de ces parametres. La liste-de-declarations2 permet si besoin est, de declarer des variables qui seront locales a la fonction, elles seront donc inaccessi-bles de l’exterieur. La liste-d’instructions est l’ensemble des instructions qui seront executees sur appel de la fonction. Parmi ces instructions, il doit y avoir au moins une instruction du type:

return expression ;

Lors de l’execution d’une telle instruction, expression est evaluee, et le contr^ole d’execution est rendu a l’appelant de la fonction. La valeur rendue par la fonction est celle de expression.

ex:

int sum_square(i,j)

/*

la fonction

sum_square delivre un int

*/

int i,j;

/*

declaration

des

parametres formels

*/

{

         

int resultat;

/*

declaration

des

variables locales

*/

15

 

resultat = i*i + j*j;

   

return(resultat);

/*

retour a l’appelant en delivrant resultat */

}

   

L’instruction return est une instruction comme une autre, il est donc possible d’en utiliser autant qu’on le desire dans le corps d’une fonction.

ex:

int max(i,j)

/*

la fonction

max

delivre un int

*/

int i,j;

/*

declaration

des

parametres formels

*/

{

/*

pas de variables locales pour max

*/

if (i > j) return(i); else return(j);

}

Dans le cas ou la derniere instruction executee par une fonction n’est pas une instruc-tion return, la valeur rendue par la fonction est indeterminee.

La liste des parametres formels est optionnelle de facon a permettre l’ecriture de fonc-tions sans parametres.

ex:

double pi()

{

/*

/*

pas de parametres formels pas de variables locales

*/

*/

return(3.14159);

}

D’autre part, la liste-de-declarations1 est optionnelle, elle est donc omise quand il n’y a pas de parametre formel, mais on peut quand m^eme l’omettre quand il y a des parametres formels ! Dans ce cas, le type des parametres formels est pris par defaut comme etant int. Par exemple, la fonction max aurait pu ^etre programmee de la facon suivante:

int max(i,j)

{

/*

/*

la fonction max delivre un int pas de declaration pour i et j

*/

*/

if (i > j) return(i); else return(j);

}

On peut considerer que pro ter de cette possibilite est un mauvais style de program-mation.

2.2 Appel d’une fonction

Syntaxe: expression :

) identi cateur ( liste-d’expressions )

Semantique:

Les expressions de liste-d’expressions sont evaluees, puis passees en tant que parametres e ectifs a la fonction de nom identi cateur, qui est ensuite activee. La valeur rendue par la fonction est la valeur de l’expression appel de fonction

ex:

     

{

     

int a,b,c;

     

double d;

     

d = sum_square(a,b) / 2;

/*

appel de sum_square

*/

c = max(a,b);

/*

appel de max

*/

}

     

2.3 Les procedures

Le langage C ne comporte pas a strictement parler le concept de procedure. Cependant,

les fonctions pouvant realiser sans aucune restriction tout e et de bord qu’elles desirent,

le programmeur peut realiser une procedure a l’aide d’une fonction qui ne rendra aucune valeur. Pour exprimer l’idee de \aucune valeur”, il existe un type special du langage qui porte le nom de void. Une procedure sera donc implementee sous la forme d’une fonc-tion retournant void et dont la partie liste-d’instructions ne comportera pas d’instruction return.

Lors de l’appel de la procedure, il faudra ignorer la valeur rendue c’est a dire ne pas l’englober dans une expression.

void print_add(i,j)

/*

la procedure et ses parametres formels

*/

int i,j;

/*

declaration des parametres formels

*/

{

     

int r;

/*

une variable locale a print_add

*/

r = i + j;

     

/*

instruction pour imprimer la valeur de r

*/

}

     

void prints()

/*

une procedure sans parametres

*/

{

     

int a,b;

/*

variables locales a prints

*/

a = 12; b = 45;

     

print_add(a,b);

/*

appel de print_add

*/

print_add(13,67);

/*

un autre appel a print_add

*/

}

     

Probleme de vocabulaire

     

Dans la suite du texte, nous utiliserons le terme de fonction pour designer indi eremment

une procedure ou une fonction, chaque fois qu’il ne sera pas necessaire de faire la distinction entre les deux.

2.4 Omission du type retourne par une fonction

Nous avons vu que la syntaxe de de nition d’une fonction est:

de nition-de-fonction :

) type identi cateur ( liste-d’identi cateursoption )

liste-de-declarations1 option

{

liste-de-declarations2 option

liste-d’instructions

}

En realite type est optionnel. Si le programmeur omet d’indiquer quel est le type de la valeur rendue par la fonction, le compilateur prend par defaut le type int.

  1. si le programmeur veut ecrire une fonction rendant un int, pro ter du fait que le compilateur prendra la valeur int par defaut pour omettre type, peut ^etre considere comme etant un mauvais style de programmation.
  2. si le programmeur veut ecrire une procedure, omettre type plut^ot que d’indiquer void est a la rigueur acceptable. On doit d’ailleur remarquer que le type void n’existait pas dans la premiere de nition du langage, et qu’il existe donc des masses de code ou les procedures sont codees de cette facon.

On peut donc a la rigueur ecrire les procedures de la maniere suivante:

print_add(i,j)

/*

pas d’indication du type de valeur rendue */

int i,j;

{

int r;

r = i + j;

}

prints()

/*

pas d’indication du type de valeur rendue */

{

int a,b;

a = 12; b = 45;

print_add(a,b);

print_add(13,67);

}

2.5 Impression formattee

Il existe une procedure standard permettant de realiser des sorties formattees: il s’agit de la procedure printf. On l’appelle de la maniere suivante:

printf ( chaine-de-caracteres , liste-d’expressions ) ; chaine-de-caracteres est le texte a imprimer dans lequel on peut librement mettre des

sequences d’echappement qui indiquent le format selon lequel on veut imprimer la valeur des expressions se trouvant dans liste-d’expressions. Ces sequences d’echappement sont composee du caractere % suivi d’un caractere qui indique le format d’impression. Il existe entre autres, %c pour un caractere, %d pour un entier a imprimer en decimal, %x pour un entier a imprimer en hexadecimal.

ex:

int i = 12;

int j = 32;

printf(“la valeur de i est %d et celle de j est %d”,i,j); imprimera:

la valeur de i est 12 et celle de j est 32

Il est possible d’avoir une liste-d’expressions vide, dans ce cas l’appel a printf devient:

printf ( chaine-de-caracteres ) ;

par exemple, pour imprimer le mot erreur en le soulignant, on peut ecrire:

printf(“e\b_r\b_r\b_e\b_u\b_r\b_”); /* \b est back-space */

2.6 Structure d’un programme

Nous ne considererons pour debuter que le cas de programmes formes d’un seul module source.

Un programme C est une suite de declarations de variables et de de nitions de fonctions dans un ordre quelconque.

Les variables sont des variables dont la duree de vie est egale a celle du programme, et qui sont accessibles par toutes les fonctions. Ces variables sont dites variables globales, par opposition aux variables declarees a l’interieur des fonctions qui sont dites locales.

Bien que ce ne soit pas obligatoire, il est generalement considere comme etant un bon style de programmation de regrouper toutes les declarations de variables en t^ete du programme.

La structure d’un programme devient alors la suivante:

programme :

  • liste-de-declarationsoption

liste-de-de nitions-de-fonctions

Il peut n’y avoir aucune declaration de variable, mais il doit y avoir au moins la de nition d’une procedure dont le nom soit main, car pour lancer un programme C, le systeme

appelle la procedure de nom main.

ex:

int i,j;

/*

i,j,a,b,c

sont des variables globales

*/

double a,b,c;

         

void p1(k)

/*

debut de la definition

de la procedure p1

*/

int k;

/*

p1 a un seul parametre

entier : k

*/

{

         

20

         
 

CHAPTER 2. FONCTIONS ET PROCEDURES

int s,t;

/*

variables

locales a p1

*/

/*

instructions de p1 qui peuvent acceder a

*/

/*

k,i,j,a,b,c,s et t

*/

}

/*

fin de la

definition de p1

*/

int f1(x,y)

/*

debut de la definition de la fonction f1

*/

double x,y;

/*

f1 a deux

parametres reeels: x et y

*/

{

         

double u,v;

/*

variables

locales a f1

*/

/*

instructions de

f1 qui peuvent acceder a

*/

/*

x,y,i,j,a,b,c,u

et v

*/

}

/*

fin de la

definition de f1

*/

void main()

/*

debut du main, il n’a pas de parametre

*/

{

         

/*

instructions du main qui appellerons p1 et f1

*/

}

/*

fin de la

definition de main

*/

2.7 Mise en uvre du compilateur C sous UNIX

Le lecteur sera suppose maitriser un editeur de textes lui permettant de creer un chier contenant le source d’un programme. Supposons que le nom d’un tel chier soit essai1.c, pour le compiler sous UNIX il faut emettre la commande:

cc -o essai1 essai1.c

le binaire executable se trouve alors dans le chier essai1. Pour l’executer, il su t

d’emettre la commande:

essai1

Pour veri er qu’il n’y a pas de probleme pour la mise en uvre du compilateur, on peut essayer sur un des plus petits programmes possibles, a savoir un programme sans

variables globales, et n’ayant qu’une seule procedure qui sera la procedure main.

ex:

void main()

{

printf(“ca marche!!\n”);

}

2.8 Exercice

Ecrire un programme comportant:

1. la declaration de 3 variables globales entieres heures, minutes, secondes.

  1. une procedure print_heure qui imprimera le message:

Il est … heure(s) … minute(s) … seconde(s) en respectant l’orthographe du singulier et du pluriel.

  1. une procedure set_heure qui admettra trois parametres de type entiers h, m, s, dont elle a ectera les valeurs respectivement a heures, minutes et secondes.
  2. une procedure tick qui incrementera l’heure de une seconde.
  3. la procedure main sera un jeu d’essai des procedures precedentes.

Une solution possible est donnee ci-apres.

 

int heures,minutes,secondes;

/*****************************************************************************/

/*

 

*/

/*

print_heure

*/

/*

 

*/

/*

But:

*/

/*

Imprime l’heure

*/

/*

 

*/

/*

Interface:

*/

/*

Utilise les variables globales heures, minutes, secondes

*/

/*

 

*/

/*****************************************************************************/

void print_heure()

{

printf(“Il est %d heure”,heures);

if (heures > 1) printf(“s”);

printf(” %d minute”,minutes);

if (minutes > 1) printf(“s”);

printf(” %d seconde”,secondes);

if (secondes > 1) printf(“s”);

printf(“\n”);

}

/*****************************************************************************/

/*

 

*/

/*

set_heure

*/

/*

 

*/

/*

But:

*/

/*

Met l’heure a une certaine valeur

*/

/*

 

*/

/*

Interface:

*/

/*

h, m, s sont les valeurs a donner a heures, minutes, secondes

*/

/*

 

*/

/*****************************************************************************/ void set_heure(h,m,s)

int h,m,s;

{

heures = h; minutes = m; secondes = s;

}

/*****************************************************************************/

/*

 

*/

/*

tick

*/

/*

 

*/

/*

But:

*/

/*

Incremente l’heure de une seconde

*/

/*

 

*/

/*

Interface:

*/

/*

Utilise les variables globales heures, minutes, secondes

*/

/*

 

*/

/*****************************************************************************/ void tick()

{

secondes = secondes + 1;

if (secondes >= 60)

{

secondes = 0;

minutes = minutes + 1;

if (minutes >= 60)

{

minutes = 0;

heures = heures + 1;

if (heures >= 24) heures = 0;

}

}

}

/*****************************************************************************/

/* */ /* main */ /* */

/*****************************************************************************/ void main()

{

set_heure(3,32,10);

tick();

print_heure();

set_heure(1,32,59);

tick();

print_heure();

set_heure(3,59,59);

tick();

print_heure();

set_heure(23,59,59);

tick();

print_heure();

}

 

Chapter 3

Les tableaux

Dans ce chapitre nous allons voir tout d’abord comment declarer un tableau, comment

l’initialiser, comment faire reference a un des ses elements. Du point de vue algorithmique,

quand on utilise des tableaux, on a besoin d’intructions iteratives, nous passerons donc en revue l’ensemble des instructions iteratives du langage. Nous terminerons par un certains nombre d’operateurs tres utiles pour mettre en uvre ces instructions.

3.1 Les tableaux

3.1.1 Declaration de tableaux dont les elements ont un type de base

Une declaration de tableau dont les elements ont un type de base, a une structure tres proche d’une declaration de variable ayant un type de base. La seule di erence consiste a indiquer entre crochets le nombre d’elements du tableau apres le nom de la variable.

ex:

int t[10];

/*

t tableau de 10 int

*/

long int t1[10], t2[20];

/*

t1

tableau

de

10

long int,

 
   

t2

tableau

de

20

long int

*/

En pratique, il est recommande de toujours donner un nom a la constante qui indique le nombre d’elements d’un tableau.

ex:

#define N 100

int t[N];

Les points importants sont les suivants:

les index des elements d’un tableau vont de 0 a N – 1.

la taille d’un tableau doit ^etre connue statiquement par le compilateur. Impossible donc d’ecrire:

int t[n];

ou n serait une variable.

3.1.2 Initialisation d’un tableau

Lorsqu’un tableau est externe a toute fonction, il est possible de l’initialiser avec une liste d’expressions constantes separees par des virgules, et entouree des signes { et }.

ex:

#define N 5

int t[N] = {1, 2, 3, 4, 5};

Il est possible de donner moins d’expressions constantes que le tableau ne comporte

d’elements. Dans ce cas, les premiers elements du tableau seront initialises avec les valeurs indiquees, les autres seront initialises a zero.

ex:

#define N 10

int t[N] = {1, 2};

Les elements d’indice 0 et 1 seront initialises respectivement avec les valeurs 1 et 2, les autres elements seront initialises a zero.

Il n’existe malheureusement pas de facteur de repetition, permettant d’exprimer \ini-tialiser n elements avec la m^eme valeur v”. Il faut soit mettre n fois la valeur v dans l’initialiseur, soit initialiser le tableau par des instructions.

Cas particulier des tableaux de caracteres

Un tableau de caracteres peut ^etre initialise selon la m^eme technique. On peut ecrire par exemple:

char ch[3] = {‘a’, ‘b’, ‘c’};

Comme cette methode est extr^emement lourde, le langage C a prevu la possibilite d’initialiser un tableau de caracteres a l’aide d’une chaine litterale. Par exemple:

char ch[8] = “exemple”;

On se rappelle que le compilateur complete toute chaine litterale avec un caractere null,

il faut donc que le tableau ait au moins un element de plus que le nombre de caracteres ecrits par le programmeur dans la chaine litterale.

Il est admissible que la taille declaree pour le tableau soit superieure a la taille de la chaine litterale.

ex:

char ch[100] = “exemple”;

dans ce cas, seuls les 8 premiers caracteres de ch seront initialises.

Il est egalement possible de ne pas indiquer la taille du tableau, et dans ce cas, le compilateur a le bon go^ut de compter le nombre de caracteres de la chaine litterale et de donner cette taille au tableau.

ex:

char ch[] = “ch aura 22 caracteres”;

 

3.1.3 Reference a un element d’un tableau

Syntaxe:

Dans sa forme la plus simple, une reference a un element de tableau a la syntaxe suivante:

expression :

) nom-de-tableau [ expression1 ]

Semantique: expression1 doit delivrer une valeur entiere, et l’expression delivre l’element d’indice expression1 du tableau. Une telle expression est une lvalue, on peut donc la rencontrer aussi bien en partie gauche qu’en partie droite d’a ectation.

ex:

Dans le contexte de la declaration:

#define N 10

int t[N];

on peut ecrire:

x = t[i]; /* reference a l’element d’indice i du tableau t */

t[i+j] = k; /* affectation de l’element d’indice i+j du tableau t */

3.2 Les instructions iteratives

3.2.1 Instruction for

Syntaxe:

instruction :

) for ( expression1 option ; expression2 option ; expression3 option ) instruction

Semantique: l’execution realisee correspond a l’organigramme suivant:

?

expression1

 

?

     

non

n du for

 

expression2 != 0 ?

 

     
   

oui

   
 
     

?

         
   

instruction

       
                 
     

?

         
   

expression3

       

Lorsque l’on omet expression1 et/ou expression2 et/ou expression3 , la semantique est celle de l’organigramme precedent, auquel on a enleve la ou les parties correspondantes.

Remarques

On voit que la vocation de expression1 et expression3 est de realiser des e ets de bord, puisque leur valeur est inutilisee. Leur fonction logique est d’^etre respectivement les parties initialisation et iteration de la boucle. expresion2 est elle utilisee pour le test de bouclage. instruction est le travail de la boucle.

Exemple de boucle for

initialisation d’un tableau

#define N 10

int t[N];

for (i = 0; i < N; i = i + 1) t[i] = 0;

3.2.2 Instruction while

Syntaxe: instruction :

) while ( expression ) instruction

Semantique:

l’execution realisee correspond a l’organigramme suivant:

 

29

3.2. LES INSTRUCTIONS ITERATIVES

         

?

       
         
 

expression != 0 ?

   

non

n du while

   

       
         

oui

     
     
           

?

         
       

instruction

         
                       
                       

3.2.3 Instruction do

Syntaxe: instruction :

) do instruction while ( expression ) ;

Semantique:

l’execution realisee correspond a l’organigramme suivant:

?

  • instruction
                 
     

?

     
 
   

expression != 0 ?

 

non

n du do

     

     
     

oui

   
 
                 

3.2.4 Instruction break

Syntaxe: instruction :

    • break ;

Semantique:

Provoque l’arr^et de la premiere instruction for, while, do englobante.

Exemple

L’instruction for ci-dessous est stoppee au premier i tel que t[i] est nul:

for (i = 0; i < N; i = i + 1)

if (t[i] == 0) break;

3.2.5 Instruction continue

Syntaxe: instruction :

) continue ;

Semantique:

Dans une instruction for, while ou do, l’instruction continue provoque l’arr^et de l’iteration courante, et le passage au debut de l’iteration suivante.

Exemple

Supposons que l’on parcoure un tableau t a la recherche d’un element satisfaisant une certaine condition algorithmiquement complexe a ecrire, mais que l’on sache qu’une valeur

negative ne peut pas convenir:

for (i = 0; i < N; i = i + 1)

{

if (t[i] < 0 ) continue; …

/*

/*

on passe au i suivant dans le for algorithme de choix

*/

*/

}

3.3 Les operateurs

3.3.1 Operateur pre et postincrement

Le langage C o re un operateur d’incrementation qui peut ^etre utilise soit de maniere pre xe, soit de maniere post xe. Cet operateur se note ++ et s’applique a une lvalue. Sa syntaxe d’utilisation est donc au choix, soit ++ lvalue (utilisation en pre xe), soit lvalue

  • (utilisation en post xe).

Tout comme l’operateur d’a ectation, l’operateur d’incrementation realise a la fois un e et de bord et delivre une valeur.

    • lvalue incremente lvalue de 1 et delivre cette nouvelle valeur. lvalue ++ incremente lvalue de 1 et delivre la valeur initiale de lvalue.

Exemples:

Soient i un int et t un tableau de int: i = 0:

t[i++] = 0; /* met a zero l’element d’indice 0 */ t[i++] = 0; /* met a zero l’element d’indice 1 */

i = 1;

 

 

31

3.3. LES OPERATEURS

t[++i] = 0; /* met a zero l’element d’indice 2 */ t[++i] = 0; /* met a zero l’element d’indice 3 */

3.3.2 Operateur pre et postdecrement

Il existe egalement un operateur de decrementation qui partage avec l’operateur increment les caracteristiques suivantes:

  1. il peut s’utiliser en pre xe ou en post xe,
  2. il s’applique a une lvalue,
  3. il fait un e et de bord et delivre une valeur.

Cet operateur se note — et decremente la lvalue de 1.

    • lvalue decremente lvalue de 1 et delivre cette nouvelle valeur. lvalue — decremente lvalue de 1 et delivre la valeur initiale de lvalue.

Exemples: Soient i un int et t un tableau de int: i = 9;

t[i–] = 0;

/*

met a zero l’element d’indice 9

*/

t[i–] = 0;

/*

met a zero l’element d’indice 8

*/

i = 8;

               

t[–i]

= 0;

/*

met

a

zero

l’element

d’indice

7

*/

t[–i]

= 0;

/*

met

a

zero

l’element

d’indice

6

*/

3.3.3 Quelques utilisations typiques de ces operateurs Utilisation dans les instructions expression

On a vu qu’une des formes d’instruction possibles en C est:

expresssion

;

               

et que cela n’a de sens que si l’expression realise

un e et de bord.

Les operateurs

++ et — realisant

precisement

un e et de bord, permettent donc d’ecrire

des instructions se reduisant

a une expression utilisant un de ces operateurs.

Une incrementation

ou une decrementation

de variable se fait classiquement en C de

la maniere suivante:

             

i++;

/*

incrementation

de i

*/

   

j–;

/*

decrementation

de j

*/

   

Utilisation dans les instructions iteratives

Une boucle for de parcours de tableau s’ecrit typiquement de la maniere suivante:

for (i = 0; i < N; i++)

{

}

 

32 CHAPTER 3. LES TABLEAUX

3.3.4 Operateur et logique

Syntaxe: expression :

) expression1 && expression2

Semantique:

expression1 est evaluee et:

  1. si sa valeur est nulle, l’expression && rend la valeur 0
  2. si sa valeur est non nulle, expression2 est evaluee, et l’expression && rend la valeur 0 si expression2 est nulle, et 1 sinon.

On voit donc que l’operateur && realise le et logique de expression1 et expression2 (en prenant pour faux la valeur 0, et pour vrai toute valeur di erente de 0).

Remarque sur la semantique

On a la certitude que expression2 ne sera pas evaluee si expression1 rend la valeur faux. Ceci presente un inter^et dans certains cas de parcours de tableau ou de liste de blocs chaines.

Par exemple dans le cas d’un parcours de tableau a la recherche d’un element ayant une valeur particuliere, supposons que l’on utilise comme test de n de boucle l’expression

i < n && t[i] == 234

ou i < n est le test permettant de ne pas deborder du tableau, et t[i] == 234 est le test de l’element recherche. S’il n’existe dans le tableau aucun element satisfaisant le

test t[i] == 234, il va arriver un moment ou on va evaluer i < n && t[i] == 234 avec

i > n et la semantique de l’operateur && assure que l’expression t[i] == 234 ne sera pas

evaluee. On ne cours donc pas le risque d’avoir une erreur materielle dans la mesure ou

t[i+1] peut referencer une adresse memoire invalide.

Exemples d’utilisation

int a,b;

if (a > 32 && b < 64) …

if ( a && b > 1) …

b = (a > 32 && b < 64);

3.3.5 Operateur ou logique

Syntaxe: expression :

) expression1 || expression2

Semantique:

expression1 est evaluee et:

 

3.4. EXERCICE

33

{ si sa valeur est non nulle, l’expression || delivre la valeur 1.

{ sinon, expression2 est evaluee, si sa valeur est nulle, l’expression || delivre la valeur 0 sinon elle delivre la valeur 1.

On voit donc que l’operateur || realise le ou logique de ses operandes, toujours avec les m^emes conventions pour les valeurs vrai et faux, a savoir 0 pour faux, et n’importe quelle valeur non nulle pour vrai.

Dans ce cas egalement, on a la certitude que le second operande ne sera pas evalue si le premier delivre la valeur vrai.

Exemples

int a,b;

if (a > 32 || b < 64) …

if ( a || b > 1) …

b = (a > 32 || b < 64);

3.3.6 Operateur non logique

Syntaxe:

expression :

) ! expression

Semantique:

expression est evaluee, si sa valeur est nulle, l’operateur ! delivre la valeur 1, sinon il delivre la valeur 0.

Cet operateur realise le non logique de son operande.

3.4 Exercice

Declarer un tableau nb_jour qui doit ^etre initialise de facon a ce que nb_jour[i] soit egal au nombre de jours du ieme mois de l’annee pour i allant de 1 a 12 (nb_jour[0] sera

inutilise).

Ecrire une procedure d’initialisation de nb_jour qui utilisera l’algorithme suivant:

si i vaut 2 le nombre de jours est 28

sinon si i pair et i <= 7 ou i impair et i > 7 le nombre de jours est 30

sinon le nombre de jours est 31

Ecrire une procedure d’impression des 12 valeurs utiles de nb_jour. La procedure main se contentera d’appeler les procedures d’initialisation et d’impression de nb_jour.

int nb_jours[13];

/*****************************************************************************/

/*

 

*/

/*

init_nb_jours

*/

/*

 

*/

/*

But:

*/

/*

Initialise le tableau nb_jours

*/

/*

 

*/

/*****************************************************************************/ void init_nb_jours()

{

int i;

for (i = 1; i <= 12; i++)

if (i == 2)

nb_jours[2] = 28;

else if ( (i % 2 == 0) && i <= 7 || (i % 2 == 1) && i > 7 )

nb_jours[i] = 30;

else nb_jours[i] = 31;

}

/*****************************************************************************/

/*

 

*/

/*

print_nb_jours

*/

/*

 

*/

/*

But:

*/

/*

Imprime le contenu du tableau nb_jours

*/

/*

 

*/

/*****************************************************************************/ void print_nb_jours()

{

int i;

for (i = 1; i <= 12; i++)

printf(“%d “,nb_jours[i]);

printf(“\n”);

}

/*****************************************************************************/

/* */ /* main */ /* */

/*****************************************************************************/ void main()

{

init_nb_jours();

print_nb_jours();

}

 

Chapter 4

Les pointeurs

4.1 Notion de pointeur

Une valeur de type pointeur repere une variable. En pratique, cela signi e qu’une valeur de type pointeur est l’adresse d’une variable.

variable de type

     

pointeur vers un int

   

int

         
   

 

372

       
         

4.2 Declarations de variables de type pointeur vers les types de base

La declaration d’une variable pointeur vers un type de base a une structure tres proche de celle de la declaration d’une variable ayant un type de base. La seule di erence consiste a faire preceder le nom de la variable du signe *.

ex:

int *pi;

/*

pi est un pointeur vers un int

*/

short int *psi;

/*

psi est un pointeur vers un short int

*/

double *pd;

/*

pd

pointeur

vers

un

flottant double precision

*/

char *pc;

/*

pc

pointeur

vers

un

char

*/

4.3 Operateur adresse de

L’operateur & applique a une variable delivre l’adresse de celle-ci, adresse qui pourra donc ^etre a ectee a une variable de type pointeur. On peut ecrire par exemple:

int i;

36 CHAPTER 4. LES POINTEURS

int *pi;

pi = &i;

/*

le pointeur pi repere la variable i

*/

4.4 Operateur d’indirection

Lorsque l’operateur * est utilise en operateur pre xe, il ne s’agit pas de l’operateur de multiplication, mais de l’operateur indirection, qui, applique a une valeur de type pointeur, designe la variable pointee.

On peut ecrire par exemple:

int i;

int *pi;

pi = &i;

/*

initialisation

du

pointeur pi

*/

*pi

= 2;

/*

initialisation

de

la valeur pointee par pi

*/

j =

*pi + 1;

/*

une utilisation de la valeur pointee par pi

*/

4.5 Exercice

  1. Declarer un entier i et un pointeur p vers un entier
  2. Initialiser l’entier a une valeur arbitraire et faire pointer p vers i
  3. Imprimer la valeur de i
  4. Modi er l’entier pointe par p (en utilisant p, pas i)
  5. Imprimer la valeur de i

Une solution possible est donnee page suivante.

 

4.5. EXERCICE

37

/*****************************************************************************/

/* */ /* main */ /* */

/*****************************************************************************/ void main()

{

int i;

int *p;

i = 1;

p = &i;

printf(“valeur de i avant: %d\n”,i);

*p = 2;

printf(“valeur de i apres: %d\n”,i);

}

 

38 CHAPTER 4. LES POINTEURS

4.6 Pointeurs et operateurs additifs

L’operateur + permet de realiser la somme de deux valeurs arithmetiques, mais il permet egalement de realiser la somme d’un pointeur et d’un entier.

Une telle operation n’a de sens cependant, que si le pointeur repere un element d’un tableau.

Soient p une valeur pointeur vers des objets de type T, et un tableau dont les elements sont du m^eme type T, si p repere le ieme element du tableau, p + j est une valeur de type pointeur vers T, qui repere le (i + j)eme element du tableau (en supposant qu’il existe).

Il en va de m^eme avec l’operateur soustraction, et si p repere le ieme element d’un tableau, p – j repere le (i – j)eme element du tableau (toujours en supposant qu’il existe).

ex:

#define N 10

int t[N];

int *p,*q,*r,*s;

p = &t[0];

/*

p repere le premier element de t

*/

q = p

+ (N-1);

/*

q repere le dernier element de t

*/

r =

&t[N-1];

/*

r

repere

le

dernier

element

de t

*/

s =

r

– (N-1);

/*

s

repere

le

premier

element

de t

*/

4.7 Difference de deux pointeurs

Il est possible d’utiliser l’operateur de soustraction pour calculer la di erence de deux pointeurs. Cela n’a de sens que si les deux pointeurs reperent des elements d’un meme tableau.

Soient p1 et p2 deux pointeurs du m^eme type tels que p1 repere le ieme element d’un tableau, et p2 repere le jeme element du m^eme tableau, p2 – p1 est une valeur de type

int qui est egale a j – i.

4.8 Exercice

Declarer et initialiser statiquement un tableau d’entiers t avec des valeurs dont certaines seront nulles.

Ecrire une procedure main qui parcoure le tableau t et qui imprime les index des elements nuls du tableau, sans utiliser aucune variable de type entier.

Une solution possible est donnee page suivante.

 

4.8. EXERCICE

39

#define N 10

int t[N] = {1,2,0,11,0,12,13,14,0,4};

/*****************************************************************************/

/* */ /* main */ /* */

/*****************************************************************************/

void main()

{

int *pdeb,*pfin,*p;

pdeb = &t[0]; pfin = &t[N-1];

/*

/*

repere le premier element de t */ repere le dernier element de t */

for (p = pdeb; p <= pfin; p++)

if (*p == 0) printf(“%d “,p – pdeb);

printf(“\n”);

}

 

40 CHAPTER 4. LES POINTEURS

4.9 Passage de parametres

4.9.1 Les besoins du programmeur

En ce qui concerne le passage de parametres, le programmeur a deux besoins fondamen-taux:

soit il desire passer a la procedure une valeur qui sera exploitee par l’algorithme de la procedure (c’est ce dont on a besoin quand on ecrit par exemple sin(x)).

soit il desire passer une reference a une variable, de maniere a permettre a la procedure de modi er la valeur de cette variable. C’est ce dont on a besoin quand on

ecrit une procedure realisant le produit de deux matrices prodmat(a,b,c) ou l’on veut qu’en n d’execution de prodmat, la matrice c soit egale au produit matriciel des matrices a et b. prodmat a besoin des valeurs des matrices a et b, et d’une reference vers la matrice c.

4.9.2 Comment les langages de programmation satisfont ces besoins

Face a ces besoins, les concepteurs de langages de programmation ont imagines di erentes manieres de les satisfaire, et quasiment chaque langage de programmation dispose de sa strategie propre de passage de parametres.

Une premiere possibilite consiste a arguer du fait que le passage de parametre par adresse est plus puissant que le passage par valeur, et a realiser tout passage de parametre par adresse ( c’est la strategie de FORTRAN et PL/1).

Une seconde possibilite consiste a permettre au programmeur de declarer explicite-ment quels parametres il desire passer par valeur, et quels parametres il desire passer par adresse (c’est la stategie de PASCAL).

La derniere possibilite consistant a realiser tout passage de parametre par valeur semble irrealiste puisqu’elle ne permet pas de satisfaire le besoin de modi cation d’un parametre. C’est cependant la stategie choisie par les concepteurs du langage C.

4.9.3 La stategie du langage C

En C, tout parametre est passe par valeur, et cette regle ne sou re aucune exception. Cela pose le probleme de realiser un passage de parametre par adresse lorsque le programmeur en a besoin. La solution a ce probleme, consiste dans ce cas, a declarer le parametre comme etant un pointeur. Cette solution n’est rendue possible que par l’existence de l’operateur adresse de qui permet de delivrer l’adresse d’une lvalue.

Voyons sur un exemple. Supposons que nous desirions ecrire une procedure add, admettant trois parametres a, b et c. Nous desirons que le resultat de l’execution de add soit d’a ecter au parametre c la somme des valeurs des deux premiers parametres. Le parametre c ne peut evidemment pas ^etre passe par valeur, puisqu’on desire modi er la valeur du parametre e ectif correspondant.

Il faut donc programmer add de la maniere suivante:

4.10. DISCUSSION

void add(a,b,c)

int a,b;

int *c; /*

c repere l’entier ou on veut mettre le resultat

*/

{

*c = a + b;

}

void main()

{

int i,j,k;

/* on passe l’adresse de k a add comme troisieme parametre */ add(i,j,&k);

}

4.10 Discussion

  1. Nous estimons qu’il est interessant pour le programmeur de raisonner en terme de passage par valeur et de passage par adresse, et qu’il est preferable d’a rmer, lorsque l’on fait f(&i); \i est passe par adresse a f”, plut^ot que d’a rmer \l’adresse de i est passee par valeur a f”, tout en sachant que c’est la deuxieme a rmation qui colle le mieux a la stricte realite du langage. Que l’on ne s’etonne donc pas dans la suite de ce manuel de nous entendre parler de passage par adresse.
  2. Nous retiendrons qu’en C, le passage de parametre par adresse est entierement gere par le programmeur. C’est a la charge du programmeur de declarer le parametre concerne comme etant de type pointeur vers …, et de bien songer, lors de l’appel de la fonction, a passer l’adresse du parametre e ectif.

4.11 Une derniere precision

Quand un langage o re le passage de parametre par valeur, il y a deux possibilies:

  1. soit le parametre est une constante (donc non modi able)
  2. soit le parametre est une variable locale a la procedure initialisee lors de l’appel de

la procedure avec la valeur du parametre e ectif.

C’est la seconde solution qui a etee retenue par les concepteurs du langage C. Voyons sur un exemple. Supposons que l’on desire ecrire une fonction sum admettant comme parametre n et qui rende la somme des n premiers entiers. On peut programmer de la maniere suivante:

int sum(n)

int n;

{

int r = 0;

 

42 CHAPTER 4. LES POINTEURS

for ( ; n > 0; n–) r = r + n;

return(r);

}

On voit que le parametre n est utilise comme variable locale, et que dans l’instruction for, la partie initialisation est vide puisque n est initialisee par l’appel de sum.

4.12 Exercice

On va coder un algorithme de cryptage tres simple: on choisit un decalage (par exemple 5), et un a sera remplace par un f, un b par un g, un c par un h, etc… On ne cryptera que les lettres majuscules et minuscules sans toucher ni a la ponctation ni a la mise en page (caracteres blancs et line feed). On supposera que les codes des lettres se suivent de a a z et de A a Z.

  1. Declarer un tableau de caracteres mess initialise avec le message en clair.
  2. Ecrire une procedure crypt de cryptage d’un caractere qui sera passe par adresse.
  3. Ecrire le main qui activera crypt sur l’ensemble du message et imprimera le resultat.

4.12. EXERCICE

43

char mess[] = “Les sanglots longs des violons de l’automne\n\ blessent mon coeur d’une langeur monotone”;

#define DECALAGE 5

/*****************************************************************************/

/*

 

*/

/*

crypt

*/

/*

 

*/

/*

But:

*/

/*

Crypte le caractere passe en parametre

*/

/*

 

*/

/*

Interface:

*/

/*

p : pointe le caractere a crypter

*/

/*

 

*/

/*****************************************************************************/ void crypt(p)

char *p;

{

#define HAUT 1

#define BAS 0

int casse;

if (*p >= ‘a’ && *p <= ‘z’) casse = BAS;

else if (*p >= ‘A’ && *p <= ‘Z’) casse = HAUT; else return;

*p = *p + DECALAGE;

if (casse == BAS && *p > ‘z’ || casse == HAUT && *p > ‘Z’) *p = *p -26;

}

/*****************************************************************************/

/* */ /* main */ /* */

/*****************************************************************************/ void main()

{

char *p;

int i;

/* phase de cryptage

p = &mess[0];

while(*p)

crypt(p++);

*/

/* impression du resultat

*/

printf(“resultat:\n”);

i = 0;

while (mess[i]) printf(“%c”,mess[i++]);

}

 

44 CHAPTER 4. LES POINTEURS

4.13 Lecture formattee

Il existe une fonction standard de lecture formattee qui fonctionne selon le m^eme principe

que la procedure printf.

Sa syntaxe d’utilisation est la suivante:

scanf ( chaine-de-caracteres , liste-d’expressions ) ;

la chaine-de-caracteres indique sous forme de sequences d’echappement le format des entites que scanf lit sur l’entree standard:

%d pour un nombre decimal

%x pour un nombre ecrit en hexadecimal

%c pour un caractere

Tous les elements de la liste-d’expressions doivent delivrer apres evaluation l’adresse de la variable dans laquelle on veut mettre la valeur lue.

ex:

scanf(“%d %x”,&i,&j);

cette instruction va lire un nombre ecrit en decimal et mettre sa valeur dans la variable

i, puis lire un nombre ecrit en hexadecimal et mettre sa valeur dans la variable j.

On aura remarque que les parametres i et j ont etes passes par adresse a scanf.

Attention

Une erreur facile a commettre est d’omettre les operateurs & devant les parametres de scanf. C’est une erreur di cile a detecter car le compilateur ne donnera aucun message d’erreur, et a l’execution, ce sont les valeurs de i et j qui seront interpretees comme des adresses par scanf. Avec un peu de chance ces valeurs seront des adresses invalides, et le programme s’avortera1 sur l’execution du scanf, ce qui donnera une idee du probleme. Avec un peu de malchance, ces valeurs donneront des adresses parfaitement acceptables,

et le scanf s’executera en allant ecraser les valeurs d’autres variables qui ne demandaient rien a personne. Le programme pourra s’avorter beaucoup plus tard, rendant tres difficile la detection de l’erreur.

4.14 Les dernieres instructions

Le langage C comporte 3 instructions que nous n’avons pas encore vu: un if generalise, un goto et une instruction nulle.

4.14.1 Instruction switch

Le langage C o re une instruction switch qui est un if generalise.

1Le terme avorter est a prendre au sens technique de abort

4.14. LES DERNIERES INSTRUCTIONS

Syntaxe:

instruction :

) switch ( expression )

{

case expression1 : liste-d’instructions1 option

case expression2 : liste-d’instructions2 option

….

break;option

break;option

case expressionn : liste-d’instructionsn option break;option

default : liste-d’instructions

}

De plus:

{ toutes les expressioni doivent delivrer une valeur connue a la compilation.

{ il ne doit pas y avoir deux expressioni delivrant la m^eme valeur. { l’alternative default est optionnelle.

Semantique:

  1. expression est evaluee, puis le resultat est compare avec expression1, expres-sion2, etc …
  2. a la premiere expressioni dont la valeur est egale a celle de expression, on execute la liste-d’instructions correspondante jusqu’a la rencontre de la premiere instruction break;. La rencontre d’une instruction break termine l’execution

de l’instruction switch.

  1. si il n’existe aucune expressioni dont la valeur soit egale a celle de expression, on execute la liste-d’instructions de l’alternative default si celle-ci exite, sinon on ne fait rien.

Discussion

Vu le nombre de parties optionnelles dans la syntaxe, il y a 3 types d’utilisations possibles pour le switch.

Premiere possibilite, on peut avoir dans chaque alternative une liste-d’instructions et un break;. Comme dans l’exemple suivant:

#define BLEU 1

#define BLANC 2

#define ROUGE 3

void print_color(color)

int color;

{

switch(color)

{

case BLEU : printf(“bleu”); break;

case BLANC : printf(“blanc”); break;

case ROUGE : printf(“rouge”); break;

default : printf(“erreur interne du logiciel numero xx\n”);

}

}

Deuxieme possibilite, on peut avoir une ou plusieurs alternatives ne possedant ni liste-d’instructions, ni break;. Supposons que l’on desire compter dans une suite de caracteres, le nombre de caracteres qui sont des chi res, et le nombre de caracteres qui ne le sont pas.

On pourra utiliser le switch suivant:

switch(c)

{

case ‘0’:

case ‘1’:

case ‘2’:

case ‘3’:

case ‘4’:

case ‘5’:

case ‘6’:

case ‘7’:

case ‘8’:

case ‘9’: nb_chiffres++; break;

default: nb_non_chiffres++;

}

Troisieme possibilite, une alternative peut ne pas avoir de break comme dans l’exemple suivant:

#define POSSIBLE 0

#define IMPOSSIBLE 1

void print_cas(cas)

int cas;

{

switch (cas)

{

case IMPOSSIBLE: printf(“im”);

case POSSIBLE: printf(“possible\n”); break;

case default: printf(“erreur interne du logiciel numero xx\n”);

}

}

Une telle utilisation du switch pose un probleme de lisibilite, car l’experience montre que l’absence du break; est tres di cile a voir. Il est donc recommande de mettre un commentaire, par exemple de la facon suivante:

case IMPOSSIBLE: printf(“im”); /* ATTENTION: pas de break; */

 

 

47

4.14. LES DERNIERES INSTRUCTIONS

4.14.2 Instruction goto

Syntaxe: instruction :

) goto identi cateur ;

Semantique:

Toute instruction peut ^etre precedee d’un identi cateur suivi du signe :. Cet iden-ti cateur est appele etiquette. Une instruction goto identi cateur a pour e et de

transferer le contr^ole d’execution a l’instruction etiquettee par identi cateur.

ex:

{

     

etiq2:

     

/*

des instructions

*/

goto etiq1;

/*

goto avant definition de l’etiquette

*/

/*

des instructions

*/

etiq1:

     

/*

des instructions

*/

goto etiq2;

/*

goto apres definition de l’etiquette

*/

}

     

4.14.3 Instruction nulle

Syntaxe: instruction :

  • ;

Semantique:

ne rien faire!

Cette instruction ne rend que des services syntaxiques. Elle peut ^etre utile quand on desire mettre une etiquette a la n d’une instruction composee.

Par exemple:

{

fin: ;

}

Elle est egalement utile pour mettre un corps nul a certaines instructions iteratives. En e et, a cause des e ets de bord dans les expressions, on peut arriver parfois a faire tout le travail dans les expressions de contr^ole des instructions iteratives.

Par exemple, on peut initialiser a zero un tableau de la maniere suivante:

for (i = 0; i < N; t[i++]= 0)

; /* instruction nulle */

 

Attention

Cette instruction nulle peut parfois avoir des e ets desastreux. Supposons que l’on veuille ecrire la boucle:

for (i = 0; i < N; i++)

t[i] = i;

si par megarde on met un ; a la n de ligne du for, on obtient un programme parfaitement correct, qui s’execute sans broncher, mais ne fait absolument pas ce qui etait prevu. En e et:

for (i = 0; i < N; i++) ;

t[i] = i;

execute le for avec le seul e et d’amener la variable i a la valeur N+1, et ensuite execute une fois t[i] = i ce qui a probablement pour e et d’ecraser la variable declaree juste apres le tableau t.

4.15 Exercice

Ecrire une procedure main se comportant comme une calculette c’est a dire executant une boucle sur:

  1. lecture d’une ligne supposee contenir un entier, un operateur et un entier (ex: 12 +

34), les operateurs seront + – * / %

  1. calculer la valeur de l’expression
  2. imprimer le resultat

#define VRAI 1

#define FAUX 0

/*****************************************************************************/

/* */ /* main */ /* */

/*****************************************************************************/

void main()

     

{

     

int i,j,r;

/*

les operandes

*/

char c;

/*

l’operateur

*/

char imp;

/*

booleen de demande d’impression du resultat */

while (1)

{

scanf(“%d %c %d”,&i,&c,&j);

imp = VRAI;

switch (c)

{

case ‘+’ : r = i + j; break;

case ‘-‘ : r = i – j; break;

case ‘*’ : r = i * j; break;

case ‘/’ :

if ( j == 0)

{

printf(“Division par zero\n”);

imp = FAUX;

}

else r = i / j;

break;

case ‘%’ : r = i % j; break;

default : printf(“l’operateur %c est incorrect\n”,c); imp = FAUX;

} /* fin du switch */

if (imp) printf(“%d\n”,r);

}

}

 

Chapter 5

Tableaux et pointeurs

5.1 Retour sur les tableaux

Nous avons jusqu’a present utilise les tableaux de maniere intuitive, en nous contentant de savoir qu’on pouvait declarer un tableau par une declaration du genre:

int t[10];

et qu’on disposait d’un operateur d’indexation, note [], permettant d’obtenir un element du tableau. L’element d’index i du tableau t se designant par t[i].

Ceci est su sant pour les utilisations les plus simples des tableaux, mais des que l’on veut faire des choses plus complexes, (par exemple passer des tableaux en parametre), il est necessaire d’aller plus a fond dans le concept de tableau tel que l’envisage le langage C.

De maniere a bien mettre en evidence les particularites des tableaux dans le langage C, nous allons faire un rappel sur la notion de tableau telle qu’elle est generalement envisagee dans les langages de programmation.

5.2 La notion de tableau en programmation

La plupart des langages de programmation disposent du concept de tableau. Une telle notion o re au programmeur deux services principaux:

declarer des tableaux d’utiliser un operateur d’indexation, qui peut ^etre note () ou [], admettant en operande un tableau t et un entier i, et delivrant l’element d’index i de t.

Derriere les particularies de chaque langage (parfois les bornes des tableaux peuvent ^etre connue dynamiquement, parfois les bornes doivent ^etre connues statiquement, etc…) le programmeur peut cependant se raccrocher a un certain nombre de points communs.

Generalement en e et, quand on declare un tableau de nom t,

  1. t est une variable,
  2. t est de type tableau de quelquechose,
  3. t designe le tableau en entier.

Voyons ce qu’il en est en ce qui concerne la facon dont C envisage les tableaux.

52 CHAPTER 5. TABLEAUX ET POINTEURS

5.3 La notion de tableau dans le langage C

Sous une similitude de surface, le langage C cache de profondes di erences avec ce que nous venons de voir. En e et, lorsqu’en C, on declare un tableau de nom t,

  1. t n’est pas une variable,
  2. t n’est pas de type tableau de quelquechose,
  3. t ne designe pas le tableau en entier.

Que se passe-t-il alors?

En C, quand on ecrit:

int t[10];

on a bien declare une variable de type tableau, mais cette variable n’a pas de nom. Le nom t est le nom d’une constante qui est de type pointeur vers int, et dont la valeur est l’adresse du premier element du tableau. (En fait, t est rigoureusement

identique a &t[0]).

Le fait de declarer une variable qui n’a pas de nom est un mecanisme qui est bien connu dans les langages de programmation. La situation est comparable a ce que l’on obtient lorsqu’on fait l’allocation dynamique d’une variable (via le new PASCAL, l’allocate de PL/1 etc…). La variable allouee n’a pas de nom, elle ne peut ^etre designee que via un pointeur. Ce qui est original ici, c’est de retrouver ce mecanisme sur une allocation de variable qui est une allocation statique.

Une telle facon d’envisager les tableaux dans le langage C va avoir deux consequences importantes: la premiere concerne l’operateur d’indexation et la seconde la passage de tableaux en parametre.

5.3.1 L’operateur d’indexation

La semantique de l’operateur d’indexation consiste a dire qu’apres les declarations:

int t[N];

int i;

t[i] est equivalent a *(t + i).

Veri ons que cela est bien conforme a la facon dont nous l’avons utilise jusqu’a present.

Nous avons vu que t a pour valeur l’adresse du premier element du tableau. D’apres ce que nous savons sur l’addition entre un pointeur et un entier, nous pouvons conclure que t + i est l’adresse de l’element de rang i du tableau. Si nous appliquons l’operateur d’indirection a (t+i) nous obtenons l’element de rang i du tableau, ce que nous notions jusqu’a present t[i].

consequence numero 1

L’operateur d’indexation note [] est donc inutile, et n’a ete o ert que pour des raisons de lisibilite des programmes, et pour ne pas rompre avec les habitudes de programmation.

 

5.3. LA NOTION DE TABLEAU DANS LE LANGAGE C

53

consequence numero 2

Puisque l’operateur d’indirection s’applique a des valeurs de type pointeur, on va pouvoir l’appliquer a n’importe quelle valeur de type pointeur, et pas seulement aux constantes reperant des tableaux.

En e et, apres les declarations:

int t[10];

int * p;

on peut ecrire:

p = &t[4];

et utiliser l’operateur d’indexation sur p, p[0] etant t[4], p[1] etant t[5], etc… p peut donc ^etre utilise comme un sous-tableau de t.

Si on est allergique au fait d’avoir des tableaux dont les bornes vont de 0 a n-1 et si

on prefere avoir des bornes de 1 a n, on peut utiliser la methode suivante:

int t[10];

int * p;

p = &t[-1];

Le \tableau” p est un synonyme du tableau t avec des bornes allant de 1 a 10.

consequence numero 3

L’operateur d’indexation est commutatif ! En e et, t[i] etant equivalent a *(t + i) et l’addition etant commutative, t[i] est equivalent a *(i + t) donc a i[t].

Lorsqu’on utilise l’operateur d’indexation, on peut noter indi eremment l’element de rang i d’un tableau, t[i] ou i[t].

Il est bien evident que pour des raisons de lisibilite, une telle notation doit etre^ prohibee, et doit ^etre consideree comme une consequence pathologique de la de nition de l’operateur d’indexation.

5.3.2 Passage de tableau en parametre

Nous avons vu que le nom d’un tableau est en fait l’adresse du premier element. Lorsqu’un tableau est passe en parametre e ectif, c’est donc cette adresse qui sera passee en parametre. Le parametre formel correspondant devra donc ^etre declare comme etant de type pointeur.

Voyons sur un exemple. Soit a ecrire une procedure imp_tab qui est chargee d’imprimer un tableau d’entiers qui lui est passe en parametre. On peut proceder de la maniere suivante:

void imp_tab(t,nb_elem)

/*

definition de imp_tab

*/

int *t;

int nb_elem;

{

int i;

54 CHAPTER 5. TABLEAUX ET POINTEURS

for (i = 0; i < nb_elem; i++) printf(“%d “,*(t + i)); }

Cependant, cette methode a un gros inconvenient. En e et, lorsqu’on lit l’en-t^ete de cette procedure, c’est a dire les deux lignes

void imp_tab(t)

int *t;

il n’est pas clair de savoir si le programmeur a voulu passer en parametre un pointeur vers un int (c’est a dire un pointeur vers un seul int), ou au contraire si il a voulu passer un tableau, c’est a dire un pointeur vers une zone de n int.

De facon a ce que le programmeur puisse exprimer cette di erence au niveau de l’entete de la procedure, le langage C admet que l’on puisse declarer un parametre formel de la facon suivante:

void proc(t)

int t[];

{

/*

corps de la procedure proc

*/

}

car le langage assure que lorsqu’un parametre formel de procedure ou de fonction est declare comme etant de type tableau de xxx, il est considere comme etant de type pointeur vers xxx.

Si d’autre part, on se souvient que la notation *(t + i) est equivalente a la notation t[i], la de nition de imp_tab peut s’ecrire:

void imp_tab(t,nb_elem)

/*

definition de imp_tab

*/

int t[];

int nb_elem;

{

int i;

for (i = 0; i < nb_elem; i++) printf(“%d “,t[i]); }

Cette facon d’exprimer les choses est beaucoup plus claire, et sera donc preferee.

L’appel se fera de la maniere suivante:

#define NB_ELEM 10

int tab[NB_ELEM];

void main()

{

imp_tab(tab,NB_ELEM);

}

5.4. MODIFICATION DES ELEMENTS D’UN TABLEAU PASSE EN PARAMETRE55

5.4 Modification des elements d’un tableau passe en parametre

Lorsqu’on passe un parametre e ectif a une procedure ou une fonction, on a vu que l’on passait une valeur. Il est donc impossible a une procedure de modi er la valeur d’une variable passee en parametre.

En ce qui concerne les tableaux par contre, on passe a la procedure l’adresse du premier element du tableau. La procedure pourra donc modi er si elle le desire les elements du tableau.

Il semble donc que le passage de tableau en parametre se fasse par adresse et non par valeur, et qu’il s’agisse d’une exception a la regle qui a rme qu’en C, tout passage de parametre se fasse par valeur.

Bien sur il n’en est rien. Puisqu’en C le nom d’un tableau a pour valeur l’adresse du premier element du tableau, c’est cette valeur qui est passee en parametre. C’est donc bien encore du passage par valeur.

Du point de vue pratique, on retiendra que l’on peut modi er les elements d’un tableau passe en parametre.

On peut ecrire par exemple:

/* incr_tab fait + 1 sur tous les elements du tableau t void incr_tab(t,nb_elem)

*/

int t[];

int nb_elem;

{

int i;

for (i = 0; i < nb_elem; i++) t[i]++;

}

5.5 Exercice

  1. Declarer et initialiser deux tableaux de caracteres (ch1 et ch2).
  2. Ecrire une fonction (lg_chaine1) qui admette en parametre un tableau de caracteres se terminant par un null, et qui rende le nombre de caracteres du tableau (null exclu).
  3. Ecrire une fonction (lg_chaine2) qui implemente le m^eme interface que lg_chaine1, mais en donnant a son parametre le type pointeur vers char.
  4. La procedure main imprimera le nombre d’elements de ch1 et ch2 par un appel a lg_chaine1 et lg_chaine2.

56 CHAPTER 5. TABLEAUX ET POINTEURS

#define NULL_C ‘\0’

char ch1[] = “cette chaine comporte 35 caracteres”; char ch2[] = “et celle ci fait 30 caracteres”;

/*****************************************************************************/

/*

 

*/

/*

lg_chaine1

*/

/*

 

*/

/*

But:

*/

/*

calcule la longueur d’une chaine de caracteres

*/

/*

 

*/

/*

Interface:

*/

/*

ch : la chaine de caracteres

*/

/*

valeur rendue : la longueur de ch

*/

/*

 

*/

/*****************************************************************************/ int lg_chaine1(ch)

char ch[];

{

int i = 0;

while (ch[i] != NULL_C) i++; /* equivalent a while(ch[i]) i++; */

return(i);

}

/*****************************************************************************/

/*

 

*/

/*

lg_chaine2

*/

/*

 

*/

/*

But:

*/

/*

identique a celui de lg_chaine1

*/

/*

 

*/

/*****************************************************************************/ int lg_chaine2(ch)

char *ch;

{

int i = 0;

while (*ch != NULL_C)

{ i++; ch++; }

return(i);

}

/*****************************************************************************/

/* */ /* main */ /* */

 

5.5. EXERCICE

57

/*****************************************************************************/

main()

{

printf(“la longeur de ch1 est %d\n”,lg_chaine1(ch1)); printf(“la longeur de ch2 est %d\n”,lg_chaine2(ch2));

}

 

58 CHAPTER 5. TABLEAUX ET POINTEURS

5.6 Declaration de tableaux multi-dimensionnels

En C, un tableau multi-dimensionnel est considere comme etant un tableau dont les elements sont eux m^emes des tableaux.

Un tableau a deux dimensions se declarera donc de la maniere suivante:

int t[10][20];

Bien evidemment, les m^emes considerations que celles que nous avons developpees sur les tableaux a une dimension s’appliquent, a savoir:

  1. t est une constante de type pointeur vers un tableau de 20 int.
  2. sa valeur est l’adresse d’une zone su sante pour stocker un tableau de 10 tableaux de 20 int.

5.7 Acces aux elements d’un tableau multi-dimensionnel

L’acces a un element du tableau se fera de preference par l’expression t[i][j].

5.8 Passage de tableaux multi-dimensionnels en parametre

Lorsqu’on desire qu’un parametre formel soit un tableau a deux dimensions, il faut le

declarer comme dans l’exemple suivant:

#define N 10

p(t)

int t[][N];

{

… /* corps de p */

}

On peut en e et omettre la taille de la premiere dimension, mais il est necessaire d’indiquer la taille de la seconde dimension, car le compilateur en a besoin pour generer le code permettant d’acceder a un element. En e et, si T est la taille des elements de t, l’adresse de t[i][j] est: adresse de t + (i N + j) T . Le compilateur a donc besoin de connaitre N.

On a vu qu’il etait possible de passer en parametre a une procedure la taille d’un

tableau unidimensionnel, de facon a permettre a cette procedure d’accepter des parametres e ectifs qui aient des tailles di erentes.

En ce qui concerne les tableaux a plusieurs dimensions, ceci reste vrai pour la premiere dimension, mais est faux pour les autres.

ex:

#define P 10

void raz_mat(t,n)

int t[][P];

 

5.9. INITIALISATION D’UN TABLEAU MULTI-DIMENSIONNEL

59

int n;

/*

taille de la dimension */

 

{

     

int i,j;

   

for (i

= 0; i < n;

i++)

 

for

(j = 0; j <

P; j++)

 
 

t[i][j] = 0;

   

}

     

raz_mat ne sera appliquable qu’a des tableaux dont la premiere dimension a une taille quelconque, mais dont la seconde dimension doit imperativement avoir P elements.

5.9 Initialisation d’un tableau multi-dimensionnel

Lorsqu’un tableau multi-dimensionnel est declare a l’exterieur de toute procedure ou fonc-tion, il peut ^etre initialise a la declaration.

ex:

int t[5][5] = {

{ 0,1,2,3,4},

{ 10,11,12,13,14}, { 20,21,22,23,24}, { 30,31,32,33,34}, { 40,41,42,43,44}

};

Un telle initialisation doit se faire avec des expressions constantes, c’est a dire delivrant une valeur connue a la compilation.

5.10 Exercice

  1. Declarer et initialiser statiquement une matrice [5,5] d’entiers (tab)
  2. Ecrire une fonction (print_mat) qui admette en parametre une matrice [5,5] et qui

imprime ses elements sous forme de tableau.

  1. La procedure main fera un appel a print_mat pour le tableau tab.

60 CHAPTER 5. TABLEAUX ET POINTEURS

#define N 5

int tab[N][N] =

{

{0,1,2,3,4},

{10,11,12,13,14},

{20,21,22,23,24},

{30,31,32,33,34},

{40,41,42,43,44}

};

/*****************************************************************************/

/*

 

*/

/*

print_mat

*/

/*

 

*/

/*

But:

*/

/*

Imprime un tableau N sur N (N est une constante)

*/

/*

 

*/

/*****************************************************************************/ print_mat(t)

int t[][N];

{

int i,j;

for (i= 0; i < N; i++)

{

for (j = 0; j < N; j++)

printf(“%d “,t[i][j]);

printf(“\n”);

}

}

/*****************************************************************************/

/* */ /* main */ /* */

/*****************************************************************************/

main()

{

print_mat(tab);

}

 

Chapter 6

Les entrees sorties

A ce moment-ci de l’etude du langage, le lecteur eprouve sans doute le besoin de disposer de moyens d’entrees/sorties plus puissants que les quelques possibilites de printf et scanf que nous avons presentees. Nous allons donc consacrer un chapitre entier a cette question, en nous restreignant a des possibilites communes a UNIX System V et UNIX Berkeley, de maniere a n’avoir aucun probleme de portabilite.

L’usage de toutes les fonctions presentees necessitant d’utiliser les facilites d’inclusion de source du preprocesseur, nous allons commencer par cela.

6.1 Inclusion de source

Lorsqu’on developpe un gros logiciel, il est generalement necessaire de le decouper en modules de taille raisonnable.

Un fois un tel decoupage realise, il est courant que plusieurs modules aient cer-taines parties communes (par exemple des de nitions de constantes a l’aide de directives #define).

De facon a eviter la repetition de ces parties communes dans les modules, le langage C o re une facilite d’inclusion de source qui est realisee a l’aide de la commande #include du preprocesseur.

Lorsque le preprocesseur rencontre une ligne du type:

#include ” nom-de- chier “

ou

#include < nom-de- chier >

il remplace cette ligne par le contenu du chier nom-de- chier.

L’endroit ou se fait la recherche de nom-de- chier depend du systeme et des caracteres choisis pour entourer nom-de- chier ( ” ou < et >).

La philosophie est la suivante:

si on utilise “, le chier a inclure est un chier utilisateur, il sera donc recherche dans l’espace de chiers de l’utilisateur (sous UNIX ce sera le directory courant).

si on utilise < et >, le chier est un chier systeme, il sera recherche dans un ou plusieurs espaces de chiers systemes (sous UNIX ce sera /usr/lib/include).

61

 

62

 

CHAPTER 6. LES ENTREES SORTIES

6.2 Ouverture et fermeture de chiers

6.2.1 Ouverture d’un chier : fopen

Lorqu’on desire acceder a un chier, il est necessaire avant tout acces d’ouvrir le chier a l’aide de la fonction fopen.

Parametres

Cette fonction admet deux parametres qui sont respectivement:

une chaine de caracteres qui est le nom du chier auquel on veut acceder.

une chaine de caracteres qui est le mode de l’ouverture, et qui peut prendre les valeurs suivantes:

“r” ouverture en lecture.

“w” ouverture en ecriture.

“a” ouverture en ecriture a la n.

Si on ouvre un chier qui n’existe pas en “w” ou en “a”, il est cree.

Si on ouvre en “w” un chier qui existe deja, son ancien contenu est perdu.

Si on ouvre un chier qui n’existe pas en “r”, c’est une erreur.

Valeur rendue

La fonction fopen retourne une valeur de type pointeur vers FILE, ou FILE est un type prede ni dans le chier stdio.h.

Si l’ouverture a reussi, la valeur retournee permet de reperer le chier, et devra etre passee en parametre a toutes les procedures d’entrees / sorties sur le chier.

Si l’ouverture s’est avere impossible, fopen rend la valeur NULL, constante prede nie dans stdio.h

Utilisation typique de fopen

#include <stdio.h>

FILE *fp;

if ((fp = fopen(“donnees”,”r”)) == NULL) printf(“Impossible d’ouvrir le fichier donnees\n”);

Quand un programme est lance par le systeme, celui-ci ouvre trois chiers correspon-dant aux trois voies de communication standard: standard input, standard output et stan-dard error. Il y a trois constantes prede nies dans stdio.h de type pointeur vers FILE qui reperent ces trois chiers. Elles ont pour nom respectivement stdin, stdout et stderr.

 

   

63

6.3. LECTURE ET ECRITURE PAR CARACTERE SUR FICHIER

6.2.2 fermeture d’un chier : fclose

Quand on a termine les E/S sur un chier, il faut en informer le systeme a l’aide de la fonction fclose qui admet comme parametre une valeur de type pointeur vers FILE qui repere le chier a fermer.

Utilisation typique:

#include <stdio.h>

FILE *f;

fclose(f);

6.3 Lecture et ecriture par caractere sur chier

6.3.1 lecture par caractere: getc

getc est une fonction a un seul parametre qui est une valeur de type pointeur vers FILE. Elle lit un caractere du chier et le retourne dans un int. La valeur rendue est un int car elle peut prendre en plus de toutes les valeurs possibles pour les caracteres, la valeur EOF qui est une constante prede nie se trouvant dans stdio.h. Cette valeur indique que l’on a atteint la n du chier.

Utilisation typique:

#include <stdio.h>

int c;

FILE *fi;

while ((c = getc(fi)) != EOF)

{

… /* utilisation de c */

}

6.3.2 ecriture par caractere : putc

putc admet deux parametres qui sont respectivement :

  1. le caractere a ecrire dans le chier
  2. la valeur de type pointeur vers FILE qui repere le chier.

En ce qui concerne le premier parametre, on peut passer une valeur de type char, mais aussi une valeur de type int. Dans ce dernier cas, la fraction de l’entier qui est interprete comme un char est celle dans laquelle getc met la valeur du caractere qu’il lit.

Ce qui fait que l’on peut ecrire:

#include <stdio.h>

int c;

FILE *fi,*fo;

 

64

 

CHAPTER 6. LES ENTREES SORTIES

while ((c = getc(fi)) != EOF)

putc(c,fo);

aussi bien que:

#include <stdio.h>

char c;

int resu;

FILE *fi,*fo;

while ((resu = getc(fi)) != EOF)

{

c = resu;

putc(c,fo);

}

6.4 Lecture et ecriture par lignes sur chier

6.4.1 lecture par ligne : fgets

fgets admet trois parametres :

  1. un tableau de caracteres
  2. un int donnant la taille du tableau.
  3. un pointeur vers FILE caracterisant le chier.

fgets lit les caracteres du chiers et les range dans le tableau jusqu’a rencontre d’un line-feed ( qui est mis dans le tableau) ou jusqu’a ce qu’il ne reste plus qu’un seul caractere libre dans le tableau. fgets complete alors les caracteres lus par un caractere a zero (un null).

fgets rend un pointeur vers le tableau de caracteres, ou NULL sur n de chier.

Utilisation typique:

#include <stdio.h>

#define LONG …

char ligne[LONG];

FILE *fi;

while (fgets(ligne,LONG,fi) != NULL)

{

… /* utilisation de ligne

}

*/

/*

stop sur fin de fichier

*/

6.4.2 ecriture par chaine : fputs

fputs admet deux parametres :

1. un tableau de caracteres (termine par un caractere null)

 

 

65

6.5. E/S FORMATTEES SUR FICHIERS

2. un pointeur vers FILE caracterisant le chier.

fputs ecrit sur le chier le contenu du tableau dont la n est indiquee par un caractere null. Le tableau de caracteres peut contenir ou non un line-feed. fputs peut donc servir indi erement a ecrire une ligne ou une cha^ne quelconque.

Utilisation typique:

#include <stdio.h>

#define LONG …

char ligne[LONG];

FILE *fo;

fputs(ligne,fo);

6.5 E/S formattees sur chiers

6.5.1 Ecriture formattee: fprintf

La fonction fprintf admet un nombre variable de parametres. Son utilisation est la suivante :

fprintf ( le-ptr , format , param1 , param2 , … , paramn )

le-ptr est une valeur de type pointeur vers FILE qui repere le chier sur lequel on veut ecrire. format est une cha^ne de caracteres qui sera recopiee sur le chier.

En plus des caracteres a copier tels quels, format contient des sequences d’escape decrivant la maniere dont doivent ^etre ecrits les parametres param1, param2, … paramn.

Le caractere introduisant une sequence d’escape est %. Une sequence d’escape se compose des elements suivants:

Un certain nombre (eventuellement zero) d’indicateurs pouvant ^etre les caracteres suivants:

  • parami sera cadre a gauche dans son champ d’impression.
  • si parami est un nombre signe il sera imprime precede du signe + ou -.

blanc si parami est un nombre signe et si son premier caractere n’est pas un signe, on imprimera un blanc devant parami . Si on a a la fois l’indicateur + et l’indicateur blanc, ce dernier sera ignore.

  • cet indicateur demande l’impression de parami sous une forme non standard. Pour les formats c, d, s, u cet indicateur ne change rien. Pour le format o, cet indicateur force la precision a ^etre augmentee de maniere a ce que parami soit imprime en commencant par un zero. Pour les formats x et X, cet indicateur a pour but de faire preceder parami respectivement de 0x ou 0X, sauf si parami est nul. Pour les formats e, E, f, g et G, cet

indicateur force parami a ^etre imprime avec un point decimal. Pour les formats g et G, cet indicateur emp^eche les zeros de la n d’^etre enleves.

 

66

 

CHAPTER 6. LES ENTREES SORTIES

Une cha^ne de nombres decimaux indiquants la taille minimum du champ d’impression, exprimee en caracteres. Si parami s’ecrit sur un nombre de caracteres inferieur a cette taille, parami est complete a gauche (ou a droite si l’indicateur – a ete utilise). Le caractere qui sert a completer parami est le caractere blanc ou le caractere zero si cette taille minimum du champ d’impression commence par un zero.

Le caractere . (point) suivi d’une cha^ne de caracteres decimaux indiquant la precision avec laquelle parami doit ^etre imprime. Cette precision est le nombre de caracteres d’impression de param i pour les formats d, o, u, x et X, le nombre de chi res apres la virgule pour les formats e, E et f, et le nombre maximum de chi res signi catifs pour les formats g et G, et le nombre maximum de caracteres pour le format s.

La chaine de chi res decimaux indiquant la taille maximum du champ d’impression

et/ou la chaine de chi res decimaux indiquant la precision peuvent ^etre remplacees par le caractere *. Si le caractere * a ete utilise une seule fois dans le format, la valeur

correspondante (taille du champ ou precision) sera prise egale a parami 1 . Si le caractere

  • a ete utilise deux fois, la taille du champ d’impression sera egale a parami 2, et la

precision sera egale a parami 1.

Le caractere l qui, s’il est utilise avec l’un des formats d, o, u, x ou X, signi e que parami sera interprete comme un long. Dans le cas ou l est utilise avec un autre format, il est ignore.

un caractere qui peut prendre les valeurs suivantes :

  • parami sera ecrit en decimal signe.
  • parami sera ecrit en decimal non signe.
  • parami sera ecrit en octal non signe.

x,X

parami sera ecrit en hexadecimal non signe. La notation hexadecimale utilisera les lettres abcdef dans le cas du format x, et les lettres ABCDEF dans le cas du format X.

Dans les 4 cas qui precedent, la precision indique le nombre minimum de chi res avec lesquels ecrire parami . Si parami s’ecrit avec moins de chi res,

il sera complete avec des zeros. La precision par defaut est 1.

  • parami sera interprete comme un caractere
  • parami sera interprete comme l’adresse d’une cha^ne de car-acteres terminee par null. Cette cha^ne sera imprimee.

e,E parami sera interprete comme un ottant et ecrit sous la forme:

-option pe . pf e signe exposant

dans laquelle pe et pf sont respectivement partie entiere et par-tie fractionnaire de la mantisse. La partie entiere est exprimee avec un seul chi re, la partie fractionnaire est exprimee avec

un nombre de chi res egal a la precision. La precision est prise

 

 

67

6.5. E/S FORMATTEES SUR FICHIERS

egale a 6 par defaut. Si la precision est 0, le point decimal n’apparait pas. Dans le cas du format E, la lettre E est im-primee a la place de e.

  • parami sera interprete comme un ottant et ecrit sous la forme:

-option pe . pf

dans laquelle pe et pf sont respectivement partie entiere et par-tie fractionnaire de la mantisse. La partie fractionnaire est exprimee avec un nombre de chi res egal a la precision. La precision est prise egale a 6 par defaut. Si la precision est 0, le point decimal n’apparait pas.

g,G

parami sera interprete comme un ottant et ecrit sous le format f ou le format e selon sa valeur. Si parami a un exposant inferieur a -4 ou plus grand que la precision, il sera imprime sous le format e, sinon il sera imprime sous le format f. Dans ce qui precede, l’utilisation du format G implique l’utilisation du format E a la place du format e.

Pour mettre le caractere % dans standard output, il faut ecrire %% dans le format.

6.5.2 Retour sur printf

La fonction printf vue precedemment est un fprintf sur standard output. Les formats admissibles pour printf sont donc rigoureusement les m^emes que ceux de fprintf.

 

68

 

CHAPTER 6. LES ENTREES SORTIES

6.5.3 Exemples d’utilisation des formats

source C

resultats

printf(“|%d|\n”,1234);

|1234|

printf(“|%-d|\n”,1234);

|1234|

printf(“|%+d|\n”,1234);

|+1234|

printf(“|% d|\n”,1234);

| 1234|

printf(“|%10d|\n”,1234);

|

1234|

printf(“|%10.6d|\n”,1234);

|

001234|

printf(“|%10.2d|\n”,1234);

|

1234|

printf(“|%.6d|\n”,1234);

|001234|

printf(“|%.2d|\n”,1234);

|1234|

printf(“|%*.6d|\n”,10,1234);

|

001234|

printf(“|%*.*d|\n”,10,6,1234);

|

001234|

printf(“|%x|\n”,0x56ab);

|56ab|

printf(“|%#x|\n”,0x56ab);

|0x56ab|

printf(“|%X|\n”,0x56ab);

|56AB|

printf(“|%#X|\n”,0x56ab);

|0X56AB|

printf(“|%f|\n”,1.234567890123456789e5);

|123456.789012|

printf(“|%.4f|\n”,1.234567890123456789e5);

|123456.7890|

printf(“|%.20f|\n”,1.234567890123456789e5);

|123456.78901234568000000000|

printf(“|%20.4f|\n”,1.234567890123456789e5);

|

123456.7890|

printf(“|%e|\n”,1.234567890123456789e5);

|1.234568e+05|

printf(“|%.4e|\n”,1.234567890123456789e5);

|1.2346e+05|

printf(“|%.20e|\n”,1.234567890123456789e5);

|1.23456789012345680000e+05|

printf(“|%20.4e|\n”,1.234567890123456789e5);

|

1.2346e+05|

printf(“|%.4g|\n”,1.234567890123456789e-5);

|1.235e-05|

printf(“|%.4g|\n”,1.234567890123456789e5);

|1.235e+05|

printf(“|%.4g|\n”,1.234567890123456789e-3);

|0.001235|

printf(“|%.8g|\n”,1.234567890123456789e5);

|123456.79|

6.5.4 Entrees formattees : fscanf

Nous presenterons les fonctionnalites qui sont communes a Unix System V et Unix BSD. La fonction fscanf admet un nombre variable de parametres. Son utilisation est la

suivante:

fscanf ( le-ptr , format , param1 , param2 , … , paramn )

dans lequel:

  1. le-ptr est une valeur de type pointeur vers FILE qui repere le chier a partir duquel on veut lire,
  2. format est une cha^ne de caracteres,
  3. les param i sont des pointeurs.
  4.  
  5. % *option
 

69

6.5. E/S FORMATTEES SUR FICHIERS

fscanf lit le chier de ni par le-ptr en l’interpretant selon format. Le resultat de cette interpretation a pour e et de mettre des valeurs dans les variables pointees par les di erents parami.

On de nit les termes suivants :

caracteres de separation il s’agit des caracteres blanc, horizontal tabulation, et line-feed.

unite (du ot d’entree): une cha^ne de caracteres ne contenant pas de caracteres de separation.

La chaine format contient :

  • des caracteres de separation qui, sauf dans les deux cas cites plus loin, font lire a

fscanf jusqu’au prochain caractere qui n’est pas un caractere de separation.

  • des caracteres ordinaires (autres que %) qui doivent co ncider avec les caracteres du ot d’entree.
  • des sequences d’escape introduites par le caractere %. Ces sequences speci ent le travail a e ectuer sur le ot d’entree.

Les sequences d’escape ont la syntaxe suivante :

nombreoption carac

  • la presence de ce signe indique que la prochaine unite du ot d’entree doit ^etre ignoree.

nombre

indique la longueur maximum de l’unite a reconna^tre.

carac

peut prendre l’une des valeurs suivantes :

  • la prochaine unite sera interpretee comme un nombre decimal.
  • la prochaine unite sera interpretee comme un nombre octal.
  • la prochaine unite sera interpretee comme un nombre hexadecimal

Dans les trois cas precedents parami est interprete comme un pointeur vers un int. De plus, carac peut ^etre precede de la lettre h ou l pour indiquer que parami n’est pas un pointeur vers un int, mais plut^ot un pointeur vers un short int, ou un pointeur vers un long int.

  • parami est interprete comme etant un pointeur vers un car-actere. Le prochain caractere du ot d’entree est mis dans le caractere pointe. Pour ce cas seulement, il n’y a plus de notion de caractere separateur. On obtient dans le caractere pointe le veritable caractere suivant du ot d’entree, m^eme s’il s’agit d’un caractere de separation. Si on desire ignorer les caracteres de separation, il faut utiliser la sequence d’escape %1c. Si la sequence d’escape comportait l’indication de longueur maxi-mum de l’unite a reconna^tre, c’est ce nombre de caracteres qui est lu.

70

 

CHAPTER 6. LES ENTREES SORTIES

  • parami est interprete comme un pointeur vers une cha^ne de caracteres. La prochaine unite du ot d’entree, terminee par un null est mise dans la cha^ne pointee.

e,f parami est interprete comme un pointeur vers un float. La

prochaine unite du ot d’entree doit avoir le format d’un nom-

bre ottant (m^eme format qu’une constante ottante du lan-

gage C). Ce nombre est a ecte au oat pointe. Les caracteres

e, ou f peuvent ^etre precedes du caractere l pour indiquer que

parami n’est pas un pointeur vers un float, mais un pointeur

vers un double.

  • Dans la cha^ne format, ce caractere introduit une sequence par-ticuliere destinee a de nir un scanset. la sequence est formee du caractere [, suivi d’une suite de caracteres quelconques, suivi du caractere ]. Si le premier caractere apres le crochet ou-vrant n’est pas le caractere ^, le scanset est l’ensemble des caracteres entre crochets. Si le caractere ^ est immediatement apres le crochet ouvrant, le scanset est l’ensemble des caracteres ne se trouvant pas dans la cha^ne entre crochets.

fscanf lit dans le chier repere par le-ptr jusqu’a la rencontre d’un caractere ne faisant pas partie du scanset. Comme dans le cas du format c, il n’y a plus de notion de caractere separateur, on ne considere que le scanset. parami est interprete comme un pointeur vers une suite de caracteres dans lesquels on met les caracteres lus.

Valeur retournee

En ce qui concerne la valeur retounee, il n’y a malheureusement pas compatibite entre System V et BSD. Dans le cas de n de chier, fscanf retourne bien EOF dans les deux versions d’Unix. Par contre, si fscanf n’a pas detecte une n de chier, il retourne le nombre d’unites qui ont ete a ectees dans le cas de System V, et le nombre d’unites illegales dans le cas de Unix BSD. Dans ce dernier cas, il est impossible de conna^tre le nombre d’unites qui ont etees a ectees.

ex:

(void)fscanf(fi,”%d %d”,&i,&j)

si le ot d’entree contient 12 34, les valeurs 12 et 34 seront a ectees respectivement a i et j.

6.6 Exercice

Soit un chier de donnees structure en une suite de lignes contenant chacune un nom de personne, un nom de piece, un nombre et un prix. ex:

dupond villebrequin 10 1000

ecrire une procedure main dans laquelle on declarera les variables suivantes:

– nom et article : tableaux de 80 caracteres

 

6.6. EXERCICE

71

– nombre et prix : entiers

le corps de la procedure consistera en une boucle dont chaque iteration lira une ligne et l’imprimera.

  • la lecture d’une ligne se fera par un appel a scanf a ectant les 4 champs de la ligne aux 4 variables nom, article, nombre et prix.
  • l’ecriture consistera a imprimer nom, article et le produit nombre prix.

72

 

CHAPTER 6. LES ENTREES SORTIES

#include “stdio.h”

/*****************************************************************************/

/* */ /* main */ /* */

/*****************************************************************************/ void main()

{

FILE * fi;

char nom[80];

char article[80];

int nombre,prix;

if ((fi = fopen(“exer6.data”,”r”)) == NULL) printf(“Impossible d’ouvrir le fichier exer6.data\n”);

else

{

while(fscanf(fi,”%s %s %d %d”,nom,article,&nombre,&prix) != EOF)

printf(“%s %s %d\n”,nom,article,nombre * prix);

fclose(fi);

}

}

 

Chapter 7

Structures et unions

7.1 Notion de structure

Il est habituel en programmation que l’on ait besoin d’un mecanisme permettant de grouper un certain nombre de variables de types di erents au sein d’une m^eme entite.

On travaille par exemple sur un chier de personnes, et on voudrait regrouper une variable de type cha^ne de caracteres pour le nom, une variable de type entier pour le numero de securite sociale, etc…

La reponse a ce besoin est la notion de structure.

7.2 Declaration de structure

Voyons sur un exemple:

struct personne

{

char nom[20];

char prenom[20];

int no_ss;

};

Cette declaration ne declare aucune variable. Elle declare simplement l’identi cateur personne comme etant le nom d’un type de structure composee de trois champs, dont le premier est un tableau de 20 caracteres nomme nom, le second un tableau de 20 caracteres nomme prenom, et le dernier un entier nomme no_ss.

On pourra par la suite declarer des variables d’un tel type de la maniere suivante:

struct personne p1,p2;

ceci declare deux variables de type struct personne de noms p1 et p2;

La methode qui vient d’^etre exposee est la methode recommandee, mais on peut utiliser des variantes.

On peut declarer des variables de type struct sans donner un nom au type de la structure, par exemple:

struct

73

 

74 CHAPTER 7. STRUCTURES ET UNIONS

{

char nom[20];

char prenom[20];

int no_ss;

}p1,p2;

declare deux variables de noms p1 et p2 comme etant deux structures de trois champs dont le premier est …, mais elle ne donne pas de nom au type de la structure. Ce qui a pour consequence que si, a un autre endroit du programme, on desire declarer une autre variable p du m^eme type, il faudra ecrire:

struct

{

char nom[20];

char prenom[20];

int no_ss;

}p;

Un autre variante consiste a donner a la fois un nom a la structure, et a declarer une ou plusieurs variables. Par exemple:

struct personne

{

char nom[20];

char prenom[20];

int no_ss;

}p1,p2;

declare les deux variables p1 et p2 et donne le nom personne a la structure.

La aussi, on pourra utiliser ulterieurement le nom struct personne pour declarer d’autres variables:

struct personne pers1,pers2,pers3;

La premiere methode est recommandee, car elle permet de bien separer la de nition du type structure de ses utilisations.

7.3 Acces aux champs des structures

Pour designer un champ d’une structure, il faut utiliser l’operateur de selection de champ qui se note . (point).

Par exemple, si p1 et p2 sont deux variables de type struct personne, on designera le champ nom de p1 par p1.nom, et on designera le champ no_ss de p2 par p2.no_ss. Les champs ainsi designes se comportent comme n’importe quelle variable, et par exemple, pour acceder au premier caractere du nom de p2, on ecrira: p2.nom[0].

 

7.4. TABLEAUX DE STRUCTURES

75

7.4 Tableaux de structures

Une declaration de tableau de structures se fait selon le m^eme modele que la declaration d’un tableau dont les elements sont de type simple.

Supposons que l’on ait deja declare la struct personne, si on veut declarer un tableau de 100 structures de ce type, on ecrira:

struct personne t[100];

Pour referencer le nom de la personne qui a l’index i dans t on ecrira: t[i].nom.

7.5 Exercice

Soit un chier de donnees identiques a celui de l’exercice precedent.

Ecrire une procedure main qui:

  1. lise le chier en memorisant son contenu dans un tableau de structures, chaque

structure permettant de memoriser le contenu d’une ligne (nom, article, nombre et prix).

  1. parcoure ensuite ce tableau en imprimant le contenu de chaque structure.

76 CHAPTER 7. STRUCTURES ET UNIONS

#include <stdio.h>

/*****************************************************************************/

/* main */ /*****************************************************************************/ void main()

{

FILE * fi;

struct commande

{

char nom[80];

char article[80];

int nombre,prix;

};

#define nb_com 100

struct commande tab_com[nb_com];

/*

tableau des commandes

*/

int i; /* int ilast;

index dans tab_com */

/* dernier index valide dans tab_com apres remplissage

*/

if ((fi = fopen(“exer7.data”,”r”)) == NULL) printf(“Impossible d’ouvrir le fichier exer7.data\n”);

else

{

/* boucle de lecture des commandes

/* ——————————-

*/

*/

i = 0;

while(i < nb_com && fscanf(fi,”%s %s %d %d”,

tab_com[i].nom,

tab_com[i].article,

&tab_com[i].nombre,

&tab_com[i].prix) != EOF)

i++; /* corps du while */

if (i >= nb_com)

printf(“le tableau tab_com est sous-dimentionne\n”); else

{

/* impression des commandes memorisees

/* ———————————–

ilast = i – 1;

*/

*/

for (i = 0; i <= ilast; i++)

printf(“%s %s %d %d\n”, tab_com[i].nom, tab_com[i].article, tab_com[i].nombre, tab_com[i].prix);

fclose(fi);

}

}

}

 

7.6. POINTEURS VERS UNE STRUCTURE

77

7.6 Pointeurs vers une structure

Supposons que l’on ait de ni la struct personne a l’aide de la declaration

struct personne

{

};

on declarera une variable de type pointeur vers une telle structure de la maniere suivante:

struct personne *p;

on pourra alors a ecter a p des adresses de struct personne.

exemple:

struct personne

{

};

void main()

{

struct personne pers; struct personne *p;

/*

/*

pers est une variable de type struct personne p est un pointeur vers une struct personne

*/

*/

p =

}

&pers;

7.7 Structures dont un des champs pointe vers une structure du meme type

Une des utilisations frequentes des structures, est de creer des listes de structures cha^nees. Pour cela, il faut que chaque structure contienne un champ qui soit de type pointeur vers une structure.

Cela se fait de la facon suivante:

struct personne

{

… /* les differents champs */

struct personne *suivant;

};

le champ de nom suivant est declare comme etant du type pointeur vers une struct personne.

 

78 CHAPTER 7. STRUCTURES ET UNIONS

7.8 Acces aux elements d’une structure pointee

Supposons que nous ayons declare p comme etant de type pointeur vers une struct personne, comment ecrire une reference a un champ de la structure pointee par p?.

Etant donne que *p designe la structure, on serait tente d’ecrire *p.nom pour referencer par exemple le champ nom.

Mais il faut savoir que les operateurs d’indirection (*) et de selection (.), tout comme les operateurs arithmetiques, ont une priorite.

Et il se trouve que l’indirection a une priorite inferieure a celle de la selection. Ce qui fait que *p.nom sera interprete comme signi ant *(p.nom). (Cela n’aurait de sens que si p etait une structure dont un des champs s’appelerait nom et serait un pointeur).

Dans notre cas, il faut ecrire (*p).nom pour forcer a ce que l’indirection se fasse avant la selection.

Etant donne que cette ecriture est assez lourde, le langage C a prevu un nouvel operateur note -> qui realise a la fois l’indirection et la selection, de facon a ce que

  • -> nom soit identique a (*p).nom.

exemple: si p est de type pointeur vers la struct personne de nie precedement, pour a ecter une valeur au champ no_ss de la structure pointee par p, on peut ecrire:

p -> no_ss = 1345678020;

7.9 Determination de la taille allouee a un type

Pour connaitre la taille en octets de l’espace memoire necessaire pour une variable, on dispose de l’operateur sizeof.

Cet operateur est un operateur unaire pre xe que l’on peut employer de deux manieres di erentes: soit sizeof expression soit sizeof ( nom-de-type )

ex:

int i,taille;

taille = sizeof i;

taille = sizeof (short int);

taille = sizeof (struct personne);

7.10 Allocation et liberation d’espace pour les structures

7.10.1 Allocation d’espace: fonctions malloc et calloc

Quand on cree une liste cha^nee, c’est parce qu’on ne sait pas a la compilation combien elle comportera d’elements a l’execution (sinon on utiliserait un tableau).

Pour pouvoir creer des listes, il est donc necessaire de pouvoir allouer de l’espace dynamiquement.

On dispose pour cela de deux fonctions malloc et calloc.

 

7.11. EXERCICE

79

Allocation d’un element: fonction malloc

La fonction malloc admet un parametre qui est la taille en octets de l’element desire, et elle rend un pointeur vers l’espace alloue. Utilisation typique:

struct personne *p;

p = malloc(sizeof(struct personne));

Allocation d’un tableau d’elements: fonction calloc

Elle admet deux parametres:

  1. le premier est le nombre d’elements desires.
  2. le second est la taille en octets d’un element

son but est d’allouer un espace su sant pour contenir les elements demandes et de rendre un pointeur vers cet espace.

Utilisation typique:

struct personne *p;

int nb_elem;

/* determination de nb_elem */

p = calloc(nb_elem,sizeof(struc personne));

On peut alors utiliser les elements p[0], p[1], … p[nb_elem-1].

7.10.2 Liberation d’espace: procedure free

On libere l’espace alloue par malloc ou calloc au moyen de la procedure free qui admet un seul parametre: un pointeur precedemment rendu par un appel a malloc ou calloc.

Utilisation typique:

struct personne *p;

p = malloc(sizeof(struct personne));

/* utilisation de la structure allouee */

free(p);

7.11 Exercice

Faire la meme chose que dans l’exercice precedent, mais en memorisant le chier de donnees, non pas dans un tableau de structures, mais dans une liste de structures chanees.

 

80 CHAPTER 7. STRUCTURES ET UNIONS

#include <stdio.h>

/*****************************************************************************/

/*

 

*/

/*

main

*/

/*

 

*/

/*

Remarque:

*/

/*

Le compilateur emet un avertissement au sujet de l’affectation

*/

/*

cour = malloc(…) mais le code genere est ok

*/

/*

 

*/

/*****************************************************************************/ void main()

{

FILE * fi;

struct commande

{

char nom[80];

char article[80];

int nombre,prix;

struct commande *suiv;

};

struct commande *l_com = NULL; struct commande *prec,*cour; int val_ret;

/* /* /*

liste des commandes */

pour la commande precedente et courante

valeur de retour de fscanf

*/

*/

/* open du fichier */

/* ————— */

if ((fi = fopen(“exer7.data”,”r”)) == NULL)

printf(“Impossible d’ouvrir le fichier exer7.data\n”);

else

{

/* lecture du fichier avec creation de la liste chainee

/* —————————————————-

do

*/

*/

{

cour = malloc(sizeof(struct commande));

val_ret = fscanf(fi,”%s %s %d %d”,

cour -> nom,

cour -> article,

&(cour -> nombre),

&(cour -> prix));

if (val_ret == EOF)

{

free(cour);

if(l_com != NULL) prec -> suiv = NULL;

}

else

{

if (l_com == NULL) l_com = cour; else prec -> suiv = cour;

prec = cour;

}

}

7.11. EXERCICE

while (val_ret != EOF);

/* parcours de la liste avec impression */

/* ———————————— */

if (l_com == NULL)

printf(“La liste de commandes est vide\n”);

else

{

for (cour = l_com; cour != NULL; cour = cour -> suiv)

printf(“%s %s %d %d\n”,

cour -> nom,cour -> article,cour -> nombre,cour -> prix);

}

/* fermeture du fichier

/* ——————–

fclose(fi);

}

*/

*/

}

 

82 CHAPTER 7. STRUCTURES ET UNIONS

7.12 Passage de structures en parametre

Le langage C a commme contrainte d’interdire de passer une structure en parametre a une procedure ou une fonction, mais rien par contre, n’emp^eche de passer en parametre un pointeur vers une structure.

Voyons sur un exemple:

Supposons que l’on ait fait la declaration suivante:

struct date

{

int jour,mois,annee;

}

une fonction de comparaison de deux dates pourra s’ecrire:

#define AVANT 1

#define IDEM 2

#define APRES 3

int cmp_date(pd1,pd2)

struct date *pd1,*pd2;

{

if (pd1 -> annee > pd2 -> annee)

return(APRES);

else if (pd1 -> annee < pd2 -> annee)

return(AVANT);

else

{

/* comparaison portant sur mois et jour */

}

}

Une utilisation de cette fonction pourra ^etre:

struct date d1,d2;

if (cmp_date(&d1,&d2) == AVANT)

7.13 Structures retournees par une fonction

Il existe la meme contrainte concernant les valeurs retournees par une fonction que pour les parametres, a savoir qu’une fonction ne peut pas retourner une structure, mais peut retourner un pointeur vers une structure.

Avec l’exemple des struct date, on peut de nir une fonction retournant un pointeur reperant une struct date initialisee au premier janvier de l’an 2000 de la facon suivante:

struct date *f()

{

/* f fonction retournant un pointeur vers une struct date */

 

   

83

7.14. DECLARATION DE PROCEDURES / FONCTIONS EXTERNES

struct date *p;

   

p = malloc(sizeof(struct

date));

 

p -> jour = 1; p -> mois = 1; p-> annee = 2000; return(p);

}

7.14 Declaration de procedures / fonctions externes

Lorsque dans les exemples precedents, on a ecrit des choses du genre:

struct date *p;

p = malloc(sizeof(struct date));

on s’est apercu, lors de la compilation, qu’il y avait emission d’un avertissement con-cernant l’instruction p = malloc(…).

Cet avertissement precisait que le type de la valeur a ectee n’etait pas compatible avec celui de la variable a laquelle on l’a ectait.

Le code genere etait parfaitement correct, mais un tel message est toujours inquietant, et il vaut toujours mieux programmer de maniere a le faire disparaitre.

Pour comprendre la raison de ce message il faut savoir 2 choses:

  1. Les procedures et fonctions printf, scanf, malloc, free etc… ne sont pas en C, contrairement a ce qu’elles sont dans la majorite des autres langages, des procedures connues du langage. Ce sont des procedures et fonctions externes, indiscernables du point de vue du langage, des autres procedures et fonctions ecrites par l’utilisateur.
  1. Lorsqu’on rencontre dans un source C une occurence d’une utilisation d’une fonction avant sa de nition, le langage considere par defaut que cette fonction retourne un int. Si tel est bien le cas, il n’y a pas de probleme, sinon, il sera necessaire de declarer cette fonction de maniere a informer le compilateur du type des valeurs retournees par la fonction.

Dans notre exemple, dans la partie des declarations externes a toutes procedures et fonctions, on ecrirait:

extern struct date *malloc();

ce qui declarerait la fonction malloc comme retournant un pointeur vers une struct date et ne provoquerait plus d’emission de message d’avertissement lorsqu’on affecterait ulterieurement le resultat de malloc() a une variable de type pointeur vers une struct date.

7.15 Exercice

Modi er le programme precedent:

84 CHAPTER 7. STRUCTURES ET UNIONS

  1. en ecrivant une procedure d’impression d’une struct commande, procedure qui ad-mettra en parametre un pointeur vers une telle structure.
  2. en ecrivant une fonction de recherche de commande maximum (celle pour laquelle le produit nombre prix est maximum). Cette fonction admettra en parametre un pointeur vers la struct commande qui est t^ete de la liste complete, et rendra un pointeur vers la structure recherchee.
  3. le main sera modi e de maniere a faire appel a la fonction de recherche de la com-mande maximum et a imprimer cette commande.

7.15. EXERCICE

#include <stdio.h>

/* les types communs a toutes les procedures

/* —————————————–

struct commande

{

char nom[80];

char article[80];

int nombre,prix;

struct commande *suiv;

};

*/

*/

/* les procedures / fonctions externes /* ———————————–

*/

*/

extern struct commande *malloc();

/*****************************************************************************/

/*

 

*/

/*

print_com

*/

/*

 

*/

/*

But:

*/

/*

Imprime une structure commande

*/

/*

 

*/

/*****************************************************************************/ void print_com(com)

struct commande *com;

{

printf(“%s %s %d %d\n”,

com -> nom,com -> article,com -> nombre,com -> prix);

}

 

86 CHAPTER 7. STRUCTURES ET UNIONS

/*****************************************************************************/

/*

 

*/

/*

max_com

*/

/*

 

*/

/*

But:

*/

/*

Recherche la commande pour laquelle le produit nombre * prix est

*/

/*

le maximun

*/

/*

 

*/

/*

Interface:

*/

/*

l_com : la liste dans laquelle doit se faire la recherche

*/

/*

valeur rendue : pointeur vers la structure commande recherchee

*/

/*

ou NULL si l_com est vide

*/

/*

 

*/

/*****************************************************************************/

struct commande *max_com(l_com)

struct commande *l_com;

{

struct commande *pmax; struct commande *cour;

/*

/*

pointeur vers le max courant */ pointeur vers l’element courant

*/

int vmax,vcour;

if (l_com == NULL)

return(NULL);

else

{

pmax = l_com; vmax = (pmax -> nombre) * (pmax -> prix);

for (cour = l_com -> suiv; cour != NULL; cour = cour ->suiv)

{

vcour = (cour -> nombre * cour -> prix);

if (vcour > vmax)

{

vmax = vcour;

pmax = cour;

}

}

return(pmax);

}

}

 

7.15. EXERCICE

87

/*****************************************************************************/

/* */ /* main */ /* */

/*****************************************************************************/ void main()

{

FILE * fi;

struct commande *l_com = NULL; /* liste des commandes */

struct commande *prec,*cour; /* pour la commande precedente et courante */

int val_ret; /* valeur de retour de fscanf */

if ((fi = fopen(“exer7.data”,”r”)) == NULL)

printf(“Impossible d’ouvrir le fichier exer7.data\n”);

else

{

/* lecture du fichier avec creation de la liste de commandes

/* ———————————————————

do

*/

*/

{

cour = malloc(sizeof(struct commande));

val_ret = fscanf(fi,”%s %s %d %d”,

cour -> nom,

cour -> article,

&(cour -> nombre),

&(cour -> prix));

if (val_ret == EOF)

{

free(cour);

if(l_com != NULL) prec -> suiv = NULL;

}

else

{

if (l_com == NULL) l_com = cour; else prec -> suiv = cour; prec = cour;

}

}

while (val_ret != EOF);

/* parcours de la liste avec impression /* ———————————— if (l_com == NULL)

*/

*/

printf(“La liste de commandes est vide\n”); else

{

for (cour = l_com; cour != NULL; cour = cour -> suiv)

print_com(cour);

/* recherche et impression de la commande maximum /* ———————————————- printf(“La commande maximum est :\n”); print_com(max_com(l_com)); }

*/

*/

 

88

CHAPTER 7. STRUCTURES ET UNIONS

/* fermeture du fichier */

/* ——————– */

fclose(fi);

}

}

 

7.16. LES CHAMPS DE BITS

89

7.16 Les champs de bits

7.16.1 Generalites

Il est parfois necessaire pour le programmeur de decrire la structure d’un mot machine, cela pour plusieurs raisons:

un mot de l’espace memoire est un registre de coupleur decoupe en di erents champs de bits.

pour des raisons de gain de place, on desire faire c xister plusieurs variables a l’interieur d’un entier.

Il existe dans le langage C un moyen de realiser cela, a l’aide du concept de structure. En e et, dans une declaration de structure, il est possible de faire suivre la de nition d’un champ par une indication du nombre de bits que doit avoir ce champ.

Voyons sur un exemple:

struct etat

{

unsigned int sexe : 1;

unsigned int situation : 3;

unsigned int invalide : 1;

};

La struct etat voit son premier champ implemente sur 1 bit, son second sur 3 bits et le dernier sur 1 bit.

7.16.2 Contraintes

  1. un champ de bits ne peut pas ^etre d’une taille superieure a celle d’un int.
  2. un champ de bits ne peut pas ^etre a cheval sur deux int.
  3. l’ordre dans lequel sont mis les champs de bits a l’interieur d’un mot depend du compilateur. Si on utilise les champs de bits pour gagner de la place, cela n’est pas genant. Dans le cas ou on les utilise pour decrire une ressource materielle de la machine (registre de coupleur, mot d’etat programme etc..) il est bien s^ur necessaire de conna^tre le comportement du compilateur.
  1. un champ de bit declare comme etant de type int, peut en fait se comporter comme un int ou comme un unsigned int (cela depend du compilateur). Il est donc recommande d’une maniere generale de declarer les champs de bits comme etant de type unsigned int.
  1. un champ de bits n’a pas d’adresse, on ne peut donc pas lui appliquer l’operateur adresse de (&).

90 CHAPTER 7. STRUCTURES ET UNIONS

7.17 Les unions

Il est parfois necessaire de manipuler des variables auxquelles on desire a ecter des valeurs de type di erents. Supposons que l’on desire ecrire un package mathematique qui manip-ulera des nombres qui seront implementes par des int, tant que la precision des entiers de la machine sera su sante, et qui passera automatiquement a une representation sous forme de reels des que ce ne sera plus le cas. Il sera necessaire de disposer de variables pouvant prendre soit des valeurs entieres, soit des valeurs reelles.

Ceci peut se realiser en C, gr^ace au mecanisme des unions. Une de nition d’union a la meme syntaxe qu’une de nition de structure, le mot cle struct etant simplememt remplace par le mot cle union.

ex:

union nombre

{

int i;

float f;

}

La di erence semantique entre les struct et les unions est la suivante: alors que pour une variable de type structure tous les champs peuvent avoir en meme temps une valeur, une variable de type union ne peut avoir, a un instant donne, qu’un seul champ ayant une valeur.

Dans l’exemple precedent, si on declare la variable n comme etant de type union nombre par:

union nombre n;

cette variable pourra posseder soit une valeur entiere, soit une valeur reelle, mais pas les deux a la fois.

7.18 Acces aux champs de l’union

Cet acces se fait avec le m^eme operateur selection (note .) que celui qui sert a acceder aux champs des structures.

Dans l’exemple precedent, si on desire faire posseder a la variable n une valeur entiere, on pourra ecrire:

n.i = 10;

si on desire lui faire posseder une valeur reelle, on pourra ecrire:

n.f = 3.14159;

7.19 Utilisation pratique des unions

Lorsqu’il manipule des variables de type union, le programmeur n’a malheureusement aucun moyen de savoir a un instant donne, quel est le champ de l’union qui possede une valeur.

 

     

91

7.20. UNE METHODE POUR ALLEGER L’ACCES AUX CHAMPS

Pour etre utilisable, une union doit donc toujours ^etre associee a une variable dont le but sera d’indiquer le champ de l’union qui est valide. En pratique, une union et son indicateur sont generalement englobes a l’interieur d’une structure.

Dans l’exemple precedent, on procederait de la maniere suivante:

#define ENTIER 0

#define REEL 1

struct arith

{

int typ_val;

/*

peut prendre les valeurs ENTIER ou REEL

*/

union

{

int i;

float f;

} u;

};

la struct arith a deux champs typ_val de type int, et u de type union d’int et de float.

on declarerait des variables par:

struct arith a1,a2;

puis on pourrait les utiliser de la maniere suivante:

a1.typ_val = ENTIER;

a1.u.i = 10;

a2.typ_val = REEL;

a2.u.f = 3.14159;

Si on passait en parametre a une procedure un pointeur vers une struct arith, la procedure testerait la valeur du champ typ_val pour savoir si l’union recue possede un entier ou un reel.

7.20 Une methode pour alleger l’acces aux champs

Quand une union est dans une structure, il faut donner un nom au champ de la structure qui est de type union, ce qui a pour consequence de rendre assez lourd l’acces aux champs de l’union.

Dans l’exemple precedent, il faut ecrire a1.u.f pour acceder au champ f.

On peut alleger l’ecriture en utilisant les facilites du pre-processeur. On peut ecrire par exemple:

#define I u.i

#define F u.f

Pour initialiser a1 avec l’entier 10, on ecrira alors:

 

92 CHAPTER 7. STRUCTURES ET UNIONS

a1.typ_val = ENTIER;

a1.I = 10;

 

Chapter 8

Les expressions

Avant de passer a l’etude de la semantique des expressions, nous allons presenter les quelques operateurs non encore vus jusqu’a present.

8.1 Les operateurs

8.1.1 Operateur non bit a bit

Syntaxe: expression :

) ~ expression

Semantique:

expression est evaluee et doit delivrer une valeur de type entier, l’operation non bit a bit est realisee sur cette valeur, et le resultat obtenu est la valeur de l’expression

~.

8.1.2 Operateur et bit a bit

Syntaxe: expression :

) expression1 & expression2

Semantique:

Les deux expressions sont evaluees et doivent delivrer des valeurs de type entier, le et bit a bit est realise, et la valeur obtenue est la valeur de l’expression &.

8.1.3 Operateur ou bit a bit

Syntaxe:

expression :

) expression1 | expression2

Semantique:

93

 

94 CHAPTER 8. LES EXPRESSIONS

Les deux expressions sont evaluees et doivent delivrer des valeurs de type entier, le ou bit a bit est realise, et la valeur obtenue est la valeur de l’expression |.

8.1.4 Operateur decalage a gauche

Syntaxe: expression :

) expression1 << expression2

Semantique:

Les deux expressions sont evaluees et doivent delivrer des valeurs de type entier, la valeur de expression1 est decalee a gauche de expression2 bits en remplissant les bits

libres avec des zeros. Le resultat obtenu est la valeur de l’expression <<.

8.1.5 Operateur decalage a droite

Syntaxe: expression :

) expression1 >> expression2

Semantique:

Les deux expressions sont evaluees et doivent delivrer des valeurs de type entier, la valeur de expression1 est decalee a droite de expression2 bits. Si expression1 delivre une valeur unsigned, le decalage est un decalage logique: les bits liberes sont remplis avec des zeros. Sinon, le decalage peut ^etre logique ou arithmetique (les bits liberes sont remplis avec le bit de signe), cela depend de l’implementation.

8.1.6 Operateur conditionnel

Syntaxe: expression :

) expression1 ? expression2 : expression3

Semantique:

expression1 est evaluee et doit delivrer une valeur de type entier. Si cette valeur est:

{ non nulle, expression2 est evaluee et le resultat est la valeur de l’expression conditionnelle.

{ nulle, expression3 est evaluee et le resultat est la valeur de l’expression condi-tionnelle.

8.1.7 Operateur virgule

Syntaxe:

expression :

) expression1 , expression2

 

8.2. OPERATEUR FORCEUR

Semantique:

expression1 est evaluee et sa valeur ignoree. expression2 la valeur de l’expression virgule.

Remarque

est evaluee et sa valeur est

Etant donne que la valeur de expression1 est ignoree, pour qu’une telle construction ait un sens, il faut que expression1 fasse un e et de bord.

On peut ecrire par exemple:

i = (j = 2 , 1);

ce qui est une maniere particulierement horrible d’ecrire:

i = 1;

j = 2;

Une utilisation agreable par contre de l’operateur virgule est dans les expressions d’une boucle for. Si on desire ecrire une boucle for qui utilise deux index, il est utile d’ecrire par exemple:

for (i = 1, j = 1; i <= LIMITE; i++, j = j + 2)

{

}

ceci permet de rendre manifeste que i = 1 et j = 1 sont la partie initialisation et i++

et j = j + 2 sont la partie iteration de la boucle.

8.1.8 Operateurs d’a ectation composee

Chacun des operateurs + – * / % >> << & ^ | peut s’associer a l’operateur d’a ectation

pour former respectivement les operateurs += -= *= /= %= >>= <<= &= ^= |=.

Nous donnerons la syntaxe et la semantique de ces operateurs dans le cas de l’operateur +=, celles des autres s’en deduit immediatement.

Syntaxe: expression :

) lvalue += expression

Semantique:

lvalue = lvalue + expression

8.2 Operateur forceur

Syntaxe:

expression :

) ( type ) expression

Semantique: expression est evaluee et convertie dans le type indique par type.

 

96 CHAPTER 8. LES EXPRESSIONS

Exemples d’utilisation

  1. conversion lors d’un passage de parametre.

Si une procedure p a un parametre de type long int, et si on desire lui passer la valeur possedee par la variable i de type int, il ne faut pas ecrire p(i) mais p((long int) i).

  1. utilisation de malloc

Supposons que nous ayons plusieurs types de structures a allouer par malloc. Nous pouvons de nir malloc de la maniere suivante:

extern char * malloc();

et lors de l’appel de malloc utiliser un forceur pour qu’il n’y ait pas d’emission d’avertissement.

p1 = (struct s1 *) malloc(sizeof(struct s1));

p2 = (struct s2 *) malloc(sizeof(struct s2));

8.3 Semantique des expressions

8.3.1 Operateurs d’adressage

Dans le langage C, les constructions suivantes:

()

pour l’appel de procedure

[]

pour l’indexation

  • pour l’indirection
    • pour la selection de champ -> pour l’indirection et selection
  • pour delivrer l’adresse d’un objet sont des operateurs a part entiere. Cela signi e que ces operateurs, que l’on peut appeler operateurs d’adressage, ont une priorite, et sont en concurrence avec les autres operateurs pour determiner la semantique d’une expression. Par exemple, la semantique de l’expression *p++ ne peut se determiner que si l’on connait les priorites relatives des operateurs * et ++.

8.3.2 Priorite et associativite des operateurs

Pour determiner la semantique d’une expression il faut non seulement conna^tre la pri-orite des operateurs mais egalement leur associativite. En e et, seule la connaissance de l’associativite de l’operateur == permet de savoir si a == b == c signi e (a == b) == c ou si elle signi e a == (b == c).

Un operateur a une associativite a droite quand:

a

a

op b op c signi e a op ( b op c). Un operateur a une associativite a gauche quand: op b op c signi e (a op b) op c.

 

Associativite

 

97

8.3. SEMANTIQUE DES EXPRESSIONS

Nous donnons ci-dessous le tableau exhaustif des operateurs avec leurs priorites et leurs associativite. priorite Operateur

  1. () [] -> .
  2. ++ ab

14

!

~

++ c

d

e

forceur

* f

& g

sizeof

 

13

* h

/

%

           

G

12

+

             

G

11

<<

>>

             

G

10

<

<=

>

>=

         

G

9

==

!=

             

G

8

& i

               

G

7

^

               

G

6

|

               

G

5

&&

               

G

4

||

               

G

3

?:

               

D

2

=

+=

-=

*=

/= %=

>>=

<<=

&=

^= |=

D

1

,

               

G

  • post xe bpost xe cpre xe dpre xe eunaire

f indirection gadresse de

hmultiplication iet bit bit

Attention

Le choix fait pour les priorites des operateurs concernant les operations bits a bits est assez

desastreux, en ce sens que a & b == c signi e a & (b == c) ce qui est rarement ce que veut le programmeur ! Attention donc a parentheser convenablement les sous-expressions.

8.3.3 Ordre d’evaluation des operandes

A part quelques exceptions, l’ordre d’evaluation des operandes d’un operateur n’est pas speci e par le langage. Ceci a pour consequence que le programmeur doit faire extr^emement attention aux e ets de bords dans les expressions. Par exemple, l’instruction:

t[i] = f();

ou la fonction f modi e la valeur de i a un comportement indetermine: il est impossible de savoir si la valeur prise pour indexer t sera celle de i avant ou apres l’appel a f.

 

98 CHAPTER 8. LES EXPRESSIONS

 

Chapter 9

Les declarations

Nous n’avons vu jusqu’a present que des exemples de declarations, il est temps maintenant de voir les declarations de maniere plus formelle.

9.1 Portee des declarations

Il existe en C quatre types de portees possibles pour les declarations:

Un identi cateur declare a l’exterieur de toute fonction, a une portee qui s’etend de son point de declaration jusqu’a la n du source.

Un parametre formel de fonction a une portee qui s’etend de son point de declaration jusqu’a la n de l’instruction composee formant le corps de la fonction;

Un identi cateur declare dans une instruction composee a une portee qui s’etend du point de declaration jusqu’a la n de l’instruction composee.

Une etiquette d’instruction a une portee qui comprend tout le corps de la fonction dans laquelle elle apparait.

ex:

int i;

/*

declaration a l’exterieur de toute fonction

*/

void proc1(j)

/*

debut de proc1

 

*/

int j;

/*

parametre de

la procedure

proc1

*/

{

         

/*

instructions

1

 

*/

k:

         

if (…)

         

{

         

int l;

/*

declaration a

l’interieur d’une instruction composee

*/

/*

instructions

2

 

*/

}

         

99

 

100

     
   

CHAPTER 9. LES DECLARATIONS

}

/*

fin de proc1

*/

int func1()

/*

debut de func1

*/

{

     

/*

instructions 3

*/

}

/*

fin de func1

*/

Dans cet exemple,

  • pourra ^etre reference par instructions1, instructions2 et instructions3 , j pourra ^etre reference par instructions1 et instructions2,

k pourra ^etre reference par instructions1 et instructions2, l pourra ^etre reference par instructions2.

9.2 Visibilite des identi cateurs

Dans le langage C, l’embo^tement des instructions composees forme une structure classique de blocs, c’est a dire que les declarations d’une instruction composee englobee cachent les declarations des instructions composees englobantes ayant le m^eme nom. De surcroit, les declarations d’une instruction composee cachent les declarations de meme nom, qui sont a l’exterieur de toute fonction.

ex:

int i;

int j;

void proc1()

{

int i;

/*

cache le i precedent

*/

int k;

if (a > b)

{

int i;

int j;

int k;

/*

/*

/*

cache le i precedent cache le j precedent cache le k precedent

*/

*/

*/

}

}

9.3 Les espaces de noms

9.3.1 Position du probleme

Il existe certaines situations ou l’on peut accepter que le m^eme nom designe plusieurs objets di erents, parce que le contexte d’utilisation du nom permet de determiner quel est l’objet reference.

Considerons l’exemple suivant:

 

9.3. LES ESPACES DE NOMS

101

struct st1

{

int i;

int j;

};

struct st2

{

int i;

double d;

};

void main()

{

struct st1 s1; struct st2 s2;

/*

/*

declaration de la variable s1 declaration de la variable s2

*/

*/

s1.i = s2.i;

}

Dans l’instruction s1.i = s2.i, il y a deux occurrence du nom i, la premiere designe le i de s1, et la seconde designe le i de s2. On voit que le contexte d’utilisation de i a permis de determiner a chaque fois de quel i il s’agit. On dit alors que le i de s1 et le i de s2 appartiennent a des espaces de noms di erents.

9.3.2 Les espaces de noms du langage C

Il y a trois espaces de noms dans le langage:

Les etiquettes de structures ou unions. Ce sont les identi cateurs qui suivent immediatement les mots-cles struct et union.

ex:

struct date

{

int jour,mois,annee; } d1,d2;

ici, date est une etiquette de structure, d1 et d2 etant des noms de variable de type struct date.

Les noms de champs de structures ou unions. Il y a un espace de nom pour chaque structure et chaque union. Ceci permet donc d’avoir des noms de champs identiques dans des structures ou unions di erentes. Un nom de champ est toujours suivi soit de l’operateur \selection” (.), soit de l’operateur \indirection et selection” (->).

Le dernier espace est forme de tous les autres noms.

Nous donnons ci-dessous un exemple ou le m^eme identi cateur i est utilise de maniere valide dans 4 espaces de noms di erents.

 

102

   

CHAPTER 9.

 
   

LES DECLARATIONS

int i;

 

/*

i est un nom didentificateur

*/

struct

i

/*

i est une etiquette de structure

*/

{

       

int

i;

/*

i est un champ de la struct i

*/

int

j;

     

}i1,i2;

     

struct

ii

     

{

       

int

i;

/*

i est un champ de la struct ii

*/

int

j;

     

}ii1,ii2;

void main()

{

i = 1;

i1.i = 2;

ii1.i = 3;

}

Remarques

  1. Certains compilateurs considerent qu’il existe 4 classes de noms en faisant une classe parti-culiere pour les noms d’etiquettes d’instructions. Ceci est la strategie adoptee par le compi-lateur UNIX SystemV. Cependant, si on s’en tient a trois espaces de noms, le programmeur est s^ur d’ecrire des programmes portables entre UNIX System V et UNIX Berkeley. C’est la raison pour laquelle nous avons adopte cette vision des choses.
  2. Certains auteurs considerent egalement qu’il existe un espace de noms pour les noms de nis a l’aide de la commande #define du macro-processeur. Nous avons refuse cette vision des choses dans la mesure ou pendant la phase de compilation proprement dite, ces noms n’ont plus d’existence.

9.4 Duree de vie

Du point de vue de la duree de vie, il existe deux types de variables: les variables statiques et le variables automatiques.

les variables statiques sont allouees au debut de l’execution du programme, et ne sont liberees qu’a la n de l’execution du programme.

les variables automatiques sont allouees a l’ entree dans une instruction composee, et liberees lors de la sortie de l’instruction composee.

On pourrait considerer qu’il existe un troisieme type de variables, les variables dynamiques, dont la creation et la destruction sont explicitement realisees par le programmeur a l’aide des fonc-tions malloc et free. Nous n’avons cependant pas retenu cette vision des choses, dans la mesure ou ces deux fonctions font partie de la librairie standard et non pas du langage stricto sensu.

Dans les langages de programmation, il y a generalement un lien etroit entre la portee de la declaration d’une variable et sa duree de vie. Il est classique en e et, qu’une variable globale (c’est a dire dont la declaration se trouve a l’exterieur de toute fonction), soit une variable statique, et qu’une variable locale a une procedure ou fonction, soit une variable automatique.

Dans le langage C, le programmeur a davantage de liberte. Les variables globales sont ici aussi des variables statiques, mais les variables locales peuvent ^etre au choix du programmeur statiques ou automatiques. Si la declaration d’une variable locale est precedee du mot cle static, cette variable sera statique, si elle est precedee du mot-cle auto, elle sera automatique, et en l’absence de l’un et l’autre de ces mots-cles, elle sera prise par defaut de type automatique.

Discussion

Cette liberte de donner a une variable locale une duree de vie egale a celle du programme, permet de resoudre des problemes du type suivant.

Imaginons une procedure qui pour une raison quelconque doit conna^tre combien de fois elle a etee appelee. Le programmeur a besoin d’une variable dont la duree de vie est superieure a celle de la procedure concernee, ce sera donc une variable statique. Cependant cette variable doit etre^ une variable privee de la procedure, (il n’y a aucune raison qu’une autre procedure puisse en modi er la valeur), il faut donc que ce soit une variable locale a la procedure.

En C, on programmera de la maniere suivante:

ex:

void proc()

{

static int nb_appel = 0;

nb_appel++;

}

Dans d’autres langages, de maniere a satisfaire les contraintes de duree de vie, on aurait ete oblige de faire de la variable nb_appel, une variable globale, la rendant ainsi accessible aux autres procedures, ce qui est tout a fait illogique.

9.5 Classes de memoire

9.5.1 Position du probleme

Lors de l’execution d’un programme ecrit en C il y a trois zones de memoire di erentes:

La zone contenant les variables statiques.

la zone contenant les variables automatiques. Cette zone est geree en pile puisqu’en C, toute fonction peut ^etre recursive.

La zone contenant les variables dynamiques. Cette zone est generalement appelee le tas.

Un tel decoupage se rencontre couramment dans les langages de programmation.

Generalement cependant, il n’est pas necessaire au programmeur de declarer la classe dans laquelle il desire mettre une variable, cette classe etant choisie de maniere autoritaire par le langage.

Les concepteurs du langage C ont voulu o rir plus de souplesse aux programmeurs. En e et, nous venons de voir que l’on pouvait mettre dans la classe des variables statiques une variable qui etait locale a une instruction composee. D’autre part, pour des raisons d’e cacite des programmes generes, les concepteurs du langage ont cree une nouvelle classe: la classe register. Quand le programmeur declare par exemple:

register int i;

ceci est une indication au compilateur, lui permettant d’allouer la variable dans une ressource de la machine dont l’acces sera plus rapide que l’acces a une memoire (si une telle ressource existe).

9.5.2 Les indicateurs de classe de memoire

Il existe 4 mots-cles du langage que la grammaire nomme indicateurs de classe de memoire.

Il s’agit des mots-cles suivants:

auto Cet indicateur de classe memoire n’est autorise que pour les variables locales a une instruction composee. Il indique que la variable concernee a une duree de vie locale a l’instruction composee.

ex:

{

auto int i;

}

static Cet indicateur de classe memoire est autorise pour les declarations de variables et de fonctions. Pour les declarations de variables, il indique que la variable concernee a une duree de vie globale. Dans tous les cas, (variables et fonctions), il indique que le nom concerne ne doit pas ^etre exporte par l’editeur de liens .

ex:

static int i;

/*

i ne sera pas exporte par l’editeur de liens

*/

int j;

/*

j sera exporte par l’editeur de liens

*/

static void f()

/*

f ne sera pas exporte par l’editeur de liens

*/

{

     

static int k;

/*

k aura une duree de vie globale

*/

     

}

     

void g()

/*

g sera exportee par l’editeur de liens

*/

{

     

     

}

     
 

105

9.6. SYNTAXE DES DECLARATIONS

register Cet indicateur n’est autorise que pour les declarations de variables locales a une instruction composee, et pour les declarations de parametres de fonctions. Sa signi cation est celle de auto avec en plus la latitude pour le compilateur d’allouer pour la variable une ressource a acces rapide.

extern Cet indicateur est autorise pour les declarations de variables et de fonctions. Il sert a indiquer que l’objet concerne a une duree de vie globale et que son nom est connu de l’editeur de liens.

9.5.3 Indicateur de classe memoire par defaut

La grammaire autorise l’omission de l’indicateur de classe memoire lorsqu’on fait une declaration. Un indicateur est alors pris par defaut selon les regles suivantes:

  1. extern est pris par defaut pour toutes les declarations de fonctions et toutes les

declarations de variables externes a toute fonction.

  1. auto est pris par defaut pour les declarations de variables locales a une instruction composee.
  2. extern est pris par defaut pour toute declaration de fonction interne a une instruc-tion composee.

Discussion

Que le lecteur ne s’inquiete pas si il a du mal a avoir en premiere lecture des idees nettes sur cette notion de indicateurs de classe memoire, car ce n’est pas une chose tres heureuse dans le langage. On peut faire les critiques suivantes:

  1. auto ne peut servir que pour les variables locales, mais si on ne le met pas, il est pris par defaut. Conclusion: il ne sert a rien.
  2. static sert a deux choses tres di erentes: il permet de rendre statique une variable

locale, et la il n’y a rien a dire, par contre, il sert aussi a cacher a l’editeur de liens les variables globales. On rappelle que par defaut (c’est a dire sans le mot-cle static,) les variables globales sont connues de l’editeur de liens. Il aurait mieux valu faire l’inverse, c’est a dire que par defaut les variables globales soient cachees a l’editeur de liens, et avoir un mot-cle (par exemple export), pour les lui faire connaitre.

  1. extern n’a rien a voir avec un quelconque probleme de classe memoire, il sert a importer le nom d’une variable.

9.6 Syntaxe des declarations

La grammaire des declarations est la suivante:

declaration :

  • indicateur-de-classeoption indicateur-de-type liste-de-declareur-init ;

indicateur-de-classe :

  • auto
  • extern
  • static
  • register

indicateur-de-type :

  • char
  • unsigned char
  • short int
  • int
  • long int
  • unsigned short int
  • unsigned int
  • unsigned long int
  • indicateur-de-struct-ou-union

indicateur-de-struct-ou-union :

) struct

) struct

{ liste-de-struct-decl }

identi cateur { liste-de-struct-decl

}

  • struct identi cateur

) union

) union

{ liste-de-struct-decl }

identi cateur { liste-de-struct-decl

}

  • union identi cateur

liste-de-struct-decl :

  • decl-de-struct
  • decl-de-struct liste-de-struct-decl

decl-de-struct :

) indicateur-de-type liste-de-declareur-de-struct

liste-de-declareur-de-struct :

  • declareur-de-struct
  • declareur-de-struct , liste-de-declareur-de-struct ;

declareur-de-struct :

  • declareur
  • declareur : expression-constante
  • : expression-constante

liste-de-declareur-init :

  • declareur-init
  • declareur-init , liste-de-declareur-init

declareur-init :

) declareur initialiseur option

declareur :

  • identi cateur
  • * declareur
  • declareur ( )

) declareur [ expression-constanteoption

) ( declareur )

]

initialiseur :

  • = expression

) = { liste-d’initialiseur }

  • = { liste-d’initialiseur , }

liste-d’initialiseur :

  • expression
  • liste-d’initialiseur , liste-d’initialiseur
  • { liste-d’initialiseur }

Exemples

Dans la declaration

int i,j = 2;

int est un indicateur-de-type et i,j = 2 est une liste-de-declareur-init composee de deux declareur-init: i et j = 2. i est un declareur-init sans la partie initialiseur, donc reduit a un declareur lui-m^eme reduit a un identi cateur. j = 2 est un declareur-init comportant un initialiseur (= 2) et un declareur reduit a un identi cateur (j).

Dans la declaration

int t[10];

t[10] est un declareur forme d’un declareur (t), suivi de [ suivi de l’expression constante

10, suivi de ].

9.7 Semantique des declarations

La partie qui necessite d’^etre explicitee est la partie de la grammaire concernant les declareur. C’est la partie suivante:

declareur :

identi cateur

  • declareur

declareur ( )

declareur [ expression-constanteoption ]

  • declareur )

La semantique est la suivante:

la seule regle de la grammaire qui derive vers un terminal est la regle: declareur :

identificateur

ce qui fait qu’a l’interieur de tout declareur se trouve un identi cateur. Cet identi-cateur est le nom de l’objet declare par la declaration.

ex:

char c; /* declaration de la variable c de type char */

il y a 3 constructeurs de type:

  1. * est un constructeur permettant de construire des types \pointeur vers …”. ex:

/* declaration de p de type pointeur vers short int short int *p;

*/

  1. ( ) est un constructeur permettant permettant de construire des types \fonc-tion retournant …”.

ex:

/* declaration de sin de type fonction retournant un double */ double sin();

  1. [ expressionoption ] est un constructeur permettant de construire des types \tableau de …”.

ex:

/* declaration de t de type tableau de 32 int int t[32];

*/

les constructeurs de type peuvent se composer et sont a ectes de priorites. Les constructeurs ( ) et [ ] ont la m^eme priorite, et celle-ci est superieure a la priorite du constructeur *.

char *t[10];

/*

tableau

de

10

pointeurs vers

des char

*/

int *f();

/*

fonction retournant un pointeur vers un int

*/

double t[10][10];

/*

tableau

de

10

tableaux de 10

double

*/

tout comme avec des expressions, la regle: declareur :

( declareur )

permet de parentheser des declareur de maniere a en changer la semantique:

char *t[10]; char (*t)[10];

/*

/*

tableau de 10 pointeurs vers un char pointeur vers un tableau de 10 char

*/

*/

9.8 Discussion sur les declarations

La syntaxe et la semantique des declarations ne sont pas faciles a apprehender dans le langage C pour les raisons que nous allons developper.

Tout d’abord, l’ordre des elements d’une declaration est assez peu naturel. Au lieu d’avoir comme en PASCAL, une declaration

c : char;

qu’on peut traduire par \c est de type char”, on a en C:

char c;

qu’il faut traduire par \de type char est c”, ce qui est une inversion peu naturelle. Ensuite, l’identi cateur qui est le nom de l’objet declare, au lieu d’^etre mis en evidence dans la declaration, est cache au beau milieu du declareur.

ex:

char **p[10];

D’autre part, le langage C fait partie des langages qui permettent au programmeur, a partir de types de base et de constructeurs de type, de construire des types complexes. Du point de vue du programmeur, il existe dans le langage les constructeurs suivants:

* pour les pointeurs,

[ ] pour les tableaux,

( ) pour les fonctions,

struct pour les structures,

union pour les unions.

Alors que le programmeur serait en droit de s’attendre a ce que tous ces constructeurs soient traites de maniere homogene, en fait, les trois premiers sont des declareur alors que les deux derniers sont des indicateur-de-type.

Autre point ajoutant encore a la confusion, le constructeur * est un constructeur pre xe alors que les constructeurs [ ] et ( ) sont post xe, et le constructeur * a une priorite differente de celle des deux autres. Ceci a la consequence extremement desagreable, qu’un type complexe ecrit en C ne peut pas se lire de la gauche vers la droite, mais peut necessiter un analyse du type de celle que l’on fait pour comprendre une expression mathematique. Par exemple, qui pourrait dire du premier coup d’ il quel est le type de f dans la declaration ci-dessous1 :

char (*(*f())[])()

9.9 En pratique

Lorsqu’il s’agit d’ecrire ou de comprendre un type complique, il est recommande non pas d’utilier la grammaire des declareur, mais de partir d’une utilisation du type, etant donne que la grammaire est faite de telle sorte que les declarations calquent tres exactement les expressions.

Voyons sur un exemple. Soit a declarer un pointeur p vers une fonction retournant un int. Considerons l’utilisation de p: a partir de p, il faut d’abord utiliser l’operateur indirection pour obtenir la fonction, soit *p. Ensuite, on va appeller la fonction delivree

1f est une fonction retournant un pointeur vers un tableau de pointeurs vers une fonction retournant un char par l’expression *p, pour cela il faut ecrire (*p)()2. Finalement cette expression nous delivre un int. La declaration de p s’ecrira donc:

int (*p)();

De la m^eme maniere, pour interpreter un type complique, il vaut mieux partir d’une utilisation. Soit a interpreter:

char (*f())[];

Imaginons une utilisation: (*f(i))[j]. L’operateur appel de fonction etant plus prioritaire que l’operateur indirection, on applique d’abord l’appel de fonction a f. Donc f est une fonction. Ensuite on applique l’operateur indirection, donc f est une fonction retour-nant un pointeur. On indexe le resultat, donc f est une fonction retournant un pointeur vers un tableau. Finalement, on voit que f est une fonction retournant un pointeur vers un tableau de char.

2A l’utilisation il faudra mettre les parametres e ectifs

 

Chapter 10

Pour Finir

10.1 De nition de types

Il existe en C un moyen de donner un nom a un type. Il consiste a faire suivre le mot cle typedef, d’une construction ayant exactement la m^eme syntaxe qu’une declaration de variable.

ex:

typedef int tab[10];

declare tab comme etant le type tableau de 10 entiers, et:

typedef struct

{

char nom[20];

int no_ss;

} personne;

declare personne comme etant le type structure dont le premier champ est …

ces noms de type sont ensuite utilisables dans les declarations

de variables, exactement

comme un type de base.

               

ex:

               

tab t1,t2;

/*

t1

et

t2

tableaux de 10

entiers

*/

personne *p1,*p2;

/*

p1

et

p2

pointeurs vers

des struct

*/

10.2 Utilite des typedef

La principale utilite des typedef est, si l’on en fait une utilisation judicieuse, de faciliter l’ecriture des programmes, et d’en augmenter la lisibilite.

10.3 Rede nition d’un type de base

Il est parfois necessaire de manipuler des variables qui ne peuvent prendre comme valeurs qu’un sous-ensemble de l’ensemble des valeurs d’un type de base.

111

 

112 CHAPTER 10. POUR FINIR

Supposons que nous voulions manipuler des booleens. Comme le type booleen n’existe pas dans le langage, il faudra utiliser des int, en se restreignant a deux valeurs, par exemple 0 et 1.

Il est alors interessant de rede nir a l’aide d’un typedef, le type int. On ecrira par exemple:

#define VRAI 1

#define FAUX 0

typedef int BOOLEAN;

On pourra par la suite declarer des \booleens” de la maniere suivante:

BOOLEAN b1,b2;

et les utiliser:

b1 = VRAI;

if (b2 == FAUX) …

mais bien entendu, ce sera a la charge du programmeur d’assurer que les variables b1 et b2 ne prennent comme valeurs que VRAI ou FAUX. Le compilateur ne protestera pas si on ecrit:

b1 = 10;

On voit que la lisibilite du programme aura ete augmentee, dans la mesure ou le program-meur aura pu expliciter une restriction semantique apportee au type int.

10.4 De nition de type structure

Lorsqu’on donne un nom a un type structure par typedef, l’utilisation est beaucoup plus aisee.

En e et, si on declare

struct personne

{

}

les declarations de variables se feront par:

struct personne p1,p2;

alors que si on declare

typedef struct

{

} PERSONNE;

les declarations de variables se feront par:

PERSONNE p1,p2;

 

         

113

10.5. GENERALITES SUR LA COMPILATION SEPAREE

on voit que la seconde methode permet d’eviter

d’avoir a repeter struct.

 

De la m^eme maniere, en ce qui concerne les pointeurs, il est plus di cile d’ecrire et de

comprendre:

         

struct personne

         

{

         

         

};

         

struct personne *p1,*p2;

/* p1 et p2 pointeurs vers des struct

*/

que la version suivante qui donne un nom parlant au type pointeur vers struct:

typedef struct

{

} PERSONNE;

typedef PERSONNE *P_PERSONNE; P_PERSONNE p1,p2;

/*

/*

P_PERSONNE type pointeur vers struct */ p1 et p2 pointeurs vers des struct */

10.5 Generalites sur la compilation separee

Des que l’on programme une application un peu consequente, il devient necessaire pour des raisons pratiques de fractionner le source en plusieurs chiers. Chaque chier est compile separement, puis les binaires obtenus sont lies a l’aide d’un editeur de liens pour creer le programme desire.

Il est necessaire cependant que le langage o re au programmeur les services suivants:

possibilite de declarer des objets (variables et procedures) et:

    1. soit les exporter (les faire connaitre aux autre sources)
    2. soit les cacher (emp^echer les autres sources d’y acceder).

referencer a partir d’un source des objets de nis dans d’autres sources.

10.6 La compilation separee dans le langage C

Nous allons passer en revue comment realiser les besoins du programmeur.

10.6.1 declarer des objets et les exporter

Toute declaration de variable se trouvant a l’exterieur de toute procedure ou fonction, exporte la variable declaree. De la m^eme facon, toute de nition de procedure ou fonction est par defaut exportee.

114 CHAPTER 10. POUR FINIR

10.6.2 declarer des objets et les cacher

Pour cacher une declaration, il faut la faire preceder du mot-cle static, ceci aussi bien pour les variables que pour les procedures et fonctions.

ex:

static int i;

static t[10];

static f(i,j)

int a,b;

{

};

10.6.3 referencer un objet declare ailleurs

Il faut pour cela, faire preceder la declaration du mot cle extern, ceci aussi bien pour les variables que pour les procedures et fonctions.

ex:

extern int i;

extern t[]; /* reference a un tableau defini ailleurs : pas de taille

extern int f(); /* ref. a une fonction definie ailleurs: pas de parametre */

*/

10.7 Le preprocesseur

Toute compilation d’un source C commence par une phase dite de pre-processing au cours de laquelle le compilateur memorise et execute un certain nombre de commandes permet-tant de faire de la substitution d’identi cateur, de la macro-substitution, de l’inclusion de source et de la compilation conditionnelle.

10.8 substitution d’identi cateur

Lorsque le compilateur rencontre une ligne de la forme

#define identi cateur reste-de-la-ligne

il memorise cette de nition, et substitue ensuite toute occurence de identi cateur par reste-de-la-ligne.

10.9 Macro-substitution

Lorsque le compilateur rencontre une ligne de la forme

#define identi cateur ( liste-d’identi cateurs ) reste-de-la-ligne

ou il n’y a pas de blanc entre identi cateur et la parenthese ouvrante, il

memorise cette de nition et remplace toute occurence de identi cateur ( liste-de-parametres-e ectifs ) par reste-de-la-ligne dans laquelle les parametres formels auront etes remplaces par les parametres e ectifs.

ex:

 

10.10. COMPILATION CONDITIONNELLE

115

#define verif(a,b) if ((a) > (b)) erreur(10)

f()

{

verif(i+1,j-1);

}

verif(i+1,j-1);

sera compile comme si on avait ecrit:

if ((i+1) > (j-1)) erreur(10);

10.10 Compilation conditionnelle

Lorsque le compilateur rencontre:

#if expression

suite-de-lignes

#endif

ou expression a une valeur connue a la compilation, le compilateur compilera suite-de-lignes si cette valeur est non nulle. Si expression a une valeur nulle, le compilateur ignorera suite-de-lignes.

Lorsque le compilateur rencontre:

#if expression

suite-de-lignes1

#else

suite-de-lignes2

#endif

ou expression a une valeur connue a la compilation, le compilateur compilera suite-de-lignes1 et ignorera suite-de-lignes2 si cette valeur est non nulle. Si expression a une valeur nulle, le compilateur ignorera suite-de-lignes1 et compilera suite-de-lignes2 .

116 CHAPTER 10. POUR FINIR

Contents

1 Les bases

5 1.1 Le compilateur

5 1.2 Les types de base

5 1.2.1 les caracteres

5 1.2.2 Les entiers

5 1.2.3 Les ottants

6 1.3 Les constantes

6 1.3.1 Les constantes entieres

6 1.3.2 Les constantes caracteres

7 1.3.3 Les constants ottantes

7 1.4 Les cha^nes de caracteres litterales

7 1.5 constantes nommees

 8 1.6 Declarations de variables ayant un type de base

8 1.7 Les operateurs les plus usuels

9 1.7.1 l’aectation

9 1.7.2 L’addition

10 1.7.3 La soustraction

10 1.7.4 La multiplication

10 1.7.5 La division

 11 1.7.6 L’operateur modulo

11 1.7.7 Les operateurs de comparaison

11 1.8 Les instructions les plus usuelles

12 1.8.1 Instruction expression

12 1.8.2 Instruction compos ee

12 1.8.3 instruction if

2 Fonctions et procedures

15 2.1 Definition d’une fonction

15 2.2 Appel d’une fonction

16 2.3 Les procedures

17 2.4 Omission du type retourne par une fonction

18 2.5 Impression formatt ee

 18 2.6 Structure d’un programme

 19 2.7 Mise en uvre du compilateur C sous UNIX

 20 2.8 Exercice

20

117

118 CONTENTS

3 Les tableaux 25 3.1 Les tableaux

25 3.1.1 Declaration de tableaux dont les elements ont un type de base

25 3.1.2 Initialisation d’un tableau

26 3.1.3 R eference a un element d’un tableau

27 3.2 Les instructions iteratives

27 3.2.1 Instruction for

 27 3.2.2 Instruction while

 28 3.2.3 Instruction do

 29 3.2.4 Instruction break

29 3.2.5 Instruction continue

30 3.3 Les operateurs

30 3.3.1 Operateur pre et postincrement

30 3.3.2 Operateur pre et postdecrement

 31 3.3.3 Quelques utilisations typiques de ces operateurs

31 3.3.4 Operateur et logique

 32 3.3.5 Operateur ou logique

 32 3.3.6 Operateur non logique

33 3.4 Exercice

33

4 Les pointeurs 35 4.1 Notion de pointeur

35 4.2 Declarations de variables de type pointeur vers les types de base

 35 4.3 Operateur adresse de

35 4.4 Operateur d’indirection

 36 4.5 Exercice

36 4.6 Pointeurs et operateurs additifs

38 4.7 Diference de deux pointeurs

38 4.8 Exercice

 38 4.9 Passage de parametres

40 4.9.1 Les besoins du programmeur

40 4.9.2 Comment les langages de programmation satisfont ces besoins

40 4.9.3 La stategie du langage C

40 4.10 Discussion

41 4.11 Une derniere precision

41 4.12 Exercice

 42 4.13 Lecture formatt ee

 44 4.14 Les dernieres instructions

 44 4.14.1 Instruction switch

44 4.14.2 Instruction goto

47 4.14.3 Instruction nulle

 47 4.15 Exercice

48

5 Tableaux et pointeurs

51 5.1 Retour sur les tableaux

51 5.2 La notion de tableau en programmation

51 5.3 La notion de tableau dans le langage C

52

CONTENTS 119

5.3.1 L’operateur d’indexation

52 5.3.2 Passage de tableau en parametre

53 5.4 Modication des elements d’un tableau passe en parametre

55 5.5 Exercice

 55 5.6 Declaration de tableaux multi-dimensionnels

58 5.7 Acces aux elements d’un tableau multi-dimensionnel

 58 5.8 Passage de tableaux multi-dimensionnels en parametre

 58 5.9 Initialisation d’un tableau multi-dimensionnel

59 5.10 Exercice 59

6 Les entrees sorties 61 6.1 Inclusion de source

 61 6.2 Ouverture et fermeture de fichiers

 62 6.2.1 Ouverture d’un fichier : fopen

62 6.2.2 fermeture d’un fichier : fclose

 63 6.3 Lecture et ecriture par caractere sur fichier

63 6.3.1 lecture par caractere: getc

63 6.3.2 ecriture par caractere : putc

 63 6.4 Lecture et ecriture par lignes sur fichier

64 6.4.1 lecture par ligne : fgets

64 6.4.2 ecriture par cha^ne : fputs

 64 6.5 E/S formattees sur fichiers

65 6.5.1 Ecriture formattee: fprintf

65 6.5.2 Retour sur printf

67 6.5.3 Exemples d’utilisation des formats

 68 6.5.4 Entrees formattees : fscanf

68 6.6 Exercice 70

7 Structures et unions

 73 7.1 Notion de structure

 73 7.2 Declaration de structure

73 7.3 Acces aux champs des structures

74 7.4 Tableaux de structures

75 7.5 Exercice

75 7.6 Pointeurs vers une structure

77 7.7 Structures dont un des champs pointe vers une structure du m^eme type ::

77 7.8 Acces aux elements d’une structure point ee

78 7.9 Determination de la taille allouee a un type

78 7.10 Allocation et liberation d’espace pour les structures

78 7.10.1 Allocation d’espace: fonctions malloc et calloc

 78 7.10.2 Liberation d’espace: procedure free

79 7.11 Exercice

79 7.12 Passage de structures en parametre

82 7.13 Structures retournees par une fonction

82 7.14 Declaration de procedures / fonctions externes

83 7.15 Exercice

83 7.16 Les champs de bits

 89

120 CONTENTS

7.16.1 Generalit es

89 7.16.2 Contraintes

89 7.17 Les unions

90 7.18 Acces aux champs de l’union

90 7.19 Utilisation pratique des unions

90 7.20 Une methode pour alleger l’acces aux champs 91

8 Les expressions

 93 8.1 Les operateurs

93 8.1.1 Operateur non bit a bit

93 8.1.2 Operateur et bit a bit

93 8.1.3 Operateur ou bit a bit

93 8.1.4 Operateur d ecalage a gauche

94 8.1.5 Operateur d ecalage a droite

94 8.1.6 Operateur conditionnel

94 8.1.7 Operateur virgule

 94 8.1.8 Operateurs d’a
ectation compos ee

95 8.2 Operateur forceur

 95 8.3 Semantique des expressions

96 8.3.1 Operateurs d’adressage

96 8.3.2 Priorite et associativite des operateurs

96 8.3.3 Ordre d’evaluation des operandes 97

9 Les declarations 99 9.1 Portee des declarations

99 9.2 Visibilite des identi
cateurs

100 9.3 Les espaces de noms

100 9.3.1 Position du probleme

100 9.3.2 Les espaces de noms du langage C

101 9.4 Duree de vie

102 9.5 Classes de memoire

103 9.5.1 Position du probleme

103 9.5.2 Les indicateurs de classe de memoire

104 9.5.3 Indicateur de classe memoire par defaut

105 9.6 Syntaxe des declarations

105 9.7 Semantique des declarations

107 9.8 Discussion sur les declarations

108 9.9 En pratique

109

10 Pour finir

 111 10.1 Definition de types

111 10.2 Utilite des typedef

111 10.3 Redefinition d’un type de base

111 10.4 Definition de type structure

112 10.5 Generalites sur la compilation separ ee

113 10.6 La compilation separee dans le langage C

113 10.6.1 declarer des objets et les exporter

113

CONTENTS 121

10.6.2 declarer des objets et les cacher

114 10.6.3 r eferencer un objet declare ailleurs

114 10.7 Le preprocesseur

114 10.8 substitution d’identificateur

114 10.9 Macro-substitution

114 10.10Compilation conditionnelle

Telecharger PDF

Leave a Reply