Los punteros en C son más abstractos de lo que piensas

El puntero se refiere a una celda de memoria, y desreferenciar un puntero significa leer el valor de la celda especificada. El valor del puntero en sí es la dirección de la celda de memoria. El estándar del lenguaje C no especifica la forma para representar las direcciones de memoria. Este es un punto muy importante, ya que diferentes arquitecturas pueden usar diferentes modelos de direccionamiento. La mayoría de las arquitecturas modernas usan un espacio de direcciones lineal o similar. Sin embargo, incluso esta pregunta no se especifica estrictamente, ya que las direcciones pueden ser físicas o virtuales. Algunas arquitecturas utilizan una representación no numérica en absoluto. Entonces, Symbolics Lisp Machine opera con tuplas de la forma (objeto, desplazamiento) como direcciones.
Algún tiempo después, después de la publicación de la traducción en Habré, el autor realizó grandes modificaciones al texto del artículo. Actualizar una traducción en Habré no es una buena idea, ya que algunos comentarios perderán su significado o se verán fuera de lugar. No quiero publicar el texto como un nuevo artículo. Por lo tanto, acabamos de actualizar la traducción del artículo en viva64.com, y aquí dejamos todo tal como está. Si es un lector nuevo, le sugiero que lea una traducción más reciente en nuestro sitio haciendo clic en el enlace de arriba.

El estándar no estipula la forma de presentación de los punteros, sino que estipula, en mayor o menor medida, las operaciones con ellos. A continuación consideramos estas operaciones y las características de su definición en el estándar. Comencemos con el siguiente ejemplo:

#include <stdio.h> int main(void) { int a, b; int *p = &a; int *q = &b + 1; printf("%p %p %d\n", (void *)p, (void *)q, p == q); return 0; } 

Si compilamos este código GCC con el nivel de optimización 1 y ejecutamos el programa en Linux x86-64, imprimirá lo siguiente:

 0x7fff4a35b19c 0x7fff4a35b19c 0 

Tenga en cuenta que los punteros pyq se refieren a la misma dirección. Sin embargo, el resultado de la expresión p == q es falso , y esto a primera vista parece extraño. ¿No deberían ser iguales dos punteros a la misma dirección?

Así es como el estándar C define el resultado de verificar la igualdad de dos punteros:
C11 § 6.5.9 párrafo 6

Dos punteros son iguales si y solo si ambos son cero, ya sea que apunten al mismo objeto (incluido un puntero al objeto y el primer subobjeto en el objeto) o una función, o apunten a la posición después del último elemento de la matriz, o un puntero se refiere a la posición después del último elemento de la matriz, y la otra se refiere al comienzo de otra matriz inmediatamente después de la primera en el mismo espacio de direcciones.

En primer lugar, surge la pregunta: ¿qué es un "objeto " ? Como estamos hablando del lenguaje C, es obvio que aquí los objetos no tienen nada que ver con objetos en lenguajes OOP como C ++. En el estándar C, este concepto no está completamente definido:
C11 § 3.15

Un objeto es un área de almacenamiento en tiempo de ejecución cuyo contenido se puede usar para representar valores

NOTA Cuando se menciona, se puede considerar que un objeto tiene un tipo específico; ver 6.3.2.1.

Vamos a hacerlo bien. Una variable entera de 16 bits es un conjunto de datos en la memoria que puede representar valores enteros de 16 bits. Por lo tanto, dicha variable es un objeto. ¿Serán iguales dos punteros si uno de ellos se refiere al primer byte de un entero dado y el segundo al segundo byte del mismo número? El comité de estandarización del idioma, por supuesto, no quiso decir esto en absoluto. Pero aquí debe tenerse en cuenta que a este respecto no tiene explicaciones claras, y nos vemos obligados a adivinar lo que realmente significaba.

Cuando el compilador se interpone


Volvamos a nuestro primer ejemplo. El puntero p se obtiene del objeto a , y el puntero q es del objeto b . En el segundo caso, se usa la aritmética de direcciones, que se define para los operadores más y menos de la siguiente manera:
C11 § 6.5.6 cláusula 7

Cuando se usa con estos operadores, un puntero a un objeto que no es un elemento de la matriz se comporta como un puntero al comienzo de una matriz con una longitud de un elemento, cuyo tipo corresponde al tipo del objeto original.

Dado que cualquier puntero a un objeto que no sea una matriz en realidad se convierte en un puntero a una matriz con una longitud de un elemento, el estándar define la aritmética de direcciones solo para punteros a matrices; este es el punto 8. Nos interesa la siguiente parte:
C11 § 6.5.6 cláusula 8

