Funciones en C — teoría con prototipos, void y paso por valor y por referencia cobrando el billete de la guagua (y el saldo que no bajaba)

Mi primera función «de verdad» en C cobraba un viaje del bono de la guagua. La llamé dos veces, imprimí el saldo y seguía en 10 euros exactos. Repasé la resta, el printf y hasta el precio del billete antes de entender lo que pasaba: la función había cobrado los viajes a una copia del saldo. En C, salvo que tú digas lo contrario, cada función trabaja con copias.

Este artículo va de eso y de todo lo que lo rodea: cómo se escribe una función en C, qué significa exactamente void (la duda que más se busca del tema), por qué existe el prototipo y por qué gcc 14 ya no te perdona olvidarlo, cómo se devuelve un resultado, qué es la pila de llamadas y, sobre todo, la diferencia entre paso por valor y paso por referencia, que es donde se juntan las funciones con lo que ya sabes de punteros en C. El ejemplo guía sigue siendo la guagua: billetes por zonas, el saldo del bono en céntimos y los viajeros de cada parada. Todas las salidas son de una ejecución real con gcc 14.

Qué es una función en C

Una función es un bloque de código con nombre que hace una tarea concreta y se puede usar desde cualquier parte del programa. main ya es una función: la que el sistema llama al arrancar. Si vienes de Funciones en Python, la idea es la misma:

def precio_billete(zonas):
    return 155 + (zonas - 1) * 20

En C, la misma función necesita decir el tipo de lo que devuelve y el de cada parámetro:

#include <stdio.h>

/* Devuelve el precio del billete en céntimos según las zonas */
int precio_billete(int zonas) {
    return 155 + (zonas - 1) * 20;      /* 1,55 euros y 20 céntimos por zona extra */
}

int main(void) {
    int precio = precio_billete(3);     /* LLAMADA: 3 entra como zonas */
    printf("Billete de 3 zonas: %d céntimos\n", precio);
    printf("Billete de 1 zona:  %d céntimos\n", precio_billete(1));
    return 0;
}

Salida:

Billete de 3 zonas: 195 céntimos
Billete de 1 zona:  155 céntimos

Las piezas de una función, de izquierda a derecha:

PiezaEn el ejemploPara qué sirve
Tipo de retornointqué tipo de valor devuelve
Nombreprecio_billetecómo se la llama
Parámetros(int zonas)qué recibe, cada uno con su tipo
Cuerpo{ return …; }lo que hace

Cuando main escribe precio_billete(3), el 3 se copia en el parámetro zonas, la función calcula 155 + 2 × 20 = 195 y el return devuelve ese número al sitio de la llamada, donde se guarda en precio. Con 1 zona, 155 + 0 = 155.

En Python los tipos son opcionales; en C son obligatorios. Si una función devuelve int y escribes return 1.95;, C convierte el resultado a 1 sin preguntar. Por eso el precio va en céntimos: con enteros no hay decimales que perder.

Qué es void en C: el tipo que más confunde

void significa «nada». Es la forma de C de decir que un hueco está vacío, y aparece en dos sitios distintos con dos significados distintos:

  • Como tipo de retorno: la función hace algo, pero no devuelve ningún valor. No lleva return valor; (como mucho un return; sin nada para salir antes).
  • Como lista de parámetros: la función no recibe nada. int primera_salida(void) se llama así: primera_salida().

Las dos juntas en un programa:

#include <stdio.h>

/* void como tipo de retorno: hace algo y no devuelve nada */
void separador(int largo) {
    for (int i = 0; i < largo; i++)
        printf("-");
    printf("\n");
}

/* void en los parámetros: no recibe nada */
int primera_salida(void) {
    return 610;                          /* 6:10, la primera guagua */
}

/* void en las dos partes */
void cabecera(void) {
    separador(26);
    printf("LÍNEA 201  San Telmo-Teror\n");
    separador(26);
}

int main(void) {
    cabecera();
    printf("Primera salida: %d\n", primera_salida());
    return 0;
}

Salida:

--------------------------
LÍNEA 201  San Telmo-Teror
--------------------------
Primera salida: 610

