Journal J'adore Linus

Posté par  .
Étiquettes :
0
23
juil.
2004
Perso, les '0' à la place de 'NULL' me gonffle. C'est très courant en C.




Vu sur lwn.net.




Voilà que Linux va faire la chasse à l'utilisation abusive de '0' pour les pointeurs :


http://lwn.net/Articles/93574/(...)




Un mail de Linus pour clarifier le tout :


http://lwn.net/Articles/93577/(...)


 > What's wrong with using 0 as the NULL pointer? In contexts where

> a plain 0 is unsafe, NULL is usually unsafe as well.

It's not about "unsafe". It's about being WRONG.

The fact is, people who write "0" are either living in the stone age, or
are not sure about the type.
[...]
In other words:

char * p = 0; /* IS WRONG! DAMMIT! */
int i = NULL; /* THIS IS WRONG TOO! */

and anybody who writes code like the above either needs to get out of the
kernel, or needs to get transported to the 21st century
.

Linus

J'adore son style.
  • # Petites precisions

    Posté par  . Évalué à 4.

    Au passage, je peux citer l'enorme discussion qu'il y a eu dessus sur Usenet.
    Voici le lien Google:
    http://groups.google.fr/groups?dq=&hl=fr&lr=&ie=UTF-8&a(...)
    De plus, sur la dernière version du noyau, on peut voir nettement que beaucoup de 0 ont été remplacés par des NULL.
    • [^] # Re: Petites precisions

      Posté par  . Évalué à 5.

      Juste aussi une petit précision.

      Globalement ça rend le code plus lisible.
      Mais dans certains cas on ne peut pas utiliser '0' à la place de 'NULL'.
      Typiquement les fonctions avec un nombre variable de paramètres (traité avec varargs).
      toto("coucou : %p, %s\n", 0, "tralala") ;
      est différent de :
      toto("coucou : %p, %s\n", NULL, "tralala") ; # NULL ou "(void *)0"

      Avec '0', sur la pile il y a :
      - char *
      - int
      - char *
      alors que la fonction toto() fait comme s'il y avait :
      - char *
      - void *
      - char *

      Si sizeof(int) != sizeof(void *) (typiquement le cas sur machine 64 bits), des problèmes sont à prévoir.

      J'ai ignoré la promotion faite en C mais pas en C++.
      • [^] # Re: Petites precisions

        Posté par  . Évalué à 10.

        AAARRRRGGGGHHHH !!!!
        Et dire que j'avais pensé sortir de ce troll collant depuis que je ne fréquantais plus les newsgroup de prog.

        Pour faire simple et rapide :
        Il existe un tas d'architecture pour laquelle l'adresse 0 est tout a fait valable. Et parmis ces architecture on trouve notre bonne vieille x86. De facon assez simple :
        - Toutes les machines capables de gérer nativement la mémoire en mode segment ou en mode page ont besoin de l'adresse 0 (violament même).
        - Pas mal de machines utilisent l'adresse 0 comme reset (Zilog et certains PICS si je me souviens bien + truc plus exotique a coté).
        - Dans le cas ou l'adresse 0 est un pointeur vers une fonction hardcodé (reset, init, firmware load etc..) alors
        a) cette fonction devrait être de type void (on voit mal a quoi une fonction d'init pourrait renvoyer des infos)
        b) elle pointe vers 0

        Sur toutes ces architectures, normalement aussi bien (void *)0 que 0 sont des pointeurs on ne peut plus valide.
        Donc difficile de faire
        #define NULL (void *)0
        ou
        #define NULL 0

        Si sizeof(int) != sizeof(void *) (typiquement le cas sur machine 64 bits), des problèmes sont à prévoir.

        Le problème c'est que de par le type (bon le non type) de void * on peut avoir sur certaines architectures :
        sizeof(void *) != sizeof(void *), ceux qui ont déjà joué un peu avec des jump far et des jump near en assembleur x86 (ou autre) voient de quoi je parle.
        La taille des pointeurs (et ce jusqu'au niveau des registres qui les stoquent) peut varier en fonction du contexte CPU. Et là c'est la fête au village quelque soit la définition de int ou de void ou même de long...

        Bon maintenant, on en est au moment ou on se rend compte que si on veut un truc qui marche sur toutes les archis, on ne peut pas définir NULL dans le code.
        A partir de là , il est assez facile de déduire que c'est le boulot du preprocesseur/precompileur de faire le tri la dedans.
        Par exemple si l'adresse 0 est valide c'est a lui de repérer les appels de type *p = NULL ou *p = 0 et de les remplacer par l'adresse non valide de son choix. Dans le cas de contextes ou les types ne font pas la même longueur c'est au dev de permettre au préprocesseur de faire la différence :
        - soit en créant des signatures de fonctions qui ne laissent pas la place au doute
        genre si toto(int i, typea j, typeb k) existe éviter de faire une autre fonction toto(void* i, typea j, typeb k), mais faire : toto(typea j, typeb k, void* j)
        Ou alors s'arranger pour que le type soit forcément défini quand il arrive dans la fonction, genre test if i!=0 then toto(i,j,k).

        Grosso modo l'erreur de prog ne vient pas quand on fait *p=0 mais quand on balade des pointeurs dont on ne connait pas le type et dont on ne sait pas si ils sont nuls ou pas dans un contexte tel que le préprocesseur ne peut pas faire la différence.

        A noter que les chances de se planter sont souvent infimes avec NULL et plutôt élevée avec 0, sauf si vous vous amusez a redéfinir NULL. Auquel cas a vous de vous débrouiller avec votre préprocesseur pour ne pas qu'il s'enmèle les pinceaux.


        Kha
        • [^] # Re: Petites precisions

          Posté par  . Évalué à 7.

          Certe pour le CPU et toussa.

          Mais :
          p = 0 ; p++

          c'est pas la même chose si p est un int ou un int *.
          Si c'est un int alors p = 1. Si c'est un (int *) alors p = 4 (si sizeof(int) = 4).

          Puis si p n'est pas un int, on passe à côté de toute l'arthémitique des pointeurs.
          p[3] n'a aucun sens si p est un int.

          Que l'adresse 0 soit valide ne change rien.
          i = 0 // initialisation avec un int
          i= (void *)0 // initialisation avec un pointeur (pour faire un reset :-))
          i = NULL // initialisation avec un pointeur invalide

          La norme ne dit pas que NULL est l'adresse 0. Le norme indique uniquement que c'est une adresse invalide en C.
          De même si tu lis la norme posix, on a souvent :
          - return NULL if ....
          Mais pas
          - return 0 if ...

          Donc il faut toujours faire les tests avec (p == NULL).

          Dans les bonnes règles de programmation ont rappèle toujours qu'il ne faut pas considérer qu'un pointeur est un entier.
          J'ai bossé sur Digital Unix et j'ai :
          sizeof(int) = 4
          sizeof(void *) = 8

          De même il ne faut jamais utiliser de int pour la différence de pointeur. Il faut utiliser le type ptr_diff.
          Ce sont des règles de basse. sinon c'est la galère pour porter une appli dans un environnement 64 bits.

          Je suis absolument d'accord avec Linus, il faut banir le '0' pour les pointeurs. Peut-être qu'un jour les compilateurs mettrons un gros warning lorsqu'un pointeur est initialisé avec un int. Car c'est fondamentalement une erreur. Les int et les pointeurs n'ont rien à voir.
          • [^] # Re: Petites precisions

            Posté par  . Évalué à 5.

            Je suis entièrement d'accord avec toi. Le principal reste cependant que le preproc puisse savoir a quoi il a à faire. Sinon ca part en vrille.

            i= (void *)0 // initialisation avec un pointeur (pour faire un reset :-))

            La le preprocesseur C t'enverra bouller. Il se dépechera de remplacer ton (void *)0 par une adresse particulière qui permettra au compilo de comprendre qu'il a à faire a un pointeur invalide. Généralement il faut passer par une autre macro.

            Au final tu as souvent un truc du genre
            {one pass preproc}
            (\*.*=) 0 -> %1 = &hFFFFFFFF (tout ce qui est de la forme *toto=0 devient *toto=&hFFFFFFFF ou &hFFFFFFFF est invalide)
            NULL -> &hFFFFFFFF
            (void\*)0 -> &hFFFFFFFF (tous les (void*)0 sont remplacés comme plus haut )
            ___reset -> &h00000000

            Bref les preprocesseurs essayent de se blinder contre les mecs qui rentrent la valeur 0 dans un pointeur et remplace avant compil tous les zéros rencontrés par une adresse invalide ou un jeu d'instruction pour que le compilo comprenne bien que ca n'est plus un pointeur.

            En bref si p est un pointeur
            p=0 sera remplacé de facon triviale par le préprocesseur et le pointeur p sera invalide.
            Derrière il sera impossible de faire un p++.

            De même si tu lis la norme posix, on a souvent :
            - return NULL if ....
            Mais pas
            - return 0 if ...


            Normalement avec la signature de la fonction on doit pouvoir s'en sortir. Si c'est un pointeur en retour alors 0=NULL, sinon c'est bien un 0 tout ce qu'il y a de plus standard.
            Bien sur si la fonction est bizaroide au point que l'on sait qu'elle retourne quelquechose mais que l'on ne sait pas si c'est un pointeur ou pas la on a un problème et le NULL est obligatoire. Mais il est théoriquement impssible de faire uen telle fonction.

            Dans les bonnes règles de programmation on rappèle toujours qu'il ne faut pas considérer qu'un pointeur est un entier.

            Faire ne serait-ce qu'une semaine d'assembleur permet de connaitre la différence pour toute la vie. Même si la mémoire est continue...

            Peut-être qu'un jour les compilateurs mettrons un gros warning lorsqu'un pointeur est initialisé avec un int.

            ben si tu fais
            int i=2;
            char *a=i;
            On peut remonter un warning (et c'est déjà le cas).

            Par contre si tu fais char *a=2 .... C'est valide et il faut que ca le reste, 2 est une adresse mémoire tout ce qu'il y a de plus valable.

            Ceci étant je suis également partisan du NULL en lieu et place du 0, mais c'est principalement pour la lisibilité. Quelqu'un qui veut utiliser des 0 a la place peut le faire si il est propre dans son code.

            Kha
            • [^] # Re: Petites precisions

              Posté par  (site web personnel) . Évalué à 2.

              > La le preprocesseur C t'enverra bouller.

              Le front-end peut être, mais le préprocesseur, surement pas.

              Pour rappel, le préprocesseur, c'est le truc qui s'occupe des lignes qui commencent par '#' et la substitution des macros. Le front-end, c'est le truc qui parse le source avant la génération de code.
              • [^] # Re: Petites precisions

                Posté par  . Évalué à 3.

                Pour rappel, le préprocesseur, c'est le truc qui s'occupe des lignes qui commencent par '#' et la substitution des macros. Le front-end, c'est le truc qui parse le source avant la génération de code.

                On a déjà un troll sur NULL on va pas aussi rentrer dans celui : "le front-end fait-il partie de l'etape de preprocessing".
                Pour ceux qui ne connaissent pas les deux réponses possibles sont :

                - Ben oui vu que c'est avant la compil
                - Ben non vu que l'init de la compil est une partie de la compil.

                Comme il arrive (vive M4, gloire a M4, longue vie a M4, je deteste M4...) qu'on est des echanges front-end <-> préparseur je range le front-end dans le preprocessing.

                Mais c'est mon opinion perso et j'ai pas envie de me faire flamer la tête une fois de plus sur ce sujet...

                Kha
                • [^] # Re: Petites precisions

                  Posté par  . Évalué à 2.

                  Si on est dans le contexte du C, alors le préprocesseur à une signification historique et désigne généralement le bidule qui transforme les #qqchose en autre chose. (c'était même un programme séparé)

                  Après si on s'interresse au fonctionnement interne des compilateurs on peut se dire que ceci ou cela _devrait_ être appelé préprocesseur, mais dans l'esprit d'un developpeur C (qui ne developpe pas de compilo :) le préprocesseur gère juste les directives et remplacement (par définition en plus).
                  • [^] # Re: Petites precisions

                    Posté par  . Évalué à 1.

                    > c'était même un programme séparé

                    C'est toujours un programme séparé.
                    Fais "cpp --help"

                    btw, cpp ne connais presque rien du C ou C++. C'est aussi un très bon outil à usage général.
        • [^] # Re: Petites precisions

          Posté par  (site web personnel) . Évalué à 6.

          La taille des pointeurs (et ce jusqu'au niveau des registres qui les stoquent) peut varier en fonction du contexte CPU.

          C'est pas le problème. Ça c'est la cuisine interne du compilateur, dépendant de la plateforme : ça ne se voit pas au niveau du source.

          Effectivement l'adresse 0 peut être valable, c'est pourquoi la norme parle de 'null pointer constant', ie une syntaxe spéciale pour désigner une adresse invalide. Cette syntaxe est 0 ou (void *)0 (quelle que soit la plateforme). La norme dit ensuite que si on convertit ça en un type pointeur, par exemple en faisant char *p = 0; ou char *p = NULL;, le compilo doit remplacer ça par une adresse invalide pour la plateforme.

          Le problème avec les vararg (ou avec une fonction sans prototype) est que la 'null pointer constant' ne va pas forcèment être convertie en un type pointeur à cause des règles de promotion.

          De toutes façons, ça n'est pas l'argument de Linus :) Son problème, c'est que la syntaxe spéciale 0 pour désigner un pointeur invalide est débile : un pointeur n'est pas un entier, il veut une syntaxe qui montre bien qu'on a affaire à un pointeur. Il veut voir NULL partout pour des raisons de style.
        • [^] # Re: Petites precisions

          Posté par  . Évalué à 5.

          Euh désolé, mais j'ai franchement l'impression que tu essayes de généraliser à partir d'un cas complètement exotique, mais bon j'ai un peu eu du mal à suivre quand même c'est pourquoi je vais tenter d'être clair (sans être sûr que je le serais plus que toi :-)...

          Cette histoire de prépocesseur qui remplace les 0 par des pointeurs invalides si le 0 n'est pas suffisament invalide m'a l'air complétement tordue!!!

          C'est peut être le cas sur certaines architectures où on veut à tout pris pouvoir exploiter l'adresse 0 au mépris de la portabilité (je n'ai aucune expérience de ces machines et c'est la première fois que j'en entend parler), mais en règle générale, en C, l'adresse 0 est considérée comme une adresse invalide. Point barre. C'est une convention indépendante du fait que le hardware accepte les lectures ou écriture à l'adresse 0 (par 0 j'entend « (void*) 0 » bien sûr) .

          Quand tu as une fonction qui retourne un pointeur 0 (ou NULL), ou une une variable pointeur contenant 0, on considère que ce pointeur est vide et ne pointe nulle part, et qu'il ne faut surtout pas lire ou écrire l'adresse pointée.

          Si tu as : char *p = NULL; //(ou 0)
          et que tu fais un : « if (*p) {...} » ou « *p='x' ; »
          le compilateur ne fera pas d'erreur (on va voir pourquoi) mais c'est pourtant incorrect!

          Maintenant il se peut que tu fasses de la programmation bas niveau et que tu aies besoin d'écrire à l'adresse 0 (tu veux par exemple fixer un vecteur d'interruption), tu as tout à fait le droit de faire un :
          #define RESET_VECTOR (void*) 0
          puis un : RESET_VECTOR = mon_gestionnaire_de_reset;
          Il faut cependant remarquer qu'il s'agit alors d'une constante... et c'est effectivement pour ça que le compilateur te laisse accéder à 0 comme tu le veux et que cette convention du « 0 <=> adresse invalide » n'est pas connu du compilateur.

          Si tu as un système où l'adresse 0 est de la RAM comme à l'adresse 0x123456 (par ex), il sera malheureusement impossible de l'exploiter normalement, comme par exemple si l'adresse était renvoyée par un malloc(), car si cette fonction te renvoie 0, ça ne signifie pas que tu as un bloc alloué à l'adresse 0, mais qu'il n'a pas été possible de faire l'allocation, car la convention « 0 <=> invalide » est par contre intégrée dans toute bibliothèque un minimum standard. Rien ne t'empêche cependant de réécrire un malloc() qui renvoie 0xFFFFFFFF si l'allocation ne peut pas se faire pour pouvoir exploiter normalement ton système, mais je t'avouerais que je préfèrerais perdre 1 octet de RAM plutôt que tenter de réinventer un ersatz de langage C avec des conventions différentes.


          Ça a d'ailleurs été un problème important rencontré par Dave Small (pour les ex-lecteurs de STMAG...) lorsqu'il a codé son émulateur Mac sur ST, car le Mac autorisait la lecture de l'adresse 0, alors que le ST déclanchait une erreur de bus. Beaucoup de programmeurs sous MAC n'hésitaient pas écrire du code tel que ça :
          if ( *p == TRUCMUCHE && p ) {...}
          qui marchait sur un MAC mais plantait sur un ST (il fallait bien sûr inverser les 2 tests pour que cela soit correct), et il a donc dû bidouiller comme un fou pour récupérer l'erreur de bus et continuer le programme comme si de rien n'était, ce qui était théoriquement impossible, et qui lui a donc vallu beaucoup de mérite de tous les petits programmeurs en herbe comme moi (et sans d'autre porgrammeurs plus expérimenté aussi!)
          • [^] # Re: Petites precisions

            Posté par  . Évalué à 3.

            Tout dépend si tu considère la norme ou l'usage.
          • [^] # Re: Petites precisions

            Posté par  . Évalué à 0.

            Pour le compilateur, l'adresse 0 (ou "(void *)0") est une adresse comme une autre.
            Si tu veux exécuter une fonction à cette adresse, le compilateur n'aura rien à dire.
            Si tu veux déférencer un pointeur pour lire ce qu'il y a à l'adresse 0, le compilateur te laisse totalement libre de le faire aussi.

            Par contre le noyau fera planter le programme vite fait. Mais pas le compilateur.

            btw, la norme du C ne dis pas que 0 ou "(void *)0" est un pointeur invalide. La norme C ("#include <stdlib.h>") dit uniquement que NULL est un pointeur invalide et cette norme ne donne pas la valeur de NULL (ce qui est normal puisque c'est invalide).
            • [^] # Re: Petites precisions

              Posté par  (site web personnel) . Évalué à 3.

              Il y a confusion entre ce que la norme appelle 'null pointer constant' et 'null pointer'.

              Un 'null pointer' est une adresse invalide, dépendant de la plateforme. C'est une valeur.
              0 ou (void *)0 est une 'null pointer constant' (par définition), c'est une expression, un élément du langage.

              Ces deux trucs n'appartienent pas au même monde: un 'null pointer' c'est un truc qui existe dans le programme compilé et lors de l'éxecution, un 'null pointer constant' ça existe dans le code source.

              Le norme dit que quand tu stocke un 'null pointer constant' dans une variable de type pointeur, le compilateur utilise la valeur 'null pointer'. La norme ne donne pas la valeur d'un 'null pointer', mais elle donne la valeur de NULL: c'est une macro qui resprésente une 'null pointer constant'.
              • [^] # Re: Petites precisions

                Posté par  . Évalué à -1.

                Ouais.

                > le compilateur utilise la valeur 'null pointer'

                C'est pas vraiment lié au compilateur. C'est la librairie C qui """gère""" ça.
                Si tu ne fais pas "#include <stdlib.h>", tu as une erreur de compilation :
                NULL : symbol non défini.
                • [^] # Re: Petites precisions

                  Posté par  (site web personnel) . Évalué à 2.

                  > C'est pas vraiment lié au compilateur.

                  Si.

                  > C'est la librairie C qui """gère""" ça. Si tu ne fais pas "#include
                  > <stdlib.h>", tu as une erreur de compilation :
                  > NULL : symbol non défini.

                  La norme dit que NULL est une macro. Si tu n'include pas stdlib.h, le préprocesseur laisse passer NULL et le compilo dit effectivement ça.

                  Maintentant la norme dit:
                  An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.

                  The macro NULL is defined in <stddef.h> as a null pointer constant.

                  Le préprocesseur remplace NULL par 0 (ou (void *)0 ou 0L ou 0U, etc) et c'est bien le compilateur (ou le front-end, en tout cas c'est pas la lib) qui a la charge de convertir ce 'null pointer constant' en 'null pointer'.
                  • [^] # Re: Petites precisions

                    Posté par  . Évalué à 0.

                    > c'est bien le compilateur (ou le front-end, en tout cas c'est pas la lib) qui a la charge de convertir ce 'null pointer constant' en 'null pointer'.

                    Mouaif...
                    J'ai beaucoup codé en C (plus actuellement mais je vais m'y remettre) et j'ai jamais vu un compilateur traiter différement NULL ou "(void*)0" ou "(void*)1" ou "(void*)29898".

                    Exemple :
                    tmp1.c :
                    #include <stdio.h>
                    int main(void) {
                    char *p = NULL ;
                    printf("%c\n", *p) ;
                    return 0 ;
                    }

                    tmp2.c :
                    #include <stdio.h>
                    int main(void) {
                    char *p = (char *)2 ;
                    printf("%c\n", *p) ;
                    return 0 ;
                    }

                    gcc -o tmp1 tmp1.c
                    gcc -o tmp2 tmp2.c
                    cmp -l tmp1 tmp2
                    900 0 2

                    Aucun traitement particulier n'est fait pour NULL et ça "Segmentation fault" de la même façon (normal).
                    Tu peux essayer d'autres exemples, NULL ou (void *)0 ou 0 est traité comme un pointeur "normal".
                    • [^] # Re: Petites precisions

                      Posté par  (site web personnel) . Évalué à 2.

                      La norme ne dit pas que ça doit être traité spécialement, mais que ça peut.
                      An integer may be converted to any pointer type. Except as previously specified, the result is implementation-defined, might not be properly aligned, and might not point to an entity of the referenced type.
                      (le previously defined, c'est ce que je cite plus haut)

                      Le cast d'un entier en pointeur est dépendant de l'implémentation, sauf pour le cas des 'null pointer constant'. Dans ce cas, ça produit un 'null pointer' et la norme spécifie alors plus de choses, notamment le résultat des comparaisons et conversions de null pointer entre eux ou avec des pointeurs valides.

                      Déréférencer un (char *)2, ça pourrais marcher, c'est pas incompatible avec la norme. Déréférencer un null pointer, c'est jamais valide, ça produit un UB (undefined behavior).
                      • [^] # Re: Petites precisions

                        Posté par  . Évalué à -1.

                        Je ne dis pas que tu as tord sur la norme (de tout manière, j'ai pas la norme sous la main...). Je dis simplement que je n'ai jamais vu d'implémentation qui traite de façon particulière un null pointer. C'est tout.

                        > notamment le résultat des comparaisons et conversions de null pointer entre eux ou avec des pointeurs valides.

                        Si c'est dans l'esprit SQL :
                        char * f (char *p, char * t) {
                        return (p + t) -t ;
                        }
                        si f(NULL, "titi") alors NULL doit être retourné.

                        Ceci demande des vérifications à l'exécution et ce'est pas dans l'esprit du language C.

                        Ceci dit, un "#define NULL ((void *)xx)" sans traitement particulier du compilateur me convient parfaitement.
                        De plus je ne fais jamais de "if (p)", "if (!p)" ou "if (p=malloc(2)) je fais toujours "if (p != NULL)", "if (p == NULL)" et "if ((p = malloc(2)) != NULL).
                        Notes que je ne suis pas le seul à faire comme ça (je ne parle pas de Linux).

                        D'usage rare, la macro isnull est sympa :
                        #define isnull(p) \
                        ((p) == NULL)

                        If faut toujours utiliser NULL et ne pas considérer que NULL est égale 0. Comme ça il y a pas de problème.

                        Je pense que les compilateurs devraient mettre un warning pour les "if (p)", "if (!p)". Ça n'a pas la même signification qu'avec les autres types.
                        • [^] # Re: Petites precisions

                          Posté par  (site web personnel) . Évalué à 1.

                          voir http://www.eskimo.com/~scs/C-faq/q5.17.html(...) pour des exemples d'archi aux pointeurs bizarres.
                          char * f (char *p, char * t) {
                          return (p + t) -t ;
                          }
                          Pas du C, on ne peut pas additionner deux pointeurs.

                          De plus je ne fais jamais de "if (p)", "if (!p)" ou "if (p=malloc(2)) je fais toujours "if (p != NULL)", "if (p == NULL)" et "if ((p = malloc(2)) != NULL).
                          Notes que je ne suis pas le seul à faire comme ça (je ne parle pas de Linux).
                          Moi aussi je fais comme ça, mais les premières écritures sont aussi valide et ont le même sens.

                          If faut toujours utiliser NULL
                          C'est purement stylistique comme recommendation

                          et ne pas considérer que NULL est égale 0.
                          Effectivement, il faut pas (elle peut être égale à (void *)0.

                          Ça n'a pas la même signification qu'avec les autres types.
                          Si c'est pareil, on compare p avec 0, une 'null pointer constant'. http://www.eskimo.com/~scs/C-faq/q5.3.html(...)
                          • [^] # Re: Petites precisions

                            Posté par  . Évalué à 0.

                            > Pas du C, on ne peut pas additionner deux pointeurs.

                            Oui on peut (même si généralement c'est une très mauvaise idée). Il faut qu'il soit du même type et pas du type void* .

                            > > Ça n'a pas la même signification qu'avec les autres types.
                            > Si c'est pareil, on compare p avec 0

                            J'ai dis "signification".

                            Sur l'aspect signification, ce n'est pas la même chose.
                            p = 0 // adresse 0
                            p = NULL // adresse invalide ou non renseigné, etc

                            Si à NULL tu lui donne la signification de '0', effectivement c'est la même c'est la même signification :-).
                            Mais la "notation" NULL est bien là pour différencier de '0'.
                            '0' indique 'rien'. Pas de patates cette semaine, 0 Francs de solde sur le compte. Mais NULL en solde sur le compte j'ai pas encore vu ça :-) Ou alors on veux indisquer que le compte est NULL (invalide ou n'existe pas). Dans ce cas c'est compte = NULL et non pas solde = NULL.

                            Beaucoup de language font la distinction entre NULL et 0. Par exemple SQL (limite casse couille leur convention) et php (qui ajoute aussi la distinction set/unset puisque qu'il n'y a pas de déclaration de variable).
                            Avec php (j'ai un peu oublié la syntax) :
                            isnull(toto) => false ; toto n'est pas défini
                            toto = NULL
                            if (toto) => erreur/warning, on ne peut pas évaluer
                            toto == 0 => false ; toto est à NULL et pas à '0'
                            toto = 0
                            isnull(toto) => false ; toto n'est pas NULL, mais à '0'

                            Le C est un peu particulier. Il n'y a que les adresses qui peuvent être NULL (toujours sur l'aspect signification) et tu peux aussi utiliser l'adresse 0 (pour faire un reset ou d'autre truc de très bas niveau). Il n'y a pas de int/float/etc NULL. Ceci pour des raisons de performances sinon il faut vérifier à l'exécution. Un int/float/etc a toujours une valeur valide (sauf pour float avec certain librairie de calcul si on chipote).
                            • [^] # Re: Petites precisions

                              Posté par  (site web personnel) . Évalué à 1.

                              > Pas du C, on ne peut pas additionner deux pointeurs.

                              Oui on peut (même si généralement c'est une très mauvaise idée). Il faut qu'il soit du même type et pas du type void* .

                              Non, on peut pas. Je vais pas te citer la norme, parce que c'est lourd, mais non on peut pas. On peut soustraire deux pointeurs par contre.

                              Sur l'aspect signification, ce n'est pas la même chose.
                              p = 0 // adresse 0
                              p = NULL // adresse invalide ou non renseigné, etc

                              Bon j'abandonne, c'est sans espoir. C'est deux instructions sont complètement équivalentes et ont exactement la même sémantique (pour peu que p soit un pointeur, hein).
                              • [^] # Re: Petites precisions

                                Posté par  . Évalué à 0.

                                > Non, on peut pas.

                                Non, on peut pas :-)
                                J'ai mélangé les pinceaux.

                                > C'est deux instructions sont complètement équivalentes et ont exactement la même sémantique (pour peu que p soit un pointeur, hein).

                                Oui c'est complètement équivalent et a la même sémantique.
                                Si on se "limite" à ça, c'est complètement inutile utiliser NULL au lieu de 0 (3 caractères de plus sans aucune valeur ajoutée).
                                Je parle de signification.

                                Puisque 0 est équivalent à null pointer (car le compilo fait une "bidouille") que ce passe-t'il dans ces cas :
                                static int i ; // équivalent à static int i = 0
                                static char* p ;
                                static char *p[10] ; // tableau de pointeur à 0 (bit à 0) ou null pointeur ?
                                static union {
                                long i[10] ;
                                void * p[10] ;
                                } titi ;

                                Tous les pointeurs sont initialisés à 0 (bits à 0) ou à null pointeur (qui n'est pas forcément 0) ?

                                char * p1 = "toto" ;
                                char * p2 = p1 ;
                                void * p = (void *)(p1 - p2) ;
                                C'est équivalent à "(void *)0" . Mais 0 ou null pointer pour p ?
                                Normalement c'est 0 et pas null pointer. La soustraction de deux trucs valide ne donne jamais NULL. Ici la réponde est 0 et pas null pointeur mais si c'est la même chose qui est stocké sur certaine implémentation.

                                Dans 99,9 % des cas, on peut remplacer NULL par 0, mais ça n'a pas la même signification.
                                • [^] # Re: Petites precisions

                                  Posté par  (site web personnel) . Évalué à 1.

                                  static char* p ;
                                  p est un null pointer

                                  static char *p[10]
                                  p est un tableau de 10 null pointer

                                  static union {
                                  long i[10] ;
                                  void * p[10] ;
                                  } titi ;

                                  bits à 0 (pas forcèment null pointer) car le premier élément est initialisé, donc ici des longs.

                                  char * p1 = "toto" ;
                                  char * p2 = p1 ;
                                  void * p = (void *)(p1 - p2) ;

                                  0 pour p

                                  La soustraction de deux trucs valide ne donne jamais NULL.
                                  ça n'a pas de sens, NULL est une macro

                                  Dans 99,9 % des cas, on peut remplacer NULL par 0
                                  100%
                                  • [^] # Re: Petites precisions

                                    Posté par  . Évalué à 0.

                                    > > static char* p ;
                                    > p est un null pointer

                                    Non, c'est initialisé à 0. C'est le chargeur qui fait ça. C'est peut-être pas dans la norme mais c'est comme ça. Le chargeur ne s'occupe pas de savoir si c'est une zone qui stocke des pointeurs ou des int, etc... Le chargeur voit une grosse zone (pour toutes les variables statiques) et met tout à 0.

                                    Si tu as la norme sous main, copie ici comment est initialisé une zone static (C'est aussi pour mon information car je n'ai pas la norme, btw, si tu as un pointeur sur la norme, je suis preneur).

                                    > > void * p = (void *)(p1 - p2) ;
                                    > 0 pour p

                                    Pourtant tu dis : '0' même signification que '(void *)0' même signification que null pointeur. Or ici j'ai bien '(void *)0' (c-à-d null pointeur) et tu dis que c'est 0. Pas claire.

                                    > ça n'a pas de sens, NULL est une macro

                                    Remplace NULL par "null pointer" si tu veux.

                                    > 100%
                                    Non. pour varargs, ça ne marche pas.
                                    Heureusement, gcc a la délicatesse de le dire :
                                    printf("%p\n", 0) ;
                                    test.c:3: attention : l'argument de format n'est pas un pointeur (arg 2)
                                    avec :
                                    fprintf("%p\n", NULL) ;
                                    Pas de warning, c'est correct.

                                    Avec c++ ça ne marche pas avec la surcharge. 0 reste un int. (void*)0 est un pointeur et NULL est null pointeur (implémentation dépendant de la plate forme).

                                    Exemple :
                                    test.c
                                    #include <stdio.h>
                                    void f(int i) {
                                        printf("f(int)\n") ;
                                    }
                                    void f(void *s) {
                                         printf("f(void *)\n") ;
                                    }
                                    void f(char *s) {
                                         printf("f(char *)\n") ;
                                    }
                                    void f(long u) {
                                         printf("f(long)\n") ;
                                    }
                                    int main(void) {
                                         f(0) ;
                                         f((void*)0) ;
                                         f((char*)0) ;
                                         f(0L) ;
                                         f(NULL) ;
                                         return 0 ;
                                    }


                                    g++ -Wall test.c
                                    test.c: Dans function « int main() »:
                                    test.c:15: attention : passing NULL used for non-pointer converting 1 of `void f(int)'

                                    NULL est remplacé par __null qui est un mot clé de g++.
                                    Mais d'autres implémentations ont un truc du style :
                                    # ifdef __cplusplus
                                    #     define NULL (0L)
                                    # else
                                    #     define NULL ((void*) 0)
                                    # endif


                                    Exécution :
                                    ./a.out
                                    f(int)
                                    f(void *)
                                    f(char *)
                                    f(long)
                                    f(int)


                                    Donc ce n'est pas la même chose et surtout pas à 100 %.
                                    • [^] # Re: Petites precisions

                                      Posté par  (site web personnel) . Évalué à 1.

                                      > > static char* p ;
                                      > p est un null pointer

                                      Non, c'est initialisé à 0.


                                      Si, section 6.5.8 'Initialization', paragraphe #10 :
                                      If an object that has static storage duration is not initialized explicitly, then:

                                      - if it has pointer type, it is initialized to a null
                                      pointer;
                                      pour la norme: http://www.vmunix.com/~gabor/c/draft.html(...)
                                      j'en ai une version dans un .txt (je sais plus où je l'ai trouvée), compréssée, elle ne fait que 288K :) !


                                      > > void * p = (void *)(p1 - p2) ;
                                      > 0 pour p

                                      Pourtant tu dis : '0' même signification que '(void *)0' même signification que null pointeur. Or ici j'ai bien '(void *)0' (c-à-d null pointeur) et tu dis que c'est 0. Pas claire.


                                      NULL, c'est '0' ou '(void *)0' mais avec un zéro comme constante (le caractère 48 du charset US-ASCII si tu préfères :).
                                      De la même façon,
                                      int i = 0
                                      char *p = (void *)i;
                                      n'est pas forcèment un null pointer.

                                      > 100%
                                      Non. pour varargs, ça ne marche pas.
                                      Heureusement, gcc a la délicatesse de le dire :
                                      printf("%p\n", 0) ;
                                      test.c:3: attention : l'argument de format n'est pas un pointeur (arg 2)

                                      oui, gcc inspecte les chaîne de format pour faire la véréfication des types, c'est gentil de sa part.

                                      avec :
                                      fprintf("%p\n", NULL) ;
                                      Pas de warning, c'est correct.


                                      Parce que:
                                      1) sur ta plateforme NULL est défini par '(void *)0'
                                      2) la fonction attend un void * (à cause du %p)

                                      Mais:
                                      1) si tu compiles ça sur une autre implémentation avec NULL défini à 0 (ce qui est légal dixit la norme), ça va pas marcher
                                      2) pour une vararg attendant un type (toto *), il faut caster le NULL de toutes façons, même s'il est défini par (void *)0 car il se peut que:
                                      sizeof(toto *) != sizeof (void *)
                                      • [^] # Re: Petites precisions

                                        Posté par  . Évalué à 0.

                                        > http://www.vmunix.com/~gabor/c/draft.html(...)

                                        Merci, je bookmark :-)

                                        > sizeof(toto *) != sizeof (void *)

                                        Non. sizeof(int*), sizeof(toto*), sizeof(n_importe_quoi *) est égale à sizeof(void *).

                                        C'est pour celà que des trucs tordus comme ci-dessous sont possibles :
                                        typedef struct list * plist ;
                                        static size_t taille = sizeof (struct list *) ;
                                        static size_t taille2 = sizeof (struct list) ; // invalide => invalid application of `sizeof' to an incomplete type
                                        static size_t taille3 = sizeof (plist) ;
                                        struct list {
                                             struct list * prec ;
                                             struct list * next ;
                                             data ;
                                        } ; // la taille de "struct list" n'est connu que ici.
                                        static size_t taille4 = sizeof (struct list) ; // valide => le taille est maintenant connue.

                                        Ceci marche car "struct list *" est connu en taille avant même sa déclaration et la taille est sizeof (void *) (ou (n_importe_quoi *)).

                                        par contre sur certains plate forme (64 bits notament) sizeof(0) != sizeof(void *).
                                        • [^] # Re: Petites precisions

                                          Posté par  (site web personnel) . Évalué à 1.

                                          > sizeof(toto *) != sizeof (void *)

                                          Non. sizeof(int*), sizeof(toto*), sizeof(n_importe_quoi *) est égale à sizeof(void *).


                                          Bah, maintenant que tu as la norme tu vas pouvoir me montrer à quel chapitre/paragraphe ça dit ça :-D. Te fatigues pas, tu vas pas trouver, ça n'est pas le cas. Rien (dans la norme) n'indique que les pointeurs de différents types ont la même taille. Par exemple certaines implémentations tordues peuvent avoir un sizeof (char *) plus grand que sizeof (int *) parce que l'accés mémoire est plus compliqué pour adresser un byte tout seul plutôt qu'un int aligné. Ou un pointeur de fonction peut avoir une taille différente de celle des pointeurs de données.

                                          Ceci marche car "struct list *" est connu en taille avant même sa déclaration
                                          c'est parce que tous les pointeurs vers des struct ont la même taille

                                          et la taille est sizeof (void *)
                                          pas nécessairement.
                                          • [^] # Re: Petites precisions

                                            Posté par  . Évalué à 0.

                                            > c'est parce que tous les pointeurs vers des struct ont la même taille

                                            struct titi {
                                            int entier ;
                                            } ;
                                            Et voilà, j'ai une structure qui fait la taille d'un int qui s'utilise comme un int, etc...
                                            De plus la norme (oui oui), autorise :
                                            (n_importe_quoi *) = (void *)
                                            et
                                            (void *) = (n_importe_quoi *)

                                            n_importe_quoi peut-être un char.
                                            Donc je me demande comme un (void *) peut ne pas voir la taille d'un (char *) alors qu'il peut stocker un (char *) et vice versa.
                                            btw, un malloc retourne toujours un (void *) et malloc peut-être utilisé pour tous les pointeurs (même (char *)).
                                            Merci de me trouver la réponse...
                                            • [^] # Re: Petites precisions

                                              Posté par  (site web personnel) . Évalué à 1.

                                              Et voilà, j'ai une structure qui fait la taille d'un int qui s'utilise comme un int, etc...

                                              gni ? le rapport ... ?

                                              De plus la norme (oui oui), autorise :
                                              (n_importe_quoi *) = (void *)
                                              et
                                              (void *) = (n_importe_quoi *)


                                              la norme autorise la conversion d'un pointeur quelconque en un pointeur générique (void *), et réciproquement, sans perte d'information. De la même façon que tu peux convertir un int en long et tu retrouves la même valeur quand tu reconvertis le long en int. Pourtant int et long n'ont pas nécessairement la même taille.

                                              Bref sizeof (void *) == sizeof (char *) >= sizeof (autre_chose *)
                                              A pointer to void shall have the same representation and alignment requirements as a pointer to a character type. [...] All pointers to structure types shall have the same representation and alignment requirements as each other. All pointers to union types shall have the same representation and alignment requirements as each other. Pointers to other types need not have the same representation or alignment requirements.


                                              pour malloc:
                                              The pointer returned if the allocation succeeds is suitably aligned so that it may be assigned to a pointer to any type of object and then used to access such an object or an array of such objects in the space allocated
                                              • [^] # Re: Petites precisions

                                                Posté par  . Évalué à 0.

                                                > Bref sizeof (void *) == sizeof (char *) >= sizeof (autre_chose *)

                                                Tu as raison (pas seulement au niveau norme).
                                                D'un point vu implémentation à cause (ou grace) à l'alignement c'est assez logique pour les petits systèmes (ou très spécialisés).

                                                Mais compte-tenu de ça (sizeof (char|void *) >= sizeof (autre_chose *)), il peu y avoir perte d'information lors d'un (autre_chose *) = (char *). Reste aussi les problèmes d'alignement "classiques" que j'ai déjà rencontré. Je devais récupérer un fichier binaire sur Dec/Alpha et j'ai bien galéré avec l'alignement (alignement de 4 pour les long et alignement de 8 pour les struct (pointeur) contre 2 et 4 pour le système d'origine (Vax)).
          • [^] # Re: Petites precisions

            Posté par  . Évalué à 2.

            Cette histoire de prépocesseur qui remplace les 0 par des pointeurs invalides si le 0 n'est pas suffisament invalide m'a l'air complétement tordue!!!

            Tu as de la chance. C'est juste que tu n'as jamais bossé sur des archis ou dans des contextes ou l'adresse 0 est interressante. Pour le coté exotique je citerais entre autres:
            - Dos et variantes
            - Windows avant 1995
            - OS2 et variantes
            -Mac OS jusqu'à 8 (et un peu 9 aussi mais moins)
            - Processeurs zilog (Z80 et consors)
            etc.

            Cette histoire de prépocesseur qui remplace les 0 par des pointeurs invalides si le 0 n'est pas suffisament invalide m'a l'air complétement tordue!!!

            Ben pourtant. Si l'adresse 0 existe et est utile, il faut bien pouvoir y acceder non ? De fait il faut bien que le compilo puisse faire la différence entre un pointeur vers l'adresse 0 valide et un autre qui est invalide. Au final c'est la front-end (on va faire plaisir aux codeurs C) qui s'en occupe.

            C'est peut être le cas sur certaines architectures où on veut à tout pris pouvoir exploiter l'adresse 0 au mépris de la portabilité

            Au moment ou la base de ces architectures a été créé on vérifiait la comptabilité fortran, éventuellement CP/M. On a gardé ce genre d'architecture après pour la compatibilité avec leurs ancètres.

            Il faut cependant remarquer qu'il s'agit alors d'une constante... et c'est effectivement pour ça que le compilateur te laisse accéder à 0 comme tu le veux et que cette convention du « 0 <=> adresse invalide » n'est pas connu du compilateur.

            C'est bienle prob, quelqu'un doit la traiter en amont sur les architectures ou 1) l'adresse 0 est valide et 2) on veut la compatibilité avec le C dans le reste du monde. Donc c'est toute l'étape de preprocessing qui s'en occupe.


            Rien ne t'empêche cependant de réécrire un malloc() qui renvoie 0xFFFFFFFF si l'allocation ne peut pas se faire pour pouvoir exploiter normalement ton système

            Alors là tu vas reveillr un troll qui n'a pas revu le jour depuis des années. Avant le C, les fonction qui retournait des valeurs (Bonc OK les jump sub routine qui remplissait des registres) se comportait comme suit :
            regsitre a 0 : tout s'est bien passé
            registre a N (N>0) : erreur n°N
            registre a -1 : instruction illégale
            Quand le C est parti pour l'inverse il y a eu des grincement de dents.
            Perso j'utilise toujours la vieille notation pour mes progs a moi, avec un gros entete dans mes fichiers. Bon ca c'est pour les fonctions en général.
            Pour malloc en particulier il renvoit un pointeur vers la zone mémoire libérée, lequel est éventuellement NULL.
            Et citer malloc ici ne fait pas vraiment avancer le débat : Si mon preprocessing corrige les sorties à 0 des fonctions dont la signature indique qu'ils renvoient un pointeur, il corrigera celle de malloc avant la compil. Donc je ne casserai aucune compatibilité source vu que la correction sera faite par le prepro/front end en aval du source...


            mais je t'avouerais que je préfèrerais perdre 1 octet de RAM plutôt que tenter de réinventer un ersatz de langage C avec des conventions différentes.

            Aujourd'hui tout le monde fait ca. Mais a l'epoque ou on achetait les 2Mo de ram pour plusieurs milliers de francs et ou on ne pouvait accéder a la mémoire que par pages de 4ko la simple pensée de fiche en l'air 512 octets par barrette ne faisait rire personne.

            Pour l'atari ST, il s'agit ici d'émulation donc de compatibilité au niveau binaire. Il est clair que l'on ne peut pas surveiller toutes les allocations et tous les jumps pour vérifier que l'adresse ne vaut pas 0. D'ou le hack.


            Kha
            Qui se sent vieux, mais vieux...
  • # La preuve de la supériorité absolue de Java

    Posté par  . Évalué à 2.

    N'empêche que si le noyau avait été écrit en Java, il n'y aurait pas eu ce genre de problème... et dire qu'il y a encore des gens pour prétendre le contraire...
  • # C != C++

    Posté par  . Évalué à 9.

    En C ok, on peut utiliser la macro NULL mais en C++ il est préférable d'utiliser le '0'.

    Extrait du livre 'La langage C++, b.Stroustrup' :
    "En langage C, il arrive fréquemment qu'une macro NULL soit définie pour représenter le pointeur 0. En C++, un contrôle de type plus sérieux rend moins problématique l'utilisation du 0 normal, plutôt que de toute macro NULL. Le nombre de problèmes et ainsi considérablement réduit."
    • [^] # Re: C != C++

      Posté par  (site web personnel) . Évalué à 10.

      En même temps, je te conseille pas d'aller proposer un passage au C++ sur la lkml ;)
    • [^] # Re: C != C++

      Posté par  . Évalué à 2.

      > en C++ il est préférable d'utiliser le '0'.

      C'est pas ce que dit Stroustrup.

      Il dit :
      - "un contrôle de type plus sérieux rend moins problématique l'utilisation du 0 normal"

      C'est différent.

      Puis ça reste un problème :
      - "Le nombre de problèmes est ainsi considérablement réduit."

      Il est réduit, il n'est pas supprimé. Donc même en C++ il faut utiliser NULL.
    • [^] # Re: C != C++

      Posté par  . Évalué à 1.

      et voila ce que dit Linus sur ça :-) :
      "The fact is, when somebody else picks up a mistake, that doesn't make it
      any less of a mistake. And it's not like C++ is the paragon of good taste
      anyway
      ."
  • # Ahem ;-)

    Posté par  (site web personnel) . Évalué à 3.

    http://wiki.gnuarch.org/moin.cgi/tla_20Code_20Style(...)

    5. Types
    NULL is unused. Testing for validity should use 0 instead of NULL. !foo is also often used.


    Nan rien, c'est juste pour relancer le troll ;-)
    • [^] # Re: Ahem ;-)

      Posté par  . Évalué à 1.

      A lire le reste et pour résumer on peut dire que arch ne fait rien comme Linux.

      http://lwn.net/Articles/93580/(...)

      Y a pas à dire, j'adore Linus. Son "coding style" c'est le mien depuis des lustres.
    • [^] # Re: Ahem ;-)

      Posté par  . Évalué à 2.

      On peut s'attendre à tout de gens qui préconisent l'affreux placement des accolades à la GNU ^^
  • # Trop violent

    Posté par  . Évalué à 3.

    and anybody who writes code like the above either needs to get out of the
    kernel, or needs to get transported to the 21st century.


    Bah en C++ je crois que la norme précise que 0 == NULL. Ceci étant dit le noyeau est en C, et donc confondre les deux est mal, mais je ne vois pas en quoi écrire ca c'est ce croire forcement au XXè siecle. On peut aussi s'être trompé de langage :)
    • [^] # Re: Trop violent

      Posté par  . Évalué à 1.

      > Bah en C++ je crois que la norme précise que 0 == NULL

      Tu me la trouve la norme qui dit ça ?
      NULL conserne la librairie standard C (stdio.h stdlib.h, etc...). Le compilateur ne traite pas de façon spécial NULL (qui est généralement une macro vers "((void*)0)" mais c'est un "détail" d'implémentation qui doit être ignoré).
      NULL n'est pas un mot clé du C ou du C++.
    • [^] # Re: Trop violent

      Posté par  . Évalué à 2.

      Bah en C++ je crois que la norme précise que 0 == NULL.

      pas tout a fait presque mais pas du tout.

      En fait en C++ les pointeurs on une structure forte, et le typage (pourtant pas violent en C++) assure que la valeur 0 va êre encadré comme il faut. A parir de là comme BS n'aime pas du tout les maccros il dit qu'il vaut beaucoup mieux faire p=0 que p=NULL.
      En effet dans le cas p=0 le compilo va reperer le pointeur et mettre le 0 dans une structure qui va bien.

      Par contre si on a une fonction de type :
      f(a) ou a peut être soit un entier soit un pointeur et que l'on apelle f(0) (ou f(5) d'ailleurs) ca sera toujours pris comme étant un entier.

      Donc a part pour "affecter" un pointeur l'écriture de 0 en leiu et place de NULL est pas forcément conseillée, mais si on sait ce que l'on fait ca passe très bien.

      Kha
      • [^] # Re: Trop violent

        Posté par  . Évalué à 0.

        > f(a)

        surtout avec la surcharge en C++ . S'il y a f(int) et f(void*) :
        f(0) et f(NULL) n'appelle pas la même fonction.

        Donc (0 != NULL) même en C++.
  • # les pointeurs nuls

    Posté par  (site web personnel) . Évalué à 1.

    Pour ceux que ça intéresse, la FAQ de comp.lang.c a des entrées trés trés complètes sur les pointeurs nuls :

    http://www.eskimo.com/~scs/C-faq/s5.html(...)

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.