🆕 Structures et pointeurs vers structure
A quoi sert une structure ?
Dérouler
Imaginons que l'on souhaite constituer une bibliothèque. Chaque livre aura les mêmes caractéristiques : un titre, un auteur, une année de publication. Comment stocker ces informations ? Un tableau nous imposerai de tout stocker avec le même type (ici des str
), mais c'est assez peu pratique quand on veut ensuite faire des opérations sur ces champs. La solution ? Les structures.
Comment déclarer une nouvelles structure ?
Dérouler
Déclarer une structure, c'est déclarer un nouveau type, qui est ensuite utilisable comme tous les types que l'on connaît déjà (int
, char
, etc.). Par exemple :
struct livre{
str titre;
str auteur;
int annee;
};
⚠ le point virgule après la déclaration de la structure est obligatoire.
Que fait le code ci-dessus ? Il
- déclare un nouveau type
struct livre
- qui possède trois champs : un champ
titre
, de type str
, un champ auteur
, de type str
, et un champ année
, de type int
Comment créer une nouvelle variable de type structure ?
Dérouler
Si l'on reprend notre exemple précécent, et que l'on veut déclarer et initialiser deux livres, voici la marche à suivre :
-
On définit le type
struct livre
selon les lignes précédentes en entête de programme (cf. 'Où placer la déclaration de la structure ?').
-
On déclare une nouvelle variable du type désiré -- ici
struct livre
⚠ le type est struct livre
et pas livre
tout seul. Pour voir comment éviter d'avoir à écrire le struct
à chaque fois, cf typedef
.
struct livre nana ;
- On initialise chacun de ses champs un par un, grâce à l'opérateur
.
:
nana.titre = "Nana" ;
nana.titre = "Zola" ;
nana.annee = 1880;
Et voilà pour le premier !
- On fait de même pour toutes les variables de type
struct livre
que l'on souhaite instancier. Par exemple, pour Germinal de Zola, 1885 :
struct livre germinal ;
germinal.titre = "Germinal" ;
germinal.titre = "Zola" ;
germinal.annee = 1885;
💡 Il est possible d'initialiser tous les champs en une seule ligne, avec la syntaxe suivante :
struct livre germinal = {"Germinal", "Zola", 1885} ;
Je déconseille cette pratique dans un premier temps puisqu'elle est plus susceptible de générer des erreurs (inversion de paramètres par exemple), et une éventuelle erreur pointera sur l'unique ligne, rendant le bug d'autant plus diffile à trouver qu'il y a de champs dans la structure (contrairement à l'approche 'un champ=une ligne', permettant de trouver l'erreur instantanément)
Où placer la déclaration de la structure ?
Dérouler
Dans le main ? Dans une fonction ? Ni l'un ni l'autre ! La définition d'une structure est comme la définition d'un nouveau type, et on a donc interêt à ce que tous les éléments du code en bénéficie. Il doit donc se placer dans l'entête du code, après les #include
et autres DEFINE
.
Le typedef
doit se placer après la structure (logique), toujours dans l'entête du code.
💡 Plus tard, quand nous aurons appris à compiler nous-mêmes, nous pourrons être élégants et mettre les définitions des structures dans un fichier dédié, permettant de partager le nouveau type à tous les fichiers d'un même programme !
Marre de devoir taper struct machin
à chaque fois ? typedef
est là pour ça !
Dérouler
Arriver un moment, ça va bien de toujours taper struct
devant le nom de notre structure, mais c'est un peu long pour pas grand chose ...
On peut alors utiliser le mot clé typedef
à la fin de l'entête du programme pour mettre des alias (des surnoms) aux types existants.
On peut ainsi changer notre précédent type struct livre
en un_livre
avec le code suivant :
struct livre{ // déja dans le code normalement
str titre;
str auteur;
int annee;
};
typedef struct livre un_livre
Notez l'abscence de point-virgule à la fin de la commande : typedef
fonctionne comme DEFINE
, les alias seront remplacé par leur valeur lors de la précompilation.
💡 typedef
fonctionne aussi pour les types qui existe déjà. Flemme de taper unsigned int
en entier ?
typedef unsigned int uint
Et le tour est joué ! Maintenant, uint
est strictement équivalent à unsigned int
!
Quelles opérations peut-on faire sur les structures ?
Dérouler
AUCUNE,AUCUNE,AUCUNE !
Les opérations ne se font pas sur les structures, mais sur leur champs ! Ceci était un test, et vous venez d'échouer. Retournez en règle d'or n°6.
Passage par valeur ou par adresse ?
Développer
Imaginons que l'on ait une structure s
très volumineuse (quelques Go) de type struct struct_volumineuse
, et que l'on veuille mettre à jout la valeur d'un champ (mettons, le champ toto
) grâce à une fonction update_toto
.
Une première façon de faire est de passer la structure en argument et de la renvoyer dans la fonction :
struct struct_volumineuse update_toto(struct struct_volumineuse s){
s.toto = s.toto + 1 ;
return s ;
}
int main(){
struct struct_volumineuse s ;
s.toto = 0
s = update_toto(s);
return EXIT_SUCCESS;
}
L'écriture est relativement simple, mais le coût mémoire et temporel est très important : non seulement on crée une copie de la structure en la passant en argument par valeur (bon courage pour la RAM), mais en plus on doit à la fin du programme recopier toutes les valeurs de la structure interne à la fonction dans celle définie dans le main
.
La solution ? Le passage par référence.
Passage par référence
L'écriture avec un passage par référence est très similaire au code précédent, sauf qu'au lieu de passer en argument et de renvoyer une structure (ce qui est l'opération coûteuse), on passe en argument une référence vers la structure à modifier, que l'on modifie directement en mémoire. En reprenant l'exemple précédent :
void update_toto(struct struct_volumineuse *p_s){
if (s == NULL){
fprinf("Pointeur nul") ;
exit(EXIT_FAILURE)
}
*(p_s).toto = *(p_s).toto + 1 ;
}
int main(){
struct struct_volumineuse s ;
s.toto = 0
update_toto(&s);
return EXIT_SUCESS;
}
On ainsi modifié le prototype de la fonction update_toto
pour
- Qu'elle ne revoie rien : on travaille directement sur la structure en mémoire
- Qu'elle prenne en argument un pointeur vers la structure, de type
struct struct_volumineuse *
Opérateur ->
Développer
Il existe un opérateur équivalent au déréférencement puis à l'accès à un champ d'une structure. Par exemple, les deux lignes suivantes sont strictement équivalentes :
*(p_s).toto = 1 ;
p_s -> toto = 1 ;
💡 A moins que l'inverse ne soit spécifié (ou nécessaire pour obtenir 100/100 sur VPL), la notation *(p_s).toto
est recommandée car
- Elle utilise l'opérateur
*()
, pour lequel on a toujours le réflexe de tester si le pointeur passé en argument est différent de NULL
(cf règle d'or n°5).
- Elle permet de suivre, à l'aide du schéma de la RAM sur une feuille, les différentes opérations effectuées.