separador dibuja la línea y no devuelve nada (void delante); primera_salida no recibe nada (void entre paréntesis) y devuelve 610; cabecera ni recibe ni devuelve: solo llama a otras dos funciones. Fíjate en que una función void no se puede usar como valor: int x = separador(26); no compila, porque no hay nada que guardar.

¿Y los paréntesis vacíos, int primera_salida()? En C11, que es el estándar de IC2, unos paréntesis vacíos en una definición significan «no digo qué parámetros tiene», no «no tiene parámetros». El compilador no comprobará que la llames sin argumentos. Con (void) sí lo comprueba. Desde C23 las dos formas significan lo mismo, pero en la asignatura se compila con -std=c11: mejor (void).

Lee la declaración de izquierda a derecha: void cabecera(void) es «no devuelve nada, se llama cabecera, no recibe nada». Si delante hay un tipo distinto de void, la función tiene que acabar con un return de ese tipo.

void main: lo que verás en los apuntes

En los apuntes de IC2 muchos ejemplos empiezan por void main(). ¿Está bien? gcc lo compila, pero avisa:

#include <stdio.h>

void main(void) {                        /* como en los apuntes */
    printf("La guagua sale a las 6:10\n");
}
void_main.c:3: warning: return type of 'main' is not 'int' [-Wmain]
La guagua sale a las 6:10

El estándar de C dice que main devuelve int: ese número es el código de salida que recibe la terminal (el echo $? de Linux), y 0 significa «todo bien». Con void main no devuelves nada y el código de salida queda al azar: en mi ejecución el programa terminó con 26 (casi seguro, el valor que dejó printf al escribir sus 26 caracteres). Un script que compruebe si el programa ha ido bien lo daría por fallido. En tus programas, siempre int main(void) y return 0;.

Cómo declarar y llamar una función correctamente

Una función tiene tres momentos: la definición (la firma con su cuerpo), el prototipo (solo la firma, terminada en punto y coma) y la llamada (usarla). El orden en que aparecen en el archivo importa, porque el compilador lee el programa de arriba abajo una sola vez.

Mira qué pasa si main llama a una función que está definida más abajo y no hay prototipo:

#include <stdio.h>

int main(void) {
    printf("%d\n", precio_billete(3));  /* el compilador aún no la conoce */
    return 0;
}

int precio_billete(int zonas) {
    return 155 + (zonas - 1) * 20;
}
sin_prototipo.c: In function 'main':
sin_prototipo.c:4: error: implicit declaration of function 'precio_billete' [-Wimplicit-function-declaration]

«implicit declaration»: cuando gcc llega a la línea 4 todavía no sabe qué es precio_billete. En compiladores antiguos esto era solo un aviso y C se inventaba que la función devolvía int; desde gcc 14 (el de Fedora) es un error y el programa no compila. Hay dos arreglos: poner la función entera antes de main, o poner su prototipo arriba.

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

El prototipo le dice al compilador «esta función existe, recibe esto y devuelve aquello; el cuerpo viene después». Con eso basta para comprobar cada llamada:

#include <stdio.h>

/* PROTOTIPOS: la firma, terminada en ; */
int precio_billete(int zonas);
void imprimir_ticket(int zonas, int precio);

int main(void) {
    for (int z = 1; z <= 3; z++)
        imprimir_ticket(z, precio_billete(z));   /* una función dentro de otra */
    return 0;
}

/* DEFINICIONES: después de main, con su cuerpo */
int precio_billete(int zonas) {
    return 155 + (zonas - 1) * 20;
}

void imprimir_ticket(int zonas, int precio) {
    printf("Zonas: %d | %d,%02d euros\n", zonas, precio / 100, precio % 100);
}

Salida:

Zonas: 1 | 1,55 euros
Zonas: 2 | 1,75 euros
Zonas: 3 | 1,95 euros
Por qué hace falta el prototipoDos programas: a la izquierda main llama a una función definida más abajo y gcc da error; a la derecha el prototipo arriba anuncia la firma y compila.Sin prototipoint main(void) { precio_billete(3);}int precio_billete(int z) {…}Con prototipoint precio_billete(int z);int main(void) { precio_billete(3);}int precio_billete(int z) {…}«implicit declaration»: aún no la conocela firma ya está arriba: compilaEl compilador lee de arriba abajo, una sola vez
El prototipo es una promesa: «esta función existe y tiene esta firma; el cuerpo viene después».

