57 votes

Can \0 et NULL sont utilisés indifféremment ?

NULL est souvent utilisé dans le contexte des pointeurs, et est défini via des macros dans plusieurs bibliothèques standard (telles que <iostream> ) pour être le nombre entier 0 . '\0' est le caractère nul, et comporte 8 bits de zéros. Incidemment, 8 bits de zéros sont équivalents à l'entier 0 .

Dans certains cas, bien que cela soit considéré comme un style horrible, ces deux éléments peuvent être interchangés :

int *p='\0';
if (p==NULL) //evaluates to true
    cout << "equal\n";

Ou

char a=NULL;
char b='\0';
if (a==b) //evaluates to true
    cout << "equal again\n";

Il existe déjà de nombreuses questions similaires sur le seul site SO ; par exemple, la réponse la plus fréquente à cette question ( Quelle est la différence entre NULL, ' \0 et 0 ) dit "qu'ils ne sont pas vraiment la même chose".

Quelqu'un peut-il fournir un exemple qui NULL y \0 ne peuvent être interchangés (de préférence une application réelle et non un cas pathologique) ?

22 votes

( -Wzero-as-null-pointer-constant )

0 votes

On voit ça tout le temps, et c'est un anti-modèle terrible.

3 votes

Biffen, mjs, ma question est sincère - j'essaie de comprendre la différence entre les deux. Voulez-vous dire que les deux sont équivalents et que c'est juste une question de style (ce qui, je suppose, est le but de l'indicateur gcc) ?

54voto

Leon Points 20011

Quelqu'un pourrait-il fournir un exemple où NULL et \0 ne peuvent être interchangés ?

La différence entre NULL y '\0' peut affecter la résolution des surcharges.

Exemple ( voir sur Coliru ) :

#include <iostream>

// The overloaded function under question can be a constructor or 
// an overloaded operator, which would make this example less silly
void foo(char)   { std::cout << "foo(char)"  << std::endl; }
void foo(int)    { std::cout << "foo(int)"   << std::endl; }
void foo(long)   { std::cout << "foo(long)"  << std::endl; }
void foo(void*)  { std::cout << "foo(void*)" << std::endl; }

int main()
{
    foo('\0'); // this will definitely call foo(char)
    foo(NULL); // this, most probably, will not call foo(char)
}

Notez que le compilateur gcc utilisé à Coliru définit NULL comme 0L ce qui, dans cet exemple, signifie que foo(NULL) se résout à foo(long) plutôt que de foo(void*) . Cette réponse traite de cet aspect en détail.

7 votes

Il suffit d'appeler aussi foo(nullptr) pour être complet

4 votes

C'est un mauvais exemple, je pense, car foo(NULL) est plus susceptible d'appeler foo(int) que foo(void*) sur les implémentations actuelles. Oui, ce n'est toujours pas foo(char) mais foo(void*) est plus susceptible de correspondre à l'intention du programmeur.

8 votes

Sur C++, foo(NULL) appellera toujours soit foo(int) o foo(long) car la norme C++ interdit de définir NULL comme (void*)0 . Voir ma réponse pour plus de détails : stackoverflow.com/a/40396865/5489178

37voto

Ville-Valtteri Points 3536

Définition de la macro NULL en C++

Léon a raison que lorsqu'il y a plusieurs surcharges pour la même fonction, \0 préfère celui qui prend un paramètre de type char . Cependant, il est important de noter que sur un compilateur typique, NULL préférerait la surcharge qui prend un paramètre de type int non de type void* !

Ce qui cause probablement cette confusion est que le langage C permet de définir NULL comme (void*)0 . La norme C++ indique explicitement (projet N3936, page 444) :

Définitions possibles [de la macro NULL ] comprennent 0 y 0L mais pas (void*)0 .

Cette restriction est nécessaire, car par exemple char *p = (void*)0 est un C valide mais un C++ invalide, alors que char *p = 0 est valable dans les deux.

En C++11 et plus, vous devez utiliser nullptr si vous avez besoin d'une constante nulle qui se comporte comme un pointeur.

Comment la suggestion de Léon fonctionne en pratique

Ce code définit plusieurs surcharges d'une même fonction. Chaque surcharge donne le type du paramètre :

#include <iostream>

void f(int) {
    std::cout << "int" << std::endl;
}

void f(long) {
    std::cout << "long" << std::endl;
}

void f(char) {
    std::cout << "char" << std::endl;
}

void f(void*) {
    std::cout << "void*" << std::endl;
}

int main() {
    f(0);
    f(NULL);
    f('\0');
    f(nullptr);
}

Sur Ideone ces sorties

int
int
char
void*

Je prétends donc que le problème des surcharges n'est pas une application réelle mais un cas pathologique. Le site NULL se comportera mal de toute façon, et devrait être remplacé par nullptr en C++11.

Que se passe-t-il si NULL n'est pas égal à zéro ?

Un autre cas pathologique est suggéré par Andrew Keeton à une autre question :

Notez que ce qu'est un pointeur nul dans le langage C. Cela n'a pas d'importance sur l'architecture sous-jacente. Si l'architecture sous-jacente a une valeur de pointeur nul définie comme l'adresse 0xDEADBEEF, alors c'est au compilateur de régler ce problème.

Ainsi, même sur cette drôle d'architecture, les méthodes suivantes sont toujours valables pour vérifier la présence d'un pointeur nul :

if (!pointer)
if (pointer == NULL)
if (pointer == 0)

Les méthodes suivantes sont INVALIDES pour vérifier la présence d'un pointeur nul :

#define MYNULL (void *) 0xDEADBEEF
if (pointer == MYNULL)
if (pointer == 0xDEADBEEF)

car elles sont vues par un compilateur comme des comparaisons normales.

Résumé

Dans l'ensemble, je dirais que les différences sont surtout stylistiques. Si vous avez une fonction qui prend int et une surcharge qui prend char et ils fonctionnent différemment, vous remarquerez la différence lorsque vous les appelez avec \0 y NULL constantes. Mais dès que l'on place ces constantes dans des variables, la différence disparaît, car la fonction qui est appelée est déduite du type de la variable.

L'utilisation de constantes correctes rend le code plus facile à maintenir et transmet mieux le sens. Vous devez utiliser 0 quand vous voulez dire un nombre, \0 quand vous voulez dire un personnage, et nullptr quand vous voulez dire un pointeur. Matthieu M. fait remarquer dans les commentaires, que GCC avait un bug dans lequel un char* a été comparé à \0 alors que l'intention était de déréférencer le pointeur et de comparer une valeur de char a \0 . De telles erreurs sont plus faciles à détecter, si un style approprié est utilisé dans toute la base de code.

Pour répondre à votre question, il n'y a pas vraiment de cas d'utilisation réelle qui vous empêcherait d'utiliser \0 y NULL de manière interchangeable. Ce ne sont que des raisons stylistiques et quelques cas limites.

1 votes

Mon NULL (g++ 6.2.0) est long int Ainsi, le code ici ne compile pas à moins que j'ajoute une balise long comme Leon l'a fait (sinon, il y a une ambiguïté entre la surcharge int y char ).

4 votes

Pourquoi est-ce que NULL spécifiquement requis no à être (void*) ?

5 votes

@KyleStrand char *p = (void *)0; est un C valide mais un C++ invalide. char *p = 0; est également valable C++

8voto

mjs Points 827

S'il te plaît, ne fais pas ça. C'est un anti-modèle, et c'est en fait une erreur. NULL est pour les pointeurs NULL, '\0' est le caractère nul. Ce sont des choses logiquement différentes.

Je ne pense pas avoir déjà vu ça :

int* pVal='\0';

Mais c'est assez courant :

char a=NULL;

Mais ce n'est pas une bonne forme. Cela rend le code moins portable, et à mon avis moins lisible. Il est également susceptible de causer des problèmes dans les environnements mixtes C/C++.

Elle repose sur des hypothèses concernant la manière dont une implémentation particulière définit NULL. Par exemple, certaines implémentations utilisent un simple

#define NULL 0

D'autres pourraient utiliser :

#define NULL ((void*) 0)

Et j'en ai vu d'autres définir comme un nombre entier, et toutes sortes de traitements bizarres.

NULL devrait, à mon avis, être utilisé uniquement pour indiquer une adresse invalide. Si vous voulez un caractère nul, utilisez '\0' . Ou définissez-le comme NULLCHR . Mais ce n'est pas aussi propre.

Cela rendra votre code plus portable - vous ne commencerez pas à recevoir des avertissements concernant les types, etc. si vous changez les paramètres du compilateur/de l'environnement/du compilateur. Cela peut être plus important dans un environnement C ou mixte C/C++.

Un exemple d'avertissement qui pourrait survenir : Considérez ce code :

#define NULL 0
char str[8];
str[0]=NULL;

Ceci est équivalent à :

#define NULL 0
char str[8];
str[0]=0;

Et nous attribuons une valeur entière à un caractère. Cela peut provoquer un avertissement du compilateur, et s'il y a suffisamment d'occurrences de cela, bientôt vous ne verrez plus aucun important avertissements. Et pour moi, c'est le vrai problème. Avoir des avertissements dans le code a deux effets secondaires :

  1. Avec suffisamment d'avertissements, on ne repère pas les nouveaux.
  2. Il donne le signal que les avertissements sont acceptables.

Dans les deux cas, il est possible de laisser passer des bogues qui auraient été détectés par le compilateur si nous avions pris la peine de lire les avertissements (ou d'activer -Werror ).

5 votes

#define NULL (void*) 0 n'est valable qu'en C (si des parenthèses sont ajoutées), pas en C++. En C++03, char c = NULL; est portable mais de mauvais style. C++11 permet #define NULL nullptr y char c = NULL; est autorisé à échouer.

0 votes

Je n'étais pas au courant des restrictions du C++11. Cela rend l'argument de ne pas le faire encore plus fort. Je mettrai la réponse à jour plus tard.

1 votes

Ce n'est plus une restriction depuis bien avant la normalisation. Il n'a jamais( ?) été autorisé de convertir implicitement void* a T* (pour T != void ), ce qui permet de définir NULL a ((void*)0) le rendrait plutôt inutile dans la plupart des contextes auxquels il est destiné.

8voto

Saurav Sahu Points 6098

Oui, ils peuvent présenter un comportement différent lors de la résolution des fonctions surchargées.

func('\0') invoque func(char) ,

tandis que

func(NULL) invoque func(integer_type) .


Vous pouvez éliminer la confusion en utilisant nullptr qui est toujours un type pointeur, ne présente aucune ambiguïté alors que affectation/comparaison de valeur ou de fonction surcharge de résolution .

char a = nullptr; //error : cannot convert 'std::nullptr_t' to 'char' in initialization
int x = nullptr;  //error : nullptr is a pointer not an integer

Notez qu'il est toujours compatible avec NULL :

int *p=nullptr;
if (p==NULL) //evaluates to true

Extrait du livre C++ Programming Stroustrup 4th Edition :

Dans un code plus ancien, 0 ou NULL est généralement utilisé à la place de nullptr (§7.2.2). Cependant, l'utilisation de nullptr élimine la confusion potentielle entre les entiers (comme 0 ou NULL) et les pointeurs (comme nullptr).


7voto

Stig Hemmer Points 2175

Les programmes informatiques ont deux types de lecteurs.

Le premier type est constitué de programmes informatiques, comme le compilateur.

Le deuxième type est constitué d'humains, comme vous-même et vos collègues de travail.

Les programmes acceptent généralement de recevoir un type de zéro à la place d'un autre. Il y a des exceptions, comme l'ont souligné les autres réponses, mais ce n'est pas vraiment important.

Ce qui est important, c'est que vous jouez avec le humain lecteurs.

Les lecteurs humains sont très sensible au contexte. En utilisant le mauvais zéro, vous mentir à vous, lecteurs humains. Ils vont vous maudire.

Un être humain à qui l'on ment peut plus facilement ignorer les bogues.
Un humain à qui l'on ment peut voir des "bugs" qui n'existent pas. En "réparant" ces bugs de phanthom, ils introduisent de vrais bugs.

Ne mentez pas à vos humains. L'un des humains à qui tu mens est ton futur toi. Tu vas être maudit toi aussi.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X