Documente Academic
Documente Profesional
Documente Cultură
■ le tri par
sélection
■ le tri shell
■ le tri rapide
(Quick
Sort)
■ le tri par
création
■ d'autres tris
■ recherches
■ la
recherche
séquentielle
■ la
dichotomie
■ calculs
mathématiques
■ calcul
vectoriel
■ polynômes
❍ tableaux
multidimensionnels
❍ conclusions
● LES TABLEAUX
DYNAMIQUES
❍ tableaux
unidimensionnels en C
❍ la méthode du
super-tableau
❍ les tableaux
multidimensionnels
■ matrices pleines
(matrices
rectangulaires
dynamiques :
mrd)
■ tableaux de
tableaux
dynamiques
❍ conclusions
● LES LISTES
❍ fonctions de base et
manipulations (base_lst)
❍ les tris
■ le tri bulle
■ le tri par insertion
■ le tri par sélection
■ le tri par création
■ les autres tris
❍ problèmes
mathématiques
❍ conclusions
● LES PILES ET FILES
❍ définition
❍ fonctions de base
❍ utilisations
● LES ARBRES
❍ introduction
❍ expressions
arithmétiques (arb_expr)
❍ listes triées
❍ les arbres généraux
● LES GRAPHES
● LES FICHIERS
❍ les fichiers séquentiels
❍ les fichiers à accès
direct
❍ l'indexation
● CORRECTION DES
EXERCICES
❍ BOUCLE
❍ TUSEXO_A
❍ TUSEXO_B
❍ GAUSS_MRD
❍ INSE_TTD
● recherche dans l'index de mon
document sur le langage C
Données et Algorithmique
Ce document décrit les structures de données et les algorithmes que l'on peut leur associer. Contrairement
à beaucoup d'ouvrages d'algorithmique, j'ai préféré mettre l'accent sur le choix des structures de données.
Il est avant tout important de définir coment modéliser le problème à traiter, ainsi que ses données. Ce
choix fait, on cherchera ensuite l'algorithme optimal, adapté aux types de données choisies. Ce document
n'est pas spécifique à un langage (il suffit qu'il soit structuré). Les exemples par contre sont tous donnés
en C (ANSI), mais sont facilement transposables dans un autre langage. Vous trouverez les informations
nécessaires dans mon document sur le langage C ou, pour un détail particulier, son index.
Autres sites sur l'algorithmique : Jean-Jacques Levy (Polytechnique), Jean Beuneu (EUDIL Lille), cours
de JL Bienvenu (CNAM Bordeaux), les polycopiés de Jean Fruitet (IUT Marne la Vallée)
● INTRODUCTION
● LES VARIABLES SCALAIRES
● LES TABLEAUX STATIQUES
● LES TABLEAUX DYNAMIQUES
● LES LISTES
● LES PILES ET FILES
● LES ARBRES
● LES GRAPHES
● LES FICHIERS
● CORRECTION DES EXERCICES
● Sommaire
INTRODUCTION
Lorsque l'on désire créer un programme répondant à un cahier des charges bien défini (condition
préalable évidement nécessaire), il faut déterminer quelles données il va falloir traiter, et comment les
traiter. La première étape est donc de choisir comment représenter en mémoire ces données, et si
plusieurs possibilités sont envisageables, choisir la plus appropriée aux traitements qu'il faudra effectuer
(c'est à dire celle pour laquelle les algorithmes seront le plus facile à mettre en oeuvre). Dans un gros
programme (C.A.O. par exemple), on appelle modèle la structure choisie pour stocker les données en
mémoire.
Une fois ce modèle défini, le programme doit être écrit de manière structurée, c'est à dire être décomposé
en petites entités (sous programmes, fonctions en C), réalisant chacune une tâche bien définie, en ayant
bien défini quelles sont les données nécessaires en entrée du sous programme, et quelles seront les
données retournées en sortie du sous programme (arguments ou dans certains cas variables globales). La
réalisation pratique de la tâche doit ne dépendre que de ses entrées et sorties, et n'accéder à aucune autre
variable (par contre elle peut utiliser pour son propre compte autant de variables locales que nécessaire).
Ceci permet d'éviter les effets de bord, qui rendent la recherche d'erreurs (débogage) presque impossible.
Le choix d'un modèle est capital : devoir le modifier une fois le programme bien avancé nécessite en
général la réécriture complète du programme, alors que modifier certaines fonctionnalités du programme
correspond à ajouter ou modifier des sous programmes sans modifier les autres. C'est un des intérêts de la
programmation structurée. Par contre, pour pouvoir plus facilement modifier le modèle, il faut des
structures de données hiérarchisées et évolutives (disponibles dans les langages orientés objets). Un autre
avantage de la programmation structurée est la possibilité de créer dans un premier temps chaque sous
programme réalisant une tâche déterminée grâce à un algorithme simple, puis d'optimiser uniquement les
sous-programmes souvent utilisés, ou demandant trop de temps de calcul, ou nécessitant trop de
mémoire.
Parlons encore de l'optimisation d'un programme. On n'optimise un programme (ou du moins certaines
parties) que si l'on estime que son fonctionnement n'est pas acceptable (en temps ou en consommation de
mémoire). On devra choisir un algorithme en fonction des conditions d'utilisation du programme (on ne
trie pas de la même manière un fichier totalement mélangé et un fichier déjà trié , mais avec quelques
valeurs non triées en fin de fichier). A partir d'un moment, on ne peut plus optimiser en temps et en
mémoire. Il faut alors choisir. Par exemple, un résultat de calcul qui doit être réutilisé plus tard peut être
mémorisé (gain de temps) ou on peut préférer refaire le calcul (gain de mémoire). Par exemple, il est rare
de passer par une variable intermédiaire pour utiliser deux fois i+1.
Sommaire général
PROGRAMMATION
Vous trouverez ici mes (gros) documents sur la programmation : C, C++, algorithmique, infographie,
Pascal). ainsi que la possibilité de télécharger les exemples (bien que le copier - coller sous Netscape ou
IE marche également très bien), mais aussi les corrections des exercices, la bibliothèque graphique, des
exemples supplémentaires, des sujets de TP, d'examens....
Copyright : utilisation de ces documents libre pour tout usage personnel. Utilisation autorisée pour tout
usage public non commercial, à condition de citer son auteur (Patrick TRAU, IPST, Université Louis
Pasteur Strasbourg, email : ) et de me signaler tout usage intensif. Utilisation commerciale interdite sans
accord écrit de ma part.
Cliquez sur l'icône (à droite) pour voter pour le site n°3464 du TOP Ouaibe (cours en
ligne de P. TRAU).
vous pouvez accéder à :
● Cours sur le C (ANSI) (y compris correction des exercices), qui détaille complètement le langage
C
● Données et Algorithmes : les différents algorithmes classiques, expliqués, comparés, en fonction
des types de données choisies. Chaque algorithme est accompagné de sa mise en oeuvre en C
● Infographie : comment dessiner sur un matériel (écran, imprimante...) sur lequel on sait au moins
allumer un point. Les tracés de base (droites, courbes...), les remplissages et hachurages, clipping,
tracés 3D,... La documentation de la bibliothèque graphique téléchargeable. Les exemples sont en
C ANSI.
● sommaire et instructions de téléchargement de la disquette d'accompagnement concernant les trois
documents cités ci-dessus (mais aussi la bibliothèque graphique en version Pascal)
● le C++ (ce document suppose que vous connaissez déjà le C)
● Cours de Pascal
● évaluez vous avec mes sujets d'examens (dont certains avec correction) (en particulier en C)
● Plus spécifiquement pour les étudiants d'IUP2 à l'IPST (les autres ont aussi le droit de regarder) :
les sujets et correction des TP (97/98 en version détaillée, et ceux de l'année en cours, au fur et à
mesure).
● Certains points du C expliqués par les étudiants de DEUG TI
La plupart de ces documents contiennent des frames (écran séparé en deux parties). Pour revenir à cette
page (et quitter les frames), une solution est de cliquer sur le bon (à vous de trouver lequel).
Bonne lecture !
Pour toute question, envoyez moi un , ou regardez mes FAQ. S'il vous plait, signalez moi toute erreur !
Liens externes : Vous cherchez un compilateur gratuit (C/Pascal) ? utilisez gcc, gpp sous Linux/Unix,
Sous DOS regardez l'ABC de la Programmation : Olivier Pecheux vous aide à démarrer (y compris à
installer le compilateur freeware DJGPP). si vous cherchez d'autres sites sur la programmation, en voici
des listes : en français ou en anglais.
Vous pouvez ici accéder aux autres informations sur ce serveur, normalement ou par carte.
❍ tableaux unidimensionnels en C
❍ la méthode du super-tableau
❍ les tableaux multidimensionnels
■ matrices pleines (matrices rectangulaires dynamiques : mrd)
■ tableaux de tableaux dynamiques
❍ conclusions
● LES LISTES
❍ fonctions de base et manipulations (base_lst)
❍ les tris
■ le tri bulle
■ le tri par insertion
■ le tri par sélection
■ le tri par création
■ les autres tris
❍ problèmes mathématiques
❍ conclusions
● LES PILES ET FILES
❍ définition
❍ fonctions de base
❍ utilisations
● LES ARBRES
❍ introduction
❍ expressions arithmétiques (arb_expr)
❍ listes triées
❍ les arbres généraux
● LES GRAPHES
● LES FICHIERS
❍ les fichiers séquentiels
❍ les fichiers à accès direct
❍ l'indexation
● CORRECTION DES EXERCICES
❍ BOUCLE
❍ TUSEXO_A
❍ TUSEXO_B
❍ GAUSS_MRD
❍ INSE_TTD
● recherche dans l'index de mon document sur le langage C
A
● accès
● direct [1]
[2]
❍ séquentiel [1] [2]
● allocation dynamique [1]
● arbre [1]
● binaire [1]
[2]
● argument [1]
B
● bande [1] [2]
● binaire [1]
● bit [1]
● boucles [1]
C
● index du langage C
● chiffres significatifs [1]
● clef [1] [2]
D
● débogage [1]
● décalage [1]
● décalages [1]
● défiler [1]
● dépiler [1]
● define [1]
● deuxaire [1]
● dichotomie [1] [2]
[3]
● dimension [1] [2]
● do-while [1]
● dynamique [1] [2]
[3] [4]
E
● effet de bord [1]
● empiler [1]
● encombrement [1]
● enfiler [1]
F
● feuille [1]
● fichier [1]
● FIFO [1]
● file [1]
● fils [1]
● float [1]
● for [1]
● free [1]
G
● Gauss [1] [2]
● graphe [1]
I
● index [1]
● indexation [1]
● infixé [1] [2]
● insertion [1] [2]
[3] [4] [5]
● interpolation [1]
L
● Lagrange [1]
● largeur de bande [1]
● LIFO [1]
● ligne de ciel [1] [2]
● liste [1] [2]
[3]
M
● malloc [1]
● mathématiques [1] [2]
[3] [4]
● matrice [1] [2]
[3]
❍ triangulée [1]
● modèle [1]
N
● noeud [1]
● notation polonaise [1]
● NULL [1] [2]
[3]
O
● objets [1]
● optimisation [1]
P
● parcours [1] [2]
[3]
● pile [1] [2]
[3]
● pivot [1]
● de Gauss [1]
● pointeur [1] [2]
[3]
● polynôme [1] [2]
● postfixé [1] [2]
● préfixé [1] [2]
Q
● queue [1]
● Quick Sort [1]
R
● récursif [1]
● récursivité [1] [2]
[3]
● racine [1]
● recherche [1] [2]
● retassage [1]
S
● séquentiel [1] [2]
● sentinelle [1]
● spline [1]
● stable [1]
● (tri) [1]
● stack [1]
● strcmp [1]
● structuré [1]
● suites [1]
● super-tableau [1] [2]
● suppression [1] [2]
[3]
T
● tableau [1] [2]
[3] [4] [5] [6] [7]
❍ dynamique [1]
❍ multidimensionnel [1]
❍ statique [1]
● ternaire [1]
● tri [1]
❍ bulle [1] [2]
● tri par
● arbre [1]
❍ comptage [1]
❍ création [1] [2] [3]
U
● unaire [1]
V
● vecteur [1]
● virgule flottante [1]
W
● while [1]
Langage C
Le Langage C
Aujourd'hui, l'informatique est présente dans tous les domaines de la vie courante, mais à des degrés
différents. Il y a pour cela trois grandes raisons :
- les gains (en temps, argent, qualité) que l'informatique peut apporter,
- le prix abordable des matériels,
- la disponibilité de logiciels dans tous les domaines.
Deux domaines sont pleinement exploités :
- les logiciels généraux, vendus en grande série, et donc relativement bon marché,
- les logiciels spécifiques, d'un coût total important et donc limités à des sujets très pointus, pour de très
grosses industries.
Le domaine intermédiaire, qui peut encore se développer, concerne les programmes spécifiques, pour des
applications de moindre importance. Pour cela, il est nécessaire de disposer de langages de
programmation. Les tableurs et bases de données par exemple disposent désormais de véritables langages
de programmation (souvent orientés objets) qui vont plus loin que les précédents langages de
macro-commandes. Pour les autres cas, le C est souvent le meilleur choix. En effet, c'est un langage
structuré, avec toutes les possibilités des autres langages structurés. Mais il permet également (avec son
extension C++) de gérer des objets. A l'inverse, il permet également une programmation proche du
langage machine, ce qui est nécessaire pour accéder aux interfaces entre l'ordinateur et son extérieur.
Mais son principal avantage est que ces trois types de programmation peuvent être combinés dans un
même programme, tout en restant portable sur tous les ordinateurs existants. Le langage C a néanmoins
deux inconvénients majeurs, c'est d'être un peu plus complexe d'utilisation (mais uniquement du fait de
ses nombreuses possibilités), et d'être séquentiel, ce qui ne lui permettra pas d'être le langage optimal
pour les machines massivement parallèles (mais aujourd'hui il n'existe pas encore de langage universel
pour ce type de machines qui puisse combiner efficacement des calculs procéduraux et du déclaratif).
Ceci est la première partie du livre "Programmation en C, langage et algorithmes", qui est composé de
trois parties.
* La première définit le langage C, avec de nombreux exemples directement dans le texte, et certains
supplémentaires en annexe.
* La seconde partie traite des structures de données et des algorithmes. Elle n'est pas spécifique, seuls les
exemples sont en C, le texte et les algorithmes restent utilisables dans tout langage séquentiel. Les
fonctionnalités du C non disponibles dans d'autres langages sont analysées, le méthodes utilisées pour
parvenir au même résultat sont précisées (par exemple, gestion d'un liste dynamique à l'aide d'un
tableau).
* La troisième partie quand à elle traite des algorithmes d'infographie. Elle est importante du fait de la
nécessité du graphisme dans les programmes. Mais elle détaille également, pour ces cas pratiques, les
méthodes et moyens utilisés pour optimiser des algorithmes. Une bibliothèque graphique est fournie en
annexe.
* Les parties que j'ai prévues, mais pas encore rédigées : utilisation des objets C++, programmation
graphique évennementielle (Microsoft Windows, X-Windows)
Langage C - Index
A,B,C,D,E,F,G,H,I,K,L,M,N,O,P,Q,R,S
,T,U,V,W
A
● accès direct [1] [2]
● accès séquentiel [1]
● addition [1]
● adresse [1]
● affectation [1] [2]
● alloc.h [1]
● allocation dynamique [1]
● ANSI [1] [2] [3] [4]
● antislash [1]
● arbre [1]
● argc [1]
● argument [1] [2] [3] [4] [5] [6]
❍ formel [1] [2]
❍ réel [1] [2]
● argv [1]
● associativité [1]
● atof [1]
● atoi [1]
● atol [1]
● auto [1]
B
● bibliothèque [1]
● bibliothèques standard [1]
● bit [1]
● blanc [1]
● bloc [1] [2] [3] [4]
● boucle [1]
● break [1] [2]
● buffer [1]
C
● calloc [1]
● caractère [1] [2]
● case [1]
● cast [1] [2]
● chaîne [1] [2]
● champ [1]
● char [1] [2]
● classe [1] [2]
● close [1]
● commentaire [1]
● conio.h [1]
● continue [1]
● contrôle [1]
● conversion [1]
● corps [1]
● ctype.h [1]
D
● déclaration [1] [2] [3]
❍ de type [1]
❍ globale [1]
❍ locale [1]
● décrémentation [1]
● default [1]
● define [1] [2] [3]
● deuxaire [1]
● directive [1]
● division [1]
● do while [1]
● double [1]
● durée de vie [1]
E
● else [1]
● entête [1]
● de fonction [1]
● entier [1] [2]
● entrées/sorties [1]
● enum [1]
● énumération [1]
● EOF [1] [2]
● errno.h [1]
● exit [1]
● expression [1] [2]
● extern [1]
F
● faire tant que [1]
● fclose [1]
● fcntl.h [1]
● feof [1]
● fflush [1]
● fgetc [1]
● fgets [1]
● fichier [1] [2]
● filelength [1]
● float [1]
● fonction [1] [2] [3]
● fopen [1]
● for [1]
● format printf [1]
● formaté [1] [2]
● fprintf [1]
● fputc [1]
● fputs [1]
● fread [1]
● free [1]
● fscanf [1]
● fseek [1]
● fwrite [1]
G
● getch [1]
● getchar [1]
● getche [1]
● gets [1]
● goto [1]
● goto calculé [1]
H
● handle [1]
● heap [1]
I
● identificateur [1]
● if [1] [2]
❍ imbriqué [1]
● ifdef [1]
● ifndef [1]
● include [1] [2]
● incrémentation [1]
● instruction [1]
● int [1]
● isalnum [1]
● isalpha [1]
● isdigit [1]
● islower [1]
● isspace [1]
● isupper [1]
K
● Kernigham [1]
L
● label [1]
● liste [1]
● long [1] [2] [3]
● longueur d'identificateur [1]
● lseek [1]
● Lvalue [1]
M
● macro [1]
● main [1]
N
● NULL [1] [2]
O
● opérande [1]
● opérateur [1] [2]
● open [1]
P
● paramètre [1]
● passage d'argument [1]
● pile [1] [2] [3]
● pointeur [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11]
● pour [1]
● pré-compilateur [1]
● printf [1] [2]
● priorité [1]
● produit [1]
● prototype [1]
● putch [1]
● puts [1] [2]
Q
● quote [1]
R
● récursivité [1]
● réel [1] [2]
● rand [1]
● read [1]
● realloc [1]
● register [1]
● return [1] [2]
● Ritchie [1]
● Rvalue [1]
S
● séquentiel [1]
● scalaire [1]
● scanf [1] [2]
● short [1] [2]
● si - Sinon [1]
● soustraction [1]
● sprintf [1]
● sscanf [1]
● static [1] [2] [3]
● stdio.h [1] [2]
● stdlib.h [1] [2]
● strcat [1]
● strcmp [1]
● strcpy [1]
● string.h [1]
● strlen [1]
● strncat [1]
● strncpy [1]
● struct [1] [2]
● structuré [1]
T
● tableau [1] [2] [3]
● tailles [1]
● tant que [1]
● tas [1]
● ternaire [1]
● tolower [1]
● toupper [1]
● typedef [1] [2] [3] [4]
U
● unaire [1]
● undef [1]
● ungetc [1]
● union [1]
● unsigned [1] [2] [3]
V
● variable [1] [2]
● variables locales [1]
● visibilité [1]
● visible [1]
● void [1]
W
● while [1]
● write [1]
codage binaire
Les seules valeurs que peut traiter l'ordinateur (et d'ailleurs tout système numérique) est le 0 et le 1 (en
fait, c'est nous qui représentons par 0 ou 1 le fait que le système numérique ait vu du courant ou non). Un
problème important est que différents types d'informations doivent être codés : instructions machine
(lignes de programmes), valeurs numériques, caractères, adresses,... et que rien ne permet de distinguer
dans un groupe de 0 et de 1 le type d'information qu'il est censé représenter (10010110 est sur un PC le
code machine permettant d'échanger les contenus des registres SI et AX, mais également le code du
caractère 'û', de l'octet signé décimal -106, du signé 150, l'exposant du flottant 1,23x10-32, etc...). Il est
donc capital d'associer à chaque mémoire que l'on utilise le type de donnée que l'on y mettra. C'est ce que
permet la déclaration des variables dans les langages évolués (explicite en Pascal et en C, implicite en
Basic et Fortran). Néanmoins cette erreur reste possible (en C, lors d'une erreur sur des pointeurs, par
l'utilisation des unions, ou simplement par erreur de format dans un printf).
Une autre erreur est due à la représentation des nombres négatifs. En effet, le signe ne peut être, lui aussi,
représenté que par 0 ou 1. Le codage choisi (pour les entiers 16 bits) fait que l'ajout de 1 à 32767 (le plus
grand entier signé) donne -32768 (cela devait de toute façon donner un nombre car soit il y a du courant,
soit il n'y en a pas, il n'est pas prévu de combinaison de 0 et 1 représentant une erreur). La plupart des
compilateurs ne signalent pas d'erreur en cas de dépassement de capacité de nombres entiers.
Autre problème, la codification des réels. Représenter la présence ou non d'une virgule par un 0 ou un 1
est évidement impossible (comment la reconnaître ?), la première solution envisagée était donc la virgule
fixe (un mot pour la partie entière, un autre pour la partie fractionnaire). On utilise désormais la "virgule
flottante" (d'où le nom de flottants ou float), représentée par un mot pour la mantisse (qui est un réel en
virgule fixe puisque pas de partie entière), un autre (souvent de taille différente) pour l'exposant (entier
signé). Ceci implique deux limitations : le nombre de chiffres significatifs (dû à la taille de la mantisse),
et le plus grand réel codifiable (dû à la taille de l'exposant, par exemple 2127=1,7.1038). On cherchera
donc à ne combiner que des réels du même ordre de grandeur. Par exemple en mécanique, ajouter à une
dimension d'un mètre une dilatation thermique d'un micron n'a de sens que si les flottants possèdent plus
de 6 chiffres significatifs, donc par exemple un algorithme cumulatif ne donnera pas de résultat si le pas
en température est trop faible.
Dans les deux cas (virgule fixe ou flottante), un problème se pose : en fait, nous ne pouvons représenter
qu'un sous ensemble des réels (je ne pense pas que les mathématiciens lui aient donné un nom) qui
correspond à la différence, en base 10, entre D et R. En fait on ne peut représenter que les nombres
pouvant s'écrire sous forme d'une partie fractionnaire comportant un nombre fini de chiffres (en binaire).
Or, comme c'est impossible en base 10 pour 1/3 (qui pourtant s'écrit 0,1 en base 3) la représentation en
binaire de 1/10 donne une suite infinie, qui est donc toujours tronquée (0,1d =
0,000100100100100100100...b). donc sur tout ordinateur calculant en binaire, (1/10)*10 donne
0,999999999... Cette erreur devient gênante dans le cas où le résultat du calcul précédent est utilisé dans
le calcul suivant, pour de grandes suites de calculs.
exemple (flottant):
#include <stdio.h>
void main(void)
{
float x=1000;
int i;
for (i=0;i<10000;i++)x+=0.1;
printf("On obtient %12.4f au lieu de 2000.0000\n",x);
}
Ce problème est moins flagrant en C que dans les autres langages, le C effectuant toujours les calculs sur
des réels en double précision. Il en résulte néanmoins que, par exemple, il ne faut pas tester dans un
programme si le résultat d'un calcul flottant est nul mais si sa valeur absolue est inférieure à un petit
nombre. On cherchera aussi, autant que possible, à choisir des algorithmes utilisant des entiers plutôt que
des réels, d'autant plus que les calculs sur des flottants sont plus lents que sur des réels.
boucles
Tous les langages possèdent des structures permettant de répéter des instructions, les boucles. En général,
certaines sont réservées aux cas où l'on connaît à l'entrée le nombre de boucles à effectuer (for en C), et
celles dont on connaît la condition d'arrêt (while et do-while en C). (voir exemples dans la première
partie).
Lorsqu'une valeur à calculer C dépend de n calculs intermédiaires Ci (que l'on ne désire pas mémoriser),
la méthode la plus simple consiste à initialiser C à la première valeur C1, puis pour i variant de 2 à n,
calculer chaque Ci et le cumuler à C. C'est en général la méthode utilisée pour les calculs de sommes,
produits, suites...
exemple (boucle_A) : calculer xn :
#include <stdio.h>
void main(void)
{
int n,i,x,result;
printf("entrez x et n : ");
scanf("%d %d",&x,&n);
result=x;
for(i=1;i<n;i++)result*=x;
printf("résultat : %d\n",result);
}
La fonction puissance est souvent disponible dans les langages (pow en C), mais correspond à un calcul
assez long et souvent moins précis (xn=exp(n*ln(x)) qui donne toujours un résultat avec une erreur de
précision même si x est entier). La méthode ci-dessus sera plus rapide soit pour n petit (quelques
multiplications d'entiers sont plus rapides qu'une exponentielle de logarithme), soit quand toutes les
puissances intermédiaires doivent être connues.
Exercice (boucle) : faire le programme calculant 2x4-3x3+x2-5x+2 pour x réel.
exemple (boucle_B) : calcul de moyenne :
#include <stdio.h>
void main(void)
{
int n=0;
float note,moy=0;
do
{
printf("entrez la note (négative pour terminer) : ");
scanf("%f",¬e);
if(note>=0)
{
moy+=note;
n++;
}
}
while(note>=0);
moy/=n;
printf("moyenne : %f\n",moy);
}
Les valeurs des notes ayant servi au calcul ne sont pas mémorisées, car toutes stockées successivement
dans la même mémoire.
récursivité
On appelle fonction récursive une fonction qui s'appelle elle-même. La récursivité n'est possible que dans
un langage acceptant des variables locales. En effet, l'emploi de la récursivité n'est réellement utile que
lorsqu'une fonction doit retrouver, après un appel récursif, toutes ses variables locales dans leur état
initial. Dans le cas contraire, une méthode itérative (boucle) sera en général plus efficace (il est inutile de
mémoriser les variables locales et les restaurer pour ne plus les réutiliser), comme par exemple pour le
calcul d'une factorielle (voir mon document sur le C, paragraphes 4.6 et 4.7, pour l'explication de la
tableaux unidimensionnels
généralités
Un tableau permet de regrouper dans une structure plusieurs valeurs scalaires de même type. Pour permettre une
maintenance aisée du programme, la dimension doit être définie par une constante. C'est cette dernière qui sera
utilisée pour les tests de dépassement (peu de compilateurs le font automatiquement, ils le feraient par exemple à
l'intérieur d'une boucle alors que le test sur la valeur finale serait suffisant). La taille réellement utilisée peut être
inférieure ou égale à la dimension du tableau, les composantes au delà de la taille utilisée peuvent être mises à 0
mais cela n'a aucun intérêt, sauf si on interdit le 0 dans le tableau (cas des caractères, en C).
Nous allons travailler dans un premier temps sur des tableaux de flottants. Nous utiliserons des tableaux dont le
premier indice est 0 puisque c'est la seule possibilité en C, et la solution optimale dans les autres langages
puisque nécessitant moins de calculs dans le code compilé. Si tous les tableaux utilisés ont la même dimension,
on peut définir de manière globale :
int i;
for(i=0;i<taille;i++) printf("%dième valeur :
%f\n",i+1,tab[i]);
}
Les déclarations globales et fonctions ci-dessus étant recopiées dans le fichier "base_tus", nous pouvons écrire le
programme suivant (ex_tus) :
#include <stdio.h>
#include "base_tus.inc"
#define dim 100
void main(void)
{
int nb;
composante v,t[dim];
init_tus(t,&nb,dim);
do
{
printf("entrez la %dième note (fin si <0 ou >20) :",nb+1);
scanf("%f",&v);
if(v<0||v>20) break; /* on aurait pu le mettre
dans la condition du while */
}
while (!ajoute_val_tus(t,&nb,dim,v));
affiche_tus(t,nb);
}
Exercice (tusexo_a) : modifier ce programme pour qu'il calcule la moyenne et affiche, pour chaque note, l'écart
avec la moyenne (ce qui nécessite l'utilisation d'un tableau car il faut d'abord calculer la moyenne puis utiliser les
notes mémorisées auparavant).
if(position>*taille)position=*taille;
if (*taille>=dim)
{
puts("dépassement de capacité du tableau");
return(2);
}
for(i=*taille;i>position;i--)tab[i]=tab[i-1];
tab[position]=val;
(*taille)++;
return(0);
}
Le décalage doit se faire par indice décroissant (par indice croissant, on recopierait progressivement la
composante à l'indice position dans tout le reste du tableau).
Les rotations sont également des manipulations fréquentes sur les tableaux :
tris
généralités (valables également pour d'autres types de données)
Les exemples qui suivent traitent des tableaux de flottants, les méthodes étant identiques pour tout type de
composante à condition d'y définir une relation d'ordre (par exemple, pour des chaînes de caractères on utilisera
strcmp au lieu de <, > et =). Mais il ne faut pas oublier que l'efficacité de l'algorithme dépend également des
types de données traitées : une comparaison de chaînes de caractères étant relativement longue, les algorithmes
effectuant beaucoup de tests seront moins efficaces qu'avec des flottants. De même, les tableaux trop gros pour
entrer en mémoire devront être traités sur support externe, rendant l'accès aux données (plusieurs millisecondes)
bien plus lent que les tests ou calculs (micro voire nanosecondes). Dans les cas plus complexes, comme par
exemple les tableaux de structures, on appelle clef le champ servant pour le tri (par exemple le nom pour un
tableau contenant nom, prénom, adresse,...). Dans ce cas, les calculs sur les clefs seront souvent plus rapides que
les déplacements des structures entières. Les méthodes de tris présentées ici sont souvent utilisables également
avec les autres types de données, mais les conclusions sur leur efficacité varieront. On qualifiera de stable un tri
laissant dans le l'ordre initial les éléments de clef identique (par exemple, un stock saisi au fur et à mesure des
arrivées de matériel, le classement par ordre alphabétique gardant les matériels de même nom dans leur ordre
d'arrivée sera stable). Tous les algorithmes décrits ici sont stables, à condition d'y prendre garde (scruter le
tableau du début vers la fin et non l'inverse par exemple). Dans ce chapitre, nous noterons N le nombre de
composantes du tableau (appelé taille auparavant).
le tri bulle
Cet algorithme est relativement connu, bien qu'il soit rarement efficace (en termes de temps de calcul, le tri est
néanmoins correct, bien évidement). Il consiste à balayer tout le tableau, en comparant les éléments adjacents et
les échangeant s'ils ne sont pas dans le bon ordre. Un seul passage ne déplacera un élément donné que d'une
position, mais en répétant le processus jusqu'à ce plus aucun échange ne soit nécessaire, le tableau sera trié.
(bull_tus)
Plutôt que de déplacer les éléments d'une position, on peut prendre un élément après l'autre dans l'ordre initial, et
le placer correctement dans les éléments précédents déjà triés, comme on le fait lorsque l'on classe ses cartes à
jouer après la donne (inse_tus) :
for(pt=1;pt<N;pt++)
{
dpg=pt-1;
tampon=tab[pt];
while(tab[dpg]>tampon&&dpg>=0)
{tab[dpg+1]=tab[dpg];dpg--;}
tab[dpg+1]=tampon;
}
Le tri par insertion peut être intéressant pour des tableaux ayant déjà été triés, mais où l'on a rajouté quelques
nouveaux éléments en fin de tableau (dans ce cas il faut améliorer l'implantation pour découvrir rapidement le
premier élément mal placé, puis utiliser l'algorithme complet pour les éléments restants). Dans les autres cas, il
sera plutôt réservé aux types de données permettant une insertion rapide (listes chaînées par exemple).
Le but est désormais de déplacer chaque élément à sa position définitive. On recherche l'élément le plus petit. Il
faut donc le placer en premier. Or cette position est déjà occupée, on se propose donc d'échanger les deux
éléments. Il ne reste plus qu'à répéter l'opération N fois (sele_tus):
{
pp=pd;
for(i=pp+1;i<N;i++) if(tab[i]<tab[pp])pp=i;
tampon=tab[pp];
tab[pp]=tab[pd];
tab[pd]=tampon;
}
}
Chaque échange met un élément en position définitive, l'autre par contre est mal placé. Mais aucun échange n'est
inutile. Un élément qui a été bien placé ne sera plus testé par la suite. Le nombre de boucles internes est environ
N(N-1)/2, ce qui est meilleur que le tri bulle, mais toujours de l'ordre de N2. Par contre le nombre de
déplacements d'éléments est au maximum de 2(N-1), la moitié étant des déplacements nécessaires, ce qui est
faible pour un fichier en désordre total, mais n'est pas optimal pour les fichiers dont la première partie est déjà
classée (et grande par rapport à la taille totale). Une amélioration possible serait d'essayer de placer le second
élément de l'échange dans une position pas trop mauvaise, à condition que cette recherche ne soit pas elle même
plus gourmande en temps.
le tri shell
C'est une amélioration du tri par insertion : au lieu d'effectuer une rotation de tous les éléments entre la position
initiale et finale (ou du moins meilleure) d'un élément, on peut faire des rotations par pas de P, ce qui rendra le
fichier presque trié (chaque élément sera à moins de P positions de sa position exacte). On répète ce tri pour P
diminuant jusqu'à 1. Une suite possible pour P est de finir par 1, les pas précédents étant de 4, 13, 40, 121, 364,
1093... (Pi=3*Pi-1 +1). D'autres suites sont évidement possibles, à condition de prendre des valeurs qui ne soient
pas multiples entre elles (pour ne pas toujours traiter les mêmes éléments et laisser de côté les autres, par exemple
les puissances successives de 2 ne traiteraient que les positions paires, sauf au dernier passage. Exemple
d'implantation (shel_tus) :
Ce tri est récursif. On cherche à trier une partie du tableau, délimitée par les indices gauche et droite. On choisit
une valeur de ce sous-tableau (une valeur médiane serait idéale, mais sa recherche ralentit plus le tri que de
prendre aléatoirement une valeur, par exemple la dernière), que l'on appelle pivot. Puis on cherche la position
définitive de ce pivot, c'est à dire qu'on effectue des déplacements de valeurs de telle sorte que tous les éléments
avant le pivot soient plus petits que lui, et que toutes celles après lui soient supérieures, mais sans chercher à les
classer pour accélérer le processus. Puis on rappelle récursivement le tri de la partie avant le pivot, et de celle
après le pivot. On arrête la récursivité sur les parties à un seul élément, qui est donc nécessairement triée.
(Quick_tus)
longues et gourmandes en mémoire du fait de la récursivité: chaque appel de fonction est assez long, il faut
mémoriser l'état actuel. On peut optimiser le tri en remplaçant la récursivité par des boucles (obligatoire si le
langage utilisé n'est pas récursif), ce qui évite d'empiler des adresses, mais la gestion des variables locales doit
être remplacée par gestion par pile (voir plus loin) pour mémoriser les sous-tris en attente, ce qui permettra
d'accélérer le tri mais nécessite une programmation complexe (rappel : la récursivité sera automatiquement
supprimée par le compilateur, cette transformation par le programmateur peut être plus efficace).
Plutôt que d'arrêter la récursivité sur des sous-tableaux de taille 1, on peut s'arrêter avant (entre 5 et 25 en
général) pour éviter une profondeur de récursivité trop importante. Le fichier est alors presque trié, on peut alors
effectuer un tri par insertion qui dans ce cas sera très rapide. Une autre amélioration possible est de mieux choisir
le pivot. la solution idéale est de trouver à chaque fois la valeur médiane du sous-tableau à trier, mais sa
recherche précise rend le tri plus lent que sans elle. Une solution quelquefois utilisée est de prendre par exemple
trois valeurs, pour en prendre la valeur médiane, par exemple tab[droite], tab[gauche] et tab[(droite+gauche)/2]
(dans le cas d'un fichier parfaitement mélangé, le choix de trois positions n'a pas d'importance, mais dans des
fichiers presque triés le choix ci-dessus est plus judicieux). La totalité de ces améliorations peut apporter un gain
de l'ordre de 20% par rapport à la version de base.
Lorsqu'il est nécessaire de disposer simultanément du tableau initial et du tableau trié, on peut recopier le tableau
initial puis effectuer un tri sur la copie, ou adapter un des algorithmes précédents. Par exemple, à partir du tri par
sélection, l'algorithme consiste à rechercher l'élément le plus petit, le copier en première position du tableau final,
rechercher le suivant, le placer en seconde position, etc... En cas d'éléments identiques, il y a lieu de marquer les
éléments déjà choisis, par exemple à l'aide d'un troisième tableau d'indicateurs (le tri est alors stable), ou suivant
l'exemple (crea_tus) ci-dessous (ceci n'est qu'un exemple, d'autres possibilités existent) :
imin=le_suivant(ti,taille,imin);
tf[i]=ti[imin];
}
}
On peut remarquer que ce tri minimise le nombre de copies des éléments, mais nécessite beaucoup de
comparaisons de clefs (en particulier un élément déjà sélectionné sera encore comparé par la suite). Ceci peut être
acceptable pour un fichier séquentiel à grands champs, sur bande par exemple, mais dont les clefs peuvent être
stockées complètement en mémoire. On verra d'autres propositions dans le cas des fichiers.
d'autres tris
Suivant les données à trier, il peut être plus efficace de construire un algorithme de tri spécifique. Par exemple, si
le tableau contient un grand nombre de valeurs similaires (exemple : gestion annuelle d'un stock où la plupart des
articles entrent et sortent plusieurs fois par jour), on peut utiliser l'algorithme simple (par création) consistant à
rechercher l'élément le plus petit, compter le nombre de ces éléments, les mettre dans le tableau destination, et
répéter l'opération jusqu'à la fin du fichier destination. C'est le tri par comptage. Dans le cas où le nombre de clefs
différentes est suffisamment faible, on peut utiliser un tableau de compteurs, ce qui permet d'effectuer le
comptage en un seul balayage du fichier.
Dans la cas où les clefs sont bornées (c'est à dire comprises entre un minimum et un maximum connus à l'avance)
et en nombre fini, on peut utiliser le tri basique : par exemple si toutes les clefs sont des entiers entre 000 et 999,
on peut séparer le tableau en 10 parties en fonction des centaines, puis récursivement traiter les dizaines puis les
unités (tri en base 10). Evidement, un tri en base 2 sera plus efficace sur ordinateur : on part à gauche ,on avance
jusqu'à trouver un nombre commençant par 1, puis par la droite jusqu'à trouver un nombre commençant par 0, les
échanger et continuer jusqu'à croisement des deux côtés. Puis on recommence (récursivement par exemple) sur le
bit suivant, jusqu'à tri complet. Pour trier des clefs alphabétiques, on peut effectuer un tri en base 26, sur les N
premiers caractères (N pouvant valoir 2 ou 3 par exemple), le fichier est alors presque trié. Il est alors plus
efficace d'effectuer un tri par insertion (passe de finition) plutôt que de répéter le tri basique jusqu'à tri complet.
Le tri par fusion utilise un algorithme de fusion de deux tableaux triés en un seul plus grand, appelé
récursivement sur les deux moitiés du tableau, jusqu'à une taille de tableau de 1 (ou plus, avec un tri spécifique
pour petits tableaux, par exemple par échange sur des sous-tableaux de 3 éléments)
recherches
On a souvent besoin de rechercher, dans un grand tableau, la position d'un élément donné. Un point particulier à
ne pas oublier pour tous les algorithmes est le traitement du cas où l'élément cherché n'est pas dans le tableau.
Une autre caractéristique importante d'un algorithme de recherche est son comportement désiré en cas d'éléments
identiques (doit-il donner le premier, le dernier, tous ?).
la recherche séquentielle
Il suffit de lire le tableau progressivement du début vers la fin. Si le tableau n'est pas trié, arriver en fin du tableau
signifie que l'élément n'existe pas, dans un tableau trié le premier élément trouvé supérieur à l'élément recherché
permet d'arrêter la recherche, de plus cette position correspond à celle où il faudrait insérer l'élément cherché
pour garder un tableau trié. Une recherche sur un tableau trié nécessitera en moyenne N/2 lectures, mais on se
rapprochera de N pour un fichier non trié avec beaucoup de recherches d'éléments inexistants.
{
int i;
for(i=0;i<N;i++)if(tab[i]==val)return(i);
return(-1);
}
la dichotomie
Dans le cas d'un tableau trié, on peut limiter le nombre de lectures à log(N)+1, en cherchant à limiter l'espace de
recherche. On compare la valeur cherchée à l'élément central du tableau, si ce n'est pas la bonne, un test permet
de trouver dans quelle moitié du tableau on trouvera la valeur. On continue récursivement jusqu'à un sous-tableau
de taille 1. Il vaut bien mieux implanter cet algorithme de manière itérative, car la fonction se rappelle jusqu'à
trouver la position désirée, puis seulement on effectue les dépilages, alors que l'on n'a plus besoin des états
intermédiaires qui ont été mémorisés par la récursivité puisque le problème est résolu.
calculs mathématiques
Les tableaux unidimensionnels permettent de résoudre simplement divers problèmes mathématiques. Nous allons
en traiter certains.
calcul vectoriel
L'utilisation des tableaux statiques est bien indiquée pour le calcul vectoriel. En effet, toutes les variables seront
de même dimension. Il est aisé de prévoir une bibliothèque de fonctions vectorielles comportant toutes les
opérations de base (multiplication par un réel, produit scalaire, produit vectoriel, produit mixte, norme...). Un
grand nombre de problèmes géométriques se résoudront bien plus facilement par calcul vectoriel que de manière
paramétrique.
polynômes
Une manière (mais il y en a d'autres) de représenter un polynôme est le tableau de ses coefficients. Par exemple
f(x)=4x3+x+2 sera représenté par le tableau 2,1,0,4 (en mettant le coefficient de xi en position i). L'évaluation
d'un polynôme (c'est à dire le calcul de sa valeur pour un x donné) ne doit pas utiliser de fonction puissances mais
uniquement des multiplications successives, puisque toutes les puissances intermédiaires sont nécessaires. La
somme de polynômes est triviale (somme des coefficients), le produit à peine plus compliqué (à moins que l'on
ait besoin d'une optimisation poussée, dans ce cas on peut réduire d'1/4 le nombre de multiplications mais avec
une complexité accrue). La résolution de l'équation f(x)=0 (recherche de racines) peut se faire par dichotomie par
exemple (il faut alors donner le premier intervalle de recherche), en cas de racines multiples on en trouve une au
hasard. Une recherche de racines plus efficace nécessite des algorithmes complexes, rarement universels (c'est à
dire que dans certains cas ils ont un résultat déplorable, voire faux ou plantage de l'ordinateur en cas de
divergence) qui ne seront pas traités ici, mais une littérature abondante existe sur ce sujet.
L'interpolation polynomiale correspond elle à la recherche d'une courbe polynomiale passant par N+1 points
donnés : P(xi)=yi pour i entre 0 et N. La solution la plus simple consiste à choisir le polynôme de Lagrange
(d'ordre N):
N N
P(x)= ( yj (x-xi)/(xj-xi) )
i=0
j=0
i!=j
qui est le plus rapide à déterminer mais donne des résultats décevants pour N assez grand (100 par exemple),
puisque les seules conditions imposées sont des points de passage, on obtient (souvent près des points extrêmes)
une courbe assez "folklorique" entre certains points. Une méthode souvent plus satisfaisante est l'utilisation des
"splines", qui cherche parmi les multiples polynômes d'ordre N passant par N+1 points celui qui minimise une
quadratique (en fait, correspond à la minimisation de l'énergie de déformation d'une poutre passant par ces points,
d'où le nom de spline, "latte" en anglais) (voir mon support de cours sur l'infographie). Ou alors on peut utiliser
une approximation (polynôme d'ordre M<N) passant au voisinage des points (par exemple moindres carrés).
tableaux multidimensionnels
Un tableau à N dimensions est en fait un tableau unidimensionnel de tableaux de N-1 dimensions. Tout ce qui a
été présenté auparavant reste donc valable. La décision d'utiliser des tableaux multidimensionnels doit être bien
réfléchie : ces tableaux nécessitant beaucoup de mémoire, il faut évaluer le ratio de composantes utiles par
rapport aux places mémoire utilisées. Par exemple un tableau 10x10x10 utilisant dans chacune des 3 directions
une seule fois les 10 mémoires prévues, les autres fois on s'arrête en moyenne à 7 (si ce n'est pas moins), réserve
1000 mémoires, dont seulement 352 utiles (7x7x7+3+3+3).
Outre les tableaux de chaînes de caractères, les tableaux multidimensionnels les plus utilisés sont les matrices Les
algorithmes de base du calcul matriciel (base_mat) sont la mise à 0, la recopie, l'addition et le produit de
matrices, qui sont simples à mettre en place (le produit entraîne néanmoins N3 multiplications, des algorithmes
plus performants existent mais ne commencent à être rentables que pour N très grand, entre 10000 et un million
!).
Par contre, le problème de l'inversion d'une matrice est plus complexe. Elle est utilisée principalement pour la
résolution de N équations à N inconnues, représentées par une matrice NxN. La méthode de Gauss est
certainement la plus simple à mettre en oeuvre, bien que pouvant poser problème dans certains cas particuliers.
On utilise en fait la méthode de résolution d'un système d'équations A.X=B par substitution. Nous allons le
préciser sur un exemple :
1 1 -2 x1 2
1 3 -4 * x2 = 6
-1 -2 6 x3 -1
A X B
On utilise le fait que de remplacer une ligne du système d'équations par une combinaison linéaire entre elle et
d'autres lignes ne modifie pas le résultat, pour éliminer le premier élément de la seconde ligne. En soustrayant la
première ligne à la seconde on obtient :
1 1 -2 x1 2
0 2 -2 * x2 = 4
-1 -2 6 x3 -1
Puis on élimine les deux premiers éléments de la troisième ligne (on ajoute la première puis on ajoute 1/2 fois la
seconde) :
1 1 -2 x1 2
0 2 -2 * x2 = 4
00 1 x3 1
On peut désormais résoudre le système (en commençant par le bas) : x3=1, donc (seconde ligne) 2x2-2.1=4, donc
x2=3, donc (première ligne) x1+3-2.1=2 donc x1=1.
est une combinaison linéaire des autres), soit aucune (0=N). Mais cette méthode n'est pas directement applicable
pour des coefficients réels (ou du moins flottants). En effet, si les substitutions précédentes ont amené un
coefficient nul, on obtient une division par zéro. S'il est proche de 0, l'utilisation de ce coefficient pour en annuler
d'autres nécessitera un multiplicateur très grand, ce qui entraînera une multiplication importante de l'erreur
inhérente à l'utilisation de réels et donc un résultat faux (en fait on se trouvera en présence de coefficients d'ordre
de grandeur très différent, et donc les petits deviendront négligeables devant l'erreur sur les très grands). La
solution est de ne pas traiter les lignes dans l'ordre mais pour une colonne donnée, choisir pour celle qui aura son
premier terme non nul celle dont ce terme est le plus grand (on l'appelle le pivot). Il suffit alors d'échanger la
ligne que l'on veut traiter avec la ligne contenant le pivot (échanger des lignes d'un système d'équations ne
modifie pas la solution) (gauss):
Remarque : on pourrait retourner le résultat dans B (si l'utilisateur désire le garder, il lui suffirait de le copier
avant). Ceci économise le tableau X et le tampon.
D'autres algorithmes existent, mais ils ne sont plus efficaces que Gauss que pour de très grosses matrices. Mais
souvent les grosses matrices possèdent des propriétés particulières nécessitant d'utiliser d'autres structures de
données que les tableaux à 2 dimensions (en cas de matrices triangulaires, symétriques, bandes, en "ligne de
ciel", creuses..., voir paragraphe 11.3.2), pour lesquelles Gauss n'est pas la meilleure solution, car la triangulation
supprime ces propriétés.
conclusions
Les tableaux unidimensionnels statiques sont d'une utilisation simple. Ils permettent un accès direct (donc quasi
immédiat) à une donnée dont on connaît la position. Les seules manipulations de base rapides sont l'insertion et la
suppression en fin du tableau. La dimension du tableau doit être connue (ou du moins maximisée) dès la phase
d'écriture du programme, ces tableaux sont donc intéressants dans le cas de dimensions petites ou presque
constantes.
tableaux unidimensionnels en C
Les tableaux statiques nécessitent de maximiser lors de l'écriture du programme la dimension des
tableaux. En cas de tableaux nombreux, il serait plus utile de ne réserver, pour chaque exécution, que la
taille nécessaire à l'application en cours. En C, cette transformation est triviale grâce à la fonction malloc
et à l'équivalence d'écriture entre les tableaux unidimensionnel et les pointeurs. Il suffira donc, pour
utiliser les fonctions décrites pour les tableaux unidimensionnels statiques, de ne remplacer que la
déclaration des tableaux :
tab=(type_tus)malloc(taille*sizeof(composante));
à partir du moment où l'on a besoin du tableau, puis free(tab); quand il redevient inutile. Le
problème des tableaux dynamiques en C est que l'on doit connaître la dimension d'un tableau avant sa
première utilisation, ce qui empêchera par exemple une introduction de données du type "entrez vos
données, tapez FIN pour terminer" mais nécessitera de demander en premier le nombre de données.
L'autre problème provient du fait de la gestion de la mémoire inaccessible au programmeur : des
créations et suppressions successives de tableaux vont créer une mémoire morcelée, et donc
l'impossibilité de réserver un gros tableau, alors que la mémoire était disponible mais non continue. On
est alors obligé de passer par la méthode du super-tableau pour pouvoir effectuer des retassages de
mémoire.
la méthode du super-tableau
Dans les langages ne disposant pas de fonctions spécifiques à l'allocation dynamique de mémoire, on
crée un grand tableau, appelé super-tableau, dimensionné au maximum de mémoire disponible. On crée
ensuite les fonctions utilitaires nécessaires : réservation de mémoire (bloc) d'une taille donnée en
argument (la fonction rend l'indice dans le super-tableau du début du sous-tableau alloué), libération d'un
bloc préalablement alloué,... L'utilisation d'un bloc alloué est alors très simple : on remplace l'écriture
bloc[i] par super_tableau[indice_debut_bloc+i]. Les parties du super-tableau utilisées
n'ont pas absolument besoin d'une gestion poussée, mais on préfère en général prévoir un tableau annexe
contenant les adresses et tailles des blocs actuellement réservés. Par contre il faut gérer les parties libres.
Une solution est d'utiliser une pile (voir plus loin), mais la libération d'un bloc autre que le dernier soit
nécessite un décalage des blocs suivants (attention, les indices de début de blocs sont modifiés), soit un
marquage spécifique de cette zone (la place ne sera effectivement libérée que lorsque tous les blocs
suivants seront libérés). Cette méthode est utilisable dans beaucoup de cas, il suffit de réserver en
premier les blocs qui seront utilisés tout au long du programme, les blocs à durée de vie plus faible par la
suite. L'autre solution consiste à gérer les blocs libres sous forme d'une liste chaînée (voir plus loin),
chaque bloc libéré contenant l'indication de sa taille et l'adresse du bloc libre suivant (la place utilisée par
ces informations est "masquée" puisque n'utilisant que des zones libres). L'allocation d'un bloc consiste
alors en la recherche du premier bloc libre de taille suffisante. Un retassage (et donc modification des
indices de débuts de blocs) n'est nécessaire qu'en cas de mémoire trop morcelée.
Cette méthode du super-tableau permet de manière très simple d'accéder à toutes les possibilités des
pointeurs, dans tout langage. C'est la raison pour laquelle des programmes très efficaces continuent à être
développés dans ces langages (pratiquement tous les gros programmes scientifiques (C.A.O., éléments
finis,...) de plusieurs centaines de milliers de lignes sont écrits en FORTRAN, et utilisent cette méthode).
De plus la gestion par le programmeur de l'allocation de mémoire peut permettre d'être plus efficace que
la gestion par le compilateur, puisque pouvant être spécifique au problème traité.
On peut prévoir une fonction (ou même une macro, plus rapide) pour effectuer ce calcul :
conclusions
Les tableaux dynamiques ne sont pas totalement dynamiques, c'est à dire que leur taille, bien que définie
en cours d'exécution du programme, ne peut pas facilement être modifiée (c'est néanmoins presque
toujours possible mais relativement coûteux en temps et mémoire). Mais ils gardent la principale qualité
des tableaux statiques : l'accès immédiat à une composante dont on connaît la position. Ils en gardent
également la faible efficacité en cas d'insertions et suppressions. Les tableaux dynamiques sont
néanmoins la solution minimisant la place occupée en mémoire pour de gros ensembles de données :
Seule la place nécessaire est réservée (excepté une mémoire pour l'adresse du début du tableau), aucune
mémoire n'est utilisée pour indiquer où se trouve la composante suivante, puisqu'elle est placée
directement après la précédente (contrairement aux listes par exemple).
LES LISTES
● fonctions de base et manipulations (base_lst)
● les tris
❍ le tri bulle
❍ le tri par insertion
❍ le tri par sélection
❍ le tri par création
❍ les autres tris
● problèmes mathématiques
● conclusions
Supposons disposer de la liste ci-dessous en mémoire (nous verrons plus loin comment la créer en mémoire). La
variable prem, de type adr_comp (donc pointeur sur une composante) contient l'adresse de la première
composante. Le champ suiv de la dernière composante contient la valeur NULL, donc ne pointe sur rien.
Pour simplifier les algorithmes, on peut mettre d'office une composante particulière en début et fin de liste
(appelée sentinelle). Ceci évite de devoir traiter de manière particulière le premier et dernier élément de la liste,
puisque chaque composante utile possède toujours un précédent et un suivant. Cette méthode prend un peu plus
de mémoire (négligeable en % pour de longues listes) et évite des tests systématiques dans le boucles, alors qu'ils
ne servent que pour les extrémités (d'où gain de temps appréciable, toujours en cas de longues listes). Une autre
méthode pour repérer le dernier élément est de le faire pointer sur lui-même. Nous utiliserons dans la suite des
listes sans sentinelle, le dernier élément pointant sur NULL.
L'appel de : affiche_lst(prem) affichera à l'écran le contenu de la liste :
void affiche_lst(adr_comp l)
{
while(l!=NULL)
{
printf("%6.1f ",l->val);
l=l->suiv;
}
printf("\n");
}
l pointe sur la première composante. On affiche la valeur pointée par l puis on fait pointer l sur son suivant. On
répète le processus jusqu'en fin de liste (lorsque l'on pointe sur NULL). La variable l doit être locale (donc passée
par valeur) pour ne pas modifier le contenu de prem, et donc perdre l'adresse du début de la liste, ce qui
empêcherait tout accès ultérieur à la liste.
Comme pour les chaînes de caractères, plutôt que de gérer un variable entière indiquant toujours la longueur
actuelle de la liste, c'est la spécificité du dernier élément (ici, le champ suiv contient NULL) qui permet de
préciser que l'on est en fin de liste. Pour connaître la longueur d'une liste, on utilisera :
int longueur_lst(adr_comp l)
{
int n=0;
while(l!=NULL)
{
l=l->suiv;
n++;
}
return(n);
}
Les listes ont l'avantage d'être réellement dynamiques, c'est à dire que l'on peut à loisir les rallonger ou les
raccourcir, avec pour seule limite la mémoire disponible (ou la taille du super-tableau). Par exemple pour insérer
une nouvelle composante en début de liste, on utilisera :
La variable prem a dû être passée par adresse, pour que l'on récupère bien l'adresse de la nouvelle première
composante (appel : insert_premier(&prem,5)). En fait le schéma ci-dessus n'est qu'une représentation
abstraite de la liste (chaque composante pointe sur la suivante), alors que les composantes sont physiquement
placées différemment en mémoire. Dans le meilleur des cas, la nouvelle composante créée a été placée juste
derrière celles déjà existantes (premier emplacement libre). Un schéma plus proche de la réalité serait donc :
Ces deux schémas sont équivalents (les liens partent des mêmes cases, et pointent sur les mêmes cases), seule la
disposition des cases diffère. En fait ceci nous montre bien que la disposition réelle en mémoire ne nous intéresse
pas (jamais la valeur effective de l'adresse contenue dans un pointeur ne nous intéressera). Dans nos schémas,
nous choisirons donc une disposition des composantes correspondant plus au problème traité qu'à la disposition
réelle en mémoire (par exemple dans le schéma suivant, on utilise une représentation bidimensionnelle, alors que
la mémoire n'est qu'unidimensionnelle).
Le second avantage des listes est que l'insertion d'une composante au milieu d'une liste ne nécessite que la
modification des liens avec l'élément précédent et le suivant, le temps nécessaire sera donc indépendant de la
longueur de la liste. Supposons disposer d'une variable X de type adr_comp, pointant sur la composante de
valeur 10. L'appel de insert_après(X,15) donnera le résultat suivant :
mais les éléments seront stockés dans l'ordre inverse de leur introduction (le dernier saisi sera placé en premier).
Pour une création de liste dans l'ordre, on fera :
adr_comp saisie_lst(void)
{
adr_comp prem=NULL,prec,actu; /* premier, précédent, actuel*/
type_val v;
int err;
do
{
printf("entrez votre prochaine valeur, un caractère pour finir :");
err=scanf("%f",&v);
if(err<=0)break; /*scanf rend le nombre de variables lues sans erreur*/
actu=(adr_comp)malloc(sizeof(type_comp));
actu->val=v;
if(prem==NULL)prem=actu; else prec->suiv=actu;
prec=actu;
}
while(1);
actu->suiv=NULL;
return(prem);
}
Cette fonction crée la liste en mémoire, effectue la saisie et retourne l'adresse du début de la liste.
Il nous reste à traiter les suppressions dans une liste. Il faut ici encore préciser l'élément précédent celui à
supprimer, et traiter de manière particulière le début de la liste :
{
adr_comp deuxieme;
while(prem!=NULL)
{
deuxieme=prem->suiv;
free(prem);
prem=deuxieme;
}
}
Il n'était à priori pas obligatoire d'utiliser la variable deuxième car la libération par free n'efface pas les
mémoires, le suivant est donc encore disponible après free et avant tout nouveau malloc. Néanmoins cette
écriture est plus sure, ne gérant pas la mémoire nous même (sauf si méthode du super-tableau), en particulier en
cas de multitâche, d'interruption matérielle...
On trouvera un exemple utilisant ces fonctions dans test_lst.c (disquette d'accompagnement)
les tris
Seuls les tris n'utilisant pas l'accès direct pourront être efficaces pour les listes. Au lieu de déplacer les valeurs
dans la liste, on changera uniquement les liens. Dans la plupart des configurations, le tri par insertion sera le plus
efficace.
le tri bulle
Le tri bulle est donc assez efficace (n'ayant pas de lien sur la composante précédente, on mémorisera toujours
l'adresse du précédent dans une variable auxiliaire). Mais il reste peu efficace lorsque des éléments sont loin de
leur position finale, puisque chaque passage ne peut déplacer un élément que d'une position (de l'ordre de N2/2
échanges, autant de boucles internes), on le réservera donc aux listes presque triées. Les autres tris, comme le tri
par insertion déplaceront chaque élément directement en bonne position, mais le temps nécessaire à la recherche
peut être assez long, alors qu'ici la position destination est directement connue (mais pas nécessairement juste)
(bull_lst) :
while(suiv!=NULL)
{
if(actu->val > suiv->val)
{
ok=0;
if(prec!=NULL) prec->suiv=suiv; else *prem=suiv;
actu->suiv=suiv->suiv;
suiv->suiv=actu;
}
prec=actu;
actu=suiv;
suiv=actu->suiv;
}
}
while(!ok);
}
En utilisant prec->suiv et prec->suiv->suiv, on pouvait éviter d'utiliser les variables actu et suiv.
Le temps d'accès aux variables aurait été un peu plus long mais on supprimait deux des trois affectations situées
en fin de boucle.
for(prec=*prem,pt=(*prem)->suiv;pt!=NULL;prec=pt,pt=pt->suiv)
if(prec->val>pt->val) /*inutile de chercher si en bonne
position */
{
prec->suiv=pt->suiv;
if((*prem)->val > pt->val) /*cas particulier du premier*/
{
pt->suiv=*prem;
*prem=pt;
}
else
{
dpp=*prem;
while(dpp->suiv->val <= pt->val)dpp=dpp->suiv;
/* on est sur d'en trouver un, vu les tests effectués plus haut */
pt->suiv=dpp->suiv;
dpp->suiv=pt;
}
}
}
Ici également, on aurait pu tenter d'éviter l'utilisation des deux variables prec et pt puisque pt est le suivant de
prec, mais les échanges auraient alors nécessité une variable tampon. Cette version du tri est stable (le nouveau
est inséré derrière les valeurs égales), mais ceci ralentit un peu la recherche de la position définitive d'une valeur
en cas de nombreuses valeurs égales (si la stabilité n'est pas nécessaire, il suffit d'arrêter la recherche au premier
élément supérieur ou égal). Dans tous les cas, on fera au maximum N échanges (aucun en bonne position), en
moyenne N/2 pour les fichiers mélangés. Dans la cas d'un fichier mélangé, le nombre de boucles internes est de
N(N-1)/2 en moyenne. Dans le cas d'un fichier presque trié, dans le cas de quelques éléments (en nombre P) pas
du tout à leur place, ce tri est très efficace : P échanges, (P+2)*N/2 boucles internes, donc on devient linéaire en
N. Par contre dans le cas de nombreux éléments pas tout à fait à leur place (à la distance D), il l'est moins que le
tri bulle, la recherche de la position exacte se faisant à partir du début de la liste (proche de N2 : (N-D)(N-1)
boucles internes). Dans ce cas, si l'on peut également disposer d'un chaînage arrière, on fera la recherche à partir
de la position actuelle, ce qui rendra le tri très efficace également dans ce cas : D(N-1) boucles internes donc
linéaire en N. Sinon, on peut mémoriser la position du Dième précédent, le comparer à la valeur à insérer et donc
dans la majorité des cas (si D bien choisi) rechercher derrière lui, dans les quelques cas où il faut l'insérer devant
lui, on effectue une recherche depuis le début.
cette implantation évité les variables pour la place définitive et le plus petit, ceux-ci étant les suivants des
variables précisées ci-dessus. Ceci nuit à la clarté mais améliore l'efficacité. On pouvait améliorer la clarté
(définir les "variables" pd et pp) sans nuire à l'efficacité (ne pas devoir les remplacer par leur suivant en fin de
boucle) par des #define :
#define pd pdpd->suiv
#define pp pdpp->suiv
Pour limiter la portée de ces substitutions à cette fonction, il suffit de déclarer juste derrière la fin de la fonction :
#undef pd
#undef pp
De plus, afin d'éviter de tester partout le cas particulier du premier de la liste, on a choisi d'ajouter un élément en
début de liste (faux_début), pointant sur le premier de la liste. Dans ce cas, tous les éléments y compris le premier
ont un précédent. Le gain est donc appréciable (en temps car moins de tests et en taille du code source), pour un
coût limité à une variable locale supplémentaire.
● scruter la liste pour créer deux sous-listes (celle des valeurs plus petites et celle des plus grandes),
● mise à jour des liens (fin de la première liste, pivot, début de la deuxième)
Cette gestion des liens ralentit beaucoup l'algorithme, son implantation doit donc être soigneusement optimisée
pour espérer un gain, du moins pour les très longues listes.
problèmes mathématiques
On utilisera les listes pour les cas nécessitant de nombreuses manipulations (en particulier dans les problèmes
graphiques). On les utilise également pour les polynômes ou matrices creuses (c'est à dire avec de nombreux
coefficients nuls, on ne stocke que les coefficients non nuls (ainsi que leur indice ou le nombre de 0 qui les
séparent du suivant). Les algorithmes restent similaires à ceux s'appliquant aux tableaux, mais sont souvent moins
efficaces (une triangulation de Gauss rend non nuls une grande partie des coefficients nuls, nécessitant de
nombreuses insertions de nouveaux éléments).
conclusions
Les listes sont parfaitement dynamiques. Toutes les modifications peuvent se faire en cours d'utilisation. Par
contre l'accès est séquentiel, ce qui peut être très pénalisant dans certains cas. Il est néanmoins possible d'utiliser
un double chaînage avant et arrière, mais au détriment de la place mémoire. L'encombrement des listes en
mémoire est important : chaque élément doit contenir l'adresse du suivant (alors que pour les tableaux elle était
facile à calculer donc non stockée). Pour de très grandes listes, on peut remédier à ce problème en utilisant des
listes de tableaux, mais l'utilisation devient complexe. Un autre avantage des listes est que l'on utilise toujours un
adressage indirect, et donc que les manipulations dans les listes ne font que des modifications du chaînage, sans
réellement déplacer les valeurs stockées, ce qui est capital en cas de champs de grande taille.
définition
Ce sont des structures de données ordonnées, mais qui ne permettent l'accès qu'à une seule donnée. On utilise
souvent le nom générique de pile pour les piles et les files, un seul nom existant en anglais (stack). Les piles
(stack LIFO : Last In First Out) correspondent à une pile d'assiettes : on prend toujours l'élément supérieur, le
dernier empilé. Les files (on dit aussi queues) (stack FIFO: First In First Out) correspondent aux files d'attente :
on prend toujours le premier élément, donc le plus ancien (on ne tolère pas ici les resquilleurs). Les piles et files
sont très souvent utiles : elles servent à mémoriser des choses en attente de traitement. Elles permettront une
clarification des algorithmes quand effectivement on n'a pas besoin d'accéder directement à tous les éléments.
Elles sont souvent associées à des algorithmes récursifs. Il n'y a pas de structures spécifiques prévues dans les
langages (sauf FORTH), il faut donc les créer de toutes pièces. Pour les piles, on utilisera un tableau
unidimensionnel (statique ou dynamique) en cas de piles de hauteur maximale prévisible (la hauteur de la pile
est mémorisée par une variable entière), ou une liste en cas de longueur très variable (ne pas oublier que dans ce
cas on a un surcoût en mémoire d'autant de liens (pointeurs) que d'éléments empilés). Pour les files, l'utilisation
d'un tableau nécessite deux variables : la position du premier et celle du dernier. La suppression du premier
élément ne se fait pas par décalage des suivants mais en incrémentant la variable indiquant le premier. La
gestion est alors un peu plus complexe que pour les piles, puisque le suivant de la fin du tableau est le début du
tableau (en fait, l'indice du suivant est l'indice plus 1, modulo la taille du tableau. La fonction modulo est en fait
très rapide pour les nombres correspondants à une puissance de 2, à condition de l'implanter à l'aide d'un
masquage. L'utilisation d'une liste pour une file par contre est aussi simple que pour une pile.
fonctions de base
Les fonctions de base pour les piles sont l'empilage et le dépilage, pour les files l'enfilage et le défilage. Dans
les deux cas on prévoira également un fonction d'initialisation, et une fonction indiquant si la pile est vide.
Seules ces fonctions de base dépendent de la méthode réelle de mise en oeuvre (tableau, liste,...). Tous les
algorithmes n'utiliseront que ces fonctions. C'est le grand avantage des piles et files, puisque l'on va pouvoir
modifier facilement le type d'implantation en mémoire sans réécrire les programme. Ceci permet de tester
d'abord la faisabilité du programme sur des petites quantités de données puis seulement on l'optimise pour
l'utilisation réelle. Pour une implantation par tableaux, on écrira par exemple, pour une pile de flottants
(base_p_t) :
void init_pile(void)
{sommet=0;}
int pile_vide(void)
{return(sommet=0);}
int empiler(composante x)
/* retourne 0 si pas d'erreur (donc il restait de la place dans la pile) */
{
if(sommet<dim_pile)
{pile[sommet++]=x;return(0);}
else
{puts("pile saturée");return(1);}
}
composante depiler(void)
{
if (sommet>0) return(pile[--sommet]);
else puts("pile vide");return(0);
}
Si l'on désire un dimensionnement totalement dynamique de la pile, on utilisera une liste (base_p_l) :
#include <alloc.h>
#include <stdio.h>
#define composante float
typedef struct s_comp
{composante val;struct s_comp *prec;}type_comp;
int empiler(composante x)
/* retourne 0 si pas d'erreur (donc il restait de la place dans la pile)*/
{
type_comp *nouv;
if((nouv=(type_comp*)malloc(sizeof(type_comp)))!=NULL)
{
nouv->val=x;
nouv->prec=sommet;
sommet=nouv;
return(0);}
else
{puts("pile saturée");return(1);}
}
composante depiler(void)
{
composante x;
if (sommet!=NULL)
{
x=sommet->val;
free(sommet); /*pour plus de sûreté, on peut passer par
une variable*/
sommet=sommet->prec;
return(x);
}
else puts("pile vide");return(0);
}
void init_pile(void)
{while(sommet!=NULL)depiler();}
int pile_vide(void)
{return(sommet==NULL);}
Pour certaines applications, on pourra préférer ne pas libérer la mémoire au dépilage pour gagner du temps : au
dépilage, mais aussi à l'empilage, on ne prend du temps que s'il faut agrandir la pile. On peut également utiliser
une pile de tableaux dynamiques, ce qui permet d'économiser de la place mémoire, sans être limité en taille.
Pour les files, l'implantation des fonctions de base est similaire, par exemple par tableaux (base_f_t) :
void init_file(void)
{bas=sommet=taille=0;}
int file_vide(void)
{return(taille=0);}
int enfiler(composante x)
/* retourne 0 si pas d'erreur (donc il restait de la place dans la pile)
*/
{
if(taille<dim_file)
{file[sommet]=x;sommet=suiv(sommet);taille++;return(0);}
else
{puts("file saturée");return(1);}
}
composante depiler(void)
{
composante x;
if (taille>0) {x=file[bas];bas=suiv(bas);taille--;return(x);}
else {puts("file vide");return(0);}
}
Il faut prévoir un indice pour chaque extrémité de la file. Il en serait de même pour une implantation par liste
chaînée (un pointeur pour chaque extrémité).
utilisations
Les piles sont souvent nécessaires pour rendre itératif un algorithme récursif. Une application courante des piles
est pour les calculs arithmétiques: l'ordre dans la pile permet d'éviter l'usage des parenthèses. Il existe trois
possibilités pour représenter une équation (du moins de manière unidimensionnelle, les arbres en sont une
généralisation bidimensionnelle), suivant la position relative des opérateurs et de leurs opérandes. Il faut avant
tout définir l'arité d'une opération : une opération unaire nécessite un opérateur (- unaire, cosinus, log,
factorielle...), une opération deuxaire (dénomination P. Trau, pour différencier d'une opération binaire qui pour
moi traite des nombres en base 2) nécessite deux arguments (+, -, *, /, produit vectoriel,...), une opération
ternaire nécessite trois opérandes (produit mixte,...). La notation préfixée (dite polonaise) consiste à placer
l'opérateur, suivi de ses arguments. La notation postfixée (polonaise inverse) consiste à placer les opérandes
devant l'opérateur. La notation infixée (parenthèsée) consiste à entourer les opérateurs deuxaires par leurs
opérandes, pour les autres arités on place l'opérateur en premier, suivi de ses opérandes (entre parenthèses,
séparés par des virgules pour les opérateurs ternaires). Les parenthèses sont nécessaires uniquement en notation
infixée, certaines règles permettent d'en réduire le nombre (priorité de la multiplication par rapport à l'addition,
en cas d'opérations unaires représentées par un caractère spécial (-, !,...). Les notations préfixée et postfixée sont
d'un emploi plus facile puisqu'on sait immédiatement combien d'opérandes il faut rechercher. Détaillons ici la
saisie et l'évaluation d'une expression postfixée (polonais) : on modifie le type de composantes de la pile (et
donc les fonctions de base) :
#define operande 1
#define operateur 0
typedef struct
{int type;
union {float op_r;char op_c;}val;
}composante;
void saisie(void)
/* marche pour toute notation puisque ne vérifie rien */
{
composante x;
char txt[100],*deb;
char rep;
init_pile();
printf("entrez opérandes (nombres) et opérateurs (+,-,*,/,C (cos),S)\n");
printf("séparés par des blancs ou virgules\n");
fflush(stdin);
gets(txt); /* on lit un texte qui sera traité par après */
deb=txt; /* deb pointe le début du texte non encore traité */
do
{
while(*deb==' '||*deb==',') deb++;
if(*deb==0)break;
if(isdigit(*deb)||*deb=='.'||(*deb=='-'&&isdigit(*(deb+1))))
{
x.type=operande;
sscanf(deb,"%f",&(x.val.op_r));
empiler(&x);
while(isdigit(*deb)||*deb=='.') deb++; /*pointer la suite*/
}
else /*cas d'un opérateur */
{
x.val.op_c=toupper(*(deb++));
x.type=operateur;
empiler(&x);
}
}
while(1);
}
float eval_post(void)
{
float r1,r2;
composante x;
if(depiler(&x)!=0) {puts("expression non postfixée");return(0);}
if(x.type==operande) return(x.val.op_r);
/* else inutile car les autres cas se finissent par return */
/* on traite d'abord les opérateurs unaires */
if (x.val.op_c=='C') return(cos(eval_post()));
if (x.val.op_c=='S') return(sin(eval_post()));
/* les deuxaires maintenant */
r2=eval_post();
r1=eval_post();
switch (x.val.op_c)
{
case '+':return(r1+r2);
case '-':return(r1-r2);
case '*':return(r1*r2);
case '/':return(r1/r2);
}
puts("erreur (code opératoire inconnu par exemple)");
return(0);
}
void main(void)
{
saisie();
LES ARBRES
● introduction
● expressions arithmétiques (arb_expr)
● listes triées
● les arbres généraux
introduction
Les listes et piles sont des structures dynamiques unidimensionnelles. Les arbres sont leur généralisation
multidimensionnelle (une liste est un arbre dont chaque élément a un et un seul fils). On utilise un
vocabulaire inspiré des arbres généalogiques (sexiste il est vrai, bien qu'on utilise quelquefois les termes
fille et mère). Chaque composante d'un arbre contient une valeur, et des liens vers ses fils. On peut
distinguer un noeud, qui est une composante ayant des fils, et une feuille, qui n'en possède pas (mais est
fils d'un noeud) mais souvent on préfère utiliser un type noeud y compris pour les feuilles (tous les fils
sont mis à NULL), surtout si l'arbre est évolutif (une feuille peut devenir un noeud, et inversement). Une
branche rassemble un lien vers un fils mais aussi ce fils et toute sa descendance. Le noeud racine est le
seul qui n'est le fils d'aucun noeud (tous les autres noeuds sont donc sa descendance). Comme pour le
premier d'une liste, l'adresse de la racine est nécessaire et suffisante pour accéder à l'intégralité d'un
arbre. Un arbre N-aire ne contient que des noeuds à N fils (et des feuilles). Une liste est donc un arbre
unaire. De nombreux algorithmes ont été développés pour les arbres binaires (nous verrons que tout arbre
peut être représenté par un arbre binaire). Comme les listes, les arbres sont complètement dynamiques,
mais les liens sont également gourmands en mémoire. L'accès aux données est encore séquentiel, mais on
verra qu'il reste néanmoins rapide.
Les composantes de l'arbre seront donc soit des opérateurs (noeuds), soit des opérandes (feuilles). Nous
allons choisir les déclarations suivantes :
/* on donne une chaîne de char contenant l'expression en notation préfixée. les opérateurs possibles sont
+,-,*,/(deuxaires), C,S(cos,sin,unaires). Les nombres sont séparés par des blancs (optionnels pour les
opérateurs) Les nombres commencent par un chiffre (exemple 0.5 au lieu de .5).*/
/* deb pointe sur le début de chaîne, il pointera ensuite sur le reste de la chaîne (pas encore traitée) donc
passage par adresse d'un pointeur */
{
lien x;
char c;
while(**deb==' '||**deb==',') (*deb)++;
if(**deb==0) /* on est arrivé en fin de chaîne */
{puts("erreur : il doit manquer des opérandes");
return(NULL);}
x=(composante*)malloc(sizeof(composante));
if(isdigit(**deb))
{
x->arite=0;
sscanf(*deb,"%f",&(x->val.f));
while(isdigit(**deb)||**deb=='.') (*deb)++;
}
else
{
c=toupper(*((*deb)++));
if(c=='*'||c=='/'||c=='+'||c=='-')
{
x->arite=2;
x->val.n2.op_c=c;
x->val.n2.fils1=saisie_prefixe(deb);
x->val.n2.fils2=saisie_prefixe(deb);
}
else if(c=='C'||c=='S')
{
x->arite=1;
x->val.n1.op_c=c;
x->val.n1.fils=saisie_prefixe(deb);
}
else printf("erreur, '%c'n'est pas un opérateur prévu\n",c);
}
return(x);
}
On peut remarquer que cette fonction est récursive. La création de l'arbre à partir d'une expression
postfixée est similaire (scruter la chaîne dans l'autre sens par exemple), à partir d'une expression infixée
c'est un peu plus dur, surtout si l'on ne veut pas imposer d'entrer toutes les parenthèses, par exemple
1+2+3 au lieu de (1+(2+3)).
L'utilisation d'un tel arbre devient maintenant très simple. On utilisera la récursivité. Pour évaluer
l'expression, si c'est une feuille unique la valeur est celle de la feuille, si c'est un noeud, la valeur est
obtenue en effectuant l'opération correspondante, après évaluation (récurrente, donc soit directement la
valeur si feuille soit nouveau calcul) de ses fils :
float eval(lien x)
{
float r1,r2;
if(x->arite==0) return(x->val.f);
else if (x->arite==1)
{
if (x->val.n1.op_c=='C')
return(cos(eval(x->val.n1.fils)));
else if (x->val.n1.op_c=='S')
return(sin(eval(x->val.n1.fils)));
}
else
{
r1=eval(x->val.n2.fils1);
r2=eval(x->val.n2.fils2);
switch (x->val.n2.op_c)
{
case '+':return(r1+r2);
case '-':return(r1-r2);
case '*':return(r1*r2);
case '/':return(r1/r2);
}
}
/* si aucun return n'a été effectué jusqu'ici*/
puts("erreur (code opératoire inconnu par exemple)");
return(0);
}
Le parcours des arbres (c'est à dire le passage par tous les noeuds et feuilles) se fait d'habitude de manière
récursive. On doit évidement parcourir l'arbre pour afficher son contenu. On peut utiliser dans le cas des
arbres binaires trois possibilités de parcours : afficher la valeur du noeud puis récursivement de ses deux
fils, afficher le premier fils, la valeur du noeud, puis le second fils, ou afficher les deux fils avant la
valeur du noeud. On obtient dans notre cas directement les notations préfixé, infixée et postfixée (avec un
petit travail supplémentaire pour la gestion des parenthèses en notation infixée, qu'on aurait pu simplifier
si l'on avait accepté des parenthèses surabondantes) :
void affiche_prefixe(lien x)
{
switch(x->arite)
{
case 0:printf("%6.1f ",x->val.f);break;
void affiche_postfixe(lien x)
{
switch(x->arite)
{
case 0:printf("%6.1f ",x->val.f);break;
case 1:affiche_postfixe(x->val.n1.fils);
printf(" %C ",x->val.n1.op_c);
break;
case 2:affiche_postfixe(x->val.n2.fils1);
affiche_postfixe(x->val.n2.fils2);
printf(" %c ",x->val.n2.op_c);
}
}
void affiche_infixe(lien x)
{
switch(x->arite)
{
case 0:printf("%6.1f ",x->val.f);break;
case 1:printf(" %C( ",x->val.n1.op_c);
affiche_infixe(x->val.n1.fils);
putch(')');
break;
case 2:if(x->val.n2.fils1->arite!=0)putch('(');
affiche_infixe(x->val.n2.fils1);
if(x->val.n2.fils1->arite!=0)putch(')');
printf(" %c ",x->val.n2.op_c);
if(x->val.n2.fils2->arite!=0)putch('(');
affiche_infixe(x->val.n2.fils2);
if(x->val.n2.fils2->arite!=0)putch(')');
}
}
Notre exemple est un peu plus complexe que dans véritable arbre binaire, puisque nous acceptions
également des noeuds à un seul fils.
listes triées
Il est possible de mémoriser une liste ordonnée à l'aide d'un arbre binaire. Chaque noeud de l'arbre
contient une valeur et deux liens : l'un vers un sous-arbre ne contenant que des valeurs inférieures, l'autre
vers des valeurs supérieures. Nous allons traiter cette fois des chaînes de caractères plutôt que des réels.
Nous définirons donc les types suivants :
La fonction effectuant la saisie et la création de cet arbre devra donc, après la saisie du nouveau nom à
insérer, rechercher sa position définitive. Cette recherche sera récursive: sur un noeud donné, si le
nouveau nom est plus petit, rechercher dans la descendance du fils1, sinon rechercher dans la
descendance du fils2. La recherche s'arrête quand on arrive à un pointeur NULL (qui sera remplacé par
l'adresse du nouveau noeud). En cas d'égalité, si l'on désire une création stable, un nouveau nom sera
placé derrière ceux déjà existants. :
lien saisie(void)
{
lien racine=NULL;
char txt[100];
do
{
printf("entrez un nouveau nom, @@@ pour finir : ");
gets(txt);
if(strcmp(txt,"@@@"))
if(racine==NULL)racine=cree_feuille(txt);
else insert_feuille(racine,cree_feuille(txt));
}
while(strcmp(txt,"@@@"));
return(racine);
}
On remarque l'efficacité de cette méthode : aucun déplacement d'élément, le tri se fait par insertion, mais
avec recherche optimisée (du type dichotomie : l'espace de recherche est réduit à chaque test). Pour N
insertions, on fait donc N recherches donc N*P lectures de composantes (P profondeur de l'arbre).
Une fois l'arbre créé, on peut afficher la liste triée par ordre alphabétique par un simple parcours infixé
(arbr_nom):
void affiche(lien x)
{
if(x!=NULL)
{
affiche(x->fils1);
printf("%s, ",x->nom);
affiche(x->fils2);
}
}
L'utilisation d'un arbre binaire pour une liste triée permet donc une programmation relativement aisée,
des algorithmes rapides, mais moyennant un peu de place mémoire supplémentaire. L'arbre va posséder
simultanément tous les avantages des autres structures de données:
● totalement dynamique: l'insertion d'un élément se fait sans aucun déplacement d'élément (ni même
d'échange de liens comme c'était le cas pour les listes)
● rapide : la recherche d'un élément se fait en P tests maximum, P étant la profondeur de l'arbre (ce
qui donne la même efficacité qu'une recherche dichotomique dans un tableau, dans le cas d'un
arbre équilibré).
● parcours facile des données sans coût trop important en mémoire (avec deux liens on obtient une
structure bidimensionnelle, alors qu'avec une liste à chaînage avant et arrière on garde une
structure unidimensionnelle, bien que bidirectionnelle).
Par contre l'arbre binaire nécessite d'être équilibré pour profiter pleinement de ces avantages. Un arbre
équilibré est un arbre organisé de telle manière à ce que sa profondeur soit minimale. A l'extrême, en cas
d'introduction d'une liste de noms déjà triée, tous les fils1 pointeront sur NULL alors que les fils2
pointeront sur le suivant, on se retrouve dans le cas d'une liste chaînée simple. Différents algorithmes
d'équilibrage d'arbres binaires existent, mais en général un algorithme simple permet un résultat
satisfaisant (sauf en cas d'un très grand nombre de données), l'équilibre parfait n'étant pas nécessaire.
En fait, plutôt que de créer cette liste hors des noeuds, le plus simple (et qui utilise autant de mémoire)
est d'associer à chaque noeud l'adresse de son fils aîné, et de son frère cadet. Accéder à tous les fils
revient donc à accéder au fils aîné puis à tous ses frères:
On peut remarquer qu'un tel arbre est un arbre binaire: chaque noeud possède deux liens. On peut donc
traiter tout arbre sous forme d'un arbre binaire.
Une autre amélioration possible d'un arbre est de permettre un accès toujours séquentiel mais
bidirectionnel : pour un arbre binaire, chaque noeud possède en plus l'adresse de son père, pour un arbre
généralisé ceci revient à ce que chaque frère connaît son aîné, l'aîné connaissant leur père commun. Cet
accès bidirectionnel est coûteux en mémoire, mais complique également les modifications (plus de liens
à gérer), pour par contre accélérer les parcours dans l'arbre. On ne prévoira cette amélioration que lorsque
l'on utilise de fréquentes remontées (l'utilisation d'algorithmes récursifs ne nécessite en général pas ces
liens, le fait de quitter une fonction appelée pour gérer un fils fait automatiquement retrouver le père).
LES GRAPHES
Un graphe est en ensemble de noeuds reliés par des liens. Ce n'est plus un arbre dès qu'il existe deux
parcours différents pour aller d'au moins un noeud à un autre. Un graphe est connexe lorsqu'il est
possible de trouver au moins un parcours permettant de relier les noeuds deux à deux (un arbre est un
graphe connexe, deux arbres forment un graphe non connexe). Un graphe est dit pondéré lorsqu'à chaque
lien est associée une valeur (appelée poids). On utilisera un graphe pondéré par exemple pour gérer des
itinéraires routiers (quelle est la route la plus courte pour aller d'un noeud à un autre), pour gérer des
fluides (noeud reliés par des tuyauteries de diamètre différent) ou pour des simulations de trafic routier,
pour simuler un circuit électrique, pour prévoir un ordonnancement dans le temps de tâches... Un graphe
est dit orienté lorsque les liens sont unidirectionnels. Nous ne traiterons pas ici en détail les algorithmes
associés aux graphes, mais nous aborderons quelques problèmes rencontrés.
On peut représenter un graphe de manière dynamique, comme les arbres (le nombre de liens par noeud
est souvent variable). Une autre solution est de numéroter les N sommets, et d'utiliser une matrice carrée
NxN, avec en ligne i et colonne j un 0 si les noeuds i et j ne sont pas reliés, ou le poids de la liaison sinon
(1 pour les graphes non pondérés). Pour des graphes non orientés, la matrice est symétrique (par rapport
à la diagonale), ce qui permet d'économiser de la mémoire mais peut compliquer les programmes (c'est la
technique employée pour les éléments finis). Une représentation par matrice est surtout intéressante
lorsqu'il y a beaucoup de liens (graphe presque complet), la représentation à l'aide de pointeurs étant
moins gourmande en mémoire pour les graphes comportant peu de liens par noeud.
Un problème important est le parcours d'un graphe : il faut éviter les boucles infinies, c'est à dire
retourner sur un noeud déjà visité et repartir dans la même direction. On utilise par exemple des
indicateurs de passage (booléens), ou une méthode à jetons (on place des "jetons" dans différents noeuds
bien choisis et on les fait suivre les liens). Une méthode souvent utilisée est de rechercher avant tout (et
une seule fois) l'arbre recouvrant : c'est un arbre permettant de visiter tous les noeuds, n'utilisant que des
liens existants dans le graphe (mais pas tous). Cet arbre recouvrant n'est évidement pas unique. En cas de
graphe non connexe, il faut rechercher plusieurs arbres recouvrants.
On peut remarquer qu'un arbre recouvrant d'un graphe connexe à N sommets aura nécessairement N-1
liens. Pour les graphes pondérés, on peut rechercher l'arbre recouvrant de poids minimum (somme des
poids des liens minimale). Différents algorithmes existent pour traiter ce problème.
LES FICHIERS
● les fichiers séquentiels
● les fichiers à accès direct
● l'indexation
Les fichiers ont pour avantage d'être dynamiques (la taille peut varier ou du moins augmenter au cours de
l'utilisation, du moins tant qu'il reste de la place sur le support), et de ne pas être volatiles comme l'est la
mémoire (c'est à dire que les informations restent présentes en l'absence d'alimentation électrique). Leur
principal inconvénient est le temps d'accès à une donnée (de l'ordre de 106 fois plus lent que la mémoire,
en général plus de 10 ms pour un disque dur, mais jusqu'à plusieurs secondes pour une bande
magnétique). L'insertion et la suppression d'éléments autre part qu'en fin d'un fichier est quasiment
impossible, en général on devra passer par la création d'un nouveau fichier, y recopier le début du fichier
initial jusqu'à la modification prévue, puis écrire les données modifiées puis recopier le reste du fichier.
Les fichiers peuvent être séquentiels ou à accès direct.
programme ou sur une autre machine, alors que la sauvegarde directe d'une valeur numérique est faite en
binaire (recopie des 0 et 1 en mémoire), ce qui prend moins de place mais n'est plus lisible que par un
programme qui utilise le même format.
Les fichiers séquentiels ne peuvent en général pas être modifiés, la seule possibilité étant l'ajout derrière
les données déjà stockées (pour un fichier texte, sur disque, on peut remplacer des caractères mais pas en
ajouter ni en supprimer sauf en fin de fichier).
l'indexation
La méthode souvent la plus efficace pour l'utilisation de fichiers séquentiels est l'indexation (elle est
également utilisable pour les autres types de données, stockées en mémoire). Les composantes sont
stockées dans le fichier dans l'ordre de leur création. On utilise alors un tableau d'index, donnant en
première position le numéro de la première composante, puis de la seconde,... L'avantage de cette
méthode est que l'ajout de composantes est optimal : on rajoute la valeur en fin de fichier, on met à jour
le tableau d'index. Tout déplacement d'une composante sera donc remplacé par une modification du
tableau d'index, sans déplacement réel de la valeur dans le fichier. En général, ce tableau peut tenir en
mémoire, ce qui permet une modification rapide, en général on préfère le sauver également sur support
magnétique avant de quitter le programme, ce qui évitera de le recréer (par exemple refaire un tri) à la
prochaine utilisation. On peut également utiliser une liste d'index si les déplacements sont fréquents
(mais alors l'accès devient séquentiel). Le second avantage de cette méthode est que l'on peut utiliser
simultanément plusieurs index : par exemple pour une liste de personnes, on peut créer un index pour le
classement alphabétique des noms, un autre sur les villes, on accédera donc plus rapidement à tous les
champs indexés, alors que les champs non indexés devront se satisfaire d'une recherche séquentielle, et
ce sans modification dans le fichier (un tri par nom puis par ville auraient été nécessaires sans
indexation). Par contre toute modification nécessitera la mise à jour de tous les tableaux d'index.
Exemple:
La suppression, par contre, pose problème. En général, toujours pour éviter les décalages dans les
fichiers, on préfère marquer d'un signe distinctif les champs supprimés (par exemple un nom non
alphabétique ou vide), puis remettre à jour les index qui ne pointeront plus sur ce champ. Le retassage,
assez long, n'est effectuée que sur ordre de l'utilisateur ou lorsqu'il quitte le programme. On peut aussi
(comme dans la méthode du super-tableau) créer une liste des champs vides, ce qui permettra d'y
accéder, plus rapidement que par une recherche séquentielle, lors de la prochaine insertion.
Sur un fichier indexé, on peut à nouveau se permettre des algorithmes utilisant l'insertion, puisque
celle-ci n'affecte que l'index (à accès rapide). Pour un tri par exemple, on pourra utiliser le tri par
insertion, à condition d'optimiser la recherche de la position d'insertion (par dichotomie pondérée par
exemple), puisque celle-ci nécessite des lectures de champs dans le fichier alors que l'insertion n'entraîne
que des décalages dans un tableau, d'une durée généralement négligeable devant le temps pris par la
recherche. On peut également utiliser une liste d'index plutôt qu'un tableau si nécessaire.
BOUCLE
#include <stdio.h>;
void main(void)
{
float puiss,x,result;
printf("entrez x ");
scanf("%f",&x);
result=2 -5*x +(puiss=x*x) -3*(puiss*=x) +2*puiss*x;
printf("résultat : %f\n",result);
}
TUSEXO_A
#include <stdio.h>
#include "base_tus.inc"
#define dim 10
void main(void)
{
int i,nb;
composante v,t[dim],moy=0;
init_tus(t,&nb,dim);
do
{
printf("entrez la %dième note (fin si <0 ou >20) :",nb+1);
scanf("%f",&v);
if(v<0||v>20) break; /* on aurait pu le mettre dans la condition du while */
moy+=v;
}
while (!ajoute_val_tus(t,&nb,dim,v));
if(nb) moy/=nb;
printf("moyenne des %d notes : %5.2f\n",nb,moy);
for(i=0;i<nb;i++) printf("%2dième valeur : %5.2f (écart/moyenne
%+5.2f)\n",i+1,t[i],t[i]-moy);
}
TUSEXO_B
#include <stdio.h>
#include <conio.h>
#include "base_tus.inc"
#include "mani_tus.inc"
#define dim 10
void lecture(type_tus t,int *nb)
{
composante v;
do
{
printf("entrez la %dième note (fin si <0 ou >20) :",(*nb)+1);
scanf("%f",&v);
if(v<0||v>20) break; /* on aurait pu le mettre dans la condition du while */
}
while (!ajoute_val_tus(t,nb,dim,v));
}
void main(void)
{
int nb;
composante t[dim];
char rep;
init_tus(t,&nb,dim);
lecture(t,&nb);
do
{
printf("0 fin, 1 affiche, 2 rot gauche, 3 rot droite, 4 suppr, 5 ins :");
rep=getche()-'0';
printf("\n");
switch (rep)
{
case 0:puts("au revoir");break;
case 1:affiche_tus(t,nb);break;
case 2:rot_gauche_tus(t,nb);break;
case 3:rot_droite_tus(t,nb);break;
case 4:{int pos;
printf("position de l'élément à supprimer ? ");
scanf("%d",&pos);
suppr_tus(t,&nb,pos);
break;}
case 5:{int pos;composante v;
printf("position de l'élément à insérer ? ");
scanf("%d",&pos);
printf("valeur à insérer ? ");
scanf("%f",&v);
insert_tus(t,&nb,dim,pos,v);
break;}
default:puts("code erroné !");
}
}
while (rep);
}
GAUSS_MRD
#include <stdio.h>
#include <alloc.h>
#include <math.h> /* pour fabs */
lpivot=ce;
for(l=ce+1;l<N;l++)
if(fabs(A[adr_mrd(l,ce,N)])>fabs(A[adr_mrd(lpivot,ce,N)])) lpivot=l;
/* echange de la ligne du pivot et de la ligne ce (ne pas oublier B)*/
for(c=ce;c<N;c++) /* tous les termes devant ce sont nuls */
{
coef=A[adr_mrd(ce,c,N)];
A[adr_mrd(ce,c,N)]=A[adr_mrd(lpivot,c,N)];
A[adr_mrd(lpivot,c,N)]=coef;
}
coef=B[ce];B[ce]=B[lpivot];B[lpivot]=coef;
for(l=ce+1;l<N;l++) /* pour chaque ligne au dessous */
{
coef=A[adr_mrd(l,ce,N)]/A[adr_mrd(ce,ce,N)];
A[adr_mrd(l,ce,N)]=0; /* normalement pas besoin de le calculer*/
for(c=ce+1;c<N;c++)
A[adr_mrd(l,c,N)]-=coef*A[adr_mrd(ce,c,N)];
B[l]-=coef*B[ce];
}
}
}
void main(void)
{
int nblA,nblB,nbcA,nbcB;
mat_dyn A,B,X;
autom_3_3(&A,&nblA,&nbcA);
affiche_mrd(A,nblA,nbcA);
autom_3_1(&B,&nblB,&nbcB);
affiche_mrd(B,nblB,nbcB);
gauss_triangulation(A,B,nblA); /*nblA=nbcA=nblB*/
puts("résultat après triangulation : ");
affiche_mrd(A,nblA,nbcA);
affiche_mrd(B,nblB,nbcB);
X=alloc_mrd(nblB,nbcB);
gauss_resolution(A,B,X,nblA);
puts("solution :");
affiche_mrd(X,nblB,1);
}
INSE_TTD
#include <stdio.h>
#include <alloc.h>
#include <string.h>
typedef char composante;
typedef composante *ligne;
/* ou composante ligne []*/
typedef ligne *mat_ttd;
/* ou ligne mat_ttd[] */
ligne tampon;
for(pt=1;pt<N;pt++)
{
dpg=pt-1;
tampon=tab[pt];
while(strcmp(tab[dpg],tampon)>0 && dpg>=0)
{tab[dpg+1]=tab[dpg];dpg--;}
tab[dpg+1]=tampon;
}
}
void main(void)
{
int nbl;
mat_ttd txt;
lecture(&txt,&nbl);
puts("avant tri :");
affiche(txt,nbl);
tri_insertion(txt,nbl);
puts("après tri :");
affiche(txt,nbl);
}
A
● accès
● direct [1]
[2]
❍ séquentiel [1] [2]
● allocation dynamique [1]
● arbre [1]
● binaire [1]
[2]
● argument [1]
B
● bande [1] [2]
● binaire [1]
● bit [1]
● boucles [1]
C
● index du langage C
● chiffres significatifs [1]
● clef [1] [2]
D
● débogage [1]
● décalage [1]
● décalages [1]
● défiler [1]
● dépiler [1]
● define [1]
● deuxaire [1]
● dichotomie [1] [2]
[3]
● dimension [1] [2]
● do-while [1]
● dynamique [1] [2]
[3] [4]
E
● effet de bord [1]
● empiler [1]
● encombrement [1]
● enfiler [1]
F
● feuille [1]
● fichier [1]
● FIFO [1]
● file [1]
● fils [1]
● float [1]
● for [1]
● free [1]
G
● Gauss [1] [2]
● graphe [1]
I
● index [1]
● indexation [1]
● infixé [1] [2]
● insertion [1] [2]
[3] [4] [5]
● interpolation [1]
L
● Lagrange [1]
● largeur de bande [1]
● LIFO [1]
● ligne de ciel [1] [2]
● liste [1] [2]
[3]
M
● malloc [1]
● mathématiques [1] [2]
[3] [4]
● matrice [1] [2]
[3]
❍ triangulée [1]
● modèle [1]
N
● noeud [1]
● notation polonaise [1]
● NULL [1] [2]
[3]
O
● objets [1]
● optimisation [1]
P
● parcours [1] [2]
[3]
● pile [1] [2]
[3]
● pivot [1]
● de Gauss [1]
● pointeur [1] [2]
[3]
● polynôme [1] [2]
● postfixé [1] [2]
● préfixé [1] [2]
Q
● queue [1]
● Quick Sort [1]
R
● récursif [1]
● récursivité [1] [2]
[3]
● racine [1]
● recherche [1] [2]
● retassage [1]
S
● séquentiel [1] [2]
● sentinelle [1]
● spline [1]
● stable [1]
● (tri) [1]
● stack [1]
● strcmp [1]
● structuré [1]
● suites [1]
● super-tableau [1] [2]
● suppression [1] [2]
[3]
T
● tableau [1] [2]
[3] [4] [5] [6] [7]
❍ dynamique [1]
❍ multidimensionnel [1]
❍ statique [1]
● ternaire [1]
● tri [1]
❍ bulle [1] [2]
● tri par
● arbre [1]
❍ comptage [1]
❍ création [1] [2] [3]
U
● unaire [1]
V
● vecteur [1]
● virgule flottante [1]
W
● while [1]
❍ tableaux unidimensionnels en C
❍ la méthode du super-tableau
❍ les tableaux multidimensionnels
■ matrices pleines (matrices rectangulaires dynamiques : mrd)
■ tableaux de tableaux dynamiques
❍ conclusions
● LES LISTES
❍ fonctions de base et manipulations (base_lst)
❍ les tris
■ le tri bulle
■ le tri par insertion
■ le tri par sélection
■ le tri par création
■ les autres tris
❍ problèmes mathématiques
❍ conclusions
● LES PILES ET FILES
❍ définition
❍ fonctions de base
❍ utilisations
● LES ARBRES
❍ introduction
❍ expressions arithmétiques (arb_expr)
❍ listes triées
❍ les arbres généraux
● LES GRAPHES
● LES FICHIERS
❍ les fichiers séquentiels
❍ les fichiers à accès direct
❍ l'indexation
● CORRECTION DES EXERCICES
❍ BOUCLE
❍ TUSEXO_A
❍ TUSEXO_B
❍ GAUSS_MRD
❍ INSE_TTD
● recherche dans l'index de mon document sur le langage C
Langage C - Sommaire
● Introduction (première partie)
❍ Organisation de l'ordinateur
❍ Langages de programmation
● Connaissances de base
● Fonctions d'entrées/sorties les plus utilisées
● La syntaxe du C
❍ Second exemple, définitions
❍ Variables / identificateurs / adresse / pointeurs
❍ Expressions / opérateurs
■ Arithmétiques
■ unaires
■ deuxaires
■ Relationnels
■ comparaisons
■ logique booléenne
■ binaires
■ Affectation
■ affectation simple =
■ incrémentation / décrémentation
■ affectation élargie
■ Opérateurs d'adresses
■ Autres
■ conditionnel ? :
■ séquentiel ,
■ Ordre de priorité et associativité
❍ Instructions
❍ Structures de contrôle
■ Boucles
■ Chaînes de caractères
■ Bibliothèques de fonctions pour tableaux et chaînes
■ Allocation dynamique de mémoire
■ Tableaux multidimensionnels
❍ Structures et unions
■ Déclaration
■ Utilisation
■ Champs de bits
■ Unions
■ Structures chaînées
● Les fichiers de données
❍ Fichiers bruts
❍ Fichiers bufférisés
● Directives du pré-compilateur
● Utiliser Turbo C (3.5 par exemple)
● Correction des exercices
● Autres sites sur le C
Langage C - Langage C
Sommaire Le Langage C
Aujourd'hui, l'informatique est présente dans tous les
● Introduction (première partie)
domaines de la vie courante, mais à des degrés différents. Il
❍ Organisation de l'ordinateur y a pour cela trois grandes raisons :
❍ Langages de - les gains (en temps, argent, qualité) que l'informatique peut
programmation apporter,
● Connaissances de base
- le prix abordable des matériels,
● Fonctions d'entrées/sorties les plus
utilisées - la disponibilité de logiciels dans tous les domaines.
■ Déclarations globales
■ Déclaration de type
❍ Fonctions
■ Définitions générales
■ Récursivité, gestion
de la pile
■ Arguments passés
par adresse
■ La fonction main
■ Fonction retournant
un pointeur et
pointeur de fonction
❍ Les types de données du C
■ Variables scalaires
■ char : caractère
(8 bits)
■ int : entier
■ float : réel
■ Tailles et
plages
■ Conversions
de type / cast
■ Enumérations
❍ Tableaux
■ Tableaux
unidimensionnels
■ Tableaux et pointeurs
/ arithmétique des
pointeurs
■ Chaînes de caractères
■ Bibliothèques de
fonctions pour
tableaux et chaînes
■ Allocation
dynamique de
mémoire
■ Tableaux
multidimensionnels
❍ Structures et unions
■ Déclaration
■ Utilisation
■ Champs de bits
■ Unions
■ Structures chaînées
● Les fichiers de données
❍ Fichiers bruts
❍ Fichiers bufférisés
● Directives du pré-compilateur
● Utiliser Turbo C (3.5 par
exemple)
● Correction des exercices
● Autres sites sur le C
Sommaire Infographie
Infographie
● JUSTIFICATION
● JUSTIFICATION ● BIBLIOTHEQUE GRAPHIQUE DE BASE
● BIBLIOTHEQUE ● TRANSFORMATIONS MATRICIELLES
GRAPHIQUE DE BASE
● PROJECTIONS 3D
❍ tracés simples
● ELIMINATION LIGNES / SURFACES CACHEES
■ matériel existant
● COURBES ET SURFACES
■ fonctions
● ANNEXE : DOCUMENTATION DE LA BIBLIOTHEQUE
élémentaires
GRAPHIQUE
❍ représentation d'une droite
● télécharger la disquette d'accompagnement
(en mode point ou sur
table traçante) ● Sommaire détaillé
❍ autres courbes Vous trouverez ici un cours sur l'infographie (c'est à dire, ici, les
■ par tâtonnements algorithmes pour faire du graphique). On débute par les tracés les
plus simples (le pixel, la ligne) pour les matériels de base, on passe
■ approximation par
aux cas plus complesxes, progressivement pour aborder le 3D, les
la tangente surfaces complexes... Les algorithmes sont expliqués en clair, les
■ formulation exemples pratiques sont en C. Une bibliothèque graphique (en C) est
paramétrique accessible en téléchargement (gratuit).
❍ remplissages / hachurages Autres liens : Paul Bourke (en anglais)
■ remplissage d'une
frontière déjà tracée
■ frontière totalement
définie
❍ les échelles
■ fenêtre objet et
fenêtre papier
■ clipping
❍ intersections
■ droite - droite
■ droite - cercle /
cercle - cercle
❍ contraintes
❍ Conclusion
● TRANSFORMATIONS
MATRICIELLES
❍ représentation de
fonctions planes par
matrices 2x2
❍ matrices 3x3
(coordonnées homogènes
2D)
❍ transformations 3D
● PROJECTIONS 3D
❍ parallèle
❍ perspective
■ méthodes
■ méthode
vectorielle :
■ méthode
matricielle :
■ clipping
■ points particuliers
● ELIMINATION LIGNES /
SURFACES CACHEES
❍ lignes cachées
❍ faces cachées
■ surfaces orientées
■ algorithme du
peintre
■ calculs de facettes
■ élimination des
arrêtes cachées
■ tubes de projection
■ plans de balayage
■ rayons de
projection
■ éclairage de
surfaces
❍ problème des surfaces
gauches
● COURBES ET SURFACES
❍ introduction
❍ courbes
■ représentation des
points
■ polynômes de
Lagrange
■ splines
■ courbes de Bezier
■ conditions
■ détermination
des
polynômes
de Bezier
■ forme
polynomiale
■ intérêt
■ exemples
❍ surfaces
■ Coons
■ surfaces de Bezier
● ANNEXE :
DOCUMENTATION DE LA
BIBLIOTHEQUE
GRAPHIQUE
❍ G2D Librairie graphique
de base en C
■ Préliminaire
■ Passage en mode
graphique
■ Les fenêtres
■ Les échelles
■ Attributs de tracé
■ Tracés de base
■ Table traçante
■ Autres
bibliothèques
■ Exemple :
❍ bibliothèque GECR
❍ remplissage - hachurage
de polygones (GPOLY)
❍ remplissage suivant
l'écran (GREMPL)
❍ bibliothèque GPLUS
❍ bibliothèque
tridimensionnelle
■ G3D
■ précisions sur le
mode G34VUES
■ G3ECR
■ G3POLY
■ Vecteurs
■ OBJets à lier à
votre EXEcutable
■ EXEMPLE G3D
❍ SOURIS : utilisation de la
souris sous G2D
■ types utilisés
■ fonctions d'usage
courant
■ forme du curseur
■ fonctions de bas
niveau
■ exemple
❍ Bibliothèque MENUG
■ Descriptif général
■ Description
détaillée
■ Détails
pratiques
■ Menus
■ lectures
(clavier ou
souris)
■ référence
■ Constante
prédéfinie
■ Variable
globale
prédéfinie
■ Types
prédéfinis
■ Gestion du
mode MENU
■ Fonctions
principales
■ Ecritures
dans la
fenêtre menu
■ Lectures
(clavier ou
souris)
■ Ecrire dans la
fenetre de
gauche (58
caractères, 25
lignes)
■ Exemple complet
■ télécharger la
disquette
d'accompagement
télécharger la Disquette
d'accompagnement - P. TRAU
● CONTENU DE LA DISQUETTE D'ACCOMPAGNEMENT
❍ correction des exercices de la première partie
❍ exemples pour la première partie
❍ correction des exercices de la seconde partie
❍ exemples pour la seconde partie
❍ exemples pour la troisième partie
❍ Bibliothèque graphique
● TELECHARGER CES DOCUMENTS
CONTENU DE LA DISQUETTE
D'ACCOMPAGNEMENT
Cedocument contient la liste des fichiers disponibles sur la disquette d'accompagnement.
Ils sont regroupés par répertoires
10 chaîne CHAINES.C
11 matrices MATRICES.C
12 determinant DETERMIN.C
13 tel TEL.C
14 liste & insertion LISTE_CH.C
15 agenda AGENDA.C
Bibliothèque graphique
répertoire bib_graph :
Fichiers inclus :
Vous devez inclure les fichiers dont vous avez besoin dans votre fichier source. Il est inutile d'inclure les
fichiers qui ne vous servent pas directement.
G2D.H Bibliothèque graphique de base
Définition du type CARACT qui définit un caractère. Ceci est nécessaire car en TURBO
CHAR.H C les caractères sont définis autrement que par un char (unsigned char). Si vous utilisez
un autre compilateur il suffit de modifier ce fichier.
CHAINES.H Fonctions de base traitement de caractères
VECTEURS.H onctions de base de traitement de vecteurs (produit scalaire, vectoriel...) utilisé par G3D.
GECR.H Fonctions d'écriture
GPOLY.H Tracé, remplissage et hachurage de polygones fermés
GREMPL.H Remplissage d'une courbe déjà tracée à l'écran
Fichiers sources à compiler et lier à vos programmes objets. Il faut lier les fichiers dont vous vous servez
explicitement mais également indirectement (par exemple si vous avez inclus dans votre programme
G3ECR.H, il faut lier votre programme à G3ECR.C mais aussi à GECR.C puisqu'il utilise ce dernier (par
contre il était inutile d'inclure GECR.H dans votre programme si vous ne vous en servez pas
directement).
G2D.C Bibliothèque graphique de base
CHAINES.C Fonctions de base traitement de caractères
Fonctions de base de traitement de vecteurs (produit scalaire, vectoriel...) utilisé par
VECTEURS.C
G3D.
Fonctions d'écriture. Implantation standard : tous les graphismes des 255 caractères d'un
PC y sont définis, un caractère sera donc dessiné à l'aide d'un ensemble de segments.
GECR_STD.C
Ceci prend pas mal de place en mémoire et est assez lent mais marchera partout (si la
couche 0 de G2D est bien définie).
Fonctions d'écriture. Implantation spécifique au TURBO C : On utilise les fonctions
existantes sur le PC pour écrire. Ceci prend moins de place et va plus vite, mais on ne
GECR_ECR.C peut pas modifier la taille des caractères, ni leur inclinaison, ni faire d'écritures sur une
table traçante. Si vous avez inclus GECR.H dans votre programme, il faudra soit lui lier
GECR_ECR.C, soit GECR_STD.C.
GPOLY.C Tracé, remplissage et hachurage de polygones fermés
GREMPL.C Remplissage d'une courbe déjà tracée à l'écran
GPLUS.C Quelques fonctions supplémentaires à G2D
G3D.C Bibliothèque de base 3D
Ecritures (2D) mais débutant en un point défini en 3D. Utilise les écritures en 2D, donc
G3ECR.C
GECR_ECR ou GECR_STD.
Tracé, remplissage et hachurage de polygones définis en 3D. Attention, le remplissage se
G3POLY.C fait sur la projection du polygone, donc peut poser des problèmes si la surface est "trop
gauche".
SOURIS.C Utilisation d'une souris pour entrer des coordonnées 2D à votre échelle.
Fonctions de menus. Ceci est donné en exemple, cette bibliothèque mérite d'être
MENUG.C
complètement modifiée (en particulier en utilisation Windows)
Vous pouvez également télécherger de la même manière la bibliothèque graphique en PASCAL (sources,
documentation et programmes d'exemples). Cette version est un peu plus ancienne que celle en C, mais
reste presque similaire (mêmes noms de procédures et fonctions, mêmes arguments)
Introduction au C++
On trouvera un document complet sur www.mis.enac.fr/~dancel ou www.mygale.org/~dancel
3) VOCABULAIRE
❍ a) la classe
❍ b) l'instance
❍ c) l'héritage
❍ d) la surcharge
❍ e) le constructeur
4)APPLICATIONS
❍ a)simple
❍ b) accès aux membres d'une classe
❍ c) héritage
❍ d) new, delete
❍ e) surcharge d'un opérateur
❍ f) classes virtuelles
❍ g) polymorphisme
le C++
Introduction pour qui connaît le C
Cette page n'est pas destinée au débutant. Je suppose que le lecteur connait bien le C standard (ANSI). Sinon, vous pouvez
d'abord voir mon cours C complet. Pour un problème précis, allez directement à son index.
1) INTRODUCTION
Le C++ est LA solution pour passer aux L.O.O. (langages orientés objet) sans trop de problèmes.
b) pourquoi un L.O.O. ?
Le passage de l'assembleur aux langages structurés a permis d'obtenir des programmes maintenables : on peut les
comprendre, les modifier, les améliorer : on a une structure de programme claire. Par contre si l'on décide de modifier la
structure des données importantes (par exemple remplacer le tableau des données par une liste chaînée), il fallait réécrire
tout le programme. Les objets structurent les données : en changeant la structure d'un objet, il suffit de modifier ses
"méthodes" pour que la transformation s'applique à tout le programme. La programmation est plus simple, les méthodes
(fonctions en C) sont classées par types de données plutôt que séquentiellement. De plus elles sont organisées
hiérarchiquement (arborescence=bidimensionnel plutôt que séquentiel=linéaire).
c) avantages - inconvénients ?
● Programmes plus faciles à écrire et à maintenir. Modification aisée, y compris des types de données. Modularité
accrue (un objet bien défini reservira dans de nombreux programmes)
● Programme résultant (exécutable) un peu moins efficace (plus gros et moins rapide, mais aujourd'hui le prix de la
vitesse et de la mémoire est inférieur au coût d'une optimisation de programme, on n'écrit plus grand chose en
assembleur).
d) Pourquoi C++ ?
C++ est sûrement un mauvais L.O.O. (du point de vue de l'informaticien puriste), par contre il permet de garder tous les
avantages du C : portable, possibilité d'utiliser différents niveaux d'optimisation au sein d'un même programme (objets -
langage structuré classique - assembleur). Il permet de passer en douceur aux objets, mais surtout de garder et réutiliser
toutes les bibliothèques existantes. Bien que plus strict que C, il acceptera à peu près tout, donc sera avare en messages
d'erreur de compilation. C'est le programmeur qui doit se forcer à programmer "objets", s'il ne le fait pas le compilateur ne
le prévient même pas. Comme vous le verrez ici, le passage aux objets (si l'on connaît déjà C) est très simple.
b) entrées-sorties (flux)
à condition d'inclure <iostream.h> (et donc pas <stdio.h>), on peut utiliser cout (pour afficher à l'écran) et cin (pour lire
sur le clavier). Exemple :
float P; int Nb;
cout << "prix unitaire ? ";
cin >> P;
cout << "Nombre ? ";
cin >> Nb;
cout.precision(2); //manipulateur (fonction membre)de cout : tous les
//flottants QUI SUIVENT seront affichés avec
// 2 chiffres après la virgule
cout << "prix total : " << P*Nb << "F \n";
L'avantage de ces fonctions est qu'elles peuvent être plus facilement surchargées que printf et scanf (par exemple étendues
aux tableaux). Les flux fstream possèdent les mêmes fonctionnalités pour les fichiers (je ne détaille pas).
f) résolution de portée
Si vous disposez de deux (ou plus) entités (donnée ou méthode) de même nom, en C standard seule la plus locale est
accessible. ou::nom permet en C++ de préciser de quel nom on parle (en général ou correspond à une classe, ::nom
3) VOCABULAIRE
a) la classe
Une classe est un regroupement de données et de méthodes. C'est donc une extension des STRUCT du C :
class MaClasse {déclaration données et méthodes } MonInstance;
Ne pas oublier le ; final même quand on ne déclare pas d'instance ici (en général les classes sont globales, les variables
devraient plutôt être locales). En fait les mots clef struct et union permettent également la déclaration de méthodes en plus
de données, simplement elles sont par défaut publiques (public) (accessibles aux autres classes) alors que pour une classe
elles sont par défaut privées (private).
b) l'instance
On utilisera le terme "instanciation" à chaque création d'une instance (ce qu'on appelait avant une variable). L'adresse de
l'instance actuelle est appelée "this" (sans avoir à la déclarer).
c) l'héritage
les classes sont structurées de manière arborescente. Si l'on crée une classe d'objets A (dite classe de base), on peut créer
une classe B qui "dérive" de A : elle en hérite toutes les composantes (données et méthodes). On peut, à partir de C++
version 2, utiliser l'héritage multiple (une classe hérite de plusieurs classes de base), alors que ce n'était pas possible avant.
d) la surcharge
On peut décrire plusieurs méthodes de même nom, à condition que chacune s'applique à des types de données différents.
Par exemple on peut définir int puissance(int,int) et float puissance(float,float), les deux
fonctions ayant une implémentation différente suivant le type de données, c'est le compilateur qui choisira en fonction des
types des arguments. On peut même surcharger les opérateurs classiques du C (redéfinir + pour les vecteurs par exemple).
On ne peut pas surcharger deux fonctions ayant exactement les mêmes types d'arguments mais retournant un type différent
(produits scalaire et vectoriel par exemple)
e) le constructeur
Pour chaque classe, il existe une méthode nécessaire (mais non obligatoire, si on ne la définit pas le compilateur en crée
une par défaut) : le constructeur. Son nom est toujours le même que celui de la classe. Il est appelé implicitement à chaque
nouvelle création d'instance ou explicitement par la fonction new (correspond au malloc, mais c'est le compilateur qui
détermine la taille nécessaire). Le constructeur est une fonction qui ne retourne rien (même pas void). Le destructeur est
appelé implicitement à la destruction d'un objet ou explicitement par delete. Remarque : le constructeur peut affecter une
valeur à un membre constant (mais qui ne pourra pas changer jusqu'à sa destruction).
4)APPLICATIONS
a)simple
class Point
{
int X;intY; //les données
int GetX(void) {return X;} // déclaration "interne" ou "inline"
int GetY(void); // déclaration externe
Point (int NewX=0, int NewY=0) {X=NewX;Y=NewY;} //déclaration
//interne du constructeur, avec initialisation par défaut
};
int Point::GetY(void)
{return Y;} //déclaration "externe", il faut préciser
//à quelle classe elle se rapporte ici POINT.
//dans la réalité j'aurai plutot utilisé une déclaration interne
si je déclare :
Point P(5,10); //appel automatique du constructeur à ladéclaration
int coordX;
je peux par exemple appeler la fonction (attention, pas n'importe où, voir paragraphe suivant) :
coordX=P.GetX();
● protégés (protected), c'est à dire accessibles au membres de la classe et des classes dérivées;
● publics (public), c'est à dire accessibles "classiquement" : même portée qu'une déclaration classique C
Exemple :
class Point
{
int X;intY; //privé par défaut
public : // tout ce qui suit est public
int GetX(void) {return X;} // ceci permet d'accéder aux
//infos sans savoir comment elles ont été stockées
int GetY(void) {return Y;}
Point (int NewX, int NewY) {X=NewX;Y=NewY;}
};
On peut utiliser les 3 accès, autant de fois que l'on veut et dans n'importe que ordre. L'accès qui s'applique est le dernier
spécifié (ou celui par défaut, private pour class et public pour struct).
c) héritage
class Point
{
protected: //accessible uniquement par héritage
int X;intY;
public : // accessible partout
int GetX(void) {return X;}
int GetY(void) {return Y;}
Point (int NewX=0, intNewY=0) {X=NewX;Y=NewY;}
};
class Pixel : public Point //dérive de point,
{
protected:
int couleur;
public :
Pixel (int nx,int ny,int coul=0);
void allume(void);
void allume(int couleur); //surcharge : on peut allumer avec une autre couleur
void eteind(void);
};
Les accès dérivés sont le plus restrictif entre celui défini dans la classe de base et celui précisé lors de la dérivation (ici
dérivation publique, les accès restent inchangés sauf pour les privés qui sont inaccessibles).
Pixel::Pixel(int nx,int ny,int coul):Point(nx,ny) // je précise la
//liste (séparée par des virgules) des constructeurs
//(sinon val par défaut), je n'ai plus qu'à construire les
//ajouts par rapport à la classe de base
{couleur=coul;}
void Pixel::allume(void)
{g_pixel(X,Y,couleur);} //g_pixel : une fonction qui allume un pixel à l'écran
void Pixel::allume(int coul)
{g_pixel(X,Y,couleur=coul);}
void Pixel::eteind(void)
{allume(0);}
On pourrait maintenant définir une classe segment contenant un pixel et un point (la couleur n'a besoin d'être stockée
qu'une fois). On redéfinirait des méthodes de même nom : Segment::allume...
d) new, delete
Pixel *ptrPixel = new Pixel(100,100,1); // construction explicite
ptrPixel->allume(); //utilisation
delete ptrPixel; //destruction, le destructeur par défaut est souvent suffisant
Si l'on veut définir explicitement le destructeur d'une classe (pour fermer un fichier par exemple), on utilise le nom de la
classe précédé de ~ :
Point::~Point() {...}
f) classes virtuelles
Soient : une classe A, deux classes B et C dérivant de A, une classe D dérivant de B et C. Nous aurons dans D deux
instances de A (qui peuvent être différentes). Mais si une seule instance de A suffisait, il suffit de les déclarer :
class A {...};
class B : virtual public A {...};
class C : virtual public A {...};
class D : public C, public D {...};
Le constructeur de D appellera une seule fois celui de A
g) polymorphisme
Si plusieurs classes (point, ligne, segment) possèdent des méthodes de même signature (écriture similaire du prototype),
on peut éviter de réécrire des fonctions dont le contenu serait identique mais d'appliquant à des objets différents
(déplacer=éteindre+ajouter+allumer pout tous mes objets). On peut pour cela utiliser les fonctions virtuelles (dynamiques :
le choix de la fonction a utiliser est déterminée à l'exécution) ou les fonctions templates (statiques : le choix des fonctions
est fait à la compilation). Voyez l'exemple complet.
class Point
{
protected: //accessible uniquement par héritage
int X;int Y;
public : // accessible partout
int GetX(void) {return X;} //hors héritage on ne peut que lire,
//pas écrire
int GetY(void) {return Y;}
Point (int NewX=0, int NewY=0) {X=NewX;Y=NewY;}
};
//On peut surcharger << (pour cout) : marche pour le point et ses héritiers :
ostream& operator << (ostream& flux, Point& P)
{ flux << "[" << P.GetX() << "," << P.GetY() << "]";
return flux;
}
void main(void)
{
g_init();
Pixel *ptrPixel = new Pixel(100,100,1);
ptrPixel->allume(); //utilisation
cout << *ptrPixel << ':' << ptrPixel->GetCouleur()<< '\n';
delete ptrPixel; //destruction, le destructeur par défaut est souvent suffisant
Segment s(50,50,100,0,10);
s.allume();
Rectangle r(150,150,100,100,4);
r.allume();
cin.get(); //équivalent du getch
Point decal(-25,25);
cout << (s+decal) << endl;
(r+decal).allume();
s.deplace(0,100);
r.deplace(0,100);
cout << r << endl; //endl envoie un \n
cin.get();
g_fin();
}
#include <graphics.h>
void g_init(void)
{
int gdriver = DETECT, gmode, errorcode;
initgraph(&gdriver, &gmode, "");
errorcode = graphresult();
if (errorcode != grOk)
#ifdef __cplusplus
cout << "g_erreur: " << grapherrormsg(errorcode) <<"\n";
#else
printf("g_erreur: %s\n", grapherrormsg(errorcode));
#endif
setcolor(getmaxcolor());
}
void g_fin(void)
{closegraph();}
{
somme=deltax/2;
while(xd!=xf)
{
xd+=pasx;
somme+=deltay;
if(somme>=deltax) {somme-=deltax;yd+=pasy;}
g_pixel(xd,yd,color);
}
}
else
{
somme=deltay/2;
while(yd!=yf)
{
yd+=pasy;
somme+=deltax;
if(somme>=deltay) {somme-=deltay;xd+=pasx;}
g_pixel(xd,yd,color);
}
}
}
Organisation de l'ordinateur
- Multipostes : plusieurs consoles sur un même ordinateur (CPU puissant, tout est partageable)
- Réseau : plusieurs CPU et MC non partageable (sauf réseau de multipostes), MdM et périphériques
partageables ou locaux.
Langages de programmation
Un ordinateur est une machine bête, ne sachant qu'obéir, et à très peu de choses :
* addition, soustraction, multiplication en binaire, uniquement sur des entiers,
* sortir un résultat ou lire une valeur binaire (dans une mémoire par exemple),
* comparer des nombres.
Sa puissance vient du fait qu'il peut être PROGRAMME, c'est à dire que l'on peut lui donner, à l'avance,
la séquence (la suite ordonnée) des ordres à effectuer l'un après l'autre. Le grand avantage de l'ordinateur
est sa rapidité. Par contre, c'est le programmeur qui doit TOUT faire. L'ordinateur ne comprenant que des
ordres codés en binaire (le langage machine), des langages dits "évolués" ont été mis au point pour
faciliter la programmation, au début des années 60, en particulier FORTRAN (FORmula TRANslator)
pour le calcul scientifique et COBOL pour les applications de gestion. Puis, pour des besoins
pédagogiques principalement, ont été créés le BASIC, pour une approche simple de la programmation, et
PASCAL au début des années 70. Ce dernier (comme le C) favorise une approche méthodique et
disciplinée (on dit "structurée"). Le C a été développé conjointement au système d'exploitation UNIX,
dans les Laboratoires BELL, par Brian W Kernigham et Dennis M Ritchie, qui ont défini en 78, dans
"The C Language", les règles de base de ce langage. Le but principal était de combiner une approche
structurée (et donc une programmation facile) avec des possibilités proches de celles de l'assembleur
(donc une efficacité maximale en exécution, quitte à passer plus de temps de programmation), tout en
restant standard (c'est à dire pouvoir être implanté sur n'importe quelle machine). Puis ce langage a été
normalisé en 88 (norme ANSI), cette norme apportant un nombre non négligeable de modifications au
langage.
Le C est un langage compilé, c'est à dire qu'il faut :
Langage C - Sommaire
● Introduction (première partie)
❍ Organisation de l'ordinateur
❍ Langages de programmation
● Connaissances de base
● Fonctions d'entrées/sorties les plus utilisées
● La syntaxe du C
❍ Second exemple, définitions
❍ Variables / identificateurs / adresse / pointeurs
❍ Expressions / opérateurs
■ Arithmétiques
■ unaires
■ deuxaires
■ Relationnels
■ comparaisons
■ logique booléenne
■ binaires
■ Affectation
■ affectation simple =
■ incrémentation / décrémentation
■ affectation élargie
■ Opérateurs d'adresses
■ Autres
■ conditionnel ? :
■ séquentiel ,
■ Ordre de priorité et associativité
❍ Instructions
❍ Structures de contrôle
■ Boucles
■ Chaînes de caractères
■ Bibliothèques de fonctions pour tableaux et chaînes
■ Allocation dynamique de mémoire
■ Tableaux multidimensionnels
❍ Structures et unions
■ Déclaration
■ Utilisation
■ Champs de bits
■ Unions
■ Structures chaînées
● Les fichiers de données
❍ Fichiers bruts
❍ Fichiers bufférisés
● Directives du pré-compilateur
● Utiliser Turbo C (3.5 par exemple)
● Correction des exercices
● Autres sites sur le C
Connaissances de base
regardons ce petit programme :
#include <stdio.h>
#define TVA 18.6
void main(void)
{
float HT,TTC;
puts ("veuillez entrer le prix H.T.");
scanf("%f",&HT);
TTC=HT*(1+(TVA/100));
printf("prix T.T.C. %f\n",TTC);
}
On trouve dans ce programme :
* des directives du pré processeur (commençant par #)
#include : inclure le fichier définissant (on préfère dire déclarant) les fonctions standard d'entrées/sorties
(en anglais STanDard In/Out), qui feront le lien entre le programme et la console (clavier/écran). Dans
cet exemple il s'agit de puts, scanf et printf.
#define : définit une constante. A chaque fois que le compilateur rencontrera, dans sa traduction de la
suite du fichier en langage machine, le mot TVA, ces trois lettres seront remplacées par 18.6. Ces
transformation sont faites dans une première passe (appelée pré compilation), où l'on ne fait que du
"traitement de texte", c'est à dire des remplacements d'un texte par un autre sans chercher à en
comprendre la signification.
* une entête de fonction. Dans ce cas on ne possède qu'une seule fonction, la fonction principale (main
function). Cette ligne est obligatoire en C, elle définit le "point d'entrée" du programme, c'est à dire
l'endroit où débutera l'exécution du programme.
* un "bloc d'instructions", délimité par des accolades {}, et comportant :
* des déclarations de variables, sous la forme : type listevariables;
Une variable est un case mémoire de l'ordinateur, que l'on se réserve pour notre programme. On définit le
nom que l'on choisit pour chaque variable, ainsi que son type, ici float, c'est à dire réel (type dit à virgule
flottante, d'où ce nom). Les trois types scalaires de base du C sont l'entier (int), le réel (float) et le
caractère (char). On ne peut jamais utiliser de variable sans l'avoir déclarée auparavant. Une faute de
frappe devrait donc être facilement détectée, à condition d'avoir choisi des noms de variables
char getche(void);
{
char caractere;
caractere=getch();
putch(caractere);
return(caractere);
}
ou même {return(putch(getch))}
Dans la pratique, ce n'est pas ainsi que getche est réellement défini, mais en assembleur pour un résultat
plus rapide.
Dans STDIO.H, on trouve des fonctions plus évoluées, pouvant traiter plusieurs caractères à la suite (par
les fonctions de conio.h), et les transformer pour en faire une chaîne de caractères ou une valeur
numérique, entière ou réelle par exemple. Les entrées sont dites "bufférisées", c'est à dire que le texte
n'est pas transmis, et peut donc encore être modifié, avant le retour chariot.
puts(chaîne) affiche, sur stdout, la chaîne de caractères puis positionne le curseur en début de ligne
suivante. puts retourne EOF en cas d'erreur.
gets(chaîne) lecture d'une chaîne sur stdin. Tous les caractères peuvent être entrés, y compris les blancs.
La saisie est terminée par un retour chariot. Gets retourne un pointeur sur le premier caractère entré (donc
égal à son paramètre d'entrée, ou le pointeur NULL en cas d'erreur (fin du fichier stdin par exemple).
printf(format,listevaleurs) affiche la liste de valeurs (variables ou expressions) dans le format choisi.
Le format est une chaîne de caractères entre guillemets (doubles quote "), dans laquelle se trouve un texte
qui sera écrit tel quel, des spécifications de format (débutant par %) qui seront remplacées par la valeur
effective des variables, dans l'ordre donné dans listevaleurs, et de caractères spéciaux (\). Printf retourne
le nombre de caractères écrits, ou EOF en cas de problème.
Une spécification de format est de la forme :
% [flag][largeur][.précision][modificateur]type (entre [] facultatifs)
Le flag peut valoir : - (cadrage à gauche, rajout de blancs si nécessaire à droite), + (impression du signe,
même pour les positifs), blanc (impression d'un blanc devant un nombre positif, à la place du signe), 0 (la
justification d'un nombre se ferra par rajout de 0 à gauche au lieu de blancs). D'autres Flags sont
possibles mais moins utiles, pour plus de détails voir l'aide en ligne de Turbo C.
La largeur est le nombre minimal de caractères à écrire (des blancs sont rajoutés si nécessaire). Si le
texte à écrire est plus long, il est néanmoins écrit en totalité. En donnant le signe * comme largeur, le
prochain argument de la liste de valeurs donnera la largeur (ex printf("%*f",largeur,réel)).
La précision définit, pour les réels, le nombre de chiffres après la virgule (doit être inférieur à la largeur).
Dans la cas d'entiers, indique le nombre minimal de chiffes désiré (ajout de 0 sinon), alors que pour une
chaîne (%s), elle indique la longueur maximale imprimée (tronqué si trop long). La précision peut,
comme la largeur, être variable en donnant .*
Le modificateur peut être : h (short, pour les entiers), l (long pour les entiers, double pour les réels), L
(long double pour les réels).
Le type est : c (char), s (chaîne de caractères, jusqu'au \0), d (int), u (entier non signé), x ou X (entier
affiché en hexadécimal), o (entier affiché en octal), f (réel en virgule fixe), e ou E (réel en notation
exponentielle), g ou G (réel en f si possible, e sinon), p (pointeur), % (pour afficher le signe %).
Les caractères spéciaux utilisables dans le format sont \t (tabulation), \n (retour à la ligne), \\ (signe \), \nb
tout code ASCII, en décimal, hexa ou octal (\32=\040=\0x20=' ').
scanf(format,listeadresse) lecture au clavier de valeurs, dans le format spécifié. Les arguments sont des
pointeurs sur les variables résultats (dans le cas de variables scalaires, les précéder par l'opérateur &).
Scanf retourne le nombre de valeurs effectivement lues et mémorisées (pas les %*).
Le format peut contenir (entre ") : du texte (il devra être tapé exactement ainsi par l'utilisateur, et ne sera
pas stocké), des séparateurs blancs (l'utilisateur devra taper un ou plusieurs blancs, tabulations, retours à
La syntaxe du C
● Second exemple, définitions
● Variables / identificateurs / adresse / pointeurs
● Expressions / opérateurs
❍ Arithmétiques
■ unaires
■ deuxaires
❍ Relationnels
■ comparaisons
■ logique booléenne
■ binaires
❍ Affectation
■ affectation simple =
■ incrémentation / décrémentation
■ affectation élargie
❍ Opérateurs d'adresses
❍ Autres
■ conditionnel ? :
■ séquentiel ,
❍ Ordre de priorité et associativité
● Instructions
● Structures de contrôle
❍ Boucles
■ While (tant que)
■ Do While (faire tant que)
■ For (pour)
❍ Branchements conditionnels
■ If - Else (Si - Sinon)
■ Switch - Case (brancher - dans le cas)
❍ Branchements inconditionnels
■ Break (interrompre)
■ Continue (continuer)
■ Goto (aller à)
■ Return (retourner)
■ Exit (sortir)
● Déclaration et stockage des variables
❍ Déclarations locales
❍ Déclarations globales
❍ Déclaration de type
● Fonctions
❍ Définitions générales
❍ Récursivité, gestion de la pile
❍ Arguments passés par adresse
❍ La fonction main
❍ Fonction retournant un pointeur et pointeur de fonction
● Les types de données du C
❍ Variables scalaires
■ char : caractère (8 bits)
■ int : entier
■ float : réel
■ Tailles et plages
■ Conversions de type / cast
■ Enumérations
● Tableaux
❍ Tableaux unidimensionnels
❍ Tableaux et pointeurs / arithmétique des pointeurs
❍ Chaînes de caractères
❍ Bibliothèques de fonctions pour tableaux et chaînes
❍ Allocation dynamique de mémoire
❍ Tableaux multidimensionnels
● Structures et unions
❍ Déclaration
❍ Utilisation
❍ Champs de bits
❍ Unions
❍ Structures chaînées
#include <stdio.h>
void affiche_calcul(float,float); /* prototype */
float produit(float,float);
int varglob;
void main(void)
{
float a,b; /* déclaration locale */
varglob=0;
puts("veuillez entrer 2 valeurs");
scanf("%f %f",&a,&b);
affiche_calcul(a,b);
printf("nombre d'appels à produit : %d\n",varglob);
}
norme ANSI) que s'il était différent de int (entier). Il doit désormais être void (rien) si la fonction ne
renvoie rien (dans un autre langage on l'aurait alors appelé sous-programme, procédure, ou sous-routine).
Les arguments, s'ils existent, sont passés par valeur. Si la fonction ne nécessite aucun argument, il faut
indiquer (void) d'après la norme ANSI, ou du moins ().
Le corps est composé de déclarations de variables locales, et d'instructions, toutes terminées par un ;
Expressions / opérateurs
Une expression est un calcul qui donne une valeur résultat (exemple : 8+5). Une expression comporte des
variables, des appels de fonction et des constantes combinés entre eux par des opérateurs (ex :
MaVariable*sin(VarAngle*PI/180) ).
Une expression de base peut donc être un appel à une fonction (exemple sin(3.1416). Une fonction est un
bout de programme (que vous avez écrit ou faisant partie d'une bibliothèque) auquel on "donne" des valeurs
(arguments), entre parenthèses et séparés par des virgules. La fonction fait un calcul sur ces arguments pour
"retourner" un résultat. Ce résultat pourra servir, si nécessaire, dans une autre expression, voire comme
argument d'une fonction exemple atan(tan(x)). Les arguments donnés à l'appel de la fonction (dits
paramètres réels ou effectifs) sont recopiés dans le même ordre dans des copies (paramètres formels), qui
elles ne pourront que modifier les copies (et pas les paramètres réels). Dans le cas de fonctions devant
modifier une variable, il faut fournir en argument l'adresse (par l'opérateur &, voir plus bas), comme par
exemple pour scanf.
Pour former une expression, les opérateurs possibles sont assez nombreux, nous allons les détailler suivant
les types de variables qu'ils gèrent.
Arithmétiques
Ces opérateurs s'appliquent à des valeurs entières ou réelles.
unaires
Ce sont les opérateurs à un seul argument : - et + (ce dernier a été rajouté par la norme ANSI). Le résultat
est du même type que l'argument.
deuxaires
Le terme "deuxaire" n'est pas standard, je l'utilise parce que binaire est pour moi associé à la base 2.
Ces opérateurs nécessitent deux arguments, placés de part et d'autre de l'opérateur. Ce sont + (addition), -
(soustraction), * (produit), / (division), % (reste de la division). % nécessite obligatoirement deux
arguments entiers, les autres utilisent soit des entiers, soit des réels. Les opérandes doivent être du même
type, le résultat sera toujours du type des opérandes. Lorsque les deux opérandes sont de type différent
(mais numérique évidement), le compilateur prévoit une conversion implicite (vous ne l'avez pas demandée
mais il la fait néanmoins) suivant l'ordre : { char -> int -> long -> float -> double } et { signed -> unsigned
}. On remarque qu'il considère les char comme des entiers, les opérations sont en fait faites sur les numéros
de code (ASCII). Les calculs arithmétiques sont faits uniquement soit en long soit en double, pour éviter
des dépassements de capacité.
exemples :
int a=1,b=2,c=32000;
float x=1,y=2;
a=(c*2)/1000; /* que des int, le résultat est 64, bien que l'on soit
passé par un résultat intermédiaire (64000) qui dépassait la capacité
Relationnels
comparaisons
Ces opérateurs sont deuxaires : = = (égalité), != (différent), <, >, <=, >=. Des deux côtés du signe
opératoire, il faut deux opérandes de même type (sinon, transformation implicite) mais numérique (les
caractères sont classés suivant leur numéro de code ASCII). Le résultat de l'opération est 0 si faux, 1 si vrai
(le résultat est de type int). Exemple : (5<7)+3*((1+1)= =2) donne 4. Attention, le compilateur ne vous
prévient pas si vous avez mis = au lieu de = = (= est aussi un opérateur, voir plus loin), mais le résultat sera
différent de celui prévu.
logique booléenne
Le résultat est toujours 0 (faux) ou 1 (vrai), les opérandes devant être de type entier (si char conversion
implicite), 0 symbolisant faux, toute autre valeur étant considérée vraie.
Opérateur unaire : ! (non). !arg vaut 1 si arg vaut 0, et 0 sinon.
Opérateurs deuxaires : && (ET, vaut 1 si les 2 opérandes sont non nuls, 0 sinon) et || (OU, vaut 0 si les
deux opérandes sont nuls, 1 sinon). Le deuxième opérande n'est évalué que si le premier n'a pas suffi pour
conclure au résultat (ex (a= =0)&&(x++<0) incrémente x si a est nul, le laisse intact sinon).
binaires
Ces opérateurs ne fonctionnent qu'avec des entiers. Ils effectuent des opérations binaires bit à bit. On peut
utiliser ~ (complément, unaire), & (et), | (ou inclusif), ^ (ou exclusif), >> (décalage à droite, le 2ème
opérande est le nombre de décalages), << (décalage à gauche). Contrairement aux opérateurs relationnels,
les résultats ne se limitent pas à 0 et 1.
exemples : 7&12 donne 4 (car 0111&1100 donne 0100); ~0 donne -1 (tous les bits à 1, y compris celui de
signe); 8>>2 donne 32.
Affectation
affectation simple =
En C, l'affectation (signe =) est une opération comme une autre. Elle nécessite deux opérantes, un à droite,
appelé Rvalue, qui doit être une expression donnant un résultat d'un type donné, et un à gauche (Lvalue)
qui doit désigner l'endroit en mémoire où l'on veut stocker la Rvalue. Les deux opérandes doivent être de
même type, dans le cas d'opérandes numériques si ce n'est pas le cas le compilateur effectuera une
conversion implicite (la Lvalue doit être de type "plus fort" que la Rvalue). L'opération d'affectation rend
une valeur, celle qui a été transférée, et peut donc servir de Rvalue.
Exemples : a=5 (met la valeur 5 dans la variable a. Si a est float, il y a conversion implicite en float);
b=(a*5)/2 (calcule d'abord la Rvalue, puis met le résultat dans b); a=5+(b=2) (Le compilateur lit
l'expression de gauche à droite. la première affectation nécessite le calcul d'une Rvalue : 5+(b=2). Celle ci
comporte une addition, dont il évalue le premier opérande (5) puis le second (b=2). Il met donc 2 dans b, le
résultat de l'opération est 2, qui sera donc ajouté à 5 pour être mis dans a. A vaut donc 7 et b, 2. Le résultat
de l'expression est 7 (si l'on veut s'en servir).
Remarque : il ne faut pas confondre = et = =. Le compilateur ne peut pas remarquer une erreur
(contrairement au Pascal ou Fortran) car les deux sont possibles. Exemple : if (a=0) est toujours faux car
quelle que soit la valeur initiale de a, on l'écrase par la valeur 0, le résultat de l'opération vaut 0 et est donc
interprété par IF comme faux.
incrémentation / décrémentation
++a : ajoute 1 à la variable a. Le résultat de l'expression est la valeur finale de a (c'est à dire après
incrémentation). On l'appelle incrémentation préfixée.
a++ : ajoute 1 à la variable a. Le résultat de l'expression est la valeur initiale de a (c'est à dire avant
incrémentation). C'est l'incrémentation postfixée.
de même, la décrémentation --a et a-- soustrait 1 à a.
exemple : j=++i est équivalent à j=(i=i+1)
affectation élargie
Opérateurs d'adresses
Ces opérateurs sont utilisées avec des pointeurs. On utilise
● &variable : donne l'adresse d'une variable
exemple : supposons déclarer : int i1=1,i2=2; int *p1,*p2; i1 et i2 sont deux mémoires contenant un entier,
alors que p1 et p2 sont des pointeurs, puisqu'ils contiennent une adresse d'entier. p1=&i1; met dans p1
l'adresse de i1. p2=p1; met la même adresse (celle de i1) dans p2. printf("%d\n",*p1) affiche ce qui est
désigné (pointé) par p1 donc i1 donc 1. p2=&i2;*p2=*p1; à l'adresse pointée par p2 mettre ce qui est pointé
par p1, donc copier la valeur de i1 dans i2. printf("%d\n",i2) affiche donc 1.
Autres
conditionnel ? :
C'est un (le seul) opérateur ternaire. L'expression a?b:c vaut la valeur de b si a est vrai (entier, différent de
0), et c si a est faux. Exemple : max=a>b?a:b
séquentiel ,
Cet opérateur permet de regrouper deux sous expressions en une seule. On effectue le premier opérande
puis le second, la valeur finale de l'expression étant celle du second opérande. On l'utilise pour évaluer
deux (ou plus) expressions là où la syntaxe du C ne nous permettait que d'en mettre une, exemple :
for(i=j=0;i>10;i++,j++). Dans le cas d'une utilisation de cet opérateur dans une liste, utilisez les parenthèses
pour distinguer les signes , : exemple (inutile) : printf("%d %d",(i++,j++),k) i est modifié mais sa valeur
n'est pas affichée.
Dans ce tableau, les opérateurs sont classés par priorité décroissante (même priorité pour les opérateurs
d'une même ligne). Les opérateurs les plus prioritaires se verront évaluer en premier. L'associativité définit
l'ordre d'évaluation des opérandes. La plupart se font de gauche à droite ( 4/2/2 donne (4/2)/2 donc 1 (et pas
4/(2/2))).
Les seules exceptions sont :
● les opérateurs unaires, écrits à gauche de l'opérateur. L'opérande est évalué puis l'opération est
effectuée, le résultat est celui de l'opération; sauf dans le cas de l'incrémentation / décrémentation
postfixée, où le résultat de l'expression est la valeur de l'argument avant l'opération.
● L'affectation : on calcule l'opérande de droite, puis on l'affecte à celui de gauche. Le résultat est la
valeur transférée.
● La virgule : la valeur à droite est calculée avant celle à gauche (en particulier lors d'un appel de
fonction)
● Les opérateurs logiques et conditionnel évaluent toujours leur premier argument. Le second par
contre n'est évalué que si c'est nécessaire.
Instructions
Une instruction peut être :
- soit une expression (pouvant comprendre une affectation, un appel de fonction...), terminé par un ; qui en
fait signifie "on peut oublier le résultat de l'expression et passer à la suite",
- soit une structure de contrôle (boucle, branchement...),
- soit un bloc d'instructions : ensemble de déclarations et instructions délimités par des accolades {}. Un
bloc sera utilisé à chaque fois que l'on désire mettre plusieurs instructions là où on ne peut en mettre qu'une.
Seule la première forme est terminée par un ;. Un cas particulier est l'instruction vide, qui se compose
uniquement d'un ; (utilisé là où une instruction est nécessaire d'après la syntaxe).
Structures de contrôle
Normalement, les instructions s'effectuent séquentiellement, c'est à dire l'une après l'autre. Pour accéder à
une autre instruction que la suivante, on a trois solutions : le branchement inconditionnel, le branchement
conditionnel et la boucle.
Boucles
Une boucle permet de répéter plusieurs fois un bloc d'instructions.
exemple :
#include <stdio.h>
void main(void)
{
float nombre,racine=0;
puts("entrez un nombre réel entre 0 et 10");
scanf("%f",&nombre);
while (racine*racine<nombre) racine+=0.01;
printf("la racine de %f vaut %4.2f à 1%% près\n",
nombre, racine);
}
Exercice (while_puiss) : faire un programme qui affiche toutes les puissances de 2, jusqu'à une valeur
maximale donnée par l'utilisateur. On calculera la puissance par multiplications successives par 2. Cliquez
ici pour une solution.
#include <stdio.h>
#include <math.h>
#define debut 100
#define pas 0.01
void main(void)
{
float nombre=debut;
int compte=0,tous_les;
puts("afficher les résultats intermédiaires
tous les ? (333 par exemple) ?");
scanf("%d",&tous_les);
while (fabs(nombre-(debut+(compte*pas)))<pas)
{
nombre+=pas;
if (!(++compte%tous_les))
printf("valeur obtenue %12.8f, au lieu de %6.2f en %d calculs\n",
nombre,(float)(debut+(compte*pas)), compte);
}
printf("erreur de 100%% en %d calculs\n",compte);
}
Cliquez ici pour une solution.
exemple :
#include <stdio.h>
void main(void)
{
int a;
do
{
puts("entrez le nombre 482");
scanf("%d",&a);
}
while (a!=482);
puts("c'est gentil de m'avoir obéi");
}
Exercice (do_while) : écrivez un programme de jeu demandant de deviner un nombre entre 0 et 10 choisi
par l'ordinateur. On ne donnera pas d'indications avant la découverte de la solution, où l'on indiquera le
nombre d'essais. La solution sera choisie par l'ordinateur par la fonction rand() qui rend un entier aléatoire
(déclarée dans stdlib.h). Cliquez ici pour une solution.
For (pour)
expr_initiale;
while (expr_condition)
{
instruction
expr_incrémentation;
}
Une ou plusieurs des trois expressions peuvent être omises, l'instruction peut être vide. for(;;); est donc une
boucle infinie.
exemple :
{ char c; for(c='Z';c>='A';c--)putchar(c); }
Exercice (for) : faire un programme qui calcule la moyenne de N notes. N et les notes seront saisies par
scanf. Le calcul de la moyenne s'effectue en initialisant une variable à 0, puis en y ajoutant progressivement
les notes saisies puis division par N. Cliquez ici pour une solution.
Branchements conditionnels
On a souvent besoin de n'effectuer certaines instructions que dans certains cas. On dispose pour cela du IF
et du SWITCH.
if(c1) i1;
else if (c2) i2;
else if (c3) i3;
else i4;
i5;
si c1 alors i1 puis i5, sinon mais si c2 alors i2 puis i5, ... Si ni c1 ni c2 ni c3 alors i4 puis i5.
Le else étant facultatif, il peut y avoir une ambiguïté s'il y a moins de else que de if. En fait, un else se
rapporte toujours au if non terminé (c'est à dire à qui on n'a pas encore attribué de else) le plus proche. On
peut aussi terminer un if sans else en l'entourant de {}.
exemple : if(c1) if(c2) i1; else i2; : si c1 et c2 alors i1, si c1 et pas c2 alors i2, si pas c1
alors (quel que soit c2) rien.
if (c1) {if (c2) i1;} else i2; : si c1 et c2 alors i1, si c1 et pas c2 alors rien, si pas c1 alors
i2.
switch (expression_entière)
{
case cste1:instructions
case cste2:instructions
........
case csteN:instructions
default :instructions
}
L'expression ne peut être qu'entière (char, int, long). L'expression est évaluée, puis on passe directement au
"case" correspondant à la valeur trouvée. Le cas default est facultatif, mais si il est prévu il doit être le
dernier cas.
exemple : fonction vérifiant si son argument c est une voyelle.
int voyelle(char c)
{
switch(c)
{
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
case 'y':return(1); /* 1=vrai */
default :return(0);
}
}
Remarque : l'instruction break permet de passer directement à la fin d'un switch (au } ). Dans le cas de
switch imbriqués on ne peut sortir que du switch intérieur.
Exemple :
switch (a)
{
case 1:inst1;inst2;....;break;
case 2:....;break;
default:.....
}
/*endroit où l'on arrive après un break*/
Exercice (calcul) : faire un programme simulant une calculatrice 4 opérations. Cliquez ici pour une
solution.
Branchements inconditionnels
Quand on arrive sur une telle instruction, on se branche obligatoirement sur une autre partie du programme.
Ces instructions sont à éviter si possible, car ils rendent le programme plus complexe à maintenir, le fait
d'être dans une ligne de programme ne suffisant plus pour connaître immédiatement quelle instruction on a
fait auparavant, et donc ne permet plus d'assurer que ce qui est au dessus est correctement terminé. Il ne
faut les utiliser que dans certains cas simples.
Break (interrompre)
Il provoque la sortie immédiate de la boucle ou switch en cours. Il est limité à un seul niveau d'imbrication.
exemples :
do {if(i= =0)break;....}while (i!=0); /* un while aurait été mieux */
for (i=0;i<10;i++){....;if (erreur) break;} /* à remplacer par
for(i=0;(i<10)&&(!erreur);i++){...} */
Continue (continuer)
Cette instruction provoque le passage à la prochaine itération d'une boucle. Dans le cas d'un while ou do
while, saut vers l'évaluation du test de sortie de boucle. Dans le cas d'un for on passe à l'expression
d'incrémentation puis seulement au test de bouclage. En cas de boucles imbriquées, permet uniquement de
continuer la boucle la plus interne.
exemple :
for (i=0;i<10;i++) {if (i= =j) continue; ...... }
peut être remplacé par :
for (i=0;i<10;i++) if (i!=j) { ...... }
Goto (aller à)
La pratique des informaticiens a montré que l'utilisation des goto donne souvent des programmes non
maintenables (impossibles à corriger ou modifier). Les problèmes qu'ils posent ont amené les
programmeurs expérimentés à ne s'en servir qu'exceptionnellement.
structure : goto label;
Label est un identificateur (non déclaré, mais non utilisé pour autre chose), suivi de deux points (:), et
indiquant la destination du saut. Un goto permet de sortir d'un bloc depuis n'importe quel endroit. Mais on
ne peut entrer dans un bloc que par son { (qui créera proprement les variables locales du bloc).
{.....
{.....
goto truc;
.....
}
.....
truc:
.....
}
Return (retourner)
Permet de sortir de la fonction actuelle (y compris main), en se branchant à son dernier }. Return permet
également (et surtout) de rendre la valeur résultat de la fonction.
structure : return ou return(valeur)
exemple :
Exit (sortir)
Ceci n'est pas un mot clef du C mais une fonction disponible dans la plupart des compilateurs (définie par
ANSI, dans STDLIB.H). Elle permet de quitter directement le programme (même depuis une fonction). On
peut lui donner comme argument le code de sortie (celui que l'on aurait donné à return dans main). Cette
fonction libère la mémoire utilisée par le programme (variables + alloc) et ferme (sur beaucoup de
compilateurs) les fichiers ouverts.
structure : exit(); ou exit(valeur);
Déclarations locales
Dans tout bloc d'instructions, avant la première instruction, on peut déclarer des variables. Elles seront alors
"locales au bloc" : elles n'existent qu'à l'intérieur du bloc. Ces variables sont mises en mémoire dans une
zone de type "pile" : quand, à l'exécution, on arrive sur le début du bloc ({), on réserve la mémoire
nécessaire aux variables locales au sommet de la pile (ce qui en augmente la hauteur), et on les retire en
quittant le bloc. L'intérêt consiste à n'utiliser, à un instant donné, que la quantité nécessaire de mémoire, qui
peut donc resservir par après pour d'autres variables. Le désavantage (rarement gênant et pouvant être
contrecarré par la classe STATIC) est qu'en quittant le bloc (par } ou un branchement), et y entrant à
nouveau plus tard (par son {), les variables locales ne sont plus nécessairement recréées au même endroit,
et n'auront plus le même contenu. De plus la libération/réservation de la pile aura fait perdre un peu de
temps. Par contre, lorsque l'on quitte temporairement un bloc (par appel à une fonction), les variables
locales restent réservées. La sortie d'un bloc par un branchement gère la libération des variables locales,
mais seule l'entrée dans un bloc par son { gère leur création.
Exemple :
#include <stdio.h>
int doubl(int b)
{int c; c=2*b; b=0; return(c); }
void main(void)
{
int a=5;
printf("%d %d\n",doubl(a),a);
}
A l'entrée du bloc main, création de a, à qui l'on donne la valeur 5. Puis appel de doubl : création de b au
sommet de la pile, on lui donne la valeur de a. Puis entrée dans le bloc, création sur la pile de c, on lui
donne la valeur b*2=10, on annule b (mais pas a), on rend 10 à la fonction appelante, et on libère le
sommet de la pile (c et b n'existent plus) mais a reste (avec son ancienne valeur) jusqu'à la sortie de main.
On affichera donc : 10 5.
Une variable locale est créée à l'entrée du bloc, et libérée à la sortie. Cette période est appelée sa durée de
vie. Mais pendant sa durée de vie, une variable peut être visible ou non. Elle est visible : dans le texte
source du bloc d'instruction à partir de sa déclaration jusqu'au }, mais tant qu'une autre variable locale de
même nom ne la cache pas. Par contre elle n'est pas visible dans une fonction appelée par le bloc (puisque
son code source est hors du bloc).
autre exemple :
void main(void);
{int a=1; [1]
{int b=2; [2]
{int a=3; [3]
fonction(a); [4]
} [5]
fonction(a); [6]
} [7]
} [8]
int fonction (int b) [a]
{int c=0; [b]
c=b+8; [c]
} [d]
analysons progressivement l'évolution de la pile au cours du temps (en gras : variable visible) :
[1] a=1
[2] a=1 | b=2
[3] a=1 | b=2 | a=3 : seul le a le plus haut est visible (a=3), l'autre vit encore (valeur 1 gardée) mais n'est
plus visible.
[4a] a=1 | b=2 | a=3 | b=3 : entrée dans la fonction, recopie de l'argument réel (a) dans l'argument formel
(b). Mais a n'est plus visible.
[4b] a=1 | b=2 | a=3 | b=3 | c=0
[4c] a=1 | b=2 | a=3 | b=3 | c=11 : quand le compilateur cherche la valeur de b, il prend la plus haute de la
pile donc 3 (c'est la seule visible), met le résultat dans le c le plus haut. L'autre b n'est pas modifié.
[4d] a=1 | b=2 | a=3 : suppression des variables locales b et c du sommet de la pile
[5] a=1 | b=2 : sortie de bloc donc libération de la pile
[6a] a=1 | b=2 | b=1 : l'argument réel (a) n'est plus le même qu'en [4a]
[6b] a=1 | b=2 | b=1 | c=0
[6c] a=1 | b=2 | b=1 | c=9
[6d] a=1 | b=2 : suppression b et c
[7] a=1
[8] la pile est vide, on quitte le programme
Notez que la réservation et l'initialisation prennent un peu de temps à chaque entrée du bloc. Mais ne
présumez jamais retrouver une valeur sur la pile, même si votre programme n'a pas utilisé la pile entre
Déclarations globales
Une déclaration faite à l'extérieur d'un bloc d'instructions (en général en début du fichier) est dite globale.
La variable est stockée en mémoire statique, sa durée de vie est celle du programme. Elle est visible de sa
déclaration jusqu'à la fin du fichier. Elle sera initialisée une fois, à l'entrée du programme (initialisée à 0 si
pas d'autre précision). Le format d'une déclaration globale est identique à une déclaration locale, seules les
classes varient.
Par défaut, la variable est publique, c'est à dire qu'elle pourra même être visible dans des fichiers compilés
séparément (et reliés au link).
La classe static, par contre, rend la visibilité de la variable limitée au fichier actuel.
La classe extern permet de déclarer une variable d'un autre fichier (et donc ne pas lui réserver de mémoire
ici, mais la rendre visible). Elle ne doit pas être initialisée ici. Une variable commune à plusieurs fichiers
devra donc être déclarée sans classe dans un fichier (et y être initialisée), extern dans les autres.
Toute fonction, pour pouvoir être utilisée, doit également être déclarée. Une déclaration de fonction ne peut
être que globale, et connue des autres fonctions. Une déclaration de fonction est appelée "prototype". Le
prototype est de la forme :
[classe] type_retourné nom_fonction(liste_arguments);
elle est donc identique à l'entête de la fonction mais :
- est terminée par un ; comme toute déclaration
- les noms des arguments n'ont pas besoin d'être les mêmes, il peuvent même être omis (les types des
arguments doivent être identiques).
Sans précision de classe, la fonction est publique. Sinon, la classe peut être extern (c'est ce que l'on trouve
dans les fichiers .H) ou static (visibilité limitée au fichier). Le prototype peut être utilisé pour utiliser une
fonction du même fichier, mais avant de l'avoir définie (par exemple si l'on veut main en début du fichier).
En général, lorsque l'on crée une bibliothèque (groupe de fonctions et variables regroupées dans un fichier
compilé séparément), on prépare un fichier regroupant toutes les déclarations extern, noté .H, qui pourra
être inclus dans tout fichier utilisant la bibliothèque.
exemples de déclarations globales :
int i,j; /* publiques, initialisées à 0 */
static int k=1; /* privée, initialisée à 1 */
extern int z; /* déclarée (et initialisée) dans un autre fichier */
float produit(float,float); /* prototype d'une fonction définie plus
loin dans ce fichier */
extern void échange(int *a, int *b); /* prototype d'une fonction définie
dans un autre fichier */
Avant la norme ANSI, le prototype n'existait pas. Une fonction non définie auparavant était considérée
comme rendant un int (il fallait utiliser un cast si ce n'était pas le cas).
Déclaration de type
La norme ANSI permet de définir de nouveaux types de variables par typedef.
structure : typedef type_de_base nouveau_nom;
Ceci permet de donner un nom à un type donné, mais ne crée aucune variable. Une déclaration typedef est
normalement globale et publique.
exemple :
typedef long int entierlong; /* définition d'un nouveau type */
entierlong i; /* création d'une variable i de type entierlong */
typedef entierlong *pointeur; /* nouveau type : pointeur = pointeur d'entierlong */
pointeur p; /* création de p (qui contiendra une adresse), peut être initialisé par =&i */
Remarque : Le premier typedef pouvait être remplacé par un #define mais pas le second.
Fonctions
Définitions générales
Une fonction est définie par son entête, suivie d'un bloc d'instructions
entête : type_retourné nom_fonction(liste_arguments) (pas de ;)
Avant la norme ANSI, le type_retourné pouvait être omis si int. Désormais il est obligatoire, si la fonction
ne retourne rien on indique : void.
La liste_arguments doit être typée (ANSI), alors qu'auparavant les types étaient précisés entre l'entête et le
bloc :
ANSI: float truc(int a, float b) {bloc}
K&R: float truc(a,b) int a;float b; {bloc}
Si la fonction n'utilise pas d'arguments il faut la déclarer (ANSI) nom(void) ou (K&R) nom(). L'appel se
fera dans les deux cas par nom() (parenthèses obligatoires).
Les arguments (formels) sont des variables locales à la fonction. Les valeurs fournies à l'appel de la
fonction (arguments réels) y sont recopiés à l'entrée dans la fonction. Les instructions de la fonction
s'exécutent du début du bloc ({) jusqu'à return(valeur) ou la sortie du bloc (}). La valeur retournée par la
fonction est indiquée en argument de return.
exemple :
int factorielle(int i)
{
if (i>1) return(i*factorielle(i-1));
else return(1);
}
analysons la pile en appelant factorielle(3) :
i=1
i=2 i=2 i=2
i=3 i=3 i=3 i=3 i=3
● (c) création de i, i=1 donc on quitte la fonction, on libère le pile de son sommet, on retourne où la
fonction factorielle(1) a été appelée en rendant 1.
● (d) on peut maintenant calculer i*factorielle(1), i (sommet de la pile) vaut 2, factorielle(1) vaut 1, on
peut rendre 2, puis on "dépile" i
● (e) on peut calculer i*factorielle(2), i vaut 3 (sommet de la pile), factorielle(2) vaut 2 3*2=6, on
retourne 6, la pile est vidée et retrouve sont état initial.
Attention, la récursivité est gourmande en temps et mémoire, il ne faut l'utiliser que si l'on ne sait pas
facilement faire autrement :
int factorielle(int i)
{
int result;
for(result=1;i>1;i--) result*=i;
return(result);
}
En conclusion, pour effectuer un passage d'argument par adresse, il suffit d'ajouter l'opérateur & devant
l'argument réel (à l'appel de la fonction), et l'opérateur * devant chaque apparition de l'argument formel,
aussi bien dans l'entête que le bloc de la fonction.
La fonction main
Si on le désire, la fonction main peut rendre un entier (non signé) au système d'exploitation (sous MSDOS,
on récupère cette valeur par ERRORLEVEL). Une valeur 0 signale en général une fin normale du
programme, sinon elle représente un numéro d'erreur. L'arrivée sur le } final retourne la valeur 0, dans le
cas où on n'a pas indiqué de return(code).
De même, le système d'exploitation peut transmettre des arguments au programme. La déclaration complète
de l'entête de la fonction main est :
int main(int argc,char *argv[], char *env[])
Le dernier argument est optionnel).
On peut aussi utiliser char **argv, mais cela peut paraître moins clair. argc indique le nombre de
mots de la ligne de commande du système d'exploitation, argv est un tableau de pointeurs sur chaque mot
de la ligne de commande, env pointe sur les variables de l'environnement (sous MSDOS obtenues par SET,
mais aussi très utilisées sous UNIX par env).
Si votre programme s'appelle COPIER, et que sous MSDOS vous ayez entré la commande COPIER TRUC
MACHIN alors argc vaut 3, argv[0] pointe sur "COPIER", argv[1] sur "TRUC" et argv[2] sur "MACHIN".
argv[3] vaut le pointeur NULL. env est un tableau de pointeurs sur les variables d'environnement, on n'en
connaît pas le nombre mais le dernier vaut le pointeur NULL.
int max(int,int);
int min(int,int);
void main(void);
{
int (*calcul)(int,int);
/* calcul est un pointeur donc une variable qui
peut être locale */
char c;
puts("utiliser max (A) ou min (I) ?");
do c=getchar(); while ((c!='A')&&(c!='I'));
calcul=(c= ='A')?&max:&min;
printf("%d\n",(*calcul)(10,20));
}
int max(int a,int b) {return(a>b?a:b);}
int min(int a,int b) {return(a<b?a:b);}
Cette fonctionnalité du C est assez peu utilisée, mais est nécessaire dans les langages orientés objets.
Variables scalaires
On appelle variable scalaire une variable ne contenant qu'une seule valeur, sur laquelle on pourra faire un
calcul arithmétique. On possède trois types de base (char, int, float) que l'on peut modifier par 3
spécificateurs (short, long, unsigned).
Une constante caractère est désignée entre apostrophes (simples quotes). 'a' correspond à un octet (alors que
"a" à deux octets : 'a' et '\0', pour plus de détails voir le paragraphe chaînes de caractères). On peut définir
certains caractères spéciaux, par le préfixe \ (antislash) :
● \n nouvelle ligne
● \t tabulation
● \b backspace
● \' apostrophe
● \\ antislash
● \0 nul
● \nombre en octal sur 3 chiffres (ou moins si non suivi d'un chiffre).
● \xnombre : en hexa
Les char sont considérés dans les calculs comme des int (on considère leur code ASCII). Par défaut en C un
char est signé donc peut varier de -128 à +127. Pour utiliser les caractères spéciaux du PC (non standard), il
vaut mieux utiliser des unsigned char (de 0 à 255). Mais le comme C fait les calculs modulo 256, ça
marche presque pareil.
int : entier
Si l'on désire une taille précise, utiliser short int (16 bits) ou long int (32 bits). Sans précision, int donnera
les programmes les plus rapides pour une machine donnée (int = short sur PC, mais long sur les stations 32
bits). Par défaut, les int sont signés, mais on peut préciser unsigned int.
Désormais certains compilateurs considèrent short comme 16 bits, int comme 32 bits et long comme 64
bits.
float : réel
Un flottant est un nombre stocké en deux parties, une mantisse et un exposant. La taille de la mantisse
définit le nombre de chiffres significatifs, alors que la taille de l'exposant définit le plus grand nombre
acceptable par la machine. Les opérations sur les réels sont plus lents que sur les entiers. Pour une addition
par exemple, il faut d'abord décaler la mantisse pour égaliser les exposants puis faire l'addition. Les réels
sont toujours signés. On peut par contre utiliser le spécificateur long pour des réels avec une précision
accrue. On peut également utiliser le nom double au lieu de long float. Certains compilateurs acceptent
même des long double (quadruple précision).
Tailles et plages
Dans les calculs, les char sont automatiquement transformés en int. Quand un opérateur possède des
arguments de type différent, une transformation de type est effectuée automatiquement, suivant l'ordre :
char -> int -> long -> float -> double
signed -> unsigned
Attention la transformation n'est effectuée que le plus tard possible, si nécessaire. 5/2+3.5 donnera donc
5.5. De plus les opérations arithmétiques sont toujours effectuées sur des long ou double, pour une
précision maximale quels que soient les résultats intermédiaires (voir exemples au chapitre expressions
arithmétiques).
On peut forcer une transformation en utilisant le cast, qui est un opérateur unaire. La syntaxe est :
(type_résultat) valeur_à_transformer
exemple : {float x;int a=5; x=(float)a;}
Un cast transformant un réel en entier prendra la partie entière. Cette transformation doit être explicite, elle
est impossible implicitement. Pour obtenir l'entier le plus proche , utiliser
(int)(réel_positif+0.5).
Il faut bien noter que le cast n'est une opération de transformation que pour les types scalaires, pour tous les
autres types, le cast ne permet que de faire croire au compilateur que la variable est d'un autre type que ce
qu'il attendait, pour qu'il n'émette pas de message d'erreur (à utiliser avec grande prudence).
Enumérations
Tableaux
Tableaux unidimensionnels
Un tableau est un regroupement, dans une même variable, de plusieurs variables simples, toutes de même
type.
déclaration : [classe] type nom [nombre_d'éléments];
exemple : int tab[10];
Ceci réserve en mémoire un espace contigu pouvant contenir 10 entiers. Le premier est tab[0], jusqu'à
tab[9]. Attention, en utilisant tab[10] ou plus, aucune erreur ne sera signalée et vous utiliserez une partie de
mémoire qui a certainement été réservée pour autre chose. Il est possible de définir un tableau de n'importe
quel type de composantes (scalaires, pointeurs, structures et même tableaux). Il est également possible de
définir un type tableau par typedef :
typedef float vecteur[3];
vecteur x,y,z;
On peut aussi initialiser un tableau. Dans ce cas la dimension n'est pas nécessaire. Mais si elle est donnée,
et est supérieure au nombre de valeurs données, les suivantes seront initialisées à 0 :
vecteur vect0={0,0,0};
int chiffres[]={0,1,2,3,4,5,6,7,8,9};
int tableau[20]={1,2,3}; /* les 17 autres à 0 */
On peut également déclarer un tableau sans en donner sa dimension. Dans ce cas là le compilateur ne lui
réserve pas de place, elle aura du être réservée autre part (par exemple tableau externe ou argument formel
d'une fonction).
Exercice (moyenne) : Ecrire le programme qui lit une liste de Nb nombres, calcule et affiche la moyenne
puis l'écart entre chaque note et cette moyenne. Cliquez ici pour une solution.
#include <stdio.h>
void annule_tableau(int *t,int max)
{
for(;max>0;max--)*(t++)=0;
}
void affiche_tableau(int t[], int max)
{
int i;
for(i=0;i<max;i++) printf("%d : %d\n",i,t[i]);
}
void main(void)
{
int tableau[10];
annule_tableau(tableau,10);
affiche_tableau(tableau,10);
}
Exercice (rotation) : Ecrire un programme qui lit une liste de Nb nombres, la décale d'un cran vers le haut
(le premier doit se retrouver en dernier), l'affiche puis la décale vers le bas. On pourra décomposer le
programme en fonctions. Cliquez ici pour une solution.
Exercice (classer) : Classer automatiquement un tableau de Nb entiers puis l'afficher dans l'ordre croissant
puis décroissant. On pourra utiliser des fonctions de l'exercice précédent. On pourra créer un (ou plusieurs)
tableau temporaire (donc local). Si vous vous en sentez la force, prévoyez le cas de valeurs égales. Cliquez
ici pour une solution.
Chaînes de caractères
En C, comme dans les autres langages, certaines fonctionnalités ont été ajoutées aux tableaux dans le cas
des tableaux de caractères. En C, on représente les chaînes par un tableau de caractères, dont le dernier est
un caractère de code nul (\0). Une constante caractères est identifiée par ses délimiteurs, les guillemets "
(double quote).
exemples :
puts("salut");
char mess[]="bonjour"; /* évite de mettre ={'b','o',..,'r',\0} */
puts (mess);
mess est un tableau de 8 caractères (\0 compris). On peut au cours du programme modifier le contenu de
mess, à condition de ne pas dépasser 8 caractères (mais on peut en mettre moins, le \0 indiquant la fin de la
chaîne). Mais on peut également initialiser un pointeur avec une chaîne de caractères :
char *strptr="bonjour";
Le compilateur crée la chaîne en mémoire de code (constante) et une variable strptr contenant l'adresse de
la chaîne. Le programme pourra donc changer le contenu de strptr (et donc pointer sur une autre chaîne),
mais pas changer le contenu de la chaîne initialement créée.
Exercice (chaînes) : écrire un programme qui détermine le nombre et la position d'une sous-chaîne dans
une chaîne (exemple ON dans FONCTION : en position 1 et 6). Cliquez ici pour une solution.
● int strcmp(char *str1,char*str2) rend 0 si str1= =str2, <0 si str1<str2, >0 si str1>str2.
Idem strncmp
Des fonctions similaires, mais pour tous tableaux (sans s'arrêter au \0) sont déclarées dans mem.h. La
longueur est à donner en octets (on peut utiliser sizeof) :
● int memcmp(void *s1,void *s2,int longueur);
On possède également des fonctions de conversions entre scalaires et chaînes, déclarées dans stdlib.h
● int atoi(char *s) traduit la chaîne en entier (s'arrête au premier caractère impossible, 0 si erreur dès le
premier caractère)
● de même atol et atof
● de même : isalpha (A à Z et a à z, mais pas les accents), isalnum (isalpha||isdigit), isascii (0 à 127),
iscntrl (0 à 31), islower (minuscule), isupper, isspace (blanc, tab, return...), isxdigit (0 à 9,A à F,a à
f)...
● int toupper(int c) rend A à Z si c est a à z, rend c sinon. Egalement tolower
retour, l'adresse du bloc modifié (pas nécessairement la même qu'avant) ou le pointeur NULL en cas
d'erreur.
Ces fonctions sont définies dans stdlib.h ou alloc.h (suivant votre compilateur).
Une erreur fréquente consiste à "perdre" l'adresse du début de la zone allouée (par tab++ par exemple) et
donc il est alors impossible d'accéder au début de la zone, ni de la libérer.
Tableaux multidimensionnels
On peut déclarer par exemple int tab[2][3] : matrice de 2 lignes de 3 éléments. Un tableau peut être
initialisé : int t[2][3]={{1,2,3},{4,5,6}} mais cette écriture est équivalente à {1,2,3,4,5,6}, car il place dans
l'ordre t[0][0],t[0][1],t[0][2],t[1][0],t[1][1],t[1][2], c'est à dire ligne après ligne. Dans un tableau
multidimensionnel initialisé, seule la dimension la plus à gauche peut être omise (ici int t[][3]={...} était
possible).
t correspond à l'adresse &t[0][0], mais t[1] est aussi un tableau (une ligne), donc désigne l'adresse &t[1][0].
En fait, une matrice est un tableau de lignes. On peut expliciter cela par typedef :
typedef int ligne[3];
typedef ligne matrice[2];
En utilisant pointeurs et allocation dynamique, pour gérer un tableau de NBLIG lignes de NBCOL
éléments, , on peut :
● soit créer une matrice complète : allocation par t=malloc(NBLIG* NBCOL* sizeof(élément)), accès
à l'élément l,c par *(t+l*NBCOL+c).
● soit créer un tableau de NBLIG pointeurs de lignes, puis chaque ligne séparément. Ceci permet une
optimisation si les lignes n'ont pas toutes la même longueur (traitement de textes par exemple) mais
aussi de manipuler facilement les lignes (exemple : échanger deux lignes sans recopier leur contenu).
Cette méthode est plus rapide que la précédente, car les adresses de chaque début de ligne sont
immédiatement connues, sans calcul.
● soit utiliser des pointeurs de pointeurs (même principe que le cas précédent, mais remplacement du
tableau de pointeurs (dimension prévue à l'avance) par une allocation dynamique.
Exercice (matrices) : faire le calcul de multiplication d'une matrice (M lignes, L colonnes) par une matrice
(L,N) : résultat (M,N).
Cliquez ici pour une solution.
Exercice (déterminant) : écrire un programme qui calcule le déterminant d'une matrice carrée (N,N),
sachant qu'il vaut la somme (sur chaque ligne) de l'élément de la ligne en 1ère colonne par le déterminant
de la sous-matrice obtenue en enlevant la ligne et la 1ère colonne (en changeant le signe à chaque fois). Le
déterminant d'une matrice (1,1) est sont seul élément. On utilisera bien évidement la récursivité. Il existe
(heureusement) d'autres méthodes plus rapides. Cliquez ici pour une solution.
Structures et unions
Dans un tableau, tous les constituants doivent être du même type. Ce n'est pas le cas des structures, qui sont
des variables composées de plusieurs variables (ou CHAMPS) de types différents. Chaque champ n'est plus
Déclaration
déclaration : struct nom_type {déclaration des champs} liste_variables ;
exemple :
Utilisation
On accède à une composante par NOM_VARIABLE . NOM_CHAMP , par l'opérateur unaire "."
gets(jean.nom);
printf("initiales : %c %c\n",lui.nom[0],lui.prenom[0]);
printf("nom %s \n",groupe[10].nom);
scanf("%d",&moi.id.age);
Une composante d'enregistrement s'utilise comme une variable du même type (avec les mêmes possibilités
mais aussi les mêmes limitations). Depuis la norme ANSI, on peut utiliser l'affectation pour des structures
(recopie de tous les champs), ainsi que le passage des structures en arguments de fonction passés par
valeur. Sur les compilateurs non ANSI, il faut utiliser des pointeurs.
On utilise des pointeurs de structures comme des pointeurs sur n'importe quel autre type. L'opérateur ->
permet une simplification d'écriture (il signifie champ pointé) :
date *ptr;
ptr=(struct date *)malloc(sizeof(date));
*ptr.jour=14;ptr->mois=7;
Champs de bits
En ne définissant que des champs entiers (signés ou non), on peut définir la taille (en bits) de chaque
champ. Il suffit pour cela de préciser, derrière chaque champ, sa taille après un ":".
struct état{unsigned ReadOnly:1;int Crc:6;}
Les champs sont créés à partir des bits de poids faible. Le nom du champ est optionnel (dans le cas de
champs réservés, non utilisés par le programme). Les champs n'ont alors pas d'adresse (impossible d'utiliser
& sur un champ). On utilise ces structures comme les autres.
Unions
déclaration : union nom_type {déclaration des champs} liste_variables ;
Les différents champs commenceront tous à la même adresse (permet d'utiliser des variables pouvant avoir
des types différents au cours du temps, mais un seul à un instant donné). Les champs peuvent être de tout
type, y compris structures. On les utilise comme les structures, avec les opérateurs "." et "->".
Exercice (tel) A l'aide d'un tableau de personnes (nom, prénom, numéro dans la rue, rue, code postal, ville,
numéro de téléphone), faire un programme de recherche automatique de toutes les informations sur les
personnes répondant à une valeur d'une rubrique donnée (tous les PATRICK , tous ceux d'Obernai,
travaillant à l'IPST, etc...). On suppose que le tableau est déjà initialisé. Cliquez ici pour une solution.
Structures chaînées
Le principal problème des données stockées sous forme de tableaux est que celles-ci doivent être ordonnées
: le "suivant" doit toujours être stocké physiquement derrière. Imaginons gérer une association. Un tableau
correspond à une gestion dans un cahier : un adhérent par page. Supposons désirer stocker les adhérents par
ordre alphabétique. Si un nouvel adhérent se présente, il va falloir trouver où l'insérer, gommer toutes les
pages suivantes pour les réécrire une page plus loin, puis insérer le nouvel adhérent. Une solution un peu
plus simple serait de numéroter les pages, entrer les adhérents dans n'importe quel ordre et disposer d'un
index : un feuille où sont indiqués les noms, dans l'ordre, associés à leur "adresse" : le numéro de page.
Toute insertion ne nécessitera de décalages que dans l'index. Cette méthode permet l'utilisation de plusieurs
index (par exemple un second par date de naissance). La troisième solution est la liste chaînée : les pages
sont numérotées, sur chaque page est indiquée la page de l'adhérent suivant, sur le revers de couverture on
indique l'adresse du premier. L'utilisation d'une telle liste nécessite un véritable "jeu de piste", mais
l'insertion d'un nouvel adhérent se fera avec le minimum d'opérations.
Appliquons cela , de manière informatique, à une liste d'entiers, avec pour chaque valeur l'adresse (numéro
de mémoire) du suivant :
Si l'on veut insérer une valeur dans la liste, les modifications à apporter sont minimes :
#include <stdio.h>
#include <conio.h>
#include <ctype.h>
#include <alloc.h> /*ou stdlib.h*/
struct page {int val; struct page *suivant; };
struct page *premier;
Les modifications sont aisées, une fois que l'on a repéré l'endroit de la modification. Exemple : suppression
d'un élément :
void suppression(void)
{
struct page *actu,*prec;
actu=premier;
while (actu!=NULL)
{
printf("\nvaleur : %d - supprimer celui_ci (O/N) ? ",
actu->val);
if (toupper(getche())= ='O')
{
if(actu= =premier)premier=actu->suivant;
else prec->suivant=actu->suivant;
free(actu);
break;
}
else
{
prec=actu;
actu=prec->suivant;
}
}
}
Exercice (insertion) : ajouter au programme précédent une procédure d'insertion d'une valeur dans la liste.
La solution de l'exercice précédent contient également cette insertion
Ce type de données (structure pointant sur un même type) est utilisé dans d'autres cas. Par exemple, pour
représenter un arbre, il suffit pour chaque élément de connaître l'adresse de chaque fils :
Remarque : si le nombre de fils n'est pas constant, on a intérêt à stocker uniquement le fils aîné, ainsi que le
frère suivant(voir partie algorithmique et structures de données).
Les données stockées en mémoire sont perdues dès la sortie du programme. Les fichiers sur support
magnétique (bande, disquette, disque) sont par contre conservables, mais au prix d'un temps d'accès aux
données très supérieur. On peut distinguer les fichiers séquentiels (on accède au contenu dans l'ordre du
stockage) ou à accès direct (on peut directement accéder à n'importe quel endroit du fichier). Les fichiers
sont soit binaires (un float sera stocké comme il est codé en mémoire , d'où gain de place mais
incompatibilité entre logiciels), soit formaté ASCII (un float binaire sera transformé en décimal puis on
écrira le caractère correspondant à chaque chiffre). Les fichiers étant dépendants du matériel, ils ne sont
pas prévus dans la syntaxe du C mais par l'intermédiaire de fonctions spécifiques.
Fichiers bruts
C'est la méthode la plus efficace et rapide pour stocker et récupérer des données sur fichier (mais aussi la
moins pratique). On accède au fichier par lecture ou écriture de blocs (groupe d'octets de taille définie par
le programmeur). C'est au programmeur de préparer et gérer ses blocs. On choisira en général une taille
de bloc constante pour tout le fichier, et correspondant à la taille d'un enregistrement physique (secteur,
cluster...). On traite les fichiers par l'intermédiaire de fonctions, prototypées dans stdio.h (ouverture et
fermeture) et dans io.h (les autres), disponibles sur la plupart des compilateurs (DOS, UNIX) mais pas
standardisés.
La première opération à effectuer est d'ouvrir le fichier. Ceci consiste à définir le nom du fichier
(comment il s'appelle sous le système) et comment on veut l'utiliser. On appelle pour cela la fonction :
int open(char *nomfic, int mode);
nomfic pointe sur le nom du fichier (pouvant contenir un chemin d'accès). Mode permet de définir
comment on utilisera le fichier. On utilise pour cela des constantes définies dans fcntl.h :
O_RDONLY lecture seule, O_WRONLY écriture seule, O_RDWR lecture et écriture. On peut combiner
cet accès avec d'autres spécifications, par une opération OU (|) :
O_APPEND positionnement en fin de fichier (permet d'augmenter le fichier), O_CREAT crée le fichier
s'il n'existe pas, au lieu de donner une erreur, sans effet s'il existe (rajouter en 3ème argument S_IREAD |
S_IWRITE | S_IEXEC déclarés dans stat.h pour être compatible UNIX et créer un fichier lecture/
écriture/ exécution autorisée, seul S_IWRITE utile sur PC), O_TRUNC vide le fichier s'il existait,
O_EXCL renvoie une erreur si fichier existant (utilisé avec O_CREAT).
Deux modes spécifiques au PC sont disponibles : O_TEXT change tous les \n en paire CR/LF et
inversement, O_BINARY n'effectue aucune transformation.
La fonction rend un entier positif dont on se servira par la suite pour accéder au fichier (HANDLE), ou -1
en cas d'erreur. Dans ce cas, le type d'erreur est donné dans la variable errno, détaillée dans errno.h.
On peut ensuite, suivant le mode d'ouverture, soit lire soit écrire un bloc (l'opération est alors directement
effectuée sur disque) :
int write(int handle, void *bloc, unsigned taille);
On désigne le fichier destination par son handle (celui rendu par open), l'adresse du bloc à écrire et la
taille (en octets) de ce bloc. Le nombre d'octets écrits est retourné, -1 si erreur.
int read(int handle, void *bloc, unsigned taille);
lit dans le fichier désigné par son handle, et le met dans le bloc dont on donne l'adresse et la taille. La
fonction retourne le nombre d'octets lus (<=taille, <si fin du fichier en cours de lecture, 0 si on était déjà
sur la fin du fichier, -1 si erreur).
int eof(int handle)
dit si on se trouve (1) ou non (0) sur la fin du fichier.
Lorsque l'on ne se sert plus du fichier, il faut le fermer (obligatoire pour que le fichier soit utilisable par
le système d'exploitation, entre autre mise à jour de sa taille :
int close(int handle)
fermeture, rend 0 si ok, -1 si erreur.
Le fichier peut être utilisé séquentiellement (le "pointeur de fichier" est toujours placé derrière le bloc
que l'on vient de traiter, pour pouvoir traiter le suivant). Pour déplacer le pointeur de fichier en n'importe
que autre endroit, on appelle la fonction :
long lseek(int handle, long combien, int code);
déplace le pointeur de fichier de combien octets, à partir de : début du fichier si code=0, position actuelle
si 0, fin du fichier si 2. La fonction retourne la position atteinte (en nb d'octets), -1L si erreur.
long filelength(int handle);
rend la taille d'un fichier (sans déplacer le pointeur de fichier).
Exemple : copie de fichier (les noms de fichiers sont donnés en argument du programme)
#include <stdio.h>
#include <fcntl.h>
#include <io.h>
#include <sys\stat.h>
#define taillebloc 1024
int main(int argc,char *argv[])
{
int source, destination;
char buffer[taillebloc];
int nb_lus,nb_ecrits;
if (argc!=3) {puts("erreur arguments");return(1);}
if((source=open(argv[1],O_RDONLY|O_BINARY))<0)
{puts("erreur ouverture");return(2);}
if((destination=open(argv[2], O_WRONLY| O_CREAT| O_TRUNC| O_BINARY,
S_IREAD| S_IWRITE| S_IEXEC))<0)
{puts("erreur ouverture");return(2);}
do
{
nb_lus=read(source,(char *)buffer,taillebloc);
if (nb_lus>0) nb_ecrits= write(destination,(char*)buffer, nb_lus);
}
while ((nb_lus==taillebloc)&&(nb_ecrits>0));
close(source);
close(destination);
return(0);
}
Fichiers bufférisés
Les opérations d'entrée / sortie sur ces fichiers se font par l'intermédiaire d'un "buffer" (bloc en mémoire)
géré automatiquement. Ceci signifie qu'une instruction d'écriture n'impliquera pas une écriture physique
sur le disque mais dans le buffer, avec écriture sur disque uniquement quand le buffer est plein.
Les fichiers sont identifiés non par un entier mais par un pointeur sur une structure FILE (définie par un
typedef dans stdio.h). Les fonctions disponibles (prototypées dans stdio.h) sont :
FILE *fopen(char *nomfic, char *mode) : ouvre le fichier, suivant le mode : r (lecture seule), w
(écriture, si le fichier existe il est d'abord vidé), a (append : écriture à la suite du contenu actuel, création
si inexistant), r+ (lecture et écriture, le fichier doit exister), w+ (lecture et écriture mais effacement au
départ du fichier si existant), a+ (lecture et écriture, positionnement en fin de fichier si existant, création
sinon). Sur PC, on peut rajouter t ou b au mode pour des fichiers texte (gestion des CR/LF, option par
défaut) ou binaires, ou le définir par défaut en donnant à la variable _fmode la valeur O_TEXT ou
O_BINARY. fopen rend un identificateur (ID) qui nous servira pour accéder au fichier. En cas d'erreur,
le pointeur NULL est retourné, le type d'erreur est donné dans une variable errno, détaillée dans errno.h.
La fonction void perror(char *s mess) affichera le message correspondant à l'erreur, en général on lui
donne le nom du fichier.
int fread(void *bloc, int taille, int nb, FILE *id) : lit nb éléments dont on donne la taille unitaire en
octets, dans le fichier désigné par id, le résultat étant stocké à l'adresse bloc. La fonction rend le nombre
d'éléments lus (<nb si fin de fichier), 0 si erreur.
int fwrite(void *bloc, int taille, int nb, FILE *id) : écriture du bloc sur fichier, si le nombre rendu est
recherche rapide par dichotomie sans lire tout le fichier (en le supposant classé par ordre alphabétique),
création de fichiers index classés alphabétiquement sur les noms, département et ville pour accès rapide
par dichotomie, les autres se faisant par recherche séquentielle, avec possibilité d'ajout, suppression,
édition du fichier.
Exercice (fic_formaté) : modifier le programme de produit de matrices en lui permettant de donner sur
la ligne de commande trois noms de fichiers : les deux premiers contiendront la description des matrices
à multiplier, le dernier sera créé et contiendra le résultat. Les fichiers seront formatés, contiendront en
première ligne le nombre de lignes puis le nombre de colonnes, puis chaque ligne du fichier contiendra
une ligne de la matrice.
Directives du pré-compilateur
Attention, les directives se terminent par le retour à la ligne et pas un ";".
#include <nomfic> permet d'insérer à cet endroit un fichier, qui serra cherché dans le répertoire
correspondant à la bibliothèque du C (sur PC dans \TC\INCLUDE)
#include "nomfic" permet d'insérer à cet endroit un fichier, comme s'il était écrit ici, en le cherchant
dans le répertoire actuel (le votre)
#define nom valeur : remplace chaque occurrence du nom par la valeur. On ne peut prendre en compte
qu'une ligne (le retour à la ligne terminant la définition). En C on a l'habitude de noter les constantes
numériques en majuscules.
exemple :
#define PI 3.1415926
#define begin {
#define end }
#define macro(parametres) définition : permet la définition d'une macro, les paramètres seront
substitués lors de la réécriture. Il ne faut pas de blanc entre le nom de la macro et la "(" pour différencier
d'un define simple.
#define double(a) a*2
remplacera double (i+5) par i+5*2. Il faut donc utiliser des parenthèses, même si elles semblent inutiles :
#define double(a) (a)*2
autre exemple:
#define max(x,y) x>y?x:y
replacera max(a,max(b,c)) par a>b>c?b:c?a:b>c?b:c
#define max(x,y) (((x)>(y)?(x):(y)
replacera max(a,max(b,c)) par (((a)>( (((b)>(c))?(b):(c)) )) ? (a) : (((( b)>(c)) ? (b) : (c))) ce qui donne des
parenthèses superflues mais le résultat escompté.
Une macro ressemble à une fonction, mais sera d'exécution plus rapide : le texte est directement inséré à
l'endroit voulu, mais pas de gestion de pile.
#undef nom : annule le précédent #define nom ...
#if (expression) : les lignes qui suivent ne seront lues (et compilées que si l'expression est vraie
#endif : fin de portée du #if précédent
#ifndef entier
#define entier int
/* si une autre bibliothèque incluse plus haut l'a
déjà défini, on ne le redéfinit plus */
#endif
#ifdef biblio_graphique
initialise_ecran();
efface_ecran();
trace(dessin);
#else
puts("si on avait eu un écran graphique j'aurai fait un dessin");
#endif
/* biblio_graphique peut être défini dans un "header"
(fichier.h) inclus ou non plus haut */
est déclarée). par exemple, tapez printf puis CTRL F1, vous aurez une indication sur tous les formats
possibles ainsi que des exemples.
En choisissant l'option menus longs, les touches de raccourci sont indiquées à côté des fonctions des
menus.
Dernier conseil, sauvez votre source avant de l'exécuter, au cas ou votre programme planterait la machine
(automatisation possible sous options)
1. while_puiss
#include <stdio.h>
void main(void)
{
int puissance=1,max;
puts("nombre maximal désiré (ne pas dépasser 16000) ?");
scanf("%d",&max);
while (puissance<max) printf("%d\n",puissance*=2);
}
retour au sujet de cet exercice
2. while_err
Ce programme démontre les erreurs de calcul toujours effectuées sur des nombres réels. On additione successivement 0.01
(qui n'a pas de représentation finie en binaire) à un réel initialement nul. On compte le nombre de calculs jusqu'à obtenir une
erreur de 100%. Dans ce cas il faut 16246 calculs. On peut essayer d'autres pas et d'autres débuts.
retour au sujet de cet exercice
3. do_while
#include <stdio.h>
#include <stdlib.h> /* pour rand() */
#include <time.h> /* pour trouver l'heure pour srand */
void main(void)
{
int solution,reponse,nb_essais=0;
{time_t t;srand((unsigned) time(&t)); } /* initialiser le générateur
à partir du compteur de temps, pour qu'il soit plus aléatoire */
solution=rand()%11; /* reste sera toujours entre 0 et 10 */
do
{
nb_essais++;
puts("proposez votre nombre entre 0 et 10");
scanf("%d",&reponse);
}
while (reponse!=solution);
printf("trouvé en %d essais\n",nb_essais);
}
retour au sujet de cet exercice
4. for
#include <stdio.h>
void main(void)
{
int i,N;
float note,somme=0,moyenne;
puts("nombre de notes ? ");
scanf("%d",&N);
for(i=0;i<N;i++)
{
printf("entrez votre %dième note",i+1);
scanf("%f",¬e);
somme+=note;
}
moyenne=somme/N;
printf("moyenne calculée :%5.2f\n",moyenne);
}
retour au sujet de cet exercice
5. jeu
#include <stdio.h>
#include <stdlib.h> /* pour rand() */
#include <time.h> /* pour trouver l'heure pour srand */
void main(void)
{
int solution,reponse,nb_essais=0;
{ time_t t;srand((unsigned) time(&t)); } /* initialiser le générateur*/
solution=rand()%11; /* reste sera toujours entre 0 et 10 */
do
{
nb_essais++;
puts("proposez votre nombre entre 0 et 10");
scanf("%d",&reponse);
if (reponse>solution) puts("trop grand");
else if (reponse!=solution) puts("trop petit");
}
while (reponse!=solution);
printf("trouvé en %d essais\n",nb_essais);
if (nb_essais==1) puts("vous avez eu un peu de chance");
else if (nb_essais<4) puts("bravo");
else if (nb_essais>6) puts("ce score me semble bien minable");
}
retour au sujet de cet exercice
6. calcul
#include <stdio.h>
void main(void)
{
float val1,val2,res;
char op;
int fin=0;
do
{
puts("calcul à effectuer (par ex 5*2), ou 1=1 pour finir ? ");
scanf("%f%c%f",&val1,&op,&val2);
switch (op)
{
case '*':res=val1*val2;break;
case '/':res=val1/val2;break;
case '+':res=val1+val2;break;
case '-':res=val1-val2;break;
case '=':fin++; /* pas besoin de break, je suis déjà au } */
}
if (!fin) printf("%f%c%f=%f\n",val1,op,val2,res);
}
while (!fin);
}
retour au sujet de cet exercice
7. moyenne
#include <stdio.h>
#define max 100
8. rotation
#include <stdio.h>
typedef int composante;
void lecture(composante *t,int *nb)
{
int i;
puts("nombre de valeurs à entrer ? ");
scanf("%d",nb);
for(i=1;i<=*nb;i++)
{
printf("%dième valeur : ",i);
scanf("%d",t++);
}
}
void affiche(composante *t, int max)
{
int i;
9. classer
#include <stdio.h>
#define dim 100
typedef int composante;
void lecture(composante *t,int *nb)
{
int i;
puts("nombre de valeurs à entrer ? ");
scanf("%d",nb);
for(i=1;i<=*nb;i++)
{
printf("%dième valeur : ",i);
scanf("%d",t++);
}
}
void affiche(composante *t, int max)
{
int i;
for(i=0;i<max;i++) printf("%d ",*t++);
puts(" ");
}
int indice_min(composante t[],int indice_dep, int nb_indices)
/* cherche l'indice de la valeur du tableau :
* -soit égale à t[indice_dep], mais d'indice > à indice_dep;
* -soit la plus petite mais >t[indice_deb]
*/
{
int i,indice_resultat=-1;
for(i=indice_dep+1;i<nb_indices;i++) if (t[i]==t[indice_dep]) return(i);
/* si on est encore là c'est qu'il n'y en pas d'égal */
for(i=0;i<nb_indices;i++)
if ((t[i]>t[indice_dep]) && ((indice_resultat<0) || (t[i]<t[indice_resultat])))
indice_resultat=i;
return(indice_resultat);
}
void copier(composante *source, composante *dest, int nb)
/* copie le tableau source dans le tableau dest */
{
int i;
for(i=0;i<nb;i++) *(dest++)=*(source++);
}
void classe(composante tab[], int nb)
{
composante tempo[dim]; /* un malloc(sizeof(composante)*nb) aurait été
mieux mais on n'en a pas encore parlé en cours */
int i,ind_faits,indice;
/* 1er : recherche du plus petit, le 1er si ex aequo */
indice=0;
for(i=1;i<nb;i++)
if(tab[i]<tab[indice]) indice=i;
tempo[ind_faits=0]=tab[indice];
/* les suivants : recherche le + petit mais > au précédent */
for(ind_faits=1;ind_faits<nb;ind_faits++)
{
indice=indice_min(tab,indice,nb);
tempo[ind_faits]=tab[indice];
}
copier(tempo,tab,nb);
}
void main(void)
{
composante tableau[dim];
int nombre;
lecture(tableau,&nombre);
puts("tableau initial :");
affiche(tableau,nombre);
classe(tableau,nombre);
puts("tableau classé :");
affiche(tableau,nombre);
}
10. chaînes
#include <stdio.h>
#define taille 255
int debut_egal(char *ch,char *sch)
/* répond si sch est exactement le début de ch */
{
while (*sch && *ch)
{
if (*sch!=*ch) return(0); else {ch++;sch++;}
}
return(!(*sch));
}
void recherche(char *ch,char *sch)
{
int pos=0;
while (*ch)
{
if (debut_egal(ch,sch)) printf("trouvé en position %d\n",pos);
ch++;pos++;
}
}
void main(void)
{
char ch[taille],sch[taille];
puts("chaîne à tester ? ");
gets(ch);
puts("sous-chaîne à trouver ?");
gets(sch);
recherche(ch,sch);
}
retour au sujet de cet exercice
11. matrices
#include <stdio.h>
#define DIM 10
typedef float ligne[DIM];
typedef ligne matrice[DIM];
typedef float *pointeur;
lecture(b,&i,&n);
if(i!=l) puts("calcul impossible : dimensions incompatibles");
affiche(a,m,l);
puts("--- FOIS ---");
affiche(b,l,n);
puts("--- FAIT ---");
produit(a,b,c,m,l,n);
affiche(c,m,n);
}
retour au sujet de cet exercice
12. determinant
#include <stdio.h>
#include <stdlib.h>
#define DIM 10
typedef float ligne[DIM];
typedef ligne matrice[DIM];
typedef float *pointeur;
long nb_appels;
for(i=0;i<l;i++)
{
printf("ligne %d : ",i);
for(j=0;j<l;j++) printf("%3.1f ",t[i][j]);
printf("\n");
}
}
void copiesauflc(matrice source,matrice dest,int dim,int ligavirer)
{
int l,c,ld=0;
for (l=0;l<dim;l++) if (l!=ligavirer)
{
for (c=1;c<dim;c++) dest[ld][c-1]=source[l][c];
ld++;
}
}
float determinant(matrice m,int dim)
{
matrice sous_m;
int l,signe=1;
float det=0;
nb_appels++;
if (dim==1) return(m[0][0]);
for(l=0;l<dim;l++)
{
copiesauflc(m,sous_m,dim,l);
det+=signe*m[l][0]*determinant(sous_m,dim-1);
signe=-signe;
}
return(det);
}
void produit(matrice a,matrice b,matrice c,int dim)
/* calcul du produit */
{
int im,il,in;
zero(c,dim);
for(im=0;im<dim;im++)
for(in=0;in<dim;in++)
for(il=0;il<dim;il++)
c[im][in]+=a[im][il]*b[il][in];
}
void main(int argc,char *argv[])
{
int taille;
matrice mat;
/* lecture(mat,&taille); */
taille=atoi(argv[1]); /* test avec matrice unité, */
unit(mat,taille); /* au moins je connais le résultat*/
affiche(mat,taille);
printf("déterminant : %20.17f ",determinant(mat,taille));
printf(" en %ld appels\n",nb_appels);
}
retour au sujet de cet exercice
13. tel
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define DIM 100
enum champs {nom,prenom,num,rue,cp,ville,tel};
char *nomchamp[7]={"Nom", "Prénom", "Numéro", "Rue",
"Code Postal", "Ville", "Tel"};
typedef struct
{
char nom[15];
char prenom[20];
int num;
char rue[60];
long codepostal;
char ville[20];
char tel[15];
} fiche;
int choix(void)
{
char lig[40];
enum champs i,rep;
for (i=nom;i<=tel;i++) printf("%d:%s ",i,nomchamp[i]);
printf("\nou -1 pour quitter. Type de recherche désirée ? ");
gets(lig);
sscanf(lig,"%d",&rep);
return(rep);
}
}
while ((tab++)->nom[0]);
}
void main(void)
{
enum champs c;
char clef[40];
fiche tab[DIM];
lecture(tab);
do
{
if (((c=choix())<0)||(c>6)) break;
printf("quel(le) %s recherche-t'on ? ",nomchamp[c]);
gets(clef);
affiche(tabfonction[c](tab,clef));
}
while (c>=0);
}
retour au sujet de cet exercice
#include <stdio.h>
#include <conio.h>
#include <ctype.h>
#include <alloc.h>
void lecture(void)
{
struct page *precedent,*nouveau;
premier=(struct page *)malloc(sizeof(struct page));
puts("entrez votre premier entier");
scanf("%d",&premier->val);
precedent=premier;
while (encore())
{
nouveau=(struct page *)malloc(sizeof(struct page));
precedent->suivant=nouveau;
precedent=nouveau;
puts("\nentrez votre entier");
scanf("%d",&nouveau->val);
}
precedent->suivant=NULL;
}
void suppression(void)
{
struct page *actu,*prec;
actu=premier;
while (actu!=NULL)
{
printf("\nvaleur : %d - supprimer celui-ci (O/N) ? ",actu->val);
if (toupper(getche())=='O')
{
if(actu==premier)premier=actu->suivant;
else prec->suivant=actu->suivant;
free(actu);
break;
}
else
{
prec=actu;
actu=prec->suivant;
}
}
}
void ajouter(void)
{
struct page *nouveau,*prec;
printf("\najouter en premier (O/N) ? ");
if (toupper(getche())=='O')
{
nouveau=(struct page *)malloc(sizeof(struct page));
nouveau->suivant=premier;
premier=nouveau;
printf("\nnouvelle valeur ? ");
scanf("%d",&(nouveau->val));
}
else
{
prec=premier;
while(prec!=NULL)
{
printf("\nvaleur : %d - insérer après celui_ci (O/N) ? ", prec->val);
if (toupper(getche())=='O')
{
nouveau=(struct page *)malloc(sizeof(struct page));
nouveau->suivant=prec->suivant;
prec->suivant=nouveau;
printf("\nnouvelle valeur ? ");
scanf("%d",&(nouveau->val));
break;
}
else prec=prec->suivant;
}
}
}
void main(void)
{
lecture();
affiche(premier);
do
{
suppression();
affiche(premier);
}
while(encore());
do
{
ajouter();
affiche(premier);
}
while(encore());
}
retour au sujet de cet exercice
15. agenda
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <conio.h>
char tel[15];
} fiche;
#define taille sizeof(fiche)
typedef fiche *ptrfiche;
/* variables globales */
FILE *fic; /* fichier de données */
char *nomfic="agenda.dat";
int nb; /* nb de fiches dans le fichier */
void init(void)
/* ouvre le fichier, détermine le nb de fiches de fic */
{
if ((fic=fopen(nomfic,"a+b"))==NULL)
{
puts("ouverture impossible du fichier de données");
exit(1);
}
fseek(fic,0,2);
nb=(int)ftell(fic)/taille;
printf("%d fiches présentes dans l'agenda\n",nb);
}
void ajouter(void)
{
char lig[40];
fiche f;
printf("nom ? ");
gets(f.nom);
printf(" prénom ? ");
gets(f.prenom);
printf(" Numero ? ");
gets(lig);
sscanf(lig,"%d",&(f.num));
printf(" rue ? ");
gets(f.rue);
printf(" code postal ? ");
gets(lig);
sscanf(lig,"%ld",&(f.codepostal));
printf(" ville ? ");
gets(f.ville);
printf("n° de téléphone ? ");
gets(f.tel);
fseek(fic,0L,2);
if(fwrite(&f,taille,1,fic)!=1)
{
puts("impossible d'ajouter cette fiche au fichier ");
exit(0);
}
nb++;
}
int choix(void)
{
char lig[40];
enum champs i,rep;
for (i=nom;i<=tel;i++) printf("%d:%s ",i,nomchamp[i]);
printf("\nou -1 pour quitter. Type de recherche désirée ? ");
gets(lig);
sscanf(lig,"%d",&rep);
return(rep);
}
void recherche(void)
{
enum champs c;
char clef[40];
fiche f;
do
{
if (((c=choix())<0)||(c>6)) break;
printf("quel(le) %s recherche-t'on ? ",nomchamp[c]);
gets(clef);
affiche(tabfonction[c](&f,clef));
}
while (c>=0);
}
void main(void)
{
char rep;
init();
do
{
puts("Ajouter une fiche, Recherche d'une fiche, Quitter le prog ? ");
switch (rep=toupper(getch()))
{
case 'A':ajouter();break;
case 'R':recherche();break;
case 'Q':fclose(fic);puts("Au revoir");break;
default :puts("option non prévue");
}
}
while (rep!='Q');
}
retour au sujet de cet exercice
● cours ANSI-C de Fred Faber (Lycée Technique des Arts et Métiers, Luxembourg)
● le cours d'Yves Papegay (Inria) (sous Unix)
vous cherchez un compilateur (freeware, évidement) ? DJGPP pour DOS, ou GNU-Win32 pour Win
95/NT, ou GCC pour Unix (c'est le seul que j'ai essayé). De nouveaux arrivants : Pacific : sous DOS
(freeware), avec un environnement sympa (un peu comme Turbo C), et une doc en pdf (la version
Windows est payante). Désormais, Inprise (Borland) vous propose ses compilateurs gratuitement !
(pour certaines versions Turbo C, Turbo Pascal, Borland C, Delphi..., évidement pas les plus récentes).