Es el estilo de los laboratorios de IC2: prototipos arriba, main en medio, definiciones al final. Así main queda a la vista y se lee como un índice del programa. Y cuando el programa crezca y lo partas en varios archivos, los prototipos son exactamente lo que va en el .h.

Dos detalles de este programa: imprimir_ticket(z, precio_billete(z)) usa el resultado de una función como argumento de otra (primero se calcula el precio y luego se imprime), y %02d rellena con un cero para que 5 céntimos salgan como 05: 175 / 100 = 1 euro y 175 % 100 = 75 céntimos.

Que el prototipo y la definición no coincidan (int precio_billete(int zonas); arriba y double precio_billete(int zonas) abajo). gcc lo detecta como «conflicting types». El prototipo es un contrato: la definición tiene que cumplirlo letra a letra.

Funciones con retorno: return

return hace dos cosas a la vez: termina la función en ese mismo instante y entrega el valor a quien la llamó. Una función puede tener varios return, pero en cada llamada solo se ejecuta uno:

#include <stdio.h>

double media(int a, int b, int c) {
    return (a + b + c) / 3.0;            /* 3.0 hace que la división sea decimal */
}

int maximo(int a, int b) {
    if (a > b)
        return a;                        /* sale aquí si a es mayor */
    return b;                            /* si no, llega hasta aquí */
}

int bono_joven(int edad) {
    return edad >= 14 && edad <= 30;     /* 1 (verdadero) o 0 (falso) */
}

int main(void) {
    printf("Media de viajeros: %.2f\n", media(12, 7, 9));
    printf("Parada con más:    %d\n", maximo(12, 15));
    printf("¿Bono joven a los 19? %d\n", bono_joven(19));
    printf("¿Bono joven a los 45? %d\n", bono_joven(45));
    return 0;
}

Salida:

Media de viajeros: 9.33
Parada con más:    15
¿Bono joven a los 19? 1
¿Bono joven a los 45? 0
  • media divide entre 3.0 y no entre 3: con tres int, (12 + 7 + 9) / 3 daría 9 (división entera); con 3.0 da 28 / 3 = 9,33.
  • maximo tiene dos salidas: si a > b, el primer return la termina y el segundo ni se mira. Con (12, 15), 12 > 15 es falso y llega al return b.
  • bono_joven devuelve directamente el resultado de una comparación: en C es un int que vale 1 (verdadero) o 0 (falso). Con 19 años, 19 >= 14 y 19 <= 30: 1. Con 45: 0.

Una función que no es void y en algún camino se queda sin return. gcc avisa con control reaches end of non-void function y, si ese camino se ejecuta, la función devuelve basura. Lo verás en «¿Qué falla aquí?» de los ejercicios, con un billete de 3 zonas que costaba 3 céntimos.

Funciones sin retorno: void

Cuando lo que importa es lo que la función hace (imprimir, modificar un vector) y no un valor, el tipo de retorno es void. Ya has visto separador, cabecera e imprimir_ticket. Una función void puede usar return; sin valor para terminar antes, por ejemplo al detectar un dato no válido:

void imprimir_ticket(int zonas, int precio) {
    if (zonas < 1)
        return;                 /* nada que imprimir: salgo ya */
    printf("Zonas: %d | %d,%02d euros\n", zonas, precio / 100, precio % 100);
}

Y al revés: si la función tiene que avisar de si algo ha ido bien o mal, no la hagas void; que devuelva un int con 1 o 0. Es lo que hace la función cobrar un poco más abajo.

Paso por valor: la copia

Aquí está la anécdota del principio. Cuando llamas a una función, C copia cada argumento en su parámetro. La función trabaja con esa copia, y lo que haga con ella no llega a las variables de quien la llamó. Imprimiendo las direcciones se ve con claridad:

#include <stdio.h>

void cobrar_copia(int saldo, int precio) {
    printf("  dentro: saldo vive en %p\n", (void *)&saldo);
    saldo = saldo - precio;               /* cambia la copia */
    printf("  dentro: saldo = %d\n", saldo);
}

