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:
int a;
static int b;
< ...>
{
int c;
static int d;
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.
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 :
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 :
# ifndef H_XXX
# define H_XXX
struct xxx;
struct xxx * xxx_create (void );
void xxx_delete (struct xxx * p);
# endif
|
# include "xxx.h"
struct xxx
{
int a;
char b[10 ];
} ;
struct xxx * xxx_create (void )
{
}
void xxx_delete (struct xxx * p)
{
}
|
Exemple d'utilisation :
# include "xxx.h"
# include <stddef.h>
int main (void )
{
struct xxx * p = xxx_create ();
if (p ! = NULL )
{
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é :
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).
# include "data.h"
int G_x;
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
typedef struct
{
int a;
char b[123 ];
}
data_s;
extern int G_x;
extern double G_a[12 ];
extern data_s G_data;
# endif
|
XVIII-E. Utilisation
Il est recommandé que le fichier qui utilise une variable globale inclue le fichier de déclaration (.h).
# 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 ;
unsigned b:3 ;
}
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
{
int year;
uint month:4 ;
uint day:5 ;
uint hour:5 ;
uint minute:6 ;
uint second:6 ;
}
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 ;
y = 6 ;
|
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é :
é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+ + ;
* pa = 123 ;
|
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 ;
a = 456 ;
|
|
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 ;
|
Cependant, le plus souvent, le compilateur signale un problème au moment de l'affectation du pointeur.
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;
|
|
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 :
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.
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.