Ebook : Support de cours Introduction au langage C de Bernard Cassagne
...
Les versions du langage C
Le langage C a subi au cours de son histoire deux grandes étapes de définition. Il a été défini une première fois par deux chercheurs des Laboratoires Bell, B. Kernighan et D. Ritchie, dans un livre intitulé« The C Programming Language », publié en 1978. Cette version est appelée « Kernighan et Ritchie 78 », ou K&R 78 en abrégé, ou encore le plus souvent, simplement K&R.
Suite à l'extraordinaire succès d'unix, qui induisit le succès du langage C, la situation devint confuse: plusieurs fournisseurs de compilateurs mirent sur le marché des compilateurs non conformes à K&R car comportant des extensions particulières. À la fin des années 80, il devint nécessaire de mettre de l'ordre dans ce chaos et donc de normaliser le langage, tâche à laquelle s'attela l'ansi1, organisme de normalisation américain. La norme ansi fut terminée en 1989. En 1990, l'iso2, organisme de normalisation international, (donc chapeautant l'ansi), adopta tel quel le standard ansi en tant que standard iso.
Cette seconde version du langage C devrait donc s'appeler iso c, mais comme les acteurs importants du monde informatique sont de culture anglo-saxonne et que ceux-ci persistent à l'appeler ansi c, (presque?) tout le monde fait de même. Dans ce manuel, nous suivrons l'usage général, et utiliserons l'expression ansi c pour désigner la norme commune à l'ansi et l'iso.
Ce document décrit C ansi, avec parfois des références à C K&R, de manière à permettre au lecteur de comprendre les sources écrits avant l'apparition de la norme.
1.2 Langage et bibliothèque standard
Le langage C a été conçu pour l'écriture de systèmes, en particulier le système unix. Pour cette raison, ses concepteurs ont fait une séparation nette entre ce qui est purement algorithmique (déclarations, instructions, etc.) et tout ce qui est interaction avec le système (entrées sorties, allocation de mémoire, etc.) qui est réalisé par appel de fonctions se trouvant dans une bibliothèque dite bibliothèque standard. Cette coupure se retrouve dans la norme qui est composée essentiellement de deux grands chapitres, les chapitres « langage » et « bibliothèque ».
Ce manuel se donne comme objectif de donner une vue d'ensemble du langage, mais pas de la bibliothèque standard. De la bibliothèque standard ne seront présentées de manière complète que les fonctions permettant de réaliser les entrées-sorties et la gestion mémoire. Cependant, la liste exhaustive des noms des fonctions de la bibliothèque, classés par type d'utilisation, est donnée dans le chapitre 10.
1.3 Les phases de compilation
Les compilateurs C font subir deux transformations aux programmes:
La fonction de préprocesseur est assez souvent implémentée par un programme séparé(cpp sous UNIx) qui est automatiquement appelépar le compilateur.
1.4 Les jeux de caractères
Le lecteur non familiariséavec les problèmes de codage de caractères peut se reporter à l'annexe A, où ces problèmes sont développés.
Le langage C n'impose pas un jeu de caractères particulier. Par contre tout le langage (mots-clés, opérateurs, etc.) est défini en utilisant les caractères ASCII. Mème les identificateurs doivent ètre écrits avec l'alphabet anglais. Par contre, le jeu de caractères utilisépour les constantes caractère, les chaînes de caractères et les commentaires est dépendant de l'implémentation.
Pendant très longtemps les programmeurs non anglophones ont utilisél'ASCII faute de mieux, pour programmer en C. Actuellement, si on est dans le monde UNIx, il ne doit pas y avoir de problème pour disposer d'un environnement de travail (la fenètre, le shell, l'éditeur, le compilateur) entièrement à la norme ISO-8859. Dans ce manuel, on suppose que le lecteur dispose d'un tel environnement: les exemples donnés sont écrits en ISO-8859.
1.5 Les unités lexicales
Le langage comprends 6 types d'unités lexicales: les mots-clés, les identificateurs, les constantes, les chaînes, les opérateurs et les signes de ponctuation.
1.5.1 Les mots-clés
Le langage C est un langage à mots-clés, ce qui signifie qu'un certain nombre de mots sont réservés pour le langage lui-même et ne peuvent donc pas être utilisés comme identificateurs. La liste exhaustive des mots-clés est la suivante:
...
Attention
Si le compilateur produit un message d'erreur syntaxique incompréhensible il est re-commandéd'avoir le réflexe de consulter la liste des mots clés pour vérifier que l'on a pas
pris comme identificateur un mot-clé. Si le lecteur désire être convaincu, il lui est suggéréde donner le nom long à une variable entière.
1.5.2 Les identificateurs
Le but d'un identificateur est de donner un nom à une entitédu programme (variable, procédure, etc.) Les identificateurs sont formés d'une suite de lettres, de chiffres et du signe souligné, suite dont le premier caractère ne peut pas être un chiffre. Les lettres formant les identificateurs peuvent être majuscules ou minuscules, mais doivent faire partie de l'alphabet anglais: les lettres accentuées sont interdites. Les noms var1, PremierIndex, i_tab, _deb sont des identificateurs valides, mais 1i et i:j ne le sont pas.
Un compilateur a le droit de tronquer les identificateurs internes (ceux qui ne sont pas exportés à un éditeur de liens) au delàd'une certaine longueur. Cette limite dépend de l'implémentation, mais ne doit pas être inférieure à 31 caractères.
De la même manière, les identificateurs externes (exportés à un éditeur de liens) pourront être tronqués au delàd'une certaine longueur. Cette limite est généralement plus sévère, mais ne peut être inférieure à 6 caractères. De surcroît, la distinction entre minuscules et majuscules n'est pas garantie (au contraire des noms internes, pour lesquels cette distinction est garantie).
Les commentaires débutent par /* et se terminent par */. Exemple:
/* Ceci est un commentaire */
Toute occurrence de /* est interprétée comme le début d'un commentaire sauf dans une chaîne littérale, ou un commentaire (les commentaires ne peuvent donc pas être imbriqués).
Dans le domaine général de la programmation, (pas seulement le langage C~, il est admis qu'il faille commenter selon les niveaux suivants:
— unitéde compilation: pour indiquer le nom de l'auteur, les droits de copyright, la date de création, les dates et auteurs des différentes modifications, ainsi que la raison d'étre de l'unité;
— procédure: pour indiquer les paramètres et la raison d'étre de la procédure;
— groupe d'intructions : pour exprimer ce que réalise une fraction significative d'une procédure;
— déclaration ou instruction: le plus bas niveau de commentaire.
Pour le niveau unitéde compilation, voici un exemple tirédu source de perl:
/*
* Copyright (c) 1991, Larry Wall
*
* You may distribute under the terms of either the GNU General Public
* License or the Artistic License, as specified in the README file.
*
* $Log: perl.c,v $
* Revision 4.0.1.8 1993/02/05 19:39:30 lwall
* Revision 4.0.1.7 92/06/08 14:50:39 lwall
* Revision 4.0.1.3 91/06/07 11:40:18 lwall
* Revision 4.0.1.2 91/06/07 11:26:16 lwall
* Revision 4.0.1.1 91/04/11 17:49:05 lwall
* Revision 4.0 91/03/20 01:37:44 lwall
* 4.0 baseline.
*
*/
Pour le niveau de la procédure, je trouve agréable de réaliser des espèces de car-touches permettant de découper visuellement un listing en ses différentes procédures, comme ceci par exemple:
...
* If nothing above worked, then we get desperate. We attempt to * open the stupid font at one of a small set of predefined sizes, * and then use PostScript scaling to generate the correct size. *
* We much prefer scaling up to scaling down, since scaling down
* and then work down.
*/
Pour le niveau déclaration ou instruction, on commentera sur la méme ligne. Exemple tirédu source du compilateur GNU CC:
char *name; /* Function unit name. */
struct function_unit *next; /* Next function unit. */
int multiplicity; /* Number of units of this type. */
int simultaneity; /* Maximum number of simultaneous insns
on this function unit or 0 if unlimited. */
struct range ready_cost; /* Range of ready cost values. */
struct range issue_delay; /* Range of issue delay values. */
Attention
Exemple: instruction
/* premier commentaire
instruction ...
instruction
/* second commentaire */
instruction
On voit que dans ce cas, tout un ensemble d'instructions sera ignorépar le compilateur sans générer le moindre message d'erreur 4.
1.7 Les types de base 1.7.1 Les caractères
Le mot-clédésignant les caractères est char. Un objet de ce type doit pouvoir contenir le code de n'importe quel caractère de l'ensemble des caractères utilisésur la machine. Le codage des caractères n'est pas défini par le langage: c'est un choix d'implémentation. Cependant, dans la grande majoritédes cas, le code utiliséest le code dit AScii, ou un surensemble comme par exemple la norme iSO-8859.
Attention
1.7.2 Les entiers
Le mot clédésignant les entiers est int. Les entiers peuvent être affectés de deux types d'attributs: un attribut de précision et un attribut de représentation.
Les attributs de précision sont short et long. Du point de vue de la précision, on peut avoir trois types d'entiers: short int, int et long int. Les int sont implémentés sur ce qui est un mot « naturel » de la machine. Les long int sont implémentés si possible plus grands que les int, sinon comme des int. Les short int sont implémentés si possible plus courts que les int, sinon comme des int. Les implémentations 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 efficace.
L'attribut de représentation est unsigned. Du point de vue de la représentation, on peut avoir deux types d'entiers: int et unsigned int. Les int permettent de contenir des entiers signés, très généralement en représentation en complément à 2, bien que cela ne soit pas imposépar le langage. Les unsigned int permettent de contenir des entiers non signés en représentation binaire.
On peut combiner attribut de précision et attribut de représentation et avoir par exemple un unsigned long int. En résumé, on dispose donc de six types d'entiers: int, short int, long int (tous trois signés) et unsigned int, unsigned short int et unsigned long int.
1.7.3 Les flottants
Il existe trois types de flottants correspondants à trois précisions possibles. En allant de la précision la plus faible vers la plus forte, on dispose des types float, double et long double. La précision effectivement utilisée pour chacun de ces types dépend de l'implémentation.
1.8 Les constantes
1.8.1 Les constantes entières
On dispose de 3 notations pour les constantes entières: décimale, octale et hexadé-cimale.
Une constante entière peut être suffixée par la lettre u ou U pour indiquer qu'elle doit être interprétée comme étant non signée. Elle peut également être suffixée par la lettre l ou L pour lui donner l'attribut de précision long.
Le type d'une constante entière est le premier type, choisi dans une liste de types, permettant de représenter la constante:
...
Attention
Ces conventions d'écriture des constantes ne respectent pas l'écriture mathématique, puisque 010 devant être interprétéen octal, n'est pas égal à 10.
1.8.2 Les constantes caractères
Une constante caractère s'écrit entourée du signe '. La règle générale consiste à écrire le caractère entourédu signe ' ; par exemple, la constante caractère correspondant au caractère g s'écrit 'g'.
Les cas particuliers
Les cas particuliers sont traités par une séquence d'échappement introduite par le caractère \.
— Caractères ne disposant pas de représentation imprimable.
...
Note
Pourquoi diable les deux caractères " et ? disposent ils de deux notations possibles?
~ Le caractère " peut ètre représentépar la notation '\"' parce que celle-ci doit ètre utilisée dans les chaînes de caractères (voir plus loin 1.9). Pour des raisons de symétrie, les concepteurs du langage n'ont pas voulu qu'une notation valable pour une chaînes de caractères ne soit pas valable pour un caractère.
— Le caractère ? est un cas à part à cause de l'existence des trigraphes. Les tri-graphes sont des séquences de trois caractères permettant de désigner les caractères # [ ] \ ^ { } | ~. En effet, les terminaux conformes à la norme iso 646:1983 ont remplacéces caractères par des caractères nationaux. Les français, par exemple, connaissent bien le problème des { et des } qui se transforment en é et è.
...
1.8.3 Les constantes flottantes
La notation utilisée est la notation classique par mantisse et exposant. La mantisse est composée d'une partie entière suivie du signe . (point) suivi de la partie frac-tionnaire. La partie entière et la partie fractionnaire sont exprimées en décimal et l'une ou l'autre peuvent ètre omises.
L'exposant est introduit par la lettre e sous la forme minuscule ou majuscule. L'ex-posant est un nombre décimal éventuellement signé.
Une constante flottante peut ètre suffixée par l'une quelconque des lettres f, F, l, L.
Une constante non suffixée a le type double. Une constante suffixée par f ou F a le type float. Une constante suffixée par l ou L a le type long double.
La valeur de la constante mantisse e exposant est mantisse x10exposant.
Si la valeur résultante ne correspond pas au type, la valeur est arrondie vers une valeur supérieure ou inférieure (le choix dépend de l'implémentation).
...
Une chaîne de caractères littérale est une suite de caractères entourés du signe ". Exemple:
"ceci est une chaîne"
Toutes les séquences d'échappement définies en 1.8.2 sont utilisables dans les chaînes. Exemple:
"ligne 1\nligne 2\nligne 3"
Le caractère \ suivi d'un passage à la ligne suivante est ignoré. Cela permet de faire tenir les longues chaînes sur plusieurs lignes de source. Exemple:
"ceci est une très très longue chaîne que l'on fait tenir \ sur deux lignes de source"
Si deux chaînes littérales sont adjacentes dans le source, le compilateur concatène les deux chaînes. Exemple: "Hello " "World!!" est équivalent à "Hello World!!".
Dans une chaîne de caractères littérale, le caractère " doit ètre désignépar la séquence d'échappement, alors que ' peut ètre désignépar sa séquence d'échappement ou par lui-mème.
1.10 Les constantes nommées
Il y a deux façons de donner un nom à une constante: soit en utilisant les possibilités du préprocesseur, soit en utilisant des énumérations.
1.10.1 Les #define
Lorsque le préprocesseur lit une ligne du type: #define identificateur reste-de-la-ligne
il remplace dans toute la suite du source, toute nouvelle occurrence de identificateur par reste-de-la-ligne. Par exemple on peut écrire:
#define PI 3.14159
et dans la suite du programme on pourra utiliser le nom PI pour désigner la constante 3.14159.
Attention
Une telle définition de constante n'est pas une déclaration mais une commande du préprocesseur. Il n'y a donc pas de ; à la fin. Rappelons que le préprocesseur ne compile pas, il fait des transformations d'ordre purement textuel. Si on écrit:
#define PI 3.14159;
le préprocesseur remplacera toute utilisation de PI par 3.14159; et par exemple, rempla-cera l'expression PI / 2 par 3.14159; / 2 ce qui est une expression incorrecte. Dans une telle situation, le message d'erreur ne sera pas émis sur la ligne fautive (le #define), mais sur une ligne correcte (celle qui contient l'expression PI / 2), ce qui gènera la détection de l'erreur.
1.10.2 Les énumérations
On peut définir des constantes de la manière suivante:
enum { liste-d'identificateurs }
Par exemple:
enum {LUNDI, MARDI, MERCREDI, JEUDI, VENDREDI, SAMEDI, DIMANCHE};
définit les identificateurs LUNDI, ... DIMANCHE comme étant des constantes de type int, et leur donne les valeurs 0, 1, ... 6. Si on désire donner des valeurs particulières aux constantes, cela est possible:
Remarque
Il est d'usage (au moins dans le monde UNIx) de donner un nom entièrement en majuscules aux constantes nommées d'un programme. Mon opinion est que ceci est une bonne convention, qui accroît la lisibilitédes programmes.
1.11 Déclarations de variables ayant un type de base
Une telle déclaration se fait en faisant suivre un type par une liste de noms de variables. Exemple:
int i; /* déclaration de la variable i de type int */
int i,j; /* déclaration de deux variables i et j de type int */
short int k; /* déclaration de la variable k de type short int */
float f; /* déclaration de la variable f de type float */
double d1,d2; /* déclaration de deux variables d1 et d2 de type double */
Il est possible de donner une valeur initiale aux variables ainsi déclarées. Exemple:
int i = 54;
int i = 34, j = 12;
1.12 Les opérateurs les plus usuels 1.12.1 L'affectation
En C, l'affectation est un opérateur et non pas une instruction.
expression:
#> lvalue = expression
Dans le jargon C, une lvalue est une expression qui doit délivrer une variable (par opposition à une constante). Une lvalue peut être par exemple une variable simple, un élément de tableau, mais pas une constante. Cette notion permet d'exprimer dans la grammaire l'impossibilitéd'écrire des choses du genre 1 = i qui n'ont pas de sens.
Exemples d'affectation:
i = 3 f = 3.4
i = j + 1
L'opérateur d'affectation a deux effets:
Exemple:
i = (j = k) + 1
La valeur de k est affectée à j et cette valeur est le résultat de l'expression (j = k) ; on y ajoute 1 et le résultat est affectéà i.
1.12.2 L'addition
expression:
#> + expression
#> expression1 + expression2
Les deux expressions sont évaluées, l'addition réalisée, et la valeur obtenue est la valeur de l'expression d'addition. (La sémantique de + expression est celle de 0 + expression).
L'ordre dans lequel les deux expressions sont évaluées, n'est pas déterminé. Si ex-pression1 et expression2 font des effets de bords, on n'est donc pas assuréde l'ordre dans lequel ils se feront.
Après évaluation des expressions, il peut y avoir conversion de type de l'un des opérandes, de manière à permettre l'addition. On pourra par exemple faire la somme d'une expression délivrant un flottant et d'une expression délivrant un entier: l'entier sera converti en flottant et l'addition sera réalisée entre flottants.
1.12.3 La soustraction
L'opérateur peut ètre utiliséde manière unaire ou binaire:
expression:
#> - expression
#> expression1 - expression2
Les deux expressions sont évaluées, la soustraction réalisée, et la valeur obtenue est la valeur de l'expression soustraction. (La sémantique de - expression est celle de 0 - expression).
Les mèmes remarques concernant l'ordre d'évaluation des opérandes ainsi que les éventuelles conversions de type faites au sujet de l'addition s'appliquent à la soustraction.
1.12.4 La multiplication
expression:
#> expression1 * expression2
Les deux expressions sont évaluées, la multiplication réalisée, et la valeur obtenue est la valeur de l'expression multiplicative.
Les mêmes remarques concernant l'ordre d'évaluation des opérandes ainsi que les éventuelles conversions de type faites au sujet de l'addition s'appliquent à la multiplication.
1.12.5 La division
expression:
#> expression1 / expression2
Si expression1 et expression2 délivrent deux valeurs entières, alors il s'agit d'une division entière. Si l'une des deux expressions au moins délivre une valeur flottante, il s'agit d'une division entre flottants.
Dans le cas de la division entière, si les deux opérandes sont positifs, l'arrondi se fait vers zéro, mais si au moins un des deux opérandes est négatif, la façon dont se fait l'arrondi dépend de l'implémentation (mais il est généralement fait vers zéro). Exemple: 13 / 2 délivre la valeur 6 et -13 / 2 ou 13 / -2 peuvent délivrer -6 ou -7 mais le résultat sera généralement -6.
Les remarques concernant l'ordre d'évaluation des opérandes faites au sujet de l'addition s'appliquent également à la division. Les remarques concernant les éventuelles conversion de type faites au sujet de l'addition s'appliquent à la division entre flottants.
1.12.6 L'opérateur modulo
#> expression1 % expression2
Les deux expressions sont évaluées et doivent délivrer une valeur de type entier, on évalue le reste de la division entière de expression1 par expression2 et la valeur obtenue est la valeur de l'expression modulo.
Si au moins un des deux opérandes est négatif, le signe du reste dépend de l'implémentation, mais il est généralement pris du même signe que le dividende. Exemples: 13 % 2 délivre 1
-13 % 2 délivre généralement -1
13 % -2 délivre généralement 1.
Les choix d'implémentation faits pour les opérateurs division entière et modulo doivent être cohérents, en ce sens que l'expression: b * (a / b) + a % b (oùa et b sont des entiers) doit avoir pour valeur a.