Initiation au langage C support pedagogique avec exemples d’application


Télécharger Initiation au langage C support pedagogique avec exemples d’application

★★★★★★★★★★1 étoiles sur 5 basé sur 1 votes.
Votez ce document:

Télécharger aussi :


Initiation au langage C support pédagogique avec exemples d’application

1.1 Historique

Le C a été conçu en 1972 par Dennis Richie et Ken Thompson, chercheurs aux Bell Labs, afin de développer un système d’exploitation UNIX sur un DEC PDP-11. En 1978, Brian Kernighan et Dennis Richie publient la définition classique du C dans le livre The C Programming language [6]. Le C devenant de plus en plus populaire dans les années 80, plusieurs groupes mirent sur le marché des compilateurs comportant des extensions particulières. En 1983, l’ANSI (American National Standards Institute) décida de normaliser le langage; ce travail s’acheva en 1989 par la définition de la norme ANSI C. Celle-ci fut reprise telle quelle par l’ISO (International Standards Organization) en 1990. C’est ce standard, ANSI C, qui est décrit dans le présent document.

1.2 La compilation

Le C est un langage compilé(par opposition aux langages interprétés). Cela signifie qu’un programme C est décrit par un fichier texte, appelé fichier source. Ce fichier n’étant évidemment pas exécutable par le microprocesseur, il faut le traduire en langage machine. Cette opération est effectuée par un programme appelé compilateur. La compilation se décompose en fait en 4 phases successives:

  1. Le traitement par le préprocesseur : le fichier source est analysé par le préprocesseur qui effectue des transformations purement textuelles (remplacement de chaînes de caractères, inclusion d’autres fichiers source ... ).
  2. La compilation: la compilation proprement dite traduit le fichier généré par le préprocesseur en assembleur, c’est-à-dire en une suite d’instructions du microprocesseur qui utilisent des mnémoniques rendant la lecture possible.
  3. L’assemblage : cette opération transforme le code assembleur en un fichier binaire, c’est-à-dire en instructions directement compréhensibles par le processeur. Généralement, la compilation et l’assemblage se font dans la foulée, sauf si l’on spécifie explicitement que l’on veut le code assembleur. Le fichier produit par l’assemblage est appelé fichier objet.
  4. L’édition de liens: un programme est souvent séparé en plusieurs fichiers source, pour des raisons de clarté mais aussi parce qu’il fait généralement appel à des librairies de fonctions standard déjà écrites. Une fois chaque code source assemblé, il faut donc lier entre eux les différents fichiers objets. L’édition de liens produit alors un fichier dit exécutable.

Les différents types de fichiers utilisés lors de la compilation sont distingués par leur suffixe. Les fichiers source sont suffixés par .c, les fichiers prétraités par le préprocesseur par .i, les fichiers assembleur par .s, et les fichiers objet par .o. Les fichiers objets correspondant aux librairies précompilées ont pour suffixe .a.

Le compilateur C sous UNIX s’appelle cc. On utilisera de préférence le compilateur gcc du projet GNU. Ce compilateur est livré gratuitement avec sa documentation et ses sources. Par défaut, gcc active toutes les étapes de la compilation. On le lance par la commande

gcc [options] fichier.c [-llibrairies]

Par défaut, le fichier exécutable s’appelle a.out. Le nom de l’exécutable peut être modifié à l’aide de l’option -o.

Les éventuelles librairies sont déclarées par la chaîne -llibrairie. Dans ce cas, le système recherche le fichier liblibrairie.a dans le répertoire contenant les librairies précompilées (généralement /usr/lib/). Par exemple, pour lier le programme avec la librairie mathématique, on spécifie -lm. Le fichier objet correspondant est libm.a. Lorsque les librairies précompilées ne se trouvent pas dans le répertoire usuel, on spécifie leur chemin d’accès par l’option -L.

Les options les plus importantes du compilateur gcc sont les suivantes:

-c: supprime l’édition de liens; produit un fichier objet.

-E: n’active que le préprocesseur (le résultat est envoyésur la sortie standard). -g: produit des informations symboliques nécessaires au débogueur.

-Inom-de-répertoire: spécifie le répertoire dans lequel doivent être recherchés les fichiers en-têtes à inclure (en plus du répertoire courant).

-Lnom-de-répertoire: spécifie le répertoire dans lequel doivent être recherchées les librairies précompilées (en plus du répertoire usuel).

-o nom-de-fichier: spécifie le nom du fichier produit. Par défaut, le exécutable fichier s’appelle a.out.

