Est-il possible d'initialiser un pointeur C sur NULL?

90

J'avais écrit des choses comme

char *x=NULL;

en supposant que

 char *x=2;

créerait un charpointeur vers l'adresse 2.

Mais, dans le didacticiel de programmation GNU C, il est dit que int *my_int_ptr = 2;stocke la valeur entière 2dans n'importe quelle adresse aléatoire my_int_ptrlorsqu'elle est allouée.

Cela semblerait impliquer que le mien char *x=NULLassigne quelle que soit la valeur de NULLcast à a charest à une adresse aléatoire en mémoire.

Tandis que

#include <stdlib.h>
#include <stdio.h>

int main()
{
    char *x=NULL;

    if (x==NULL)
        printf("is NULL\n");

    return EXIT_SUCCESS;
}

imprime en fait

est NULL

lorsque je le compile et l'exécute, je crains de me fier à un comportement non défini, ou du moins à un comportement sous-spécifié, et que je devrais écrire

char *x;
x=NULL;

au lieu.

fagricipni
la source
72
Il y a une différence très déroutante entre ce qui int *x = whatever;fait et ce qui int *x; *x = whatever;fait. int *x = whatever;se comporte réellement comme int *x; x = whatever;, non *x = whatever;.
user2357112 prend en charge Monica le
78
Ce tutoriel semble avoir mal compris cette distinction déroutante.
user2357112 prend en charge Monica le
51
Tant de tutoriels merdiques sur le web! Arrêtez immédiatement de lire. Nous avons vraiment besoin d'une liste noire SO où nous pouvons publiquement honte des livres merdiques ...
Lundin
9
@MM Ce qui ne le rend pas moins merdique en 2017. Compte tenu de l'évolution des compilateurs et des ordinateurs depuis les années 80, c'est fondamentalement la même chose que si j'étais médecin et que je lisais des livres de médecine écrits au 18ème siècle.
Lundin
13
Je ne pense pas que ce tutoriel qualifie de « La programmation GNU C Tutorial » ...
marcelm

Réponses:

114

Est-il possible d'initialiser un pointeur C sur NULL?

TL; DR Oui, beaucoup.


La réclamation réelle faite sur le guide se lit comme suit

En revanche, si vous n'utilisez que la seule affectation initiale int *my_int_ptr = 2;, le programme essaiera de remplir le contenu de l'emplacement mémoire pointé par my_int_ptravec la valeur 2. Comme my_int_ptrest rempli de déchets, il peut s'agir de n'importe quelle adresse. [...]

Eh bien, ils ont tort, vous avez raison.

Pour l'instruction, (en ignorant, pour l'instant, le fait que la conversion du pointeur vers un entier est un comportement défini par l'implémentation )

int * my_int_ptr = 2;

my_int_ptrest une variable (de type pointeur vers int), elle a une adresse qui lui est propre (type: adresse du pointeur vers un entier), vous stockez une valeur de 2dans cette adresse.

Maintenant, my_int_ptrétant un type pointeur, nous pouvons dire, il pointe vers la valeur de "type" à l'emplacement mémoire pointé par la valeur contenue dans my_int_ptr. Ainsi, vous affectez essentiellement la valeur de la variable pointeur, et non la valeur de l'emplacement mémoire pointé par le pointeur.

Donc, pour conclure

 char *x=NULL;

initialise la variable du pointeur xà NULL, pas la valeur à l'adresse mémoire pointée par le pointeur .

C'est la même chose que

 char *x;
 x = NULL;    

Expansion:

Maintenant, étant strictement conforme, une déclaration comme

 int * my_int_ptr = 2;

est illégale, car elle implique une violation de contrainte. Pour être clair,

  • my_int_ptr est une variable de pointeur, tapez int *
  • une constante entière, 2a un type int, par définition.

et ce ne sont pas des types "compatibles", donc cette initialisation est invalide car elle enfreint les règles d'assignation simple, mentionnées au chapitre §6.5.16.1 / P1, décrites dans la réponse de Lundin .

