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

Notes sur le langage C

Date de publication : 29 avril 2009


XV. size_t, c'est quoi ?
XVI. Données
XVI-A. Initialisation
XVII. Structures
XVII-A. Structures visibles
XVII-B. Structures opaques
XVII-C. Pseudonymes (ou alias)
XVIII. Variables globales
XVIII-A. Nommage
XVIII-B. Organisation
XVIII-C. Définition
XVIII-D. Déclaration
XVIII-E. Utilisation
XIX. Champs de bits
XX. Bien utiliser const
XX-A. Objets simples
XX-B. Pointeurs
XX-C. Usage


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 suffisament 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:

/* permanente de portee illimitee */
int a;

/* permanente de portee limitee a l'unite de compilation */
static int b;

<...>
{
/* locale (de portee limitee au bloc) */
   int c;

/* permanente de portee limitee au bloc */
   static int d;

/* controlee de portee limitee a la validite 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é une structure. Cette opération ne réserve aucune mémoire.

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.

struct mastructure mastructure;
			
Nota : il est autorisé d'utiliser le même nom, même si ce n'est probablement pas le meilleur des choix possible.

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.

struct mastructure;
Cette définition dite incomplète ne permet évidemment pas de créer un 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 :

struct mastructure *p;
Il devient alors possible de créer une fonction qui retourne un pointeur de ce type :

struct mastructure *fonction(void);
de passer ce pointeur en paramètre une fonction :

void fonction(struct mastructure *p);
d'en faire un élément de structure etc.

Evidemment, 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 :

/* xxx.h Interface */
#ifndef H_XXX
#define H_XXX

/* definition incomplete de structure */
struct xxx;

/* prototypes des fonctions */
struct xxx *xxx_create (void);
void xxx_delete (struct xxx *p);
/* etc. */

#endif

/* xxx.c Implementation */
#include "xxx.h"

/* definition 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 :

/* test.c */
#include "xxx.h"

#include <stddef.h>

int main (void)
{
   /* instanciation */
   struct xxx *p = xxx_create ();

   /* tout appel de fonction peut echouer... */
   if (p != NULL)
   {
      /* utilisation de l'objet xxx
         (via de nouvelles fonctions a creer)

         ...
       */

      /* 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 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éduite en C et il est bon que le programmeur garde en tête qu'il manipule des pointeurs.

Ceci est possible :

typedef struct xxx xxx_s;
mais ceci est déconseillé :

typedef struct xxx *xxx; /* /!\ */
Détails d'application dans l'article sur les TAD (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. Ca 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 (en-tête).

/* data.c */

#include "data.h"

int G_x;

/* la taille du tableau est definie
 * dans la declaration.
 */
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'en-têtes, celui-ci dispose de protections contre les inclusions multiples.

#ifndef H_DATA
#define H_DATA

/* data.h */

typedef struct
{
   int a;
   char b[123];
}
data_s;

extern int G_x;

/* la defintion 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).

/* 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.

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 http://mapage.noos.fr/emdel/clib.htm Module DATE (date.h) (Les tailles indiquées en commentaire sont les tailles minimales garanties)...

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 :

int x = 3;
   int const y = 4;

   x = 5; /* acces en ecriture possible */
   y = 6; /* acces en ecriture interdit */
Le compilateur signale l'erreur.

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

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é :

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 2 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 * :

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 type :

int a = 123;
   int const * pa = &a;
   const int * pb = &a;
mais là encore, pour des question 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).

int a = 123;
   int const * pa = &a;

   *pa = 456; /* interdit */
   a = 456; /* OK */
warning 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.

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.

int * pa = &a;
mais il convient de rester extrêmement prudent. Le C est un langage qui demande rigueur et maitrise.

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

int const a = 123;
   int * pa = (int *) &a; /* NE PAS FAIRE CECI */
warning 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 :

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 :

void display (T const *p)
Une utilisation astucieuse et intelligente du qualificateur const permet d'écrire du code plus sûr.

 

Valid XHTML 1.1!Valid CSS!

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.