Unicorn with delicious cookie
Nous utilisons des cookies pour améliorer votre expérience de navigation. En savoir plus
Accepter
to the top
>
>
>
Doit-on vérifier le pointeur pour NULL …

Doit-on vérifier le pointeur pour NULL avant d'appeler la fonction free ?

06 Fév 2024
Author:

La réponse brève est non. Cependant, cette question apparaît régulièrement sur Reddit, Stack Overflow et d'autres sites web, il est temps d'aborder le sujet. Il y a beaucoup de choses prenantes à réfléchir.

Fonction free

La fonction free est déclarée dans le fichier d'en-tête <stdlib.h> comme suit :

void free( void *ptr );

La fonction libère la mémoire tampon avant alloué avec les fonctions malloc, calloc, realloc et aligned_alloc.

Si ptr est un pointeur NULL, la fonction ne fait rien.

Il n'est donc pas vérifier le pointeur avant d'appeler free.

if (ptr)     // a redundant check
  free(ptr);

Ce code est obsolète, car la vérification n'est d'aucune utilité. Si le pointeur est NULL, on peut le passer à la fonction free en toute sécurité. Les développeurs de la norme du C délibérément choisi cette solution :

cppreference.com : free

La fonction accepte (et ne fait rien avec) le pointeur null afin de réduire le nombre de cas particuliers.

Si le pointeur est non NULL, mais toujours invalide, la vérification ne protège de rien. Un pointeur non NULL invalide est toujours passé à la fonction free, ce qui cause un comportement indéfini.

cppreference.com : free

Le comportement est indéfini si la valeur de ptr n'est pas égale à une valeur renvoyée précédemment par malloc(), calloc(), realloc(), ou aligned_alloc() (depuis C11).

En outre, le comportement est indéfini si la zone de mémoire désigné par ptr a déjà été libérée, c'est-à-dire, free(), free_sized(), free_aligned_sized(), ou realloc() a déjà été appelée avec comme argument ptr et aucun appel à malloc(), calloc(), realloc(), ou aligned_alloc() abouti à un pointeur égal à ptr après.

Par conséquent, on peut et on doit écrire simplement :

free(ptr);

D'où viennent les questions de la vérification préliminaire des pointeurs ?

La documentation de la fonction free indique explicitement qu'on peut lui passer un pointeur NULL et qu'il ne pose aucun risque. Cependant, les discussions sur ce sujet continuent d'apparaître sur différents sites web. Les questions se divisent en deux catégories.

Les questions des développeurs débutants. Ce type de questions est le plus courant. C'est simple : les gens commencent à apprendre la programmation, ne sont pas conscientes quand il faut vérifier un pointeur et quand il ne faut pas le faire. Pour les jeunes développeurs, une simple explication suffit. Lorsque vous allouez de la mémoire avec malloc, vérifiez le pointeur. Sinon, le déréférencement d'un pointeur NULL peut causer un comportement indéfini. Avant de libérer la mémoire (en utilisant de free), on n'a pas besoin de vérifier le pointeur, car la fonction le fera elle-même.

Voilà, c'est tout. Un autre bon point pour un développeur débutant est d'utiliser un analyseur en ligne pour comprendre plus rapidement ce qui ne va pas dans le code.

Les questions posées par des développeurs expérimentés et trop tatillons. C'est là que les choses deviennent intéressantes. Cette catégorie de personnes sait ce que la documentation contient. Cependant, ils posent toujours les questions parce qu'ils ne sont pas sûrs que l'appel à free(NULL) est toujours sûr. Ils s'inquiètent que leur code soit compilé sur de très vieux systèmes, où free ne garantit pas une gestion sûre des pointeurs NULL. Ou une bibliothèque tierce particulière qui implémente free d'une manière non standard (en ne vérifiant pas pour NULL) peut être utilisée.

On peut discuter de la théorie. En réalité, cela n'a pas de sens.

Commençons par les anciens systèmes. Tout d'abord, il n'est pas facile de trouver un tel système. La première norme C89 montre que la fonction free doit bien gérer NULL.

C89 : 4.10.3.2 The free function.

La fonction free permet de désallouer l'espace pointé par ptr et le rendre disponible pour une nouvelle allocation. Si ptr est un pointeur NULL, aucune action n'a lieu.

