Cours de programmation en C par l'exemple

 

Le but de ce document est d'apprendre à programmer le plus vite possible, en découvrant la théorie petit à petit en même temps que la pratique, ceci dans le langage soit-disant réservé aux professionnels, et le plus déconseillé aux débutants.
Il s'adresse aux débutants, mais peut très bien convenir aussi aux programmeurs désirant apprendre le C (c'est d'ailleurs mon cas en ce moment !), car les préliminaires sont réduits à l'essentiel.

Les explications pour non-programmeurs sont en rouge.
Les termes à retenir sont en gras.
 
 

Petite présentation du C

 

Le C est un langage de programmation compilé, c'est à dire qu'il est traduit en langage machine par un compilateur, qui transforme le code source en code exécutable. D'autres langages (par ex. certains Basic) sont interprétés, et échappent à la phase de compilation : chaque instruction du code source est traduite en code exécutable juste avant son exécution.
 

Les langages compilés sont

- plus rapides (les instructions sont déjà en langage machine).
- moins souples : avant d'exécuter le programme, une compilation est nécessaire si le programme a été modifié.
- dépendants de la machine : la compilation est spécifique à chaque système (alias plate-forme matérielle). Pour utiliser le programme sur un autre système, il faudra reprendre le code source, et le recompiler spécifiquement pour ce nouveau système.

Les langages interprétés sont

- plus souples (pas besoin de compilation)
- souvent indépendants de la machine (le code source identique sera traduit différemment pour toutes les machines pour lesquelles un interpréteur de ce langage a été prévu).
- plus lents (l'interprétation en temps réel consomme de la puissance)

Le C est compilé donc rapide, mais assez portable (on parle de portabilité multi-plates-formes), ceci dans sa version normalisée ANSI (bien sûr après recompilation).

Il est très structuré et très exigeant au niveau syntaxique, c'est un langage d'assez bas niveau.

On parle de langage de bas-niveau pour un langage dont les instruction sont proches des instructions du processeur, par exemple additionner deux entiers, et de langage de haut-niveau pour un langage dont les instructions sont totalement détachées de celles du processeur, par exemple afficher une fenêtre de dialogue. Une instruction de haut-niveau peut représenter des millions d'instructions de bas-niveau.
Les codes sources en langage de haut-niveau sont donc beaucoup plus courts qu'en bas-niveau mais sont souvent plus lents, car en programmant en bas-niveau, on ne retient que les instructions strictement nécessaires, ce qui est impossible en haut-niveau (on ne peut rentrer dans le détail des opérations effectuées).
 
 

Mise en pratique

 

Ouvrez un éditeur de textes pour taper votre premier programme en C (tapez ce qui est en bleu)
 
 

Exemple A

Bonjour le monde !

 

A-1. Directives de précompilation

Hormis quelques instructions de base, les instructions du langage C sont contenues dans des bibliothèques. Pour éviter de surcharger le code executable, on n'y retient que les bibliothèques utilisées. Il faut donc préciser quelles bibliothèques vous comptez utiliser (c'est une directive passée au pré-compilateur, dans le code source lui-même).
Ici on va inclure (include) la bibliothèque d'entrées/sorties standard (il s'agit de tout ce qui permet à l'ordinateur de communiquer avec l'extérieur, comme l'écran, le clavier, l'imprimante, les lecteurs de disquettes, CD, disques durs). Celle-ci s'appelle en principestdio.h .

#<include stdio.h>

A-2. Corps du programme

On va maintenant commencer le programme lui-même, par main (principal en anglais, désigne la partie principale du programme par opposition aux sous-programmes).