Au cas où quelqu'un serait intéressé par la manière dont l'initialisation est liée à de simples contraintes d'affectation, citant C11, chapitre §6.7.9, P11

L'initialiseur d'un scalaire doit être une expression unique, éventuellement entre accolades. La valeur initiale de l'objet est celle de l'expression (après conversion); les mêmes contraintes et conversions de type que pour l'affectation simple s'appliquent, en prenant le type du scalaire comme étant la version non qualifiée de son type déclaré.

Sourav Ghosh
la source
@ Random832n Ils ont tort. J'ai cité la partie correspondante dans ma réponse, veuillez me corriger dans le cas contraire. Oh, et l'accent mis sur intentionnel.
Sourav Ghosh
"... est illégal, car il implique une violation de contrainte. ... un littéral entier, 2 a le type int, par définition." est problématique. Cela ressemble à parce que 2c'est un int, l'affectation est un problème. Mais c'est plus que ça. NULLpeut également être un int, un int 0. C'est juste que cela char *x = 0;est bien défini et char *x = 2;ne l'est pas. 6.3.2.3 Pointeurs 3 (BTW: C ne définit pas un littéral entier , seulement un littéral de chaîne et un littéral composé . 0Est une constante entière )
chux - Réintègre Monica
@chux Vous avez tout à fait raison, mais n'est-ce pas char *x = (void *)0;, d'être conforme? ou est-ce seulement avec d'autres expressions qui donne la valeur 0?
Sourav Ghosh
10
@SouravGhosh: les constantes entières avec valeur 0sont spéciales: elles se convertissent implicitement en pointeurs nulles séparément des règles habituelles pour convertir explicitement des expressions entières générales en types pointeurs.
Steve Jessop
1
Le langage décrit par le Manuel de référence C de 1974 ne permettait pas aux déclarations de spécifier des expressions d'initialisation, et l'absence de telles expressions rend l'utilisation des miroirs de déclaration beaucoup plus pratique. La syntaxe int *p = somePtrExpressionest à mon humble avis plutôt horrible car elle semble définir la valeur de, *pmais elle définit en fait la valeur de p.
supercat
53

Le tutoriel est faux. En ISO C, int *my_int_ptr = 2;c'est une erreur. Dans GNU C, cela signifie la même chose que int *my_int_ptr = (int *)2;. Cela convertit l'entier 2en une adresse mémoire, d'une manière déterminée par le compilateur.

Il n'essaye pas de stocker quoi que ce soit à l'emplacement adressé par cette adresse (le cas échéant). Si vous continuiez à écrire *my_int_ptr = 5;, alors il essaierait de stocker le numéro 5à l'emplacement adressé par cette adresse.

MM
la source
1
Je ne savais pas que la conversion d'entier en pointeur était définie par l'implémentation. Merci pour l'information.
taskinoor
1
@taskinoor Veuillez noter qu'il n'y a une conversion que dans le cas où vous la forcez par un casting, comme dans cette réponse. Sinon pour le cast, le code ne doit pas se compiler.
Lundin
2
@taskinoor: Oui, les différentes conversions en C sont assez déroutantes. Ce Q contient des informations intéressantes sur les conversions: C: Quand la conversion entre les types de pointeurs n'est-elle pas un comportement indéfini? .
sleske
17

Pour clarifier pourquoi le didacticiel est erroné, int *my_int_ptr = 2;est une "violation de contrainte", c'est du code qui n'est pas autorisé à compiler et le compilateur doit vous donner un diagnostic lorsqu'il le rencontre.

Selon 6.5.16.1 Affectation simple:

Contraintes