-O, -O1, -O2, -O3: options d’optimisations. Sans ces options, le but du compilateur est de minimiser le coût de la compilation. En rajoutant l’une de ces options, le compilateur tente de réduire la taille du code exécutable et le temps d’exécution. Les options cor-respondent à différents niveaux d’optimisation: -O1 (similaire à -O) correspond à une faible optimisation, -O3 à l’optimisation maximale.

-S: n’active que le préprocesseur et le compilateur; produit un fichier assembleur.

...

Les opérateurs

1.7.1 L’affectation

En C, l’affectation est un opérateur à part entière. Elle est symbolisée par le signe =. Sa syntaxe est la suivante:

variable = expression

Le terme de gauche de l’affectation peut être une variable simple, un élément de tableau mais pas une constante. Cette expression a pour effet d’évaluer expression et d’affecter la valeur obtenue à variable. De plus, cette expression possède une valeur, qui est celle expression. Ainsi, l’expression i = 5 vaut 5.

L’affectation effectue une conversion de type implicite: la valeur de l’expression (terme de droite) est convertie dans le type du terme de gauche. Par exemple, le programme suivant

main()

{

int i, j = 2;

float x = 2.5;

i = j + x;

x = x + i;

printf("\n %f \n",x);

}

imprime pour x la valeur 6.5 (et non 7), car dans l’instruction i = j + x;, l’expression j + x a étéconvertie en entier.

1.7.2 Les opérateurs arithmétiques

Les opérateurs arithmétiques classiques sont l’opérateur unaire - (changement de signe) ainsi que les opérateurs binaires

+ addition

- soustraction

* multiplication

/ division

% reste de la division (modulo)

Ces opérateurs agissent de la façon attendue sur les entiers comme sur les flottants. Leurs seules spécificités sont les suivantes:

– Contrairement à d’autres langages, le C ne dispose que de la notation / pour désigner à la fois la division entière et la division entre flottants. Si les deux opérandes sont de type entier, l’opérateur / produira une division entière (quotient de la division). Par contre, il délivrera une valeur flottante dès que l’un des opérandes est un flottant. Par exemple,

float x; x = 3 / 2;

affecte à x la valeur 1. Par contre x = 3 / 2.;

affecte à x la valeur 1.5.

– L’opérateur % ne s’applique qu’àdes opérandes de type entier. Si l’un des deux opérandes est négatif, le signe du reste dépend de l’implémentation, mais il est en général le même que celui du dividende.

Notons enfin qu’il n’y a pas en C d’opérateur effectuant l’élévation à la puissance. De façon générale, il faut utiliser la fonction pow(x,y) de la librairie math.h pour calculer xy.

1.7.3 Les opérateurs relationnels

>             strictement supérieur

>= supérieur ou égal

<             strictement inférieur

<=          inférieur ou égal

== égal



!= différent Leur syntaxe est

expression-1 op expression-2

Les deux expressions sont évaluées puis comparées. La valeur rendue est de type int (il n’y a pas de type booléen en C); elle vaut 1 si la condition est vraie, et 0 sinon.

Attention à ne pas confondre l’opérateur de test d’égalité== avec l’opérateur d’affection =. Ainsi, le programme

main()

{

int a = 0;

int b = 1;

if (a = b)

printf("\n a et b sont egaux \n");

else

printf("\n a et b sont differents \n");

}

imprime à l’écran a et b sont egaux !

1.7.4 Les opérateurs logiques booléens

&& et logique || ou logique

!  négation logique

Comme pour les opérateurs de comparaison, la valeur retournée par ces opérateurs est un int qui vaut 1 si la condition est vraie et 0 sinon.

Dans une expression de type

expression-1 op-1 expression-2 op-2 ...expression-n

l’évaluation se fait de gauche à droite et s’arrête dès que le résultat final est déterminé. Par exemple dans

int i;

int p[10];

if ((i >= 0) && (i <= 9) && !(p[i] == 0))

...

la dernière clause ne sera pas évaluée si i n’est pas entre 0 et 9.

Les instructions de branchement conditionnel

On appelle instruction de contrôle toute instruction qui permet de contrôler le fonction-nement d’un programme. Parmi les instructions de contrôle, on distingue les instructions de branchement et les boucles. Les instructions de branchement permettent de déterminer quelles instructions seront exécutées et dans quel ordre.

1.8.1 Branchement conditionnel if---else La forme la plus générale est celle-ci:

if ( expression-1 ) instruction-1

else if ( expression-2 )

instruction-2

...

else if ( expression-n ) instruction-n

else

instruction-∞

avec un nombre quelconque de else if ( ... ). Le dernier else est toujours facultatif. La forme la plus simple est

if ( expression ) instruction

