Indización de puntero

11

Actualmente estoy leyendo un libro titulado "Recetas numéricas en C". En este libro, el autor detalla cómo ciertos algoritmos funcionan inherentemente mejor si tenemos índices que comienzan con 1 (no sigo completamente su argumento y ese no es el punto de esta publicación), pero C siempre indexa sus matrices comenzando con 0 Para evitar esto, sugiere simplemente disminuir el puntero después de la asignación, por ejemplo:

float *a = malloc(size);
a--;

Esto, dice, le dará efectivamente un puntero que tiene un índice que comienza con 1, que luego se liberará con:

free(a + 1);

Sin embargo, hasta donde yo sé, este es un comportamiento indefinido del estándar C. Aparentemente, este es un libro de gran reputación dentro de la comunidad de HPC, por lo que no quiero simplemente ignorar lo que está diciendo, pero simplemente decrementar un puntero fuera del rango asignado me parece muy incompleto. ¿Es este comportamiento "permitido" en C? Lo probé usando gcc y icc, y ambos resultados parecen indicar que no me preocupa nada, pero quiero ser absolutamente positivo.

wolfPack88
fuente
3
¿A qué estándar C se refiere? Pregunto porque según mi recuerdo, "Recetas Numéricas en C" se publicó en la década de 1990, en la antigüedad de K&R y tal vez ANSI C
gnat
2
Pregunta SO relacionada: stackoverflow.com/questions/10473573/…
dan04
3
"Lo probé usando gcc e icc, y ambos resultados parecen indicar que no me preocupa nada, pero quiero ser absolutamente positivo". Nunca asuma que porque su compilador lo permite, el lenguaje C lo permite. A menos, por supuesto, que esté bien con que su código se rompa en el futuro.
Doval
55
Sin querer ser sarcástico, "Recetas numéricas" generalmente se considera un libro útil, rápido y sucio, no un paradigma de desarrollo de software o análisis numérico. Consulte el artículo de Wikipedia sobre "Recetas numéricas" para obtener un resumen de algunas de las críticas.
Charles E. Grant
1
Como comentario aparte
Russell Borogove

Respuestas:

16

Tienes razón en ese código como

float a = malloc(size);
a--;

produce un comportamiento indefinido, según el estándar ANSI C, sección 3.3.6:

A menos que tanto el operando del puntero como el resultado apunten a un miembro del mismo objeto de matriz, o uno pasado el último miembro del objeto de matriz, el comportamiento es indefinido

Para un código como este, la calidad del código C en el libro (cuando lo usé a fines de la década de 1990) no se consideraba muy alta.

El problema con el comportamiento indefinido es que no importa qué resultado produzca el compilador, ese resultado es, por definición, correcto (incluso si es altamente destructivo e impredecible).
Afortunadamente, muy pocos compiladores hacen un esfuerzo para causar un comportamiento inesperado en tales casos y la mallocimplementación típica en las máquinas utilizadas para HPC tiene algunos datos de contabilidad justo antes de la dirección que devuelve, por lo que la disminución generalmente le daría un puntero a esos datos de contabilidad. No es una buena idea escribir allí, pero solo crear el puntero es inofensivo en esos sistemas.

Solo tenga en cuenta que el código podría romperse cuando se cambia el entorno de tiempo de ejecución o cuando el código se transfiere a un entorno diferente.

Bart van Ingen Schenau
fuente
44
Exactamente, es posible en una arquitectura de múltiples bancos que malloc pueda darle la dirección 0 en un banco y su disminución puede causar una trampa de CPU con un flujo inferior para uno.
Vality
1
No estoy de acuerdo con que eso sea "afortunado". Creo que sería mucho mejor si los compiladores emitieran código que se bloqueaba inmediatamente cada vez que invocaba un comportamiento indefinido.
David Conrad
44
@DavidConrad: Entonces C no es el lenguaje para ti. Gran parte del comportamiento indefinido en C no se puede detectar fácilmente o solo con un golpe de rendimiento severo.
Bart van Ingen Schenau
Estaba pensando en agregar "con un conmutador de compilador". Obviamente no querrás eso para un código optimizado. Pero tienes razón, y es por eso que dejé de escribir C hace diez años.
David Conrad
@BartvanIngenSchenau, dependiendo de lo que quiere decir con 'golpe de rendimiento severo', existe una ejecución simbólica para C (por ejemplo, clang + klee), así como desinfectantes (asan, tsan, ubsan, valgrind, etc.) que tienden a ser muy útiles para la depuración.
Maciej Piechotka
10

Oficialmente, es un comportamiento indefinido tener un punto de puntero fuera de la matriz (excepto uno pasado el final), incluso si nunca se desreferencia .

En la práctica, si su procesador tiene un modelo de memoria plana (a diferencia de los extraños como x86-16 ), y si el compilador no le da un error de tiempo de ejecución u optimización incorrecta si crea un puntero no válido, entonces el código funcionará muy bien

dan04
fuente
1
Eso tiene sentido. Desafortunadamente, son dos demasiados para mi gusto.
wolfPack88
3
El último punto es, en mi humilde opinión, el más problemático. Como los compiladores en estos tiempos no solo dejan que suceda lo que la plataforma hace "naturalmente" en el caso de UB, sino que los optimizadores lo explotan agresivamente , no jugaría con él tan a la ligera.
Matteo Italia
3

Primero, es un comportamiento indefinido. Algunos compiladores optimizadores hoy en día se vuelven muy agresivos con el comportamiento indefinido. Por ejemplo, dado que a-- en este caso es un comportamiento indefinido, el compilador podría decidir guardar una instrucción y un ciclo de procesador y no disminuir a. Lo cual es oficialmente correcto y legal.

