Le monde de Karpok

Communiquons librement...
logo

Le mot du jour



Nous aurions souvent honte de nos plus belles actions si le monde voyait tous les motifs qui les produisent.

La Rochefoucauld

Le petit monde du C

Ce petit tutoriel n'a pas l'ambition d'être un cours sur le langage C. Il expose seulement les bases de la programmation en C. Il donne une vue d'ensemble du langage, et permet également au débutant de trouver quelques points d'ancrage important.
Il décrit le langage C tel qu'il est définit par la norme ANSI de l'ISO. À présent bonne lecture.

I. La petite histoire du C

La fin des année 60 est marquée par l'apparition d'un nouveau système d'exploitation, UNIX. L'idée est de mettre au point un système aussi indépendant que possible de la machine sur laquelle il va être implanté. Il devra être robuste, puissant et efficace. Pour faciliter sa mise au point et son portage sur différentes plateformes, un nouveau langage de programmation va être créé. Ce sera le C.
Ce nouveau langage voit en fait le jour en 1972, fruit des travaux de Denis Ritchie. Sa richesse opérationnelle va peu à peu lui permettre de connaître un grand succès au sein de la communauté des développeurs. Depuis, il n'a cessé d'évoluer.
En 1978 Ritchie et Kernighan publient la première définition rigoureuse du C, The C programming language.
Très rapidement, il va être normalisé afin d'unifier les différents compilateurs ayant vu le jour. La norme universelle aujourd'hui est celle de l'ISO, connu sous le nom de C ANSI (l'ANSI, American National Standard Institute ayant été le premier à normaliser le langage). 

II. L'environnement de programmation

Tout comme la plupart des langages "modernes", le C nécessite un environnement de programmation. Celui-ci comporte au moins un éditeur de texte, un compilateur, et un éditeur de lien.

1. Le C, un langage compilé

En effet, le C est ce que l'on appelle un langage compilé par opposition au Basic par exemple, qui est lui un langage interprété. Derrière ces mots se cache en fait une notion fort simple. Le processeur (ou le système d'exploitation) est incapable d'exécuter un programme C (on parle de source C). En effet, il s'agit d'un simple fichier texte. Pour que le processeur comprenne le programme, il faut le convertir dans le seul langage qu'il connaisse, le langage machine. Heureusement plusieurs utilitaires sont là pour remplir cette fonction.

2. Le préprocesseur

En fait, il ne s'agit pas d'un seul utilitaire, mais d'une chaîne d'utilitaires dont chaque membre a un rôle précis.
Le premier d'entre eux par ordre d'apparition est le préprocesseur. Il s'agit en fait d'un analyseur syntaxique. Il va réaliser un traitement sur toutes les lignes commençant par le symbole #.
Pour comprendre son rôle, il faut savoir que le langage C est à la base très bête. Il ne sait à l'origine faire que très peu de chose. Sa richesse provient des librairies qu'on peut lui ajouter. Celles-ci sont des morceaux de codes, déjà "compilés". Mais pour pouvoir les utiliser, il faut les décrire au compilateur. C'est l'un des rôles du préprocesseur, qui insère ces définitions, ces headers, dans le code source. Par exemple, la ligne de code suivant est très courante :

#include stdio.h

Cette ligne de code indique au préprocesseur qu'il doit insérer le fichier header stdio.h à cet endroit du code. Par la suite le programmeur pourra utiliser tous les outils de la librairie standard pour gérer les entrées sorties.
Plus généralement, le préprocesseur remplace un morceau de code par un autre :

#define PI 3.14159

Cette ligne définit la constante PI. Lorsque le préprocesseur rencontre cette ligne, il va parcourir tout le reste du code à la recherche des occurrences de PI, et les remplacer par 3.14159.
#include et #define sont les deux principales commandes du préprocesseur. Certains compilateurs en implémentent parfois d'autres qui leur sont propres.
Au passage nous soulignerons un abus de langage fréquent. On parle souvent du compilateur pour englober l'ensemble des opérations permettant de générer du code machine, bien qu'il n'en soit qu'une étape.

3. Le précompilateur

Son rôle est de préparer le travail du compilateur à proprement parler.

4. Le compilateur

C'est lui qui va convertir le source C en code machine, appelé code objet. Il ne s'occupe nullement de résoudre les références aux librairies. Mais transcrit les variables, les fonctions, les structures de boucles en langage machine. Il ne vérifie pas l'existence des diverses procédures ou fonction utilisées mais seulement que leur appel correspond à celui défini dans les fichiers header et/ou votre code.
Il fait la différence entre majuscule et minuscule, mais ignore les caractères d'espacement et tous les caractères compris entre un /* et le premier */ qui le suit ( ce sont des commentaires ).

5. L'éditeur de liens

C'est cet outil qui va terminer la génération d'un exécutable. Comme son nom l'indique, il va lier les différents fichiers objets de votre programme, et les librairies incluses.

6. Autres utilitaires

Les environnements de programmation proposent généralement d'autres outils. Le plus répandu est l'outil deboggueur, qui permet de suivre l'exécution du programme au niveau du code source. Cela peut s'avérer très utile pour localiser certaines erreurs. Le dernier outil que nous citerons ici est hérité du monde UNIX. Il s'agit de make. Celui-ci permet de spécifier sous forme de script les opérations à réaliser pour créer un exécutable à partir de vos sources. Cela peut inclure des directives de compilation, d'édition de liens, de renommage de fichier, de création de répertoires,... Certains compilateurs non UNIX, proposent une version moins évoluée de make, qui sert la plupart du temps uniquement à regrouper en une commande compilation et édition des liens.

III Les variables

Les variables sont les conteneurs de vos données. C'est elles que vous allez manipuler.
On distingue deux grandes familles de variables : globales ou locales. Les variables globales ont une existence et peuvent être manipulée dans l'intégralité de votre source. Les variables locales sont définies au sein d'une fonction, et n'ont aucun sens au dehors. Dans la pratique, on utilise le moins de variables globales possibles, par soucis de clarté, et d'économie mémoire.

1. Les types

En C comme dans la plupart des langages modernes, les variables sont typées. Cela veut dire qu'elles ne peuvent pas contenir n'importe quel type de données. Le C reconnaît les types suivant :

  • void : ne représente rien. Peut être utilisé pour définir les fonctions.

  • int : type entier pouvant stocker des valeurs comprises entre -32 767 et 32767

  • long : type entier permettant de représenter des valeurs entre -2 147 483 647 et 2 147 483 647

  • char : représente les caractères (en code ASCII)

  • float : pour les nombres à virgules flottante entre 1E-37 et 1E37 avec 6 chiffres significatifs

  • double : pour les nombres à virgule flottante entre 1E-37 et 1E37 avec 10 chiffres significatifs.

Les définissions que nous venons de donner sont celles prévues par la norme ANSI. Néanmoins les domaines numériques précisés ne sont que les domaines minimum. En effet suivant les machines, et le type de représentation des entiers, ces intervalles peuvent être plus grand. Par ailleurs le type char peut être utiliser pour stocker des entiers. Mais là encore suivant le type de représentation ASCII de la machine, le type char pourra contenir des entiers entre -127 et 127 ou 0 et 255.
D'autre part, nous n'avons définit là que les types de base. Tous les types précédemment énoncés (à l'exception de void) existent également sous forme non signée, identifié par le mot clé unsigned. Par exemple unsigned int définit un entier non signé compris entre 0 et 65 535 (ou plus). En effet l'intervalle des valeur négatif est alors déplacé dans le positif.
Il existe encore 3 autres types : 

  • short : entier court signé. La norme ne le différencie du type int, que par les opérations que l'on réalise dessus (cf opérateurs).

  • Byte : représente un octet mémoire

  • Pointeur : ce n'est pas vraiment un type. Nous en reparlerons plus loin.

Notons au passage que le C ne possède aucun type booléen. En fait dans les expressions logiques, toute valeur nulle sera interprétée comme fausse et toute valeur non nulle comme vraie.

2. Les modificateurs

Sous ce nom un peu barbare se cache en fait un terme très simple. Il apporte des propriétés supplémentaires aux variables.
Le premier de ces modificateurs est le modificateur const, qui précise une valeur constante. Par exemple pour définir une constante entière, on définira une variable de type const int. La valeur de cette variable est donné lors de sa définition (ex : const char txt='a';) et ne peut plus être changé après. Cependant le C ne considère pas ce genre de variables comme une véritable constante au sens du C. Nous en reparlerons lorsque nous introduirons les tableaux.
Un autre modificateur important est le modificateur static. Il est généralement utilisé pour des variables locales. Il indique que d'un appel à l'autre de la fonction la variable doit conserver la même valeur. Par exemple, si vous définissez une variable static int memoire, et qu'à la fin de votre fonction elle vaut 7, alors au prochain appel de votre fonction, elle vaudra toujours 7. Si aucune valeur n'est précisée lors de sa déclaration, elle est initialisée à 0. Par définition, une variable globale est toujours statique.
Le modificateur volatile quant à lui signale une variable qui ne peut être modifiée par un élément extérieur au programme. Un périphérique par exemple.
Le dernier modificateur est register. Il impose à la variable d'être stockée dans un registre du processeur. Ceci permet un accès plus rapide. Toutefois cela est très restrictif, dans la mesure où le nombre de registres du processeur limite son utilisation. Dans la pratique, ce modificateur est très rarement utilisé.

3. Les conversions de type

Le langage C prévoit deux mécanismes de conversions de type, et un mécanisme de promotion numérique.
Pour commencer, nous allons parler des conversions implicites. Elles concernent les variables de types numérique : int, long, float, double, et leurs dérivés non signés. Il est toujours possible d'attribuer une variable de l'un de ces types à une variable d'un autre type. Le compilateur effectuera les conversions qui s'imposent. Mais cela peut s'accompagner d'une perte d'information. Par exemple lors du passage de float à int. Lors de l'attribution d'un nombre à virgule à un nombre entier, le compilateur affectera uniquement la partie entière. Dans les autres cas de conversions impliquant une perte d'information, la norme ne définit pas le comportement du compilateur. Cependant si vous chercher à affecter un nombre négatif à un entier non signé, la plupart des compilateurs affecteront son complément à 2. De même, si vous chercher à affecter un nombre en dehors de l'intervalle définit pour un type, la plupart des compilateurs ignoreront tous simplement les bits de poids forts du nombre à affecter.
Les conversions implicites sont également utilisées par le compilateur lorsque vous réalisez des opérations mathématiques. Il convertira systématiquement tous les nombres dans un format commun, permettant de représenter tous les opérandes.

Par exemple dans l'exemple suivant : 

long a=5;
int b=2;
float x=1,4;

b=(a+b)*x; 

Le compilateur va commencer par convertir b en long afin de calculer a+b. Le résultat (7) sera stocké sous forme de long. Il le convertira ensuite sous forme de float, pour réaliser la multiplication avec x. Le résultat (9,8) sous forme de float sera ensuite convertit en int pour être attribué à b. Pour cela, la partie décimale sera tronquée. b contiendra alors la valeur 9.
Le C a également recours à la promotion numérique pour réaliser des calculs sur des short ou des char. Ces types permettent de représenter des entiers, mais ne peuvent êtres des opérandes pour le calcul. Ainsi, avant tout calcul, ils seront systématiquement convertis en entier. Concernant les affectations, short et char respectent les mêmes règles que int ou long.
Enfin, le C met un opérateur (cast) de conversion explicite de type, pour réaliser des conversions non prévues par la norme, en particulier sur des types définis par l'utilisateur. Si vous voulez affecter a de type baguette à b de type pain, il vous suffira de taper b = (pain) a;. Mais attention, le compilateur ne vérifie pas la faisabilité de ses conversions, et les résultats peuvent devenir imprévisibles.

IV La structure d'un programme

Une bonne programmation en C nécessite de bien comprendre la structure du source et de respecter des règles de bonne conduite.
Il est également recommandé de bien commenté son code. Cela peut éviter des erreurs, et permettra surtout à celui qui relira votre code, peut être vous même, de retrouver rapidement le sens et l'utilité des lignes que vous avez tapées.

1. La notion de fonction

Dans beaucoup de langage structurés, on distingue les notions de fonction et de procédure. Les fonctions sont des entités qui traitent des données, sans les modifier, et retournent un résultat. Au contraire des procédures qui ne retourne rien, mais peuvent modifier leurs paramètres.
En C, il n'y a a en fait ni fonction ni procédure, mais un mélange des deux. En effet, il s'agit d'entités qui peuvent retourner une valeur et/ou modifier leurs paramètres. Cependant, par abus de langage on continue de parler de fonctions si elles retournent un résultat et de procédures dans le cas contraire.
Cette notion de fonction (c'est le terme que nous emploierons dans tous les cas même si l'on parle en fait d'une procédure) est très importante car c'est elle qui permet d'organiser le code de façon logique. La signature d'une fonction est toujours de la forme :

Type de valeur retournée NOM ([paramètres])

Cette forme de signature est également valable pour les procédures. Dans ce cas on dit que la valeur retournée est de type void (rien). Dans tous les cas, vous devez indiquer cette signature avant de définir la fonction. Pour cela deux grandes tendances existent. Les uns préféreront les rassembler en tête de fichier source. Les autres créeront un fichier header, qu'ils incluront en début du source par l'instruction #include.
Le nombre et le type des paramètres est fixé une fois pour toute à la définition de la fonction. En particulier, le nombre de paramètres ne peut pas être variable. Il faut aussi noté que les paramètres sont des copies locales des valeurs passées au moment de l'appel de la fonction. Ainsi la modification d'un paramètre n'aura d'effet direct qu'au niveau local. Nous verrons comment contourner cet aspect à l'aide des pointeurs.
Le type de valeur retournée peut être un type propre au langage (cf III), un pointeur, ou même un type défini par le programmeur (y compris une structure). Il est également possible d'utiliser les modificateurs énoncés au chapitre précédent.
Les fonctions permettent de décomposer le programme en briques élémentaires, qui réalisent chacune un traitement élémentaire des données. Nous vous engageons vivement à décomposer votre programme au maximum. Cela s'avère très pratique si vous souhaitez réutiliser une partie de votre programme dans un autre contexte, y apporter des modifications, ou chercher une erreur.
Le nom de la fonction peut être quelconque dans la mesure où ce n'est pas un mot réservé par le langage. Il existe néanmoins une fonction élémentaire que l'on retrouve dans tout programme. Il s'agit de la fonction main, qui constitue le point d'entrée de votre programme. En d'autres termes c'est elle qui est exécutée au lancement de votre programme. Celle ci peut prendre deux arguments : argc de type entier (nous traiterons les types au III) et argv un tableau de caractères. Si votre programme est lancé depuis une ligne de commande (DOS par exemple) avec des arguments, argc contiendra le nombre d'arguments et argv leur valeur. Enfin main peut retourner soit void, soit un entier utile pour un traitement d'erreur par exemple.
Le corps d'une fonction est toujours délimité par une paire d'accolades. Le retour d'une valeur s'effectue grâce à l'expression return. La syntaxe est la suivante : return Valeur;. Valeur doit être du type retourné par la fonction à moins qu'une conversion implicite ne soit possible. Pour une fonction de type void, il n'est pas nécessaire de préciser une instruction return. On peut néanmoins utiliser l'instruction return;.

2. La segmentation

Comme nous l'avons vu. L'art de bien programmer en C, c'est aussi l'art de bien décomposer le travail à effectuer. L'utilisation de fonctions offre un certain niveau de séparation. Pour aller plus loin, on a recours à la segmentation. Elle consiste à utiliser plusieurs fichiers sources. Chacun dédié à un type de traitement. C'est l'éditeur de lien qui rassemblera tous ces morceaux de code.
Naturellement, votre source ne devra contenir qu'une seule fonction main, et si vous souhaitez utiliser dans un fichier source, une fonction définit dans un autre, vous devrez repréciser sa signature dans le nouveau source.

3. L'encapsulation

L'encapsulation est une technique consistant à masquer les données manipulée. Le programmeur ne détaille ni ne documente le type de variables sur lesquels il travaille, où la façon dont il les manipule. Cependant, il met un certain nombre de fonctions à la disposition des utilisateurs de son travail (ce peut être lui même). Cela permet un portage, et une amélioration du code plus facile. Pour être plus clair, nous allons travailler sur un exemple.

Imaginons que vous souhaitiez manipulez des cartes à jouer. Vous pourriez définir un type Carte (nous verrons comment dans un prochain chapitre). Il est inutile de dire, comment se comporte ce type et ce qui ce cache derrière son nom. Par ailleurs, vous fournirez des primitives (des fonctions) du type _NouvelleCarte, estCarteCıur, _FixerValeurCarte, qui permettront de réaliser toutes les opérations souhaitées sans avoir à ce préoccuper de la façon dont une carte est représentée en mémoire.
L'intérêt principal est que l'utilisateur du type Carte est obligé d'utiliser les primitives définies. Ainsi, dans le cas d'une modification de la définition du type Carte, une simple modification des primitives de base devrait assurer que le reste du code fonctionne toujours. Sinon, il faudrait vérifier chaque bout de code, pour s'assurer qu'il n'y a pas de conflit avec la nouvelle définition de Carte. Cela empêche également les utilisateurs du type Carte, de modifier son comportement de sorte que vos propres fonctions ne fonctionnent plus. Enfin c'est une bonne habitude à prendre pour la programmation en groupe. Chaque membre du groupe sait qu'il dispose de telles fonctionnalités sans avoir à se préoccuper de la façon dont elle a été mise au point.
Nous reviendrons prochainement sur les définitions de type, de structure, et par conséquent sur l'encapsulation.

V Les instructions C

Les instructions sont les opérations que vous exécutez au sein de votre programme. Ce peut être des boucles conditionnelles, des tests, des opérations arithmétiques, logiques ou binaires, ou encore des appels de fonctions. D'un point de vue conceptuel une instruction C est une expression C suivie d'un point virgule. Une expression C pouvant être une opération, un appel de fonction, ou un bloc d'instructions. Dans ce dernier cas, le point virgule n'est pas obligatoire. Attention, le C est un langage case sensitive, il fait la différence entre lettre majuscule et lettre minuscule.

1. Les instructions de boucles

Le langage C comporte 5 types d'instructions de boucles ou conditionnelles : while, do while, for, if, switch.
La première de ses instructions est l'instruction :

while(condition)
     Instruction;

Cette instruction exécute Instruction tant que la condition est vérifiée.
Une variante est la boucle :

do
    Instruction;
while(condition);

Avec cette construction Instruction sera exécutée au moins une fois.
Une autre variante de la boucle while, est la structure for :

for(étape initiale ; condition ; action)
    Instruction;

Cette construction exécute Instruction tant que la condition est vérifiée. On peut également préciser une instruction à réaliser lors du premier passage, et une action à réaliser à chaque passage. Typiquement l'étape initiale correspond à l'initialisation d'un compteur de boucle, condition à la valeur finale du compteur, et action à l'opération effectuée sur le compteur à chaque étape.
Le C possède aussi des instruction conditionnelles. En particulier la structure :

if(condition1)
    Instruction1;
else if(condition2)
    Instruction2;
else
    Instruction3;

Il peut y avoir autant de clause else if que vous le souhaitez. Mais dans le cas où toutes les conditions sont des cas d'égalité, une construction switch peut être intéressante :

switch(expression) {
    case Valeur1 :
        Instruction1;
    case Valeur2 :
        Instruction2;
    case Valeur3 :
        Instruction3;
    default :
        Instruction4;
}

On examine la valeur de l'expression. Dès qu'elle vaut une valeur listée, on exécute, l'instruction associée. Dans tous les cas la clause default est exécutée. Il peut y avoir autant de champs case que vous le souhaitez, et la clause default est facultative.
A chaque fois que nous avons fait figurée une instruction, il peut aussi bien y avoir un bloc d'instructions (entre accolades).

2.- Les opérateurs

Le C possède un certain nombre d'opérateurs arithmétiques, relationnels, logiques ou binaires, ainsi que d'autres opérateurs. Ceux-ci respectent les règles de priorité de l'algèbre classique. Le tableau ci-dessous les rassemblent par ordre de priorité décroissante de haut en bas

Catégorie

Opérateurs

Associativité

Signification

Référence

( )

gauche->droite

appel de fonction

[ ]

gauche->droite

accès à un élément d'un tableau

->

gauche->droite

Accès aux champs d'une structure

unaire

+

droite->gauche

identité

-

droite->gauche

opposé

++

droite->gauche

incrémentation

--

droite->gauche

décrémentation

!

droite->gauche

négation logique

~

droite->gauche

inversion de tous les bits d'un mot

*

droite->gauche

déréférenciation d'un pointeur

&

droite->gauche

adresse

(cast)

droite->gauche

conversion forcée

sizeof

droite->gauche

taille d'un objet

arithmétique

*

gauche->droite

produit

/

gauche->droite

division

%

gauche->droite

modulo (reste de la division entière)

décalage

>>

gauche->droite

décalage de bit vers la droite

<<

gauche->droite

décalage de bit vers la gauche

relationnels

<

gauche->droite

inférieur à

<=

gauche->droite

inférieur ou égal à

>

gauche->droite

supérieur à

>=

gauche->droite

supérieur ou égal à

==

gauche->droite

égal à

!=

gauche->droite

différent de

Manipulation de bits

&

gauche->droite

et bit à bit

^

gauche->droite

ou exclusif bit à bit

|

gauche->droite

ou bit à bit

logique

&&

gauche->droite

et logique

||

gauche->droite

ou logique

Conditionnel

? :

droite->gauche

équivalent à if(condition) instruction1 else Instruction2

affectation

=

droite->gauche

affectation usuelle

+=

droite->gauche

équivalent à x=x+valeur

-=

droite->gauche

équivalent à x=x-valeur

*=

droite->gauche

équivalent à x=x*valeur

/=

droite->gauche

équivalent à x=x/valeur

%=

droite->gauche

équivalent à x=x%valeur

&=

droite->gauche

équivalent à x=x&valeur

^=

droite->gauche

équivalent à x=x^valeur

|=

droite->gauche

équivalent à x=x|valeur

<<=

droite->gauche

équivalent à x=x<<valeur

>>=

droite->gauche

équivalent à x=x>>valeur

séquentiel

.

droite->gauche

Accès aux champs d'une structure

3. Les blocs d'instructions

Les blocs d'instructions sont des unités de codes entre accolades. Elles peuvent figurer partout où peut figurer une instruction. On peut définir des variables locales au sein d'un bloc d'instructions.

4. Les instructions de rupture de boucle

Elles sont au nombre de deux. La première est break. Elle provoque la sortie prématurée d'une boucle. Par exemple :

for(i=1 ; i<10; i++)
{
    titi+=5;

    if(titi==32)
        break;

    titi-=3;
}

provoquera la fin de la boucle for si la condition titi==32 est remplie et ce même si la condition de fin de boucle n'est pas remplie.
L'instruction continue; elle force le passage immédiat à la prochaine occurrence de la boucle.

for(i=1 ; i<10; i++)
{
    titi += 5;
    if(titi==32)
       continue;

    titi -= 3;
}

provoquera le passage au prochain cycle de la boucle si titi==32, n'exécutant pas l'instruction titi -=3.

VI Pointeurs et tableaux

Le langage C se distingue de la plupart de autres langages par la notion de pointeur et la gestion des tableaux. Ce sont là des points délicats sur lesquels buttent bon nombre de débutant. C'est pourquoi nous avons choisi d'y consacrer un chapitre entier et détaillé.

1. Les pointeurs

La notion de pointeur est généralement la plus difficile à assimiler en langage C. Pourtant derrière ce type de variable particulier et tous les traitements qui s'y applique se cache un fait très simple. Pour l'expliquez, regardons la façon dont une information est stockée en mémoire, l'entier 5 par exemple. L'application dispose d'un espace mémoire pour stocker des valeurs. Elle peut en particulier y stocker l'entier 5. Mais elle doit savoir où exactement elle a stockée cette valeur. Un peu comme vous, lorsque vous ranger une paire de chaussette dans un tiroir. Vous garder en mémoire dans quel tiroir vous les avez rangées. Et bien l'application raisonne de la même façon, elle mémorise l'"adresse" de l'entier 5 en mémoire. Par la suite, un pointeur sera tout simplement un type de variable particulier qui contient l'adresse d'une information en mémoire.
En fait il n'existe pas un type pointeur, mais autant de types pointeurs qu'il y a de type de variables. Vous pouvez définir un pointeur sur entier, un pointeur sur caractère, un pointeur sur une variable de type Carte, où même un pointeur sur pointeur. La syntaxe pour définir un pointeur est cependant toujours la même :

Type pointé * nom du pointeur;

Cette notion peut sembler bien abstraite au premier abord. Après tout si l'on stocke une valeur dans une variable, cette variable à un nom, et nous suffit pour retrouver notre valeur. Pourtant, les pointeurs sont à la base de tout programme C. Imaginez par exemple que vous souhaitiez travailler sur un fichier texte. Vous connaissez son chemin d'accès. Mais lorsque vous allez l'ouvrir, c'est l'application qui va attribuer l'emplacement mémoire du texte. Le chemin d'accès ne permet pas d'accéder à l'information texte, et aucune type de variable ne peut représenter un texte entier. Vous aurez alors besoin d'un pointeur, qui vous précisera où l'information de texte à été stockée et vous permettra d'y accéder.
Imaginons maintenant que votre programme ait besoin d'une liste d'informations spécifiées au moment de l'exécution par l'utilisateur, ses coordonnées par exemple. Vous ne pouvez lui redemander ces informations à chaque fois que vous en avez besoin. Vous devez donc les stocker de façon à pouvoir y accéder à n'importe quel endroit de votre code. Première possibilité, utilisée des variables globales. Mais cela risque de devenir lourd, en particulier si plusieurs utilisateurs interagissent sur le programme. Ou bien, vous utilisez des variables locales que vous traînez dans chaque appel de fonction. Toutefois, cela soulève un autre problème. Nous avons vu que les paramètres d'une fonction ne sont que des copies éphémères des variables effectivement passées en argument. Cela signifie que les modifications effectuées au niveau local, n'auront aucune influence au niveau de la fonction parente. Regardons cela sur un exemple :

void main()
{
    int i=1;
    int j=2;

    calcul(i, j);
    printf("i+j=%d et i-j=%d",i,j);
}

void calcul(int i, int j)
{
    int k=i;

    i+=j; /* on somme i et j */
    j=k - j; /* on soustrait j à la valeur initiale de i */
}

Ce petit programme est censé calculer la somme et la différence de i et j et afficher le résultat. Mais le résultat obtenu sera :

i+j=1 et i-j=2

En clair les calcul effectués au niveau de la procédure calcul n'auront pas été répercutée au niveau du main. Certes on pourrait contourner le problème en écrivant deux fonction retournant le résultat des opérations, mais cela deviendrait à nouveau lourd voir impossible dans des cas plus complexes.
En fait la solution consiste à utiliser des pointeurs.Le code deviendrait alors :

void main()
{
    int i=1;
    int j=2;

    calcul(&i, &j);
    printf("i+j=%d et i-j=%d",i,j);
}

void calcul(int *pi, int *pj)
{
    int k=*pi;

    *pi +=*pj; /* on fait la somme */
    *pj =k - *pj; /* et la différence */
}

Nous reviendrons plus loin sur le sens des * et des &. Ce programme affichera effectivement.

i+j=3 et i - j=-1

Que se passe-t-il en pratique ? En fait nous ne passons pas les variables entières elles-mêmes à la procédure calcul( ), mais des pointeurs sur ces variables. Bien que la procédure calcul travaille sur des copies locales de ces pointeurs, il n'y a qu'un seul exemplaire de i et de j en mémoire. La valeur stockée dans les pointeurs est toujours l'adresse du i et du j du main. En fait, pi et pj, contiennent les adresses où nous allons lire et écrire les valeurs de i et j.
Cela n'est qu'un avant goût de l'utilité réelle des pointeurs. Ils permettent aussi de stocker des collections de valeurs en mémoire. Mais nous y reviendrons lorsque nous parlerons des tableaux.
Il existe deux opérateurs fondamentaux pour l'utilisation des pointeurs. Le premier est & qui permet d'accéder au pointeur associé à une variable. Dans l'exemple précédent pour passer le pointeur sur i à la procédure calcul, nous avons utilisé &i.
Le second opérateur est *, à ne pas confondre avec l'* de déclaration des pointeurs. Contrairement à &, il permet d'accéder à la valeur pointée. Ainsi, dans l'exemple précédent, lorsque nous voulions accéder à i à partir de pi, nous avons utilisé *pi.
Nous venons de voir comment créer un pointeur à partir d'une variable. Dans ce cas, c'est le système qui s'occupe de gérer le stockage du pointeur en mémoire. Mais il est possible de créer un pointeur vide en lui lui allouant dynamiquement de la mémoire. Pour cela on a recours à la fonction malloc de la librairie standard, définie dans le fichier header stdlib.h. Par exemple :

int *i = (int *) malloc(sizeof(int));

Il est nécessaire de préciser la taille de l'espace mémoire à réserver. Pour cela, on peut utiliser l'opérateur sizeof( ), en lui passant comme paramètre le type de donnée pointée. Pour libérer l'espace mémoire réservée, on appellera :

free(i);

2. Les tableaux

En fait, il n'existe pas de type tableau en C. Mais l'association de la notion de pointeur avec l'opérateur [ ] donne l'illusion d'un type tableau. Par abus de langage, on appellera tout de même les entités manipulées tableaux.
Tout comme pour les pointeurs, il faut préciser le type de variables stockées dans le tableau. Par exemple :

char Chaine[10];

définit un tableau de 10 caractères. 10 est la taille du tableau. Elle est fixée une fois pour toute à sa création et ne peut pas être modifiée. C'est pourquoi la taille est forcément une constante définie. Cela signifie qu'une variable définie const, ne peut pas être utilisée pour spécifier la taille d'un tableau. Il n'est pas nécessaire de préciser la taille du tableau si elle peut être déduite du reste du code. C'est le cas lorsque le tableau et un paramètre d'une fonction (dans ce cas c'est la fonction appelant qui fixe sa taille), où si vous remplissez manuellement le tableau :

int Suite[]={1, 2, 3};

Dans la pratique un tableau est en fait un pointeur sur son premier élément. Cela a une conséquence fondamentale si vous passez des tableaux en argument de fonction. En effet vous passez uniquement un pointeur sur le premier élément du tableau, et rien ne vous assure que vous pourrez accéder aux autres éléments du tableau depuis la fonction. Dans ce cas, la meilleure solution consiste à travailler avec des pointeurs plutôt qu'avec des tableaux et à leur allouer la mémoire de façon dynamique à l'aide de malloc. Par exemple si vous souhaitez manipuler un tableau de 10 entiers, vous pourrez avoir recours à :

int *Suite;
    Suite=(int *)malloc(sizeof(int)*10);

Vous déclarerez ainsi des pointeurs, mais vous pourrez les manipuler comme s'il s'agissait de tableaux. Une telle déclaration fait de l'ensemble des 10 entiers, un tout indivisible, qui peut donc être passé en argument d'un fonction sans problème.
Les élément d'un tableau sont tous numérotés de 0 à Taille-1. L'accès aux élément d'un tableau Suite de taille Taille peut se faire de deux manières. Pour accéder au iéme élément, on peut utiliser l'analogie avec les pointeurs, et l'expression *Suite+i. Mais l'on préférera généralement l'opérateur [ ] par soucis d'homogénéité avec d'autres langage, et l'on utilisera l'expression Suite[i]. La norme ne prévoit pas le comportement du programme si l'on cherche à accéder à un élément en dehors du tableau.
En C, il n'existe pas non plus de type dédié à la représentation de chaîne de caractères. C'est pourquoi on utilise des tableaux de caractères. Vous pouvez par exemple déclarer une chaîne bonjour de la sorte :

char bonjour[ ]="Bonjour";

La donnée de la chaîne lors de la déclaration est suffisante pour que le compilateur puisse déterminer la taille à attribuer au tableau. Une chaîne C comporte toujours au moins un caractère, le caractère \0 qui marque la fin de la chaîne.

3. Les pointeurs de fonction

Le langage C introduit également la notion de pointeur de fonction. Il s'agit en fait d'une syntaxe permettant de passer une fonction en argument d'une autre fonction. Le pointeur se comporte ici comme l'adresse dans la mémoire programme de la fonction passée en argument. La définition d'une telle fonction se fait de la manière suivante :

int (*f1)(int, long);

Dans ce cas f1 est un pointeur sur une fonction retournant un entier et prenant en argument un entier et un entier long. Il faut savoir qu'une fonction est toujours appelée implicitement par un pointeur, et donc si l'on souhaite passer une fonction en argument d'une autre la syntaxe sera la suivante :

boolean estPositive(int (*calcul)(int, int), int a, int b);  /* signature d'une fonction testant le signe d'une opération */
int difference(int a, int b) ;                                                         /* signature d'une fonction retournant a-b */
int produit(int a, int b);                                                               /* signature d'une fonction effectuant le produit a*b */

/*.....*/

estPositive(difference,a,b);     /* Il est ainsi possible de traiter le produit et la différence à l'aide d'une seule fonction ... */
estPositive(produit,a,b);          /* ... qui appellera elle même la fonction de calcul adéquate */

VII Les fichiers

La programmation nécessite souvent de concerver des données d'une exécution du programme à une autre et de pouvoir les récupérer au besoin. C'est l'objet des fichiers. La manipulation de ces entités se fait à l'aide de pointeurs et de flux.
Un flux est une collection d'élément binaires se succédant les uns derrières les autres. En fait la manipulation de fichier se fait à l'aide de pointeurs sur des flux. La manipulation des fichiers se fait ensuite à l'aide des fonctions de stdio.

1. Marche à suivre pour manipuler un fichier

La première chose à faire est de charger le fichier en mémoire, on dit l'ouvrir. Cela se fait à l'aide de la fonction fopen qui prend en argument le chemin d'accès du fichier dans votre arborescence, ainsi que le mode d'accès au fichier. Les principaux modes d'accès sont "r" pour lecture seule, "w" pour écriture seule, "a", pour écriture à la fin du fichier, "b" pour préciser que l'on travaille sur un flux binaire (par défaut le flux est en mode ASCII), et "+" pour une ouverture en mise à jour (lecture + écriture). Il est possible de combiner ces modes. Ainsi :

FILE *f=fopen("toto","rb+");

va ouvrir le fichier toto du répertoire courant en mode lecture-écriture et en binaire. Pour lire ensuite le fichier, on peut utiliser la fonction size_t fread(void *adr, size_t taille, size_t nblocs, FILE *f). Celle ci va lire au plus nblocs de taille taille octets du fichiers f à partir de la position courante, stocker ces octets à partir de l'adresse adr, et retourner le nombre de blocs effectivement lus. Pour écrire, on dispose de size_t fwrite(void *adr, size_t taille, size_t nblocs, FILE *f). On peut également travailler avec des primitives de manpulation de texte adaptées aux fichiers (fscanf, fprintf, fgetc, fputc,...).
Lorsque l'on a fini de travailler sur un fichier, on le ferme à l'aide de l'instruction fclose(FILE *f) 

2. Positionnement dans un fichier

Pour se positionner à un endroit déterminer d'un fichier (utile par exemple pour sauter les information d'entêtes de certains types de fichiers), on dispose de int fsetpos(FILE *f, const fpos_t *pos). En général on ne définit pas soit même pos, mais on utilise la valeur retournée par int fgetpos(FILE *f, fpos_t *pos).
Pour se déplacer dans le fichier, on préférera int fseek(FILE *f, long deplacement, int origine) qui nous place à déplacement octets de la position d'origine. Les valeurs particulière de origine SEEK_SET et SEEK_END nous place respectivement en début et en fin de fichier.

VIII Structures et types abstraits

Nous avons déjà parlé des avantages de l'encapsulation. Nous allons ici présenter un outil d'encapsulation, les types abstraits. Mais auparavant nous allons monter comment il est possible de rassembler un ensemble de données disparates au sein d'un ensemble cohérent.

1. Structures et énumérations

Une énumération est un type de variable particulier qui ne peut prendre qu'un nombre fini de valeurs définies lors de sa création. Par exemple on peut définir de la sorte les couleurs d'un jeu de carte :

enum Couleur={trefle , carreau , pique , coeur};

qui est équivalent à 

enum Couleur={trefle=0 , carreau=1 , pique=2 , coeur=3};

On définit ainsi un type enum Couleur qui ne peut prendre que 4 valeurs : trefle, carreau, pique ou coeur.
Le langage C comporte également la notion de structure. Une structure est un type de variable qui contient plusieurs autres variables. Par exemple on pourrait représenter une carte à l'aide d'une structure comportant deux champs de type énumération :

struct Carte {
    enum Couleur coul;
    enum Valeur val;
};

Nous avons bien définit ici des types, et non pas déclaré des variables. Pour pouvoir utiliser des variables de ce type, il faut les déclarer à l'aide des instructions enum Couleur coul; ou struct Carte c1;.

On dispose de deux méthodes pour accéder aux champs d'une structure. Si l'on dispose d'une variable d'un type structure donnée, on utilisera l'opérateur . . Ainsi dans l'exemple précédent c1.Couleur désigne la couleur de la carte c1. Si l'on dispose cette fois d'un pointeur pc1 sur une structure de type Carte, la syntaxe (*pc1).Couleur est bien entendu valable, mais l'on dispose aussi de la syntaxe pc1->Couleur.

2. Les types abstraits

La notion de type abstrait est liée à la notion de types définis par le programmeur. Celui-ci a mis en place toute les fonctions nécessaires à la manipulation de ce nouveau type, et il se comporte comme s'il s'agissait d'un type de base. Dès lors, l'utilisateur du nouveau type n'a pas besoin de connaître la façon dont le nouveau type est implanté.
L'instruction de base pour définir un nouveau type est typedef . Il peut tout simplement s'agir du renommage d'un type déjà existant, ou la définition d'un type plus complexe. Imaginons que vous souhaitiez réaliser une sorte d'annuaire téléphonique. Vous pourriez définir les types abstraits suivant :

typedef char[50] Nom;                               /* Nom est un type équivalent à un tableau de 10 caractères */
typedef char[50] Prenom;
typedef char[100] Adresse;
typedef char[10]Telephone;

Par la suite, il est inutile de savoir que ce sont des chaînes de caractères qui se cachent derrière ces nouveaux types. C'est à vous de prévoir les fonction adéquates, pour pouvoir les utiliser sans savoir qu'il s'agit de chaîne de caractère. A partir de là vous pouvez définir une entrée dans le répertoire :

struct entree {
    Nom NomPers;
    Prenom PrenomPers;
    Adresse AdrPers;
    Telephone TelPers;
};

De la sorte, vous contrôlez la façon dont on accède à ces données, et évitez des pratiques incorrectes. De plus, si vous souhaitez modifier l'agencement de vos données, vous êtes certain qu'il suffira de modifier uniquement le comportement de vos fonctions pour que les modifications soient intégralement prises en compte. C'est également un mode de programmation très commode si vous faîtes parti d'un groupe travaillant en commun sur un même projet. Il est très agréable de savoir qu'il est inutile de se préoccuper de la façon dont tel type sera implémenté tout en sachant qu'il peut être manipulé.

IX Morceaux choisis de la librairies standard

Nous venons de voir les outils de base de la programmation en C. Toutefois, cela paraît bien pauvre pour un langage de haut niveau. En fait comme nous l'avons signalé, la richesse du C repose en grande partie sur les librairies qui lui sont ajoutées. La librairies standard fournit ainsi de nombreuses instructions, qui vous permettront de réaliser bon nombre de chose. Nous allons en présenter quelques une ici de façon assez générale. Ce n'est qu'un tour d'horizon des instructions les plus utilisées. Il existe de nombreux ouvrages qui vous donneront des informations plus complètes. Par ailleurs votre environnement de programmation dispose sans doute d'une documentation présentant, une partie de cette librairie.

Instruction

Descriptipon

Exemple

Remarques

Défini dans

double atof(const char *nptr)

Convertit une chaîne en double

atof("2.3");


stdlib.h

int atoi(const char *nptr)

Convertit une chaîne en entier



stdlib.h

long atol(const char *nptr)

Convertit une chaîne en long



stdlib.h

int close(FILE *Stream)

Ferme le fichier Stream

fp = fopen("CeFichier", "w");
fclose(fp);


stdio.h

File *fopen(const char *Nom, const char*mode)

Ouvre le fichier Nom en mode mode


Les principaux modes sont :
r: lecture
w: écriture, remplace le fichier s’il existait déjà
a: ajoute en fin de fichier

stdio.h

int fprintf(FILE *Stream, const char *format,…)

Ecrit une chaîne dans un flux

int i=3;
fp = fopen("CeFichier", "w");
fprintf(fp, "Dans ce cas i=%d", i);

Les formats de variables sont :
%d et %ld : nombre décimal (int et long)
%f et % lf : nombre à virgule flotante (float et double)
%s = chaîne de caractères
%c : caractère

stdio.h

int fputc(int c, FILE *Stream)

Écrit un caractère sur le flux Stream



stdio.h

void free(void *ptr)

Libère l'espace mémoire occupé par le pointeur ptr



stdlib.h

int fscanf(FILE *Stream, const char *format)

Lit une entrée depuis le flux Stream et le stocke dans format

int i;
FILE fp=fopen("CeFichier","r");
fscanf("%d",&i);

Le format est le même que pour fprintf

stdio.h

int getc(FILE *Stream)

Retourne le prochain caractère du flux



stdio.h

int getchar( )

Retourne le prochain caractère du flux standard


Equivalent à int getc(stdin)

stdio.h

void *malloc(size_t size)

Alloue de la mémoire à un pointeur

void *ptr=malloc(sizeof(int));

size peut être obtenu à l'aide de l'opérateur sizeof

stdlib.h

int putc(char c, FILE *Stream)

Écrit un caractère sur le flux Stream



stdio.h

int putchar(char c)

Écrit un caractère sur le flux standard


Equivalent à int putchar(stdout)

stdio.h

int scanf(const char *format,...)

Lit une entrée sur le flux standard


Equivalent à int fscanf(stdin, const char *Format)

stdio.h

char strcat(char *s1, const char *s2)

Concaténe s1 et s2

char s1[ ] = "Bonjour";
char s2[ ] = "monsieur";
strcat(s1,s2);         /* -> Bonjourmonsieur */

Le résultat de la concaténation est à la fois écrit dans s1, et retourné par strcat.

string.h

int strcmp(const char *s1, const char *s2)

Compare s1 et s2


retourne 0 si s1=s2,
un nombre positif si s1>s2,
et un nombre négatif si s1<s2

string.h

char *strcpy(char *s1, const char *s2)

copie s2 dans s1


Attention s1=s2, ne copie pas les chaînes, mais les pointeurs sur les chaînes.

string.h

size_t strlen(const char *s)

retourne la taille de la chaîne s


Attention s1=s2, ne copie pas les chaînes, mais les pointeurs sur les chaînes.

string.h

X Un exemple commenté

L'exemple suivant, vous montre un petit programme qui permet de créer une main de 5 cartes d'une même couleur. Ce petit programme est volontairement très fortement documenté afin de vous permettre d'en comprendre chaque étape. Il se constitue d'un fichier header dans lequel sont défini plusieurs type abstraits et d'un fichier source.

Carte.h : fichier header du programme 

typedef enum {sept, huit, neuf, dix, valet, dame, roi, as} Valeur;       /* On défini un type Valeur */
typedef enum {trefle, pique, coeur, carreau} Couleur;                           /* et un type Couleur */

typedef struct stCarte {              /* Une carte comprend */
    Couleur coul;                              /* une information de couleur */
    Valeur val;                                    /* et une information de valeur */
} Carte;                                              /* on les rassemblent au sein d'un nouveau type : Carte */

Carte.c : fichier source du programme

#include "Carte.h"              /* On inclut notre fichier header. On suppose qu'il est dans le même répertoire que Carte.c */
#include <stdio.h>                /* et quelques headers standards */
#include <stdlib.h>

#define TAILLE_MAIN 5                /* Nombre de cartes dans une main */
#define TAILLE_COULEUR 8      /* Nombre de cartes dans une couleur */

Couleur QuelleCouleur ();          /* on définit la signature des fonctions que l'on va créer */
void _CreerMain ( Carte *, Couleur );
void _Resultat ( Carte * );

int main()                                          /* L'exécution du programme commencera ici */
{
    Couleur coul;                               /* stockera la Couleur à trier */
    Carte *Main;                                 /* Une main sera un tableau de 5 cartes */

    Main = (Carte *)malloc(sizeof(Carte)*TAILLE_MAIN);              /* On réserve de la place pour notre main
                                                                              c'est pour pouvoir utiliser malloc que l'on a défini Main explicitement comme pointeur */
    coul=QuelleCouleur();             /* On demande la couleur à trier */
    _CreerMain(Main, coul);         /* On crée la main */
    _Resultat(Main);                        /* et on affiche le résultat */
    free(Main);                                   /* On libère la place occupée par Main */
}

Couleur QuelleCouleur()           /* retourna la couleur choisie */
{
    int iChoix;

    printf("Quel couleur souhaitez vous avoir ?\n »);            /* On affiche un petit menu */
    printf("\t1. Trefle\n");                                                                  /* \n est le caractère de fin de ligne */
    printf("\t2. Pique\n");                                                                  /* et \t une tabulation horizontale */
    printf("\t3. Coeur\n");
    printf("\t4. Carreau\n");
    printf("Votre choix : ");

    scanf("%d",&iChoix);                               /* on récupère le choix de l'utilisateur */
    return iChoix;                                              /* et on le retourne */
}

void _CreerMain(Carte Main[], Couleur Coul)
{
    int i;                                            /* sera notre compteur de boucle */
    unsigned int val;                   /* variable de travail sur la valeur */

    for(i=0;i {
        Main[i].coul=Coul;            /* on lui attribue sa couleur */

        do
        {
            val=(unsigned int)rand();            /* on tire en entier au hasard */
        } while(val>TAILLE_COULEUR);    /* jusqu'à ce qu'il convienne */

        switch(val)                              /* en fonction de cette entier, on attribue la valeur */
        {
            case sept :
                Main[i].val=sept;           /* on attribue alors la valeur */
                break;                                /* et l'on a fini */

            case huit :
                Main[i].val=huit;
                break;

            case neuf :
                Main[i].val=neuf;
                break;

            case dix :
                Main[i].val=dix;
                break;

            case valet :
                Main[i].val=valet;
                break;

            case dame :
                Main[i].val=dame;
                break;

            case roi :
                Main[i].val=roi;
                break;

            case as :
                Main[i].val=as;
                break;
        }
    }
}                                                /* On a généré une main */

void _Resultat(Carte Main[])            /* reste à l'afficher */
{
    int i;                               /* toujours notre compteur de boucle */

    for(i=0;i

Ce petit programme n'est pas un modèle parfait de bonne conception. Mais nous avons voulu le faire le plus simple possible, tout en faisant appel à un maximum de notion possible. Cela nous permet également de montrer ce qu'il faut éviter.
Pour bien faire les choses, il faudrait définir un type Main. Et définir des primitives pour travailler sur les types Couleur et Valeur. En effet, QuelCouleur est défini pour retourner une Couleur, et nous retournons un entier. Cela n'entraîne pas d'erreur, mais n'est pas du meilleur style. Nous refaisons par ailleurs cette analogie lors de l'affichage du résultat. D'autre part, on modifie ici les champs des Cartes directement, ce qui n'est pas très évolutif. Il vaudrait mieux créer des fonctions pour le faire, et masquer la définition du type Carte.
D'autre part, l'affichage du résultat n'est pas très explicite. Ce sont des chiffres, alors que des chaînes de caractères seraient plus explicites.
Bref, ce programme est à la fois un exemple de comment utiliser certaines notions, et un exemple de ce qu'il faut inviter. Nous vous invitons, à essayer de bien le comprendre et à le corriger afin de tenir compte des remarques ci-dessus.
Ce programme a été testé sur ©CodeWarrior IDE 3.0 de Metrowerksô, mais devrait fonctionner correctement sous d'autres environnements.

XI Bêtisier

Nous avons essayé de répertorier les principales grandes erreurs commis par les débutants. Ce sont pour la plupart des fautes d'inattention. Encore une fois, c'est à force de pratique, que vous finirez par ne plus les faire. Parmi les grands classiques on retrouve donc :

  • l'oubli du ';' en fin d'instruction.

  • utiliser une majuscule au lieu d'une minuscule et vice versa

  • l'oubli de définir le prototype (signature) d'une fonction avant son utilisation.

  • confusion entre l'opérateur d'affectation = et celui d'égalité ==

Attention, une syntaxe du type ci-dessous et correcte mais certainement différente de celle que vous souhaiteriez :

if(valeur=3)
    Instruction;

En effet, pour le langage C, vous affectez la valeur 3 à la variable valeur, ce qui retourne 3. 3 n'étant pas nul, le compilateur considère que la condition est toujours vérifiée, et donc l'instruction sera toujours exécutée.

  • passer une variable en paramètre d'une procédure au lieu d'un pointeur (si vous souhaitez modifier cette variable)

  • oublier d'allouer de la mémoire à un pointeur qui vous sert de référence sur une collection de Donnée

  • donner le même nom à une variable globale et à une variable locale.

XII Bibliographie

Comme nous l'avons signalé au début. Ceci n'est qu'un petit tour d'horizon du langage C. Pour en apprendre de plus, il existe de très bon ouvrage dans la littérature. Nous citerons en particulier :

  • La Référence du C norme ANSI/ISO de Claude Delannoy, Eyrolles 1998

  • Turbo C, mode d'emploi de Gabriel Lechner, Sybex 1990

  • Support de cours de l'INT par Christian BAC

©2001-2022 Karpok - Contact me