IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Notes sur le langage C


précédentsommairesuivant

XV. size_t, c'est quoi ?

size_t est le type retourné par l'opérateur sizeof. C'est un entier non signé. Il est suffisamment grand pour contenir la valeur représentant, en nombre de bytes (ou char), la taille du plus grand objet possible d'une implémentation donnée.

Il convient pour les tailles, les dimensions de tableau, les index croissants et non négatifs…

Ce type est défini dans <stddef.h> qui est inclus dans la plupart des headers standards courants (<stdio.h>, <stdlib.h> <string.h>, etc.)

XVI. Données

Le langage C utilise 3 zones mémoire pour implémenter les données :

  • la mémoire statique, qui contient les variables permanentes (modifiables ou non) ;
  • la mémoire automatique qui contient les variables locales et les paramètres des fonctions ;
  • la mémoire allouée qui contient des variables dynamiques gérées à l'exécution par le programme (malloc() / free().

Les données en C sont caractérisées par :

  • leur portée ;
  • leur durée de vie.

La portée peut être :

  • locale à un bloc ;
  • limitée à une unité de compilation ;
  • illimitée ;
  • contrôlée par l'application.

La durée de vie peut être :

  • limitée à un bloc ;
  • permanente ;
  • contrôlée par l'application.

Exemples :

 
Sélectionnez
/* permanente de portée illimitée */
int a;
 
/* permanente de portée limitée à l'unité de compilation */
static int b;
 
<...>
{
/* locale (de portée limitée au bloc) */
   int c;
 
/* permanente de portée limitée au bloc */
   static int d;
 
/* contrôlée de portée limitée à la validité du pointeur (non NULL) */
   int *p = malloc (sizeof *p * 3);
 
 
   free (p), p = NULL;
}

XVI-A. Initialisation

Seules les données statiques sont initialisées avant le lancement de main(). Les autres ont une valeur indéterminée. Il est donc nécessaire de les initialiser avant utilisation (lecture). Sauf indication explicite contraire, l'initialisation par défaut des données statiques est 0.

XVII. Structures

XVII-A. Structures visibles

On appelle structure visible une structure dont les éléments sont visibles de l'utilisateur.

Une 'définition de structure' est le moyen par lequel le programmeur indique au compilateur comment est constituée une structure. Cette opération ne réserve aucune mémoire.

 
Sélectionnez
struct mastructure
   {
      type_1 element_a;
      type_2 element_b;
   };

Ensuite, une structure peut être instanciée, c'est-à-dire qu'une instance de cette structure est définie en mémoire.

 
Sélectionnez
struct mastructure mastructure;

il est autorisé d'utiliser le même nom, même si ce n'est probablement pas le meilleur des choix possibles.

Ces informations suffisent à définir et instancier n'importe quelle structure 'visible'.

XVII-B. Structures opaques

On appelle structure opaque une structure dont les éléments ne sont pas visibles de l'utilisateur.

Pour cela, on utilise une définition réduite (ou incomplète) qui consiste à définir le nom de la structure sans en préciser le contenu.

 
Sélectionnez
struct mastructure;

Cette définition dite incomplète ne permet évidemment pas de créer une instanciation, puisque le compilateur ignore le contenu de la structure. Il n'a donc pas les moyens d'en déterminer la taille.

Par contre, il est possible de créer un pointeur de ce type :

 
Sélectionnez
struct mastructure *p;

Il devient alors possible de créer une fonction qui retourne un pointeur de ce type :

 
Sélectionnez
struct mastructure *fonction(void);

de passer ce pointeur en paramètre une fonction :

 
Sélectionnez
void fonction(struct mastructure *p);

d'en faire un élément de structure, etc.

Évidemment, il faudra que la structure soit définie 'quelque part' afin qu'elle soit instanciable et que ses éléments soient manipulables.

On va donc créer un fichier source (.c) séparé d'implémentation contenant les fonctions permettant la création (instanciation) des données, et une interface (header ou .h) ne comportant que la définition incomplète de la fonction et, au minimum, les 2 fonctions permettant la création et la suppression d'une instance de la structure.

Soit la structure 'xxx'. On obtient :

 
Sélectionnez
/* xxx.h Interface */
#ifndef H_XXX
#define H_XXX
 
/* définition incomplète de structure */
struct xxx;
 
/* prototypes des fonctions */
struct xxx *xxx_create (void);
void xxx_delete (struct xxx *p);
/*, etc. */
 
#endif
 
Sélectionnez
/* xxx.c Implémentation */
#include "xxx.h"
 
/* définition de la structure (exemple) */
struct xxx
{
   int a;
   char b[10];
};
 
/* fonctions publiques */
struct xxx *xxx_create (void)
{
   /* a completer */
}
 
void xxx_delete (struct xxx *p)
{
   /* a completer */
}

Exemple d'utilisation :

 
Sélectionnez
/* test.c */
#include "xxx.h"
 
#include <stddef.h>
 
