C

C

Tutorial C: Punteros y funciones

Aprende a usar punteros en C para modificar variables originales y retornar punteros desde funciones con ejemplos prácticos y buenas prácticas.

Aprende C y certifícate

Pasar punteros como parámetros

Cuando trabajamos con funciones en C, normalmente pasamos valores que se copian dentro de la función. Esto significa que cualquier cambio realizado a estos parámetros dentro de la función no afecta a las variables originales. Sin embargo, hay situaciones donde necesitamos modificar las variables originales desde dentro de una función, y aquí es donde los punteros se vuelven extremadamente útiles.

Un puntero como parámetro permite a una función acceder y modificar directamente la variable original que se encuentra en otra parte del programa. Esto se logra pasando la dirección de memoria de la variable en lugar de su valor.

Sintaxis básica

Para pasar un puntero como parámetro, la declaración de la función debe especificar que espera recibir una dirección de memoria:

void miFuncion(int *parametro) {
    // Código que trabaja con el puntero
}

Y al llamar a la función, usamos el operador & para obtener la dirección de la variable:

int numero = 10;
miFuncion(&numero);  // Pasamos la dirección de 'numero'

Ejemplo práctico

Veamos un ejemplo sencillo donde una función incrementa el valor de una variable:

#include <stdio.h>

// Función que recibe un puntero a entero
void incrementar(int *valor) {
    // Usamos el operador * para acceder al valor apuntado
    *valor = *valor + 1;
    // También podríamos escribir: (*valor)++;
}

int main() {
    int numero = 5;
    
    printf("Antes de llamar a la función: %d\n", numero);
    
    // Pasamos la dirección de 'numero'
    incrementar(&numero);
    
    printf("Después de llamar a la función: %d\n", numero);
    
    return 0;
}

Este programa mostrará:

Antes de llamar a la función: 5
Después de llamar a la función: 6

Observa cómo el valor de numero cambió aunque la modificación ocurrió dentro de la función. Esto es posible porque pasamos un puntero que permite a la función acceder directamente a la ubicación de memoria donde se almacena numero.

Trabajando con múltiples punteros

Las funciones pueden recibir varios punteros como parámetros, lo que nos permite modificar múltiples variables originales:

#include <stdio.h>

// Función que intercambia los valores de dos variables
void intercambiar(int *a, int *b) {
    int temporal = *a;  // Guardamos el valor de 'a' en una variable temporal
    *a = *b;            // Asignamos a 'a' el valor de 'b'
    *b = temporal;      // Asignamos a 'b' el valor temporal (antiguo valor de 'a')
}

int main() {
    int x = 10;
    int y = 20;
    
    printf("Antes del intercambio: x = %d, y = %d\n", x, y);
    
    intercambiar(&x, &y);  // Pasamos las direcciones de 'x' e 'y'
    
    printf("Después del intercambio: x = %d, y = %d\n", x, y);
    
    return 0;
}

Este programa mostrará:

Antes del intercambio: x = 10, y = 20
Después del intercambio: x = 20, y = 10

La función intercambiar es un ejemplo clásico donde los punteros son necesarios. Sin ellos, sería imposible intercambiar los valores de dos variables desde una función.

Punteros a arrays

Cuando pasamos un array a una función, en realidad estamos pasando un puntero al primer elemento del array:

#include <stdio.h>

// Esta función recibe un puntero al primer elemento del array
void duplicarElementos(int *array, int longitud) {
    for (int i = 0; i < longitud; i++) {
        // Podemos acceder a los elementos usando notación de índice
        array[i] = array[i] * 2;
        
        // O usando aritmética de punteros
        // *(array + i) = *(array + i) * 2;
    }
}

int main() {
    int numeros[5] = {1, 2, 3, 4, 5};
    
    printf("Array original: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", numeros[i]);
    }
    
    duplicarElementos(numeros, 5);  // No necesitamos usar &
    
    printf("\nArray modificado: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", numeros[i]);
    }
    
    return 0;
}

