Date de première publication : 2018/09/24
Dans ce premier TP de C, nous allons introduire les outils dont nous aurons besoin pour travailler en C cette année.
Débogage
Nous allons tester et corriger le programme suivant :
#include
void essai(int j)
{
int i;
while(i<10)
{
printf("%d ", i+j );
++i;
}
}
int main()
{
int j;
for(j=0; j< 10; ++j)
{
essai(j);
printf("\n");
}
return 0;
}
Mise en place (git)
Il est possible de récupérer le programme de différentes manières :
- Le taper (et ajouter les bons entêtes)
- le copier-coller
- Utiliser un gestionnaire de version comme git
Il faut tout d'abord configurer son compte pour utiliser un tel utilitaire. Tapez les commandes suivantes :
git config --global user.name "un_nom"
git config --global user.email "un_mail"
If faut ensuite récupérer le code, cela va créer le répertoire C1A avec le bon fichier dedans :
git clone https://gitlab.com/kiux/C1A.git
Vous pouvez trouver ce qui ne va pas sans les outils mais vous allez les utiliser quand même.
- Compilez le programme et exécutez-le.
- Il est tout à fait possible que le programme semble donner ce que l'on attend mais il faut tester ce que cela donne !
- Recompilez le programme en ajoutant l'option
-g
pour donner l'ordre au compilateur d'insérer des informations de déboguage dans l'exécutable.
En règle générale, c'est une excellente idée de compiler les programmes en développement avec cette option -g
. On enlève cette option lorsque le programme est compilé pour le distribuer à l'utilisateur final.
valgrind
Le premier outil que nous allons utiliser est valgrind. Cet outil simule l'exécution de notre programme et va analyser tout un tas d'informations. Nous allons avons avoir, entre autres, toutes les anomalies mémoire :
valgrind ./executable
Le résultat est le suivant :
==19248== Conditional jump or move depends on uninitialised value(s)
==19248== at 0x400574: essai (ddd01.c:17)
==19248== by 0x400593: main (ddd01.c:30)
==19248== Use --track-origins=yes to see where uninitialised values come from
==19248== ERROR SUMMARY: 800 errors from 7 contexts (suppressed: 0 from 0)
L'exécution suivante peut donner encore plus d'informations :
valgrind --track-origins=yes ./executable
Vous avez le numéro de ligne, c'est facile maintenant de corriger ! Mais ne le faites pas encore...
Les "sanitizers"
Les "sanitizers" sont des outils que l'on ajoute dès la compilation au code que l'on veut analyser. Cela permet, sur certains aspects, de retrouver les "vérifications mémoire" qui sont faites dans les langages de plus haut niveau (comme le Python ou le Java) : indices d'un tableau par exemple.
Les sanitizers sont malheureusement dépendants des compilateurs mais ces options marchent pour gcc et clang
gcc ddd03.c -g -fsanitize=address,undefined
À l'exécution, on obtient le résultat suivant :
ddd03.c:20:21: runtime error: index 5 out of bounds for type 'int [5]'
ddd03.c:20:3: runtime error: load of address 0xaaaaddb500b4 with insufficient space for an object of type 'int'
Complément :
Voici en lien un article bien intéressant sur ces outils.
ddd
Nous allons maintenant utiliser le débogeur gdb
au travers de sa surcouche graphique ddd
.
ddd nom_executable_a_tester &
L'interface graphique de débogueur se lance alors et ressemble à ça :
Posez un point d'arrêt sur la première instruction de la fonction essai()
en double cliquant (Un STOP apparait). Puis
exécutez le programme ligne par ligne jusqu'à trouver l'erreur.
Le curseur flèche verte indique la prochaine instruction à être exécutée.
Si vous avez de la "chance", vous verrez tout de suite ce qui se passe et vous pourrez alors corriger !
ddd n'aime pas certains encodages de fichier ou les accents dans les commentaires. Les symptômes : l'affichage graphique des variables qui ne marche pas ou bien un affichage incomplet du code.
Dans le support de cours en ligne C3, vous trouverez les commandes pour utiliser le débogueur en version texte.
Compilation séparée
Nous allons souvent mettre du code de programme dans des fichiers séparés pour faciliter la lecture et la maintenance du code.
Déclaration dans un fichier simple
En C, tout ce que l'on utilise doit être déclaré au préalable. C'est ce que nous allons vérifier dans cette section en reprenant les exemples du cours :
#include <stdio.h>
#include <stdlib.h>
int main() {
int x = 3, y = 5;
printf("%d", somme(x, y));
return EXIT_SUCCESS;
}
int somme(int a, int b) {
return a+b;
}
Pour que ce code compile, il faut que la fonction somme()
soit placée avec la fonction main()
ou bien que la fonction ait été déclarée au préalable
// int somme(int e, int f);
// int somme(int, int);
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("%f", a);
return EXIT_SUCCESS;
}
float a = 3;
Là encore, il est impératif de déclarer (donner le type) ou définir (donner le type et une valeur) la variable a
avant son utilisation
Si on ne donne pas de valeur par défaut à une variable globale, dont la portée par défaut est le fichier, on peut vérifier avec valgrind qu'il n'y a aucun souci : en C, les variables globlales sont initialisées avant le début du programme.
Partager des fonctions dans plusieurs fichiers
Nous écrivons souvent du code pour une tâche à résoudre que nous voulons ensuite réutiliser et
l'idée est d'éviter de faire un copier-coller. Dans le cas ci-dessous, on veut utiliser dans la fonction main()
une fonction f()
écrite pour un autre programme :
// main.c
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("%d", f());
return EXIT_SUCCESS;
}
// module.c
int f() {
return 3;
}
La ligne de compilation peut être :
gcc main.c module.c -o exe -Wall -Wextra -g
Faites les opérations suivantes :
- Vérifier que tout à l'air de marcher correctement
- Changer le prototype de
f()
pour que la fonction renvoie unfloat
et non plus unint
- Compiler et exécuter la nouvelle version de ce programme
- Ajouter la déclaration de la fonction
f()
dans le fichiermain.c
- Vérifier ce qui se passe
- Ce n'est pas optimal. Créer un fichier
module.h
et y déplacer la déclaration. Faire que les fichiers d'extension .c incluent le fichier d'entête. - Vérifier ce qui se passe
Vous avez créé votre premier module C. Félicitations !
Attention toutefois, on ne compile JAMAIS un fichier entête sauf si on sait ce que l'on fait.
Une erreur classique est de déclarer une fonction mais d'oublier de fournir son code/implémentation.
- Mettre en commentaire le code de
f()
et voir ce qui se passe
Cette erreur "undefined reference" est une erreur à l'édition des liens. C'est une erreur très fréquente !
Utiliser une variable (globale) dans plusieurs fichiers
Il est parfois nécessaire de partager une variable entre plusieurs fichiers (on parle alors de lien externe)
Partager une variable peut être une manière intéressante de passer de l'information d'un fichier à un autre. On va supposer qu'il n'y a pas de collision fonctionnelle de nom. Nous ne pouvons pas le faire n'importe comment. Voici comment faire :
- ajouter une variable globale réelle
a
dansmodule.c
et l'affecter dansf()
- l'utiliser dans
main()
et voir ce qui se passe - Déplacer la déclaration dans le fichier
module.h
et voir ce qui se passe
On obtient une erreur Multiple definition of 'a'
. Cette erreur
reste même si on utilise des gardiens.
- Placer le mot-clé
extern
devant la déclaration de la variable dansmodule.h
- Vérifier ce qui se passe.
On obtient une erreur undefined reference to 'a'
. Cette erreur
est une erreur de lien : la déclaration n'est pas suivie d'une création de variable. Il faut donc créer la variable
dans un fichier C
. Le plus logique est module.c
- Définir la variable
a
dans un fichier de code - Vérifier ce qui se passe.
Peut-on lire plusieurs fois le même fichier lors d'une même compilation ?
Un ficher peut être lu plusieurs fois par compilation mais il faut éviter de redéfinir les choses au sein d'une même unité de translation. Pour ce faire on utilise des gardiens même si c'est loin de régler tous les problèmes (il suffit de se rappeler de la manipulation pour une variable globale). Ces gardiens sont obligatoires alors ajoutez-les sytématiquement à votre module :
#ifndef IDENTIFIANT_MODULE_UNIQUE
#define IDENTIFIANT_MODULE_UNIQUE
// ...
#endif
Pour ceux qui connaissent, il faut éviter le
#pragma once
Compilation du module
Les lignes pour faire de la compilation séparée sont :
gcc -c main.c
gcc -c module.c
gcc module.o main.o -o exe
Constater que les fichiers sont bien créés s'il n'y a pas d'erreur de compilation.
Metter en place un Makefile
minimaliste
#pas de copier/coller a cause des tabulations
# l editeur peut aussi remplacer les tabulations par des espaces
main.o: main.c module.h
gcc -c main.c
module.o:module.c module.h
gcc -c module.c
exe: module.o main.o
gcc main.o module.o -o exe
Ce fichier peut être exécuté par la commande
make
Si vous obtenez une erreur "SEPARATEUR MANQUANT", les tabulations ont été remplacées par des espaces (vérifier avec vi)
On peut ensuite ajouter toutes sortes de règles et de variables, mais on bénéficie déjà des avantages de la compilation séparée. Un fichier non modifié n'est pas compilé s'il l'est déjà.
Bibliothèque de tests
De nombreux TPs vont se faire en utilisant des "tests unitaires". Le principe est de tester régulièrement le code que l'on est en train d'écrire en le confrontant à ce que l'on veut obtenir. Le développement est réputé "terminé" lorsque tous les tests réussissent. (Bien entendu, dans un mode idéal, où les tests couvrent tous les cas possibles d'exécution et que d'autres types de tests ont été réussis)
Vous devez récupérer les fichiers de code pour cet exercice :
main.c
contient le point d'entrée du programme de test ainsi que les tests eux-mêmes.mon_code.h
contient les déclarations des fonctions à écriremon_code.c
contient l'implémentation des fonctions à écrire (c'est à compléter)teZZt.h
contient les déclarations de tout ce qui est nécessaire pour la bibliothèque de teststeZZt.c
contient l'implémentation de la bibliothèque de tests.
git clone https://gitlab.com/kiux/C1B.git
Pour compiler le tout, vous pouvez faire une commande gcc
générale mais vous pouvez aussi simplement lancer la commande :
make
Cela exécute des instructions de compilation (encore appelées règles) contenues dans le fichier Makefile
lui-aussi fourni.
Premiers tests
On vous demande de coder la fonction pgcd()
qui calcule le plus grand diviseur commun entre deux nombres entiers. Si vous complétez le code de la fonction dans le fichier mon_code.c
, les tests seront réussis (OK ou passed) et non plus en echec (KO ou failed) !
main.c: 9 | EXPECT : 12 == pgcd(36, 24) main.c: 10 | EXPECT : 3 == pgcd(96, 81) main.c: 11 | EXPECT : 1 == pgcd(17, 1) main.c: 17 | EXPECT : 12 == pgcd(24, 36) main.c: 18 | EXPECT : 3 == pgcd(81, 96) main.c: 19 | EXPECT : 1 == pgcd( 1, 17) --- teZZT REPORT --- 6 test(s) failed 0 test(s) passed
Au passage, le pgcd de deux nombres a
et b
vautb
si le reste de la division entière de a
par b
est nul sinon c'est le pgcd de b
et de ce reste (définition récursive).
Transformer en majuscules
Vous pouvez ensuite décommenter les tests suivants. Il s'agit de tester la fonction majuscules()
que vous avez écrite pendant les semaines bloquées (vous pouvez réutiliser ce que vous avez écrit naturellement).
On vérifie ce qu'il se passe quand la chaîne en paramètre est vide, quand elle ne contient que des majuscules, que des minuscules et quand elle contient également des caractères qui ne sont pas des lettres.
TEST(maj4) {
char s[255] = "aAbB2eDdD!";
majuscules(s);
CHECK( 0 == strcmp( s, "AABB2EDDD!") );
}
Vous pouvez ajouter des tests complémentaires si vous le désirez.