Struct

Un struct permet de regrouper plusieurs informations dans un bloc. Par exemple, un point sur l'écran regroupe deux informations : l'abscisse et l'ordonnée.

struct Point
{
    int x;
    int y;
};

Initialisation

Nouveauté ;) C99 ! On peut initialiser en une ligne un struct :

struct Point point = {2, 1};

ou alors en nommant les champs :

struct Point point = {.x = 2, .y = 1};

On peut aussi assigner un struct plus tard mais il faut caster (i.e. mentionner le type entre parenthèses) :

point = (struct Point) {.x = 2, .y = 1};

Accès aux champs

On accède aux champs avec un point (!). Par exemple pour l'abcisse de notre point point on écrit :

point.x

Et pour récupérer son ordonnée :

point.y

Accès aux champs d'un struct pointé

Quand on a un pointeur p vers un struct Point, on a un raccourci d'écriture :

(*p).x

s'écrit

p->x

C'est plus lisible.

Enum

Un enum est un type avec un domaine fini, dont les éléments sont listés. Par exemple, un jour de la semaine est parmi lundi, mardi, mercredi, jeudi, vendredi, samedi ou dimanche :

enum weekday {Mon, Tue, Wed, Thur, Fri, Sat, Sun};

On déclare une variable comme ça, par exemple disant que mon jour favori est le dimanche :

enum weekday favoriteDay = Sun;

Voici un autre exemple de type enum pour les couleurs des cartes :

enum cardsuit {CLUBS, HEARTS, SPADES, DIAMONDS};

Union

Un type union permet de lire/écrire dans la même portion mémoire mais avec des champs différents. Considérons :

union mask
{
    unsigned char n[4];
    unsigned long fulldata;
};

En déclarant

union mask m = {.fulldata = 1025};

on a une variable m sur 4 octets dont les bits sont :

00000000 00000000 00000010 00000001

Avec m.fulldata, on lit 00000000 00000000 00000010 00000001 en entier que l'on interprète comme 1025, alors que m.n[i] permet de lire chaque octets.

  • Que vaut m.n[0] ? On lit 00000000, donc il vaut 0.

Que vaut m.n[1] ? On lit 00000000, donc il vaut 0.

Que vaut m.n[2] ? On lit 00000010, donc il vaut 2.

Que vaut m.n[3] ? On lit 00000001, donc il vaut 1.

Donner un nom à un type

Le mot-clé typedef permet de donner un nom personalisé à un type existant.

typedef struct Point
{
    int x;
    int y;
} tPoint;

Ainsi, au lieu de faire struct Point p;, on peut écrire tout simplement tPoint p;.

typedef unsigned int distance;

distance d = 3;

Bref, l'utilisation de typedef est :

Champs de bits

Alors là, c'est très bas niveau. Dans l'exemple qui suit, on a un point où l'abcisse est codé sur 4 bits, l'ordonnée sur 3 bits, auquel on ajoute 1 bit pour savoir si le point est sélectionné ou non.

typedef struct Point
{
    int x: 4;
    int y: 3;
    int selected: 1;
} point_t;

A priori, vous pouvez oublier les champs de bits ; on ne va pas les utiliser je pense.

Exemples plus compliqués

Vendeur.se de livres, mugs et Tshirt, vous tenez une base de données des objets dans votre magasin. Voici un enum pour le type d'objets (un livre, un mug ou un T-shirt) :

enum itemType {Book, Mug, Shirt};

On pourrait définit un struct pour stocker le prix, le type d'objets puis les informations de l'objet :

#define TITLE_LEN 30
#define AUTHOR_LEN 20
#define DESIGN_LEN 20

struct item_crazy
{
    double price;
    enum itemType type;
    char booktitle[TITLE_LEN + 1];
    int book_num_pages;
    char design[DESIGN_LEN + 1];
    int shirt_color;
    int shirt_size;
};

Mais c'est dommage car il y aura toujours des champs que l'on utilisera pas. Par exemple, pour un livre, on utilisera booktitle et book_num_pages, mais pas design, shirt_color, shirt_size.

La solution est d'utiliser un union pour réutiliser la même portion de mémoire :

struct item
{
    double price;
    enum itemType type;
    union
    {
        struct
        {
            char title[TITLE_LEN + 1];
            int num_pages;
        } book;
        struct
        {
            char design[DESIGN_LEN + 1];
        } mug;
        struct
        {
            char design[DESIGN_LEN + 1];
            int color;
            int size;
        } shirt;
    } item;
};

Pour l'initialisation, on peut faire comme ça (merci le C99) :

struct Item item = {.price = 10.0, .type = Shirt, .item = {.shirt = {.design = "miaou", .color = 0, .size = 2}}};

ou alors affecter chaque champ séparemment :

item.price = 10.0;
item.type = Shirt;
strcpy(item.item.shirt.design, "miaou");
item.item.shirt.color = 0;
item.item.shirt.size = 2;

Switch case

switch case est une construction de conditionnelle sur la valeur d'une expression, ici item.type. On peut donc effectuer différentes actions selon le type d'objet à promouvoir.

switch (item.type)
{
case Book:
    printf("The book %s is available!\n", item.item.book.title);
    break;
case Shirt:
    printf("Shirt with %s of size %d available!\n", item.item.shirt.design, item.item.shirt.size);
    break;
case Mug:
    printf("Mug with %s available!\n", item.item.mug.design);
    break;
default:
    exit(-1);
}

Pourquoi mettre des break ? Car sinon, ça passe au case d'après. C'est pratique car on peut mettre du code commun pour plusieurs cas :

    switch(i) {
        case 0: case 1: dothejob(); break();
        case 2: doanotherthing(); break();
        default: doDefault();
    }

Pour aller plus loin : Rust

Rust est un langage beaucoup plus sûr. On peut y définir directement des enum qui sont aggrémentés des données. On fait ensuite du pattern matching pour effectuer la bonne action selon le type de l'objet.

enum Item {
    Book(f64, String, i64),
    Mug(f64, String),
    Shirt(f64, String, i32, i32)
}

fn main() {
    let item = Item::Shirt(10.0, "miaou".to_string(), 0, 2);

    match item {
        Item::Book(price, title, nb_pages) => &println!("The book {} is available!", title),
        Item::Mug(price, design) => &println!("Mug with {} available!", design),
        Item::Shirt(price, design, color, size) => &println!("Shirt with {} of size {} available!", design, size),
    };
}

En tout cas, le C contrairement au Rust montre mieux comment les objets sont représentés en mémoire. C'est tout l'intérêt pédagogique du C, même si c'est lourdingue à programmer...