Que signifie faire une «vérification nulle» en C ou C ++?

21

J'ai appris le C ++ et j'ai du mal à comprendre null. En particulier, les tutoriels que j'ai lus mentionnent une "vérification nulle", mais je ne sais pas ce que cela signifie ni pourquoi c'est nécessaire.

  • Qu'est-ce qui est nul exactement?
  • Que signifie «vérifier la nullité»?
  • Dois-je toujours vérifier la null?

Tout exemple de code serait très apprécié.

kdi
la source
Quand: programmers.stackexchange.com/questions/186036/…
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
Je conseillerais d'obtenir de meilleurs tutoriels, si tous ceux que vous lisez parlent de vérifications nulles sans jamais les expliquer et fournir un exemple de code ...
underscore_d

Réponses:

26

En C et C ++, les pointeurs sont intrinsèquement dangereux, c'est-à-dire que lorsque vous déréférencez un pointeur, il est de votre responsabilité de vous assurer qu'il pointe vers un endroit valide; cela fait partie de la "gestion manuelle de la mémoire" (contrairement aux schémas de gestion automatique de la mémoire implémentés dans des langages comme Java, PHP ou le runtime .NET, qui ne vous permettront pas de créer des références invalides sans effort considérable).

Une solution courante qui détecte de nombreuses erreurs consiste à définir tous les pointeurs qui ne pointent vers rien comme NULL(ou, en C ++ correct 0), et à vérifier cela avant d'accéder au pointeur. Plus précisément, il est courant d'initialiser tous les pointeurs sur NULL (sauf si vous avez déjà quelque chose vers lequel les pointer lorsque vous les déclarez) et de les définir sur NULL lorsque vous deleteou free()eux (sauf s'ils sortent du champ d'application immédiatement après cela). Exemple (en C, mais aussi en C ++ valide):

void fill_foo(int* foo) {
    *foo = 23; // this will crash and burn if foo is NULL
}

Une meilleure version:

void fill_foo(int* foo) {
    if (!foo) { // this is the NULL check
        printf("This is wrong\n");
        return;
    }
    *foo = 23;
}

Sans la vérification nulle, le passage d'un pointeur NULL dans cette fonction entraînera une erreur de segmentation, et il n'y a rien que vous puissiez faire - le système d'exploitation tuera simplement votre processus et peut-être le vidage de mémoire ou affichera une boîte de dialogue de rapport de plantage. Avec la vérification nulle en place, vous pouvez effectuer une gestion des erreurs appropriée et récupérer correctement - corrigez le problème vous-même, abandonnez l'opération en cours, écrivez une entrée de journal, informez l'utilisateur, tout ce qui est approprié.

tdammers
la source
3
@MrLister que voulez-vous dire, les vérifications nulles ne fonctionnent pas en C ++? Il vous suffit d'initialiser le pointeur sur null lorsque vous le déclarez.
TZHX
1
Ce que je veux dire, c'est que vous devez vous rappeler de régler le pointeur sur NULL sinon cela ne fonctionnera pas. Et si vous vous souvenez, en d'autres termes, si vous savez que le pointeur est NULL, vous n'aurez pas besoin d'appeler fill_foo de toute façon. fill_foo vérifie si le pointeur a une valeur, pas si le pointeur a une valeur valide . En C ++, les pointeurs ne sont pas garantis être NULL ou avoir une valeur valide.
Mr Lister
4
Un assert () serait une meilleure solution ici. Il ne sert à rien d'essayer de "être en sécurité". Si NULL a été transmis, c'est évidemment faux, alors pourquoi ne pas simplement planter explicitement pour que le programmeur soit pleinement conscient? (Et en production, cela n'a pas d'importance, parce que vous avez prouvé que personne n'appellera fill_foo () avec NULL, non? Vraiment, ce n'est pas si difficile.)
Ambroz Bizjak
7
N'oubliez pas de mentionner qu'une version encore meilleure de cette fonction devrait utiliser des références au lieu de pointeurs, ce qui rend la vérification NULL obsolète.
Doc Brown
4
Ce n'est pas l'objet de la gestion manuelle de la mémoire, et un programme géré explosera également (ou déclenchera une exception au moins, tout comme un programme natif le fera dans la plupart des langues) si vous essayez de déréférencer une référence nulle.
Mason Wheeler
7

Les autres réponses couvraient à peu près votre question exacte. Une vérification nulle est effectuée pour être sûr que le pointeur que vous avez reçu pointe réellement vers une instance valide d'un type (objets, primitives, etc.).

