« Programmation C/Types avancés » : différence entre les versions

Contenu supprimé Contenu ajouté
balise source
Ligne 3 :
</noinclude>
== Structures ==
<source lang="c">
<pre>
struct ma_structure {
type1 champ1;
Ligne 10 :
typeN champN;
} var1, var2, ..., varM;
</presource>
 
Déclare une structure (ou enregistrement) ''ma_structure'' composé de N champs, ''champ1'' de type ''type1'', ''champ2'' de type ''type2'', etc. On déclare, par la même occasion, M variables de type ''struct ma_structure''.
 
=== Accès aux champs ===
L'accès aux champs d'une structure se fait avec un point :
 
<pre>
<source lang="c">
struct complexe {
int reel;
Ligne 23 ⟶ 25 :
c.reel = 1;
c.imaginaire = 2;
</presource>
 
=== Initialisation ===
Ligne 39 ⟶ 41 :
Il s'agit d'un concept relativement avancé, mais qu'il est bien de connaitre pour agir en connaissance de cause. Lorsqu'on déclare une structure, on pourrait naïvement croire que les champs se suivent les uns à la suite des autres en mémoire. Considérez la structure suivante&nbsp;:
 
<source lang="c">
<pre>
struct ma_structure {
char champ1; /* 8bits */
int champ2; /* 32bits */
char champ3; /* 8bits */
};
</presource>
 
On pourrait penser que cette structure fasse 6 octets et pourtant sur la majeure partie des compilateurs, pour ne pas dire tous, on obtiendrait une taille de 12 octets.
Ligne 54 ⟶ 56 :
 
Dans le cas d'un ordinateur dit «32 bits», par exemple (i.e. dont le processeur central utilise des ''registres'' de 32 bits), il est courant que la mémoire ne puisse être adressée directement que par «bloc» de 32 bits. En se représentant la mémoire comme un tableau continu, on peut tracer le dessin suivant:
 
<pre>
| bloc N | bloc N + 1 | bloc N + 2 |
---+---------------+---------------+---------------+---
| a | b | c | d | e | f | g | h | i | j | k | l |
---+---------------+---------------+---------------+---
</pre>
 
Les cases <code>a</code>, <code>b</code>, <code>c</code>, ... représentent des octets, et les ''blocs'' des sections de 32 bits, qui seules peuvent être adressées directement par la machine. Si on suppose qu'une variable de type <code>ma_structure</code> doive être placée en mémoire à partir du bloc numéro N, alors un compilateur pourra, pour des raisons de performance, placer <code>champ1</code> en <code>a</code>, <code>champ2</code> de <code>e</code> à <code>h</code>, et <code>champ3</code> en <code>i</code>. Cela permettrait en effet d'accéder simplement à <code>champ2</code>: une lecture ou une écriture de ce champ se fait simplement en lisant ou écrivant le bloc N + 1. Dans ce cas, les octets de <code>b</code> à <code>d</code> ne sont pas utilisés; on dit alors que ce sont des octets ''de bourrage'' (ou ''padding'' en anglais).
Un autre compilateur (ou le même, appelé avec des options différentes) peut aussi placer <code>champ2</code> de <code>b</code> à <code>e</code>, et <code>champ3</code> en <code>f</code>, pour optimiser l'utilisation mémoire. Mais alors il devra générer un code plus complexe lors des accès à <code>champ2</code>, le matériel ne lui permettant pas de le faire directement.
 
Même si les options commandant l'utilisation de bourrage sont spécifiques à chaque compilateur, il peut s'avérer nécessaire de connaître la «distance» (''offset'') d'un champ par rapport au début de la structure, et ce, de manière portable. Pour cela il existe une fonction (en fait, macro), déclarée dans l'entête <code>stddef.h</code>&nbsp;:
 
<source lang="c">
<pre>size_t offsetof(type, champ);</pre>
</source>
 
La valeur renvoyée est le nombre de mots machines (octets la plupart du temps), entre le début de la structure et celui du champ. Le premier argument attendu est bel et bien le type de la structure et non un pointeur vers ce type (avec un <code>typedef</code> à la limite, du moment que le type déclaré n'est pas un pointeur). Pour s'en souvenir, il suffit de savoir que beaucoup d'implémentations d'<code>offsetof</code> utilise une arithmétique de ce genre&nbsp;:
 
<source lang="c">
<pre>size_t distance = (size_t) &((type *)NULL)->champ;</pre>
</source>
 
Si <code>type</code> était un pointeur, il faudrait faire un déréférencement supplémentaire (ou éviter l'étoile dans la macro). À noter que même si cette macro peut s'avérer contraignante (notamment lorsqu'on ne dispose que de type pointeur), il est quand même préférable de l'utiliser pour des raisons de portabilité.
Ligne 74 ⟶ 83 :
Voici un petit exemple d'utilisation de cette macro&nbsp;:
 
<source lang="c">
<pre>
#include <stddef.h>
#include <stdio.h>
Ligne 92 ⟶ 101 :
printf("L'offset de 'champ2' vaut %lu.\n", (unsigned long) offsetof(struct ma_structure, champ2));
return 0;
}
}</pre>
</source>
 
Sur une architecture 32bits, vous obtiendrez très certainement la réponse:
Ligne 99 ⟶ 109 :
=== Pointeurs vers structures ===
Il est (bien entendu) possible de déclarer des variables de type pointeur vers structure :
 
<pre>
<source lang="c">
struct ma_struct * ma_variable;
</presource>
 
Comme pour tout pointeur, on doit allouer de la mémoire pour la variable avant de l'utiliser :
 
<pre>
<source lang="c">
ma_variable = malloc( sizeof(struct ma_struct) );
</presource>
 
L'accès aux champs peut se faire comme pour une variable de type structure « normale » :
 
<pre>
<source lang="c">
(* ma_variable).champ
</presource>
 
Ce cas de figure est en fait tellement fréquent qu'il existe un raccourci pour l'accès aux champs d'un pointeur vers structure :
 
<pre>
<source lang="c">
ma_variable->champ
</presource>
 
== Unions ==
Une union et un enregistrement se déclarent de manière identique :
 
<source lang="c">
<pre>
union type_union
{
Ligne 129 ⟶ 146 :
/* Déclaration de variables */
union type_union var1, var2, /* ... */ varM;
</presource>
 
Toutefois, à la différence d'un enregistrement, les N champs d'une instance de cette union occupent le même emplacement en mémoire. Modifier l'un des champ modifie donc tous les champs de l'union. Typiquement, une union s'utilise lorsqu'un enregistrement peut occuper plusieurs fonctions bien distinctes et que chaque fonction ne requiert pas l'utilisation de tous les champs.
Ligne 135 ⟶ 152 :
Voici un exemple ultra classique d'utilisation des unions, qui provient en fait de la gestion des événements du système X-Window, très répandu sur les systèmes Unix&nbsp;:
 
<source lang="c">
<pre>union _XEvent
{
int type;
Ligne 150 ⟶ 168 :
XKeymapEvent xkeymap;
long pad[24];
};</pre>
</source>
 
La déclaration a juste été un peu raccourcie pour rester lisible. Les types <code>X*Event<code> de l'union sont en fait des structures contenant des champs spécifiques au message. Par exemple, si <code>type</code> vaut <code>ButtonPress</code>, seules les valeurs du champ <code>xbutton</code> seront significatives. Parmi ces champs, il y a <code>button</code> qui permet de savoir quel bouton de la souris à été préssé&nbsp;:
 
<source lang="c">
<pre>XEvent ev;
XNextEvent(display, &ev);
 
Ligne 164 ⟶ 184 :
printf("Message type %d\n", ev.type);
}
</presource>
 
== Définitions de synonymes de types (typedef) ==
 
Le langage C offre un mécanisme assez pratique pour définir des synonymes de types. Il s'agit du mot-clé <code>typedef</code>.
 
<pre>
<source lang="c">
typedef un_type synonyme_du_type;
</presource>
 
Contrairement aux langages à typage fort comme le C++, le C se base sur les types atomiques pour décider de la compatibilité entre deux types. Dit plus simplement, la définition de nouveaux types est plus un mécanisme d'alias qu'une réelle définition de type. Les deux types sont effectivement parfaitement interchangeables. À la limite on pourrait presque avoir les mêmes fonctionnalités en utilisant le préprocesseur C, bien qu'avec ce dernier vous aurez certainement beaucoup de mal à sortir de tous les pièges qui vous seront tendus.
 
=== Quelques exemples ===
<source lang="c">
<pre>
typedef unsigned char octet;
typedef double matrice4_4[4][4];
Ligne 187 ⟶ 208 :
ma_struct pointeur = NULL;
gestionnaire_t fonction = NULL;
</presource>
 
Ce mot clé est souvent utilisé conjointement avec la déclaration des structures, pour ne pas devoir écrire à chaque fois le mot clé <code>struct</code>. Elle permet aussi de '''grandement''' simplifier les prototypes de fonctions qui prennent des pointeurs sur des fonctions en argument, ou retournent de tels types. Il est conseillé dans de tels cas de définir un type synonyme, plutôt que de l'écrire in extenso dans la déclaration du prototype. Considérez les deux déclarations :
 
<pre>
<source lang="c">
/* Déclaration confuse */
void (*fonction(int, void (*)(int)))(int);
Ligne 198 ⟶ 220 :
 
handler_t fonction(int, handler_t);
</presource>
 
Les vétérans auront reconnu le prototype imbitable de l'appel système <code>signal()</code>, qui permet de rediriger les signaux (interruption, alarme périodique, erreur de segmentation, division par zéro, ...).
 
== Énumérations ==
<source lang="c">
<pre>
enum nom_enum { val1, val2, ..., valN };
</presource>
 
Les symboles ''val1'', ''val2'', ..., ''valN'' pourront être utilisés littéralement dans la suite du programme. Ces symboles sont en fait remplacés par des entiers lors de la compilation. La numérotation commençant par défaut à 0, s'incrémentant à chaque déclaration. Dans l'exemple ci-dessus, ''val1'' vaudrait 0, ''val2'' 1 et ''valN'' N-1.
 
On peut changer à tout moment la valeur d'un symbole, en affectant au symbole, la valeur '''constante''' voulue (la numérotation recommençant à ce nouvel indice). Par exemple :
 
<pre>
<source lang="c">
enum Booleen { Vrai = 1, Faux = 0 };
 
/* Pour l'utiliser */
enum Booleen variable = Faux;
</presource>
 
Ce qui est assez pénible en fait, puisqu'il faut à chaque fois se souvenir que le type <code>Booleen</code> est dérivé d'une énumération. Il est préférable de simplifier les déclarations, grâce à l'instruction ''typedef'' :
 
<pre>
<source lang="c">
typedef enum { Faux, Vrai } Booleen;
 
/* Pour l'utiliser */
Booleen variable = Faux;
</presource>
 
== Type incomplet ==
Ligne 229 ⟶ 253 :
Pour garantir un certain degré d'encapsulation, il peut être intéressant de masquer le contenu d'un type complexe, pour éviter les usages trop « optimisés » de ce type. Pour cela, le langage C permet de déclarer un type sans indiquer explicitement son contenu.
 
<source lang="c">
<pre>
struct ma_structure;
 
/* Plus loin dans le code */
struct ma_structure * nouvelle = alloue_objet();
</presource>
 
Les différents champs de la structure n'étant pas connus, le compilateur ne saura donc pas combien de mémoire allouer. On ne peut donc utiliser les types incomplets qu'en tant que pointeur. C'est pourquoi, il est pratique d'utiliser l'instruction <code>typedef</code> pour alléger les écritures :
 
<pre>
<source lang="c">
typedef struct ma_structure * ma_struct;
 
/* Plus loin dans le code */
ma_struct nouvelle = alloue_objet();
</presource>
 
Cette construction est relativement simple à comprendre, et proche d'une conception objet. À noter que le nouveau type défini par l'instruction <code>typedef</code> peut très bien avoir le même nom que la structure. C'est juste pour éviter les ambiguités, qu'un nom différent a été choisi dans l'exemple.
 
Un autre cas de figure relativement classique, où les types incomplets sont très pratiques, ce sont les structures s'auto-référençant, comme les listes chainées, les arbres, etc.
 
<pre>
<source lang="c">
struct liste
{
Ligne 253 ⟶ 280 :
void * element;
};
</presource>
 
Ou de manière encore plus tordue, plusieurs types ayant des références croisées :
 
<pre>
<source lang="c">
struct type_a;
struct type_b;
Ligne 272 ⟶ 300 :
void * element;
};
</presource>