L’un des éléments suivants doit détenir:

  • l'opérande de gauche a un type arithmétique atomique, qualifié ou non qualifié, et la droite a un type arithmétique;
  • l'opérande de gauche a une version atomique, qualifiée ou non qualifiée d'un type de structure ou d'union compatible avec le type de droite;
  • l'opérande de gauche a un type de pointeur atomique, qualifié ou non qualifié, et (compte tenu du type que l'opérande de gauche aurait après la conversion de lvalue) les deux opérandes sont des pointeurs vers des versions qualifiées ou non de types compatibles, et le type pointé par la gauche a tout les qualificatifs du type indiqué par la droite;
  • l'opérande gauche a un type de pointeur atomique, qualifié ou non qualifié, et (compte tenu du type que l'opérande gauche aurait après la conversion de lvalue) un opérande est un pointeur vers un type d'objet et l'autre est un pointeur vers une version qualifiée ou non qualifiée de void, et le type pointé par la gauche a tous les qualificatifs du type pointé par la droite;
  • l'opérande de gauche est un pointeur atomique, qualifié ou non qualifié, et la droite est une constante de pointeur nul; ou
  • l'opérande de gauche est de type _Bool atomique, qualifié ou non qualifié, et la droite est un pointeur.

Dans ce cas, l'opérande gauche est un pointeur non qualifié. Nulle part il ne mentionne que l'opérande de droite est autorisé à être un entier (type arithmétique). Donc, le code enfreint la norme C.

GCC est connu pour se comporter mal à moins que vous ne lui disiez explicitement qu'il s'agit d'un compilateur C standard. Si vous compilez le code en tant que -std=c11 -pedantic-errors, il donnera correctement un diagnostic comme il se doit.

Lundin
la source
4
voté pour avoir suggéré -pedantic-errors. Bien que j'utilise probablement le -Wpedantic.
fagricipni
2
Une exception à votre déclaration selon laquelle l'opérande droit n'est pas autorisé à être un entier: la section 6.3.2.3 dit: «Une expression constante entière avec la valeur 0, ou une telle expression convertie en type void *, est appelée une constante pointeur nul.» Notez l'avant-dernier point dans votre devis. C'est donc int* p = 0;une manière légale d'écrire int* p = NULL;. Bien que ce dernier soit plus clair et plus conventionnel.
Davislor
1
Ce qui rend également l'obfuscation pathologique int m = 1, n = 2 * 2, * p = 1 - 1, q = 2 - 1;légale.
Davislor
@Davislor qui est couvert par la puce 5 dans la citation standard de cette réponse (convenez que le résumé devrait probablement le mentionner par la suite)
MM
1
@chux Je pense qu'un programme bien formé aurait besoin de convertir un intptr_texplicitement en l'un des types autorisés sur le côté droit. Autrement dit, void* a = (void*)(intptr_t)b;est légal par le point 4, mais (intptr_t)bn'est ni un type de pointeur compatible, ni a void*, ni une constante de pointeur nul, et void* an'est ni un type arithmétique ni _Bool. La norme dit que la conversion est légale, mais pas qu'elle est implicite.
Davislor
15

int *my_int_ptr = 2

stocke la valeur entière 2 à n'importe quelle adresse aléatoire dans my_int_ptr lorsqu'elle est allouée.

C'est complètement faux. Si cela est réellement écrit, veuillez obtenir un meilleur livre ou un tutoriel.

int *my_int_ptr = 2définit un pointeur entier qui pointe vers l'adresse 2. Vous obtiendrez probablement un plantage si vous essayez d'accéder à l'adresse 2.

*my_int_ptr = 2, c'est-à-dire sans le intdans la ligne, stocke la valeur deux à n'importe quelle adresse aléatoire my_int_ptrpointée. Cela dit, vous pouvez attribuer NULLà un pointeur lorsqu'il est défini. char *x=NULL;est parfaitement valide C.

Edit: En écrivant ceci, je ne savais pas que la conversion d'entier en pointeur était un comportement défini par l'implémentation. Veuillez consulter les bonnes réponses de @MM et @SouravGhosh pour plus de détails.

taskinoor
la source
1
C'est complètement faux car c'est une violation de contrainte, pas pour une autre raison. En particulier, c'est incorrect: "int * my_int_ptr = 2 définit un pointeur entier qui pointe vers l'adresse 2".
Lundin
@Lundin: Votre phrase "pas pour une autre raison" est elle-même fausse et trompeuse. Si vous résolvez le problème de compatibilité de type, vous vous retrouvez toujours avec le fait que l'auteur du didacticiel déforme grossièrement le fonctionnement des initialisations et des affectations de pointeurs.
Courses de légèreté en orbite le
14