void main (void)
{
Pourquoi void ?

Une fonction est une instruction renvoyant une valeur souvent à partir d'un ou plusieurs paramètres qu'on lui fourni.
exemple : valeur=fonction(parametre1, parametre2, ...);

Ici, on ne se soucie pas des paramètres ni de la valeur de renvoi, donc on les évite parvoid (éviter en anglais).

Pourquoi une accolade ouvrante { sur la ligne suivante ? Pour indiquer le début du programme indiqué parmain, la fin sera signalé par une accolade fermante } .

A-3. Contenu du programme

Quelque soit le langage, le premier programme à écrire pour l'apprendre consiste à écrire "Bonjour le monde !" à l'écran : cela permet de vérifier si le compilateur ou l'interpréteur fonctionne, et de savoir comment faire afficher le résultat d'un programme par la suite.

printf ("Bonjour le monde !\n");
Pourquoi printf ?Print signifie imprimer en anglais, f signifie formater.

Imprimer ne signifie ici ni imprimer sur l'imprimante, ni écrire à l'écran, mais envoyer vers la sortie standard, qui est généralement fixée sur l'écran, mais qui peut être détournée vers l'imprimante par exemple.

Formater consiste à spécifier (par des codes passés à la commande printf) de quelle manière il faut afficher ce qu'on lui donne en paramètre.
Ici, on utilise\n , ( \ , backslash annonçant un code, etn(sans doute pour le n de new line) faisant un saut à la ligne suivante).

Pourquoi un point-virgule en fin de ligne ? Pour signaler la fin de l'instruction et le début de la suivante. Historiquement, les instructions étaient séparées soit par un point-virgule, soit par un retour à la ligne (par exemple en BASIC). Seul la première solution a été conservée.
Attention, on ne met de point-virgule qu'après des instructions, pas après les repères faisant partie de l'articulation des instructions entre elles, commemain, if then else, for, while, until(que vous verrez plus tard).

}
Explication de cette accolade : plus haut, concernant main.

Listing complet de l'exemple A :

#include <stdio.h>
void main (void)
{

printf ("Bonjour le monde !\n");

}

A-4. Compilation et exécution

Après avoir enregistré votre code source, par exemple sous le nompremier.c, compilez-le. Je ne peux pas vous en dire plus sur ce point sauf si vous êtes sous Unix/Linux et disposez comme moi de GCC.

Dans ce cas, tapez la ligne de commande suivante : gcc premier.c

Maintenant, lancez votre programme exécutable, fabriqué par le compilateur. Dans mon cas, celui-ci toujours a.out (sauf si on demande un autre nom au compilateur).

Toujours si vous êtes dans mon cas, tapez./a.out (./ pour le chercher dans le répertoire courant au lieu de /usr/bin)

Normalement, ca marche.

Exemple B

Bonjour Toto !

 

B-1. Comment fournir des informations au programme

Ce nouveau programme ne dira plus "Bonjour le monde !", mais "Bonjour votre prénom", après vous l'avoir demandé. Pour cela, il faudra stocker votre prénom dans une variable (une case de la mémoire de l'ordinateur, dotée d'un nom). Ensuite on affichera le contenu de cette variable.
 

#include <stdio.h>
void main (void)
{
char prenom[50]="";
char est la commande pour déclarer une variable de type chaine de caractères. prenom est le nom de la variable, et [50] signifie que sa longueur maximale est de 50 caractères.

printf("Quel est votre prénom ?\n");
scanf("%s",prenom);
scanfest la commande permettant de lire des chaînes de caractère sur l'entrée standard, ici le clavier, jusqu'à ce que la touche <Entrée> soit pressée. f signifie là encore formatage, c'est à dire qu'on annonce à la commande scanf la forme des donées qu'elle recevra. % annonce un code, et s (pour string, chaine en anglais) signifie que la variable suivante (donc prenom) est une chaine de caractères.

printf ("Bonjour %s !\n",prenom);
Un nouveau code pour la commande printf :%s (le même code que pour scanf) signifie qu'il faudra afficher ici une chaine, qui est fournie comme paramètre plus loin. Ici, la variable prenom.

}

Listing complet de l'exemple B :

#include <stdio.h>
void main (void)
{

char prenom[50]="";
printf("Quel est votre prénom ?\n");
scanf("%s",prenom);
printf ("Bonjour %s !\n",prenom);

}

 
Comme avant, compilez et exécutez.
Exemple C

