Quand faut-il vérifier les pointeurs pour NULL en C?

18

Résumé :

Une fonction en C doit-elle toujours vérifier pour ne pas déréférencer un NULLpointeur? Sinon, quand est-il approprié de sauter ces vérifications?

Détails :

J'ai lu des livres sur la programmation des interviews et je me demande quel est le degré approprié de validation d'entrée pour les arguments de fonction en C? De toute évidence, toute fonction qui prend la saisie d'un utilisateur doit effectuer la validation, y compris la vérification d'un NULLpointeur avant de le déréférencer. Mais qu'en est-il dans le cas d'une fonction dans le même fichier que vous ne vous attendez pas à exposer via votre API?

Par exemple, ce qui suit apparaît dans le code source de git:

static unsigned short graph_get_current_column_color(const struct git_graph *graph)
{
    if (!want_color(graph->revs->diffopt.use_color))
        return column_colors_max;
    return graph->default_column_color;
}

Si *graphest NULLalors un pointeur nul sera déréférencé, plantant probablement le programme, mais pouvant entraîner un autre comportement imprévisible. D'un autre côté, la fonction est staticet donc peut-être que le programmeur a déjà validé l'entrée. Je ne sais pas, je viens de le sélectionner au hasard car c'était un court exemple dans un programme d'application écrit en C. J'ai vu beaucoup d'autres endroits où des pointeurs sont utilisés sans vérifier NULL. Ma question n'est généralement pas spécifique à ce segment de code.

J'ai vu une question similaire posée dans le contexte de la remise d'exceptions . Cependant, pour un langage dangereux tel que C ou C ++, il n'y a pas de propagation d'erreur automatique des exceptions non gérées.

D'un autre côté, j'ai vu beaucoup de code dans des projets open source (comme l'exemple ci-dessus) qui ne vérifie pas les pointeurs avant de les utiliser. Je me demande si quelqu'un a des réflexions sur les directives pour savoir quand mettre des vérifications dans une fonction par rapport à l'hypothèse que la fonction a été appelée avec des arguments corrects.

Je m'intéresse à cette question en général pour écrire du code de production. Mais je m'intéresse également au contexte des interviews de programmation. Par exemple, de nombreux manuels d'algorithmes (tels que CLR) ont tendance à présenter les algorithmes en pseudocode sans vérification d'erreur. Cependant, bien que cela soit bon pour comprendre le cœur d'un algorithme, ce n'est évidemment pas une bonne pratique de programmation. Je ne voudrais donc pas dire à un intervieweur que je sautais la vérification des erreurs pour simplifier mes exemples de code (comme le pourrait un manuel). Mais je ne voudrais pas non plus sembler produire un code inefficace avec une vérification d'erreur excessive. Par exemple, le graph_get_current_column_colorpourrait avoir été modifié pour vérifier *graphnull, mais ce n'est pas clair ce qu'il ferait s'il *graphétait nul, sinon il ne devrait pas le déréférencer.

Gabriel Southern
la source
7
Si vous écrivez une fonction pour une API où les appelants ne sont pas censés comprendre les entrailles, c'est l'un de ces endroits où la documentation est importante. Si vous documentez qu'un argument doit être un pointeur non NULL valide, le vérifier devient la responsabilité de l'appelant.
Blrfl
1
Voir aussi: stackoverflow.com/questions/4390007/…
Billy ONeal
Avec le recul de l'année 2017, en gardant à l'esprit la question et la plupart des réponses ont été écrites en 2013, l'une des réponses aborde-t-elle la question des comportements indéfinis dans le temps dus à l'optimisation des compilateurs?
rwong
Dans le cas d'appels d'API qui attendent des arguments de pointeur valides, je me demande quelle est la valeur des tests pour NULL uniquement? Tout pointeur invalide qui est déréférencé serait tout aussi mauvais que NULL et segfault tout de même.
PaulHK

Réponses:

15

Les pointeurs nuls invalides peuvent être causés par une erreur de programmation ou par une erreur d'exécution. Les erreurs d'exécution sont quelque chose qu'un programmeur ne peut pas résoudre, comme un mallocéchec dû à une mémoire insuffisante ou au fait que le réseau a perdu un paquet ou à l'utilisateur d'entrer quelque chose de stupide. Les erreurs du programmeur sont causées par un programmeur utilisant la fonction de manière incorrecte.

La règle générale que j'ai vue est que les erreurs d'exécution doivent toujours être vérifiées, mais les erreurs de programmation ne doivent pas être vérifiées à chaque fois. Disons qu'un programmeur idiot a directement appelé graph_get_current_column_color(0). Il sera défectueux la première fois qu'il sera appelé, mais une fois que vous le corrigerez, le correctif sera compilé en permanence. Pas besoin de vérifier à chaque exécution.

Parfois, en particulier dans les bibliothèques tierces, vous verrez un assertpour vérifier les erreurs de programmation au lieu d'une ifinstruction. Cela vous permet de compiler les vérifications pendant le développement et de les laisser de côté dans le code de production. J'ai aussi occasionnellement vu des vérifications gratuites où la source de l'erreur potentielle du programmeur est très éloignée du symptôme.

Évidemment, vous pouvez toujours trouver quelqu'un de plus pédant, mais la plupart des programmeurs C que je connais préfèrent un code moins encombré à un code légèrement plus sûr. Et "plus sûr" est un terme subjectif. Une erreur de segmentation flagrante pendant le développement est préférable à une erreur de corruption subtile sur le terrain.

Karl Bielefeldt
la source
La question est quelque peu subjective mais cela semblait être la meilleure réponse pour l'instant. Merci à tous ceux qui ont donné leur avis sur cette question.
Gabriel Southern
1
Dans iOS, malloc ne renverra jamais NULL. S'il ne trouve pas de mémoire, il demandera d'abord à votre application de libérer de la mémoire, puis il demandera au système d'exploitation (qui demandera à d'autres applications de libérer de la mémoire et éventuellement de les tuer), et s'il n'y a toujours pas de mémoire, il tuera votre application . Aucun contrôle nécessaire.
gnasher729
11