Este programa mostrará:

Array original: 1 2 3 4 5
Array modificado: 2 4 6 8 10

Nota que al pasar el array numeros a la función, no usamos el operador &. Esto es porque el nombre de un array en C se convierte automáticamente en un puntero a su primer elemento.

Consideraciones importantes

Cuando trabajamos con punteros como parámetros, debemos tener en cuenta algunas consideraciones:

  • Validación de punteros: Siempre es buena práctica verificar que un puntero no sea NULL antes de desreferenciarlo:
void funcionSegura(int *ptr) {
    if (ptr != NULL) {
        *ptr = 100;  // Solo modificamos si el puntero es válido
    }
}
  • Documentación clara: Es importante documentar en el código cuando una función modificará una variable a través de un puntero:
// Esta función modifica el valor apuntado por 'resultado'
void calcularResultado(int entrada, int *resultado) {
    *resultado = entrada * entrada;
}
  • Constancia cuando sea apropiado: Si no planeas modificar el contenido apuntado, usa const para indicarlo:
// Esta función no modifica el contenido del array
void imprimirArray(const int *array, int longitud) {
    for (int i = 0; i < longitud; i++) {
        printf("%d ", array[i]);
    }
    printf("\n");
}

Pasar punteros como parámetros es una técnica fundamental en C que te permite crear funciones más flexibles y eficientes, capaces de modificar directamente las variables originales en lugar de trabajar con copias.

Modificar variables originales

Uno de los usos principales de los punteros en C es permitirnos modificar variables originales desde dentro de una función. Cuando pasamos variables a funciones de manera convencional (por valor), C crea una copia local de esas variables, lo que significa que cualquier cambio realizado dentro de la función no afecta a las variables originales.

Para entender mejor este concepto, veamos primero qué ocurre cuando pasamos variables por valor:

#include <stdio.h>

void intentarCambiar(int numero) {
    numero = 100;  // Esta modificación solo afecta a la copia local
    printf("Dentro de la función: numero = %d\n", numero);
}

int main() {
    int x = 5;
    printf("Antes de llamar a la función: x = %d\n", x);
    
    intentarCambiar(x);
    
    printf("Después de llamar a la función: x = %d\n", x);
    return 0;
}

Este programa mostrará:

Antes de llamar a la función: x = 5
Dentro de la función: numero = 100
Después de llamar a la función: x = 5

Como puedes ver, el valor de x no cambió, a pesar de que dentro de la función modificamos numero. Esto ocurre porque numero es simplemente una copia de x.

Usando punteros para modificar variables originales

Para modificar la variable original, necesitamos usar punteros. Veamos cómo funciona:

#include <stdio.h>

void cambiarValor(int *ptrNumero) {
    *ptrNumero = 100;  // Modificamos el valor en la dirección de memoria
    printf("Dentro de la función: *ptrNumero = %d\n", *ptrNumero);
}

int main() {
    int x = 5;
    printf("Antes de llamar a la función: x = %d\n", x);
    
    cambiarValor(&x);  // Pasamos la dirección de memoria de x
    
    printf("Después de llamar a la función: x = %d\n", x);
    return 0;
}

Este programa mostrará:

Antes de llamar a la función: x = 5
Dentro de la función: *ptrNumero = 100
Después de llamar a la función: x = 100

Ahora el valor de x sí cambió porque:

  1. Pasamos la dirección de memoria de x a la función usando &x
  2. Dentro de la función, usamos el operador de desreferencia * para acceder y modificar el valor almacenado en esa dirección

Ejemplo práctico: función de intercambio (swap)

Un ejemplo clásico donde necesitamos modificar variables originales es la función de intercambio. Sin punteros, es imposible crear una función que intercambie los valores de dos variables:

#include <stdio.h>

// Versión incorrecta (no funciona)
void swapIncorrecto(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}