Une grande confusion sur les pointeurs C vient d'un très mauvais choix qui a été fait à l'origine concernant le style de codage, corroboré par un très mauvais petit choix dans la syntaxe du langage.

int *x = NULL;est correct C, mais il est très trompeur, je dirais même absurde, et cela a gêné la compréhension de la langue pour de nombreux novices. Cela fait penser que plus tard, nous pourrions faire *x = NULL;ce qui est bien sûr impossible. Vous voyez, le type de la variable n'est pas int, et le nom de la variable ne l'est pas *x, et le *dans la déclaration ne joue aucun rôle fonctionnel en collaboration avec le =. C'est purement déclaratif. Donc, ce qui a beaucoup plus de sens, c'est ceci:

int* x = NULL;ce qui est également correct C, bien qu'il n'adhère pas au style de codage K&R d'origine. Cela rend parfaitement clair que le type est int*et que la variable de pointeur est x, de sorte qu'il devient clairement évident, même pour les non-initiés, que la valeur NULLest stockée dans x, qui est un pointeur vers int.

De plus, cela facilite la dérivation d'une règle: lorsque l'étoile est éloignée du nom de la variable, il s'agit d'une déclaration, tandis que l'étoile attachée au nom est un déréférencement de pointeur.

Donc, maintenant, il devient beaucoup plus compréhensible que plus bas, nous pouvons le faire x = NULL;ou *x = 2;en d'autres termes, il est plus facile pour un novice de voir comment variable = expressionmène à pointer-type variable = pointer-expressionet dereferenced-pointer-variable = expression. (Pour les initiés, par «expression», je veux dire «rvalue».)

Le choix malheureux dans la syntaxe du langage est que lors de la déclaration de variables locales, vous pouvez dire int i, *p;qui déclare un entier et un pointeur vers un entier, cela laisse donc penser que le *est une partie utile du nom. Mais ce n'est pas le cas, et cette syntaxe n'est qu'un cas spécial excentrique, ajouté par commodité, et à mon avis, elle n'aurait jamais dû exister, car elle invalide la règle que j'ai proposée ci-dessus. Autant que je sache, nulle part ailleurs dans le langage cette syntaxe n'a de sens, mais même si c'est le cas, elle indique une divergence dans la façon dont les types de pointeurs sont définis en C. Partout ailleurs, dans les déclarations à variable unique, dans les listes de paramètres, dans les membres de structure, etc., vous pouvez déclarer vos pointeurs comme type* pointer-variableau lieu de type *pointer-variable; c'est parfaitement légal et a plus de sens.

Mike Nakis
la source
int *x = NULL; is correct C, but it is very misleading, I would even say nonsensical,... Je dois accepter de ne pas être d'accord. It makes one think.... arrêtez de penser, lisez d'abord un livre C, sans offense.
Sourav Ghosh
^^ cela m'aurait été parfaitement logique. Donc, je suppose que c'est subjectif.
Mike Nakis
5
@SouravGhosh En tant qu'opinion, je pense que C aurait dû être conçu de manière à int* somePtr, someotherPtrdéclarer deux pointeurs, en fait, j'avais l'habitude d'écrire int* somePtrmais cela conduit au bogue que vous décrivez.
fagricipni
1
@fagricipni J'ai arrêté d'utiliser la syntaxe de déclaration de variables multiples à cause de cela. Je déclare mes variables une par une. Si je les veux vraiment sur la même ligne, je les sépare par des points-virgules plutôt que par des virgules. "Si un endroit est mauvais, n'y allez pas."
Mike Nakis
2
@fagricipni Eh bien, si j'avais pu concevoir Linux à partir de zéro, j'aurais utilisé à la createplace de creat. :) Le fait est que c'est comme ça et que nous devons nous façonner pour nous adapter à cela. Tout se résume à un choix personnel à la fin de la journée, d'accord.
Sourav Ghosh
6