int main(void) {
    int saldo = 1000;
    printf("main:   saldo vive en %p\n", (void *)&saldo);
    cobrar_copia(saldo, 155);
    printf("main:   saldo = %d\n", saldo);   /* sigue igual */
    return 0;
}

Salida:

main:   saldo vive en 0x7ffc212aef64
  dentro: saldo vive en 0x7ffc212aef4c
  dentro: saldo = 845
main:   saldo = 1000

El saldo de main vive en …ef64; el saldo de la función, en …ef4c. Son dos variables distintas que se llaman igual. Dentro, la copia baja a 845; fuera, main sigue con 1000. Es lo mismo que pasa en Python cuando le pasas un número a una función y lo cambias dentro.

El paso por valor es lo normal y es seguro: una función no puede estropear tus variables por accidente. Pero a veces es justo lo que quieres que haga.

Paso por referencia, con punteros

C no tiene paso por referencia «de verdad», como C++; tiene algo que hace lo mismo: pasar la dirección de la variable. La función recibe un puntero y, con *, escribe en la variable original. Es exactamente lo que viste en el swap de la práctica de punteros.

Esta versión de cobrar combina las dos cosas: el saldo cambia a través del puntero y el return dice si se ha podido cobrar:

#include <stdio.h>

/* Cobra si hay saldo: devuelve 1 si pudo cobrar y 0 si no.
   El saldo cambia a través del puntero; el éxito sale por return. */
int cobrar(int *saldo, int precio) {
    if (*saldo < precio)
        return 0;                         /* no toco nada */
    *saldo = *saldo - precio;             /* cambio la variable de main */
    return 1;
}

int main(void) {
    int saldo = 400;
    int viaje = 1;
    while (cobrar(&saldo, 155)) {         /* &saldo: la dirección, no el valor */
        printf("Viaje %d pagado, quedan %d céntimos\n", viaje, saldo);
        viaje++;
    }
    printf("Sin saldo para el viaje %d (quedan %d)\n", viaje, saldo);
    return 0;
}

Salida:

Viaje 1 pagado, quedan 245 céntimos
Viaje 2 pagado, quedan 90 céntimos
Sin saldo para el viaje 3 (quedan 90)

Vuelta a vuelta: 400 − 155 = 245, 245 − 155 = 90, y con 90 céntimos ya no hay para otro viaje: cobrar devuelve 0 sin tocar el saldo, el while termina y el programa dice que el viaje 3 no se puede pagar.

Paso por valorPaso por referencia
Parámetroint saldoint *saldo
Llamadacobrar_copia(saldo, 155)cobrar(&saldo, 155)
Dentro se usasaldo*saldo
¿Cambia la variable de main?NoSí
Úsalo cuandola función solo necesita el datola función tiene que modificarlo o devolver más de un resultado

Olvidar el & en la llamada: cobrar(saldo, 155). Con gcc 14 es un error de compilación (pasas un int donde se espera un int *); con compiladores antiguos compilaba y la función escribía en la dirección 400, con un Segmentation fault casi seguro.

Comparativa visual: valor frente a referencia

Con las direcciones reales de la ejecución anterior, este es el dibujo que conviene tener en la cabeza:

Paso por valor frente a paso por referenciaArriba, cobrar_copia recibe una copia del saldo en otra dirección y la cambia sin efecto en main. Abajo, cobrar recibe la dirección del saldo de main y lo modifica a través del puntero.mainsaldo = 1000…ef64cobrar_copia(saldo, 155)saldo = 1000 → 845copia en …ef4ccopiacobrar(&saldo, 155)int *saldo = …ef64*saldo = *saldo – 155main sigue en 1000 tras la copia; con el puntero baja de verdad
Por valor viaja el número; por referencia viaja la dirección.

Arriba, la copia vive en otra casilla y muere con la función; abajo, la función guarda la dirección de la casilla de main y escribe en ella. Si quieres verlo moverse, el programa de la sección de Python Tutor hace las dos llamadas seguidas.

Paso de arrays a funciones