Chaque instruction peut être un bloc d’instructions.

1.8.2 Branchement multiple switch Sa forme la plus générale est celle-ci:

switch ( expression )

{

case constante-1:

liste d’instructions 1

break;

case constante-2:

liste d’instructions 2

break;

...

case constante-n:

liste d’instructions n

break; default:

liste d’instructions ∞

break;

}

Si la valeur de expression est égale à l’une des constantes, la liste d’instructions correspondant est exécutée. Sinon la liste d’instructions ∞ correspondant à default est exécutée. L’instruction default est facultative.

1.9 Les boucles

Les boucles permettent de répéter une série d’instructions tant qu’une certaine condition n’est pas vérifiée.

1.9.1 Boucle while

La syntaxe de while est la suivante:

while ( expression ) instruction

Tant que expression est vérifiée (i.e., non nulle), instruction est exécutée. Si expression est nulle au départ, instruction ne sera jamais exécutée. instruction peut évidemment être une instruction composée. Par exemple, le programme suivant imprime les entiers de 1 à 9.

i = 1;

while (i < 10)

{

printf("\n i = %d",i);

i++;

}

1.9.2 Boucle do---while

Il peut arriver que l’on ne veuille effectuer le test de continuation qu’après avoir exécutél’instruction. Dans ce cas, on utilise la boucle do---while. Sa syntaxe est

do

instruction

while ( expression );

Ici, instruction sera exécutée tant que expression est non nulle. Cela signifie donc que instruction est toujours exécutée au moins une fois. Par exemple, pour saisir au clavier un entier entre 1 et 10:

int a;

{

printf("\n Entrez un entier entre 1 et 10 : ");

scanf("%d",&a);

}

while ((a <= 0) || (a > 10));

1.9.3 Boucle for

La syntaxe de for est:

for ( expr 1 ; expr 2 ; expr 3) instruction

Une version équivalente plus intuitive est:

expr 1;

while ( expr 2 ) {

instruction expr 3;

}

Par exemple, pour imprimer tous les entiers de 0 à 9, on écrit:

for (i = 0; i < 10; i++) printf("\n i = %d",i);

A la fin de cette boucle, i vaudra 10. Les trois expressions utilisées dans une boucle for peuvent être constituées de plusieurs expressions séparées par des virgules. Cela permet par exemple de faire plusieurs initialisations à la fois. Par exemple, pour calculer la factorielle d’un entier, on peut écrire:

int n, i, fact;

for (i = 1, fact = 1; i <= n; i++) fact *= i;

printf("%d ! = %d \n",n,fact);

On peut également insérer l’instruction fact *= i; dans la boucle for ce qui donne:

int n, i, fact;

for (i = 1, fact = 1; i <= n; fact *= i, i++); printf("%d ! = %d \n",n,fact);

1.10 Les instructions de branchement non conditionnel

1.10.1 Branchement non conditionnel break

On a vu le rôle de l’instruction break; au sein d’une instruction de branchement multiple switch. L’instruction break peut, plus généralement, être employée à l’intérieur de n’importe quelle boucle. Elle permet d’interrompre le déroulement de la boucle, et passe à la première instruction qui suit la boucle. En cas de boucles imbriquées, break fait sortir de la boucle la plus interne. Par exemple, le programme suivant:

main()

{

int i;

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



{

printf("i = %d\n",i);

if (i == 3)

break;

}

printf("valeur de i a la sortie de la boucle = %d\n",i);

}

imprime à l’écran

i = 0 i = 1 i = 2 i = 3 valeur de i a la sortie de la boucle = 3

1.10.2 Branchement non conditionnel continue

L’instruction continue permet de passer directement au tour de boucle suivant, sans exécuter les autres instructions de la boucle. Ainsi le programme

main()

{

int i;

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

{

if (i == 3)

continue;

printf("i = %d\n",i);

}

printf("valeur de i a la sortie de la boucle = %d\n",i);

}

imprime

i = 0

i = 1

i = 2

i = 4

valeur de i a la sortie de la boucle = 5

1.10.3 Branchement non conditionnel goto

L’instruction goto permet d’effectuer un saut jusqu’à l’instruction étiquette correspondant. Elle est à proscrire de tout programme C digne de ce nom.

1.11 Les fonctions d’entrées-sorties classiques

Il s’agit des fonctions de la librairie standard stdio.h utilisées avec les unités classiques d’entrées-sorties, qui sont respectivement le clavier et l’écran. Sur certains compilateurs, l’appel à la librairie stdio.h par la directive au préprocesseur

#include <stdio.h>

n’est pas nécessaire pour utiliser printf et scanf.

