Saltar al contenido
Portada » C/C++ » 6. Punteros en C/C++

6. Punteros en C/C++

Punteros en C/C++. En este capítulo nos adentramos en uno de los conceptos más potentes, distintivos y, a la vez, más temidos del lenguaje C, los punteros. Un puntero es una variable especial que almacena la dirección de memoria de otra variable. Esta capacidad hace posible un control directo sobre la memoria, lo que permite una programación muy eficiente aunque también requiere mayor cuidado por parte del programador.

Durante este capítulo veremos:

  • ¿Qué es un puntero?
  • Declaración, inicialización y uso
  • Operadores * y &
  • Punteros y arrays
  • Punteros a funciones
  • Punteros dobles y punteros a estructuras

Empezamos!!!

6.1 ¿Qué es un puntero?

En los lenguajes C y C++, un puntero es una variable especial que almacena una dirección de memoria. Es decir, no guarda directamente un dato, sino la ubicación en la memoria donde se encuentra ese dato.

Cuando declaras una variable normal, como por ejemplo:

int edad = 30;

Estás reservando una zona de memoria donde se almacenará el valor 30. Esa zona tiene una dirección, por ejemplo: 0x7ffeebf4a9d8.

Puedes obtener esa dirección con el operador &:

printf("%p\n", &edad);  // Imprime la dirección de memoria de 'edad'

Un puntero guarda precisamente esa dirección. Lo declaras así:

int *p;  // p es un puntero a entero

Ahora puedes hacer que p apunte a edad:

p = &edad;

Esto significa: «p contiene la dirección de memoria donde está almacenado el valor de edad.»

Acceso indirecto: desreferenciación (*)

Si quieres acceder al valor que está en la dirección almacenada por el puntero, usas el operador * delante del puntero:

printf("%d\n", *p);  // Imprime 30, el valor apuntado

Este proceso se llama desreferenciar un puntero: vas a la dirección que contiene y recuperas el dato que hay allí.

Veamos una analogía para entenderlo mejor, imagina que:

  • Una variable normal es como una caja con una etiqueta que dice “edad”, y dentro hay un número: 30.
  • Un puntero es como una nota que dice: “Ve a la estantería A23 del almacén”. El puntero no tiene el número 30 dentro, sino la ubicación donde puedes ir a buscar ese número.
  • Cuando usas *p, es como ir físicamente a esa dirección y mirar dentro de la caja.

Veamos un ejemplo básico de como utilizar los punteros.

#include <stdio.h>

int main() {
int edad = 30;
int *ptr;

ptr = &edad;

printf("Valor de edad: %d\n", edad); // 30
printf("Dirección de edad: %p\n", &edad); // 0x.... (ejemplo)
printf("Contenido del puntero ptr: %p\n", ptr); // Igual que &edad
printf("Valor apuntado por ptr: %d\n", *ptr); // 30

return 0;
}

6.2. Declaración, inicialización y uso de punteros en C/C++

Cuando trabajamos con punteros, debemos seguir tres pasos fundamentales y en este orden:

  1. Declarar el puntero.
  2. Inicializarlo, es decir, asignarle una dirección válida.
  3. Usarlo para acceder, modificar o manipular los datos a los que apunta.

Veámoslo cada uno de ellos con más detalle.

– Declaración de punteros

Para declarar un puntero, usamos el operador * en la declaración. El puntero debe indicar el tipo de dato al que apuntará, ya que esto permite al compilador saber cuántos bytes leer o escribir.

int *ptr;       // puntero a entero
float *pfloat; // puntero a float
char *pchar; // puntero a char

– Inicialización de punteros

Una vez declarado, un puntero debe contener una dirección válida antes de usarse. Puedes inicializarlo de varias maneras:

A. Asignándole la dirección de una variable existente

int edad = 25;
int *ptr = &edad; // ptr guarda la dirección de 'edad'

B. Inicializándolo con NULL (puntero nulo)

Esto es útil si no tienes una dirección válida en ese momento. Significa: “no apunta a nada”.

int *ptr = NULL;

Luego puedes asignarle una dirección más adelante:

ptr = &edad;

IMPORTANTE: ¡Nunca uses un puntero sin inicializar!

