tete du loic

 Loïc YON [KIUX]

  • Enseignant-chercheur
  • Référent Formation Continue
  • Responsable des contrats pros ingénieur
  • Référent entrepreneuriat
  • Responsable de la filière F2 ingénieur
  • Secouriste Sauveteur du Travail
mail
loic.yon@isima.fr
phone
(+33 / 0) 4 73 40 50 42
location_on
ISIMA
  • twitter
  • linkedin
  • viadeo

[C] TP 1 : Outils

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 :

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.

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 :

fenetre de ddd

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 :

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.

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 :

On obtient une erreur Multiple definition of 'a'. Cette erreur reste même si on utilise des gardiens.

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

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 :

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.