Los vectores son el caso especial. Cuando pasas un vector a una función, C no copia las casillas: pasa la dirección de la primera (lo que ya sabes de punteros: el nombre del vector se convierte en esa dirección). Consecuencias:

  1. La función puede modificar el vector original sin necesidad de &.
  2. int v[] en un parámetro es exactamente lo mismo que int *v.
  3. La función no sabe cuántas casillas tiene el vector: hay que pasarle el tamaño aparte.

El tercer punto es una trampa clásica, y gcc la vigila:

#include <stdio.h>

/* int v[] en un parámetro es lo mismo que int *v */
int total_subidas(int v[], int n) {
    printf("  sizeof v en la función = %zu\n", sizeof v);
    int total = 0;
    for (int i = 0; i < n; i++)
        total += v[i];
    return total;
}

void duplicar(int *v, int n) {            /* cambia el vector de main */
    for (int i = 0; i < n; i++)
        v[i] = v[i] * 2;
}

int main(void) {
    int subidas[5] = {12, 7, 9, 4, 15};
    printf("sizeof subidas en main = %zu\n", sizeof subidas);
    printf("Total: %d\n", total_subidas(subidas, 5));
    duplicar(subidas, 5);                 /* sin &: el nombre ya es una dirección */
    printf("Tras duplicar, subidas[0] = %d\n", subidas[0]);
    return 0;
}
arrays.c: In function 'total_subidas':
arrays.c:5: warning: 'sizeof' on array function parameter 'v' will return size of 'int *' [-Wsizeof-array-argument]
arrays.c:4: note: declared here

Salida:

sizeof subidas en main = 20
  sizeof v en la función = 8
Total: 47
Tras duplicar, subidas[0] = 24

En main, sizeof subidas da 20 (5 enteros de 4 bytes). Dentro de la función, sizeof v da 8: el tamaño de un puntero. Por eso el aviso dice que sizeof sobre un parámetro vector «devolverá el tamaño de int *». El total sí es correcto, 12 + 7 + 9 + 4 + 15 = 47, porque se recorre con la n que le pasamos. Y duplicar cambia el vector de main: la casilla 0 pasa de 12 a 24 sin & y sin return.

Si una función recibe un vector que no debe modificar, decláralo const int v[]. Si por error escribes v[i] = … dentro, gcc da error. Es una forma barata de dejar claro qué funciones leen y cuáles escriben.

La pila de llamadas y las variables locales

Cada vez que llamas a una función, el programa reserva para ella un marco en la pila: ahí viven sus parámetros y sus variables locales. Cuando la función hace return, el marco se libera y esas variables dejan de existir. Por eso dos funciones pueden tener una variable total cada una sin pisarse.

Con las direcciones de una variable de cada marco se ve cómo se apilan:

#include <stdio.h>

double media(int total, int n) {
    double m = (double)total / n;
    printf("    media:   m       en %p\n", (void *)&m);
    return m;
}

void resumen(int *v, int n) {
    int total = 0;
    printf("  resumen:   total   en %p\n", (void *)&total);
    for (int i = 0; i < n; i++)
        total += v[i];
    printf("  resultado: %.2f\n", media(total, n));
}

int main(void) {
    int subidas[4] = {12, 7, 9, 4};
    printf("main:        subidas en %p\n", (void *)subidas);
    resumen(subidas, 4);
    return 0;
}

Salida:

main:        subidas en 0x7ffe57bcaf00
  resumen:   total   en 0x7ffe57bcaee0
    media:   m       en 0x7ffe57bcaeb0
  resultado: 8.00

main llama a resumen y resumen llama a media: tres marcos vivos a la vez. Las direcciones bajan (…af00, …aee0, …aeb0): en Linux la pila crece hacia direcciones más pequeñas. La media de 12, 7, 9 y 4 es 32 / 4 = 8,00.

La pila de llamadasTres marcos apilados: main, resumen y media, cada uno con su variable local y su dirección real. Las direcciones bajan con cada llamada.mainsubidas[4] …af00resumentotal …aee0mediam …aeb0la pila crece↓ llama a↓ llama aAl hacer return, el marco de arriba desaparece con sus variables
Cada llamada abre un marco nuevo más abajo en la memoria; el return lo cierra.