1.11.1 La fonction d’écriture printf

La fonction printf est une fonction d’impression formatée, ce qui signifie que les données sont converties selon le format particulier choisi. Sa syntaxe est

printf("chaîne de contrôle ",expression-1, ..., expression-n);

La chaîne de contrôle contient le texte à afficher et les spécifications de format correspondant à chaque expression de la liste. Les spécifications de format ont pour but d’annoncer le format des données à visualiser. Elles sont introduites par le caractère %, suivi d’un caractère désignant le format d’impression. Les formats d’impression en C sont donnés à la table 1.5.

En plus du caractère donnant le type des données, on peut éventuellement préciser certains paramètres du format d’impression, qui sont spécifiés entre le % et le caractère de conversion dans l’ordre suivant:

– largeur minimale du champ d’impression: %10d spécifie qu’au moins 10 caractères seront réservés pour imprimer l’entier. Par défaut, la donnée sera cadrée à droite du champ. Le signe - avant le format signifie que la donnée sera cadrée à gauche du champ (%-10d).

– précision: %.12f signifie qu’un flottant sera imprimé avec 12 chiffres après la virgule. De même %10.2f signifie que l’on réserve 12 caractères (incluant le caractère .) pour imprimer le flottant et que 2 d’entre eux sont destinés aux chiffres après la virgule. Lorsque la précision n’est pas spécifiée, elle correspond par défaut à 6 chiffres après la virgule. Pour une chaîne de caractères, la précision correspond au nombre de caractères imprimées: %30.4s signifie que l’on réserve un champ de 30 caractères pour imprimer la chaîne mais que seulement les 4 premiers caractères seront imprimés (suivis de 26 blancs).

...

Les types composés

A partir des types prédéfinis du C (caractères, entiers, flottants), on peut créer de nouveaux types, appelés types composés, qui permettent de représenter des ensembles de données organisées.

2.1 Les tableaux

Un tableau est un ensemble fini d’éléments de même type, stockés en mémoire à des adresses contiguës.

La déclaration d’un tableau à une dimension se fait de la façon suivante:

type nom-du-tableau[nombre-éléments];

où nombre-éléments est une expression constante entière positive. Par exemple, la déclaration int tab[10]; indique que tab est un tableau de 10 éléments de type int. Cette déclaration alloue donc en mémoire pour l’objet tab un espace de 10 × 4 octets consécutifs.

Pour plus de clarté, il est recommandé de donner un nom à la constante nombre-éléments par une directive au préprocesseur, par exemple

#define nombre-éléments 10

On accède à un élément du tableau en lui appliquant l’opérateur []. Les éléments d’un tableau sont toujours numérotés de 0 à nombre-éléments -1. Le programme suivant imprime les éléments du tableau tab:

#define N 10 main()

{

int tab[N]; int i;

...

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

printf("tab[%d] = %d\n",i,tab[i]);

}

Un tableau correspond en fait à un pointeur vers le premier élément du tableau. Ce pointeur est constant. Cela implique en particulier qu’aucune opération globale n’est autorisée sur un tableau. Notamment, un tableau ne peut pas figurer à gauche d’un opérateur d’affectation.

Par exemple, on ne peut pas écrire “tab1 = tab2;”. Il faut effectuer l’affectation pour chacun des éléments du tableau:

#define N 10

main()

{

int tab1[N], tab2[N];

int i;

...

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

}

On peut initialiser un tableau lors de sa déclaration par une liste de constantes de la façon suivante:

type nom-du-tableau[N] = {constante-1,constante-2,...,constante-N}; Par exemple, on peut écrire

#define N 4

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

main()

{

int i;

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

printf("tab[%d] = %d\n",i,tab[i]);

}

Si le nombre de données dans la liste d’initialisation est inférieur à la dimension du tableau, seuls les premiers éléments seront initialisés. Les autres éléments seront mis à zéro si le tableau est une variable globale (extérieure à toute fonction) ou une variable locale de classe de mémorisation static (cf. page 64).

De la même manière un tableau de caractères peut être initialisépar une liste de caractères, mais aussi par une chaîne de caractères littérale. Notons que le compilateur complète toute chaîne de caractères avec un caractère nul ’\0’. Il faut donc que le tableau ait au moins un élément de plus que le nombre de caractères de la chaîne littérale.

#define N 8

char tab[N] = "exemple";

main()

{



int i;

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

printf("tab[%d] = %c\n",i,tab[i]);

}

Lors d’une initialisation, il est également possible de ne pas spécifier le nombre d’éléments du tableau. Par défaut, il correspondra au nombre de constantes de la liste d’initialisation. Ainsi le programme suivant imprime le nombre de caractères du tableau tab, ici 8.

