funciones en C void prototipos paso por valor referencia guía

Funciones en C — declaración, void, paso por valor y por referencia desde cero

Las funciones en C son el mecanismo que te permite dividir un programa en bloques reutilizables, igual que en Python, pero con reglas más estrictas que al principio parecen complicadas y luego tienen todo el sentido. En IC2 ya usas funciones sin saberlo , main es una función.

En este artículo vemos cómo crear las tuyas propias: cómo declararlas, qué es void, por qué existe el prototipo, y la diferencia entre paso por valor y paso por referencia, que es donde se conecta todo lo que ya sabes de punteros.

¿Qué es una función en C?

Una función es un bloque de código con nombre que realiza una tarea concreta y puede reutilizarse desde cualquier punto del programa. En Python las habías visto así:

def sumar(a, b):
    return a + b

En C la misma función se escribe así:

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

Las diferencias inmediatas respecto a Python: tienes que declarar el tipo de lo que devuelve la función (int) y el tipo de cada parámetro (int a, int b). En Python eso era opcional, en C es obligatorio.

La estructura general de cualquier función en C es:

tipo_retorno nombre_funcion(tipo param1, tipo param2, ...) {
    /* cuerpo de la función */
    return valor;    /* si tipo_retorno no es void */
}

Qué es void — el tipo que más confunde

void es el tipo que usas cuando una función no devuelve nada o no recibe parámetros. Es la forma de C de decir «este hueco está vacío».

Hay dos usos distintos de void y vale la pena entenderlos por separado:

void como tipo de retorno — la función hace algo pero no devuelve ningún valor:

void imprimir_saludo(char *nombre) {
    printf("Hola, %s\n", nombre);
    /* no hay return — o hay return; sin valor */
}

void como lista de parámetros — la función no recibe ningún parámetro:

int leer_numero(void) {
    int n;
    scanf("%d", &n);
    return n;
}

En C moderno void en los parámetros es equivalente a dejar los paréntesis vacíos (), pero en versiones antiguas del estándar significaban cosas distintas. En IC2 verás las dos formas.

La combinación más común que verás en IC2:

void mostrar_resultado(void) {
    printf("Resultado: ...\n");
}

Esta función no recibe nada y no devuelve nada solo hace algo. Es el equivalente en C de un método que solo imprime.

Cómo declarar y llamar una función correctamente

Una función en C tiene tres partes: la definición (el cuerpo completo), el prototipo (la declaración previa) y la llamada (usarla desde otro sitio).

#include <stdio.h>

/* PROTOTIPO — declaración antes de main */
int sumar(int a, int b);
void imprimir_resultado(int resultado);

int main(void) {
    int x = 5;
    int y = 3;
    int r;

    r = sumar(x, y);           /* LLAMADA */
    imprimir_resultado(r);     /* LLAMADA */

    return 0;
}

/* DEFINICIÓN — el cuerpo real */
int sumar(int a, int b) {
    return a + b;
}

void imprimir_resultado(int resultado) {
    printf("El resultado es: %d\n", resultado);
}
El resultado es: 8

Por qué existe el prototipo — la razón real

Esta es la pregunta que más aparece en IC2 y que más vale entender de verdad.

En C el compilador lee el código de arriba hacia abajo, en orden. Cuando llega a la llamada sumar(x, y) en main, necesita saber: ¿cuántos parámetros acepta sumar? ¿De qué tipo? ¿Qué devuelve? Si la definición de sumar está después de main, el compilador todavía no la ha visto, y da error.

El prototipo es la solución: le dices al compilador «esta función existe, tiene esta firma, la definición viene después». Con eso el compilador puede verificar que la llamas correctamente sin necesitar el cuerpo completo todavía.

/* SIN PROTOTIPO — error si la definición está después de main */
int main(void) {
    int r = sumar(5, 3);    /* error: función no declarada */
    return 0;
}

int sumar(int a, int b) {
    return a + b;
}
/* CON PROTOTIPO — funciona correctamente */
int sumar(int a, int b);    /* prototipo: basta con la firma */

int main(void) {
    int r = sumar(5, 3);    /* el compilador ya sabe qué es sumar */
    return 0;
}

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

La alternativa al prototipo es poner todas las funciones antes de main. Eso también funciona, pero en programas grandes no es práctico, los prototipos van arriba del todo y las definiciones al final.