int main (void)
{
   /* instanciation */
   struct xxx *p = xxx_create ();
 
   /* tout appel de fonction peut échouer... */
   if (p != NULL)
   {
      /* utilisation de l'objet xxx
         (via de nouvelles fonctions à créer)
 
         ...
       */
 
      /* fin d'utilisation : destruction de l'objet */
      xxx_delete (p), p = NULL;
   }
   return 0;
}

Je laisse au lecteur le soin de proposer une ou des implémentations de xxx_create() et de xxx_delete(), sachant qu'on n’a pas forcément besoin d'un nombre illimité d'instanciations…

XVII-C. Pseudonymes (ou alias)

Il est possible, afin de simplifier l'écriture (notamment pour les interfaces publiques), de remplacer le nom de la structure par un nom différent (alias ou pseudonyme) généralement plus court. Il est recommandé de ne pas abuser de l'abstraction, car les possibilités sont réduites en C et il est bon que le programmeur garde en tête qu'il manipule des pointeurs.

Ceci est possible :

 
Sélectionnez
typedef struct xxx xxx_s;

mais ceci est déconseillé :

 
Sélectionnez
typedef struct xxx *xxx; /* /!\ */

Détails d'application dans l'article Les types abstraits de données (ADT).

XVIII. Variables globales

Une variable globale est une variable définie en dehors d'une fonction et de portée globale. Sa durée de vie est égale à celle du programme. Elle est initialisée par défaut (0) ou explicitement avant l'exécution de main(). Sa valeur est persistante.

Un usage abusif des variables globales est fortement déconseillé pour diverses raisons :

  • on ne sait ni comment ni quand ni qui accède à cette variable. Cela rend le code intestable et incompréhensible. On n'a aucune certitude sur le code ;
  • cela crée une dépendance, ce qui le code rend non modulaire et non réutilisable ;
  • l'instance étant unique, ça rend le code impropre à la récursion et à l'utilisation dans des threads et même au simple appel imbriqué.

Ceci dit, il est des cas rares (ou plus fréquents s'il s'agit de lecture seule) où les variables globales sont utiles, voire indispensables. Dans le cadre d'une application professionnelle, ces cas doivent être justifiés. Voici comment les définir correctement dans le cadre d'une application composée d'unités de compilations séparées.

XVIII-A. Nommage

Il est recommandé d'utiliser le préfixe g_ ou G_ pour signifier qu'une variable est globale.

XVIII-B. Organisation

Il est préférable pour éviter la dispersion, d'utiliser une ou des structures de variables globales regroupées par fonction, plutôt qu'une multitude de variables.

XVIII-C. Définition

Il est recommandé que la définition d'une variable globale soit faite exclusivement dans un fichier source (.c). Ce fichier doit inclure le fichier de déclaration (entête).

 
Sélectionnez
/* data.c */
 
#include "data.h"
 
int G_x;
 
/* la taille du tableau est définie
 * dans la déclaration.
 */
double G_a[];
 
data_s G_data;

XVIII-D. Déclaration

Il est recommandé que la déclaration d'une variable globale soit faite exclusivement dans un fichier d'entête (.h). Ce fichier doit être inclus dans le fichier de définition et dans tous les fichiers d'utilisation (.c). Comme tous les fichiers d'entêtes, celui-ci dispose de protections contre les inclusions multiples.

 
Sélectionnez
#ifndef H_DATA
#define H_DATA
 
/* data.h */
 
typedef struct
{
   int a;
   char b[123];
}
data_s;
 
extern int G_x;
 
/* la dentition de la taille du tableau est unique. Elle est faite ici. */
extern double G_a[12];
 
extern data_s G_data;
 
#endif /* guard */

XVIII-E. Utilisation

Il est recommandé que le fichier qui utilise une variable globale inclue le fichier de déclaration (.h).

 
Sélectionnez
/* appli.c */
 
#include "data.h"
 
int main (void)
{
   G_x = 123;
 
   G_data.a = 456;
 
   G_a[3] = 123.456;
 
   return 0;
}

XIX. Champs de bits

Afin de réduire la taille des objets, il est possible de définir un champ de bits. La définition doit se faire dans une structure. Le type de l'objet unitaire doit être int ou unsigned int (recommandé) ou _Bool (bool) en C99.

 
Sélectionnez
typedef struct
{
   unsigned a:1; /* variable de largeur 1 bit */
   unsigned b:3; /* variable de largeur 3 bits */
 
   /*, etc. */
}
data_s;

Il faut garder à l'esprit que l'implantation mémoire des bits n'est pas spécifiée par le langage C. (Et j'ai effectivement constaté sur le terrain des différences selon les implémentations, notamment concernant l'ordre des bits.)

