Vous pouvez pointer vers presque n’importe quoi en C. Le langage vous permet de créer des pointeurs pour les types primitifs, les tableaux, les fonctions et les types définis par l’utilisateur. Mais les pointeurs vers les structures sont là où les choses deviennent intéressantes. Ils sont extrêmement courants. Si vous écrivez du code C, vous les rencontrerez probablement souvent.
Voici un exemple basique. Il montre comment définir une structure, puis créer un pointeur vers celle-ci.
Cet extrait fait plus que simplement attribuer une adresse mémoire. Il établit une relation entre la variable « ptr » et la structure « p1 ». Le pointeur contient l’adresse de la structure. Il vous permet d’accéder indirectement aux membres « x » et « y ». Il s’agit d’un modèle fondamental. Vous le voyez dans les listes chaînées, l’allocation dynamique de mémoire et les arguments de fonction.
Pourquoi utiliser des pointeurs vers des structures ?
Copier des structures entières coûte cher. Si votre structure contient beaucoup de données, la transmettre par valeur à une fonction signifie que le compilateur en fait une copie complète. Cela fait perdre du temps et de la mémoire. L’utilisation d’un pointeur évite la copie. Vous transmettez l’adresse à la place. La fonction peut modifier les données d’origine sans surcharge.
Se pose également la question du dimensionnement dynamique. Les tableaux ont des tailles fixes au moment de la compilation. Les structures ne résolvent pas nécessairement à elles seules le problème dynamique. Mais lorsque vous combinez une structure avec « malloc », vous obtenez un stockage de données flexible. Vous pouvez allouer une structure sur le tas. Un pointeur vers cette structure vous permet de gérer manuellement sa durée de vie.
Accéder aux membres via des pointeurs
Une fois que vous avez un pointeur, vous devez accéder aux données qu’il contient. Vous ne pouvez pas utiliser l’opérateur point. L’expression ptr.x ne pourra pas être compilée. ptr est une adresse, pas la structure elle-même. Vous devez d’abord le déréférencer.
Il existe deux façons de procéder. Le premier est verbeux.
Les parenthèses sont ici obligatoires. L’opérateur point a une priorité plus élevée que l’opérateur de déréférencement. Sans eux, le compilateur tente d’accéder à « ptr.x », qui n’existe pas.
La deuxième façon est plus propre. C fournit l’opérateur flèche ->. Il combine le déréférencement et l’accès aux membres en une seule étape.
Cette syntaxe est standard. C’est ce que vous verrez dans les vraies bases de code. Cela se lit naturellement comme « allez à l’objet pointé par « ptr » et récupérez son membre « x ». C’est concis. Cela réduit l’encombrement visuel.
Pièges courants
Les pointeurs nuls constituent un risque. Si vous déclarez un pointeur mais ne l’initialisez pas, il pointe vers des déchets. L’accès à ptr->x sur un pointeur nul provoque une erreur de segmentation. Vous devez vérifier null avant d’utiliser le pointeur.
Cette vérification n’est pas facultative dans un code robuste. Cela évite les accidents.
Un autre problème concerne les fuites de mémoire. Si vous allouez de la mémoire avec malloc et perdez le pointeur, cette mémoire reste réservée jusqu’à la fin du programme. Vous devez garder une trace de chaque allocation. free() est votre responsabilité. Oublie ça
Les pointeurs vers des structures en C font souvent trébucher les développeurs qui sont à l’aise avec les pointeurs de base mais qui s’emmêlent dans la syntaxe. Prenons une simple définition d’enregistrement pour un profil utilisateur.
structure typedef {
nom du caractère[21] ;
ville de char[21];
état de caractère[3] ;
} Rec;
typedef Rec *RecPointer;
Ici, RecPointer est juste un alias pour un pointeur vers une structure Rec. Lorsque vous déclarez une variable de ce type, vous avez affaire à une adresse mémoire et non aux données elles-mêmes.
RecPointeur r ;
La variable « r » occupe quatre octets sur un système 32 bits (ou huit sur un système 64 bits). C’est juste un indicateur. Il ne contient pas le nom, la ville ou l’état. Pour stocker des données réelles, vous devez allouer de la mémoire sur le tas.
r = (RecPointer)malloc(sizeof(Rec));
Cet appel malloc réserve 45 octets. Cela représente 21 octets pour le nom, 21 pour la ville et 3 pour l’état, plus un octet pour le remplissage ou l’alignement afin de respecter les limites de la mémoire. Maintenant, « r » pointe vers un bloc de mémoire valide qui se comporte exactement comme une structure « Rec ».
Accès aux membres via déréférencement
Pour interagir avec les données, vous devez déréférencer le pointeur. C’est là que les erreurs de priorité surviennent. Vous pouvez accéder aux membres en utilisant la notation par points standard, mais vous devez mettre le déréférencement entre parenthèses.
strcpy((r).name, “Leigh”);
strcpy(( r).city, “Raleigh”);
strcpy((*r).state, “NC”);
Notez la syntaxe. (*r).name est correct. Si vous écrivez *r.name, la compilation échoue. Pourquoi? Parce que l’opérateur point a une priorité plus élevée que l’opérateur de déréférencement. Le compilateur interprète *r.name comme *(r.name). Puisque r est un pointeur, r.name est une syntaxe invalide et l’expression s’effondre. Les parenthèses forcent le déréférencement « *r » à se produire en premier, donnant la structure, puis l’opérateur point accède au champ « nom ».
C’est fastidieux de taper. Cela a l’air encombré. Cela invite aux erreurs.
La notation des flèches
C fournit une manière plus propre de gérer cela. L’opérateur flèche -> est un sucre syntaxique pour (*pointer).member. Ce n’est pas un mécanisme différent. Ce n’est pas un nouvel opérateur qui change la façon dont la mémoire est accédée. Il s’agit simplement d’un moyen plus court d’écrire le déréférencement et l’accès aux membres en une seule étape.
strcpy(r->nom, “Leigh”);
Ceci est identique à strcpy((*r).name, "Leigh"). Il enregistre deux caractères. Cela supprime le besoin de parenthèses imbriquées. C’est la manière standard dont la plupart des développeurs C interagissent avec les pointeurs de structure.
Implications sur la gestion de la mémoire
L’appel free(r) est obligatoire. La mémoire a été allouée à partir du tas. Si vous ne le relâchez pas, il fuit. Le pointeur « r » lui-même, la variable de quatre octets contenant l’adresse, est local au cadre de pile ou à la portée globale, mais les données vers lesquelles il pointe résident ailleurs.
Lorsque vous utilisez r->name, vous modifiez les données à l’adresse stockée dans r. Le pointeur « r » reste inchangé. L’adresse fait

