Punteros en C: Conceptos Esenciales y Operaciones Básicas

El lenguaje C, conocido por su cercanía al hardware, utiliza un concepto fundamental que permite una manipulación de memoria de bajo nivel: los punteros. Entender su funcionamiento es crucial para escribir código eficiente y potente en C.

¿Qué son los Punteros?

Antes de sumergirnos en los punteros, es vital comprender cómo se organiza la memoria de un sistema. Cuando el CPU procesa datos, los lee de la memoria y luego guarda los resultados. La memoria se gestiona dividiéndola en unidades de almacenamiento, donde cada unidad ocupa 1 byte. Un byte, a su vez, se compone de 8 bits. A continuación, se muestra una tabla de equivalencias de unidades de memoria:

Unidad Equivalencia
1 Byte 8 bits
1 KB 1024 Bytes
1 MB 1024 KB
1 GB 1024 MB
1 TB 1024 GB
1 PB 1024 TB

Cada una de estas unidades de memoria posee una identificación única, similar a una dirección postal. Para que el CPU pueda acceder a un dato específico en la memoria, necesita conocer esta dirección. En C, a esta dirección de memoria la conocemos como puntero. Por lo tanto, podemos afirmar que una dirección de memoria es un puntero.

La comunicación entre el CPU y la memoria se realiza a través de varios conjuntos de "líneas" o buses: el bus de direcciones, el bus de datos y el bus de control.

  • El bus de direcciones es el encargado de transportar la ubicación de memoria que el CPU desea acceder. Por ejemplo, en un sistema de 32 bits, existen 32 líneas de dirección. Cada línea puede estar en uno de dos estados (0 o 1). Esto permite al sistema referenciar 232 direcciones de memoria únicas.
  • Una vez que la dirección se transmite y la ubicación es identificada en la memoria, el bus de datos se utiliza para transferir el contenido de esa dirección (el dato) hacia los registros internos del CPU.

Cómo Obtener Direcciones de Memoria

El Operador de Dirección (&)

Cuando declaramos una variable en C, el compilador asigna un espacio en la memoria para almacenar su valor. Por ejemplo, una variable de tipo int generalmente ocupa 4 bytes. Si tenemos una variable miNumero, podría ocupar los bytes en las direcciones 0x006FFD70, 0x006FFD71, 0x006FFD72 y 0x006FFD73 (en un sistema con little-endian, el valor real se almacena comenzando por la dirección más baja).

Para obtener la dirección de memoria donde reside una variable, utilizamos el operador de dirección &. Este operador devuelve la dirección del primer byte de la variable.

#include <stdio.h>

int main() {
    int valorEntero = 10;
    // El operador & devuelve la dirección de memoria de valorEntero
    printf("La dirección de memoria de 'valorEntero' es: %p\n", (void *)&valorEntero);
    return 0;
}

La salida mostrará una dirección de memoria en formato hexadecimal, por ejemplo, 0x7ffee0b7c6bc.

Variables Puntero y el Operador de Desreferencia (*)

Variables Puntero

Las direcciones de memoria, una vez obtenidas con &, son valores que a menudo necesitamos almacenar para su uso posterior. Para esto, C proporciona las variables puntero, que están diseñadas específicamente para contener direcciones de memoria.

#include <stdio.h>

int main() {
    float temperatura = 25.5f;
    // 'pTemperatura' es una variable puntero que almacena la dirección de 'temperatura'
    float *pTemperatura = &temperatura;

    printf("Valor de temperatura: %.1f\n", temperatura);
    printf("Dirección de temperatura (almacenada en pTemperatura): %p\n", (void *)pTemperatura);
    return 0;
}

En este ejemplo, pTemperatura es una variable puntero que ahora contiene la dirección de memoria de temperatura.

Anatomía de una Declaración de Puntero

Considerando la declaración float *pTemperatura = &temperatura;:

  • El asterisco (*) junto al nombre de la variable (*pTemperatura) indica que pTemperatura es una variable puntero.
  • El tipo de dato que precede al asterisco (float) especifica el tipo de dato de la variable a la que el puntero apunta. Es decir, pTemperatura apunta a una variable de tipo float.
El Operador de Desreferencia (*)

Una vez que tenemos una dirección de memoria almacenada en una variable puntero, podemos acceder o modificar el valor de la ubicación de memoria a la que apunta. Para ello, utilizamos el operador de desreferencia *.

#include <stdio.h>

