Esta pregunta pretende ser una entrada de preguntas frecuentes para todos los problemas de inicialización/asignación entre números enteros y punteros.
Quiero escribir código donde un puntero se establece en una dirección de memoria específica, por ejemplo 0x12345678
. Pero al compilar este código con el compilador gcc, obtengo advertencias/errores de “la inicialización hace que el puntero sea un número entero sin conversión”:
int* p = 0x12345678;
De manera similar, este código da “la inicialización hace un número entero desde el puntero sin conversión”:
int* p = ...;
int i = p;
Si hago lo mismo fuera de la línea de declaración de variables, el mensaje es el mismo pero dice “asignación” en lugar de “inicialización”:
p = 0x12345678; // "assignment makes pointer from integer without a cast"
i = p; // "assignment makes integer from pointer without a cast"
Las pruebas con otros compiladores populares también dan mensajes de error/advertencia:
- clang dice “conversión de entero a puntero incompatible”
- icc dice “un valor de tipo
int
no se puede utilizar para inicializar una entidad de tipo int*
“
- MSVC (cl) dice “inicializando
int*
difiere en los niveles de indirección de int
“.
Pregunta: ¿Los ejemplos anteriores son C válidos?
Y una pregunta de seguimiento:
Esto no da ninguna advertencia/error:
int* p = 0;
¿Por qué no?
No, no es C válido y nunca ha sido C válido. Estos ejemplos se denominan violaciones de restricciones de la norma.
El estándar no le permite inicializar/asignar un puntero a un número entero, ni un número entero a un puntero. Debe forzar manualmente una conversión de tipo con un molde:
int* p = (int*) 0x1234;
int i = (int)p;
Si no usa el molde, el código no es C válido y su compilador no puede dejar pasar el código sin mostrar un mensaje. El operador cast afirma: C17 6.5.4/3:
Restricciones
/–/
Las conversiones en las que intervienen punteros, excepto cuando lo permitan las restricciones de 6.5.16.1, se especificarán mediante una conversión explícita.
6.5.16.1 siendo las reglas de tarea sencilla que permiten ciertas conversiones implícitas de punteros, ver C17 6.5.16.1 §1:
6.5.16.1 Asignación simple
Restricciones
Se cumplirá uno de los siguientes:
- 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 una estructura o tipo de 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 al que apunta la izquierda tiene todos los calificativos 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 nulo, 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 de la izquierda tiene tipo _Bool atómico, calificado o no calificado, y el de la derecha es un puntero.
En caso de int* p = 0x12345678;
el operando izquierdo es un puntero y el derecho es un tipo aritmético.
En caso de int i = p;
el operando izquierdo es un tipo aritmético y el derecho es un puntero.
Ninguno de estos encaja con ninguna de las limitaciones citadas anteriormente.
En cuanto a por qué int* p = 0;
funciona, es un caso especial. El operando izquierdo es un puntero y el derecho es un constante de puntero nulo. Más información sobre la diferencia entre punteros nulos, constantes de puntero nulo y la macro NULL.
Algunas cosas a tener en cuenta:
-
Si asigna una dirección sin procesar a un puntero, es probable que el puntero deba ser volatile
calificado, dado que apunta a algo como un registro de hardware o una ubicación de memoria EEPROM/Flash, que puede cambiar su contenido en tiempo de ejecución.
-
De ninguna manera se garantiza que la conversión de un puntero a un número entero funcione incluso con el molde. El estándar (C17 6.3.2.3 §5 y §6 dice):
Un entero se puede convertir en cualquier tipo de puntero. Salvo que se especifique lo contrario, el resultado está definido por la implementación, es posible que no esté correctamente alineado, que no apunte a una entidad del tipo al que se hace referencia y que sea una representación de captura. 68)
Cualquier tipo de puntero se puede convertir a un tipo entero. Salvo que se especifique lo contrario, el resultado está definido por la implementación. Si el resultado no se puede representar en el tipo entero, el comportamiento no está definido. El resultado no necesita estar en el rango de valores de ningún tipo entero.
Nota informativa al pie:
68) Las funciones de mapeo para convertir un puntero en un número entero o un número entero en un puntero están destinadas a ser coherentes con la estructura de direccionamiento del entorno de ejecución.
Además, la dirección de un puntero puede ser más grande que la que cabe dentro de un int
, como es el caso de la mayoría de los sistemas de 64 bits. Por lo tanto, es mejor usar el uintptr_t
desde <stdint.h>
la pregunta podría decir explícitamente que “Quiero tener un puntero que apunte a la dirección de memoria 0x12345678” y luego la respuesta podría abordar esto
– Antti Haapala — Слава Україні
5 sep 2018 a las 13:56
@AnttiHaapala Sí, buena decisión, especialmente porque la respuesta ya insinuaba las mejores prácticas cuando se trata de direcciones de memoria. Agregué una nueva oración en la parte superior de la pregunta.
– Lundin
5 de septiembre de 2018 a las 14:02