// Versión correcta usando punteros
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int num1 = 10, num2 = 20;
    
    printf("Antes del intento incorrecto: num1 = %d, num2 = %d\n", num1, num2);
    swapIncorrecto(num1, num2);
    printf("Después del intento incorrecto: num1 = %d, num2 = %d\n", num1, num2);
    
    printf("\nAntes del swap correcto: num1 = %d, num2 = %d\n", num1, num2);
    swap(&num1, &num2);
    printf("Después del swap correcto: num1 = %d, num2 = %d\n", num1, num2);
    
    return 0;
}

Este programa mostrará:

Antes del intento incorrecto: num1 = 10, num2 = 20
Después del intento incorrecto: num1 = 10, num2 = 20

Antes del swap correcto: num1 = 10, num2 = 20
Después del swap correcto: num1 = 20, num2 = 10

Modificando elementos de un array

Los arrays en C tienen una relación especial con los punteros. Cuando pasamos un array a una función, estamos pasando un puntero al primer elemento, lo que nos permite modificar el array original:

#include <stdio.h>

void duplicarElementos(int arr[], int tamanio) {
    for (int i = 0; i < tamanio; i++) {
        arr[i] *= 2;  // Multiplicamos cada elemento por 2
    }
}

int main() {
    int numeros[5] = {1, 2, 3, 4, 5};
    
    printf("Array original: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", numeros[i]);
    }
    printf("\n");
    
    duplicarElementos(numeros, 5);
    
    printf("Array modificado: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", numeros[i]);
    }
    printf("\n");
    
    return 0;
}

Este programa mostrará:

Array original: 1 2 3 4 5
Array modificado: 2 4 6 8 10

Nota que no necesitamos usar el operador & al pasar el array, ya que el nombre del array (numeros) ya es un puntero al primer elemento.

Modificando estructuras

También podemos modificar estructuras completas usando punteros:

#include <stdio.h>

// Definimos una estructura para un punto en 2D
struct Punto {
    int x;
    int y;
};

// Función para mover un punto
void moverPunto(struct Punto *p, int dx, int dy) {
    p->x += dx;  // Equivalente a (*p).x += dx;
    p->y += dy;  // Equivalente a (*p).y += dy;
}

int main() {
    struct Punto miPunto = {5, 10};
    
    printf("Posición original: (%d, %d)\n", miPunto.x, miPunto.y);
    
    moverPunto(&miPunto, 3, 7);
    
    printf("Nueva posición: (%d, %d)\n", miPunto.x, miPunto.y);
    
    return 0;
}

Este programa mostrará:

Posición original: (5, 10)
Nueva posición: (8, 17)

Observa el uso del operador -> que es una forma abreviada de acceder a los miembros de una estructura a través de un puntero. Es equivalente a usar (*p).x pero más legible.

Buenas prácticas al modificar variables originales

Cuando modificamos variables originales a través de punteros, es importante seguir algunas buenas prácticas:

  • Validar punteros: Siempre verifica que los punteros no sean NULL antes de usarlos:
void funcionSegura(int *ptr) {
    if (ptr == NULL) {
        printf("Error: Puntero nulo\n");
        return;
    }
    *ptr = 100;  // Solo modificamos si el puntero es válido
}
  • Documentar claramente en los comentarios o en el nombre de la función que se modificarán las variables originales:
// Esta función modifica el valor de 'resultado'
void calcularYGuardar(int entrada, int *resultado) {
    *resultado = entrada * entrada;
}
  • Usar const para punteros que no deben modificar el contenido:
// Esta función usa el valor pero no lo modifica
void imprimirValor(const int *valor) {
    printf("El valor es: %d\n", *valor);
    // *valor = 10;  // Esto generaría un error de compilación
}

Modificar variables originales a través de punteros es una técnica fundamental en C que te permite crear funciones más potentes y flexibles, capaces de producir cambios reales en los datos con los que trabajan, en lugar de simplemente operar con copias temporales.

Retornar punteros de funciones