Bonjour Anne, Myriam, Cyril, Pierre !

 

C-1. Comment faire des opérations répétitives

Supposons un ordinateur particulièrement amical, qui voudrait dire bonjour à de nombreuses personnes, et ce avec leur prénom.
Avec 5 personnes, cela donnerait ça (n'essayez pas cet exemple...) :

 
#include <stdio.h>
void main (void)
{
char prenom[50]="";
printf("Quel est votre prénom ?\n");
scanf("%s",prenom);
printf ("Bonjour %s !\n",prenom);
printf("Quel est votre prénom ?\n");
scanf("%s",prenom);
printf ("Bonjour %s !\n",prenom);
printf("Quel est votre prénom ?\n");
scanf("%s",prenom);
printf ("Bonjour %s !\n",prenom);
printf("Quel est votre prénom ?\n");
scanf("%s",prenom);
printf ("Bonjour %s !\n",prenom);
printf("Quel est votre prénom ?\n");
scanf("%s",prenom);
printf ("Bonjour %s !\n",prenom);
}

Mais avec 300000 personnes ???

La solution : faire une boucle, répétée 300000 fois. Dans l'exemple, on se contentera de 10 fois...
 

#include <stdio.h>
void main (void)
{
char prenom[50]="";
int i;
int déclare un entier comme variable (int pour integer, entier en anglais). Cet entier va servir de compteur d'incrémentation pour la boucle : au nième tour, i vaudra n.

for (i=1;i<=10;i++)
C'est du chinois ? Alors lisez attentivement TOUT ce qui suit.
for est l'instruction permettant de répéter une boucle (entre accolades après le for, POUR des valeurs définies du compteur d'incrémentation, ici i. D'où l'anglais for.
Initialement, en Basic par exemple, la syntaxe était pour i de 1 à n (for i=1 to n). On fixait donc la valeur de début et de fin, et i s'incrémentait de 1 à chaque tour. En rajoutant step 2, on pouvait l'incrémenter par 2, ou par n'importe quel entier même négatif.
Le C a davantage de possibilités sur ce plan :
le premier paramètre de for assigne la valeur de départ à i, le second précise la condition nécessaire pour réexécuter la boucle (ici, i inférieur ou égal à 10, c'est comme si on indiquait traditionnellement la dernière valeur de i, mais on pourrait théoriquement raffiner davantage).
Enfin le dernier paramètre indique l'opération d'incrémentage à effectuer. Ici, c'est comme si on faisait i=i+1 (c'est à dire donner à i la valeur de i+1). Mais dans ce cas le processeur aurait d'abord donne à i la valeur de i, puis y aurait rajouté 1. On passe donc directement à la seconde étape (c'est 2 fois plus rapide), en notant i+=1. Mais l'informaticien-type étant paresseux, il existe un raccourci pour la valeur 1 (i++) ou la valeur -1 (i--).
{

printf("Quel est votre prénom ?\n");
scanf("%s",prenom);
printf ("Bonjour %s !\n",prenom);

}

}

Listing complet de l'exemple C :

#include <stdio.h>
void main (void)
{

char prenom[50]="";
int i;
for (i=1;i<=10;i++)
{
printf("Quel est votre prénom ?\n");
scanf("%s",prenom);
printf ("Bonjour %s !\n",prenom);
}

}
 
 

 

Après avoir vu comment importer et afficher des données, nous allons apprendre à les retraiter . Mais pour traiter un grand nombre de données, il est bien utile de les stocker, afin d'éviter de les saisir à chaque essai du programme de retraitement. Nous verrons donc d'abord le stockage des données.
 
 

Exemple D

Stockage de données

 

D-1. Les différents types de fichiers