Ignorando eso, puede restar 1, 2 o 1980. Por ejemplo, si tengo datos financieros para los años 1980 a 2013, podría restar 1980. Ahora, si tomamos float * a = malloc (tamaño); seguramente hay una gran constante k tal que a - k es un puntero nulo. En ese caso, realmente esperamos que algo salga mal.

Ahora tome una estructura grande, digamos un megabyte de tamaño. Asigne un puntero p que apunte a dos estructuras. p - 1 puede ser un puntero nulo. p - 1 podría ajustarse (si una estructura es un megabyte, y el bloque malloc es de 900 KB desde el inicio del espacio de direcciones). Por lo tanto, podría ser sin ninguna malicia del compilador que p - 1> p. Las cosas pueden ponerse interesantes.

gnasher729
fuente
1

... simplemente disminuir un puntero fuera del rango asignado me parece muy incompleto. ¿Es este comportamiento "permitido" en C?

¿Permitido? Si. ¿Buena idea? No Usualmente.

C es una abreviatura de lenguaje ensamblador, y en lenguaje ensamblador no hay punteros, solo direcciones de memoria. Los punteros de C son direcciones de memoria que tienen un comportamiento secundario de aumento o disminución por el tamaño de lo que apuntan cuando se someten a aritmética. Esto hace que lo siguiente esté bien desde una perspectiva de sintaxis:

double *p = (double *)0xdeadbeef;
--p;  // p == 0xdeadbee7, assuming sizeof(double) == 8.
double d = p[0];

Las matrices no son realmente una cosa en C; son solo punteros a rangos contiguos de memoria que se comportan como matrices. El []operador es una abreviatura para hacer aritmética de puntero y desreferenciar, por lo que en a[x]realidad significa *(a + x).

Hay razones válidas para hacer lo anterior, como algunos dispositivos de E / S que tienen un par de doubles asignados a 0xdeadbee7y 0xdeadbeef. Muy pocos programas tendrían que hacer eso.

Cuando crea la dirección de algo, como mediante el uso del &operador o la llamada malloc(), desea mantener intacto el puntero original para que sepa que lo que apunta en realidad es algo válido. Disminuir el puntero significa que un poco de código errante podría intentar desreferenciarlo, obteniendo resultados erróneos, golpeando algo o, dependiendo de su entorno, cometiendo una violación de segmentación. Esto es especialmente cierto con malloc(), porque usted ha puesto la carga sobre quien llama free()para recordar pasar el valor original y no alguna versión alterada que hará que se suelte todo.

Si necesita matrices basadas en 1 en C, puede hacerlo de forma segura a expensas de asignar un elemento adicional que nunca se utilizará:

double *array_create(size_t size) {
    // Wasting one element, so don't allow it to be full-sized
    assert(size < SIZE_MAX);
    return malloc((size+1) * sizeof(double));
}

inline double array_index(double *array, size_t index) {
    assert(array != NULL);
    assert(index >= 1);  // This is a 1-based array
    return array[index];
}

Tenga en cuenta que esto no hace nada para proteger contra exceder el límite superior, pero eso es lo suficientemente fácil de manejar.


Apéndice:

Algunos capítulos y versículos del borrador del C99 (lo siento, es todo lo que puedo vincular):

§6.5.2.1.1 dice que la segunda expresión ("otra") utilizada con el operador de subíndice es de tipo entero. -1es un número entero, y eso lo hace p[-1]válido y, por lo tanto, también hace que el puntero sea &(p[-1])válido. Esto no implica que acceder a la memoria en esa ubicación produzca un comportamiento definido, pero el puntero sigue siendo un puntero válido.

§6.5.2.2 dice que el operador de subíndice de matriz evalúa el equivalente de agregar el número de elemento al puntero, por p[-1]lo tanto, es equivalente a *(p + (-1)). Sigue siendo válido, pero puede no producir un comportamiento deseable.

§6.5.6.8 dice (énfasis mío):

Cuando una expresión que tiene un tipo entero se agrega o resta de un puntero, el resultado tiene el tipo del operando del puntero.

... si la expresión Papunta al ielemento -th de un objeto de matriz, las expresiones (P)+N(equivalentemente N+(P)) y (P)-N (donde Ntiene el valor n) apuntan, respectivamente, a los elementos i+n-th y i−n-th del objeto de matriz, siempre que existan .

Esto significa que los resultados de la aritmética del puntero tienen que apuntar a un elemento en una matriz. No dice que la aritmética debe hacerse de una vez. Por lo tanto:

double a[20];

// This points to element 9 of a; behavior is defined.
double d = a[-1 + 10];

double *p = a - 1;  // This is just a pointer.  No dereferencing.

double e = p[0];   // Does not point at any element of a; behavior is undefined.
double f = p[1];   // Points at element 0 of a; behavior is defined.

¿Recomiendo hacer las cosas de esta manera? No lo hago, y mi respuesta explica por qué.

Blrfl
fuente
8
-1 Una definición de 'permitido' que incluye el código que el estándar C declara como generar resultados indefinidos no es útil.
Pete Kirkham
Otros han señalado que es un comportamiento indefinido, por lo que no debe decir que está "permitido". Sin embargo, la sugerencia de asignar un elemento adicional no utilizado 0 es buena.
200_success
Esto realmente no está bien, al menos tenga en cuenta que esto está prohibido por el estándar C.
Vality
@PeteKirkham: No estoy de acuerdo. Vea el apéndice de mi respuesta.
Blrfl
44
@Blrfl 6.5.6 del estándar ISO C11 establece en el caso de agregar un número entero a un puntero: "Si tanto el operando del puntero como el resultado apuntan a elementos del mismo objeto de matriz, o uno pasado el último elemento del objeto de matriz , la evaluación no producirá un desbordamiento; de lo contrario, el comportamiento no está definido ".
Vality