clavier ouvert

Blog d'enseignement d'Adrien Foucart.
Toutes les opinions présentées ici n'engagent que moi. Blog garanti sans pub, sans traqueurs, et 100% rédigé par un humain.

🟨2026.04.03
C Orienté Objet | 2

Nous avons lors du premier épisode créé une “classe” Person, mais elle se sent bien seule. Les objets sont fait pour communiquer, avoir des relations, s’envoyer des messages. Pour l’instant, nous n’avons rien de tout ça. Commençons.

Une table de personnes

Je voudrais avoir un tableau de personnes, avec la possibilité d’y ajouter et d’en retirer des éléments, de supprimer ou récupérer des éléments par leur index, de savoir si la table contient une certaine personne, et d’en afficher le contenu. On a du boulot. Commençons par écrire personsTable.h pour définir tout ça proprement:

#include <stdbool.h>
#include "person.h"

typedef struct PersonsTable PersonsTable;

PersonsTable* PersonsTable_make();
void PersonsTable_destroy(PersonsTable* t);
void PersonsTable_add_person(PersonsTable* t, Person* p);
bool PersonsTable_remove(PersonsTable* t, int idx);
Person* PersonsTable_get(PersonsTable* t, int idx);
bool PersonsTable_contains(PersonsTable* t, Person* p);
void PersonsTable_print(PersonsTable* t);

Il y a de nombreuses façons d’implémenter ces fonctionnalités. L’idée est donc ici que les utilisateurs de ma “classe” ne devront pas s’en préoccuper. Et si je veux changer, ça ne devrait pas les affecter.

En particulier, j’ai deux grands choix à faire ici:

Cette seconde question va me faire évoluer la classe Person. Commençons par là.

Égalité des personnes

Je dois rajouter une méthode dans person.h:

bool Person_equals(Person* p, Person* other);

Et évidemment l’implémenter dans person.c. Je peux utiliser strcmp pour comparer des chaînes de caractères. Je prendrai donc la définition de l’égalité ici comme: “a le même nom et le même prénom”. J’aurais pu à la place choisir de vérifier si l’adresse des deux objets en mémoire était la même, mais ce serait fort limitant. Évidemment, dans un vrai système, j’aurais besoin de quelque chose de plus “identifiant” que le nom et le prénom, mais on va rester simple pour l’instant. strcmp compare deux chaînes de caractères (“null-terminated”, c’est-à-dire qu’il y a un caractère nul \0 pour marquer la fin), et renvoie 0 si tous les caractères sont les mêmes.

bool Person_equals(Person* p, Person* other) {
    return (strcmp(p->first_name, other->first_name)==0 && 
            strcmp(p->last_name, other->last_name)==0);
}

Testons dans le main:

Person* p = Person_make("Adrien", "Foucart");
Person* other = Person_make("Adrien", "Foucart");
Person_print(p);
Person_print(other);
if (Person_equals(p, other)){
    printf("Persons are equal.\n");
}
else {
    printf("Persons are not equal.\n");
}
Person_destroy(p);
Person_destroy(other);

Je reçois bien Persons are equal. Et si je change une lettre à mon nom, j’ai bien unequal. Tout va bien! Revenons à notre table.

Tableau redimensionnable

Une manière classique de réaliser une table est le principe du “tableau redimensionnable”. Il implique de garder en parallèle une “taille physique” (espace mémoire occupé par la table) et une “taille logique” (nombre d’éléments dans la table). Lorsque la taille logique rattrape la taille physique, on agrandit la table (généralement en la doublant). On traduit ça dans notre struct PersonsTable:

// personsTable.c
struct PersonsTable {
    Person** table;
    int capacity;
    int size;
};

Le constructeur devra donc donner une capacité initiale à la table, et une taille logique (size) de 0. Notons le “double pointeur” pour table. Schématiquement, j’ai:

[table] --> Addr: 0x1234 (exemple)
[0x1234] --> [Person a]
[0x1235] --> [Person b]
[0x1236] --> [Person c]

table pointe vers l’adresse du premier élément du tableau, qui se trouve quelque part dans la mémoire du programme. Ce premier élément est lui-même un pointeur vers une Person. L’élément suivant dans la mémoire sera le pointeur vers la personne suivante, etc. Au niveau du constructeur, j’aurai alors:

PersonsTable* PersonsTable_make(){
    PersonsTable* pt = malloc(sizeof(PersonsTable));
    pt->size = 0;
    pt->capacity = 16;
    pt->table = malloc(sizeof(Person*)*pt->capacity);
    return pt;
}

J’ai deux malloc dans le constructeur, j’aurai donc besoin de deux free dans le destructeur:

void PersonsTable_destroy(PersonsTable* t) {
    free(t->table);
    free(t);
}

Notons un choix fait ici: je ne libère pas les Person. Elles n’ont pas été construites par la classe table, donc elles peuvent exister ailleurs dans le programme. Ce n’est pas la responsabilité de PersonsTable de s’en préoccuper.

Quand on ajoute une personne, on doit d’abord vérifier si on ne doit pas allonger la table (avec realloc). Et il ne faut pas oublier d’incrémenter la taille.

void PersonsTable_add_person(PersonsTable* t, Person* p) {
    if (t->size == t->capacity){
        t->capacity *= 2;
        t->table = realloc(t->table, t->capacity);
    }
    t->table[t->size++] = p;
}

Pour retirer une personne, il faut s’assurer que l’index est valide. On retire une personne en décalant les éléments suivants vers la gauche. On renvoie un booléen pour indiquer si l’opération a réussit. Et on n’oublie pas de décrémenter la taille.

bool PersonsTable_remove(PersonsTable* t, int idx) {
    if (idx < 0 || idx >= t->size)
        return false;

    for (int i = idx; i < t->size - 1; i++)
        t->table[i] = t->table[i+1];

    t->size--;
    return true;
}

Récupérer un élément est assez simple. On renvoie NULL si l’index n’est pas valide:

Person* PersonsTable_get(PersonsTable* t, int idx) {
    if (idx < 0 || idx >= t->size)
        return NULL;

    return t->table[idx];
}

Pour savoir si notre table contient un élément, on n’a pas trop le choix: il faut les parcourir tous. Je ne prétends pas ici à une grande efficacité!

bool PersonsTable_contains(PersonsTable* t, Person* p) {
    for (int i = 0; i < t->size; i++){
        if (Person_equals(p, t->table[i]))
            return true;
    }

    return false;
}

Et finalement on veut pouvoir les afficher:

void PersonsTable_print(PersonsTable* t) {
    printf("[PersonsTable]:\n");
    for (int i=0; i < t->size; i++){
        printf("\t");
        Person_print(t->table[i]);
    }
}

Important ici: pour ces deux dernière méthodes, nous avons fait appel aux méthodes de Person. Des objets qui s’envoient des messages, comme il faut!

Reste à tester que ça fonctionne.


int main(){
    printf("Start program\n");

    // Create test persons
    Person* p = Person_make("Adrien", "Foucart");
    Person* other = Person_make("Leonardo", "Da Vinci");

    // Create and fill table
    PersonsTable* pt = PersonsTable_make();
    PersonsTable_add_person(pt, p);
    PersonsTable_add_person(pt, other);

    PersonsTable_print(pt);
    
    // check contains method
    if (PersonsTable_contains(pt, p)){
        printf("Table contains person: ");
        Person_print(p);
    }

    // check get method
    Person* found = PersonsTable_get(pt, 1);
    Person_print(found);

    // check remove
    PersonsTable_remove(pt, 0);
    PersonsTable_print(pt);
    
    if (!PersonsTable_contains(pt, p)){
        printf("Table does not contain person: ");
        Person_print(p);
    }

    PersonsTable_destroy(pt);
    Person_destroy(p);
    Person_destroy(other);

    printf("End program\n");
    return 0;
}

Ce qui me donne:

[PersonsTable]:
        [Person]: Adrien Foucart
        [Person]: Leonardo Da Vinci
Table contains person: [Person]: Adrien Foucart
[Person]: Leonardo Da Vinci
[PersonsTable]:
        [Person]: Leonardo Da Vinci
Table does not contain person: [Person]: Adrien Foucart
End program

Un peu de recul

Qu’avons-nous jusqu’à présent?

C’est déjà pas mal, pour une poignée de lignes de code!

Mais j’ai l’impression qu’on peut aller un peu plus loin. Une prochaine fois.

Commentaires, remarques, erreurs qu'il faut absolument me faire remarquer? Contactez-moi sur Mastodon ou par mail (adrien@adfoucart.be)