En C, además de pasar punteros como parámetros, también podemos retornar punteros desde una función. Esta técnica nos permite devolver la dirección de memoria de un dato, en lugar de devolver el dato en sí mismo. Esto resulta especialmente útil cuando necesitamos acceder a datos que se crean o se localizan dentro de una función.

Cuando una función retorna un puntero, está devolviendo una dirección de memoria que podemos utilizar para acceder o modificar los datos a los que apunta desde fuera de la función.

Sintaxis básica

La sintaxis para declarar una función que retorna un puntero es la siguiente:

tipo_dato *nombreFuncion(parámetros) {
    // Código de la función
    return puntero;
}

Donde tipo_dato es el tipo de dato al que apuntará el puntero que retorna la función.

Retornar punteros a variables locales estáticas

Podemos retornar un puntero a una variable local estática. Las variables estáticas mantienen su valor entre llamadas a la función:

#include <stdio.h>

int *obtenerContador() {
    static int contador = 0;  // Variable estática
    contador++;
    return &contador;  // Retornamos la dirección de memoria
}

int main() {
    int *ptr;
    
    ptr = obtenerContador();
    printf("Valor del contador: %d\n", *ptr);
    
    ptr = obtenerContador();
    printf("Valor del contador: %d\n", *ptr);
    
    return 0;
}

Este programa mostrará:

Valor del contador: 1
Valor del contador: 2

La variable contador es estática, lo que significa que conserva su valor entre llamadas a la función. Cada vez que llamamos a obtenerContador(), incrementamos el contador y retornamos su dirección.

¡Cuidado con retornar punteros a variables locales!

Es importante nunca retornar un puntero a una variable local no estática, ya que estas variables dejan de existir cuando la función termina:

#include <stdio.h>

// ¡INCORRECTO! No hagas esto
int *funcionIncorrecta() {
    int numero = 10;
    return &numero;  // ¡ERROR! 'numero' dejará de existir
}

int main() {
    int *ptr = funcionIncorrecta();
    
    // Comportamiento indefinido: 'numero' ya no existe
    printf("Valor: %d\n", *ptr);  
    
    return 0;
}

Este código puede compilar (posiblemente con advertencias), pero producirá un comportamiento indefinido al ejecutarse, ya que intenta acceder a memoria que ya no es válida.

Retornar punteros a memoria dinámica

Una forma segura y común de retornar punteros es asignar memoria dinámicamente usando malloc() o funciones similares:

#include <stdio.h>
#include <stdlib.h>  // Necesario para malloc y free

int *crearEntero(int valor) {
    int *ptr = (int *)malloc(sizeof(int));  // Asignamos memoria
    
    if (ptr != NULL) {  // Verificamos que la asignación fue exitosa
        *ptr = valor;   // Guardamos el valor en la memoria asignada
    }
    
    return ptr;  // Retornamos el puntero a la memoria asignada
}

int main() {
    int *miEntero = crearEntero(42);
    
    if (miEntero != NULL) {
        printf("Valor creado: %d\n", *miEntero);
        
        // Importante: liberar la memoria cuando ya no la necesitamos
        free(miEntero);
    } else {
        printf("Error al asignar memoria\n");
    }
    
    return 0;
}

Este programa mostrará:

Valor creado: 42

En este ejemplo:

  1. La función crearEntero() asigna memoria para un entero
  2. Guarda el valor proporcionado en esa memoria
  3. Retorna un puntero a esa memoria
  4. En main(), usamos el puntero y luego liberamos la memoria con free()

Retornar punteros a elementos de arrays

También podemos retornar punteros a elementos específicos dentro de un array:

#include <stdio.h>

int *encontrarMaximo(int array[], int tamanio) {
    if (tamanio <= 0) return NULL;
    
    int *max = &array[0];  // Comenzamos asumiendo que el primer elemento es el máximo
    
    for (int i = 1; i < tamanio; i++) {
        if (array[i] > *max) {
            max = &array[i];  // Actualizamos el puntero si encontramos un valor mayor
        }
    }
    
    return max;  // Retornamos un puntero al elemento máximo
}