L’allocation de mémoire aux tableaux à la volée est un élément essentiel de la programmation C, mais cela nécessite de comprendre comment les pointeurs interagissent avec les blocs de mémoire bruts. Lorsque vous avez besoin d’un tableau de taille fixe qui n’est pas connu au moment de la compilation, l’allocation de pile standard ne suffira pas. Il faut atteindre le tas.
L’extrait de code ci-dessous illustre un modèle courant :
Ici, « malloc » réserve de l’espace pour dix entiers. La conversion en (int *) garantit que le pointeur correspond au type attendu, même si les compilateurs C modernes mettent souvent en garde contre les conversions explicites pour void*. La boucle initialise ensuite chaque élément à zéro en utilisant la notation en indice. Enfin, « free » libère la mémoire au système.
Mais ce n’est pas la seule façon de l’écrire.
Vous pouvez obtenir exactement le même résultat en remplaçant « p[i] » par l’arithmétique du pointeur :
Pourquoi est-ce important ? Parce que p[i] n’est qu’un sucre syntaxique pour *(p+i). Le compilateur les traite de la même manière. Si vous travaillez avec des systèmes embarqués ou écrivez des boucles serrées où chaque cycle compte, savoir que ceux-ci sont interchangeables vous aide à lire le code existant et à écrire le vôtre sans confusion.
Pourtant, il existe un piège subtil.
Si vous déclarez directement un pointeur vers un type de tableau, comme int (*p)[10], vous avez affaire à une bête complètement différente. Ce pointeur pointe vers le tableau entier, pas seulement vers le premier élément. L’incrémenter déplace le pointeur de la taille de l’ensemble du tableau, et non d’un seul entier. La plupart des développeurs s’en tiennent à « int * » car c’est plus simple et plus flexible.
Quand utiliser quelle approche
Choisir entre la notation en indice et l’arithmétique explicite des pointeurs se résume souvent à une question de lisibilité et d’intention.
- Utilisez
p[i]lorsque vous souhaitez mettre l’accent sur l’accès basé sur l’index. C’est plus clair pour la plupart des lecteurs. - Utilisez
*(p+i)lorsque vous effectuez une manipulation de mémoire de bas niveau ou que vous devez éviter les bizarreries de dégradation des tableaux dans des expressions complexes.
Les deux approches nécessitent une gestion minutieuse de la mémoire. Oubliez « gratuit », et vous fuiez. Appelez-le trop tôt et vous rencontrerez un comportement indéfini. Et bien que « malloc » soit simple ici, vérifiez toujours que la valeur de retour n’est pas « NULL » avant de déréférencer.
“Les pointeurs vers des tableaux sont puissants, mais ils exigent de la discipline. Un faux pas et vous lisez des ordures ou, pire encore, corrompt la mémoire d’une autre variable. ”
En pratique, la plupart des développeurs ont rarement besoin d’écrire une arithmétique de pointeur brute pour des tableaux simples. Les bibliothèques comme std::vector en C++ ou les abstractions de niveau supérieur dans d’autres langages gèrent cela automatiquement. Mais en C ? Vous êtes seul.
Et c’est pourquoi il est important de comprendre la mécanique. Pas seulement pour passer des entretiens ou écrire des manuels, mais aussi lorsque le code se brise à 2 heures du matin et que vous avez besoin de savoir exactement ce qui se passe en mémoire.

