Funciones en C — declaración, void, paso por valor y por referencia desde cero
Tabla de Contenidos
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 sí 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.

2 comentarios