Esto explica dos errores que verás pronto: devolver la dirección de una variable local (su marco ya no existe cuando la usas, como en los ejercicios de punteros) y una recursión sin fin, que apila marcos hasta que la pila se acaba.

Variables globales: las de los apuntes

Una variable declarada fuera de todas las funciones es global: la ven y la modifican todas. Los apuntes de IC2 tienen un ejemplo con dos funciones, F1 y F2, que suman y restan sobre una variable global R. Este es el mismo programa con la guagua:

#include <stdio.h>
#define N 5              /* paradas en las que sube un viajero */
#define M 2              /* paradas en las que bajan dos */

int dentro = 0;          /* GLOBAL: la ven todas las funciones */

void suben(void) {
    for (int i = 0; i < N; i++) {
        dentro++;
        printf("Suben: %d\n", dentro);
    }
}

void bajan(void) {
    for (int i = 0; i < M; i++) {
        dentro = dentro - 2;
        printf("Bajan: %d\n", dentro);
    }
}

int main(void) {
    suben();             /* no pasamos nada: usan la global */
    bajan();
    printf("Al final: %d viajeros\n", dentro);
    return 0;
}

Salida:

Suben: 1
Suben: 2
Suben: 3
Suben: 4
Suben: 5
Bajan: 3
Bajan: 1
Al final: 1 viajeros

Funciona: 5 viajeros suben de uno en uno y luego bajan dos, dos veces (5 − 2 − 2 = 1). Pero mira las llamadas: suben() y bajan() no dicen nada de lo que tocan. En un programa grande, una global que cualquiera puede cambiar es una fuente de errores difíciles de seguir (y en el tema de hilos verás que es justo la variable compartida que provoca las condiciones de carrera). La versión con funciones limpias sería void suben(int *dentro, int n): la firma dice qué modifica.

Las constantes con #define (como N y M) no son variables globales: el preprocesador las sustituye por su valor antes de compilar. Usarlas para tamaños y límites es buena práctica.

Un programa completo con todo junto

Prototipos arriba, main en medio, definiciones al final, funciones que devuelven un valor, funciones void que modifican el vector y una que llama a otra pasándole las direcciones de dos casillas:

#include <stdio.h>

/* Prototipos */
int maximo(int v[], int n);
int minimo(int v[], int n);
double media(int v[], int n);
void intercambiar(int *a, int *b);
void ordenar(int v[], int n);
void mostrar(const char *titulo, int v[], int n);

int main(void) {
    int viajeros[8] = {42, 7, 19, 3, 85, 26, 11, 54};   /* por parada, línea 201 */
    int n = 8;

    mostrar("Original: ", viajeros, n);
    printf("Máximo:   %d\n", maximo(viajeros, n));
    printf("Mínimo:   %d\n", minimo(viajeros, n));
    printf("Media:    %.2f\n", media(viajeros, n));
    ordenar(viajeros, n);                                /* cambia el original */
    mostrar("Ordenado: ", viajeros, n);
    return 0;
}

int maximo(int v[], int n) {
    int m = v[0];
    for (int i = 1; i < n; i++)
        if (v[i] > m) m = v[i];
    return m;
}

int minimo(int v[], int n) {
    int m = v[0];
    for (int i = 1; i < n; i++)
        if (v[i] < m) m = v[i];
    return m;
}

double media(int v[], int n) {
    int suma = 0;
    for (int i = 0; i < n; i++)
        suma += v[i];
    return (double)suma / n;
}

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

/* Burbuja: en cada pasada el mayor que queda sube al final */
void ordenar(int v[], int n) {
    for (int i = 0; i < n - 1; i++)
        for (int j = 0; j < n - 1 - i; j++)
            if (v[j] > v[j + 1])
                intercambiar(&v[j], &v[j + 1]);         /* & de cada casilla */
}

void mostrar(const char *titulo, int v[], int n) {
    printf("%s", titulo);
    for (int i = 0; i < n; i++)
        printf("%d ", v[i]);
    printf("\n");
}

Salida:

Original: 42 7 19 3 85 26 11 54 
Máximo:   85
Mínimo:   3
Media:    30.88
Ordenado: 3 7 11 19 26 42 54 85 

