Punteros

Un puntero es una variable que almacena una dirección de memoria. A través de este, se puede acceder o modificar el valor almacenado allí.

Ubicación en memoria

Conceptos básicos de punteros

Uso:

¿Cómo se declara?:

Sintaxis:

tipo_de_dato *nombre_del_puntero;

Ejemplo:

int *pi;

int x = 10;
int *pi;
pi = &x; // Direccion de memoria de x, pi ahora guarda esa direccion.

Acceder al valor usando un puntero (desreferenciar - object-of):

Especificador de formato para direcciones:

Teoría aplicada Ejemplo 1

Makefile:


CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -pedantic

PROG = declaracion
OBJ = main.o

$(PROG): $(OBJ)
	$(CC) $(OBJ) -o $(PROG)

$(OBJ): main.c
	$(CC) $(CFLAGS) -c main.c

.PHONY: clean
clean:
	del $(OBJ) $(PROG).exe

Código:


// gcc -std=c11 -Wall -Wextra -pedantic main.c -o declaracion

#include <stdio.h>

int main(void)
{
    int x = 5;
    int *pi = &x; //Declaracion y asignacion de memoria

    printf("Valor de x: %d\n", x);
    printf("Direccion de x: %p\n", &x); // El compilador espera que a la direccion se le haga un cast (void*)&x, pero lo deja pasar con solo un Warning, con esa solucion.
    printf("Direccion usando pi: %p\n", pi); // El compilador espera que a la direccion se le haga un cast (void*)&x, pero lo deja pasar con solo un Warning, con esa solucion.
    printf("Direccion de x: %p\n", (void *)&x); // Siguiendo el estandar
    printf("Direccion usando pi: %p\n", (void *)pi); //Siguiendo el estandar
    printf("Acceso al valor de x usando el puntero %d\n", *pi);
    
    return 0;
}

Tamaño de los punteros

Se utilizará el sinónimo dimensión en los comentarios para claridad del texto.

TODOS los punteros tienen la misma dimensión. Sin importar a que tipo apunten, ya que su dimensión proviene del tipo de arquitectura en 32 bits es de 4 bytes, en 64 bits es de 8 bytes, se puede verificar con sizeof()


sizeof(int *);
sizeof(float *);
sizeof(char *);
sizeof(double *);

No confundir con:


sizeof(int);

Ejemplo:


int *pi;
float *pf;
char *pc;

En una misma arquitectura, siempre será cierto que:


sizeof(pi) == sizeof(pf) == sizeof(pc)

Para imprimir sizeof(puntero a tipo de dato) usamos el especificador de formato %zu:


printf("%zu", sizeof(int *));
printf("%zu", sizeof(char *));

Van a imprimir lo mismo, que corresponde a la dimensión del puntero en esa arquitectura.

No confundir:

int x;
int *pi = &x;
printf("%zu", sizeof(pi)); // Dimension del puntero pi. Mostrara 8 bytes. En arquitectura de 64 bits
printf("%zu", sizeof(*pi)); // Dimension del tipo de dato al que apunta pi. Mostrara 4 bytes ya que sizeof(int) es 4 bytes

Relación con arrays:


int a[10]; // sizeof(a) da 10 * sizeof(int) = 10 * 4 bytes = 40 bytes
int *p = a; // sizeof(p); da sizeof(int *) = 8 bytes

Teoría aplicada Ejemplo 2

Makefile:


CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -pedantic

PROG = dimension
OBJ = main.o

$(PROG): $(OBJ)
	$(CC) $(OBJ) -o $(PROG)

$(OBJ): main.c
	$(CC) $(CFLAGS) -c main.c

.PHONY: clean
clean:
	del $(OBJ) $(PROG).exe

Código:


// gcc -std=c11 -Wall -Wextra -pedantic main.c -o dimension

#include <stdio.h>

int main(void)
{
    int x;
    char c;
    double d;

    int *pi = &x;
    char *pc = &c;
    double *pd = &d;

    printf("int *:\t%zu bytes\n", sizeof(pi)); // Dimension de puntero
    printf("int:\t%zu bytes\n", sizeof(*pi));  // Dimension de tipo de dato

    printf("char *:\t%zu bytes\n", sizeof(pc));
    printf("char:\t%zu bytes\n", sizeof(*pc));

    printf("double *:\t%zu bytes\n", sizeof(pd));
    printf("double:\t%zu bytes\n", sizeof(*pd));

    return 0;
}

Operadores de punteros

Operador & (Direccionamiento:address-of)

¿Qué hace?

Obtiene la dirección de memoria de una variable


int x;
int *pi = &x;

Se usa para:

Operador * (Desreferenciación/Indirección - object-of):

¿Qué hace?

Accede al valor almacenado en la dirección a la que apunta el puntero.