Deuxièmement, si vous voyez un système " pré-norm ", on ne pourra pas créer votre appli pour diverses raisons. Je doute également qu'on n'ait jamais besoin de le faire. Cette issue semble farfelue.

Maintenant, imaginons que le système ne soit pas préhistorique, mais spécial. La bibliothèque tierce inhabituelle de fonctions système qui implémente la fonction free à sa manière : on ne peut pas passer NULL à la fonction.

Dans ce cas, la fonction free brisée n'est pas votre plus grosse issue. Si l'une des fonctions de base du langage est brisée dans la bibliothèque, il y a beaucoup d'autres choses brisées que la dernière chose dont vous devez vous préoccuper est un appel sûr pour free.

C'est comme être dans une voiture artisanale sans freins, sans rétroviseurs et avec un volant bloqué. Mais vous vous inquiétez de savoir si les bornes sont bien connectées à la batterie. Les bornes sont importantes, mais l'issue ne vient pas d'eux, mais de la situation dans son ensemble.

La question de la vérification préliminaire des pointeurs est parfois abordée sous l'angle de la micro-optimisation du code : " Vous pouvez éviter d'appeler la fonction free si vous vérifiez le pointeur vous-même ". C'est là que le perfectionnisme se retourne contre vous. On examine cette idée plus en détail ci-dessous.

Macro du malaise

La chose la plus inutile et potentiellement dangereuse que vous puissiez faire est d'implémenter le contrôle des pointeurs à l'aide d'une telle macro :

#define SAFE_FREE(ptr) if (ptr) free(ptr)

Notre équipe a même une question d'entretien : " Qu'est-ce qui ne va pas avec cette macro ? " Tout y est mauvais. Il semble qu'une telle macro ne devrait pas exister dans la réalité, mais nous l'avons rencontrée dans des projets. Démontons-le un par un.

Tout d'abord, cette macro est une entité redondante.

Comme je vous disais, la fonction free gère bien les pointeurs NULL, et cette vérification ne fournit aucune sécurité supplémentaire lorsque l'on travaille avec des pointeurs.

Deuxièmement, cette macro est également redondante en cas de micro-optimisation.

J'ai lu dans les commentaires que la vérification supplémentaire optimise un peu le code, puisque le compilateur n'a pas à faire un appel coûteux à la fonction free. Je pense qu'il s'agit d'un non-sens et non d'une optimisation.

Le coût de l'appel de fonction est exagéré. Dans tous les cas, ce n'est rien comparé à des opérations consommant des ressources telles que l'allocation et la désallocation de mémoires tampons. En termes d'optimisation, vous devriez vous efforcer de réduire le nombre d'opérations d'allocation de mémoire au lieu de vérifier avant d'appeler la fonction free.

En programmation, le scénario standard est d'allouer la mémoire et de la libérer à nouveau. Les pointeurs NULL passés à free sont très probablement des cas spéciaux, rares et non standard. On n'a pas besoin de les " optimiser ". Une vérification supplémentaire serait très sûrement une pessimisation. En effet, deux vérifications sont effectuées au lieu d'un seul avant que la mémoire ne soit libérée. C'est possible que le compilateur l'optimise, mais ce cas, je ne comprends pas pourquoi on fait tant d'histoires. Au fait, puisque nous parlons d'optimisations, l'optimisation manuelle en utilisant de macros semble naïve et inutile. C'est mieux d'écrire un code simple et compréhensible plutôt que d'essayer de faire des micro-optimisations qu'un compilateur fait mieux qu'un humain.

Je pense que cette volonté d'optimisation superflue confirme la célèbre déclaration de Donald Knuth :

Il ne fait aucun doute que le Graal de l'efficacité conduit à des abus. Les programmeurs perdent énormément de temps à réfléchir ou à s'inquiéter de la vitesse des parties non critiques de leurs programmes, et ces tentatives d'efficacité ont en fait un impact négatif important lorsque l'on prend en compte la debugging et la maintenance. On devrait oublier les petites optimisations locales, disons, 97 % du temps : l'optimisation prématurée est la source de tous les maux.

Troisièmement, la macro provoque des erreurs.

Lorsqu'on utilise la macro, c'est très facile d'écrire un code incorrect.

#define SAFE_FREE(ptr) if (ptr) free(ptr)
....
if (A)
  SAFE_FREE(P);
else
  foo();

Le code ne se présente pas correctement. Étendrons la macro.

if (A)
  if (P) free(P);
else
  foo();