Si haces uso de un puntero que no has inicializado, este tendrá una dirección de memoria que no se sabe cual puede ser. Esta dirección de memoria puede ser partes críticas de otras partes del programa o del sistema operativo, por lo que si escribimos en él podríamos tener resultados inesperados. Por ese motivo, siempre hay que inicializar los punteros.

int *ptr;     // sin inicializar
*ptr = 10; // ERROR: ptr apunta a una dirección aleatoria → comportamiento indefinido

– Uso de punteros

Una vez declarado e inicializado, puedes usar el puntero para acceder o modificar el dato apuntado mediante el operador * (llamado operador de desreferenciación).

Veamos un ejemplo de c

#include <stdio.h>

int main() {
int edad = 25;
int *ptr = &edad;

printf("Edad original: %d\n", edad); // 25
printf("Desde puntero: %d\n", *ptr); // 25

*ptr = 30; // Cambia el valor de 'edad' a través del puntero

printf("Edad modificada: %d\n", edad); // 30

return 0;
}

Aquí vemos que modificar el valor a través del puntero cambia directamente el contenido de la variable original, porque el puntero actúa sobre la misma zona de memoria.

int x = 10;
int *p;

p = &x; // p guarda la dirección de x
*p = 20; // el valor de x ahora es 20
VariableValorDirección
x200x100
p0x100 (apunta a x)0x200
Recuerda.

Para trabajar correctamente con punteros:

  • Siempre inicialízalos con una dirección válida o con NULL.
  • Comprueba si el puntero es NULL antes de usarlo (opcional pero recomendable).
  • Usa * solo cuando estás seguro de que el puntero apunta a una dirección válida.
Tabla resumen
AcciónEjemploExplicación
Declarar punteroint *p;p es un puntero a entero
Inicializar con direcciónp = &x;p guarda la dirección de x
Desreferenciar*pAccede o modifica el valor apuntado por p

6.3 Operadores * y &

Aunque ya lo hemos visto, vamos a profundizar un poco más en el contexto de los punteros de los operadores * (asterisco) y & (ampersand):

  • & (operador de dirección): devuelve la dirección de memoria de una variable.
  • * (operador de desreferenciación): accede al valor almacenado en una dirección de memoria.

Aunque * y & se usan también en otros contextos (por ejemplo, * en multiplicación o & en operaciones a nivel de bit), no hay que confundirlos con su uso en el ámbito de los punteros. Su uso es totalmente diferente y es imprescindible tener bien claro cuando y como se utilizan.

Operador & — Dirección de una variable

El operador & nos permite obtener la dirección de memoria donde está almacenada una variable.

#include <stdio.h>

int main() {
int x = 42;
printf("El valor de x es: %d\n", x);
printf("La dirección de x es: %p\n", &x); // %p para imprimir direcciones

return 0;
}

En este ejemplo, &x devuelve algo como 0x7ffeeaa12a3c, que es la dirección en memoria donde está guardado el valor 42.

Operador * — Desreferenciación de un puntero

El operador * se utiliza para acceder al contenido de la dirección a la que apunta un puntero. Esto se llama desreferenciar un puntero.

Ejemplo:

#include <stdio.h>

int main() {
int x = 42;
int *p = &x;

printf("Valor de x usando puntero: %d\n", *p); // desreferencia p, accede a x

*p = 100; // cambia el valor de x a través del puntero
printf("Nuevo valor de x: %d\n", x); // 100

return 0;
}

*p accede a lo que hay en la dirección que contiene p, en este caso, el valor de x.

Relación entre ambos operadores

  • & se usa para obtener una dirección.
  • * se usa para acceder al valor de esa dirección.
int x = 10;
int *p = &x;

printf("Dirección de x: %p\n", &x); // dirección de x
printf("Contenido de p: %p\n", p); // misma dirección
printf("Valor de x a través de p: %d\n", *p); // 10

Resumen

CódigoSignificado
int *p;Declarar p como puntero a entero
p = &x;p guarda la dirección de x
*p = 20;Modifica el valor apuntado por p (o sea, x)
&xDirección de x
*pValor al que apunta p

6.4. Punteros y Arrays en C/C++

En C, los arrays y los punteros están profundamente relacionados. De hecho, un array puede ser tratado como un puntero al primer elemento de la secuencia. Esta relación permite manipular los arrays usando punteros, lo cual es muy útil en programación de bajo nivel o cuando se necesita un mayor control sobre la memoria.

Relación entre Arrays y Punteros

Cuando declaras un array:

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

Aquí numeros es en realidad una dirección constante al primer elemento del array. Es decir, numeros equivale a &numeros[0].

Esto significa que puedes hacer:

int *p = numeros;

Ahora p apunta al primer elemento del array. ¿Y como accedemos a los elementos del array mediante punteros? Pues usando notación de punteros, puedes acceder a los elementos del array como si recorrieras memoria:

#include <stdio.h>

int main() {
int numeros[5] = {10, 20, 30, 40, 50};
int *p = numeros;

for (int i = 0; i < 5; i++) {
printf("Elemento %d: %d\n", i, *(p + i));
}

return 0;
}

Aquí, *(p + i) accede al valor en la posición i del array.

Esto es exactamente equivalente a numeros[i].

Equivalencias comunes entre arrays y punteros

Notación de arrayNotación con punterosSignificado
numeros[i]*(numeros + i)Elemento en posición i
&numeros[i](numeros + i)Dirección del elemento i
p[i]*(p + i)Cuando p apunta al array

Recorrer arrays con punteros

Otra forma de recorrer un array usando punteros es incrementando el puntero directamente:

#include <stdio.h>

int main() {
int numeros[3] = {100, 200, 300};
int *ptr = numeros;

for (int i = 0; i < 3; i++) {
printf("Valor: %d\n", *ptr);
ptr++; // mover al siguiente elemento
}

return 0;
}

⚠️ Importante: No debes incrementar el puntero más allá del tamaño del array, o estarás accediendo a memoria no válida.

Ventajas de usar punteros con arrays

  • Permite recorrer arrays sin necesidad de usar índices.
  • Permite pasar arrays a funciones y manipular su contenido directamente.
  • Permite trabajar con estructuras de datos dinámicas como listas, árboles, etc.
  • Hace posible trabajar con subarrays o porciones del array.

Cuidados importantes

  1. Los arrays no son punteros dinámicos.
    • No puedes redimensionar un array estático.
    • Su dirección (numeros) es constante, no puedes hacer numeros++.
  2. Cuidado con la aritmética de punteros:
    • Al hacer p + i, el incremento se hace en función del tamaño del tipo apuntado (por ejemplo, sizeof(int)).

Veamos un ejemplo combinanado punteros y arrays

#include <stdio.h>

void imprimirArray(int *arr, int n) {
for (int i = 0; i < n; i++) {
printf("Elemento %d: %d\n", i, *(arr + i));
}
}

int main() {
int datos[4] = {1, 2, 3, 4};
imprimirArray(datos, 4); // Pasamos el array como puntero

return 0;
}

En este ejemplo, datos se pasa como puntero a la función, este es el modo en el que trabaja con arrays en funciones en C.

Recuerda

  • Un array puede ser tratado como un puntero al primer elemento.
  • Puedes acceder a los elementos de un array con notación de puntero: *(arr + i).
  • Puedes recorrer arrays incrementando punteros.
  • Es común pasar arrays a funciones como punteros.

6.5 Punteros a funciones

En C/C++, los punteros a funciones son una característica muy potente que permite almacenar la dirección de una función en una variable. Esto nos abre un abaníco de posibilidades que permite, por ejemplo, pasar funciones como parámetros, implementar callbacks, y diseñar estructuras dinámicas como menús o tablas de operaciones, etc.

A pesar de que su sintaxis puede parecer un poco compleja al principio, el concepto es sencillo: una función también reside en una dirección de memoria, por lo tanto, podemos tener un puntero que la apunte y usar ese puntero para invocar la función.

¿Qué es un puntero a función?

Un puntero a función es una variable que almacena la dirección de memoria de una función, lo que permite llamarla a través del puntero.

La declaración básica es:

tipo_retorno (*nombre_puntero)(tipo_parámetro1, tipo_parámetro2, ...);

Por ejemplo:

int (*operacion)(int, int);

Aquí operacion es un puntero a una función que recibe dos int y devuelve un int.

Veámoslo con un ejemplo


int suma(int a, int b) {
return a + b;
}

int main() {
int (*ptrFuncion)(int, int); // Declaramos el puntero a función
ptrFuncion = suma; // Asignamos la dirección de la función

int resultado = ptrFuncion(4, 5); // Llamamos a la función vía puntero
printf("Resultado: %d\n", resultado);

return 0;
}

Observa que en este caso asignamos dirección de memoria como: ptrFuncion = suma; y no ptrFuncion = &suma; — ambas formas son válidas, pero en C la función se convierte automáticamente a puntero al asignarla.

Tabla de operaciones con punteros a funciones

Podemos usar punteros a funciones para construir una especie de “menú” o “tabla” dinámica de operaciones:

#include <stdio.h>

int sumar(int a, int b) { return a + b; }
int restar(int a, int b) { return a - b; }
int multiplicar(int a, int b) { return a * b; }

int main() {
// Array de punteros a funciones
int (*operaciones[3])(int, int) = {sumar, restar, multiplicar};

int x = 10, y = 5;

for (int i = 0; i < 3; i++) {
printf("Resultado operación %d: %d\n", i, operaciones[i](x, y));
}

return 0;
}

Este enfoque permite acceder a diferentes funciones de forma dinámica a través de un índice o puntero.

Pasar funciones como argumentos

Una de las grandes ventajas de los punteros a funciones es que podemos pasarlos como argumentos a otras funciones:

#include <stdio.h>

int operar(int a, int b, int (*f)(int, int)) {
return f(a, b);
}

int sumar(int x, int y) {
return x + y;
}

int main() {
int resultado = operar(3, 7, sumar);
printf("Resultado: %d\n", resultado);
return 0;
}

Esto es útil, por ejemplo, en algoritmos como ordenación, donde podemos pasar funciones de comparación.

Precauciones

  • Asegúrate de que la firma de la función coincida exactamente con la declaración del puntero.
  • Usar punteros nulos o no inicializados puede causar fallos en tiempo de ejecución.
  • Las funciones deben estar declaradas antes de ser usadas como punteros.

Ejemplo con función de comparación

#include <stdio.h>

int mayor(int a, int b) {
return (a > b) ? a : b;
}

int menor(int a, int b) {
return (a < b) ? a : b;
}

void comparar(int x, int y, int (*f)(int, int)) {
printf("Resultado de la comparación: %d\n", f(x, y));
}

int main() {
comparar(10, 20, mayor); // Usamos puntero a función
comparar(10, 20, menor); // Cambiamos el comportamiento

return 0;
}

6.6 Punteros dobles y punteros a estructuras

En este punto avanzamos con dos conceptos clave y avanzados en el manejo de punteros en C/C++:

  • Punteros dobles (o punteros a punteros)
  • Punteros a estructuras

Ambos son esenciales para trabajar con estructuras dinámicas más complejas como listas enlazadas, matrices dinámicas y estructuras de datos avanzadas.

Punteros Dobles (punteros a punteros)

Un puntero doble es una variable que almacena la dirección de un puntero. Es decir, es un puntero que apunta a otro puntero.

Declaración:

int **ptr;

Aquí ptr es un puntero que apunta a un puntero a int.

Veámosolo con un ejemplo:

#include <stdio.h>

int main() {
int valor = 100;
int *p1 = &valor;
int **p2 = &p1;

printf("valor = %d\n", valor); // 100
printf("*p1 = %d\n", *p1); // 100
printf("**p2 = %d\n", **p2); // 100

return 0;
}

**p2 accede al valor original (valor) a través de dos niveles de indirección: p2 → p1 → valor.

¿Para qué sirven los punteros dobles?

  • Modificar un puntero desde una función:
void asignar(int **pptr) {
    *pptr = malloc(sizeof(int));
    **pptr = 50;
}

int main() {
    int *p = NULL;
    asignar(&p);
    printf("Valor asignado: %d\n", *p);
    free(p);
    return 0;
}

Manejo de matrices dinámicas (matrices 2D)