Je voudrais ajouter quelque chose d'orthogonal aux nombreuses excellentes réponses. En fait, l'initialisation vers NULLest loin d'être une mauvaise pratique et peut être pratique si ce pointeur peut ou non être utilisé pour stocker un bloc de mémoire alloué dynamiquement.

int * p = NULL;
...
if (...) {
    p = (int*) malloc(...);
    ...
}
...
free(p);

Étant donné que selon la norme ISO-IEC 9899 free est un nop lorsque l'argument est NULL, le code ci-dessus (ou quelque chose de plus significatif dans le même sens) est légitime.

Luca Citi
la source
5
Il est redondant de convertir le résultat de malloc en C, à moins que ce code C ne doive également être compilé en C ++.
chat
Vous avez raison, le void*est converti au besoin. Mais avoir du code qui fonctionne avec un compilateur C et C ++ pourrait avoir des avantages.
Luca Citi
1
@LucaCiti C et C ++ sont des langages différents. Il n'y a que des erreurs qui vous attendent si vous essayez de compiler un fichier source écrit pour l'un à l'aide d'un compilateur conçu pour l'autre. C'est comme essayer d'écrire du code C que vous pouvez compiler à l'aide des outils Pascal.
Tarte au chien maléfique
1
Bon conseil. J'essaye de toujours initialiser mes constantes de pointeur vers quelque chose. Dans le C moderne, cela peut généralement être leur valeur finale et ils peuvent être des constpointeurs déclarés dans medias res , mais même lorsqu'un pointeur doit être mutable (comme celui utilisé dans une boucle ou par realloc()), le paramétrer pour NULLattraper les bogues là où il était utilisé auparavant il a sa valeur réelle. Sur la plupart des systèmes, le déréférencement NULLprovoque une erreur de segmentation au point de défaillance (bien qu'il y ait des exceptions), alors qu'un pointeur non initialisé contient des déchets et l'écriture corrompt la mémoire arbitraire.
Davislor
1
En outre, il est très facile de voir dans le débogueur qu'un pointeur contient NULL, mais il peut être très difficile de distinguer un pointeur de garbage d'un valide. Il est donc utile de s'assurer que tous les pointeurs sont toujours valides ou NULL, à partir du moment de la déclaration.
Davislor
1

c'est un pointeur nul

int * nullPtr = (void*) 0;
Ahmed Nabil El-Gawahergy
la source
1
Cela répond au titre, mais pas au corps de la question.
Fabio dit Réintégrer Monica le
1

C'est correct.

int main()
{
    char * x = NULL;

    if (x==NULL)
        printf("is NULL\n");

    return EXIT_SUCCESS;
}

Cette fonction est correcte pour ce qu'elle fait. Il attribue l'adresse 0 au pointeur char x. Autrement dit, il pointe le pointeur x vers l'adresse mémoire 0.

Alternative:

int main()
{
    char* x = 0;

    if ( !x )
        printf(" x points to NULL\n");

    return EXIT_SUCCESS;
}

Ma conjecture quant à ce que vous vouliez est:

int main()
{
    char* x = NULL;
    x = alloc( sizeof( char ));
    *x = '2';

    if ( *x == '2' )
        printf(" x points to an address/location that contains a '2' \n");

    return EXIT_SUCCESS;
}

x is the street address of a house. *x examines the contents of that house.
Vanderdecken
la source
"Il attribue l'adresse 0 au pointeur char x." -> Peut-être. C ne spécifie pas la valeur du pointeur, seulement cela char* x = 0; if (x == 0)sera vrai. Les pointeurs ne sont pas nécessairement des entiers.
chux
Il ne «pointe pas le pointeur x vers l'adresse mémoire 0». Il définit la valeur du pointeur sur une valeur non valide non spécifiée qui peut être testée en la comparant à 0 ou NULL. L'opération réelle est définie par l'implémentation. Il n'y a rien ici qui répond à la question réelle.
Marquis of Lorne