Si se agrega o resta una expresión entera del puntero, el puntero resultante es del mismo tipo que el puntero original. Si el puntero fuente se refiere a un elemento de matriz y la matriz tiene una longitud suficiente, entonces la fuente y los elementos resultantes se separan entre sí para que la diferencia entre sus índices sea igual al valor de la expresión entera. En otras palabras, si la expresión P apunta al elemento i-ésimo de la matriz, las expresiones (P) + N (o su equivalente N + (P) ) y (P) -N (donde N tiene el valor n) indican respectivamente (i + n) th y (i - n) th elementos de la matriz, siempre que existan. Además, si la expresión P apunta al último elemento de la matriz, entonces la expresión (P) +1 indica la posición después del último elemento de la matriz, y si la expresión Q indica la posición después del último elemento de la matriz, entonces la expresión (Q) -1 indica el último elemento matriz. Si tanto los punteros fuente como los punteros resultantes se refieren a elementos de la misma matriz o a la posición después del último elemento de la matriz, se excluye el desbordamiento; de lo contrario, el comportamiento es indefinido. Si el puntero resultante se refiere a la posición después del último elemento de la matriz, el operador unario * no se puede aplicar a él.

De ello se deduce que el resultado de la expresión & b + 1 definitivamente debe ser una dirección, y por lo tanto pyq son punteros válidos. Permítame recordarle cómo se define la igualdad de dos punteros en el estándar: " Dos punteros son iguales si y solo [...] un puntero se refiere a la posición después del último elemento del conjunto, y el otro al comienzo de otro conjunto inmediatamente después del primero en el mismo espacio de direcciones " (C11 § 6.5.9 cláusula 6). Esto es exactamente lo que observamos en nuestro ejemplo. El puntero q se refiere a la posición después del objeto b, seguido inmediatamente por el objeto a, al que se refiere el puntero p. Entonces, ¿hay un error en GCC? Esta contradicción se describió en 2014 como el error n. ° 61502 , pero los desarrolladores de GCC no lo consideran un error y, por lo tanto, no lo solucionarán.

Un problema similar fue encontrado en 2016 por los programadores de Linux. Considere el siguiente código:

 extern int _start[]; extern int _end[]; void foo(void) { for (int *i = _start; i != _end; ++i) { /* ... */ } } 

Los símbolos _start y _end especifican los límites del área de memoria. Como se transfieren a un archivo externo, el compilador no sabe cómo se ubican realmente las matrices en la memoria. Por esta razón, debe tener cuidado aquí y proceder de la suposición de que se siguen en el espacio de direcciones. Sin embargo, GCC compila la condición del bucle para que siempre sea verdadera, lo que hace que el bucle sea infinito. Este problema se describe aquí en esta publicación en LKML : allí se usa un fragmento de código similar. Parece que en este caso, los autores de GCC sin embargo tomaron en cuenta los comentarios y cambiaron el comportamiento del compilador. Al menos no pude reproducir este error en GCC versión 7.3.1 bajo Linux x86_64.

¿Solución - en el informe de error # 260?


Nuestro caso puede aclarar el informe de error # 260 . Se trata más de valores inciertos, pero puede encontrar un curioso comentario del comité en él:

Las implementaciones del compilador [...] también pueden distinguir los punteros obtenidos de diferentes objetos, incluso si estos punteros tienen el mismo conjunto de bits.

Si tomamos este comentario literalmente, entonces es lógico que el resultado de la expresión p == q sea ​​"falso", ya que pyq se obtienen de diferentes objetos que no están conectados de ninguna manera. Parece que nos estamos acercando a la verdad, ¿o no? Hasta ahora, hemos tratado con operadores de igualdad, pero ¿qué pasa con los operadores de relación?

La pista final es en relación a los operadores?


La definición de los operadores de relación < , <= , > y > = en el contexto de las comparaciones de punteros contiene un pensamiento curioso:
C11 § 6.5.8 párrafo 5

El resultado de comparar dos punteros depende de la posición relativa de los objetos indicados en el espacio de direcciones. Si dos punteros a tipos de objeto se refieren al mismo objeto, o ambos se refieren a la posición después del último elemento de la misma matriz, entonces dichos punteros son iguales. Si los objetos indicados son miembros del mismo objeto compuesto, los punteros a los miembros de la estructura declarada más tarde son más que punteros a los miembros declarados anteriormente, y los punteros a elementos de una matriz con índices más altos son más que punteros a elementos de la misma matriz con índices más bajos. Todos los punteros a los miembros de la misma asociación son iguales. Si la expresión P apunta a un elemento de la matriz y la expresión Q apunta al último elemento de la misma matriz, entonces el valor de la expresión de puntero Q + 1 es mayor que el valor de la expresión P. En todos los demás casos, el comportamiento no está definido.

Según esta definición, el resultado de comparar punteros se determina solo si los punteros se obtienen del mismo objeto. Mostramos esto con dos ejemplos.

 int *p = malloc(64 * sizeof(int)); int *q = malloc(64 * sizeof(int)); if (p < q) //   foo(); 