Lorsque vous déclarez un pointeur vers un tableau d’entiers, vous ne créez rien d’exotique. C’est juste un pointeur standard vers un « int ». La magie opère avec malloc. Vous allouez un bloc de mémoire suffisamment grand pour le nombre d’entiers dont vous avez besoin. Le pointeur cible alors le premier élément de ce bloc.
C ne se soucie pas de la façon dont vous y accédez. Vous pouvez utiliser des crochets comme « p[5] » ou utiliser l’arithmétique de pointeur comme « *(p + 5) ». Le compilateur les traite comme identiques. Cette flexibilité explique pourquoi les tableaux dynamiques sont si utiles pour les chaînes. Vous ne devinez pas la taille. Vous allouez exactement suffisamment de stockage pour la longueur de la chaîne plus le terminateur nul.
Tableaux de pointeurs et tableaux de structures
Pourquoi utiliser un tableau de pointeurs alors que vous pourriez simplement utiliser un tableau de structures ? Espace. Ou plutôt son absence.
Considérons une structure Rec avec trois tableaux de caractères de 81 octets chacun. Cela représente 243 octets par enregistrement. Si vous déclarez Rec records[10], vous réservez instantanément 2 430 octets en mémoire. Tout cela. Même si vous n’utilisez qu’un seul enregistrement.
Un tableau de pointeurs modifie les calculs.
Le tableau « a » lui-même ne contient que 10 pointeurs. Sur un système 64 bits, cela représente 80 octets. Cela représente une fraction de la mémoire requise pour les structures complètes. La mémoire des enregistrements réels reste inutilisée jusqu’à ce que vous en ayez besoin.
Vous pouvez attribuer un seul enregistrement à la demande.
Ce modèle résout les problèmes gourmands en mémoire en différant l’allocation. Vous ne payez que ce que vous utilisez. Lorsque vous avez terminé l’enregistrement, vous appelez « gratuitement ». Le pointeur devient une référence pendante, mais la mémoire est renvoyée au système.
Structures contenant des pointeurs
Les structures peuvent contenir des pointeurs. Cela vous permet de mélanger des données de taille fixe avec des données de taille variable dans le même objet.
Prenez une entrée du carnet d’adresses. Le nom, la ville et le numéro de téléphone peuvent avoir des longueurs maximales raisonnables. Un commentaire, cependant, peut aller d’un simple mot à un roman. Vous ne voulez pas perdre d’espace en allouant un énorme tampon pour chaque entrée au cas où une personne écrivait un long commentaire.
La structure Addr elle-même est petite

Comment les champs de commentaires gèrent les enregistrements vides et remplis
Tous les enregistrements de la base de données ne comportent pas de commentaire. Lorsqu’un champ est laissé vide, il ne reste pas vide. Il contient un pointeur. Plus précisément, un pointeur de 4 octets qui ne pointe vers rien de substantiel. Le système traite cette absence comme un état valide. Le dossier est toujours complet. Les métadonnées sont intactes.
Mais que se passe-t-il lorsqu’un utilisateur tape quelque chose ?
La répartition change. La base de données ne réserve pas de tampon fixe pour ces commentaires. Il ne devine pas une longueur maximale et complète le reste avec des octets nuls. Cela gaspillerait de l’espace. Au lieu de cela, le système calcule la longueur exacte de la chaîne. Ensuite, il alloue exactement autant d’octets.
Cette allocation dynamique est efficace. Il évite la fragmentation due aux ballonnements. Une courte note prend quelques octets. Un long essai prend plus de temps. Le pointeur dans l’enregistrement vierge pointe vers une référence nulle, ce qui permet de minimiser l’empreinte. Les enregistrements dont le contenu s’étire pour s’adapter aux données. Rien n’est gaspillé. Rien n’est forcé.
Est-ce la seule façon de stocker du texte ? Non, mais c’est une façon intelligente d’équilibrer la vitesse et l’espace. Vous bénéficiez de la réactivité des en-têtes de taille fixe avec la flexibilité des données de longueur variable. C’est un petit détail. Un mécanicien de bas niveau. Mais cela s’additionne lorsque vous gérez des millions d’enregistrements. La base de données respire plus facilement. L’utilisation du disque reste faible.
Et l’utilisateur ? Ils ne voient jamais le pointeur. Ils voient juste leur commentaire. Ou l’absence d’un. La complexité est cachée. Le stockage est optimisé. Le résultat est un système qui semble léger, même lorsque les données augmentent.
