Autant une utilisation interne est possible et peut se justifier pour réduire la taille des objets (stockage en mémoire, notamment), extrait de CLIB Module DATE (date.h) (Les tailles indiquées en commentaire sont les tailles minimales garanties)…

 
Sélectionnez
typedef unsigned int uint;
<...>
typedef struct
{
   /* 16 bits */
   int year;        /* -32767..+32767 */
   uint month:4;    /* 0-15 */
   uint day:5;      /* 0-31 */
 
   uint hour:5;     /* 0-31 */
   uint minute:6;   /* 0-63 */
   uint second:6;   /* 0-63 */
}
sDATE;

… autant il est illusoire d'utiliser les champs de bits pour créer une interface avec l'extérieur du programme, comme un flux de bytes ou un périphérique en accès direct (mémoire, bus I/O, etc.).

Autre pratique non portable, faire une union entre un champ de bits et une variable en s'imaginant pouvoir accéder à la variable, soit d'un bloc, soit bit à bit.

La solution portable pour accéder aux bits d'une variable est d'utiliser les opérateurs binaires (&, |, ~, <<, >>, ^)

XX. Bien utiliser const

Voici à quoi sert le qualificateur const et comment l'utiliser correctement.

XX-A. Objets simples

Le mot clé 'const' est un qualificateur (qualifier) d'objet. Il lui fait perdre sa qualité par défaut qui est 'accessible en lecture ou en écriture' pour le modifier en 'accessible en lecture seule'. Par exemple :

 
Sélectionnez
int x = 3;
int const y = 4;
 
x = 5; /* accès en écriture possible */
y = 6; /* accès en écriture interdit */

Le compilateur signale l'erreur.

On peut placer indifféremment le qualificateur const avant ou après le type.

 
Sélectionnez
int const a = 7;
const int b = 8;

Mais je conseille néanmoins la première forme, car elle est beaucoup plus claire (notamment avec les pointeurs).

Il est techniquement possible de définir un objet const non initialisé :

 
Sélectionnez
int const c;

Évidemment, l'intérêt est limité, mais il a son application dans un contexte particulier : les paramètres de fonctions.

XX-B. Pointeurs

Le cas des pointeurs est un peu plus complexe, puis qu'il y a en quelque sorte deux objets pour le prix d'un !

  • le pointeur lui-même qui peut être qualifié const ;
  • l'objet pointé qui peut lui-même être qualifié const.

Pour le pointeur, celui-ci étant un objet comme autre, la même règle s'applique, sachant que const doit être placé juste avant l'identificateur, c'est-à-dire après le dernier * :

 
Sélectionnez
int a;
int * const pa = &a;
 
pa++; /* interdit */
*pa = 123; /* OK */

Mais un autre qualificateur const peut être utilisé pour préciser les droits du pointeur sur l'objet pointé.

Celui-ci se place à la gauche de l'*, avant ou après le typem :

 
Sélectionnez
int a = 123;
int const * pa = &a;
const int * pb = &a;

mais là encore, pour des questions de clarté du code, je recommande la première forme. Ce qualificateur interdit la modification de l'objet via le pointeur (mais s'il n'est pas lui-même qualifié const, l'objet reste modifiable directement, évidemment).

 
Sélectionnez
int a = 123;
int const * pa = &a;
 
*pa = 456; /* interdit */
a = 456; /* OK */

Par contre, attention. Il est techniquement possible de définir un pointeur sur un objet qualifié const et de tenter de modifier l'objet. Cela produit un comportement indéfini qui n'est pas forcément signalé par le compilateur.

 
Sélectionnez
int const a = 123;
int * pa = &a;
 
*pa = 456; /* comportement indéfini */

Cependant, le plus souvent, le compilateur signale un problème au moment de l'affectation du pointeur.

 
Sélectionnez
int * pa = &a;

mais il convient de rester extrêmement prudent. Le C est un langage qui demande rigueur et maîtrise.

Bien évidemment, le typecast n'est pas la solution :

 
Sélectionnez
int const a = 123;
int * pa = (int *) &a; /* NE PAS FAIRE CECI */

il ne fait éventuellement que masquer le problème au compilateur (« je sais ce que fais »), mais il ne résout rien et le comportement indéfini est toujours là.

XX-C. Usage

Le rôle du qualificateur const est particulièrement utile avec les pointeurs, notamment sur des chaines de caractères, qui, rappelons-le, ne sont pas modifiables. Il est fortement recommandé de définir tout pointeur sur une chaine de caractères avec le qualificateur const :

 
Sélectionnez
char const *p = "hello";

Il est utile aussi pour les pointeurs passés en paramètre à des fonctions. Il permet en effet de restreindre l'accès à la variable pointée à un mode 'lecture seule', ce qui évite bien des erreurs de codage. (La modification d'une variable étant une opération lourde de conséquences si elle est faite au mauvais moment).

Une fonction qui affiche le contenu d'un tableau ou d'une structure, par exemple, n'a pas à la modifier. On fixe donc les règles du jeu dès la définition du prototype :

 
Sélectionnez
void display (T const *p)

Une utilisation astucieuse et intelligente du qualificateur const permet d'écrire du code plus sûr.


précédentsommairesuivant

Copyright © 2009 Emmanuel Delahaye. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.