Résumé :
Une fonction en C doit-elle toujours vérifier pour ne pas déréférencer un NULL
pointeur? 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 NULL
pointeur 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 *graph
est NULL
alors 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 static
et 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_color
pourrait avoir été modifié pour vérifier *graph
null, 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.
la source
Réponses:
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
assert
pour vérifier les erreurs de programmation au lieu d'uneif
instruction. 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.
la source
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é.)
la source
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 desNULL
vérifications.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
argv
vecteur des chaînes passéesmain
doit ê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.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 ungraph *
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é, agraph *
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:
la source
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.
la source
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_color
sont 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.la source
Je dirais que cela dépend des éléments suivants:
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.
la source
À 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é.
assert
a 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:
... 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_vertex
fonction 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:... 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
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.
la source
NULL
vérifications supplémentaires feront beaucoup. Pensez-y:B()
vérifie maintenantNULL
et ... fait quoi? Retour-1
? Si l'appelant ne vérifie pasNULL
, quelle confiance pouvez-vous avoir de toute façon qu'il traitera le-1
cas de la valeur de retour?