Comprobación: 42 + 7 + 19 + 3 + 85 + 26 + 11 + 54 = 247 viajeros, y 247 / 8 = 30,875, que %.2f redondea a 30,88. Lo más interesante es ordenar (el método de la burbuja): cuando dos casillas seguidas están desordenadas llama a intercambiar(&v[j], &v[j + 1]). Aunque v ya es un puntero, v[j] es un int, así que hace falta el & para pasar la dirección de esa casilla concreta.

Errores típicos con funciones

  1. Llamar a una función antes de declararla. implicit declaration: error en gcc 14. Prototipo arriba.
  2. Esperar que una función cambie una variable pasada por valor. Si tiene que modificarla, el parámetro es un puntero y la llamada lleva &.
  3. Olvidar el & en la llamada a una función que recibe punteros.
  4. Calcular el tamaño de un vector con sizeof dentro de la función. Pasa siempre n como parámetro.
  5. Un camino sin return en una función que devuelve algo.
  6. Usar el resultado de una función void, o ignorar el de una que sí devuelve algo importante (como el 1/0 de cobrar, o lo que devuelve scanf).
  7. void main en lugar de int main(void) con return 0;.

Míralo paso a paso en Python Tutor

Python Tutor dibuja la pila de llamadas: cada función en su propio marco, con sus variables, y los punteros como flechas. Elige C y pega este programa, que hace una llamada por valor y otra por referencia:

#include <stdio.h>

void cobrar_copia(int saldo, int precio) {
    saldo = saldo - precio;
}

int cobrar(int *saldo, int precio) {
    if (*saldo < precio) {
        return 0;
    }
    *saldo = *saldo - precio;
    return 1;
}

int main(void) {
    int saldo = 400;
    cobrar_copia(saldo, 155);
    int ok = cobrar(&saldo, 155);
    printf("ok = %d, saldo = %d\n", ok, saldo);
    return 0;
}

Qué observar paso a paso:

  1. Al entrar en cobrar_copia aparece un marco nuevo con su saldo (400) y precio. Hay dos variables saldo en pantalla, una en cada marco.
  2. En saldo = saldo - precio; cambia la del marco de cobrar_copia (245). La de main sigue en 400.
  3. Al salir, el marco desaparece entero y con él el 245.
  4. Al entrar en cobrar, su saldo ya no es un número: es una flecha que apunta al saldo de main.
  5. En *saldo = *saldo - precio; cambia la caja de main (de 400 a 245), y ok recibe el 1 del return. Debe imprimir ok = 1, saldo = 245.

Resumen

Quiero…CódigoEjemplo con la guagua
Una función que devuelve un valorint precio_billete(int zonas)195 céntimos con 3 zonas
Una función que no devuelve nadavoid separador(int largo)dibuja la línea
Una función sin parámetrosint primera_salida(void)610
Usarla antes de definirlaprototipo int precio_billete(int zonas); arribasin «implicit declaration»
Cambiar una variable de mainvoid cobrar(int *saldo, …) y cobrar(&saldo, …)el saldo baja de verdad
Devolver dos resultadosuno por return y otro por punterocobrar devuelve 1/0 y cambia el saldo
Pasar un vectorint v[] (o int *v) y n apartetotal_subidas(subidas, 5)
Un vector que no se tocaconst int v[]solo lectura

Tres ideas para quedarte:

  • Una función de C recibe copias: si tiene que modificar algo de quien la llama, recibe su dirección.
  • El compilador lee de arriba abajo: prototipos arriba, y void cuando no hay valor de retorno o no hay parámetros.
  • Un vector viaja como la dirección de su primera casilla: la función puede cambiarlo, pero no sabe su tamaño.

Ahora toca practicar con programas completos. En la práctica de funciones en C escribimos funciones de búsqueda que devuelven dos resultados, funciones que transforman vectores en su sitio y una calculadora de estadísticas de la línea 201 que lee los datos con scanf.

Siguiente paso: prácticaFunciones en C — práctica con 3 programas de la guagua: búsquedas con dos resultados, vectores que se transforman y las estadísticas de la línea (con scanf)

¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Sigue aprendiendo

Deja una respuesta

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