¿Es posible inicializar un puntero C a NULL?

90

Había estado escribiendo cosas como

char *x=NULL;

asumiendo que

 char *x=2;

crearía un charpuntero a la dirección 2.

Pero, en The GNU C Programming Tutorial dice que int *my_int_ptr = 2;almacena el valor entero 2en cualquier dirección aleatoria en la my_int_ptrque se encuentre cuando se asigna.

Esto parecería implicar que el mío char *x=NULLestá asignando cualquier valor de NULLcast a a chares a alguna dirección aleatoria en la memoria.

Mientras

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

int main()
{
    char *x=NULL;

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

    return EXIT_SUCCESS;
}

de hecho, imprime

es nulo

cuando lo compilo y ejecuto, me preocupa que confíe en un comportamiento indefinido, o al menos un comportamiento subespecificado, y que debería escribir

char *x;
x=NULL;

en lugar.

fagricipni
fuente
72
Hay una diferencia muy confusa entre lo que int *x = whatever;hace y lo que int *x; *x = whatever;hace. int *x = whatever;en realidad se comporta como int *x; x = whatever;, no *x = whatever;.
user2357112 apoya a Monica
78
Este tutorial parece haberse equivocado en esa confusa distinción.
user2357112 apoya a Monica
51
¡Tantos tutoriales de mierda en la web! Deja de leer de inmediato. Realmente necesitamos una lista negra SO donde podamos avergonzar públicamente a los libros malos ...
Lundin
9
@MM Lo que no lo hace menos cutre en el año 2017. Dada la evolución de los compiladores y las computadoras desde los años 80, es básicamente lo mismo que si fuera médico y leyera libros de medicina escritos durante el siglo XVIII.
Lundin
13
No creo que este tutorial se califique como " El tutorial de programación de GNU C" ...
marcelm

Respuestas:

114

¿Es posible inicializar un puntero C a NULL?

TL; DR Sí, mucho.


La afirmación real hecha en la guía se lee como

Por otro lado, si usa solo la asignación inicial única int *my_int_ptr = 2;, el programa intentará llenar el contenido de la ubicación de memoria señalada por my_int_ptrcon el valor 2. Como my_int_ptrestá lleno de basura, puede ser cualquier dirección. [...]

Bueno, están equivocados, tienes razón.

Para la declaración, ( ignorando, por ahora, el hecho de que la conversión de puntero a entero es un comportamiento definido por la implementación )

int * my_int_ptr = 2;

my_int_ptres una variable (de tipo puntero a int), tiene una dirección propia (tipo: dirección de puntero a entero), está almacenando un valor de 2en esa dirección.

Ahora, al my_int_ptrser un tipo de puntero, podemos decir que apunta al valor de "tipo" en la ubicación de memoria apuntada por el valor retenido my_int_ptr. Por lo tanto, esencialmente está asignando el valor de la variable de puntero, no el valor de la ubicación de memoria a la que apunta el puntero.

Entonces, para concluir

 char *x=NULL;

inicializa la variable de puntero xa NULL, no el valor en la dirección de memoria apuntada por el puntero .

Esto es lo mismo que

 char *x;
 x = NULL;    

Expansión:

Ahora, siendo estrictamente conforme, una declaración como

 int * my_int_ptr = 2;

es ilegal, ya que implica una violación de la restricción. Para ser claro,

  • my_int_ptr es una variable de puntero, tipo int *
  • una constante entera, 2tiene tipo int, por definición.

y no son tipos "compatibles", por lo que esta inicialización no es válida porque viola las reglas de asignación simple, mencionadas en el capítulo §6.5.16.1 / P1, descritas en la respuesta de Lundin .

En caso de que alguien esté interesado en cómo la inicialización está vinculada a restricciones de asignación simples, citando C11, capítulo §6.7.9, P11

El inicializador de un escalar será una sola expresión, opcionalmente encerrada entre llaves. El valor inicial del objeto es el de la expresión (después de la conversión); Se aplican las mismas restricciones de tipo y conversiones que para la asignación simple, tomando el tipo del escalar como la versión no calificada de su tipo declarado.

Sourav Ghosh
fuente
@ Random832n Ellos están mal. He citado la parte relacionada en mi respuesta, corríjame si no es así. Ah, y el énfasis en intencional.
Sourav Ghosh
"... es ilegal, ya que implica una violación de la restricción. ... un literal entero, 2 tiene el tipo int, por definición". es problemático. Parece que porque 2es un int, la tarea es un problema. Pero es más que eso. NULLtambién puede ser un int, un int 0. Es solo que char *x = 0;está bien definido y char *x = 2;no lo está. 6.3.2.3 Punteros 3 (BTW: C no define un literal entero , sólo el literal de cadena y literal compuesto . 0Es una constante entera )
Chux - Restablecer Monica
@chux Tienes mucha razón, pero ¿no es así char *x = (void *)0;, ser conforme? ¿O es solo con otras expresiones lo que da el valor 0?
Sourav Ghosh
10
@SouravGhosh: las constantes enteras con valor 0son especiales: se convierten implícitamente en punteros nulos por separado de las reglas habituales para convertir explícitamente expresiones enteras generales en tipos de punteros.
Steve Jessop
1
El lenguaje descrito por el Manual de referencia C de 1974 no permitía que las declaraciones especificaran expresiones de inicialización, y la falta de tales expresiones hace que el "uso de espejos de declaración" sea mucho más práctico. En int *p = somePtrExpressionmi humilde opinión, la sintaxis es bastante horrible, ya que parece que está estableciendo el valor de *ppero en realidad está estableciendo el valor de p.
supercat
53

El tutorial está mal. En ISO C, int *my_int_ptr = 2;es un error. En GNU C, significa lo mismo que int *my_int_ptr = (int *)2;. Esto convierte el entero 2en una dirección de memoria, de alguna manera según lo determine el compilador.

No intenta almacenar nada en la ubicación a la que se dirige esa dirección (si corresponde). Si continuara escribiendo *my_int_ptr = 5;, intentaría almacenar el número 5en la ubicación a la que se dirige esa dirección.

MM
fuente
1
No sabía que la conversión de entero a puntero está definida por la implementación. Gracias por la información.
taskinoor
1
@taskinoor Tenga en cuenta que solo hay una conversión en el caso de que la fuerce mediante un elenco, como en esta respuesta. Si no fuera por el elenco, el código no debería compilarse.
Lundin
2
@taskinoor: Sí, las diversas conversiones en C son bastante confusas. Esta Q tiene información interesante sobre conversiones: C: ¿Cuándo la conversión entre tipos de puntero no es un comportamiento indefinido? .
sleske
17

Para aclarar por qué el tutorial es incorrecto, int *my_int_ptr = 2;es una "violación de restricción", es un código que no se puede compilar y el compilador debe proporcionarle un diagnóstico al encontrarlo.

Según 6.5.16.1 Asignación simple:

Restricciones

Uno de los siguientes deberá ser válido:

  • el operando izquierdo tiene tipo aritmético atómico, calificado o no calificado, y el derecho tiene tipo aritmético;
  • el operando izquierdo tiene una versión atómica, calificada o no calificada de un tipo de estructura o unión compatible con el tipo de la derecha;
  • el operando izquierdo tiene un tipo de puntero atómico, calificado o no calificado, y (considerando el tipo que tendría el operando izquierdo después de la conversión de lvalue) ambos operandos son punteros a versiones calificadas o no calificadas de tipos compatibles, y el tipo apuntado por la izquierda tiene todo los calificadores del tipo señalado por la derecha;
  • el operando izquierdo tiene un tipo de puntero atómico, calificado o no calificado, y (considerando el tipo que tendría el operando izquierdo después de la conversión de lvalue) un operando es un puntero a un tipo de objeto, y el otro es un puntero a una versión calificada o no calificada de void, y el tipo señalado por la izquierda tiene todos los calificadores del tipo señalado por la derecha;
  • el operando izquierdo es un puntero atómico, calificado o no calificado, y el derecho es una constante de puntero nulo; o
  • el operando izquierdo tiene tipo _Bool atómico, calificado o no calificado, y el derecho es un puntero.

En este caso, el operando izquierdo es un puntero no calificado. En ninguna parte menciona que el operando derecho puede ser un número entero (tipo aritmético). Entonces el código viola el estándar C.

Se sabe que GCC se comporta mal a menos que le indique explícitamente que sea un compilador C estándar. Si compila el código como -std=c11 -pedantic-errors, dará correctamente un diagnóstico como debe hacerlo.

