c pointers language-lawyer pointer-arithmetic

¿El estándar C permite asignar un valor arbitrario a un puntero e incrementarlo?



pointers language-lawyer (5)

Es un comportamiento indefinido.

A partir de N1570 (énfasis añadido):

Un entero se puede convertir a cualquier tipo de puntero. Excepto como se especificó anteriormente, el resultado está definido por la implementación, podría no estar correctamente alineado, podría no apuntar a una entidad del tipo referenciado y podría ser una representación de captura.

Si el valor es una representación de captura, leerlo es un comportamiento indefinido:

Ciertas representaciones de objetos no necesitan representar un valor del tipo de objeto. Si el valor almacenado de un objeto tiene una representación de este tipo y es leído por una expresión lvalue que no tiene tipo de carácter, el comportamiento no está definido. Si una representación de este tipo se produce por un efecto secundario que modifica todo o parte del objeto por una expresión lvalue que no tiene tipo de carácter, el comportamiento no está definido. ) Tal representación se llama una representación de trampa.

Y

Un identificador es una expresión primaria, siempre que se haya declarado que designa un objeto (en cuyo caso es un valor lime ) o una función (en cuyo caso es un designador de función).

Por lo tanto, la línea void *ptr = (char *)0x01; ya es un comportamiento potencialmente indefinido, en una implementación donde (char*)0x01 o (void*)(char*)0x01 es una representación de captura. El lado izquierdo es una expresión lvalue que no tiene tipo de carácter y lee una representación de captura.

En algunos equipos, cargar un puntero no válido en el registro de una máquina podría bloquear el programa, por lo que fue un movimiento forzado por parte del comité de estándares.

¿Está bien definido el comportamiento de este código?

#include <stdio.h> #include <stdint.h> int main(void) { void *ptr = (char *)0x01; size_t val; ptr = (char *)ptr + 1; val = (size_t)(uintptr_t)ptr; printf("%zu/n", val); return 0; }

Quiero decir, ¿podemos asignar algún número fijo a un puntero e incrementarlo incluso si está apuntando a una dirección aleatoria? (Sé que no se puede desreferirlo)


La Norma no requiere que las implementaciones procesen conversiones de entero a puntero de manera significativa para ningún valor entero en particular, o incluso para cualquier valor entero posible distinto de las constantes de puntero nulo. Lo único que garantiza acerca de tales conversiones es que un programa que almacena el resultado de tal conversión directamente en un objeto de tipo puntero adecuado y no hace nada con él, excepto examinar los bytes de ese objeto, en el peor de los casos, verá valores no especificados. Si bien el comportamiento de convertir un entero en un puntero está definido por la implementación, nada prohibiría que cualquier implementación (¡no importa lo que realmente haga con tales conversiones!) Especifique que algunos (o incluso todos) los bytes de la representación que tienen valores no especificados , y especificando que algunos (o incluso todos) valores enteros pueden comportarse como si produjeran representaciones de trampas.

Las únicas razones por las que el Estándar dice algo acerca de las conversiones de entero a puntero son que:

  1. En algunas implementaciones, la construcción es significativa, y algunos programas para esas implementaciones lo requieren.

  2. A los autores de la Norma no les gustó la idea de una construcción que se utilizó en algunas implementaciones representaría una violación de restricción en otras.

  3. Hubiera sido extraño que el Estándar describiera una construcción pero luego especifique que tiene un comportamiento indefinido en todos los casos.

Personalmente, creo que el Estándar debería haber permitido que las implementaciones traten las conversiones de entero a puntero como infracciones de restricciones si no definen situaciones en las que serían útiles, en lugar de requerir que los compiladores acepten el código sin sentido, pero eso no fue así. La filosofía de la época.

Creo que sería más sencillo decir simplemente que cualquier operación que implique conversiones de entero a puntero con algo distinto a los valores intptr_t o uintptr_t recibidos de conversiones de puntero a entero invoca un comportamiento indefinido, pero luego note que es común para las implementaciones de calidad previstas. para que la programación de bajo nivel procese un comportamiento indefinido "de una manera documentada característica del entorno". El Estándar no especifica cuándo las implementaciones deben procesar los programas que invocan UB de esa manera, sino que lo tratan como un problema de Calidad de la Implementación.

Si una implementación especifica que las conversiones de entero a puntero operan de una manera que definiría el comportamiento de

char *p = (char*)1; p++;

como equivalente a "char p = (char ) 2;", entonces se debe esperar que la implementación funcione de esa manera. Por otro lado, una implementación podría definir el comportamiento de la conversión de entero a puntero de tal manera que incluso:

char *p = (char*)1; char *q = p; // Not doing any arithmetic here--just a simple assignment

liberaría demonios nasales. En la mayoría de las plataformas, un compilador donde la aritmética de los punteros producidos por conversiones de enteros a punteros se comportó de manera extraña no se vería como una implementación de alta calidad adecuada para la programación de bajo nivel. Un programador que no tiene la intención de apuntar a ningún otro tipo de implementaciones podría esperar que tales construcciones se comporten de manera útil en los compiladores para los cuales el código era adecuado, incluso aunque el Estándar no lo requiera.