En IC2 verás los dos estilos. El más común en los apuntes es: prototipos arriba, main en el medio, definiciones al final.

Funciones con retorno — return

Cuando una función devuelve un valor usas return con ese valor. En cuanto Python ejecuta return la función termina y devuelve el valor, en C es exactamente igual:

float media(int a, int b, int c) {
    return (float)(a + b + c) / 3;    /* termina aquí y devuelve el resultado */
}

int maximo(int a, int b) {
    if (a > b) {
        return a;    /* termina aquí si a > b */
    }
    return b;        /* termina aquí en caso contrario */
}

int es_par(int n) {
    return n % 2 == 0;    /* devuelve 1 (verdadero) o 0 (falso) */
}
int main(void) {
    printf("Media: %.2f\n", media(4, 7, 10));     /* → 7.00 */
    printf("Máximo: %d\n", maximo(8, 13));         /* → 13 */
    printf("¿6 es par? %d\n", es_par(6));          /* → 1 */
    printf("¿7 es par? %d\n", es_par(7));          /* → 0 */
    return 0;
}

El cast (float) antes de la suma es importante, sin él, (4 + 7 + 10) / 3 hace división entera y da 7, no 7.0. El cast convierte el resultado a float antes de dividir.

Funciones sin retorno — void

Cuando una función solo hace algo sin devolver un valor usa void como tipo de retorno. No lleva return al final, o lleva return; sin valor para salir antes:

void imprimir_separador(int longitud) {
    int i;
    for (i = 0; i < longitud; i++) {
        printf("-");
    }
    printf("\n");
}

void mostrar_tabla(int n) {
    int i;
    printf("Tabla del %d:\n", n);
    imprimir_separador(20);
    for (i = 1; i <= 10; i++) {
        printf("  %d x %d = %d\n", n, i, n * i);
    }
    imprimir_separador(20);
}
int main(void) {
    mostrar_tabla(7);
    return 0;
}
Tabla del 7:
--------------------
  7 x 1 = 7
  7 x 2 = 14
  7 x 3 = 21
  ...
  7 x 10 = 70
--------------------

Fíjate en que mostrar_tabla llama a imprimir_separador, una función puede llamar a otra. El orden en el programa sería: prototipo de ambas arriba, main en el medio, definiciones al final.

Paso por valor — la copia

Cuando llamas a una función en C, por defecto los argumentos se pasan por valor, la función recibe una copia de los datos, no los datos originales. Cualquier modificación dentro de la función no afecta a las variables originales.

void intentar_doblar(int n) {
    n = n * 2;    /* modifica la copia, no el original */
    printf("Dentro de la función: %d\n", n);
}

int main(void) {
    int x = 5;
    intentar_doblar(x);
    printf("Después de llamar: %d\n", x);    /* x sigue siendo 5 */
    return 0;
}
Dentro de la función: 10
Después de llamar: 5

Esto es exactamente igual que en Python con tipos inmutables, si pasas un int a una función Python y lo modificas dentro, el original no cambia.

El paso por valor es el comportamiento por defecto. Es seguro, la función no puede romper tus variables. Pero tiene un límite: ¿y si quieres que la función modifique la variable original?

Paso por referencia, con punteros

Para que una función modifique una variable del que la llama, le pasas la dirección de la variable con & y la función recibe un puntero. Dentro de la función usas * para acceder al valor y modificarlo:

void doblar(int *n) {
    *n = *n * 2;    /* modifica el valor en la dirección de n */
}

int main(void) {
    int x = 5;
    doblar(&x);                    /* pasamos la dirección de x */
    printf("Después de doblar: %d\n", x);    /* → 10 */
    return 0;
}
Después de doblar: 10

La diferencia clave:

Paso por valor:      doblar(x)   → la función recibe 5 (una copia)
Paso por referencia: doblar(&x)  → la función recibe la dirección de x

Dentro de la función:

int *n    → n es un puntero — guarda la dirección de x
*n        → el valor en esa dirección — el valor de x
*n = 10   → modifica el valor en esa dirección — modifica x

El ejemplo más clásico de paso por referencia es una función intercambiar que necesita modificar dos variables:

void intercambiar(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

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

    printf("Antes:  x=%d, y=%d\n", x, y);
    intercambiar(&x, &y);
    printf("Después: x=%d, y=%d\n", x, y);

    return 0;
}
Antes:  x=10, y=25
Después: x=25, y=10

