Blog ENI : Toute la veille numérique !
🐠 -25€ dès 75€ 
+ 7 jours d'accès à la Bibliothèque Numérique ENI. Cliquez ici
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici

L'art de programmer en C

Limiter les risques d’erreurs de segmentation

Problème

Pour stabiliser votre programme, vous souhaitez connaître quelques règles de programmation qui permettent d’éviter des erreurs de segmentation (segmentation fault).

Solution

Il n’existe pas de solution à proprement dire, mais seulement des règles de programmation qui, lorsque nous les suivons, réduisent les risques d’erreurs de segmentation. Parmi ces règles, celles qui consistent à vérifier l’utilisation des fonctions et à maîtriser les données utilisées sont probablement les plus importantes. 

Discussion

Qu’est-ce qu’une erreur de segmentation ? La mémoire d’un ordinateur est segmentée et lors de l’allocation de mémoire à un programme, le système d’exploitation lui réserve un espace sur un certain nombre de segments. Si ce programme tente d’accéder à de la mémoire qui ne lui a pas été réservée, sur d’autres segments, il s’agit d’une faute de segmentation que le système détecte. Il réagit en envoyant un signal qui, par défaut, provoque l’arrêt du programme (voyez le chapitre "Signaux" pour en savoir plus).

Voici quelques règles de programmation que vous pouvez suivre systématiquement afin de diminuer les risques d’erreurs de segmentation.

De l’utilisation des fonctions

Lorsque vous appelez une fonction, faites attention aux arguments que vous donnez. Une erreur commune consiste à indiquer un pointeur vers un espace mémoire non alloué :


/* Exemple d'espace non initialisé */ 
char *buffer; 
scanf ("%s", buffer);
 

Lorsque vous appelez une fonction, il faut toujours tester la valeur de retour avec l’idée que celle-ci peut contenir une erreur. Si certaines fonctions ne renvoient en général pas d’erreur comme le calcul de la racine carrée d’un nombre réel positif, d’autres sont susceptibles d’échouer comme malloc() en cas de mémoire insuffisante ou fork() lorsque le nombre de processus a atteint la limite autorisée par le système. Un appel à malloc() pourra avoir cette forme :


char *str; ...

Écrire du code réutilisable

Problème

Pour éviter de réécrire certains éléments de code à chaque nouveau programme, vous souhaitez rendre votre code réutilisable.

Solution

Placez le code réutilisable dans des fonctions, à raison d’une tâche par fonction et réciproquement. Écrivez la fonction pour la tâche qu’elle doit effectuer et adaptez le programme à la fonction.

Discussion

Le fait d’écrire du code qui ne puisse être facilement réutilisé est normal : le code est écrit en fonction du programme et ne tient pas compte des besoins d’autres programmes. Cependant, dans certains cas, le code est assez générique et indépendant du programme. C’est ce code que vous pouvez tenter de rendre réutilisable. Pour cela, il faut identifier les tâches qu’il exécute et les délimiter. Par exemple, une fonction d’affichage quelconque peut avoir besoin de l’heure au format ISO 8601 (AAAA/MM/JJ HH:MM:SS). Nous pouvons alors isoler une fonction qui prend en argument un tampon de taille supposée suffisante, et cherche l’heure système afin de remplir la chaîne de caractères. L’exemple suivant montre comment afficher un message sur la sortie d’erreur :


char strtime[] = "AAAA/MM/JJ HH:MM:SS"; 
time_t t; 
struct tm *tm; 
 
t = time (NULL); 
tm = localtime (&t); 
strftime (strtime, sizeof (strtime), "%Y/%m/%d %H:%M:%S", tm); 
 
fprintf (stderr, "%s [%s:%d] : %s", strtime, __FILE__, __LINE__, 
         "Un message d'erreur");
 

Le code suivant montre la partie réutilisable une fois isolée :