Les données seront stockées sous forme de fichiers sous le disque. Mais il existe 2 sortes de fichiers : les fichiers séquentiels et les fichiers à accès direct.
- Les fichiers séquentiels se lisent et s'écrivent normalement donnée par donnée, du début à la fin du fichier.
- Les fichiers à accès directs procurent un accès direct à la donnée recherchée, sans avoir à lire les précédentes.

Nous ne traiterons ici que les fichiers séquentiels car ils sont plus simples (et car je n'ai pas encore étudié les fichiers à accès direct en C !!!)

D-2. L'exemple

Ce programme va écrire dans un fichier tous les prénoms rentrés par l'utilisateur (un éditeur de textes comme NotPad sous Windows, SimpleText sous Mac OS, VI sous Unix aurait très bien pu faire l'affaire, mais cela aurait été moins rigolo).
On commence comme d'habitude...

#include <stdio.h>
void main(void)
{
int i;
char prenom[30];

Ca se corse :
FILE *FICHIER1;
Ceci déclare le pointeur FICHIER.

FICHIER1=fopen("/home/chr/premierfichier.txt","w");
Nous ouvrons le fichier premierfichier.txt en écriture (fopen pour file open, fichier et ouvrir en anglais, w=write, écrire en anglais).
Le pointeur FICHIER servira désormais à désigner le fichier premierfichier.txt. En effet, celui-ci ne peut être travaillé directement, il faut préalablement l'ouvrir, dans un endroit de la mémoire (parfois appelé buffer, mémoire tampon en français) désigné par ce pointeur. Par convention, les pointeurs de fichiers sont souvent en majuscules, pour plus de lisibilité, car il s'agit souvent de la partie la plus cruciale des programmes, car la plus importante et la plus dangereuse (risque de perte de données).

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

printf("Rentrez un prénom :\n");scanf("%s",prenom);
fprintf(FICHIER1,"%s",prenom);
Au lieu d'écrire à l'écran avec printf, on écrit dans un fichier, désigné par son pointeur, ici FICHIER1 avec fprintf (le f devant pour file, fichier en anglais).

}

fclose(FICHIER1);
On fait le ménage, en fermant le fichier désigné par FICHIER1. C'EST OBLIGATOIRE. Tout ce qui précède ne sert à rien si l'on ne ferme pas le fichier.

Règles générales quant à l'ouverture et à la fermeture des fichiers : ouvrez-les aussi tard que possible, et fermez les aussi tôt que possible (dès la dernière modification). Car un fichier ouvert est une brèche de sécurité : si une coupure de courant ou un plantage intervient avant la fermeture d'un fichier, toutes les modifications seront perdues.

}
 

Vous pouvez vérifier le résultat très simplement en ouvrant le fichier créé avec un éditeur de texte (ou tapez more premierfichier.txtsous Unix).
Vous devriez voir à peu près ceci :

AnneCyrilChristopheMatthieuMyriamEléonore...

Comme vous pouvez le constater, c'est inexploitable. Il faut donc rajouter un séparateur entre les entrées du fichier, par exemple \n, ou | ou une virgule, ou n'importe quoi d'autre (mais le \n est plus pratique). Faute de quoi il ne sera plus possible de distinguer les différentes entrées du fichier entre elles sauf dans le cas où celles-ci sont de longueur fixe et connue.
Changez donc la ligne

fprintf(FICHIER1,"%s",prenom);

par

fprintf(FICHIER1,"%s\n",prenom);
On écrit \n (retour à la ligne) à la suite de chaque prénom, pendant l'inscription dans le fichier.
Attention, il arrive souvent que les entrées contiennent déjà le caractère de séparation. Dans ce cas, le fichier en contiendra 2 à la suite, c'est à dire une entrée vide. Source importante de bugs...

Listing complet de l'exemple D :

#include <stdio.h>
void main(void)
{

int i;
char prenom[30];
FILE *FICHIER1;
FICHIER1=fopen("/home/chr/premierfichier.txt","w");
for(i=1;i<=10;i++)
{
printf("Rentrez un prénom :\n");scanf("%s",prenom);
fprintf(FICHIER1,"%s\n",prenom);
}
fclose(FICHIER1);

}
 
 

