¿Cómo se implementan realmente los punteros?

Eso depende mucho de la arquitectura subyacente de la máquina. Sin embargo, en general, la mayoría de las computadoras usan “números enteros” de algún tamaño (posiblemente con algunos bits especiales establecidos) como direcciones.

En los viejos tiempos, esos enteros no eran necesariamente del mismo tamaño que los enteros “normales”, a veces más grandes (las computadoras Prime usaban enteros de 48 bits como punteros, pero varios de los bits eran especiales y enteros de 32 bits normalmente) y a veces más pequeños (punteros en un CDC 6600 tenía 18 bits y los enteros normales casi 60 bits, solo eran “números de coma flotante” con un exponente especial). Había toda una serie de máquinas de Digital Equipment (y otros proveedores) donde dos enteros de tamaño de puntero cabían en un entero normal. Probablemente no quiera saber cómo se implementaron los punteros en el Data General Nova II, que tenía diferentes punteros de palabras y caracteres y que hizo que implementar el intérprete lisp dentro de RNL (una herramienta para diseñar lógica alguna vez) fuera un desafío: Mis primeros trabajos de contratación.

Con el dominio de C y “todas las máquinas son un Vax”, se usaron enteros de 32 bits como enteros normales y direcciones en la memoria. La mayoría de las arquitecturas modernas, x86, Mips, ARM, etc. Sigue alguna variación sobre eso. Si te fijas lo suficiente, quedan algunos si se usan enteros de 16 bits y punteros. Más frecuentes son las versiones donde se usan enteros y punteros de 64 bits.

Ok, ya que quieres detalles. Aquí está el siguiente nivel. Para acceder a la memoria, uno tiene que saber la dirección específica de esa memoria. Érase una vez, la mayoría de las direcciones estaban codificadas en las instrucciones, por ejemplo, cargar el acumulador desde la dirección 5 (codificada como: lda 5). Sin embargo, para permitir matrices, los diseñadores de computadoras inventaron “registros de índice”, cargan el registro de índice con el subíndice en la ubicación 2 (ldx 2) y luego cargan el acumulador desde la dirección 5 suscrita por el índice (lda 5, x). Esto se hizo más avanzado al permitir el direccionamiento indirecto, cargar el acumulador con datos direccionados por la memoria en la dirección 5 (lda * 5). Ahora la ubicación 5 es un puntero. Sin embargo, si observa detenidamente, la operación de subíndice también es esencialmente un puntero. La sintaxis de C incluso hace eco de esto donde 5 [a] y a [5] significan lo mismo.

Ahora, las secuencias de instrucciones exactas utilizadas y la cantidad de valores permitidos dependen de la arquitectura, pero la mayoría de las máquinas ahora permiten el direccionamiento con direccionamiento indirecto de 32 bits (o 64 bits) y / o con registros de índice del mismo tamaño.

No conozco ninguna máquina actual que use espacios de direcciones separados para punteros. En un momento creo que hubo algunos, aunque nunca he usado uno. La semántica de C, que casi todos tienen que apoyar, hace que esta sea una propuesta perdedora. Los punteros son simplemente ubicaciones de memoria que contienen enteros que corresponden a las direcciones a las que queremos acceder. Esto no es un accidente: el compilador, el enlazador, el cargador, la biblioteca de tiempo de ejecución y el sistema operativo lo hacen así, pero no hay demasiado especial sobre esas ubicaciones.

La razón por la cual las máquinas como Primes y CDC desaparecieron es que los formatos de puntero especiales que no son simplemente enteros “normales” no son prácticos para implementar C de manera eficiente, y hacerlo es la métrica base por la cual se miden la mayoría de las computadoras modernas. Incluso la computadora en su teléfono inteligente cumple con ese estándar, porque eso hace que Android sea eficiente.

Ahora, ¿hay algo especial en las máquinas actuales que influyen en los enteros utilizados como punteros? Sí. Algún día pregunte sobre “tablas de páginas” y TLB o quizás sobre accesos no alineados y líneas de caché.

