1 Introduction 2 Printf

Transcription

1 Introduction 2 Printf
Stage de programmation
Mise au point d’un programme
Olivier Aumage
LIP, bureau 302
email : [email protected]
Septembre 2001
1
Introduction
Le TD d’aujourd’hui a pour but de faire un rapide rappel sur les techniques usuelles de mise au point
de programmes C. L’objectif principal est de limiter autant que possible les séances de corrections de programmes dans les périodes de stress intensif.
La recherche d’une erreur au sein d’un programme est avant tout une recherche d’information, sur le
contexte, les symptômes, les causes possibles de l’erreur, et finalement sur sa localisation et sur la manière
de la corriger. La méthode traditionnelle consistant à utiliser la commande printf (et la seule disponible
dans certains contextes) en divers endroits du programme est l’expression de cette recherche d’information. Les outils tels que GDB vont dans le même sens, en essayant précisémment de faciliter l’obtention
d’informations sur les programmes.
2
Printf
La méthode à base de printf est utilisée dans les cas où on ne peut (ou ne veut) pas utiliser de debugger.
L’exemple boom.c de la figure 1 en montre le danger si elle est mal utilisée. Quel est l’affichage généré par
un tel programme ?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
#include <stdio.h>
int main()
{
int *p;
printf("1");
p = NULL;
printf("2");
*p = 1;
printf("3");
return 0;
}
F IG . 1 – boom.c
Ce genre d’erreur peut faire perdre un temps considérable en suggérant une localisation erronée du
problème. Il convient donc d’éviter autant que possible d’introduire des bugs dans le code sensé aider à les
1
supprimer ! Ici, la solution est d’ajouter un retour à la ligne \n à la fin de chaque chaîne affichée par printf,
ou mieux, d’utiliser fprintf(stderr, "..."). Dans ce dernier cas, les messages sont envoyés sur la
sortie standard d’erreur qui n’est pas tamponnée. Si on tient absolument à utiliser printf, sans pour autant
être obligé d’ajouter un retour à la ligne après chaque message, on peut également effectuer un appel à
fflush(stdout) après chaque printf pour forcer les informations à être affichées.
3
GDB
Lorsqu’ils sont utilisables, les debuggers tels que GDB du projet GNU remplacent avantageusement la
technique précédente dans la mesure où ils permettent non seulement d’analyser l’état du programme à un
instant donné ou d’en observer l’évolution, mais également de le modifier en cours d’exécution.
3.1
Principes de base
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdlib.h>
#define N 256
int *tab = NULL;
void f(int n)
{
char i = 0;
for (i = 0; i <= n; i++);
{
tab[i] = 1;
}
}
int main()
{
printf("Debut\n");
tab = calloc(N, sizeof(int));
f(N);
free(tab);
printf("Fin\n");
return 0;
}
F IG . 2 – boucle.c
Compilation Au cours de ces quelques paragraphes, nous utiliserons le programme boucle.c (fig. 2). Il
s’agit bien évidemment d’un cas d’école. Pour le compiler, utilisez la commande suivante :
> gcc -o boucle boucle.c
Pour le lancer, la commande est :
> ./boucle
Pour l’arrêter, utilisez CTRL-C.
2
L’option -Wall de GCC active un certain nombre de warnings à la compilation permettant d’identifier
les erreurs et fautes de frappe les plus courantes. L’utiliser systématiquement est donc une bonne habitude.
Le warning affiché par la commande ci-dessous donne par exemple une première indication de la présence
de problèmes au niveau du programme boucle.c.
> gcc -Wall -o boucle boucle.c
Note : contrairement à ce qu’on peut penser, -Wall n’active pas tous les warnings. On peut en rajouter
au gré des besoins (pour détecter des problèmes tels que des comparaisons de type signé/non signé ou
des problèmes affectant la portabilité) en se référant à la documentation de GCC (info gcc, Invoking
GCC/Warning options).
Pour exploiter au mieux les capacités de GDB sans avoir à apprécier la convivialité de l’assembleur,
il est nécessaire de compiler les programmes avec des informations de débogage. Avec le compilateur GCC,
l’option permettant d’inclure ces informations est l’option -g :
> gcc -Wall -g -o boucle boucle.c
Ces informations permettent notamment à GDB de faire le lien entre les instructions assembleur du programme compilé et les instructions C (ou l’un des autres langages supportés par GDB). Elles permettent
également de mettre en relation les adresses mémoires utilisées par le processus analysé et les structures
de données, variables et fonctions définies dans le code source du programme. Pour garantir une précision
maximale de ces informations, il est préférable d’éviter l’usage d’options d’optimisations (i.e. options de
type -Ox avec GCC) pour compiler un programme en phase de mise au point, ces options opérant des
modifications sur l’ordonnancement du code ainsi que des simplifications de ce dernier.
Chargement d’un processus dans GDB Une session de débogage avec GDB est démarrée, dans le cas
le plus simple, de la manière suivante :
> gdb boucle
Cette commande lance l’environnement de GDB et charge le fichier exécutable boucle. Si le fichier exécutable spécifié en paramètre dispose d’informations de débogage et si ses fichiers source sont disponibles, ces
derniers sont également chargés. On peut également lancer uniquement GDB puis charger le programme à
analyser avec la commande file
L’environnement GDB est contrôlé par l’intermédiaire de commandes entrées au clavier. Les plus importantes à notre niveau sont help et quit (ou CTRL-D). Vous pouvez également essayer la commande
list qui, en principe affiche le code du programme à partir de la fonction main par série de 10 lignes. Si
le programme n’a pas été compilé avec l’option -g, cette commande n’est évidemment pas disponible.
Exécution Pour lancer l’exécution du programme dans l’environnement, la commande la plus simple
est run (ou r). Cette commande lance l’exécution du programme sans traitement particulier à partir du
début jusqu‘à la fin (si elle existe). Si un signal est reçu par le processus en cours d’exécution sans être traité
par celui-ci, GDB stoppe le processus. Celui-ci est alors figé dans son état à l’instant du stop. Il convient
d’insister sur le fait que l’exécution est stoppée et non terminée. La commande cont permet de reprendre
l’exécution d’un programme stoppé. La notion de signal UNIX proprement-dite sort du cadre de ce TD. La
liste ci-dessous indique les signaux les plus courants dans notre contexte de mise au point de programmes :
SIGINT (program interrupted) Résultat d’un CTRL-C ;
SIGSEGV (segment violation) Accès mémoire à une adresse invalide ;
SIGBUS (bus error) Accès mémoire non aligné ;
SIGABRT (abort) Signal généré par la fonction abort, présentée plus loin ;
SIGFPE (floating point exception) Exception au niveau du calcul flottant (ex : division par 0).
Essayez la session suivante :
3
– Chargez boucle dans GDB.
– Exécutez le programme avec la commande run.
– Appuyez sur CTRL-C, ce qui doit avoir pour effet de stopper le programme. GDB affiche alors une
information sur le signal qui a provoqué l’arrêt (ici SIGINT), sur la position du compteur ordinal du
programme (adresse en hexadécimal) et si possible sur la fonction en cours d’exécution et la valeur de
ses arguments. Si les informations de débogage sont disponibles, GDB indique le fichier et le numéro
de la ligne du programme en cours d’exécution, et celle-ci est affichée.
– Exécutez la commande list. Elle affiche une partie du fichier source contenant la ligne courante, en
principe un groupe d’une dizaine de lignes centrées sur la ligne courante. Ensuite, un simple appui
sur la touche Entrée permet d’obtenir la suite du code par groupe de 10 lignes (la touche Entrée
exécute la dernière commande utilisée si la ligne est vide).
– Essayez la commande cont pour relancer le programme, puis à nouveau CTRL-C pour le stopper.
Vérifiez que le programme n’a pas progressé.
– Aidez le programme à franchir la zone difficile à l’aide de la commande jump (ou j) suivie du numéro
de ligne approprié (ligne 11 si vous avez scrupuleusement copié le programme boucle.c).
– Le programme doit se terminer normalement.
Note : il est fortement conseillé de limiter l’argument de la commande jump aux lignes de la fonction
courante !
GDB permet également d’exécuter le programme pas-à-pas. Il dispose pour cela des commandes
next (n) & step (s). Ces deux commandes exécutent une seule ligne de programme puis stoppent à nouveau le processus. Cependant, si la ligne à exécuter contient un appel de fonction, step stoppe le processus
au niveau de la première instruction de la fonction appelée alors que next stoppe le processus au niveau
de la ligne suivant l’appel de fonction.
Quittez GDB, copiez boucle.c vers boucle2.c (cp boucle.c boucle2.c), puis supprimez le
point-virgule en fin de ligne de 10 (après le for) dans boucle2.c. Compilez boucle2 puis chargez le
dans GDB. Lancez l’exécution, interrompez le processus puis testez la commande next ou la commande
step pour vérifier le comportement du programme. Pourquoi ces mêmes commandes semblent-elles ne
pas fonctionner avec le programme boucle.c ?
Points d’arrêt Lorsqu’on recherche une erreur, il arrive souvent qu’on ait déjà une idée de sa localisation
potentielle. GDB permet donc de spécifier des points d’arrêt permettant de stopper automatiquement l’exécution du programme en un point précis du code. La commande break (b) suivie d’un nom de fonction
ou d’un numéro de ligne (éventuellement associé à un fichier) insère un point d’arrêt à l’endroit spécifié.
La commande clear supprime le point d’arrêt de la localisation spécifiée.
Chargez boucle2 dans GDB, spécifiez un point d’arrêt sur la fonction main puis lancez l’exécution.
Après un court instant, GDB stoppe le programme au niveau de l’entrée de main. Profitez de l’occasion
pour vérifier la différence entre step et next.
Pile et cadres Tous les processus en cours d’exécution possèdent une structure de donnée appelée pile
d’exécution. Cette pile est utilisée pour stocker les données locales des fonctions du programme. Chaque
appel de fonction génère l’ajout de données sur la pile, et chaque retour de fonction libère ces mêmes
données. La commande backtrace (bt) indique la liste des fonctions qui ont empilé des informations sur
la pile à l’instant observé, c’est-à-dire la liste des appels de fonction imbriqués à cet instant.
Compilez le programme fact.c (fig. 3, page 5) puis chargez fact dans GDB. Spécifiez un point d’arrêt sur la ligne 13 (x=1) et lancez l’exécution. Lorsque le processus est stoppé, exécutez la commande
backtrace. La liste affichée indique tout d’abord les appels récursifs à f et termine par main. Les fonctions sont donc listées depuis l’appel le plus imbriqué (regardez la valeur indiquée pour le paramètre n de
f pour chaque cadre) vers l’appel le moins imbriqué (donc dans l’ordre inverse de l’ordre chronologique,
d’où le nom backtrace).
Chaque ligne constitue ce que l’on appelle un cadre de pile, ou frame en anglais. Le cadre désigne la zone
de la pile associée à l’appel de fonction auquel il correspond. Les noms de fichiers et numéros de lignes
4
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
int f(int n)
{
int x = 0;
if (n > 0)
{
x = n * f(n - 1);
}
else
{
x = 1;
}
return x;
}
int main()
{
int a = 10;
int b = 0;
b = f(a);
printf("%d! = %d\n", a, b);
return 0;
}
F IG . 3 – fact.c
indiquent la position du compteur de programme pour chaque cadre.
Par défaut, le cadre courant (ciblé par défaut par les commandes GDB) est le cadre numéroté 0. On
peut passer d’un cadre à l’autre à l’aide de la commande frame suivie du numéro du cadre à sélectionner.
Beaucoup de commandes de GDB font implicitement référence au cadre courant (essayez list dans le
cadre 0 puis dans le cadre 11 par exemple). Le cadre courant correspond de plus au contexte dans lequel
sont interprétés les noms d’objets locaux : par exemple, le nom n (argument de f) désignera des cases
mémoire (et des valeurs) différentes dans les cadres 0 à 10. Dans le cadre 11 (celui de main) il sera considéré
comme indéfini.
Variables et expressions
Affichage Il possible d’afficher les valeurs des variables du contexte courant. Les commandes print
et printf de GDB permettent d’afficher le contenu de ces variables ou de calculer des expressions à partir
de celles-ci.
Placez un point d’arrêt sur f puis relancez l’exécution de fact avec run. Affichez le contenu de n avec
print n.
La commande disp est similaire à print mais provoque l’affichage du résultat de l’expression spécifiée
à chaque interruption du programme. Exécutez, par exemple, disp (char)n+65 puis utilisez répétitivement cont.
5
Modification On peut également modifier le contenu des variables en cours d’exécution (par exemple
pour étudier l’influence de telle ou telle valeur d’une variable sur l’apparition d’un symptôme d’erreur).
La commande à utiliser est set variable VAR=EXP où VAR est le nom de la variable à modifier et EXP
l’expression dont le résultat est à lui affecter. Si le nom de la variable à modifier n’entre pas en conflit avec
les variables internes de GDB, on peut omettre le mot-clé variable.
Rechargez fact, mettez un point d’arrêt sur f et lancez l’exécution. Lorsque le GDB reprend la main
sur le point d’arrêt, modifiez la valeur de n, supprimez le point d’arrêt et utilisez la commande cont pour
laisser le programme terminer son exécution. Comparez le résultat obtenu avec 10 ! pour vérifier que le
changement a bien été pris en compte.
Watchpoints Il est également possible d’utiliser des points d’arrêts activés par le changement de valeur d’expressions ou de zones de données. Ces points d’arrêts sont appelés points d’observation ou watchpoints et sont mis en place à l’aide de la commande watch. Les objets référencés par noms doivent impérativement être dans le contexte.
Essayons cette commande pour observer la valeur de b dans main par exemple. Pour cela il faut d’abord
se placer dans le contexte de main : rechargez fact, mettez un point d’arrêt classique sur main et lancez l’exécution pour arriver au contexte de main. Quand GDB reprend le contrôle, entrez la commande
watch b, puis exécutez répétitivement cont. A chaque arrêt, GDB affiche l’ancienne valeur et la nouvelle
valeur de b. Lorsque l’exécution sort de main, le contexte de main est détruit et le point d’observation est
supprimé.
Note : la commande watch détecte uniquement des accès en écriture à des variables ou zones de mémoire. GDB propose également les commandes rwatch et awatch, non détaillées ici, pour détecter les
accès en lecture (uniquement ou non). La disponibilité et la fiabilité de ces deux commandes reposent entièrement sur les capacités du processeur.
3.2
Exploitation de fichiers core
Lorsqu’un processus UNIX est interrompu pour cause d’erreur interne, le système génère un fichier
représentant une image de l’état du processus à l’instant où l’erreur s’est produite. Ce fichier est habituellement nommé core et est écrit dans le répertoire courant du processus. Les utilisateurs ont la possibilité
d’interdire la génération de ces fichiers ou d’en limiter la taille. Utilisez la commande ulimit -c avec un
shell de type sh ou limit coredumpsize avec un shell csh pour vérifier la taille maximale autorisée
pour vos fichiers core. Si nécessaire, changez la valeur en unlimited. Note : pour que le changement soit
permanent, il faut mettre cette commande dans votre fichier .cshrc ou .bashrc (ou assimilé).
Dans une phase de développement, il est très important d’autoriser la génération de fichiers core.
En effet, certains types de bug génèrent des symptômes non déterministes : certaines exécutions du programme se déroulent normalement tandis que d’autres exécutions échouent, sans changement apparent
des conditions initiales du programme. Or la fréquence des échecs peut être extrêmement faible, et il est
donc important de pouvoir exploiter ces échecs dès qu’ils surviennent. Dans cette optique, l’image de l’état
du processus au moment du crash est une source d’informations considérable.
GDB permet d’exploiter le contenu d’un fichier core et d’explorer l’état du processus à l’instant de la
faute avec les outils habituels. Compilez boom.c (c.f. fig. 1, page 1) et exécutez boom dans un simple shell
(i.e. hors de GDB). Si tout se passe bien, boom doit terminer sur un signal SIGSEGV et générer un fichier
core. Chargez boom et le core dans GDB avec :
> gdb boom core
Ensuite, déterminez la cause de l’erreur avec les commandes de GDB vues précédemment.
3.3
Prise de contrôle en cours d’exécution
Parfois, une erreur de programmation ne se traduit pas par un arrêt brutal du programme mais par un
blocage apparent, généralement causé par une boucle infinie. Les programmes boucle et boucle2.c en
6
sont des exemples. Dans une telle situation, les deux questions qui se posent sont souvent : “À quel endroit
le programme boucle-t-il ?” et “Pourquoi le programme boucle-t-il ?” GDB dispose de fonctionnalités permettant de répondre plus facilement à ces questions.Vous pouvez utiliser un seul ou deux terminaux pour
cette séquence.
Avec un seul terminal, lancez le programme boucle en tâche de fond ./boucle &. Exemple :
> ./boucle &
[1] 584
Dans ce cas, le shell indique une information comme ci-dessus. Le 1 entre crochets est le numéro de job, il
ne nous concerne pas ici. Le nombre 584 est le numéro attribué au processus boucle qui vient d’être lancé.
On l’appelle également process id ou PID.
Avec deux terminaux, lancez boucle dans un des terminaux, et déterminez son PID avec la commande
ps dans l’autre terminal (utilisez man ps pour les détails d’utilisation de ps).
Dans le terminal unique, ou dans le terminal ne contenant pas boucle, suivant votre situation, lancez
GDB avec comme paramètre le fichier exécutable boucle et le PID du processus déterminé ci-dessus. Ex :
gdb boucle 584. L’environnement GDB est lancé et le processus de numéro PID est stoppé (en supposant
que le PID spécifié soit valide et que vous soyez propriétaire du processus). Après avoir arrêté le processus,
GDB indique la position du compteur de programme à l’instant de l’arrêt et donne accès aux fonctionnalités
habituelles (observation, altération, exécution). Utilisez par exemple à nouveau la commande jump pour
aider le programme à terminer.
3.4
Auto-contrôle
Pour faciliter le travail de mise-au-point de programme avec GDB, la bibliothèque standard du langage C intègre un certain nombre de fonctionnalités permettant par exemple de vérifier des assertions et de
générer un fichier core en cas de problème de cohérence interne.
La base de ces mécanismes est la fonction abort. Cette fonction envoie simplement un signal SIGABRT
à son propre processus, ce qui a généralement pour effet de l’arrêter brutalement et de faire générer un
fichier core par le système (ou de redonner le contrôle à GDB si le programme est exécuté au sein de GDB).
Comme dans toutes les situations vues précédemment, l’intérêt d’un tel comportement est de permettre de
mener des investigations sur les causes ayant conduit à une incohérence au niveau du programme.
Rajoutez un test dans la fonction f de fact.c appelant abort (utilisez man abort pour connaître
l’entête contenant la déclaration de abort) si l’argument de la fonction est négatif. Puis vérifier le bon
fonctionnement de ce test en exécutant fact dans GDB ou hors de GDB.
Lorsque la phase de mise au point est terminée, on peut la remplacer par la fonction exit qui termine
le programme un peu moins violemment, mais ne génère pas de fichier core.
Les différentes variantes de la macrocommande assert peuvent être disséminées dans le code source
pour vérifier des assertions et générer un appel à abort en cas de problème. Elles sont désactivées à la
compilation par la définition du symbole NDEBUG. Si ces macrocommandes peuvent largement aider dans
une phase de débogage, il est très important de ne pas en abuser. Ceci pour ne pas nuire à la lisibilité du
code qui reste le facteur fondamental en ce qui concerne la facilité d’exploitation d’un programme.
3.5
Outils annexes
Bibliothèques spécialisées Pour réduire encore le temps de débogage, il existe des bibliothèques proposant des fonctionnalités d’analyse et de détection de problèmes plus spécifiques et pouvant travailler de
concert avec GDB. Pour l’essentiel, ces bibliothèques sont ciblées sur le contrôle des allocations mémoire,
l’un des points les plus sensibles de la programmation en C. Cette courte section présente l’une de ces bibliothèques appelée Electric Fence (disponible sous Linux et Solaris notamment). Il en existe beaucoup
d’autres plus ou moins similaires, et même la GLIBC intègre des mécanismes de débogage des allocations
mémoire.
7
Afin de disposer d’un exemple de travail, copiez boucle2.c vers boucle3.c (cp boucle2.c
boucle3.c), puis remplacez ‘char i’ par ‘int i’ dans f (dans boucle3.c).
E.F. vise principalement à détecter les problèmes suivant :
– les débordements, par exemple l’écriture de 32 octets alors que seulement 16 ont été alloués ;
– les libérations de zones mémoires non allouées (ou déjà libérées, ce qui revient au même).
Ces deux types d’erreurs sont relativement courants (le premier type intervient justement dans
boucle3.c). En l’absence d’outils appropriés, le premier type d’erreur écrase les structures internes de
l’allocateur et le second type place ce même allocateur dans un état incohérent. Dans les deux cas, la manifestation du problème (souvent un crash) peut survenir beaucoup plus tard que l’erreur elle-meme. E.F.
substitue donc à l’allocateur de mémoire habituel un allocateur plus robuste capable de résister (dans une
certaine mesure) aux blagues du programmeur et de les détecter (également dans une certaine mesure).
Pour tester E.F., compilez boucle3.c avec la commande suivante :
> gcc -Wall -g -o boucle3 boucle3.c -lefence
L’option -lefence a pour effet de linker la bibliothèque libefence.a avec les autres composants de
l’exécutable final. Note : sur le compte “Solaris” de l’ÉNS, cette bibliothèque est en principe fournie par
Soft-Élèves.
Exécutez boucle3. Si tout se passe bien, le programme doit terminer sur un signal SIGSEGV (ou similaire). Exécutez le programme dans GDB et constatez que le signal est émis exactement au niveau de
l’instruction fautive.
Par curiosité, vous pouvez recompiler boucle3.c sans E.F. et constater que le programme termine
correctement et que l’erreur passe inaperçue. Note : ce dernier point est toutefois un peu aléatoire. Il est
également possible que le programme échoue au niveau du free, suivant les détails d’implémentation
de l’allocateur. Cependant, même dans ce cas, l’information obtenue sur l’erreur potentielle est beaucoup
moins précise que celle obtenue avec E.F.
Expérience Pour terminer, rien ne remplace un peu d’expérience et de rigueur. Par exemple, parmi les
trois erreurs majeures décorant le programme boucle.c (et que vous aurez — je l’espère — identifiées),
l’une d’elle est signalée par l’option -Wall de GCC et une autre est indiquée par le système d’indentation
automatique d’Emacs en mode C (et vraisemblablement la plupart des éditeurs de texte disposant de ce
type de fonctionnalité). La troisième est indiquée immédiatement par E.F.
En revanche, sans une utilisation appropriée des outils mis à disposition des développeurs, ces erreurs
peuvent facilement passer inaperçues (et faire perdre inutilement du temps), même dans un code de seulement quelques centaines de lignes.
À ces remarques, on peut ajouter ces quelques conseils, à compléter par votre propre expérience :
– ne jamais appeler un programme test ;
– exécuter un programme en le préfixant avec ./ ;
– vérifier que le programme édité est le même que le programme exécuté ;
– initialiser chaque variable dès sa déclaration ;
– utiliser un code aéré, convenablement indenté et de qualité ;
D’une manière générale, une bonne approche est de préférer à la concision la simplicité du code et des
expressions :
– écrire uniquement des fonctions courtes ;
– utiliser une seule instruction par ligne ;
– n’utiliser des abréviations (ex ? :) que si elles simplifient la lecture du programme ;
– limiter la portée des variable au strict nécessaire ;
Il est d’ailleur intéressant de noter que ces dernières recommandations facilitent non seulement la lecture du
code par le programmeur, mais également l’optimisation du code par le compilateur. Par exemple, l’écriture
de fonctions courtes augmente considérablement les possibilités d’expansions inline et la limitation de la
portée des variables améliore très sensiblement la qualité des analyses de dépendances... C.f. cours de
compilation !
8
4
Question subsidiaire
Pour ceux d’entre vous qui auront terminé ce TP prématurément, cette question vous propose d’exercer
vos (nouveaux ?) talents en implémentant un petit programme chaine.c de gestion de listes simplement
chaînées d’entiers : chaque maillon possède uniquement un entier et un pointeur vers le maillon suivant.
Les insertions de maillons doivent préserver l’ordre croissant des entiers. Après avoir défini vos structures
de données, écrivez et testez les fonctions suivantes :
nil
insert
extract
disp
dup
...
Permet d’obtenir une liste vide.
Ajoute un entier dans la liste.
Retourne le premier entier de la liste passée en paramètre après avoir supprimé le maillon correspondant de la liste ; si la liste est vide, vous pouvez faire un appel à abort par exemple.
Affiche le contenu de la liste.
Duplique la liste.
Laissez libre cours à vos idées !
Ensuite, suivant la qualité de votre programme, utilisez les outils que nous avons vu au cours de ce TD pour
le corriger ou utilisez votre programme pour explorer ces outils plus en détail (l’affichage et le parcours de
structures de données complexes dans GDB par exemple)...
-=-=-=-
9

Documents pareils