int **matriz = malloc(filas * sizeof(int *));
for (int i = 0; i < filas; i++) {
    matriz[i] = malloc(columnas * sizeof(int));
}

Punteros a estructuras

En C/C++, puedes crear estructuras (struct) y manipularlas a través de punteros. Esto es muy común en programación de sistemas y estructuras dinámicas.

#include <stdio.h>

struct Persona {
char nombre[20];
int edad;
};

int main() {
struct Persona p1 = {"Carlos", 30};
struct Persona *ptr = &p1;

printf("Nombre: %s\n", ptr->nombre);
printf("Edad: %d\n", ptr->edad);

return 0;
}

Crear estructuras dinámicamente con malloc

malloc (abreviatura de memory allocation) es una función de la biblioteca estándar de C que se utiliza para reservar memoria dinámica en tiempo de ejecución. Esto significa que puedes pedirle al sistema una cantidad de memoria que no conoces en tiempo de compilación, y la función te devolverá un puntero a la primera dirección del bloque reservado.

Sintaxis:

void *malloc(size_t tamaño_en_bytes);
  • Devuelve un puntero void* que normalmente se convierte al tipo deseado (int*, char*, etc.).
  • Si la memoria no puede ser reservada, devuelve NULL.

Ejemplo:

int *numeros = (int *) malloc(5 * sizeof(int)); // Reserva espacio para 5 enteros

En este caso, malloc reserva memoria suficiente para almacenar 5 enteros consecutivos y devuelve un puntero al primer elemento.

Es importante liberar esa memoria con free() cuando ya no se necesita para evitar fugas de memoria.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Persona {
char nombre[20];
int edad;
};

int main() {
struct Persona *ptr = malloc(sizeof(struct Persona));

strcpy(ptr->nombre, "Lucía");
ptr->edad = 25;

printf("Nombre: %s\n", ptr->nombre);
printf("Edad: %d\n", ptr->edad);

free(ptr);
return 0;
}

Edl uso de malloc es muy útil cuando no conocemos la cantidad de estructuras necesarias en tiempo de compilación.

Combinando punteros dobles y estructuras

Imagina que quieres crear un arreglo dinámico de punteros a estructuras:

struct Persona **grupo = malloc(n * sizeof(struct Persona*));

for (int i = 0; i < n; i++) {
grupo[i] = malloc(sizeof(struct Persona));
strcpy(grupo[i]->nombre, "Persona");
grupo[i]->edad = 20 + i;
}

Esto permite construir arreglos de objetos complejos, como listas de usuarios, productos, etc.

Resumen

ConceptoDescripción
* (un puntero)Apunta a un valor
** (puntero doble)Apunta a un puntero
->Accede a miembros de estructuras vía puntero
struct *ptrPuntero a una estructura
malloc(sizeof(...))Asigna memoria dinámica para punteros o estructuras

Conclusión.

Aquí termina este capítulo, hemos visto uno de los conceptos más potentes y distintivos del lenguaje C/C++: los punteros. Aprendimos que un puntero es una variable que almacena la dirección de memoria de otra variable, lo que permite manipular directamente el contenido de la memoria. Estudiamos cómo se declaran, inicializan y utilizan punteros, haciendo uso de los operadores & (dirección) y * (acceso al valor apuntado). Vimos también cómo los punteros se relacionan estrechamente con los arrays, cómo pueden usarse para acceder dinámicamente a sus elementos, y cómo existen punteros a funciones y punteros dobles. Finalmente, introdujimos el uso de la función malloc para la asignación dinámica de memoria, permitiendo al programa reservar espacio durante la ejecución. Vamos con la parte práctica.

Ejercicio 1 – Copiar un array usando aritmética de punteros
Enunciado
:
Escribe un programa que copie los 8 valores de un array origen en otro array destino sin usar notación de índices ([i]). Utiliza exclusivamente aritmética de punteros para recorrer ambos arrays y realizar la copia, y muestra el contenido final de destino.

Código:

#include <stdio.h>

#define N 8