La memoria de una computadora es una larga secuencia de bits, que se puede considerar de manera equivalente como una larga secuencia de enteros no negativos. La memoria está compuesta solo por estos enteros, nada más. Por lo tanto, debemos hacer un seguimiento muy cuidadoso de cómo interpretar cada dirección de memoria. Después de todo, debemos usar enteros no negativos para almacenar: texto, números de punto flotante, enteros con signo, etc.

Por ejemplo, tome el número entero 65. ¡Este número en sí mismo no tiene sentido, a menos que sepamos cómo interpretarlo! Por ejemplo, podríamos interpretarlo como el entero 65. O podríamos interpretarlo como el carácter ASCII ‘A’. O incluso podríamos interpretarlo como una dirección de memoria, 65 bytes desde el comienzo de la memoria direccionable de la computadora. Afortunadamente, los lenguajes de programación como C hacen un seguimiento de cómo debe interpretarse la memoria para nosotros, a través de los tipos de datos.

Para ilustrar, echemos un vistazo a los tipos de datos “unsigned int” y “igned int “.

Supongamos que el entero 65 se almacena en la memoria: “unsigned int age = 65;”. Para demostrar que `age` es solo una etiqueta para una dirección de memoria, podemos escribir` & age`, que evalúa la dirección de memoria a la que se refiere nuestra etiqueta `age`. Por defecto, cuando escribimos `age` (de tipo” int “), estamos interpretando el valor en la dirección de memoria` & age` como un eger “int”.

Ahora, supongamos que definimos: “igned int daysago = -65; “. En nuestra memoria, solo podemos almacenar enteros no negativos. ¡Entonces, el compilador de C usará algún método para almacenar enteros negativos como enteros no negativos! De hecho, podemos ver el número entero no negativo exacto convirtiendo el “firmado int” en un “unsigned int”: `(unsigned int) daysago`. Este “reparto” simplemente interpreta el valor almacenado en la dirección `& daysago` como un int sin signo, en lugar del int predeterminado firmado. (Usando GNU GCC, el int -65 firmado se almacena como el entero no negativo 25439484.)

Entonces, los tipos de datos nos recuerdan cómo interpretar el contenido de las direcciones de memoria. ¿Qué pasa si el contenido de una dirección de memoria es en sí misma una dirección de memoria? Por ejemplo, 65 podría referirse a 65 bytes desde el comienzo de nuestra memoria direccionable. O bien, podríamos almacenar la dirección de nuestra variable anterior, `& daysago`.

Esto se llama puntero. “int * ptr = & age;” almacena la dirección de memoria de `age` en un lugar al que hace referencia la etiqueta` ptr`. Tenga en cuenta que esto funciona exactamente de la misma manera que cualquier otro tipo de datos discutido anteriormente. ¡`ptr` interpreta la dirección de memoria a la que se refiere como una dirección de memoria! Y, `& ptr` es la dirección de memoria a la que se refiere la etiqueta` ptr`.

Con los punteros, ahora podemos usar un nuevo operador unario: el operador de desreferencia *. Con esto, `* ptr` es el contenido de la dirección de memoria almacenada en` ptr`. En este caso, “& age” se almacena en `ptr`, por lo que` * ptr` recupera el contenido de `& age`, y los interpreta como un” int “(porque nuestro tipo de datos de puntero era” int * “).

Entonces, ¿cómo se implementan los punteros? Al igual que cualquier otro tipo de datos. Cada tipo de datos define una etiqueta que se refiere a una dirección de memoria particular. Esa dirección de memoria se puede interpretar como un entero sin signo, un entero con signo o incluso otra dirección de memoria. El último caso es un “puntero”.

¿Cómo funcionan los punteros? En casi todos los casos, las CPU tienen soporte directo para acceder directamente a la memoria a través de direcciones. Una simple instrucción “MOV” de CPU normalmente puede recuperar y almacenar valores directamente desde / a direcciones de memoria particulares. Entonces, una desreferencia de puntero se traduce en una instrucción MOV, que mueve un valor desde la dirección de memoria a un registro o variable.

Los punteros no tienen diferencias con otros valores / variables, solo son enteros, que representan las direcciones físicas en la memoria total. No residen físicamente en una región diferente de la memoria que los otros datos. Aunque restablecerlos a cero es un buen hábito, no necesita desreferenciarlos.