L'instruction else fait référence à la deuxième instruction if et est exécutée lorsque le pointeur est NULL. Eh bien, ils ne se fonctionnent pas comme prévu. La macro SAFE_FREE s'est avérée ne pas être si " SAFE " que cela.

Il y a d'autres façons de créer accidentellement un code incorrect. Examinons le code avec suppression d'un tableau deux dimensions.

int **A = ....;
....
int **P = A;

for (....)
  SAFE_FREE(*P++);
SAFE_FREE(A);

Certes, le code est farfelu, mais il montre comment la macro est dangereuse lorsqu'il s'agit d'expressions complexes. Un pointeur est vérifié et le pointeur suivant est libéré :

for (....)
  if (*P++) free(*P++);

Il y a également un dépassement de tableau.

En résumé, c'est mauvais.

Peut-on fixer une macro ?

On peut, mais on n'en a pas besoin. Examinons les solutions possibles pour y corriger — c'est à des fins éducatives uniquement. C'est aussi l'une des questions que nous posons lors des entretiens.

Tout d'abord, on doit protéger la macro contre l'issue du else. La manière la plus simple, mais la plus inefficace, est d'ajouter des accolades :

#define SAFE_FREE(ptr) { if (ptr) free(ptr); }

Le code dont nous avons parlé plus haut ne sera plus compilé ( l'error: 'else' without a previous 'if' ) :

if (A)
  SAFE_FREE(P);
else
  foo();

On peut utiliser le truc suivant :

#define SAFE_FREE(ptr) do { if (ptr) free(ptr); } while(0)

Le code se compile à nouveau. La première issue est résolue, mais qu'en est-il des nouveaux calculs ? Une solution standard recommandée n'existe pas. Toutefois, il y a des solutions de rechange si vous le souhaitez vraiment.

Une issue similaire se pose lors de la mise en œuvre de macros telles que max. Voici un exemple :

#define max(a, b) ((a) > (b) ? (a) : (b))
....
int X = 10;
int Y = 20;
int Z = max(X++, Y++);

21 au lieu de 20 seront écrites dans la variable Z parce que la variable Y aura été incrémentée au moment de la sélection :

int X = 10;
int Y = 20;
int Z = ((X++) > (Y++) ? (X++) : (Y++));

Pour éviter cela, on peut utiliser la magie : l'extension du compilateur GCC : referring to a Type with typeof.

#define max(a,b) \
  ({ typeof (a) _a = (a); \
      typeof (b) _b = (b); \
    _a > _b ? _a : _b; })

Il s'agit de copier des valeurs dans des variables temporaires et d'éliminer ainsi le calcul répété d'expressions. L'opérateur typeof est un analogue du decltype C++, mais pour C.

Une fois encore, notez qu'il s'agit d'une extension non standard. Je ne recommande pas de l'utiliser à moins d'en avoir vraiment besoin.

Appliquons cette méthode à SAFE_FREE :

#define SAFE_FREE(ptr) do { \
  typeof(ptr) copy = (ptr); \
  if (copy) \
    free(copy); \
} while(0)

Cela fonctionne. Mais j'ai dû écrire un code terrible, insupportable et en fait inutile pour le faire.

Une solution plus élégante consiste à convertir la macro en fonction. De cette manière, on peut éliminer les issues évoquées ci-dessus et simplifier le code :

void SAFE_FREE(void *ptr)
{
  if (ptr)
    free(ptr);
}

Attendez, attendez, attendez ! On revient à l'appel de fonction ! Sauf que maintenant, on a une couche de fonction supplémentaire. La fonction free effectue le même travail de vérification du pointeur.

La meilleure façon de corriger la macro SAFE_FREE est donc de la supprimer !

Remise à zéro du pointeur après free

Il y a un sujet qui n'a presque rien à voir avec la vérification des pointeurs, mais discutons-en également. Certains programmeurs recommandent de remettre à zéro le pointeur après la libération de la mémoire. Au cas où.

free(pfoo);
pfoo = NULL;

On pourrait dire que le code est écrit selon le paradigme de la programmation défensive. Je parle d'actions optionnelles extra qui permettent parfois de se prémunir contre les erreurs.

Dans notre cas, si le pointeur pfoo n'est pas utilisé, il est inutile de le mettre à zéro. Toutefois, on peut le faire pour les raisons suivantes.