int main() {
    int numeros[] = {5, 8, 3, 12, 7, 4};
    int tamanio = sizeof(numeros) / sizeof(numeros[0]);
    
    int *maximo = encontrarMaximo(numeros, tamanio);
    
    if (maximo != NULL) {
        printf("El valor máximo es: %d\n", *maximo);
        printf("Se encuentra en la posición: %ld\n", maximo - numeros);
    }
    
    return 0;
}

Este programa mostrará:

El valor máximo es: 12
Se encuentra en la posición: 3

En este ejemplo, la función encontrarMaximo() retorna un puntero al elemento más grande del array. Luego usamos aritmética de punteros (maximo - numeros) para calcular la posición del elemento máximo dentro del array.

Retornar punteros a estructuras

Retornar punteros a estructuras es una técnica muy útil, especialmente cuando las estructuras son grandes:

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

// Definimos una estructura para un estudiante
struct Estudiante {
    int id;
    char nombre[50];
    float promedio;
};

struct Estudiante *crearEstudiante(int id, const char *nombre, float promedio) {
    struct Estudiante *nuevo = (struct Estudiante *)malloc(sizeof(struct Estudiante));
    
    if (nuevo != NULL) {
        nuevo->id = id;
        strncpy(nuevo->nombre, nombre, 49);
        nuevo->nombre[49] = '\0';  // Aseguramos que la cadena termine correctamente
        nuevo->promedio = promedio;
    }
    
    return nuevo;
}

int main() {
    struct Estudiante *alumno = crearEstudiante(101, "Ana García", 9.5);
    
    if (alumno != NULL) {
        printf("ID: %d\n", alumno->id);
        printf("Nombre: %s\n", alumno->nombre);
        printf("Promedio: %.1f\n", alumno->promedio);
        
        free(alumno);  // Liberamos la memoria
    }
    
    return 0;
}

Este programa mostrará:

ID: 101
Nombre: Ana García
Promedio: 9.5

Buenas prácticas al retornar punteros

Para usar punteros retornados de manera segura y eficiente:

  • Siempre verifica si el puntero retornado es NULL antes de usarlo:
int *ptr = algunaFuncion();
if (ptr != NULL) {
    // Usar el puntero de forma segura
} else {
    // Manejar el error
}
  • Documenta claramente quién es responsable de liberar la memoria:
// Esta función retorna un puntero a memoria asignada dinámicamente.
// El llamador es responsable de liberar esta memoria con free() cuando ya no la necesite.
int *crearArray(int tamanio) {
    return (int *)malloc(tamanio * sizeof(int));
}
  • Libera la memoria cuando ya no la necesites para evitar fugas de memoria:
int *datos = crearArray(100);
// Usar datos...
free(datos);  // Liberar cuando ya no se necesita
datos = NULL; // Buena práctica: establecer el puntero a NULL después de liberarlo
  • Usa const cuando retornes un puntero a datos que no deben ser modificados:
const char *obtenerMensaje() {
    return "Hola, mundo";  // Retorna un puntero a una cadena constante
}

Retornar punteros de funciones es una técnica poderosa en C que te permite trabajar con datos de manera más flexible y eficiente. Sin embargo, requiere un manejo cuidadoso para evitar problemas como fugas de memoria o acceso a memoria inválida.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende C online

Todas las lecciones de C

Accede a todas las lecciones de C y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a C y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender cómo pasar punteros como parámetros para modificar variables originales dentro de funciones.
  • Aprender a retornar punteros desde funciones y las precauciones necesarias.
  • Conocer la relación entre arrays y punteros al pasarlos a funciones.
  • Aplicar punteros para modificar estructuras y realizar operaciones como intercambio de valores.
  • Adoptar buenas prácticas en el manejo de punteros para evitar errores comunes y fugas de memoria.