Je vais ajouter ici mon propre conseil. Évitez les contrôles nuls. :) Les contrôles nuls (et d'autres formes de programmation défensive) encombrent le code et le rendent plus sujet aux erreurs que les autres techniques de gestion des erreurs.

Ma technique préférée en ce qui concerne les pointeurs d'objets est d'utiliser le modèle Null Object . Cela signifie renvoyer une (pointeur - ou mieux encore, une référence à un) tableau ou liste vide au lieu de null, ou renvoyer une chaîne vide ("") au lieu de null, ou même la chaîne "0" (ou quelque chose d'équivalent à "rien "dans le contexte) où vous vous attendez à ce qu'il soit analysé en un entier.

En bonus, voici un petit quelque chose que vous ne saviez peut-être pas sur le pointeur nul, qui a été (formellement) implémenté par CAR Hoare pour la langue Algol W en 1965.

J'appelle cela mon erreur d'un milliard de dollars. C'était l'invention de la référence nulle en 1965. A cette époque, je concevais le premier système de type complet pour les références dans un langage orienté objet (ALGOL W). Mon objectif était de garantir que toute utilisation des références soit absolument sûre, la vérification étant effectuée automatiquement par le compilateur. Mais je n'ai pas pu résister à la tentation de mettre une référence nulle, tout simplement parce qu'elle était si facile à mettre en œuvre. Cela a conduit à d'innombrables erreurs, vulnérabilités et pannes de système, qui ont probablement causé un milliard de dollars de douleur et de dommages au cours des quarante dernières années.

Yam Marcovic
la source
6
Null Object est encore pire que d'avoir simplement un pointeur nul. Si un algorithme X nécessite des données Y que vous n'avez pas, alors c'est un bug dans votre programme , que vous cachez simplement en faisant semblant de le faire.
DeadMG
Cela dépend du contexte, et dans tous les cas, le test de «présence de données» bat le test de null dans mon livre. D'après mon expérience, si un algorithme fonctionne sur, disons, une liste et que la liste est vide, l'algorithme n'a tout simplement rien à faire, et il accomplit cela en utilisant simplement des instructions de contrôle standard telles que for / foreach.
Yam Marcovic
Si l'algorithme n'a rien à voir, alors pourquoi l'appelez-vous? Et la raison pour laquelle vous auriez pu vouloir l'appeler en premier lieu, c'est parce qu'elle fait quelque chose d'important .
DeadMG
@DeadMG Parce que les programmes concernent l'entrée, et dans le monde réel, contrairement aux devoirs, les entrées peuvent ne pas être pertinentes (par exemple, vides). Le code est toujours appelé dans les deux cas. Vous avez deux options: soit vous vérifiez la pertinence (ou la vacuité), soit vous concevez vos algorithmes de manière à ce qu'ils lisent et fonctionnent correctement sans vérifier explicitement la pertinence à l'aide d'instructions conditionnelles.
Yam Marcovic
Je suis venu ici pour faire presque le même commentaire, alors je vous ai donné mon vote à la place. Cependant, j'ajouterais également que cela est représentatif d'un problème plus important des objets zombies - chaque fois que vous avez des objets avec une initialisation (ou destruction) à plusieurs étapes qui ne sont pas entièrement vivants mais pas tout à fait morts. Lorsque vous voyez du code "sûr" dans des langages sans finalisation déterministe qui a ajouté des vérifications dans chaque fonction pour voir si l'objet a été supprimé, c'est ce problème général d'élever sa tête. Vous ne devriez jamais if-null, vous devriez travailler avec des états qui ont les objets dont ils ont besoin pour leur durée de vie.
ex0du5
4

La valeur du pointeur nul représente un "nulle part" bien défini; il s'agit d'une valeur de pointeur non valide qui est garantie de comparer inégale à toute autre valeur de pointeur. Tenter de déréférencer un pointeur nul entraîne un comportement indéfini et conduit généralement à une erreur d'exécution, vous devez donc vous assurer qu'un pointeur n'est pas NULL avant de tenter de déréférencer. Un certain nombre de fonctions de bibliothèque C et C ++ renverront un pointeur nul pour indiquer une condition d'erreur. Par exemple, la fonction de bibliothèque mallocrenverra une valeur de pointeur nulle si elle ne peut pas allouer le nombre d'octets qui ont été demandés, et tenter d'accéder à la mémoire via ce pointeur entraînera (généralement) une erreur d'exécution:

int *p = malloc(sizeof *p * N);
p[0] = ...; // this will (usually) blow up if malloc returned NULL

Nous devons donc nous assurer que l' mallocappel a réussi en vérifiant la valeur de par prapport à NULL:

int *p = malloc(sizeof *p * N);
if (p != NULL) // or just if (p)
  p[0] = ...;

Maintenant, accrochez-vous à vos chaussettes une minute, cela va devenir un peu cahoteux.

Il existe une valeur de pointeur nul et une constante de pointeur nul , et les deux ne sont pas nécessairement les mêmes. La valeur du pointeur nul correspond à la valeur que l'architecture sous-jacente utilise pour représenter "nulle part". Cette valeur peut être 0x00000000, ou 0xFFFFFFFF, ou 0xDEADBEEF, ou quelque chose de complètement différent. Ne supposez pas que la valeur du pointeur nul est toujours 0.

La constante de pointeur nul , OTOH, est toujours une expression intégrale à valeur 0. En ce qui concerne votre code source , 0 (ou toute expression intégrale évaluée à 0) représente un pointeur nul. C et C ++ définissent la macro NULL comme constante de pointeur null. Lorsque votre code est compilé, la constante de pointeur nul sera remplacée par la valeur de pointeur nul appropriée dans le code machine généré.

Sachez également que NULL n'est qu'une des nombreuses valeurs de pointeur non valides possibles ; si vous déclarez une variable de pointeur automatique sans l'initialiser explicitement, comme

int *p;

la valeur initialement stockée dans la variable est indéterminée et peut ne pas correspondre à une adresse mémoire valide ou accessible. Malheureusement, il n'existe aucun moyen (portable) de savoir si une valeur de pointeur non NULL est valide ou non avant d'essayer de l'utiliser. Donc, si vous avez affaire à des pointeurs, c'est généralement une bonne idée de les initialiser explicitement sur NULL lorsque vous les déclarez et de les définir sur NULL lorsqu'ils ne pointent activement vers rien.

Notez que c'est plus un problème en C qu'en C ++; idiomatique C ++ ne devrait pas utiliser autant de pointeurs.

John Bode
la source
3

Il existe deux méthodes, toutes font essentiellement la même chose.

int * foo = NULL; // parfois défini sur 0x00 ou 0 ou 0L au lieu de NULL

vérification nulle (vérifier si le pointeur est nul), version A

if (foo == NULL)

contrôle nul, version B

if (! foo) // puisque NULL est défini comme 0,! foo retournera une valeur à partir d'un pointeur nul

contrôle nul, version C

si (foo == 0)

Parmi les trois, je préfère utiliser la première vérification car elle indique explicitement aux futurs développeurs ce que vous essayez de vérifier ET cela indique clairement que vous vous attendiez à ce que foo soit un pointeur.


la source
2

Non. La seule raison d'utiliser un pointeur en C ++ est que vous souhaitez explicitement la présence de pointeurs NULL; sinon, vous pouvez prendre une référence, qui est à la fois sémantiquement plus facile à utiliser et garantit non null.

DeadMG
la source
1
@James: 'nouveau' en mode noyau?
Nemanja Trifunovic
1
@James: une implémentation de C ++ qui représente les capacités dont jouit une majorité significative de codeurs C ++. Cela inclut toutes les fonctionnalités du langage C ++ 03 (sauf export) et toutes les fonctionnalités de la bibliothèque C ++ 03 et TR1 et un bon morceau de C ++ 11.
DeadMG
5
Je fais les gens de souhaits ne dirais pas que « les références garantissent non nul. » Ils ne le font pas. Il est aussi facile de générer une référence nulle qu'un pointeur nul, et ils se propagent de la même manière.
mjfgates
2
@Stargazer: La question est 100% redondante lorsque vous utilisez simplement les outils comme le suggèrent les concepteurs de langage et les bonnes pratiques.
DeadMG
2
@DeadMG, peu importe qu'il soit redondant. Vous n'avez pas répondu à la question . Je le répète: -1.
riwalk
-1

Si vous ne cochez pas la valeur NULL, en particulier, s'il s'agit d'un pointeur vers une structure, vous avez peut-être rencontré une vulnérabilité de sécurité - déréférence de pointeur NULL. La déréférence du pointeur NULL peut entraîner d'autres vulnérabilités de sécurité graves telles que le dépassement de tampon, la condition de concurrence ... qui peuvent permettre à un attaquant de prendre le contrôle de votre ordinateur.

De nombreux éditeurs de logiciels comme Microsoft, Oracle, Adobe, Apple ... publient des correctifs logiciels pour corriger ces vulnérabilités de sécurité. Je pense que vous devriez vérifier la valeur NULL de chaque pointeur :)

oDisPo
la source