Aquí, los punteros p y q se refieren a dos objetos diferentes que no están interconectados. Por lo tanto, el resultado de su comparación no está definido. Pero en el siguiente ejemplo:

 int *p = malloc(64 * sizeof(int)); int *q = p + 42; if (p < q) foo(); 

Los punteros pyq se refieren al mismo objeto y, por lo tanto, están interconectados. Por lo tanto, se pueden comparar, a menos que malloc devuelva un valor nulo.

Resumen


El estándar C11 no describe adecuadamente las comparaciones de punteros. El punto más problemático que encontramos fue el párrafo 6 § 6.5.9, donde se permite explícitamente comparar dos punteros que hacen referencia a dos matrices diferentes. Esto contradice el comentario del informe de error # 260. Sin embargo, allí estamos hablando de significados indefinidos, y no quisiera construir mi razonamiento sobre la base de este comentario solo e interpretarlo en otro contexto. Al comparar punteros, los operadores de relación se definen de manera ligeramente diferente que los operadores de igualdad, es decir, los operadores de relación se definen solo si ambos punteros se obtienen del mismo objeto.

Si ignoramos el texto del estándar y preguntamos si es posible comparar dos punteros obtenidos de dos objetos diferentes, entonces, en cualquier caso, la respuesta probablemente sea "no". El ejemplo al comienzo del artículo demuestra un problema teórico. Como las variables ayb tienen duraciones de almacenamiento automáticas, nuestras suposiciones sobre su ubicación en la memoria no serán confiables. En algunos casos, podemos adivinar, pero es obvio que dicho código no se puede portar de forma segura, y puede descubrir el significado del programa solo compilando, ejecutando o desensamblando el código, y esto contradice cualquier paradigma de programación serio.

Sin embargo, en general, no estoy satisfecho con la redacción del estándar C11, y dado que varias personas ya se han encontrado con este problema, la pregunta sigue siendo: ¿por qué no formular las reglas más claramente?

Además
Punteros a la posición después del último elemento de la matriz.


En cuanto a la regla sobre comparación y direccionamiento aritmético de punteros a la posición después del último elemento de la matriz, a menudo puede encontrar excepciones. Suponga que el estándar no permitiría comparar dos punteros obtenidos de la misma matriz, aunque al menos uno de ellos se refiera a la posición más allá del final de la matriz. Entonces el siguiente código no funcionaría:

 const int num = 64; int x[num]; for (int *i = x; i < &x[num]; ++i) { /* ... */ } 

Usando un bucle, recorremos toda la matriz x , que consta de 64 elementos, es decir El cuerpo del bucle debe ejecutarse exactamente 64 veces. Pero, de hecho, la condición se verifica 65 veces, una vez más que el número de elementos en la matriz. En las primeras 64 iteraciones, el puntero i siempre se refiere al interior de la matriz x , mientras que la expresión & x [num] siempre indica la posición después del último elemento de la matriz. En la 65ª iteración, el puntero i también se referirá a la posición más allá del final de la matriz x , por lo que la condición del bucle se vuelve falsa. Esta es una forma conveniente de omitir toda la matriz, y se basa en una excepción a la regla de incertidumbre en el comportamiento al comparar dichos punteros. Tenga en cuenta que el estándar solo describe el comportamiento al comparar punteros; la desreferenciación es un tema aparte.

¿Es posible cambiar nuestro ejemplo para que ningún puntero se refiera a la posición después del último elemento de la matriz x ? Es posible, pero será más difícil. Tendremos que cambiar la condición del bucle y prohibir el incremento de la variable i en la última iteración.

 const int num = 64; int x[num]; for (int *i = x; i <= &x[num-1]; ++i) { /* ... */ if (i == &x[num-1]) break; } 

Este código está lleno de sutilezas técnicas, lo que hace que se distraiga de la tarea principal. Además, apareció una rama adicional en el cuerpo del bucle. Por lo tanto, considero razonable que el estándar permita excepciones al comparar punteros de posición después del último elemento de una matriz.

Nota del equipo PVS-Studio

Al desarrollar el analizador de código PVS-Studio, a veces tenemos que lidiar con problemas sutiles para que los diagnósticos sean más precisos o para brindar consultas detalladas a nuestros clientes. Este artículo nos pareció interesante, ya que toca temas en los que nosotros mismos no nos sentimos completamente seguros. Por lo tanto, le pedimos a la autora que publicara su traducción. Esperamos que más programadores de C y C ++ la conozcan y comprendan que no es tan simple y que cuando el analizador muestra de repente un mensaje extraño, no debe apresurarse a considerarlo como un falso positivo :).

El artículo se publicó por primera vez en inglés en stefansf.de. Las traducciones se publican con permiso del autor.

Source: https://habr.com/ru/post/es418023/


All Articles