int x = 10;
int *pi = &x;
printf("%d", *pi); Muestra 10

Usar * sobre un puntero no inicializado da un comportamiento indefinido-Undefined Behaviour (UB)

Teoría aplicada Ejemplo 3

Makefile:


CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -pedantic

PROG = operadores
OBJ = main.o

$(PROG): $(OBJ)
	$(CC) $(OBJ) -o $(PROG)

$(OBJ): main.c
	$(CC) $(CFLAGS) -c main.c

.PHONY: clean
clean
	del $(OBJ) $(PROG).exe

Código:


// gcc -std=c11 -Wall -Wextra -pedantic main.c -o operadores

#include <stdio.h>

int main(void)
{
    int x = 10;
    int y = 20;

    /*
        Operador & address-of
    */
    int *p1 = &x;
    int *p2 = &y;

    printf("Valores iniciales:\n");
    printf("x = %d, y = %d\n", x, y);

    /*
        Operador * Desrefenciacion, object-of
    */
    printf("\nAcceso mediante punteros a los valores almacenados en la direccion apuntada:\n");
    printf("*p1 = %d\n", *p1);
    printf("*p2 = %d\n", *p2);

    return 0;
}

Operaciones que admiten los punteros

Operador = (Asignación entre punteros)

¿Qué hace?

Copia la dirección de memoria, no el valor almacenado en esa dirección a la que apunta.


int *p1, *p2;
p1 = &x; // Inicializa el puntero almacenando la direccion en memoria de x

/*Se asigna la direccion del puntero p1 en el puntero p2,
ambos apuntan a la misma direccion que almacena el tipo de dato,
de este modo p1 y p2 apuntan a la misma direccion*/
p2 = p1;

Operadores de comparacion (==, !=, <, >, ≤, ≥)

¿Qué hace?

Para comparar valores:

Teoría aplicada Ejemplo 4

Makefile:


CC = gcc
CFLAGS = -std=c11 -Wall -Werror -pedantic

PROG = operaciones
OBJ = main.o

$(PROG):$(OBJ)
	$(CC) $(OBJ) -o $(PROG)

$(OBJ): main.c
	$(CC) $(CFLAGS) -c main.c

.PHONY: clean
clean
	del $(OBJ) $(PROG).exe

Código:


  // gcc -std=c11 -Wall -Wextra -pedantic main.c -o operaciones

#include <stdio.h>

int main(void)
{
    int x = 10;
    int y = 20;

    int *p1 = &x;
    int *p2 = &y;

    /*
        Operador = Asignacion entre punteros
    */
    p2 = p1; // Ambos apuntan a la misma direccion, la de x

    printf("\n Despues de p2 = p1 los valores a los que acceden ambos punteros es:\n");
    printf("*p1 = %d\n", *p1);
    printf("*p2 = %d\n", *p2);

    /*
        Modificacion del valor almacenado en la direccion a traves del puntero
    */
    *p1 = 50;

    printf("\nDespues de modificar *p1 el valor de x almacenado en la direccion a la que apunta es:\n");
    printf("x = %d\n", x);
    printf("*p2 = %d (p2 apunta a x)\n", *p2);

    /*
        Operadores de comparacion
    */
    if (p1 == p2)
    {
        printf("\np1 y p2 apuntan a la misma direccion\n");
    }

    return 0;
}

Aritmética de punteros (+, -, ++, --)

¿Qué hace?

Operaciones permitidas (con restricciones):

Operaciones que NO TIENEN SENTIDO con punteros:

Regla formal importante:

La aritmética de punteros está definida únicamente cuando el puntero apunta a elementos de un mismo array (o one-past-the-end).

Fuera de ese contexto ⇒ comportamiento indefinido

Regla fundamental:

Sea T cualquier tipo valido en C:


T *p;

Entonces:

En general:

p + n ⇒ avanza n * sizeof(T) bytes

Ejemplo con array:


int a[5] = {10, 20, 30, 40, 50}; // vector de 5 elementos 0-4 de tipo entero llamado a
int *p = a; // Equivale a &a[0] es decir le esta asignando la direccion el primer elemnto del array declarado
p = p + 1;
/* Avanza sizeof(int) bytes,
apunta al siguiente elemento (a[1] cuyo valor en esa direccion es 20).
No suma bytes, suma elementos del tipo de dato.*/

Válido:

int *p = a + 5; // Apunta "uno despues" de a[4]

Inválido:


*p = *(a + 5); // Comportamiento indefinido(UB)
p = a - 1; // Comportamiento indefinido(UB)

¿Qué es ptrdiff_t?:

Cuando restamos dos punteros del mismo array:

p - a; // No devuelve int, ni size_t, devuelve ptrdiff_t

¿Qué representa?:

Se debe incluir para seguir el estándar, la libreria stddef.h

