Bonjour. Aujourd'hui, on va parler pointeurs et structures en langage C. Nous allons tenter, depuis un pointeur vers le membre d'une structure, de récupérer cette structure.
Introduction
Cas de figure
Nous allons considérer cette structure :
typedef struct s_person {
unsigned long long id;
char const *name;
unsigned int age;
void (*get_info)(struct s_person *p);
} Person;
void get_info(Person *p)
{
printf("Myself ID: %llu (0x%llx)\n", p->id, p->id);
printf("Myself Name: %s\n", p->name);
printf("Myself Age: %u\n", p->age);
printf("Myself Address: %p\n", (void *) p);
}
Ainsi que cette instance :
Person myself = {
.id = (unsigned long long) 0xdeadbeef,
.name = "Malo",
.age = 19,
.get_info = get_info
};
L'objectif
Avec toutes ces informations, nous allons implémenter une fonction appelée get_person_handle_by_name
dont l'objectif est, à partir d'un pointeur vers un member name
d'une instance de type Person
, de récupérer un pointeur vers cette instance.
Pour que ça soit plus claire, voici le prototype de la fonction :
Person *get_person_handle_by_name(char const **name);
name
étant ici une référence à Person.name
par exemple.
Comment c'est possible ?
En utilisant un langage bas niveau, le travail sur la mémoire est rendu plus simple. Concrètement, lorsque vous définissez une structure, une taille lui est associée en fonction de ses membres.
Par exemple, la taille de notre structure s_person
est égale à la taille des membres id
, name
, age
et get_info
, ce qui correspond à 8
+ 8
+ 4
+ 8
= 28
.
Le padding
des structures
Lorsque vous déclarez une structure, le compilateur va calculer la taille qu'elle occupe en mémoire en fonction de ses membres, de la même manière que ce que nous venons de faire.
Cependant, il ne s'arrête pas là, puisque si la taille n'est pas divisible par 8 (système 64 bits) ou par 4 (système 32 bits), il calcule la différence d'espace pour l'ajouter à la taille totale. Cela permet d'aligner la mémoire et simplifie grandement la tâche au processeur qui souhaite accéder à la structure.
Pour observer ce comportement, vous pouvez tester ceci :
typedef struct s_person {
unsigned long long id;
char const *name;
unsigned int age;
void (*get_info)(struct s_person *p);
} Person;
/* Indique au compilateur de ne pas aligner la mémoire */
#pragma pack(1)
typedef struct s_person_packed {
unsigned long long id;
char const *name;
unsigned int age;
void (*get_info)(struct s_person_packed *p);
} PersonPacked;
printf("sizeof(Person): %zu\n", sizeof(Person));
printf("sizeof(PersonPacked): %zu\n", sizeof(PersonPacked));
/*
Sortie :
sizeof(Person): 32
sizeof(PersonPacked): 28
*/
Cette information n'est pas capitale pour ce que nous allons faire, mais il est bon de savoir comment est stockée une structure en mémoire.
Accéder à un membre depuis la structure
Reprennons cette exemple :
Person myself = {
.id = (unsigned long long) 0xdeadbeef,
.name = "Malo",
.age = 19,
.get_info = get_info
};
Si vous affichez l'adresse de myself
, et l'adresse de myself.id
, quelque chose devrait vous choquer :
printf("&myself : %p\n", (void *) &myself);
printf("&myself.id : %p\n", (void *) &myself.id);
/*
Sortie :
&myself : 0x16fb96f40
&myself.id : 0x16fb96f40
*/
En effet, l'adresse de l'instance de la structure, et l'adresse du premier membre sont identiques. Il est donc techniquement possible de faire ça :
printf("Ox%llx\n", *((unsigned long long *) (&myself)));
/*
Sortie :
0xdeadbeef
*/
Ou encore ça :
void (*_get_info)(Person) = (void *) * (void **) ((void *) &myself + 24ULL);
_get_info(myself);
/*
Sortie :
Myself ID: 3735928559 (0xdeadbeef)
Myself Name: Malo
Myself Age: 19
Myself Address: 0x16ae9af00
*/
Eh oui, on peut aussi accéder aux fonctions. Notez bien les conversions en void *
et void **
, cela permet d'ajouter 24ULL
à l'adresse de myself
puisque la fonction membre get_info
se situe à &myself + 24ULL
.
Le soucis est que si on essaie d'ajouter 24ULL
sans passer par ces conversions, c'est en réalité 24 * 8
qui sera ajouté.
Vous aurez une petite surprise si vous essayé d'exécuter une fonction depuis une mauvaise adresse.
Accéder à la structure depuis un membre
Maintenant que vous avez tous les concepts en tête, on peut commencer à attaquer le vif du sujet.
Souvenez vous de cette fonction :
Person *get_person_handle_by_name(char const **name);
Son premier paramètre est un pointeur vers le member name
d'une instance de type Person
. Voici l'algorithme qui sera à mettre en place :
- Déterminer la différence entre l'adresse du membre
name
et celle du début (ou du premier élément) d'une instancePerson
. - Utiliser cette différence pour la soustraire à l'adresse du paramètre
name
de la fonction, pour retrouver l'instance associée. - Retourner le pointeur de l'instance.
/* Uniquement pour avoir un code plus court. */
typedef unsigned long long ull;
Person *get_person_handle_by_name(char const **name)
{
/* Création d'une instance quelconque */
Person handle;
/* On détermine la différence entre les deux adresses : */
ull offset = (ull *) &handle.name - (ull *) &handle;
/* On soustrait la différence pour obtenir le parent de `name`. */
return (Person *) (name - offset);
}
Et voilà ! C'est tout. Rien de bien méchant. Vous pouvez maintenant tester ce code :
#include <stdio.h>
typedef struct s_person {
unsigned long long id;
char const *name;
unsigned int age;
void (*get_info)(struct s_person *p);
} Person;
void get_info(Person *p)
{
printf("Myself ID: %llu (0x%llx)\n", p->id, p->id);
printf("Myself Name: %s\n", p->name);
printf("Myself Age: %u\n", p->age);
printf("Myself Address: %p\n", (void *) p);
}
typedef unsigned long long ull;
Person *get_person_handle_by_name(char const **name)
{
Person handle;
ull offset = (ull *) &handle.name - (ull *) &handle;
return (Person *) (name - offset);
}
int main(void)
{
Person myself = {
.id = (unsigned long long) 0xdeadbeef,
.name = "Malo",
.age = 19,
.get_info = get_info
};
Person *handle = get_person_handle_by_name(&myself.name);
printf("------ Direct Reference ------\n");
myself.get_info(&myself);
printf("------------------------------\n");
printf("\n");
printf("------ Handle Reference ------\n");
handle->get_info(handle);
printf("------------------------------\n");
return 0;
}
Voici une sortie que vous devriez obtenir :
------ Direct Reference ------
Myself ID: 3735928559 (0xdeadbeef)
Myself Name: Malo
Myself Age: 19
Myself Address: 0x16b4fef40
------------------------------
------ Handle Reference ------
Myself ID: 3735928559 (0xdeadbeef)
Myself Name: Malo
Myself Age: 19
Myself Address: 0x16b4fef40
------------------------------
Même si les adresses ne sont pas les mêmes que les miennes, elles doivent être identiques entre Direct Reference
et Handle Reference
.
Conclusion
Comprendre comment est gérée la mémoire en C est très important parce que vous pouvez faire ce que vous voulez. Ça peut sembler incroyable ce que nous venons de réaliser ici, pourtant il ne s'agit que d'une simple soustraction d'adresses.
J'espère que le contenu vous aura plus et que vous aurez appris quelque chose.
Top comments (0)