Menu

Pointeur sur structure en C : l'opérateur flèche et les structures sur le tas

Comment pointer sur une structure en C - l'opérateur flèche, pourquoi (*p).x a besoin de ses parenthèses, passer des structures par pointeur pour les modifier ou éviter les copies, allouer une structure avec malloc, et construire un nœud de liste chaînée.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Une structure passée à une fonction est copiée. C'est très bien pour un point à deux entiers et coûteux pour un enregistrement de 200 octets - et cela rend la modification impossible, car la fonction ne voit jamais que sa propre copie. Pointer sur une structure résout les deux problèmes d'un coup, et c'est aussi le seul moyen de construire des structures qui grandissent : listes, arbres, graphes.

Prendre un pointeur sur une structure

Rien de spécial n'est nécessaire. & donne l'adresse, et le type est « pointeur sur structure » :

Les trois affichent 3. Les deux derniers sont la même opération écrite de deux façons.

Pourquoi (*ptr).x a besoin de ses parenthèses

Vous pourriez être tenté de les retirer et d'écrire *ptr.x. Cela compile en quelque chose de très différent, ou plus souvent refuse de compiler - parce que l'opérateur de membre . a une précédence plus forte que le déréférencement * :

*ptr.x        // s'analyse comme *(ptr.x)  -- faux
(*ptr).x      // dereferencer d'abord, puis prendre le membre -- correct
ptr->x        // la meme chose, dite brievement

ptr.x demande un membre du pointeur, et un pointeur n'a pas de membres. La plainte du compilateur (« request for member 'x' in something not a structure or union ») est un bug de précédence déguisé.

Parce que cette forme parenthésée est laide et facile à rater, le C fournit ->. En pratique, vous ne verrez presque jamais (*p).x dans du vrai code ; prenez p->x et oubliez que la forme longue existe, sauf comme explication de ce que la flèche signifie.

Passer une structure par pointeur

Un paramètre pointeur donne à la fonction la vraie structure de l'appelant, donc les changements persistent :

Deux habitudes à prendre ici :

  • Marquez les pointeurs en lecture seule const. const struct Point *p promet que la fonction ne changera pas ce que p pointe. Si quelqu'un ajoute plus tard p->x = 0 à l'intérieur, la compilation échoue au lieu que le bug parte en production. Cela documente aussi l'intention au point d'appel.
  • Passez un pointeur pour les grosses structures même en lecture. Une structure avec un tampon de nom de 64 octets coûte 64 octets de copie par appel par valeur ; un pointeur en coûte 8.

Les petites structures restent très bien à passer par valeur - struct Point à 8 octets se copie aussi peu cher qu'un pointeur, et la version par valeur ne peut pas être NULL, ce qui supprime tout un mode de défaillance.

Des pointeurs dans des tableaux de structures

L'arithmétique de pointeurs fonctionne sur les tableaux de structures exactement comme sur les tableaux d'int : p + 1 avance d'une structure entière, remplissage compris.

Un nom de tableau se dégrade en pointeur sur son premier élément, donc staff est déjà un struct Employee * à l'appel. C'est aussi pourquoi la fonction a besoin que n soit passé séparément - sizeof dans la fonction mesurerait le pointeur, pas le tableau.

Allouer une structure sur le tas

Les structures de pile meurent à la fin de leur portée. Pour en créer une qui survive à la fonction qui l'a créée - ou pour en créer autant que l'entrée l'exige - allouez avec malloc :

Quatre détails de cette petite fonction portent tout le poids :

  • sizeof *e, pas sizeof(struct Employee). Cela se lit « la taille de ce que e pointe », donc si le type change un jour, l'allocation suit automatiquement. Il n'y a aucun moyen pour les deux de diverger.
  • Vérifiez NULL. malloc renvoie NULL quand il ne peut pas satisfaire la demande. Écrire e->id à travers un pointeur nul est une erreur de segmentation.
  • Initialisez chaque membre. malloc ne met pas la mémoire à zéro ; la structure arrive pleine de garbage. (calloc(1, sizeof *e) la met à zéro pour vous.)
  • Quelqu'un doit la free. Renvoyer un pointeur alloué transfère ce devoir à l'appelant. Dites-le dans un commentaire - une règle de propriété floue est ainsi que commencent les fuites de mémoire.

Le bénéfice : un nœud de liste chaînée

Voici ce que les structures ne peuvent pas faire sans pointeurs. Une structure ne peut pas se contenir elle-même - elle serait infiniment grande - mais elle peut contenir un pointeur sur une structure de son propre type, et cette seule astuce construit toutes les structures de données chaînées en C.

Notez que struct Node *next; fait référence à struct Node pendant que struct Node est encore en cours de déclaration. C'est légal précisément parce qu'un pointeur a une taille connue quelle que soit sa cible - le compilateur n'a pas encore besoin de la définition complète. C'est aussi le seul endroit où vous devez utiliser l'étiquette struct Node même si vous avez aussi écrit un typedef : le nom du typedef n'existe pas encore à l'intérieur de sa propre définition.

La boucle free_list sauvegarde head->next avant d'appeler free(head). Lire head->next après la libération est une utilisation après libération - la mémoire n'est plus à vous, et la valeur lue est ce que l'allocateur y a mis.

Erreurs courantes

  • p->x sur un pointeur non initialisé ou NULL. La cause la plus courante de plantage dans du code riche en structures. Initialisez les pointeurs à NULL et vérifiez avant de déréférencer.
  • Renvoyer un pointeur vers une structure locale. struct Point *bad(void) { struct Point p = {1,2}; return &p; } renvoie l'adresse d'une mémoire qui cesse d'exister au retour de la fonction. Renvoyez la structure par valeur, ou allouez-la.
  • Libérer deux fois, ou oublier de libérer. Chaque malloc s'apparie avec exactement un free. Après libération, mettez le pointeur à NULL pour qu'un usage accidentel ultérieur plante bruyamment au lieu de corrompre la mémoire en silence.
  • Copier une structure contenant un pointeur. L'affectation de structure copie la valeur du pointeur, pas ce qu'il pointe. Les deux structures référencent maintenant le même tampon, et celle qui le libère en premier laisse l'autre pendouiller.

Questions fréquentes

Que fait l'opérateur -> en C ?

p->x accède au membre x via le pointeur p. C'est exactement équivalent à (*p).x - déréférencer le pointeur, puis prendre le membre. La flèche existe parce que les pointeurs sur structures sont si courants que la forme longue encombre le code.

Pourquoi *p.x ne fonctionne-t-il pas pour un pointeur de structure ?

Parce que . s'attache plus fort que *. *p.x s'analyse comme *(p.x) - prendre le membre x de p (qui est un pointeur, pas une structure) et le déréférencer. Il vous faut (*p).x, ou simplement écrire p->x.

Faut-il passer une structure par valeur ou par pointeur en C ?

Passez par pointeur quand la fonction doit modifier la structure de l'appelant, ou quand la structure est grande et que la copier serait du gaspillage. Passez par valeur pour de petites structures en lecture seule, où la copie est peu coûteuse et le code plus simple. Marquez les paramètres pointeurs en lecture seule const struct T * pour que le compilateur fasse respecter la promesse.

Comment alloue-t-on une structure avec malloc en C ?

struct Node *n = malloc(sizeof *n); - la forme sizeof *n demande la taille de ce que n pointe, elle reste donc correcte si vous changez le type plus tard. Vérifiez le résultat contre NULL, initialisez chaque membre, et faites free(n) quand vous avez fini.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER