¿Por qué se usa la dirección cero para el puntero nulo?

En C (o en C ++ para el caso), los punteros son especiales si tienen el valor cero: se me aconseja establecer los punteros a cero después de liberar su memoria, porque significa que liberar el puntero nuevamente no es peligroso; cuando llamo malloc, devuelve un puntero con el valor cero si no puede recuperar la memoria; Uso if (p != 0) todo el tiempo para asegurarme de que los punteros pasados ​​sean válidos, etc.

Pero dado que el direccionamiento de memoria comienza en 0, ¿no es 0 solo una dirección válida como cualquier otra? ¿Cómo se puede usar 0 para manejar punteros nulos si ese es el caso? ¿Por qué no es un número negativo nulo en su lugar?


Editar:

Un montón de buenas respuestas. Resumiré lo que se ha dicho en las respuestas expresadas como mi propia mente lo interpreta y espero que la comunidad me corrija si no entiendo bien.

  • Como todo lo demás en la progtwigción, es una abstracción. Solo una constante, no realmente relacionada con la dirección 0. C ++ 0x enfatiza esto agregando la palabra clave nullptr .

  • Ni siquiera es una abstracción de direcciones, es la constante especificada por el estándar C y el comstackdor puede traducirla a algún otro número siempre que se asegure de que nunca sea igual a una dirección “real”, y equivale a otros punteros nulos si 0 no es el el mejor valor para usar en la plataforma.

  • En caso de que no se trate de una abstracción, como ocurrió en los primeros días, el sistema utiliza la dirección 0 y está fuera del scope del progtwigdor.

  • Mi sugerencia de número negativo fue una lluvia de ideas alocada, lo admito. Usar un entero con signo para direcciones es un poco desperdiciado si significa que, aparte del puntero nulo (-1 o lo que sea), el espacio de valor se divide en partes iguales entre enteros positivos que hacen direcciones válidas y números negativos que simplemente se desperdician.

  • Si un número es siempre representable por un tipo de datos, es 0. (Probablemente 1 también. Creo que el entero de un bit sería 0 o 1 si no está firmado, o solo el bit firmado si está firmado, o el entero de dos bits que Sería [-2, 1]. Pero luego podría ir por 0 siendo nulo y 1 siendo el único byte accesible en la memoria.)

Todavía hay algo que no está resuelto en mi mente. El puntero de la pregunta de desbordamiento de stack a una dirección fija específica me dice que incluso si 0 para el puntero nulo es una abstracción, otros valores de puntero no son necesariamente. Esto me lleva a publicar otra pregunta de Desbordamiento de stack. ¿Alguna vez podría querer acceder a la dirección cero? .

2 puntos:

  • solo el valor constante 0 en el código fuente es el puntero nulo: la implementación del comstackdor puede usar cualquier valor que desee o necesite en el código en ejecución. Algunas plataformas tienen un valor de puntero especial que es “no válido” y que la implementación podría usar como puntero nulo. La pregunta frecuente de C tiene una pregunta: “En serio, ¿alguna máquina real realmente ha utilizado punteros nulos distintos de cero, o representaciones diferentes para punteros a diferentes tipos?” , que señala varias plataformas que usaron esta propiedad de 0 siendo el puntero nulo en la fuente C mientras que se representa de manera diferente en el tiempo de ejecución. El estándar de C ++ tiene una nota que deja en claro que al convertir “una expresión de constante integral con valor cero siempre se obtiene un puntero nulo, pero la conversión de otras expresiones que tienen valor cero no tiene por qué producir un puntero nulo”.

  • un valor negativo podría ser tan útil para la plataforma como una dirección: el estándar C simplemente tenía que elegir algo para indicar un puntero nulo y se eligió cero. Sinceramente, no estoy seguro de si se consideraron otros valores centinelas.

Los únicos requisitos para un puntero nulo son:

  • está garantizado para comparar desigual a un puntero a un objeto real
  • dos punteros nulos se compararán iguales (C ++ refina esto de modo que esto solo debe mantenerse para punteros del mismo tipo)

Históricamente, el espacio de direcciones que comenzaba en 0 siempre era ROM, utilizado para algunas rutinas de sistema operativo o de interrupción de bajo nivel, hoy en día, dado que todo es virtual (incluido el espacio de direcciones), el sistema operativo puede asignar cualquier asignación a cualquier dirección, por lo que puede específicamente NO asignar nada en la dirección 0.

IIRC, no se garantiza que el valor de “puntero nulo” sea cero. El comstackdor traduce 0 en cualquier valor “nulo” que sea apropiado para el sistema (que en la práctica es probablemente siempre cero, pero no necesariamente). La misma traducción se aplica siempre que compara un puntero contra cero. Debido a que solo puede comparar punteros uno contra el otro y contra este valor-especial-0, aísla al progtwigdor de saber algo acerca de la representación de la memoria del sistema. En cuanto a por qué eligieron 0 en lugar de 42 o algo así, supongo que es porque la mayoría de los progtwigdores comienzan a contar a 0 🙂 (Además, en la mayoría de los sistemas 0 es la primera dirección de memoria y querían que fuera conveniente, ya que Prácticamente las traducciones como las que describo rara vez tienen lugar, el lenguaje solo las permite.

Debe malinterpretar el significado del cero constante en el contexto del puntero.

Ni en C ni en C ++ los punteros pueden “tener valor cero”. Los punteros no son objetos aritméticos. No pueden tener valores numéricos como “cero” o “negativo” o algo de esa naturaleza. Entonces su afirmación sobre “punteros … tiene el valor cero” simplemente no tiene sentido.

En C & C ++, los punteros pueden tener el valor de puntero nulo reservado. La representación real del valor del puntero nulo no tiene nada que ver con ningún “ceros”. Puede ser absolutamente cualquier cosa apropiada para una plataforma dada. Es cierto que en la mayoría de las plataformas, el valor del puntero nulo se representa físicamente mediante un valor real de dirección cero. Sin embargo, si en alguna plataforma la dirección 0 se usa realmente para algún propósito (es decir, puede que necesite crear objetos en la dirección 0), el valor del puntero nulo en dicha plataforma probablemente será diferente. Podría representarse físicamente como valor de dirección 0xFFFFFFFF o como valor de dirección 0xBAADBAAD , por ejemplo.

Sin embargo, independientemente de cómo se represente el valor del puntero nulo en una plataforma determinada, en su código seguirá designando null-pointers por 0 constante. Para asignar un valor de puntero nulo a un puntero dado, continuará usando expresiones como p = 0 . Es responsabilidad del comstackdor darse cuenta de lo que quiere y traducirlo a la representación correcta del valor del puntero nulo, es decir, traducirlo al código que colocará el valor de la dirección de 0xFFFFFFFF en el puntero p , por ejemplo.

En resumen, el hecho de que use 0 en su código sorce para generar valores de puntero nulo no significa que el valor del puntero nulo esté de alguna manera vinculado a la dirección 0 . El 0 que usa en su código fuente es simplemente “azúcar sintáctico” que no tiene absolutamente ninguna relación con la dirección física real a la que apunta el valor del puntero nulo.

Pero dado que el direccionamiento de memoria comienza en 0, ¿no es 0 solo una dirección válida como cualquier otra?

En algunos / muchos / todos los sistemas operativos, la dirección de memoria 0 es especial de alguna manera. Por ejemplo, a menudo se asigna a la memoria no válida / inexistente, lo que provoca una excepción si intenta acceder a ella.

¿Por qué no es un número negativo nulo en su lugar?

Creo que los valores del puntero normalmente se tratan como números sin signo: de lo contrario, por ejemplo, un puntero de 32 bits solo podría direccionar 2 GB de memoria, en lugar de 4 GB.

Supongo que el valor mágico 0 se eligió para definir un puntero no válido, ya que podría probarse con menos instrucciones. Algunos lenguajes de máquina configuran automáticamente los bits de cero y de signo cuando carga registros para que pueda probar un puntero nulo con una carga simple y luego ramificar las instrucciones sin hacer una carga, luego comparar y luego bifurcar.

En el Commodore Pet, Vic20 y C64 que fueron las primeras máquinas en las que trabajé, RAM comenzó en la ubicación 0 por lo que era totalmente válido para leer y escribir usando un puntero nulo si realmente lo deseaba.

Creo que es solo una convención. Debe haber algún valor para marcar un puntero no válido.

Simplemente pierde un byte de espacio de direcciones, que rara vez debería ser un problema.

No hay punteros negativos Los punteros siempre están sin firmar. Además, si pudieran ser negativos, su convención significaría que pierde la mitad del espacio de direcciones.

Aunque C usa 0 para representar el puntero nulo, tenga en cuenta que el valor del puntero en sí no puede ser cero. Sin embargo, la mayoría de los progtwigdores solo usarán sistemas donde el puntero nulo es, de hecho, 0.

Pero, ¿por qué cero? Bueno, es una dirección que todos los sistemas comparten. Y a menudo, las direcciones bajas están reservadas para fines del sistema operativo, por lo que el valor funciona bien como fuera de los límites de los progtwigs de aplicación. La asignación accidental de un valor entero a un puntero es tan probable que termine en cero como cualquier otra cosa.

Históricamente, la baja memoria de una aplicación estaba ocupada por recursos del sistema. Fue en esos días que cero se convirtió en el valor nulo predeterminado.

Si bien esto no es necesariamente cierto para los sistemas modernos, sigue siendo una mala idea establecer valores de puntero a cualquier cosa que no sea la asignación de memoria que le haya entregado.

En cuanto al argumento sobre no establecer un puntero a nulo después de eliminarlo para que en el futuro se elimine “exponer errores” …

Si realmente estás realmente preocupado por esto, entonces un mejor enfoque, uno que se garantiza que funciona, es apalancar assert ():

 ... assert(ptr && "You're deleting this pointer twice, look for a bug?"); delete ptr; ptr = 0; ... 

Esto requiere algo de tipeo adicional, y una verificación adicional durante las comstackciones de depuración, pero es seguro que le dará lo que desea: observe cuando ptr se borra ‘dos ​​veces’. La alternativa dada en la discusión de comentarios, no establecer el puntero a nulo para que tenga un colapso, simplemente no garantiza que tenga éxito. Peor aún, a diferencia de lo anterior, puede causar un locking (¡o mucho peor!) En un usuario si uno de estos “errores” llega al estante. Finalmente, esta versión le permite continuar ejecutando el progtwig para ver qué sucede en realidad.

Me doy cuenta de que esto no responde a la pregunta, pero me preocupaba que alguien que leyera los comentarios pudiera llegar a la conclusión de que se considera una “buena práctica” NO establecer punteros a 0 si es posible que se envíen a free () o eliminar dos veces. En esos pocos casos, cuando es posible, NUNCA es una buena práctica usar el Comportamiento no definido como una herramienta de depuración. Nadie que alguna vez haya tenido que buscar un error que en última instancia fue causado al eliminar un puntero no válido lo propondría. Este tipo de errores toman horas para cazar y casi siempre afectan el progtwig de una manera totalmente inesperada que es difícil o imposible rastrear el problema original.

En una de las antiguas máquinas DEC (PDP-8, creo), el tiempo de ejecución C protegería la memoria de la primera página de la memoria, de modo que cualquier bash de acceder a la memoria en ese bloque provocaría una excepción.

La elección del valor centinela es arbitraria, y esto de hecho está siendo abordado por la próxima versión de C ++ (informalmente conocida como “C ++ 0x”, que probablemente se conozca en el futuro como ISO C ++ 2011) con la introducción del palabra clave nullptr para representar un puntero nullptr nulo. En C ++, un valor de 0 se puede usar como una expresión de inicialización para cualquier POD y para cualquier objeto con un constructor predeterminado, y tiene el significado especial de asignar el valor centinela en el caso de una inicialización de puntero. En cuanto a por qué no se eligió un valor negativo, las direcciones suelen oscilar entre 0 y 2 N -1 para algún valor N. En otras palabras, las direcciones generalmente se tratan como valores sin signo. Si el valor máximo se usó como valor centinela, entonces debería variar de un sistema a otro según el tamaño de la memoria, mientras que 0 siempre es una dirección representable. También se usa por razones históricas, ya que la dirección de memoria 0 era inutilizable en los progtwigs, y hoy en día la mayoría de los sistemas operativos tienen partes del kernel cargadas en la (s) página (s) inferior (es) de la memoria, y tales páginas normalmente están protegidas de tal forma que tocado (desreferenciado) por un progtwig (guardar el kernel) causará un error.

Tiene que tener algún valor. Obviamente, no desea pisar los valores que el usuario legítimamente podría querer usar. Me gustaría especular que dado que el tiempo de ejecución de C proporciona al segmento BSS datos de inicialización cero, tiene cierto sentido interpretar cero como un valor de puntero no inicializado.

Una razón importante por la que muchos sistemas operativos usan todos los bits cero para la representación del puntero nulo es que memset(struct_with_pointers, 0, sizeof struct_with_pointers) y similares establecerán todos los punteros dentro de struct_with_pointers para struct_with_pointers los punteros. Esto no está garantizado por el estándar C, pero muchos progtwigs lo asumen.

Rara vez un sistema operativo le permite escribir en la dirección 0. Es común incluir cosas específicas del sistema operativo en la memoria baja; a saber, IDT, tablas de páginas, etc. (Las tablas tienen que estar en la RAM, y es más fácil pegarlas en la parte inferior que tratar de determinar dónde está la parte superior de la RAM). Y ningún sistema operativo en su sano juicio le permitirá editar tablas del sistema de cualquier manera.

Esto puede no haber estado en la mente de K & R cuando hicieron C, pero (junto con el hecho de que 0 == null es bastante fácil de recordar) hace que 0 sea una opción popular.

El valor 0 es un valor especial que toma varios significados en expresiones específicas. En el caso de los punteros, como se ha señalado muchas veces, se usa probablemente porque en ese momento era la forma más conveniente de decir “inserte aquí el valor de centinela predeterminado”. Como expresión constante, no tiene el mismo significado que el cero a nivel de bit (es decir, todos los bits configurados en cero) en el contexto de una expresión de puntero. En C ++, hay varios tipos que no tienen una representación bit a bit cero de NULL , como un miembro de puntero y un puntero a la función de miembro.

Afortunadamente, C ++ 0x tiene una nueva palabra clave para “expresión que significa un puntero no válido conocido que no se asigna también a cero bit a bit para las expresiones integrales”: nullptr . Aunque hay algunos sistemas a los que se puede dirigir con C ++ que permiten desreferenciar la dirección 0 sin desviarse, entonces tenga cuidado con el progtwigdor.

Ya hay muchas buenas respuestas en este hilo; probablemente haya muchas razones diferentes para preferir el valor 0 para punteros nulos, pero voy a agregar dos más:

  • En C ++, la inicialización cero de un puntero lo establecerá en nulo.
  • En muchos procesadores es más eficiente establecer un valor en 0 o probarlo igual / no igual a 0 que para cualquier otra constante.

Esto depende de la implementación de punteros en C / C ++. No hay una razón específica por la cual NULL sea equivalente en las asignaciones a un puntero.

Hay razones históricas para esto, pero también hay razones de optimización para ello.

Es común que el sistema operativo proporcione un proceso con páginas de memoria inicializadas en 0. Si un progtwig quiere interpretar parte de esa página de memoria como un puntero, entonces es 0, por lo que es bastante fácil para el progtwig determinar que ese puntero es no inicializado. (Esto no funciona tan bien cuando se aplica a páginas flash no inicializadas)

Otra razón es que en muchos procesadores es muy fácil probar la equivalencia de un valor a 0. A veces se realiza una comparación gratuita sin necesidad de instrucciones adicionales, y generalmente se puede hacer sin necesidad de proporcionar un valor cero en otro registro o como un literal en la secuencia de instrucciones para comparar.

Las comparaciones baratas para la mayoría de los procesadores son firmadas con menos de 0, e igual a 0. (ambas partes implican más de 0 y 0 como máximo)

Dado que 1 valor de todos los valores posibles debe reservarse como malo o no inicializado, entonces también puede hacerlo el que tenga la prueba más barata para la equivalencia al valor malo. Esto también es válido para cadenas de caracteres terminadas ‘\ 0’.

Si intentara usar mayor o menor que 0 para este fin, terminaría cortando su rango de direcciones a la mitad.

La constante 0 se usa en lugar de NULL porque C fue hecha por algunos hombres de las cavernas billones de años atrás, NULL , NIL , ZIP o NADDA tendrían mucho más sentido que 0 .

Pero dado que el direccionamiento de memoria comienza en 0, ¿no es 0 solo una dirección válida como cualquier otra?

En efecto. Aunque muchos sistemas operativos le impiden mapear cualquier cosa en la dirección cero, incluso en un espacio de direcciones virtual (la gente se dio cuenta de que C es un lenguaje inseguro, y reflejando que los errores de desreferencia del puntero nulo son muy comunes, decidieron “arreglarlos” al disuadirlos código de espacio de usuario para asignar a la página 0; Por lo tanto, si llama a una callback, pero el puntero de callback es NULO, no terminará ejecutando algún código arbitrario).

¿Cómo se puede usar 0 para manejar punteros nulos si ese es el caso?

Porque 0 utilizado en comparación con un puntero se reemplazará con algún valor específico de implementación , que es el valor de retorno de malloc en una falla malloc.

¿Por qué no es un número negativo nulo en su lugar?

Esto sería aún más confuso.

( Lea este párrafo antes de leer la publicación. Le pido a cualquier persona interesada en leer esta publicación que intente leerla detenidamente y, por supuesto, no la rechace hasta que la comprenda por completo, gracias).

Ahora es wiki comunitario, como tal, si alguien no está de acuerdo con alguno de los conceptos, modifíquelo, con una explicación clara y detallada de lo que está mal y por qué, y si es posible, cite fonts o proporcione pruebas que puedan reproducirse.

Responder

Aquí hay algunas otras razones que podrían ser los factores subyacentes para NULL == 0

  1. El hecho de que cero es falso, por lo que uno puede hacer directamente if(!my_ptr) lugar de if(my_ptr==NULL) .
  2. El hecho de que los enteros globales no iniciados se inicialicen por defecto a todos los ceros, y como tal, un puntero de todos los ceros se consideraría no inicializado.

Aquí me gustaría decir una palabra en otras respuestas

No a causa del azúcar sintáctico

Decir que NULL es cero debido al azúcar sintáctico, no tiene demasiado sentido, de ser así, ¿por qué no usar el índice 0 de una matriz para mantener su longitud?

In fact C is the language that most closely resembles the internal implementation, does it make sense to say that C picked zero just because of syntactic sugar? They would rather provide a keyword null (as many other languages do) rather than mapping zero to NULL!

As such while as of today it might just syntactic sugar, it is clear that the original intention of the C language developers was not for syntactic sugar, as I will show further.

1) The Specification

Yet while it is true that the C specification speak from the constant 0 as the null pointer (section 6.3.2.3), and also define NULL to be implementation defined (section 7.19 in the C11 specification, and 7.17 in the C99 specification), the fact remains that in the book “The C Programming Language” written by the inventors of C the following is stated in section 5.4:

C guarantees that zero is never a valid address for data, so a return value of zero can be used to signal an abnormal event, in this case, no space.

Pointer and integers are not interchangeable, Zero is the sole exception: the constant zero may be assigned to a pointer, and a pointer may be compared with the constant zero. The symbolic constant NULL is often used in place of zero, as a mnemonic to indicate more clearly that this is a special value for a pointer. NULL is defined in . We will use NULL henceforth.

As one can see (from the words “zero address”) at least the original intention of the authors of C were of the address zero, and not the constant zero, moreover it appears from this excerpt that the reason why the specification speaks from the constant zero is probably not to exclude an expression that evaluates to zero, but instead to include the integer constant zero to be the only integer constant allowed for use in a pointer context without casting.

2) Summary

While the specification does not say explicitly that a zero address can be treated different than the zero constant, it does not say that not, and the fact the when dealing with the null-pointer constant it does not claim it to be implementation defined as it does by the NULL defined constant, instead claim it to be zero, shows that there might be a difference between the zero constant and the zero address.

(However if this is the case I just wonder why NULL is implementation defined, since in such a case NULL can also be the constant zero, as the compiler anyway has to convert all zero constants into the actual implementation defined NULL?)

However I don not see this in real action, and in the general platforms the address zero and the constant zero are treated the same, and throw the same error message.

Furthermore the fact is that today’s operating systems are actually reserving the entire first page (range 0x0000 to 0xFFFF), just to prevent access to the zero address because of C’s NULL pointer, (see http://en.wikipedia.org/wiki/Zero_page , as well as “Windows Via C/C++ by Jeffrey Richter and Christophe Nasarre (published by Microsoft Press)”).

Thus I would ask from anyone claiming to actually have it seen in action, to please specify the platform, and compiler, and the exact code he actually did, (although due to the vague definition in the specification [as I have shown] any compiler and platform is free to do whatever he wants).

However it apparently seems that the authors of C didn’t had this in mind, and they were speaking of the “zero address”, and that “C guarantees that it is never a valid address”, as well as “NULL is just a mnemonic”, clearly showing that it’s original intention was not for “syntactic sugar”.

Not Because Of The Operating System

Also claiming that the operating system denies access to address zero, for a few reasons:

1) When C was written there was no such restriction, as one can see on this wikipage http://en.wikipedia.org/wiki/Zero_page .

2) The fact is that C compilers did accessed memory address zero.

This appears to be the fact from the following paper by BellLabs ( http://www.cs.bell-labs.com/who/dmr/primevalC.html )

The two compilers differ in the details in how they cope with this. In the earlier one, the start is found by naming a function; in the later, the start is simply taken to be 0. This indicates that the first compiler was written before we had a machine with memory mapping, so the origin of the program was not at location 0, whereas by the time of the second, we had a PDP-11 that did provide mapping.

(In fact as of today (as I cited references above from wikipedia and microsoft press), the reason for restricting access to the zero address is because of C’s NULL pointers! So at the end it turns out to be the other way around!)

3) Remember that C is also used to write operating systems, and even C compilers!

In fact C was developed for the purpose of writing the UNIX operating system with it, and as such it appears to be no reason why they should restrict themselves from address zero.

(Hardware) Explanation On How Computers Are (Physically) Able To Access Address Zero

There is another point I want to explain here, how is it possible to reference address zero at all?

Think of it for a second, the addresses are fetched by the processor, and then sent as voltages on the memory bus, which is then used by the memory system to get to the actual address, and yet a address of zero will mean no voltage, so how is the physical hardware of the memory system accessing address zero?

The answer appears to be, that address zero is the default, and in other words address zero is always accessible by the memory system when the memory bus is completly off, and as such any request to read or write without specifying an actual address (which is the case with address zero) is automatically accessing address zero.