Lundin
fuente
4
votado por sugerir -pedantic-errores. Aunque probablemente usaré el relacionado -Wpedantic.
fagricipni
2
Una excepción a su afirmación de que el operando derecho no puede ser un número entero: la sección 6.3.2.3 dice: "Una expresión constante de número entero con el valor 0, o una expresión de este tipo convertida en tipo void *, se llama una constante de puntero nulo". Observe la penúltima viñeta en su cotización. Por tanto, int* p = 0;es una forma legal de escribir int* p = NULL;. Aunque este último es más claro y convencional.
Davislor
1
Lo que hace que la ofuscación patológica int m = 1, n = 2 * 2, * p = 1 - 1, q = 2 - 1;también sea legal.
Davislor
@Davislor que está cubierto por el punto 5 en la cita estándar en esta respuesta (aunque esté de acuerdo en que el resumen posterior probablemente debería mencionarlo)
MM
1
@chux Creo que un programa bien formado necesitaría convertir intptr_texplícitamente a uno de los tipos permitidos en el lado derecho. Es decir, void* a = (void*)(intptr_t)b;es legal según el punto 4, pero (intptr_t)bno es un tipo de puntero compatible, ni a void*, ni una constante de puntero nulo, y void* ano es un tipo aritmético ni _Bool. El estándar dice que la conversión es legal, pero no implícita.
Davislor
15

int *my_int_ptr = 2

almacena el valor entero 2 en cualquier dirección aleatoria que esté en my_int_ptr cuando se asigna.

Esto está completamente mal. Si esto está realmente escrito, consiga un mejor libro o tutorial.

int *my_int_ptr = 2define un puntero entero que apunta a la dirección 2. Lo más probable es que se bloquee si intenta acceder a la dirección 2.

*my_int_ptr = 2, es decir, sin el inten la línea, almacena el valor dos en cualquier dirección aleatoria a la my_int_ptrque apunte. Habiendo dicho esto, puede asignar NULLa un puntero cuando esté definido. char *x=NULL;es perfectamente válido C.

Editar: Mientras escribía esto, no sabía que la conversión de entero a puntero es un comportamiento definido por la implementación. Consulte las buenas respuestas de @MM y @SouravGhosh para obtener más detalles.

taskinoor
fuente
1
Es completamente incorrecto porque es una violación de la restricción, no por ninguna otra razón. En particular, esto es incorrecto: "int * my_int_ptr = 2 define un puntero entero que apunta a la dirección 2".
Lundin
@Lundin: Tu frase "no por ninguna otra razón" es en sí misma incorrecta y engañosa. Si soluciona el problema de compatibilidad de tipos, todavía le queda el hecho de que el autor del tutorial está tergiversando enormemente cómo funcionan las inicializaciones y asignaciones de punteros.
Lightness Races in Orbit
14

Mucha confusión acerca de los punteros C proviene de una muy mala elección que se hizo originalmente con respecto al estilo de codificación, corroborada por una muy mala elección en la sintaxis del lenguaje.

int *x = NULL;C es correcta, pero es muy engañosa, incluso diría absurda, y ha dificultado la comprensión del idioma para muchos novatos. Hace pensar que más adelante podríamos hacer lo *x = NULL;que, por supuesto, es imposible. Verá, el tipo de la variable no lo es int, y el nombre de la variable no lo es *x, ni el *en la declaración juega ningún papel funcional en colaboración con el =. Es puramente declarativo. Entonces, lo que tiene mucho más sentido es esto:

int* x = NULL;que también es C correcta, aunque no se adhiere al estilo de codificación original de K&R. Deja perfectamente claro que el tipo es int*, y la variable de puntero lo es x, por lo que resulta claramente evidente incluso para los no iniciados que el valor NULLse está almacenando en x, que es un puntero a int.

Además, hace que sea más fácil derivar una regla: cuando la estrella está lejos del nombre de la variable, entonces es una declaración, mientras que la estrella que se adjunta al nombre es una desreferenciación del puntero.

Entonces, ahora se vuelve mucho más comprensible que más abajo podamos hacer x = NULL;o, *x = 2;en otras palabras, facilita que un novato vea cómo variable = expressionconduce a pointer-type variable = pointer-expressiony dereferenced-pointer-variable = expression. (Para los iniciados, por 'expresión' me refiero a 'rvalue').

La desafortunada elección en la sintaxis del lenguaje es que al declarar variables locales puedes decir int i, *p;cuál declara un número entero y un puntero a un número entero, por lo que te lleva a creer que *es una parte útil del nombre. Pero no lo es, y esta sintaxis es solo un caso especial peculiar, agregado por conveniencia y, en mi opinión, nunca debería haber existido, porque invalida la regla que propuse anteriormente. Hasta donde yo sé, en ninguna otra parte del lenguaje esta sintaxis es significativa, pero incluso si lo es, apunta a una discrepancia en la forma en que se definen los tipos de puntero en C.En cualquier otro lugar, en declaraciones de una sola variable, en listas de parámetros, en miembros de estructura, etc., puede declarar sus punteros como en type* pointer-variablelugar de type *pointer-variable; es perfectamente legal y tiene más sentido.