L'accès aux pointeurs. Si des données sont écrites accidentellement dans le pointeur, il ne s'agit pas d'une corruption de la mémoire, mais d'un déréférencement du pointeur NULL. Une telle erreur est détectée et corrigée plus rapidement. La même chose se produit lors de la lecture de données à partir d'un pointeur.

Double-free. La mise à zéro du pointeur protège contre les erreurs lorsque le tampon est à nouveau libéré. Cependant, les avantages ne sont pas aussi évidents au premier coup d'œil. Examinons le code qui contient l'erreur :

float *ptr1;
char *ptr2;
....
free(ptr1);
ptr1 = NULL;
....
free(ptr1);  // the ptr2 variable should have been used here
ptr2 = NULL;

Un développeur a fait une erreur : au lieu d'écrire ptr2, on a utilisé le pointeur ptr1 pour libérer à nouveau de la mémoire. En raison de la mise à zéro du pointeur ptr1, rien ne se produit lorsque vous le libérez à nouveau. Le code est protégé contre l'erreur double-free. En revanche, la mise à zéro du pointeur cache l'erreur plus profondément. Il y a une fuite de mémoire qui peut être difficile à détecter.

La programmation défensive est critiquée à cause de ces cas (masquer les erreurs, remplacer une erreur par une autre). C'est un vaste sujet, et je ne suis pas prêt à m'y plonger. Cependant, je pense qu'il est juste de vous mettre en garde contre les inconvénients de la programmation défensive.

Quelle est la meilleure façon de procéder si vous décidez de remettre à zéro les pointeurs après avoir libéré la mémoire ?

Commençons par la manière la plus dangereuse :

#define FREE_AND_CLEAR(ptr) do { \
  free(ptr); \
  ptr = NULL; \
} while(0)

La macro n'est pas conçue pour être utilisée de cette manière :

int **P = ....;
for (....)
  FREE_AND_CLEAR(*P++);

Un pointeur est libéré et le pointeur suivant est mis à zéro. Polissons la macro :

#define FREE_AND_CLEAR(ptr)  do { \
  void **x = &(ptr); \
  free(*x); \
  *x = NULL; \
} while(0)

Il fait l'affaire, mais franchement, ce genre de macro n'est pas mon truc. Je préférerais que le pointeur soit explicitement mis à zéro :

int **P = ....;
for (....)
{
  free(*P);
  *P = NULL;
  P++;
}

Le code ci-dessus est trop long, je ne l'aime pas. Il n'y a pas de magie macro. Je n'aime pas les macros. Le fait que le code soit long et laid est une bonne raison d'envisager sa réécriture. Est-il vraiment nécessaire d'itérer et de libérer des pointeurs d'une manière aussi maladroite ? Nous pourrions peut-être rendre le code plus élégant. C'est une bonne raison de procéder à un remaniement.

Conclusion

N'essayez pas de résoudre des issues inventées à l'avance et au cas où. Écrire un code simple et clair.

Sections connexes

Voir tous les articles

Poll:

Do you use PVS-Studio?

Subscribe
and get the e-book
for free!

book terrible tips
Popular related articles

S'abonner

Comments (1)

Guest 
04/26/2024, 10:01:56

Oui, c'est exact.

Reply

close comment form
close form

Remplissez le formulaire ci‑dessous en 2 étapes simples :

Vos coordonnées :

Étape 1
Félicitations ! Voici votre code promo !

Type de licence souhaité :

Étape 2
Team license
Enterprise licence
** En cliquant sur ce bouton, vous déclarez accepter notre politique de confidentialité
close form
Demandez des tarifs
Nouvelle licence
Renouvellement de licence
--Sélectionnez la devise--
USD
EUR
* En cliquant sur ce bouton, vous déclarez accepter notre politique de confidentialité

close form
La licence PVS‑Studio gratuit pour les spécialistes Microsoft MVP
close form
Pour obtenir la licence de votre projet open source, s’il vous plait rempliez ce formulaire
* En cliquant sur ce bouton, vous déclarez accepter notre politique de confidentialité

close form
I want to join the test
* En cliquant sur ce bouton, vous déclarez accepter notre politique de confidentialité

close form
check circle
Votre message a été envoyé.

Nous vous répondrons à


Si l'e-mail n'apparaît pas dans votre boîte de réception, recherchez-le dans l'un des dossiers suivants:

  • Promotion
  • Notifications
  • Spam