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 :
/* 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é 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;
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.
É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 :
/* 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
/* 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 :
/* 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 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 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 (en-tête).
/* 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'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 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).
/* 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 CLIB 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
; /* 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.
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 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 * :
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 :
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 */
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 maîtrise.
Bien évidemment, le typecast n'est pas la solution :
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 chaînes de caractères, qui, rappelons le, ne sont pas modifiables. Il est fortement recommandé de définir tout pointeur sur une chaîne 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.