c++ - que - punteros y matrices en c
¿Incrementar un puntero nulo está bien definido? (9)
§5.2.6 / 1:
El valor del objeto operando se modifica agregando
1
a él, a menos que el objeto sea de tipobool
[..]
Y las expresiones aditivas que implican punteros se definen en §5.7 / 5:
Si tanto el operando del puntero como el resultado apuntan a elementos del mismo objeto del arreglo, o uno más allá del último elemento del objeto del arreglo, la evaluación no producirá un desbordamiento; de lo contrario, el comportamiento no está definido.
Hay muchos ejemplos de comportamiento indefinido / no especificado al hacer aritmética de puntero: los punteros deben apuntar dentro del mismo conjunto (o uno más allá del final), o dentro del mismo objeto, restricciones sobre cuándo puede hacer comparaciones / operaciones en función de lo anterior , etc.
¿La siguiente operación está bien definida?
int* p = 0;
p++;
Como dijo Columbo, es UB. Y desde el punto de vista de un abogado de idiomas, esta es la respuesta definitiva.
Sin embargo, todas las implementaciones del compilador de C ++ que conozco darán el mismo resultado:
int *p = 0;
intptr_t ip = (intptr_t) p + 1;
cout << ip - sizeof(int) << endl;
da 0
, lo que significa que p
tiene el valor 4 en una implementación de 32 bits y 8 en uno de 64 bits
Dicho de manera diferente:
int *p = 0;
intptr_t ip = (intptr_t) p; // well defined behaviour
ip += sizeof(int); // integer addition : well defined behaviour
int *p2 = (int *) ip; // formally UB
p++; // formally UB
assert ( p2 == p) ; // works on all major implementation
Dado que puede incrementar cualquier puntero de un tamaño bien definido (por lo que cualquier cosa que no sea un puntero nulo), y el valor de cualquier puntero es solo una dirección (no hay un manejo especial para los punteros NULL una vez que existen), supongo no hay ninguna razón por la cual un puntero nulo incrementado no apunte (inútilmente) al elemento ''uno después de NULL''.
Considera esto:
// These functions are horrible, but they do return the ''next''
// and ''prev'' items of an int array if you pass in a pointer to a cell.
int *get_next(int *p) { return p+1; }
int *get_prev(int *p) { return p-1; }
int *j = 0;
int *also_j = get_prev(get_next(j));
también_ le han hecho matemáticas, pero es igual a j, por lo que es un puntero nulo.
Por lo tanto, sugeriría que está bien definido, es inútil.
(Y el puntero nulo que parece tener el valor cero cuando printfed es irrelevante. El valor del puntero nulo depende de la plataforma. El uso de un cero en el idioma para inicializar las variables del puntero es una definición del lenguaje).
De ISO IEC 14882-2011 §5.2.6 :
El valor de una expresión postfix ++ es el valor de su operando. [Nota: el valor obtenido es una copia del valor original - nota final] El operando debe ser un valor l modificable. El tipo del operando debe ser un tipo aritmético o un puntero a un tipo de objeto completo.
Dado que nullptr es un puntero a un tipo de objeto completo. Entonces no veo por qué esto sería un comportamiento indefinido.
Como se ha dicho antes, el mismo documento también establece en §5.2.6 / 1 :
Si tanto el operando del puntero como el resultado apuntan a elementos del mismo objeto del arreglo, o uno más allá del último elemento del objeto del arreglo, la evaluación no producirá un desbordamiento; de lo contrario, el comportamiento no está definido.
Esta expresión parece un poco ambigua. En mi interpretación, la parte indefinida podría ser la evaluación del objeto. Y creo que nadie estaría en desacuerdo con que este sea el caso. Sin embargo, las aritméticas de puntero parecen requerir solo un objeto completo.
Por supuesto, los operadores postfix [] y las restas o multiplicaciones en el puntero a los objetos de la matriz están solo bien definidos, si de hecho apuntan a la misma matriz. Principalmente importante porque uno podría tener la tentación de pensar que 2 matrices definidas sucesivamente en 1 objeto pueden repetirse como si fueran una única matriz.
Entonces mi conclusión sería que la operación está bien definida, pero la evaluación no lo sería.
De vuelta en los divertidos días C, si p era un puntero a algo, p ++ efectivamente estaba agregando el tamaño de p al valor del puntero para hacer p punto en el siguiente algo. Si configura el puntero p en 0, entonces es lógico pensar que p ++ todavía lo apuntará a lo siguiente al agregarle el tamaño de p.
Además, podría hacer cosas como sumar o restar números de p para moverlos a lo largo de la memoria (p + 4 señalaría el 4to algo pasado p.) Estos fueron tiempos buenos que tenían sentido. Dependiendo del compilador, puede ir a cualquier lugar que desee dentro de su espacio de memoria. Los programas corrían rápido, incluso con hardware lento porque C acababa de hacer lo que le pedías y se bloqueaba si te volvías loco / descuidado.
Entonces, la respuesta real es que establecer un puntero a 0 está bien definido e incrementar el puntero está bien definido. El constructor de compiladores, los desarrolladores de SO y los diseñadores de hardware le imponen cualquier otra restricción.
El estándar C requiere que ningún objeto creado a través de los medios definidos por el estándar pueda tener una dirección que sea igual a un puntero nulo. Sin embargo, las implementaciones pueden permitir la existencia de objetos que no se crean a través de medios definidos por el estándar, y el estándar no dice si tal objeto podría tener una dirección que (debido a problemas de diseño de hardware) es igual a un puntero nulo .
Si una implementación documenta la existencia de un objeto multibyte cuya dirección se compare igual a nulo, entonces en esa implementación se dice char *p = (char*)0;
Haría que p
mantuviera un puntero al primer byte de ese objeto [que se compararía igual a un puntero nulo], y p++
lo haría apuntar al segundo byte. Sin embargo, a menos que una implementación documente la existencia de dicho objeto o especifique que realizará la aritmética del puntero como si tal objeto existiera, no hay razón para esperar un comportamiento particular. Hacer que la implementación intercepte deliberadamente los intentos de realizar cualquier tipo de aritmética en punteros nulos, además de sumar o restar cero u otros punteros nulos, puede ser una medida de seguridad útil, y el código que incrementaría los punteros nulos para algún fin útil sería incompatible. Peor aún, algunos compiladores "inteligentes" pueden decidir que pueden omitir las comprobaciones nulas en los casos en los punteros que se incrementarían incluso si tienen nulo, lo que permitiría todo tipo de estragos.
Las operaciones en un puntero (como incrementar, agregar, etc.) generalmente solo son válidas si tanto el valor inicial del puntero como el resultado apuntan a elementos de la misma matriz (o a uno más allá del último elemento). De lo contrario, el resultado no está definido. Hay varias cláusulas en el estándar para los diversos operadores que dicen esto, incluso para incrementar y agregar.
(Hay un par de excepciones como agregar cero a NULL o restar cero de NULL siendo válido, pero eso no se aplica aquí).
Un puntero NULL no apunta a nada, por lo que al incrementarlo se obtiene un comportamiento indefinido (se aplica la cláusula "de lo contrario").
Parece que hay una comprensión bastante baja de lo que significa "comportamiento indefinido".
En C, C ++ y en lenguajes relacionados como Objective-C, existen cuatro tipos de comportamiento: Hay un comportamiento definido por el estándar de lenguaje. Hay un comportamiento definido en la implementación, lo que significa que el estándar de lenguaje explícitamente dice que la implementación debe definir el comportamiento. Existe un comportamiento no especificado, donde el estándar de lenguaje dice que varios comportamientos son posibles. Y hay un comportamiento indefinido, donde el estándar de lenguaje no dice nada sobre el resultado . Debido a que el estándar de lenguaje no dice nada sobre el resultado, puede suceder cualquier cosa con un comportamiento indefinido.
Algunas personas aquí suponen que "comportamiento indefinido" significa "algo malo sucede". Eso está mal. Significa que "cualquier cosa puede suceder", y eso incluye "algo malo puede pasar", no "algo malo debe suceder". En la práctica, significa que "nada malo sucede cuando se prueba el programa, pero tan pronto como se envía a un cliente, se desata el infierno". Como puede pasar cualquier cosa, el compilador puede suponer que no hay un comportamiento indefinido en su código, porque o es verdadero o es falso, en cuyo caso puede pasar cualquier cosa, lo que significa que todo lo que sucede debido a la suposición incorrecta del compilador correcto.
Alguien afirmó que cuando p apunta a una matriz de 3 elementos, y se calcula p + 4, no pasará nada malo. Incorrecto. Aquí viene tu compilador de optimización. Diga que este es su código:
int f (int x)
{
int a [3], b [4];
int* p = (x == 0 ? &a [0] : &b [0]);
p + 4;
return x == 0 ? 0 : 1000000 / x;
}
La evaluación de p + 4 es un comportamiento indefinido si p apunta a a [0], pero no si apunta a b [0]. Por lo tanto, se permite al compilador suponer que p apunta a b [0]. El compilador, por lo tanto, puede suponer que x! = 0, porque x == 0 conduce a un comportamiento indefinido. Por lo tanto, el compilador puede quitar la verificación x == 0 en la declaración de devolución y simplemente devolver 1000000 / x. Lo que significa que su programa falla cuando llama a f (0) en lugar de devolver 0.
Otra suposición que se hizo fue que si incrementa un puntero nulo y luego lo reduce de nuevo, el resultado es nuevamente un puntero nulo. Nuevamente incorrecto. Además de la posibilidad de que el incremento de un puntero nulo solo se bloquee en algún hardware, ¿qué pasa con esto? Ya que incrementar un puntero nulo es un comportamiento indefinido, el compilador verifica si un puntero es nulo y solo incrementa el puntero si no es un puntero nulo , entonces p + 1 es nuevamente un puntero nulo. Y normalmente haría lo mismo para la disminución, pero como es un compilador inteligente, advierte que p + 1 es siempre un comportamiento indefinido si el resultado fue un puntero nulo, por lo tanto, se puede suponer que p + 1 no es un puntero nulo, por lo tanto, se puede omitir la verificación del puntero nulo. Lo que significa que (p + 1) - 1 no es un puntero nulo si p era un puntero nulo.
Resulta que en realidad no está definido. Hay sistemas para los cuales esto es verdad
int *p = NULL;
if (*(int *)&p == 0xFFFF)
Por lo tanto, ++ p dispararía la regla de desbordamiento indefinido (resulta que sizeof (int *) == 2)). No se garantiza que los punteros sean enteros sin signo, de modo que la regla de ajuste sin signo no se aplica.