Kernighan & Plauger, dans "Software Tools", a écrit qu'ils vérifieraient tout, et, pour les conditions qui, selon eux, ne pourraient en fait jamais se produire, ils avorteraient avec un message d'erreur "Impossible."

Ils rapportent avoir été rapidement humiliés par le nombre de fois qu'ils ont vu "Ça ne peut pas arriver" sortir sur leurs terminaux.

Vous devez TOUJOURS vérifier le pointeur pour NULL avant de (tenter de) le déréférencer. TOUJOURS . La quantité de code que vous dupliquez en vérifiant les valeurs NULL qui ne se produisent pas et les cycles de processeur que vous "gaspillez" seront plus que payés par le nombre de plantages que vous n'avez pas à déboguer à partir d'un vidage sur incident - si vous êtes aussi chanceux.

Si le pointeur est invariant à l'intérieur d'une boucle, il suffit de le vérifier à l'extérieur de la boucle, mais vous devez ensuite le "copier" dans une variable locale à portée limitée, à utiliser par la boucle, qui ajoute les décorations const appropriées. Dans ce cas, vous DEVEZ vous assurer que chaque fonction appelée depuis le corps de la boucle inclut les décorations const nécessaires sur les prototypes, TOUTE LA VOIE. Si vous ne le faites pas, ou ne peut pas ( en raison par exemple d' un fournisseur ou un paquet collègue de travail de obstinée), alors vous devez vérifier pour NULL CHAQUE FOIS IL POURRAIT ÊTRE MODIFIÉ , parce que comme COL Murphy était un optimiste incurable, quelqu'un EST va pour le zapper quand vous ne regardez pas.

Si vous êtes dans une fonction et que le pointeur est supposé être non NULL, vous devez le vérifier.

Si vous le recevez d'une fonction et qu'il est censé être non NULL, vous devez le vérifier. malloc () est particulièrement connu pour cela. (Nortel Networks, maintenant disparu, avait une norme de codage écrite à ce sujet. J'ai pu déboguer un plantage à un moment donné, que j'ai retracé à malloc () en renvoyant un pointeur NULL et le codeur idiot ne prenant pas la peine de vérifier avant qu'il ne l'écrive, parce qu'il SAVAIT juste qu'il avait beaucoup de mémoire ... J'ai dit des choses très désagréables quand je l'ai finalement trouvé.)

John R. Strohm
la source
8
Si vous êtes dans une fonction qui nécessite un pointeur non NULL, mais vous vérifiez quand même et c'est NULL ... et ensuite?
detly
1
@detly soit arrêtez ce que vous faites et retournez un code d'erreur, soit déclenchez une assertion
James
1
@James - je n'y ai pas pensé assert, bien sûr. Je n'aime pas l'idée du code d'erreur si vous parlez de changer le code existant pour inclure des NULLvérifications.
detly
10
@detly vous n'allez pas aller très loin en tant que développeur C si vous n'aimez pas les codes d'erreur
James
5
@ JohnR.Strohm - c'est C, c'est des affirmations ou rien: P
detly
5