int main(void) {
    int origen[N]  = {2, 4, 6, 8, 10, 12, 14, 16};
    int destino[N];

    /* punteros de recorrido */
    int *p_src = origen;           /* apunta a origen[0]  */
    int *p_end = origen + N;       /* apunta a origen[N]  */
    int *p_dst = destino;          /* apunta a destino[0] */

    while (p_src < p_end) {        /* recorre hasta la última dirección */
        *p_dst = *p_src;           /* copia valor */
        p_src++;                   /* avanza en origen  */
        p_dst++;                   /* avanza en destino */
    }

    /* Mostrar destino */
    for (p_dst = destino; p_dst < destino + N; p_dst++)
        printf("%d ", *p_dst);

    return 0;
}

Explicación

  • p_src y p_dst son punteros que avanzan elemento a elemento.
  • p_end marca la dirección justo después del último elemento de origen; cuando p_src la alcanza termina el bucle.
  • La copia se hace con *p_dst = *p_src, accediendo al valor de cada dirección.
  • No se usan índices; todo el recorrido se basa en comparar direcciones y desplazarlas con ++.
Ejercicio 2 – Contar vocales en una cadena con punteros
Enunciado
Pide al usuario una frase de hasta 80 caracteres y cuenta cuántas vocales (mayúsculas o minúsculas) contiene. Recorre la cadena exclusivamente con un puntero; no utilices índices ni funciones de <string.h> salvo fgets para la entrada.

Código:

#include <stdio.h>
#include <ctype.h>

int main(void) {
    char frase[81];
    int contador = 0;

    printf("Introduce una frase: ");
    fgets(frase, sizeof(frase), stdin);

    /* puntero que recorre la cadena hasta el carácter nulo */
    for (char *p = frase; *p != '\0'; p++) {
        char c = tolower(*p);          /* homogeniza a minúscula */
        if (c=='a'||c=='e'||c=='i'||c=='o'||c=='u')
            contador++;
    }

    printf("Número de vocales: %d\n", contador);
    return 0;
}

Explicación

  • fgets lee la línea completa (incluye espacios).
  • El puntero p avanza carácter a carácter hasta encontrar '\0'.
  • tolower normaliza el carácter, así una sola comparación cubre mayúsculas y minúsculas.
  • Cada coincidencia incrementa contador, sin usar índices.
Ejercicio 3 – Matriz dinámica: reserva‑libera y suma de diagonal
Enunciado
Crea dinámicamente una matriz cuadrada N×N (donde N lo introduce el usuario), rellénala con los enteros del 1 al N×N en orden fila‑columna y calcula la suma de la diagonal principal. Utiliza punteros dobles para gestionar la matriz y recuerda liberar toda la memoria reservada.

Código:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int N;
    printf("Tamaño de la matriz (N): ");
    if (scanf("%d", &N) != 1 || N <= 0) {
        puts("Valor no válido");
        return 1;
    }

    /* reserva del array de punteros a filas */
    int **mat = malloc(N * sizeof(int*));
    if (!mat) { puts("Fallo malloc"); return 1; }

    /* reserva de cada fila */
    for (int i = 0; i < N; i++) {
        mat[i] = malloc(N * sizeof(int));
        if (!mat[i]) { puts("Fallo malloc"); return 1; }
    }

    /* rellenar matriz y calcular diagonal */
    int valor = 1, sumaDiag = 0;
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            mat[i][j] = valor++;
            if (i == j)               /* posición en diagonal principal */
                sumaDiag += mat[i][j];
        }
    }

    printf("Suma diagonal principal: %d\n", sumaDiag);

    /* liberar memoria */
    for (int i = 0; i < N; i++)
        free(mat[i]);
    free(mat);

    return 0;
}

Explicación

  1. Reserva dinámica
    • mat es un puntero doble (int **) que primero apunta a un array de N punteros‑fila.
    • Cada mat[i] apunta a una fila con N enteros.
  2. Relleno y cálculo
    • Se recorre con dos bucles anidados asignando valores secuenciales.
    • Cuando i==j, el elemento pertenece a la diagonal principal; se acumula en sumaDiag.
  3. Liberación
    • Primero se libera cada fila (free(mat[i])) y finalmente el array de punteros (free(mat)), evitando fugas de memoria.
Logo C++

Bibliografía del tutorial de C/C++.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Disponible para Amazon Prime