char tab[] = "exemple";

main()

{

int i;

printf("Nombre de caracteres du tableau = %d\n",sizeof(tab)/sizeof(char));

}

De manière similaire, on peut déclarer un tableau à plusieurs dimensions. Par exemple, pour un tableau à deux dimensions:

type nom-du-tableau[nombre-lignes][nombre-colonnes]

En fait, un tableau à deux dimensions est un tableau unidimensionnel dont chaque élément est lui-même un tableau. On accède à un élément du tableau par l’expression “tableau[i][j]”. Pour initialiser un tableau à plusieurs dimensions à la compilation, on utilise une liste dont chaque élément est une liste de constantes:

#define M 2 #define N 3

int tab[M][N] = {{1, 2, 3}, {4, 5, 6}};

main()

{

int i, j;

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

{

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

printf("tab[%d][%d]=%d\n",i,j,tab[i][j]);

}

}

2.2 Les structures

Une structure est une suite finie d’objets de types différents. Contrairement aux tableaux, les différents éléments d’une structure n’occupent pas nécessairement des zones contiguës en mémoire. Chaque élément de la structure, appelémembre ou champ, est désignépar un identificateur.

On distingue la déclaration d’un modèle de structure de celle d’un objet de type structure correspondant à un modèle donné. La déclaration d’un modèle de structure dont l’identifica-teur est modele suit la syntaxe suivante:

struct modele

{

type-1 membre-1; type-2 membre-2; ...

type-n membre-n;

Pour déclarer un objet de type structure correspondant au modèle précédent, on utilise la syntaxe:

struct modele objet;

ou bien, si le modèle n’a pas étédéclaréau préalable:

struct modele

{

type-1 membre-1; type-2 membre-2;

...

type-n membre-n; } objet;

On accède aux différents membres d’une structure grâce à l’opérateur membre de structure, noté“.”. Le i-ème membre de objet est désignépar l’expression

objet.membre-i

On peut effectuer sur le i-ème membre de la structure toutes les opérations valides sur des données de type type-i. Par exemple, le programme suivant définit la structure complexe, composée de deux champs de type double; il calcule la norme d’un nombre complexe.

#include <math.h> struct complexe {

double reelle;

double imaginaire;

};

main()

{

struct complexe z;

double norme;

...

norme = sqrt(z.reelle * z.reelle + z.imaginaire * z.imaginaire);

printf("norme de (%f + i %f) = %f \n",z.reelle,z.imaginaire,norme);

}

Les règles d’initialisation d’une structure lors de sa déclaration sont les mêmes que pour les tableaux. On écrit par exemple:

struct complexe z = {2. , 2.};

En ANSI C, on peut appliquer l’opérateur d’affectation aux structures (àla différence des tableaux). Dans le contexte précédent, on peut écrire:

...

main()

{

struct complexe z1, z2;

 ...

z2 = z1;

}

2.3 Les champs de bits

Il est possible en C de spécifier la longueur des champs d’une structure au bit près si ce champ est de type entier (int ou unsigned int). Cela se fait en précisant le nombre de bits du champ avant le ; qui suit sa déclaration. Par exemple, la structure suivante

struct registre {

unsigned int actif            1;

unsigned int valeur 31; };

possède deux membres, actif qui est codésur un seul bit, et valeur qui est codésur 31 bits. Tout objet de type struct registre est donc codésur 32 bits. Toutefois, l’ordre dans lequel les champs sont placés à l’intérieur de ce mot de 32 bits dépend de l’implémentation.

Le champ actif de la structure ne peut prendre que les valeurs 0 et 1. Aussi, si r est un objet de type struct registre, l’opération r.actif += 2; ne modifie pas la valeur du champ.

La taille d’un champ de bits doit être inférieure au nombre de bits d’un entier. Notons enfin qu’un champ de bits n’a pas d’adresse; on ne peut donc pas lui appliquer l’opérateur &.

2.4 Les unions

Une union désigne un ensemble de variables de types différents susceptibles d’occuper alternativement une même zone mémoire. Une union permet donc de définir un objet comme pouvant être d’un type au choix parmi un ensemble fini de types. Si les membres d’une union

sont de longueurs différentes, la place réservée en mémoire pour la représenter correspond à la taille du membre le plus grand.

Les déclarations et les opérations sur les objets de type union sont les mêmes que celles sur les objets de type struct. Dans l’exemple suivant, la variable hier de type union jour peut être soit un entier, soit un caractère.

union jour

{

char lettre; int numero; };

main()

{

union jour hier, demain;


517