Mike Nakis
fuente
int *x = NULL; is correct C, but it is very misleading, I would even say nonsensical,... Tengo que aceptar estar en desacuerdo. It makes one think.... deja de pensar, lee un libro en C primero, sin ofender.
Sourav Ghosh
^^ esto hubiera tenido mucho sentido para mí. Entonces, supongo que es subjetivo.
Mike Nakis
5
@SouravGhosh Como cuestión de opinión, creo que C debería haber sido diseñado para que int* somePtr, someotherPtrdeclare dos punteros, de hecho, solía escribir, int* somePtrpero eso conduce al error que describe.
fagricipni
1
@fagricipni Dejé de usar la sintaxis de declaración de variables múltiples debido a esto. Declaro mis variables una por una. Si realmente los quiero en la misma línea, los separo con punto y coma en lugar de comas. "Si un lugar es malo, no vayas a ese lugar".
Mike Nakis
2
@fagricipni Bueno, si pudiera haber diseñado Linux desde cero, habría usado en createlugar de creat. :) El caso es que es así y tenemos que moldearnos para adaptarnos a eso. Al final del día, todo se reduce a una elección personal, de acuerdo.
Sourav Ghosh
6

Me gustaría agregar algo ortogonal a las muchas respuestas excelentes. En realidad, la inicialización en NULLestá lejos de ser una mala práctica y puede ser útil si ese puntero puede o no usarse para almacenar un bloque de memoria asignado dinámicamente.

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

Dado que de acuerdo con el estándar ISO-IEC 9899 free es un nop cuando el argumento lo es NULL, el código anterior (o algo más significativo en la misma línea) es legítimo.

Luca Citi
fuente
5
Es redundante convertir el resultado de malloc en C, a menos que el código C también se compile como C ++.
gato
Tienes razón, el void*se convierte según sea necesario. Pero tener un código que funcione con un compilador C y C ++ podría tener beneficios.
Luca Citi
1
@LucaCiti C y C ++ son lenguajes diferentes. Solo hay errores esperándolo si intenta compilar un archivo fuente escrito para uno usando un compilador diseñado para el otro. Es como intentar escribir código C que se puede compilar con las herramientas de Pascal.
Pastel de perro malvado
1
Buen consejo. Siempre (trato de) inicializar mis constantes de puntero a algo. En C moderno, este suele ser su valor final y pueden ser constpunteros declarados en medias res , pero incluso cuando un puntero necesita ser mutable (como uno que se usa en un bucle o por realloc()), configurándolo para NULLdetectar errores donde se usa antes está configurado con su valor real. En la mayoría de los sistemas, la desreferenciación NULLcausa una falla de segmentación en el punto de falla (aunque hay excepciones), mientras que un puntero no inicializado contiene basura y escribir en él corrompe la memoria arbitraria.
Davislor
1
Además, es muy fácil ver en el depurador que contiene un puntero NULL, pero puede ser muy difícil diferenciar un puntero de basura de uno válido. Por lo tanto, es útil asegurarse de que todos los punteros sean siempre válidos o NULL, desde el momento de la declaración.
Davislor
1

este es un puntero nulo

int * nullPtr = (void*) 0;
Ahmed Nabil El-Gawahergy
fuente
1
Esto responde al título, pero no al cuerpo de la pregunta.
Fabio dice Reincorporar a Monica
1

Esto es correcto.

int main()
{
    char * x = NULL;

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

    return EXIT_SUCCESS;
}

Esta función es correcta para lo que hace. Asigna la dirección de 0 al puntero char x. Es decir, apunta el puntero x a la dirección de memoria 0.

Alternativa:

int main()
{
    char* x = 0;

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

    return EXIT_SUCCESS;
}

Mi conjetura sobre lo que querías es:

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
fuente
"Asigna la dirección de 0 al puntero char x". -> Quizás. C no especifica el valor del puntero, solo eso char* x = 0; if (x == 0)será cierto. Los punteros no son necesariamente números enteros.
chux - Reincorporar a Monica
No 'apunta el puntero x a la dirección de memoria 0'. Establece el valor del puntero en un valor no válido no especificado que se puede probar comparándolo con 0 o NULL. La operación real está definida por la implementación. No hay nada aquí que responda a la pregunta real.
Marqués de Lorne