void 
time2string (char *strtime, int len) 
{ 
  time_t t; 
  struct tm *tm; 
 
  t = time (NULL); 
  tm = localtime (&t); 
  assert(len >= sizeof("AAAA/MM/JJ HH:MM:SS")); 
  strftime (strtime, len, "%Y/%m/%d %H:%M:%S", tm); 
}
 

Le code qui suit...

Faire renvoyer plusieurs valeurs à une fonction

Problème

Une fonction effectue des calculs et les résultats sont multiples. Vous voulez faire renvoyer ces résultats à la fonction.

Solution

Lorsque le but est de faire renvoyer plusieurs valeurs à une fonction, il n’y a pas d’autre choix que d’utiliser une structure. Par contre, pour des résultats multiples, d’autres moyens de les obtenir consistent à programmer la fonction pour les passer par référence, ou même en l’appelant plusieurs fois afin qu’elle renvoie le résultat suivant.

Discussion

Au sens strict du terme, si une fonction doit renvoyer plusieurs résultats, implémentez une structure dans laquelle vous placez ces résultats. La fonction doit alors allouer l’espace pour la structure, la remplir avec les résultats, puis renvoyer un pointeur dessus. Voici une fonction calculant la division entière de deux nombres et renvoyant le quotient et le reste :


typedef struct 
{ 
  int q; 
  int r; 
} div_t; 
 
div_t * 
div1 (int a, int b) 
{ 
  div_t *result; 
  assert (b != 0); 
  if (NULL == (result = malloc (sizeof *result))) 
    return (NULL); 
  result->q = a / b; 
  result->r = a % b; 
  return (result); 
} 
 
/* ... */ 
 
div_t *r; 
r = div1 (10, 3); ...

Le bon usage des macros

Problème

Vous souhaitez utiliser une macro, mais vous ne savez pas comment l’écrire pour qu’elle fonctionne dans tous les cas.

Solution

Mettez tous les arguments entre parenthèses. Si votre macro génère des instructions, placez celles-ci dans un bloc do { } while(0).

Discussion

La création d’une macro s’effectue simplement à l’aide du mot-clé #define d’une des deux manières suivantes :


#define nom_de_la_macro définition 
#define nom_de_la_macro (liste_de_paramètres_formels) définition
 

Les macros présentent l’intérêt de définir des constantes ou des instructions qui seront systématiquement et littéralement remplacées par le préprocesseur là ou leur nom apparaît. Elles sont préférables aux fonctions dans certains cas, comme la définition d’opérations sur des variables qui n’ont pas forcément le même type. Pour mettre un nombre au carré, par exemple, il nous faudra au moins deux fonctions, l’une si l’argument est un entier, l’autre s’il est de type flottant. La macro que nous pouvons écrire pour cela ne regarde pas le type des données tant qu’il est possible de remplacer le nom de la macro par sa définition et de compiler le résultat. Cela permet d’avoir le même code pour un nombre entier ou flottant.

Autre avantage des macros...

Goto et les traitements d’exception

Problème

Votre programme doit traiter une situation exceptionnelle et vous souhaitez placer à part le code qui la traite pour ne pas diminuer la lisibilité du code principal.

Solution

Une situation exceptionnelle nécessite une section de code à part. Le plus simple est de s’y brancher avec un goto employé à bon escient.

Discussion

L’instruction goto est maudite. En effet, aux débuts de l’informatique personnelle dans les années 1980 est apparu le langage BASIC dont une des caractéristiques à cette époque était d’obliger la numérotation des lignes. Une autre caractéristique était l’instruction GOTO qui permettait de se brancher de façon inconditionnelle à la ligne indiquée en argument. Par ailleurs, l’instruction IF était limitée car le code à exécuter en fonction de la condition devait tenir sur la suite de la ligne. La notion de blocs d’instructions n’existait pas. Cela entraîna l’écriture de lignes très longues, à l’origine de la dénomination dite de code spaghetti. Pour éviter ces longs spaghetti indigestes, les programmeurs ont utilisé l’instruction GOTO conjointement à IF puis à tort et à travers, en particulier pour écrire des boucles...