Teoría aplicada Ejemplo 5

Makefile:


CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -pedantic

PROG = aritmetica
OBJ = main.o

$(PROG): $(OBJ)
	$(CC) $(OBJ) -o $(PROG)

$(OBJ): main.c
	$(CC) $(CFLAGS) -c main.c

.PHONY: clean
clean
	del $(OBJ) $(PROG).exe

Código:


//gcc -std=c11 -Wall -Wextra -pedantic main.c -o aritmetica

#include <stdio.h>
#include <stddef.h> //Puede omitirse porque no declaramos nada del tipo ptrdiff_t pero el estandar lo recomienda

int main(void)
{
    int a[5] = {10, 20, 30, 40, 50};
    int *p = a; // Declaracion y asignacion de la direccion del primer elemento del array, es decir apunta a a[0].

    printf("p apunta a a[0] cuyo valor almacenado en esa direccion es: %d\n", *p);

    p = p + 1;
    printf("p + 1 apunta a a[1], cuyo valor almacenado en esa direccion es: %d\n", *p);

    p++; //sumamos 1 elemento, llevandonos de a[1] a a[2]
    printf("\np++ apunta a a[2], cuyo valor almacenado en la direccion es: %d\n", *p);

    p = p - 2;
    printf("\np - 2 vuelve a a[0], cuyo valor almacenado en la direccion es: %d\n", *p);

    /*
    One-past-the-end es valido como puntero
    pero NO se puede desreferenciar
    */
    p = a + 5;
    printf("\np apunta a one-past-the-end (a + 5)\n");
    printf("Direccion de p %p\n", (void *)p);
    /*
        Comparacion valida
    */
    if (p > a)
    {
        printf("p es mayor que a (apunta a una direccion mas alta)\n");
    }
    /*
        Diferencia entre punteros
    */
    printf("\nCantidad de elementos entre p y a: %td\n", p - a);
    

    return 0;
}

Punteros y Vectores

En C, un vector y un puntero NO son lo mismo, pero están íntimamente relacionados.

¿Qué es un array?:

Un array es un objeto que representa N objetos del mismo tipo, almacenados en memoria contigua, cuyo tipo incluye su tamaño.

int a[5] = {10, 20, 30, 40, 50};

¿Qué pasa cuando usamos el nombre del array?:

En la mayoría de las expresiones, un array se convierte implícitamente en un puntero al primer elemento:

a

Se convierte automáticamente en:

Ejemplo:


int *p = a; // Es correcto, se declara el puntero a int p y se le asigna la direccion de memoria del primer elemento de a[0]
int *q = &a[0]; // Es correcto y exactamente lo mismo que en la linea anterior.

Pero esto no convierte a a en un puntero, solo significa que en esa expresion se usa su direccion inicial

Excepciones donde el array NO decae (decay) a puntero.

Diferencia fundamental con el puntero:


int a[5];
sizeof(a) // Es 5 * sizeof(int) = 5 * 4 bytes = 20 bytes su dimension
a = p; //NO PERMITIDO

No puede cambiar a que direccion de memoria apunta

Puntero:


int *p;
sizeof(p) // Dimension del puntero segun arquitectura
p = a; // PERMITIDO

Puede apuntar a otra cosa.

Acceso a elementos:

Misma sintaxis, distinto significado.


a[2] // Acceso al elemento 2 del array a.
p[2] // Acceso al objeto que esta 2 elementos despues de la direccion que guarda el puntero p.

Ambos significan:


*(a + 2)
*(p + 2)

[]: El operador [] es pura aritmética de punteros.

Relación formal entre punteros y arrays:

Por definición del lenguaje:


a[i] == *(a + i)
p[i] == *(p + i)

Esto explica porque funciona esto:


int *p = a;
printf("%d", p[3]); // Imprime 40

&a NO es lo mismo que a

Esto es clave y suele confundir.

Direcciones iguales, tipos distintos. Ej:


int a[5];
int *p =a;
printf("a\n", (void *)a);
printf("&a\n", (void *)&a); Direccion del array completo
printf("p\n", (void *)p);

Las direcciones se ven iguales, los tipos NO, el compilador las trata distinto

Un array no es un puntero, pero en la mayoría de las expresiones se convierte implícitamente en un puntero al primer elemento. El operador [] está definido en términos de aritmética de punteros, lo que explica por qué la misma sintaxis funciona tanto con arrays como con punteros.

Teoría aplicada Ejemplo 6

Makefile:


CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -pedantic

PROG = vectores
OBJ = main.o

$(PROG): $(OBJ)
	$(CC) $(OBJ) -o $(PROG)

$(OBJ): main.c
	$(CC) $(CFLAGS) -c main.c
	
.PHONY: clean
clean:
	del $(OBJ) $(PROG).exe