Con paso por valor esto sería imposible, la función solo recibiría copias de 10 y 25 y no podría tocar las variables reales.

Comparativa visual — valor vs referencia

PASO POR VALOR:
main:  x = 5        ──copia──►  función: n = 5
                                          n = 10  (modifica la copia)
       x sigue = 5  ◄──────────────────────────── (x no cambia)

PASO POR REFERENCIA:
main:  x = 5        ──dirección──►  función: *n apunta a x
       x = 10       ◄──*n = 10────────────────── (modifica el original)

Puedes copiar cualquiera de los códigos de este artículo y vizualizar en pythontutor.com como va cambiando la memoria

Paso de arrays a funciones

Los arrays son el único caso donde C hace algo diferente al paso por valor automáticamente. Cuando pasas un array a una función, C pasa automáticamente la dirección del primer elemento, no una copia del array. Eso significa que la función puede modificar el array original sin necesitar &.

void rellenar(int vec[], int n) {
    int i;
    for (i = 0; i < n; i++) {
        vec[i] = i * 10;    /* modifica el array original */
    }
}

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

int main(void) {
    int numeros[5];

    rellenar(numeros, 5);
    mostrar(numeros, 5);    /* → 0 10 20 30 40 */

    return 0;
}
0 10 20 30 40

La firma int vec[] en los parámetros es equivalente a int *vec, ambas dicen «recibo un puntero al primer elemento del array». Por eso siempre hay que pasar también el tamaño n como parámetro separado, la función no tiene forma de saber cuántos elementos tiene el array solo con el puntero.

c

/* Estas dos firmas son equivalentes en C */
void mostrar(int vec[], int n) { ... }
void mostrar(int *vec, int n) { ... }

En IC2 verás las dos formas. La primera vec[] es más legible, deja claro que esperas un array. La segunda *vec es más explícita sobre lo que realmente pasa por debajo.

Un programa completo con todo junto

#include <stdio.h>

/* Prototipos */
int maximo_array(int vec[], int n);
int minimo_array(int vec[], int n);
float media_array(int vec[], int n);
void ordenar_array(int vec[], int n);
void mostrar_array(int vec[], int n);
void intercambiar(int *a, int *b);

int main(void) {
    int numeros[8] = {42, 7, 19, 3, 85, 26, 11, 54};
    int n = 8;

    printf("Array original: ");
    mostrar_array(numeros, n);

    printf("Máximo: %d\n", maximo_array(numeros, n));
    printf("Mínimo: %d\n", minimo_array(numeros, n));
    printf("Media:  %.2f\n", media_array(numeros, n));

    ordenar_array(numeros, n);
    printf("Array ordenado: ");
    mostrar_array(numeros, n);

    return 0;
}

int maximo_array(int vec[], int n) {
    int i, max = vec[0];
    for (i = 1; i < n; i++) {
        if (vec[i] > max) max = vec[i];
    }
    return max;
}

int minimo_array(int vec[], int n) {
    int i, min = vec[0];
    for (i = 1; i < n; i++) {
        if (vec[i] < min) min = vec[i];
    }
    return min;
}

float media_array(int vec[], int n) {
    int i, suma = 0;
    for (i = 0; i < n; i++) {
        suma += vec[i];
    }
    return (float)suma / n;
}

