Structures et alias
Les tableaux permettent de manipuler une collection d’objets homogènes, soit directement, soit, c’est préférable voire indispensable, à l’aide de pointeurs.
Pour manipuler une collection d’objets inhomogènes, le langage C introduit la notion de structure que l’on définit avec le mot clé struct
.
Un premier exemple
Imaginons que l’on souhaite concevoir un programme de gestion d’une librairie pour lequel chaque livre serait défini par les informations suivantes :
titre ;
nom de l’auteur ;
numéro ISBN ;
prix.
Voyons comment implémenter ces données.
Déclaration de la structure
Pour stocker cette information, on peut créer une structure Book
de la façon suivante:
struct Book
{
char title[100];
char author[50];
long int isbn;
double price;
};
Chaque information correspond à un champ de la structure et lors de la compilation la mémoire sera réservée conformément au type et à la taille de chacun de ces champs. Dans le cas présent, elle réserve 2 tableaux de char
pour le titre et l’auteur, un long int
pour le numéro ISBN et un double
pour le prix.
Pour savoir à quel endroit déclarer cette structure, rappelons les bonnes pratiques de l’organisation d’un programme C. Le code source se décompose en 4 parties:
Les directives
include
etdefine
;les variables globales ;
les fonctions secondaires ;
la fonction principale
main()
.
Dans le code source, la structure sera donc placée avant les fonctions secondaires, dans l’espace des variables globales. Comme elle est définie en dehors des fonctions, elle sera donc accessible par chacune de ces fonctions.
Note
Le long int
est ici nécessaire car le simple int
ne permet pas de stocker de très grands nombre. Or le numéro ISBN peut comporter jusqu’à 13 chiffres.
Observons les valeurs limites de chacun des deux types, en prêtant attention à l’emplacement réservé pour l’affichage d’un long int
. Cette opération nécessite le fichier limits.h
:
//limit.c
#include <stdio.h>
#include <limits.h>
int main( ) {
printf( "Valeur max pour un int : %d\n", INT_MAX);
printf( "Valeur max pour un long int : %ld\n", LONG_MAX);
return 0;
}
Le résultat de l’exécution
$ gcc -std=c99 -Wall -Wextra limit.c -o limit
$ ./limit
Valeur max pour un int : 2147483647
Valeur max pour un long int : 9223372036854775807
Une fois la structure déclarée, l’étape suivante est de l’initialiser, puis de l’afficher.
Initialisation et affichage
Ecrivons une fonction main()
permettant de manipuler cette structure. L’accès aux champs de la structure se fait avec l’opérateur .
:
int main()
{
struct Book book1 = {"Le Seigneur des anneaux", "J.R.R. Tolkien", 2266286269, 18.90};
printf("Book 1 title : %s\n", book1.title);
printf("Book 1 author : %s\n", book1.author);
printf("Book 1 ISBN : %li\n", book1.isbn);
printf("Book 1 price : %.2f €\n", book1.price);
return 0;
}
Remarquer le modificateur .2
utilisé pour l’affichage du prix. Il sert à ne conserver que deux chiffres décimaux lors de l’affichage. C’est sans incidence sur sa représentation en mémoire qui continue à utiliser la pleine résolution.
Exercice
Utiliser les fragments de code ci dessus pour construire le programme exécutable affichant les caractéristiques du livre de Tolkien dans le terminal:
$ gcc -std=c99 -Wall -Wextra book.c -o book
$ ./book
Book title : Le Seigneur des anneaux
Book author : J.R.R. Tolkien
Book ISBN : 2266286269
Book price : 18.90
Exercice
Evaluer la taille d’une instance de la structure Book
ainsi que la taille de chacun de ses champs en intégrant le code ci dessous dans la fonction main()
. %zu
est le modificateur de format pour afficher une taille en octets.
printf("size of Book structure: %zu bytes\n", sizeof(myBook));
printf(" size of title field: %zu bytes\n", sizeof(myBook.title));
printf(" size of author field: %zu bytes\n", sizeof(myBook.author));
printf(" size of ISBN field: %zu bytes\n", sizeof(myBook.isbn));
printf(" size of price field: %zu bytes\n", sizeof(myBook.price));
Les résultats obtenus sont ils cohérents ? Pour expliciter la réponse, il faut s’intéresser aux offsets obtenus avec la fonction offsetof()
de la bibliothèque stddef.h
. Le type size_t
est également défini dans cette bibliothèque. C’est un type entier non signé qui est capable de stocker la taille de n’importe quel objet en octets.
size_t titleOffset = offsetof(struct Book, title);
size_t authorOffset = offsetof(struct Book, author);
size_t isbnOffset = offsetof(struct Book, isbn);
size_t priceOffset = offsetof(struct Book, price);
printf("Offset de title: %zu bytes\n", titleOffset);
printf("Offset de author: %zu bytes\n", authorOffset);
printf("Offset de isbn: %zu bytes\n", isbnOffset);
printf("Offset de price: %zu bytes\n", priceOffset);
Quel est le champ concerné par la mémoire réservée mais non utilisée ?
Utiliser les résultats ci dessus pour répondre au quizz
char title[100]
occupe octets en mémoirechar author[50]
occupe octets en mémoireun
long int
occupe octets en mémoireun
double
occupe octets en mémoirela structure
Book
réserve octets en mémoire pour chaque enregistrementla structure
Book
occupe plus de mémoire que strictement nécessaireil y a octets réservés et non utilisés par le champ
La raison de cette perte de mémoire est que la mémoire est adressée par blocs de 4 octets pour une plus grande efficacité. Ce mécanisme s’appelle l’alignement mémoire.
Structures et fonctions
Une structure est un type de variable au même titre qu’un int
, un double
, un char
, un tableau ou un pointeur. Avec une fonction, on peut donc théoriquement utiliser les structures comme :
argument de la fonction ;
ou valeur de retour de la fonction.
Note
Cependant on se souvient que les arguments sont passés à une fonction par copie de valeurs. Comme pour les tableaux, il est donc extrêmement couteux de passer la structure elle même en argument et il sera préférable d’utiliser plutôt des pointeurs pour localiser celle ci dans la mémoire. Ce point sera abordé dans le prochain paragraphe.
Pour illustrer l’utilisation d’une structure comme argument, écrivons la fonction printBook()
qui :
prend en argument une structure de type
Book
;affiche les champs de la structure ;
et ne retourne rien.
1void printBook(struct Book book)
2{
3 printf("Book title : %s\n", book.title);
4 printf("Book author : %s\n", book.author);
5 printf("Book ISBN : %li\n", book.isbn);
6 printf("Book price : %.2f\n", book.price);
7}
Remarquez la construction structure.champ
utilisée. Lorsqu’on manipule directement une structure, l’accès aux champs de cette structure s’obtient avec l’opérateur .
.
Son appel, depuis la fonction main()
est immédiat :
1int main()
2{
3 struct Book book1 = {"Le Seigneur des anneaux", "J.R.R. Tolkien", 2266286269, 18.90};
4 printBook(book1);
5 return 0;
6}
L’exemple ci dessus montre l’utilisation d’une structure comme argument d’une fonction.
On préfèrera cependant utiliser un pointeur vers la structure plutôt que la structure elle même pour économiser de la mémoire.
Pointeur vers une structure
Comme pour les autres types, on peut définir un pointeur vers une structure avec l’opérateur *
.
Structure en lecture seule
Afin de ne pas gaspiller de mémoire par recopie de valeurs, ré écrivons la fonction printBook()
pour qu’elle prenne en argument un pointeur vers une structure Book
plutôt que la structure elle même :
void printBook(struct Book *book)
{
printf("Book title : %s\n", book->title);
printf("Book author : %s\n", book->author);
printf("Book ISBN : %li\n", book->isbn);
printf("Book price : %.2f\n", book->price);
}
L’accès aux champ d’une structure manipulée avec un pointeur utilise l’opérateur ->
. Ainsi, la construction book->title
est une écriture abrégée, équivalente à (*book).title
.
Modification d’une structure
La fonction printBook()
ci dessus utilise la structure en lecture. Elle ne modifie aucun champ de celle ci. Mais comme un pointeur vers la structure à modifier est passé en argument, on peut également manipuler la structure en écriture, en la modifiant.
Ecrivons une fonction changePrice()
qui:
prend en argument un pointeur vers une structure
Book
et un pourcentage décimal entre 0 et 100% ;modifie le prix conformément au pourcentage ;
et ne retourne rien.
Les consignes ci dessus permettent d’écrire la signature de la fonction. Le code est trivial :
void changePrice(struct Book *book, double percent){
book->price *= 1+percent/100;
}
La fonction est appelée dans main()
de la façon suivante :
struct Book book2 = {"Game Of Thrones, Le trône de fer", "George R.R. Martin", 2290208876, 22.0};
printBook(&book2);
changePrice(&book2, -5);
printBook(&book2);
Avant l’appel:
Book title : Game Of Thrones, Le trône de fer
Book author : George R.R. Martin
Book ISBN : 2290208876
Book price : 22.00
Après l’appel de changePrice()
:
Book title : Game Of Thrones, Le trône de fer
Book author : George R.R. Martin
Book ISBN : 2290208876
Book price : 19.80
Le prix a bien été diminué de 5%.
Exercice
Utiliser les fragments de code ci dessus pour construire le programme exécutable produisant l’affichage suivant:
$ gcc -std=c99 -Wall -Wextra book.c -o book
$ ./book
Book title : Le Seigneur des anneaux
Book author : J.R.R. Tolkien
Book ISBN : 2266286269
Book price : 18.90
Book title : Game Of Thrones, Le trône de fer
Book author : George R.R. Martin
Book ISBN : 2290208876
Book price : 22.00
Baisse de prix !
Book title : Game Of Thrones, Le trône de fer
Book author : George R.R. Martin
Book ISBN : 2290208876
Book price : 20.90
Définition de type
Le code ci dessus est fonctionnel mais présente quelques lourdeurs dans la déclaration des structures qui nécessite deux mots clés :
struct
pour indiquer à C que le type qui va suivre est une structure ;et le nom de la structure proprement dite.
On peut définir un type personnalisé (on dit aussi un alias) avec le mot clé typedef
qui est suivi de deux arguments :
le type pour lequel on veut créer un alias ;
suivi du nom de l’alias.
Note
Si l’utilisation de typedef
est illustrée dans ce qui va suivre avec une structure, ça fonctionne de manière similaire pour tous les types avec la syntaxe:
typedef type alias;
Un alias s’utilise de la façon suivante. On en profite pour manipuler les deux premiers champs comme des pointeurs char*
et non plus comme des tableaux de char
.
typedef struct Book
{
char *title;
char *author;
long int isbn;
double price;
} Book;
Ce code suivant indique au compilateur C que chaque fois que l’on rencontrera Book
il faudra le remplacer par struct Book {...}
. Et donc :
une déclaration
struct Book book
sera remplacée par :une déclaration
Book book
.
On a modifié deux champs de la structure. Qu’en est il maintenant de la taille de celle ci ?
char *title
occupe octets en mémoirechar *author
occupe octets en mémoireun
long int
occupe octets en mémoireun
double
occupe octets en mémoirela structure
Book
réserve donc octets en mémoire pour chaque enregistrement
La taille est bien inférieure à celle de la première version, celle avec les tableaux de char
. Quelle en est la raison ?
les chaines de caractères ont été compressées ;
les chaines de caractères sont stockées en dehors de la structure.
Pour connaitre la longueur d’une chaine de caractère, on peut faire appel à la fonction strlen()
, dont le prototype est déclaré dans <string.h>
. Une version simplifiée:
int strlen(char *str);
Utiliser strlen()
pour connaitre la taille du champ title et author des livres de Tolkien et Martin.
Le champ
title
du livre Le Seigneur des anneaux occupe octets en mémoireLe champ
author
du livre Le Seigneur des anneaux occupe octets en mémoireLe champ
title
du livre Game Of Thrones occupe octets en mémoireLe champ
author
du livre Game Of Thrones occupe octets en mémoire
Le programme peut ainsi être ré écrit comme ci dessous :
// book2.c
#include <stdio.h>
typedef struct Book
{
char *title;
char *author;
long int isbn;
double price;
} Book;
void printBook(Book *book)
{
printf("Book title : %s\n", book->title);
printf("Book author : %s\n", book->author);
printf("Book ISBN : %li\n", book->isbn);
printf("Book price : %.2f\n", book->price);
}
int main()
{
Book book1 = {"Le Seigneur des anneaux", "J.R.R. Tolkien", 2266286269, 18.90};
Book book2 = {"Game Of Thrones, Le trône de fer", "George R.R. Martin", 2290208876, 22.0};
printBook(&book1);
printBook(&book2);
return 0;
}
Tableau de structures
En C on peut manipuler des tableaux :
de types prédéfinis :
int
,double
,char
, etc. ;de pointeurs :
int*
,double*
,char*
, etc. ;mais également de
struct
.
Illustrons ça en définissant une collection de Book
avec un tableau :
typedef Book Books[50];
L’instruction ci dessus définit Books
comme un tableau de 50 Book
.
On peut définir une fonction setBookFields()
pour renseigner un Book
:
void setBookFields(Book* book, char* title, char* author, long int isbn, double price){
book->title = title;
book->author = author;
book->isbn = isbn;
book->price = price;
}
Quelques questions pour une meilleure compréhension de la fonction :
La variable
book
utilisée dans la fonctionsetBookFields()
est de typeBook
La variable
book
utilisée dans la fonctionsetBookFields()
est de typeBook*
La variable
book
utilisée dans la fonctionsetBookFields()
est une structureLa variable
book
utilisée dans la fonctionsetBookFields()
est un pointeur
Utilisons là pour remplir quelques éléments du tableau :
int main()
{
Books books;
setBookFields(&books[0], "Le Seigneur des anneaux", "J.R.R. Tolkien", 2266286269, 18.90);
setBookFields(&books[1], "Game Of Thrones, Le trône de fer", "George R.R. Martin", 2290208876, 22.0);
setBookFields(&books[2], "Le Nom de la rose", "Umberto Eco", 2253033138, 8.90);
return 0;
}
La variable
books
utilisée dans la fonctionmain()
est de typeBooks
La variable
books
utilisée dans la fonctionmain()
est de typeBooks*
La variable
books
utilisée dans la fonctionmain()
est une structureLa variable
books
utilisée dans la fonctionmain()
est un pointeurLa variable
books
utilisée dans la fonctionmain()
est un tableau
L’opérateur d’indexation [ ]
est prioritaire par rapport à l’opérateur d’adressage &
, ce qui nous permet d’écrire :
&books[0]
;plutôt que
&(books[0])
, pour une écriture allégée.
Si l’on souhaite afficher la collection, on peut écrire une fonction printBooks()
qui fait appel à printBook()
:
void printBooks(Books *books, int n){
for (int i=0; i<n; i++){
printBook(*books+i);
printf("\n");
}
}
La variable
books
utilisée dans la fonctionprintBooks()
est de typeBooks
La variable
books
utilisée dans la fonctionprintBooks()
est de typeBooks*
La variable
books
utilisée dans la fonctionprintBooks()
est une structureLa variable
books
utilisée dans la fonctionprintBooks()
est un pointeurLa variable
books
utilisée dans la fonctionprintBooks()
est un pointeur vers une structureLa variable
books
utilisée dans la fonctionprintBooks()
est un pointeur vers un tableauLa variable
*books
utilisée dans la fonctionprintBooks()
est de typeBooks
La variable
*books
utilisée dans la fonctionprintBooks()
est de typeBooks*
La variable
*books
utilisée dans la fonctionprintBooks()
est une structureLa variable
*books
utilisée dans la fonctionprintBooks()
est un pointeurLa variable
*books
utilisée dans la fonctionprintBooks()
est un tableau
L’opérateur d’indirection *
est prioritaire par rapport à l’opérateur d’addition +
, ce qui nous permet d’écrire :
*books+i
;plutôt que
(*books)+i
, pour une écriture allégée.
Le résultat est tel qu’attendu:
Book title : Le Seigneur des anneaux
Book author : J.R.R. Tolkien
Book ISBN : 2266286269
Book price : 18.90
Book title : Game Of Thrones, Le trône de fer
Book author : George R.R. Martin
Book ISBN : 2290208876
Book price : 22.00
Book title : Le Nom de la rose
Book author : Umberto Eco
Book ISBN : 2253033138
Book price : 8.90