🟨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:
- Quelle structure de donnée utiliser pour la “table”?
- Quelle est la définition de l’égalité pour mes Personnes (c’est-à-dire: comment je détermine si ma table contient une certaine Personne).
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?
- Des objets qui offrent des interfaces publiques et disposent d’attributs privés.
- Des objets qui sont responsables des opérations qui les concernent, et peuvent déléguer du travail en envoyant des messages.
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.