Exemple E

Lecture et retraitement de données

 

Nous allons maintenant écrire un programme de traitement automatisé des données stockées ci-dessous. Imaginons que tous ces prénoms aient été tapés par Gaston Lagaffe, et aient pour but un publipostage (lettre identique sauf l'identitié du destinataire, envoyée à de nombreux exemplaires) auprès de 10000 de ses fidèles lecteurs, pour leur annoncer la prochaine BD. Malheureusement, les prénoms ont été rentrés en plusieurs fois, tantôt en minuscules, tantôt en majuscules, tantôt la première lettre seule en majuscules.
Désirant obtenir un résultat de qualité (au lieu de "Bonjour mATTHieu, j'ai le plaisir de vous annoncer la sortie de la nouvelle BD de Gaston Lagaffe"), mais ne voulant à aucun prix saisir à nouveau ces prénoms, Gaston vient vous voir.

Il faudra donc écrire un programme qui lit toutes les données du fichier, passe la première lettre de chaque prénom en majuscule et les suivantes en minuscule, puis les écrit dans un nouveau fichier. Nous ouvrirons parallèlement les deux fichiers, et chaque donnée sera écrite aussitôt lue et modifiée. C'est un procédé peu courant, mais plus simple que le procédé habituel qui sera étudié dans l'exemple suivant.
 

#include <stdio.h>

#include <string.h>
Cette bibliothèque contient des fonctions de manipulation de chaine de caractères. Juste ce qu'il nous faut ici.

void main(void)
{

int i,prem,l;
char prenom[30],prem[2];
prem servira à retenir le caractère en cours de conversion. Mais il faut penser au caractère de fin de chaine, donc longueur 2 !!!
FILE *LISTE1,*LISTE2;

LISTE1=fopen("prenoms.txt","r");
Ouverture en lecture seulement : rpour read, lire en anglais. Il s'agit du fichier source.

LISTE2=fopen("jolisprenoms.txt","w");
Ouverture en écriture seulement. Il s'agit du fichier destination.

Au lieu d'utiliser la boucle for, qui ne marche que pour un nombre connu d'enregistrements, nous allons utiliser la boucle while, car Gaston ne se rappelle plus très bien du nombre de prénoms du fichier. Cette instruction exécute la boucle suivante tant que la condition entre parenthèses qui lui est donnée est vraie.
Ici, nous allons lire dans le fichier jusqu'à la fin de celui-ci. La fonction feof(*FICHIER) renvoie VRAI si la lecture du fichier désigné par le pointeur *FICHIER est terminée. Il faudra donc utiliser la condition inverse, celle qui renvoie FAUX quand la lecture du fichier est terminée et VRAI autrement.. Pour inverser une fonction booléenne, il suffit en C de la précéder d'un point d'exclamation.
C'est de l'algèbre booléenne, du nom du célèbre mathématicien Boole qui a codifié les calculs portant sur des valeurs vraies ou fausses. C'est la base du fonctionnement de tous les ordinateurs actuels, ceux-ci ne manipulant que des 0 ou des 1. (0=faux et 1=vrai).
while (!feof(LISTE1))
{

fscanf(LISTE1,"%s\n",prenom);
Comme scanf, mais on lit dans un fichier au lieu de l'entrée standard (le clavier en principe). D'ou comme toujours le f de file.
Il faut préciser en plus dans quel fichier on veut lire, en indiquant son pointeur. On indique également dans la chaine de formatage que la chaine à lire se finit par \n. Si l'on avait utilisé un autre séparateur dans l'écriture du fichier, on l'aurait indiqué ici.

printf("Ancien prénom : %s",prenom);
On affiche à l'écran le prénom initial, pour contrôler le résultat.
 


Avant de passer à la suite, consultez la  table ASCII, qui indique les codes numériques correspondant aux caractères. Vous verrez que les majuscules et les minuscules sont décalées de 32 dans la table, y compris les caractères accentués. Pour mettre une majuscule en minuscule, il suffit de rajouter 32 à son code, et inversement. Or le C permet de manipuler un caractère aussi bien en tant que caractère qu'en tant que code. On peut donc additionner ou soustraire 32 à une lettre !!!

prem=prenom[0];
Uniquement pour simplifier les écritures. Attention, il s'agit d'un entier représentant un caractère par son code, pas d'un caractère.

Pour déterminer si la première lettre est en minuscule, nous allons utiliser une condition ou expression conditionnelle. En examinant la table ASCII, vous avez peut-etre remarqué que les minuscules sont regroupées des codes 97 à 122 (non accentuées) et 224 à 239 (accentuées). Donc si prem, le premier caractère de prenom, est compris dans ces valeurs, il s'agit d'une minuscule.
Faisons le test, sachant que && signifie ET et || signifie OU. En français, cela donne : SI (prem>=97 et prem<=122) ou (prem>=224 et prem<=239). En anglais, IF... Un éventuel SINON devient ELSE, un SINON SI devient ELSE IF
if ((prem>=97 && prem<=122)||(prem>=224 && prem<=253))

Attention à la syntaxe : la condition globale doit ^etre entre parenthèses comme chaque condition élémentaire. Toujours pas de point virgule.

{
prenom[0]-=32;
La première lettre de prenom est une minuscule, donc on lui soustrait 32 pour en faire une majuscule.
}

Nous allons maintenant règler le sort des lettres suivantes :
Pour chaque lettre du prenom, nous allons répéter l'opération effectuée sur la première lettre, mais à l'envers : si c'est une majuscule, nous en ferons une minuscule. Pour connaitre le nombre de tours de la boucle, nous allons utiliser la fonction strlen(), qui donne la longueur d'une chaine (cette fonction appartient à la bibliothèque string.h). On y enlève 1 car le premier caractère est déjà traité.
l=strlen(prenom)-1;
for (i=1;i<=l;i++)
Attention, cette boucle ne démarre pas au premier caractère, mais au second, le premier étant en fait le caractère à la position 0.
{

prem=prenom[i];
if (prem>=65 && prem<=90)||(prem>=192 && prem<=221)
Les valeurs sont changées par rapport à ci-dessus pour correspondre aux majuscules.
{
prenom[i]+=32;
Cette fois-ci, on RAJOUTE 32, pour en faire une minuscule.
}
 

}

Le prénom est maintenant correct, on va l'écrire dans le fichier destination.
fprintf(LISTE2,"%s\n",prenom);

L'écran affichera un compte-rendu de l'opération :
printf(" -> Nouveau prénom : %s\n",prenom);

}

Le dernier prénom a été lu, modifié, et écrit, il faut maintenant penser à fermer LES DEUX fichiers.
fclose(LISTE1);
fclose(LISTE2);

}

Vérifiez le contenu du fichier créé avec un éditeur de texte.

Listing complet de l'exemple E

#include <stdio.h>
#include <string.h>
void main(void)
{

int i,prem,l;
char prenom[30];
FILE *LISTE1,*LISTE2;
LISTE1=fopen("prenoms.txt","r");
LISTE2=fopen("jolisprenoms.txt","w");
while (!feof(LISTE1))
{
fscanf(LISTE1,"%s\n",prenom);
printf("Ancien prénom : %s",prenom);
prem=prenom[0];
if ((prem>=97 && prem<=122)||(prem>=224 && prem<=253))
{
prenom[0]-=32;
}
l=strlen(prenom)-1;
for (i=1;i<=l;i++)
{
prem=prenom[i];
if ((prem>=65 && prem<=90)||(prem>=192 && prem<=221))
{
prenom[i]+=32;
}
}
fprintf(LISTE2,"%s\n",prenom);
printf(" -> Nouveau prénom : %s\n",prenom);
}
fclose(LISTE1);
fclose(LISTE2);

}
 
 

 

Copyright Christophe Baegert 1999-2013