void intercambiar(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

void ordenar_array(int vec[], int n) {
    int i, j;
    for (i = 0; i < n - 1; i++) {
        for (j = 0; j < n - i - 1; j++) {
            if (vec[j] > vec[j + 1]) {
                intercambiar(&vec[j], &vec[j + 1]);
            }
        }
    }
}

void mostrar_array(int vec[], int n) {
    int i;
    for (i = 0; i < n; i++) {
        printf("%d ", vec[i]);
    }
    printf("\n");
}
Array original: 42 7 19 3 85 26 11 54
Máximo: 85
Mínimo: 3
Media:  30.88
Array ordenado: 3 7 11 19 26 42 54 85

Fíjate en cómo ordenar_array llama a intercambiar con &vec[j], aunque vec ya es un puntero, vec[j] es un valor entero, así que necesitas & para pasar su dirección a intercambiar.

Resumen rápido

/* ESTRUCTURA DE UNA FUNCIÓN */
tipo_retorno nombre(tipo param1, tipo param2) {
    /* cuerpo */
    return valor;    /* si no es void */
}

/* VOID — función sin retorno */
void imprimir(int n) {
    printf("%d\n", n);
    /* sin return, o return; sin valor */
}

/* VOID — función sin parámetros */
int leer(void) {
    int n;
    scanf("%d", &n);
    return n;
}

/* PROTOTIPO — declaración antes de main */
int sumar(int a, int b);    /* el compilador necesita esto si la */
                            /* definición viene después de main */

/* PASO POR VALOR — recibe copia, no modifica original */
void doblar_valor(int n) {
    n = n * 2;    /* solo modifica la copia */
}
doblar_valor(x);    /* x no cambia */

/* PASO POR REFERENCIA — recibe dirección, modifica original */
void doblar_ref(int *n) {
    *n = *n * 2;    /* modifica el valor en la dirección */
}
doblar_ref(&x);     /* x sí cambia — pasamos la dirección */

/* PASO DE ARRAYS — automáticamente por referencia */
void modificar(int vec[], int n) {
    vec[0] = 99;    /* modifica el array original */
}
modificar(numeros, 5);    /* sin & — los arrays ya pasan su dirección */

/* REGLA PRÁCTICA */
/* ¿La función necesita MODIFICAR la variable? → paso por referencia (*) */
/* ¿La función solo necesita LEER la variable?  → paso por valor */
/* ¿Es un array? → siempre pasa la dirección automáticamente */

/* ERRORES TÍPICOS */
/* 1. Olvidar el prototipo cuando la definición va después de main */
/* 2. Pasar x cuando hay que pasar &x para modificar */
/* 3. Pasar &x cuando la función espera int, no int* */
/* 4. Olvidar pasar el tamaño n junto con el array */
/* 5. Usar return valor en una función void */

En el próximo artículo practicamos funciones en C con programas reales, incluyendo funciones que buscan, ordenan y calculan estadísticas sobre arrays.


Functions in C — declaration, void, pass by value and by reference from scratch

C functions let you divide a program into reusable blocks, just like Python, but with stricter rules. In IC2 you’ve been using functions without realising it, main is a function. This article covers everything: how to declare them, what void means, why prototypes exist, and the difference between pass by value and pass by reference — where everything you already know about pointers connects.

What is a C function?

# Python
def add(a, b):
    return a + b
/* C */
int add(int a, int b) {
    return a + b;
}

The differences: you must declare the return type (int) and each parameter’s type (int a, int b). In Python that was optional, in C it’s mandatory.

General structure:

return_type function_name(type param1, type param2) {
    /* body */
    return value;    /* if return_type is not void */
}

What is void — the type that confuses most

void is the type you use when a function returns nothing or takes no parameters, C’s way of saying «this slot is empty.»

void as return type — function does something but returns no value:

void print_greeting(char *name) {
    printf("Hello, %s\n", name);
    /* no return needed */
}

void as parameter list — function takes no parameters:

int read_number(void) {
    int n;
    scanf("%d", &n);
    return n;
}

How to declare and call a function correctly

A C function has three parts: the definition (full body), the prototype (advance declaration), and the call (using it).

#include <stdio.h>

/* PROTOTYPE — declaration before main */
int add(int a, int b);
void print_result(int result);

int main(void) {
    int x = 5, y = 3, r;
    r = add(x, y);           /* CALL */
    print_result(r);         /* CALL */
    return 0;
}

/* DEFINITION — the actual body */
int add(int a, int b) {
    return a + b;
}

void print_result(int result) {
    printf("The result is: %d\n", result);
}

Why prototypes exist — the real reason

C’s compiler reads code top to bottom. When it reaches add(x, y) in main, it needs to know: how many parameters does add take? What types? What does it return? If add‘s definition comes after main, the compiler hasn’t seen it yet, error.

The prototype is the solution: you tell the compiler «this function exists, here’s its signature, the body comes later.»

/* WITHOUT prototype — error if definition comes after main */
int main(void) {
    int r = add(5, 3);    /* error: function not declared */
    return 0;
}

int add(int a, int b) { return a + b; }
/* WITH prototype — works correctly */
int add(int a, int b);    /* prototype: just the signature */

int main(void) {
    int r = add(5, 3);    /* compiler already knows what add is */
    return 0;
}

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

Functions with return value

float average(int a, int b, int c) {
    return (float)(a + b + c) / 3;
}

int maximum(int a, int b) {
    if (a > b) return a;
    return b;
}

int is_even(int n) {
    return n % 2 == 0;    /* returns 1 (true) or 0 (false) */
}

The (float) cast matters — without it (4 + 7 + 10) / 3 does integer division and gives 7, not 7.0.

Functions without return value — void

void print_separator(int length) {
    int i;
    for (i = 0; i < length; i++) printf("-");
    printf("\n");
}

void show_table(int n) {
    int i;
    printf("Table of %d:\n", n);
    print_separator(20);
    for (i = 1; i <= 10; i++)
        printf("  %d x %d = %d\n", n, i, n * i);
    print_separator(20);
}

Pass by value — the copy

By default C passes arguments by value — the function receives a copy, not the original. Modifications inside the function don’t affect the original variables:

void try_double(int n) {
    n = n * 2;    /* modifies the copy */
    printf("Inside: %d\n", n);
}

int main(void) {
    int x = 5;
    try_double(x);
    printf("After call: %d\n", x);    /* x is still 5 */
    return 0;
}
Inside: 10
After call: 5

Pass by reference — with pointers

To let a function modify the caller’s variable, pass its address with & and the function receives a pointer. Inside, use * to access and modify the value:

void double_it(int *n) {
    *n = *n * 2;    /* modifies the value at address n */
}

int main(void) {
    int x = 5;
    double_it(&x);                   /* pass address of x */
    printf("After doubling: %d\n", x);    /* → 10 */
    return 0;
}

The classic example — swapping two variables:

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(void) {
    int x = 10, y = 25;
    printf("Before: x=%d, y=%d\n", x, y);
    swap(&x, &y);
    printf("After:  x=%d, y=%d\n", x, y);
    return 0;
}
Before: x=10, y=25
After:  x=25, y=10

Visual comparison — value vs reference

PASS BY VALUE:
main:  x = 5       ──copy──►  function: n = 5
                                         n = 10  (modifies copy)
       x still = 5  ◄───────────────────────── (x unchanged)

PASS BY REFERENCE:
main:  x = 5       ──address──►  function: *n points to x
       x = 10       ◄──*n = 10────────────── (modifies original)

Passing arrays to functions

Arrays are the one case where C does something different automatically. When you pass an array, C automatically passes the address of the first element, not a copy. The function can modify the original without needing &:

void fill(int vec[], int n) {
    int i;
    for (i = 0; i < n; i++)
        vec[i] = i * 10;    /* modifies original array */
}

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

int main(void) {
    int numbers[5];
    fill(numbers, 5);
    display(numbers, 5);    /* → 0 10 20 30 40 */
    return 0;
}

int vec[] and int *vec are equivalent in parameters — both say «I receive a pointer to the first element.» Always pass the size n separately — the function can’t know the array size from the pointer alone.

Complete program with everything together

(Same as Spanish version above)

Quick summary

/* FUNCTION STRUCTURE */
return_type name(type param1, type param2) {
    /* body */
    return value;    /* if not void */
}

/* VOID — no return value */
void print(int n) { printf("%d\n", n); }

/* VOID — no parameters */
int read(void) { int n; scanf("%d", &n); return n; }

/* PROTOTYPE */
int add(int a, int b);    /* needed if definition comes after main */

/* PASS BY VALUE — receives copy, doesn't modify original */
void double_val(int n) { n = n * 2; }
double_val(x);    /* x unchanged */

/* PASS BY REFERENCE — receives address, modifies original */
void double_ref(int *n) { *n = *n * 2; }
double_ref(&x);   /* x changes — we pass the address */

/* ARRAYS — automatically pass by reference */
void modify(int vec[], int n) { vec[0] = 99; }
modify(numbers, 5);    /* no & needed */

/* PRACTICAL RULE */
/* Function needs to MODIFY the variable? → pass by reference (*) */
/* Function only needs to READ the variable? → pass by value */
/* Is it an array? → address passed automatically */

/* COMMON MISTAKES */
/* 1. Forgetting prototype when definition comes after main */
/* 2. Passing x when &x is needed to modify */
/* 3. Passing &x when function expects int, not int* */
/* 4. Forgetting to pass size n alongside the array */
/* 5. Using return value in a void function */

Publicaciones Similares

Deja una respuesta

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