1. Arithmétique des pointeurs et conversions de types entiers
En C, il est possible de convertir explicitement une valeur entière en un pointeur et vice versa. Cependant, l'arithmétique appliquée diffère selon que le compilateur traite la donnée comme un entier pur ou comme une adresse mémoire associée à un type spécifique.
#include <stdio.h>
void demo_arithmetique() {
int *adresse_virtuelle = (int*)2000;
// Conversion en entier puis addition simple
printf("Résultat entier : %lu\n", (unsigned long)adresse_virtuelle + 1);
// Arithmétique de pointeur : l'adresse augmente de 1 * sizeof(int)
printf("Résultat pointeur : %lu\n", (unsigned long)(adresse_virtuelle + 1));
}
Dans cet exemple, si sizeof(int) est de 4 octets, la première opération retournera 2001, tandis que la seconde retournera 2004. L'arithmétique des pointeurs prend toujours en compte la taille du type pointé.
2. Manipulation d'octets et accès direct à la mémoire
L'utilisation de pointeurs de types différents sur une même zone mémoire permet d'observer la représentation binaire des données, notamment l'ordre des octets (Endianness).
#include <stdio.h>
void analyse_memoire() {
int cible = 10;
int *ptr_int = &cible;
// On décale l'adresse d'un octet via un cast en char*
char *ptr_byte = (char*)ptr_int + 1;
printf("Adresse initiale : %p, Adresse décalée : %p\n", ptr_int, ptr_byte);
// Modification partielle de l'entier d'origine
*ptr_byte = 1;
printf("Nouvelle valeur de cible : %d\n", cible);
}
Ici, en modifiant un seul octet à l'intérieur de l'espace mémoire alloué à l'entier cible, la valeur finale change drastiquement. Sur une architecture Little-Endian, l'écriture de la valeur 1 dans le second octet transforme la représentation binaire, illustrant comment les pointeurs peuvent corrompre ou modifier précisément des structures de données.
3. Relation entre tableaux et pointeurs
Bien que souvent confondus, les tableaux et les pointeurs ont des comportements distincts, notamment lors de l'utilisaiton de l'opérateur sizeof ou lors du passage en paramètre de fonction.
#include <stdio.h>
#include <stdlib.h>
void verifier_taille(int tableau[]) {
// Ici, tableau est traité comme un pointeur (decay)
printf("Taille en paramètre : %zu\n", sizeof(tableau));
}
void manipulation_tableaux() {
int nombres[5] = {10, 20, 30, 40, 50};
int *p = nombres;
// Accès exotique mais valide : l'indice est commutable
// nombres[2] == *(nombres + 2) == *(2 + nombres) == 2[nombres]
printf("Valeur via 2[p] : %d\n", 2[p]);
printf("Taille réelle du tableau : %zu\n", sizeof(nombres));
verifier_taille(nombres);
}
Points clés à retenir :
sizeof(tableau)renvoie la taille totale occupée en mémoire (ex: 5 * 4 = 20 octets).sizeof(pointeur)renvoie la taille de l'adresse (généralement 4 ou 8 octets selon l'architecture).- Lorsqu'un tableau est passé à une fonction, il "dégrade" (decay) en un simple pointeur vers son premier élément.
4. Gestion de la durée de vie : Pile (Stack) vs Tas (Heap)
Une erreur classique consiste à retourner l'adresse d'une variable locale à une fonction. Ces variables résident sur la pile et sont détruites à la fin de l'exécution du bloc.
#include <string.h>
#include <stdlib.h>
// ERREUR : Retourne l'adresse d'une variable locale
char* tentative_incorrecte(char* str) {
char tampon[100];
strcpy(tampon, str);
return tampon; // Danger : la mémoire sera libérée immédiatement
}
// CORRECT : Allocation sur le tas
char* solution_valide(char* str) {
size_t len = strlen(str);
char *espace = (char*)malloc(len + 1);
if (espace != NULL) {
strcpy(espace, str);
}
return espace; // La mémoire persiste jusqu'au free()
}
L'utilisation de malloc permet de réserver de la mémoire sur le tas, dont la durée de vie est contrôlée manuellement par le développeur. Cela garantit que les données restent accessibles même après la sortie de la fonction ayant effectué l'allocation.