La asignación:

void *ptr = (char *)0x01;

El comportamiento de la implementación está definido porque está convirtiendo un entero en un puntero. Esto se detalla en la sección 6.3.2.3 del estándar C con respecto a los punteros:

5 Un entero se puede convertir a cualquier tipo de puntero. Excepto como se especificó anteriormente, el resultado está definido por la implementación, podría no estar correctamente alineado, podría no apuntar a una entidad del tipo referenciado y podría ser una representación de captura.

En cuanto a la aritmética de puntero posterior:

ptr = (char *)ptr + 1;

Esto depende de algunas cosas.

Primero, el valor actual de ptr puede ser una representación de trampa como se indica en 6.3.2.3 anterior. Si lo es, el comportamiento es indefinido .

La siguiente es la pregunta de si 0x1 apunta a un objeto válido. Agregar un puntero y un entero solo es válido si el operando del puntero y el resultado apuntan a los elementos de un objeto de matriz (un solo objeto cuenta como una matriz de tamaño 1) o un elemento más allá del objeto de matriz. Esto se detalla en la sección 6.5.6:

7 A los efectos de estos operadores, un puntero a un objeto que no es un elemento de una matriz se comporta igual que un puntero al primer elemento de una matriz de longitud uno con el tipo del objeto como su tipo de elemento

8 Cuando una expresión que tiene un tipo entero se agrega o se resta de un puntero, el resultado tiene el tipo del operando del puntero. Si el operando puntero apunta a un elemento de un objeto de matriz, y la matriz es lo suficientemente grande, el resultado apunta a un elemento desplazado desde el elemento original, de manera que la diferencia de los subíndices de los elementos de la matriz resultante y original sea igual a la expresión entera. En otras palabras, si la expresión P apunta al elemento i-th de un objeto de matriz, las expresiones (P) + N (equivalentemente, N + (P) ) y (P) -N (donde N tiene el valor n) punto a, respectivamente, los elementos i + n-th e i-n-th del objeto de matriz, siempre que existan. Además, si la expresión P apunta al último elemento de un objeto de matriz, la expresión (P) +1 apunta uno más allá del último elemento del objeto de matriz, y si la expresión Q apunta uno más allá del último elemento de un objeto de matriz, la expresión (Q) -1 apunta al último elemento del objeto de matriz. Si tanto el operando puntero como el resultado apuntan a elementos del mismo objeto de matriz, o uno más allá del último elemento del objeto de matriz, la evaluación no producirá un desbordamiento; de lo contrario, el comportamiento es indefinido. Si el resultado apunta a uno más allá del último elemento del objeto de la matriz, no se utilizará como el operando de un operador unario * que se evalúa.

En una implementación hospedada, el valor 0x1 casi no apunta a un objeto válido, en cuyo caso la adición no está definida . Sin embargo, una implementación incrustada podría admitir la configuración de punteros a valores específicos, y de ser así, 0x1 en realidad apunta a un objeto válido. Si es así, el comportamiento está bien definido , de lo contrario no está definido .


No, el comportamiento de este programa es indefinido. Una vez que se alcanza una construcción indefinida en un programa, cualquier comportamiento futuro queda indefinido. Paradójicamente, cualquier comportamiento pasado también está indefinido.

El resultado de void *ptr = (char*)0x01; está definido por la implementación, debido en parte al hecho de que un char puede tener una representación de captura.

Pero el comportamiento de la aritmética de punteros resultante en la declaración ptr = (char *)ptr + 1; no está definido . Esto se debe a que la aritmética de punteros solo es válida dentro de matrices, incluida una pasada el final de la matriz. Para este propósito un objeto es una matriz de longitud uno.


Sí, el código está bien definido como definido por la implementación. No está indefinido. Ver ISO / IEC 9899: 2011 [6.3.2.3] / 5 y nota 67.

El lenguaje C fue creado originalmente como un lenguaje de programación del sistema. La programación de sistemas requería la manipulación de hardware mapeado en memoria, lo que requería que usted rellenara las direcciones codificadas en los punteros, a veces los incrementara, y los datos de lectura y escritura desde y hacia la dirección resultante. Para ese fin, la asignación de un puntero a un puntero y su manipulación mediante la aritmética está bien definida por el lenguaje. Al definirlo con la implementación, lo que permite el lenguaje es que pueden suceder todo tipo de cosas: desde el clásico detener y atrapar un error de bus cuando se intenta eliminar la referencia de una dirección impar.

La diferencia entre un comportamiento indefinido y un comportamiento definido por la implementación es básicamente un comportamiento indefinido significa "no hagas eso, no sabemos qué pasará" y un comportamiento definido por la implementación significa "está bien seguir adelante y hacerlo, depende de Que sepas lo que va a pasar ".