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:

  1. Les directives include et define ;

  2. les variables globales ;

  3. les fonctions secondaires ;

  4. 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émoire

  • char author[50] occupe octets en mémoire

  • un long int occupe octets en mémoire

  • un double occupe octets en mémoire

  • la structure Book réserve octets en mémoire pour chaque enregistrement

  • la structure Book occupe plus de mémoire que strictement nécessaire

  • il 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émoire

  • char *author occupe octets en mémoire

  • un long int occupe octets en mémoire

  • un double occupe octets en mémoire

  • la 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émoire

  • Le champ author du livre Le Seigneur des anneaux occupe octets en mémoire

  • Le champ title du livre Game Of Thrones occupe octets en mémoire

  • Le 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 fonction setBookFields() est de type Book

  • La variable book utilisée dans la fonction setBookFields() est de type Book*

  • La variable book utilisée dans la fonction setBookFields() est une structure

  • La variable book utilisée dans la fonction setBookFields() 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 fonction main() est de type Books

  • La variable books utilisée dans la fonction main() est de type Books*

  • La variable books utilisée dans la fonction main() est une structure

  • La variable books utilisée dans la fonction main() est un pointeur

  • La variable books utilisée dans la fonction main() 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 fonction printBooks() est de type Books

  • La variable books utilisée dans la fonction printBooks() est de type Books*

  • La variable books utilisée dans la fonction printBooks() est une structure

  • La variable books utilisée dans la fonction printBooks() est un pointeur

  • La variable books utilisée dans la fonction printBooks() est un pointeur vers une structure

  • La variable books utilisée dans la fonction printBooks() est un pointeur vers un tableau

  • La variable *books utilisée dans la fonction printBooks() est de type Books

  • La variable *books utilisée dans la fonction printBooks() est de type Books*

  • La variable *books utilisée dans la fonction printBooks() est une structure

  • La variable *books utilisée dans la fonction printBooks() est un pointeur

  • La variable *books utilisée dans la fonction printBooks() 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