Vous pouvez ignorer la vérification lorsque vous pouvez vous convaincre que le pointeur ne peut pas être nul.

Habituellement, les vérifications de pointeur nul sont implémentées dans du code dans lequel nul devrait apparaître comme un indicateur qu'un objet n'est actuellement pas disponible. Null est utilisé comme valeur sentinelle, par exemple pour terminer des listes liées, ou même des tableaux de pointeurs. Le argvvecteur des chaînes passées maindoit être terminé par un pointeur nul, de la même manière que la chaîne se termine par un caractère nul: argv[argc]est un pointeur nul, et vous pouvez vous en remettre à cela lors de l'analyse de la ligne de commande.

while (*argv) {
   /* process argument string *argv */
   argv++; /* increment to next one */
}

Ainsi, les situations de vérification de null sont celles dans lesquelles a est une valeur attendue. Les vérifications nulles implémentent la signification du pointeur nul, comme l'arrêt de la recherche d'une liste liée. Ils empêchent le code de déréférencer le pointeur.

Dans une situation dans laquelle une valeur de pointeur nul n'est pas attendue par conception, il est inutile de la vérifier. Si une valeur de pointeur invalide apparaît, elle apparaîtra très probablement non nulle, ce qui ne peut être distingué des valeurs valides d'aucune manière portable. Par exemple, une valeur de pointeur obtenue en lisant un stockage non initialisé interprété comme un type de pointeur, un pointeur obtenu via une conversion louche ou un pointeur incrémenté hors limites.

À propos d'un type de données tel que graph *: cela pourrait être conçu de sorte qu'une valeur nulle soit un graphique valide: quelque chose sans bords et sans nœuds. Dans ce cas, toutes les fonctions qui prennent un graph *pointeur devront traiter cette valeur, car il s'agit d'une valeur de domaine correcte dans la représentation des graphiques. D'un autre côté, a graph *pourrait être un pointeur vers un objet de type conteneur qui n'est jamais nul si nous tenons un graphe; un pointeur nul pourrait alors nous dire que "l'objet graphique n'est pas présent; nous ne l'avons pas encore alloué, ou nous l'avons libéré; ou cela n'a actuellement aucun graphique associé". Cette dernière utilisation des pointeurs est une combinaison booléenne / satellite: le pointeur étant non nul indique "J'ai cet objet soeur", et il fournit cet objet.

Nous pourrions définir un pointeur sur null même si nous ne libérons pas un objet, simplement pour dissocier un objet d'un autre:

tty_driver->tty = NULL; /* detach low level driver from the tty device */
Kaz
la source
L'argument le plus convaincant que je sais qu'un pointeur ne peut pas être nul à un certain point est d'envelopper ce point dans "if (ptr! = NULL) {" et un "}" correspondant. Au-delà de cela, vous êtes en territoire de vérification formel.
John R. Strohm
4

Permettez-moi d'ajouter une voix de plus à la fugue.