Código:


// gcc -std=c11 -Wall -Wextra -pedantic main.c -o vectores

#include <stdio.h>

int main(void)
{
	int a[5] = {10, 20, 30, 40, 50}; // Declaracion de un array de 5 elementos del tipo int con dimension 20 bytes(5*sizeof(int))
	int *p = a; // Declaracion de puntero a int p y asignacion de la primer direccion de memoria del array a (a[0])

	printf("a[2] = %d\n", a[2]);
	printf("p[2] = %d\n", p[2]);

	printf("*(a + 2) = %d\n", *(a + 2));
	printf("*(p + 2) = %d\n", *(p + 2));

	printf("\nsizeof(a) = %zu\n", sizeof(a));
	printf("\nsizeof(p) = %zu\n", sizeof(p));

	// Direccion de a cast a (void *)a. Ya que a (en esta expresion) decae a int *. &a es tipo: int (*)[5] array de 5 int(20bytes-array completo). p es tipo int *
	printf("\na\t = %p\n", (void *)a); // Direccion a traves de a
	printf("\n&a\t = %p\n", (void *)&a); // Direccion de a
	printf("\np\t = %p\n", (void *)p); // Direccion del puntero p

	return 0;
}

Pasaje de parámetros por referencia

En C NO existe el pasaje por referencia como concepto del lenguaje. C siempre pasa los argumentos por valor.

Lo que hacemos en C es:

Ejemplo clásico:

Por qué NO funciona el "pasaje por valor"


void cambiar(int x)
{
  x = 100;
}
int main(void)
{
  int a = 10;
  cambiar(a);
  printf("%d\n", a); // Imprime 10
  return 0;
}

¿Qué sucede acá?

Esto es pasaje por valor puro.

Solución:

Pasar la direccion(Puntero)


void cambiar(int *p)
{
  *p = 100;
}
int main(void)
{
  int a = 10;
  cambiar(&a);
  printf("%d\n", a); // Imprime 100
  return 0;
}

¿Qué sucede acá?

int a = 10; // Asignacion de variable
cambiar(&a); // Direccion de memoria de a, se pasa esa direccion a la funcion
void cambiar(int *p) // p recibe una copia de la direccion, p apunta a a

Desreferenciación (object-of) a:

*p = 100; // Se escribe en la memoria de a el valor 100, 'a' cambia.

Relación con arrays:


void imprimir(int *p, int n)
{
  for (int i = 0; i < n; i++)
      printf("%d ", p[i]);
}

Llamada:

int a[5] = {1, 2, 3, 4, 5};
imprimir(a, 5);

Teoría aplicada Ejemplo 7

Makefile:


CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -pedantic

PROG = parametros
OBJ = main.o

$(PROG): $(OBJ)
	$(CC) $(OBJ) -o $(PROG)

$(OBJ): main.c
	$(CC) $(CFLAGS) -c main.c

.PHONY: clean
clean:
	del $(OBJ) $(PROG).exe

Código:


// gcc -std=c11 -Wall -Wextra -pedantic main.c -o parametros

#include <stdio.h>

/* ---------- Ejemplo 1: PASAJE POR VALOR (NO FUNCIONA) ---------- */

void cambiar_valor(int x)
{
    x = 100;   // Se modifica SOLO la copia
}

/* ---------- Ejemplo 2: PASAJE DE DIRECCION (FUNCIONA) ---------- */

void cambiar_direccion(int *p)
{
    *p = 100;  // Se modifica el objeto apuntado
}

/* ---------- Ejemplo 3: ARRAY COMO PARAMETRO ---------- */

void imprimir_array(int *p, int n)
{
    for (int i = 0; i < n; i++)
        printf("%d ", p[i]);
    printf("\n");
}

int main(void)
{
    /* ===== Ejemplo 1 ===== */
    int a = 10;

    printf("Antes de cambiar_valor: a = %d\n", a);
    cambiar_valor(a);
    printf("Despues de cambiar_valor: a = %d\n", a);

    /*
        a NO cambia porque:
        - Se paso el valor 10
        - Se modifico una copia
    */

    /* ===== Ejemplo 2 ===== */
    printf("\nAntes de cambiar_direccion: a = %d\n", a);
    cambiar_direccion(&a);
    printf("Despues de cambiar_direccion: a = %d\n", a);

    /*
        a cambia porque:
        - Se paso su direccion
        - Se escribio en su memoria
    */

    /* ===== Ejemplo 3 ===== */
    int v[5] = {1, 2, 3, 4, 5};

    printf("\nArray original:\n");
    imprimir_array(v, 5);

    /*
        v decae a &v[0]
        Se pasa la direccion del primer elemento
        La funcion accede al array original
    */

    return 0;
}
Ejercicios de Punteros