c++ - matrices - ¿Por qué la aritmética de punteros fuera de límites es un comportamiento indefinido?
punteros y matrices en c (7)
El siguiente ejemplo es de Wikipedia .
int arr[4] = {0, 1, 2, 3};
int* p = arr + 5; // undefined behavior
Si nunca elimino la referencia p, entonces ¿por qué arr + 5 solo es un comportamiento indefinido? Espero que los punteros se comporten como enteros, con la excepción de que cuando se hace referencia al valor de un puntero se considera una dirección de memoria.
"Comportamiento indefinido" no significa que deba bloquearse en esa línea de código, pero sí significa que no se puede garantizar el resultado. Por ejemplo:
int arr[4] = {0, 1, 2, 3};
int* p = arr + 5; // I guess this is allowed to crash, but that would be a rather
// unusual implementation choice on most machines.
*p; //may cause a crash, or it may read data out of some other data structure
assert(arr < p); // this statement may not be true
// (arr may be so close to the end of the address space that
// adding 5 overflowed the address space and wrapped around)
assert(p - arr == 5); //this statement may not be true
//the compiler may have assigned p some other value
Estoy seguro de que hay muchos otros ejemplos que puedes incluir aquí.
Además de los problemas de hardware, otro factor fue el surgimiento de implementaciones que intentaron interceptar varios tipos de errores de programación. Aunque muchas de estas implementaciones podrían ser más útiles si se configuran para interceptar en construcciones que un programa sabe que no utilizan, aunque están definidas por el Estándar C, los autores de la Norma no querían definir el comportamiento de constructos que podrían - en muchos campos de programación - ser sintomático de errores.
En muchos casos, será mucho más fácil capturar las acciones que usan aritmética de punteros para calcular la dirección de los objetos no deseados que registrar de alguna manera el hecho de que los punteros no pueden usarse para acceder al almacenamiento que identifican, pero podrían modificarse para que puedan Acceder a otro almacenamiento. Excepto en el caso de matrices dentro de matrices más grandes (bidimensionales), se permitiría a una implementación reservar espacio que está "más allá" del final de cada objeto. Dado algo como doSomethingWithItem(someArray+i);
, una implementación podría interceptar cualquier intento de pasar cualquier dirección que no apunte a un elemento de la matriz o al espacio justo después del último elemento. Si la asignación de someArray
espacio reservado de someArray
para un elemento no utilizado adicional, y doSomethingWithItem()
solo accede al elemento al que recibe un puntero, la implementación podría garantizar de manera relativamente económica que cualquier ejecución no atrapada del código anterior podría, en el peor de los casos -acceso de lo contrario, el almacenamiento no utilizado.
La capacidad de calcular direcciones "justas y pasadas" hace que la verificación de límites sea más difícil de lo que sería (la situación errónea más común sería pasar doSomethingWithItem()
un puntero justo después del final de la matriz, pero el comportamiento estaría definido a menos que doSomethingWithItem
trataría de no hacer referencia a ese puntero (algo que la persona que llama no puede probar). Sin embargo, dado que la Norma permitiría a los compiladores reservar espacio justo después de la matriz en la mayoría de los casos, dicha asignación permitiría a las implementaciones limitar el daño causado por errores no resueltos, algo que probablemente no sería práctico si se permitiera una aritmética de punteros más generalizada.
Algunos sistemas, sistemas muy raros y no puedo nombrar uno, causarán trampas cuando incrementas los límites pasados de esa manera. Además, permite una implementación que proporciona protección de límites para existir ... otra vez, aunque no puedo pensar en una.
Esencialmente, no deberías hacerlo y, por lo tanto, no hay razón para especificar qué sucede cuando lo haces. Especificar qué sucede pone una carga injustificada en el proveedor de la implementación.
El x86 original puede tener problemas con tales declaraciones. En el código de 16 bits, los punteros son 16 + 16 bits. Si agrega un desplazamiento a los 16 bits inferiores, es posible que deba lidiar con el desbordamiento y cambiar los 16 bits superiores. Esa fue una operación lenta y es mejor evitarla.
En esos sistemas, se garantizó que array_base+offset
no se desbordaría, si el offset estaba en el rango (<= tamaño de matriz). Pero la array+5
se desbordaría si la matriz contuviera solo 3 elementos.
La consecuencia de ese desbordamiento es que tienes un puntero que no apunta detrás de la matriz, sino antes. Y eso podría no ser RAM, sino hardware de memoria asignada. El estándar de C ++ no intenta limitar lo que sucede si construye punteros a componentes de hardware aleatorios, es decir, es un comportamiento indefinido en sistemas reales.
Eso es porque los punteros no se comportan como enteros. Es un comportamiento indefinido porque el estándar lo dice.
Sin embargo, en la mayoría de las plataformas (si no todas), no se producirá un bloqueo o se ejecutará un comportamiento dudoso si no se desreferen los arreglos. Pero entonces, si no lo eliminas, ¿para qué hacer la adición?
Dicho esto, tenga en cuenta que una expresión que va sobre el final de una matriz es técnicamente "correcta" al 100% y se garantiza que no se bloquea según §5.7 ¶5 de la especificación C ++ 11. Sin embargo, el resultado de esa expresión no se especifica (solo se garantiza que no será un desbordamiento); mientras que cualquier otra expresión que pase más de uno más allá de los límites de la matriz es explícitamente un comportamiento indefinido .
Nota: Eso no significa que sea seguro leer y escribir desde un desplazamiento de más de uno. Es probable que esté editando datos que no pertenecen a esa matriz y causará daños en el estado / memoria. Simplemente no causará una excepción de desbordamiento.
Mi conjetura es que es así porque no solo la desreferenciación está mal. También puede apuntar aritmética, comparar punteros, etc. Así que es más fácil decir que no haga esto en lugar de enumerar las situaciones en las que puede ser peligroso.
Este resultado que está viendo es debido a la protección de memoria basada en segmentos del x86. Considero que esta protección está justificada, ya que cuando se incrementa la dirección del puntero y se almacena, significa que en el futuro, en su código, se eliminará la referencia del puntero y se usará el valor. Así que el compilador quiere evitar este tipo de situaciones en las que terminará cambiando la ubicación de la memoria de otra persona o eliminando la memoria que pertenece a otra persona en su código. Para evitar tal compilador de escenario ha puesto la restricción.
Si arr
está justo al final del espacio de memoria de la máquina, arr+5
podría estar fuera de ese espacio de memoria, por lo que el tipo de puntero podría no representar el valor, es decir, podría desbordarse y el desbordamiento no está definido.