Comme la plupart des autres réponses, je dis - ne vous embêtez pas à vérifier à ce stade; c'est la responsabilité de l'appelant. Mais j'ai une base sur laquelle bâtir plutôt qu'une simple opportunité (et l'arrogance de la programmation C).

J'essaie de suivre le principe de Donald Knuth de rendre les programmes aussi fragiles que possible. En cas de problème, faites - le plantage grand et le référencement d' un pointeur NULL est généralement une bonne façon de le faire. L'idée générale est un crash ou une boucle infinie est bien mieux que de créer des données erronées. Et cela attire l'attention des programmeurs!

Mais référencer des pointeurs nuls (en particulier pour les grandes structures de données) ne provoque pas toujours un plantage. Soupir. C'est vrai. Et c'est là qu'interviennent les assertions. Elles sont simples, peuvent planter instantanément votre programme (ce qui répond à la question "Que doit faire la méthode si elle rencontre un null?"), Et peuvent être activées / désactivées pour diverses situations (je recommande NE PAS les désactiver, car il est préférable pour les clients d'avoir un crash et de voir un message crypté que d'avoir de mauvaises données).

C'est mes deux cents.

Scott Biggs
la source
1

En règle générale, je vérifie uniquement lorsqu'un pointeur est attribué, ce qui est généralement la seule fois où je peux réellement faire quelque chose et éventuellement récupérer s'il n'est pas valide.

Si j'obtiens un descripteur sur une fenêtre par exemple, je vérifierai qu'elle est nulle à droite et puis et là, et je ferai quelque chose à propos de la condition nulle, mais je ne vérifierai pas qu'elle est nulle à chaque fois J'utilise le pointeur, dans chaque fonction vers laquelle le pointeur est transmis, sinon j'aurais des montagnes de code de gestion des erreurs en double.

Des fonctions comme graph_get_current_column_colorsont probablement totalement incapables de faire quoi que ce soit d'utile dans votre situation si elles rencontrent un mauvais pointeur, donc je laisserais la vérification de NULL à ses appelants.

comment s'appelle-t-il
la source
1

Je dirais que cela dépend des éléments suivants:

  1. L'utilisation du processeur est-elle critique? Chaque vérification de NULL prend un certain temps.
  2. Quelles sont les chances que le pointeur soit NULL? Était-ce juste utilisé dans une fonction précédente. La valeur du pointeur a-t-elle pu être modifiée?
  3. Le système est-il préemptif? Cela signifie-t-il qu'un changement de tâche pourrait se produire et changer la valeur? Un ISR pourrait-il entrer et changer la valeur?
  4. Dans quelle mesure le code est-il étroitement lié?
  5. Existe-t-il une sorte de mécanisme automatique qui vérifiera automatiquement les pointeurs NULL?

Utilisation du processeur / pointeur de cotes est NULL Chaque fois que vous recherchez NULL, cela prend du temps. Pour cette raison, j'essaie de limiter mes contrôles à l'endroit où le pointeur aurait pu voir sa valeur modifiée.

Système préemptif Si votre code est en cours d'exécution et qu'une autre tâche pourrait l'interrompre et potentiellement changer la valeur qu'une vérification serait bonne d'avoir.

Modules étroitement couplés Si le système est étroitement couplé, il serait logique que vous ayez plus de contrôles. Ce que je veux dire par là, c'est que s'il y a des structures de données qui sont partagées entre plusieurs modules, un module peut changer quelque chose sous un autre module. Dans ces situations, il est logique de vérifier plus souvent.

Vérifications automatiques / assistance matérielle La dernière chose à prendre en compte est de savoir si le matériel sur lequel vous exécutez possède une sorte de mécanisme qui peut vérifier la valeur NULL. Plus précisément, je fais référence à la détection des défauts de page. Si votre système a une détection de défaut de page, le CPU lui-même peut vérifier les accès NULL. Personnellement, je trouve que c'est le meilleur mécanisme car il fonctionne toujours et ne dépend pas du programmeur pour effectuer des vérifications explicites. Il présente également l'avantage de zéro frais généraux. Si cela est disponible, je le recommande, le débogage est un peu plus difficile mais pas trop.

Pour tester s'il est disponible, créez un programme avec un pointeur. Réglez le pointeur sur 0, puis essayez de le lire / écrire.

barrem23
la source
Je ne sais pas si je classerais une erreur de segmentation comme effectuant une vérification NULL automatique. Je suis d'accord que la protection de la mémoire du processeur aide à ce qu'un processus ne puisse pas faire autant de dégâts au reste du système, mais je n'appellerais pas cela une protection automatique.
Gabriel Southern
1

À mon avis, valider les entrées (pré / post-conditions, c.-à-d.) Est une bonne chose pour détecter les erreurs de programmation, mais seulement si cela se traduit par des erreurs de show-stop bruyantes et désagréables d'un type qui ne peut être ignoré. asserta généralement cet effet.

Tout ce qui ne correspond pas à cela peut se transformer en cauchemar sans équipes très soigneusement coordonnées. Et bien sûr, idéalement, toutes les équipes sont très soigneusement coordonnées et unifiées selon des normes strictes, mais la plupart des environnements dans lesquels j'ai travaillé sont loin de cela.

À titre d'exemple, j'ai travaillé avec certains collègues qui pensaient qu'il fallait vérifier religieusement la présence de pointeurs nuls, alors ils ont saupoudré beaucoup de code comme celui-ci:

void vertex_move(Vertex* v)
{
     if (!v)
          return;
     ...
}

... et parfois comme ça sans même retourner / définir un code d'erreur. Et c'était dans une base de code vieille de plusieurs décennies avec de nombreux plugins tiers acquis. C'était également une base de code en proie à de nombreux bogues, et souvent des bogues qui étaient très difficiles à retracer jusqu'aux causes profondes car ils avaient tendance à planter dans des sites éloignés de la source immédiate du problème.

Et cette pratique était l'une des raisons. C'est une violation d'une condition préalable établie de la move_vertexfonction ci-dessus de lui passer un sommet nul, mais une telle fonction l'a simplement acceptée en silence et n'a rien fait en réponse. Donc, ce qui avait tendance à se produire était qu'un plugin pouvait avoir une erreur de programmeur qui le faisait passer nul à ladite fonction, seulement pour ne pas le détecter, seulement pour faire beaucoup de choses après, et finalement le système commencerait à s'effriter ou à planter.

Mais le vrai problème était l'incapacité de détecter facilement ce problème. J'ai donc essayé une fois de voir ce qui se passerait si je transformais le code analogique ci-dessus en un assert, comme ceci:

void vertex_move(Vertex* v)
{
     assert(v && "Vertex should never be null!");
     ...
}

... et à mon horreur, j'ai trouvé que cette affirmation échouait à gauche et à droite même au démarrage de l'application. Après avoir corrigé les premiers sites d'appels, j'ai fait encore plus de choses, puis j'ai reçu un plus grand nombre d'échecs d'assertion. J'ai continué jusqu'à ce que j'aie modifié tellement de code que j'ai fini par revenir sur mes modifications car elles étaient devenues trop intrusives et ont gardé à contrecœur cette vérification du pointeur nul, documentant plutôt que la fonction permet d'accepter un sommet nul.

Mais c'est le danger, bien que le pire des cas, de ne pas rendre les violations des pré / post-conditions facilement détectables. Vous pouvez ensuite, au fil des ans, accumuler silencieusement une cargaison de code violant de telles pré / post-conditions en volant sous le radar des tests. À mon avis, de telles vérifications de pointeur nul en dehors d'un échec d'assertion flagrant et odieux peuvent en fait faire beaucoup, beaucoup plus de mal que de bien.

En ce qui concerne la question essentielle de savoir quand vérifier les pointeurs nuls, je crois qu'il faut affirmer généreusement s'il est conçu pour détecter une erreur de programmation, et ne pas laisser cela silencieux et difficile à détecter. Si ce n'est pas une erreur de programmation et quelque chose échappant au contrôle du programmeur, comme une panne de mémoire insuffisante, alors il est logique de vérifier la valeur null et d'utiliser la gestion des erreurs. Au-delà, c'est une question de conception et basée sur ce que vos fonctions considèrent comme des conditions de pré / post valides.


la source
0

Une pratique consiste à toujours effectuer la vérification nulle sauf si vous l'avez déjà vérifiée; donc si l'entrée est passée de la fonction A () à B (), et A () a déjà validé le pointeur et vous êtes certain que B () n'est appelé nulle part ailleurs, alors B () peut faire confiance à A () pour avoir a désinfecté les données.

Geai
la source
1
... jusqu'à ce que dans 6 mois, quelqu'un arrive et ajoute un peu plus de code qui appelle B () (en supposant peut-être que celui qui a écrit B () a sûrement vérifié correctement les NULL). Alors tu es foutu, non? Règle de base - s'il existe une condition non valide pour l'entrée d'une fonction, vérifiez-la, car l'entrée est hors du contrôle de la fonction.
Maximus Minimus
@ mh01 Si vous détruisez simplement du code aléatoire (c.-à-d. en faisant des hypothèses et en ne lisant pas la documentation), alors je ne pense pas que des NULLvérifications supplémentaires feront beaucoup. Pensez-y: B()vérifie maintenant NULLet ... fait quoi? Retour -1? Si l'appelant ne vérifie pas NULL, quelle confiance pouvez-vous avoir de toute façon qu'il traitera le -1cas de la valeur de retour?
detly
1
C'est la responsabilité des appelants. Vous assumez votre propre responsabilité, ce qui inclut de ne pas faire confiance aux entrées arbitraires / inconnaissables / potentiellement non vérifiées qui vous sont données. Sinon, vous êtes en ville cop-out. Si l'appelant ne vérifie pas, alors l'appelant a foiré; vous avez vérifié, votre propre cul est couvert, vous pouvez dire à celui qui a écrit à l'appelant qu'au moins vous avez bien fait les choses.
Maximus Minimus