int main() {
    int cantidad = 500;
    int *pCantidad = &cantidad; // pCantidad almacena la dirección de 'cantidad'

    printf("Valor inicial de 'cantidad': %d\n", cantidad); // Muestra 500

    // Usamos *pCantidad para acceder al valor en la dirección apuntada
    // y lo modificamos a 125
    *pCantidad = 125;

    printf("Valor de 'cantidad' después de la desreferencia: %d\n", cantidad); // Muestra 125
    return 0;
}

Al hacer *pCantidad = 125;, estamos accediendo a la ubicación de memoria a la que pCantidad apunta (que es la variable cantidad) y asignándole el nuevo valor de 125.

Tamaño de las Variables Puntero

El tamaño que ocupa una variable puntero en memoria es fijo para una arquietctura de sistema dada, independientemente del tipo de dato al que apunte.

  • En sistemas de 32 bits, los punteros tienen un tamaño de 4 bytes.
  • En sistemas de 64 bits, los punteros tienen un tamaño de 8 bytes.

Esto significa que un int*, un char* o un double* ocuparán el mismo espacio en memoria (4 u 8 bytes, dependiendo de la arquitectura), ya que todos ellos almacenan una dirección de memoria.

Significado del Tipo de Puntero

A pesar de que todas las variables puntero tienen el mismo tamaño, su tipo (int*, char*, etc.) es de vital importancia y define dos aspectos clave de su comportamiento.

Permisos de Desreferencia

El tipo de un puntero determina cuántos bytes puede "ver" y modificar el operador de desreferencia (*) a la vez.

#include <stdio.h>

int main() {
    unsigned int dato = 0x12345678; // Un valor de 4 bytes
    printf("Valor inicial de 'dato': 0x%X\n", dato);

    // Caso 1: Puntero de tipo int (modifica 4 bytes)
    int *pEntero = &dato;
    *pEntero = 0x0; // Establece los 4 bytes a cero
    printf("Después de *pEntero = 0x0: 0x%X\n", dato); // Imprime 0x0

    // Restablecemos el valor para la siguiente prueba
    dato = 0x12345678;
    printf("Valor restablecido de 'dato': 0x%X\n", dato);

    // Caso 2: Puntero de tipo char (modifica 1 byte)
    char *pByte = (char *)&dato; // Se debe hacer un 'cast' explícito
    *pByte = 0x0; // Establece solo el byte menos significativo a cero (asumiendo little-endian)
    printf("Después de *pByte = 0x0: 0x%X\n", dato); // Imprime 0x12345600
    return 0;
}

En el primer caso, *pEntero = 0x0; modifica los 4 bytes de dato a cero. En el segundo caso, *pByte = 0x0;, debido a que pByte es un puntero a char (1 byte), solo modifica el primer byte de dato, dejando los otros bytes inalterados (asumiendo una arquitectura little-endian, donde el byte menos significativo se almacena en la dirección más baja).

Por lo tanto, el tipo de puntero decide la cantidad de bytes sobre los que el operador de desreferencia tiene permiso para operar.

Aritmética de Punteros (+- Enteros)

El tipo de puntero también determina el "paso" o la "distancia" que avanza o retrocede un puntero cuando se le suma o resta un entero.

#include <stdio.h>

int main() {
    int unNumero = 100;
    printf("Dirección de 'unNumero': %p\n", (void *)&unNumero);

    // Puntero de tipo char*
    char *pChar = (char *)&unNumero;
    printf("Dirección inicial (pChar): %p\n", (void *)pChar);
    printf("Dirección pChar + 1 (avanza 1 byte): %p\n", (void *)(pChar + 1));
    printf("Dirección pChar + 2 (avanza 2 bytes): %p\n", (void *)(pChar + 2));

    printf("\n"); // Separador

    // Puntero de tipo int*
    int *pInt = &unNumero;
    printf("Dirección inicial (pInt): %p\n", (void *)pInt);
    printf("Dirección pInt + 1 (avanza 4 bytes): %p\n", (void *)(pInt + 1));
    printf("Dirección pInt + 2 (avanza 8 bytes): %p\n", (void *)(pInt + 2));

    return 0;
}

Observando la salida de este código, veremos que cuando un puntero char* se incrementa en 1 (pChar + 1), su dirección aumenta en 1 byte. Sin embargo, cuando un puntero int* se incrementa en 1 (pInt + 1), su dirección aumenta en 4 bytes (el tamaño de un int).

En resumen, el tipo de puntero determina la magnitud del salto cuando se realiza una operación aritmética con enteros.

Etiquetas: C Punteros memoria direcciones desreferencia

Publicado el 8-17 21:34