AulaML Python · C++
C++ principiante · 40 min · Funciones

Funciones: declaración, parámetros y retorno

Dividir un programa en piezas con nombre, entradas y una única salida

Una función agrupa instrucciones bajo un nombre, recibe datos por parámetros y devuelve un resultado. Aprenderás a declararla, definirla, llamarla y leer la pila de llamadas, además de distinguir prototipo, ámbito y paso por valor.

Lección 1 de 2 del módulo
Al terminar esta lección podrás
  • Declarar, definir y llamar funciones con el tipo de retorno correcto
  • Explicar la diferencia entre declaración, definición y prototipo
  • Pasar datos por valor y entender que la función trabaja con copias
  • Usar `void` y argumentos por defecto con criterio
  • Seguir una llamada en la pila de llamadas paso a paso

Antes necesitas: Bucles for, while y do-while

En esta lección
  1. ¿Qué es una función?
  2. ¿Para qué sirve?
  3. ¿Cómo funciona por dentro?
  4. Sintaxis
  5. Ejemplo básico: una función con dos parámetros
  6. Ejemplo intermedio: valores por defecto y void
  7. Caso práctico: separar un cálculo en funciones
  8. Visualización: la pila de llamadas
  9. Errores comunes
  10. Buenas prácticas
  11. Ejercicios
  12. Soluciones explicadas
  13. Resumen
  14. Evaluación

Hasta ahora todos tus programas viven dentro de main. Eso funciona con veinte líneas y se vuelve inmanejable con doscientas. Una función pone nombre a un fragmento de trabajo, declara qué datos necesita y qué devuelve, y permite usarlo desde varios sitios sin copiar el código.

¿Qué es una función?

Una función es un bloque de código con nombre que se ejecuta cuando la llamas. Tiene cuatro partes: un tipo de retorno, un nombre, una lista de parámetros entre paréntesis y un cuerpo entre llaves. Al llamarla, pasas argumentos, el cuerpo se ejecuta y, si hay retorno, obtienes un valor.

  • Tipo de retorno: qué valor entrega, como int o double; void si no entrega ninguno.
  • Nombre: un verbo que describe la acción, como calcularArea o esPrimo.
  • Parámetros: las entradas, cada una con su tipo; pueden ser cero.
  • Cuerpo: las sentencias que se ejecutan en cada llamada.

main también es una función, solo que especial: el sistema operativo la llama al iniciar el programa y su valor de retorno es el código de salida. Todo lo que has escrito hasta ahora era el cuerpo de una función.

¿Para qué sirve?

  • Evitar duplicación: si la misma fórmula aparece tres veces, un cambio futuro exige tocar una sola definición.
  • Nombrar la intención: calcularMedia(notas) se lee mejor que cinco líneas de suma y división.
  • Dividir problemas: resolver un cálculo grande como suma de cálculos pequeños y verificables.
  • Probar por partes: una función con entradas y salidas claras se puede comprobar con casos concretos.
  • Reutilizar: la misma función sirve en varios programas o en varias partes del mismo.

En Machine Learning cada operación es una función: la sigmoide, la pérdida, la normalización de un lote. En C++ esas piezas se escriben una vez y se llaman millones de veces; por eso importa que tengan un contrato claro y un nombre honesto.

¿Cómo funciona por dentro?

Al llamar a una función, el programa reserva un marco de pila con los parámetros, las variables locales y la dirección a la que debe volver. El control salta al cuerpo; cuando llega a return o al final, el marco se destruye y el valor de retorno vuelve a quien llamó. Esa pila de marcos es la pila de llamadas.

Los parámetros se pasan por valor por defecto: la función recibe copias. Si modificas un parámetro dentro, el argumento original no cambia. Esa independencia protege tus datos, pero implica una copia por llamada; en la siguiente lección verás cómo evitarla con referencias.

El ámbito de una variable local es el bloque donde se declara. Una variable creada dentro de una función no existe fuera, y otra función no puede verla. Si dos ámbitos declaran el mismo nombre, el más interno lo oculta: se llama shadowing y suele ser una fuente de confusión.

El compilador necesita conocer la función antes de la llamada. Una declaración o prototipo anuncia el nombre, los tipos y el retorno, y termina en punto y coma. La definición aporta el cuerpo. Puedes declarar arriba y definir abajo, algo imprescindible cuando dos funciones se llaman entre sí.

Cuando el programa crece, los prototipos se agrupan en archivos de cabecera .hpp que se incluyen con #include "miarchivo.hpp". Así todos los archivos comparten la misma declaración y el compilador detecta si una definición no coincide. La palabra inline permite definir una función pequeña en la cabecera sin romper la regla de una sola definición por programa.

Sintaxis

La forma canónica es tipo nombre(parámetros) { cuerpo }. Un prototipo repite la cabecera y añade ;. Una función void puede usar return; para salir antes, pero no devuelve ningún valor.

CPP sintaxis.cpp
#include <iostream>

int sumar(int a, int b);        // declaracion (prototipo)

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

void imprimir(int valor) {      // no devuelve nada
    std::cout << valor << '\n';
}

int main() {
    imprimir(sumar(2, 3));
    return 0;
}
  • Cada parámetro declara su tipo: int a, int b, no int a, b con dos tipos distintos.
  • La llamada pasa valores, no tipos: sumar(2, 3).
  • void significa «sin valor de retorno»; no se puede asignar a una variable.
  • Un prototipo sin definición compila pero falla al enlazar: el enlazador no encuentra el cuerpo.

Ejemplo básico: una función con dos parámetros

Este programa calcula el área de un rectángulo. La función recibe dos double, multiplica y devuelve el resultado. main solo se encarga de llamar e imprimir.

CPP area.cpp
#include <iostream>

double area(double base, double altura) {
    return base * altura;
}

int main() {
    double rectangulo = area(3.0, 4.0);
    std::cout << "Area: " << rectangulo << '\n';
    return 0;
}
  1. area se define antes de main, así que el compilador ya la conoce cuando aparece la llamada.
  2. base y altura son copias de 3.0 y 4.0; viven solo dentro del marco de area.
  3. return base * altura; calcula 12.0, destruye el marco y entrega el valor.
  4. double rectangulo = area(3.0, 4.0); guarda ese 12.0 en una variable local de main.

La salida es Area: 12. Si llamaras area(3, 4) con enteros, se convertirían a double sin problema porque la conversión no pierde información. Si el tipo de retorno fuera int, el 12.5 de area(2.5, 5.0) se truncaría a 12: el contrato incluye el tipo.

Ejemplo intermedio: valores por defecto y void

Un argumento por defecto se usa cuando la llamada no lo proporciona. Se declara una sola vez y debe ser el último de la lista: no puedes tener un parámetro obligatorio después de uno opcional.

CPP saludar.cpp
#include <iostream>
#include <string>

void saludar(std::string nombre, int veces = 1) {
    for (int i = 0; i < veces; ++i) {
        std::cout << "Hola, " << nombre << '\n';
    }
}

int main() {
    saludar("Ada");         // usa el valor por defecto
    saludar("Grace", 2);    // sobrescribe el valor por defecto
    return 0;
}

La primera llamada usa veces = 1 e imprime un saludo; la segunda pasa 2 e imprime dos. El valor por defecto no cambia el tipo ni el número de parámetros: es una comodidad para el llamador. Si la función se declara en una cabecera, el valor por defecto va en la declaración, no en la definición, para que todos los que la incluyan lo vean.

saludar devuelve void porque su efecto es escribir en la consola. Ese efecto se llama efecto secundario: no aparece en el tipo de retorno. Una función que solo devuelve un valor y no imprime nada es más fácil de probar; una que imprime es más fácil de ver funcionando. Elige según el uso.

Caso práctico: separar un cálculo en funciones

Este programa imprime los números primos menores que 30. La función esPrimo responde una pregunta de sí o no y main recorre el rango. Dividir así el trabajo permite probar esPrimo(7) sin ejecutar el bucle completo.

CPP primos.cpp
#include <iostream>

bool esPrimo(int n) {
    if (n < 2) {
        return false;
    }
    for (int d = 2; d * d <= n; ++d) {
        if (n % d == 0) {
            return false;
        }
    }
    return true;
}

int main() {
    int encontrados = 0;
    for (int n = 2; n < 30; ++n) {
        if (esPrimo(n)) {
            std::cout << n << ' ';
            ++encontrados;
        }
    }
    std::cout << "\nTotal: " << encontrados << '\n';
    return 0;
}

La condición d * d <= n se detiene en la raíz cuadrada sin llamar a std::sqrt: si n tuviera un divisor mayor que su raíz, tendría otro menor. La salida es 2 3 5 7 11 13 17 19 23 29 y Total: 10. Observa que esPrimo tiene varios return: el primero que se ejecuta termina la función de inmediato.

Visualización: la pila de llamadas

Sigue la llamada doble(3). Cada marco muestra sus variables; el marco superior es el que se está ejecutando. Fíjate en que x existe solo mientras doble está activa y en que r no recibe el valor hasta que la llamada termina.

Ejecución visual ·Una llamada, dos marcos
1 / 7
1 #include <iostream>
2
3 int doble(int x) {
4 return 2 * x;
5 }
6
7 int main() {
8 int r = doble(3);
9 std::cout << r << '\n';
10 return 0;
11 }
main empieza. Todavía no existe r: la inicialización espera al resultado de la llamada.
Pila de llamadas
main()
Variables

Sin variables en este paso.

Salida
—

Avanza paso a paso: observa la línea resaltada, las variables y la salida.

Errores comunes

Mensaje o síntomaCausaSolución
error: use of undeclared identifier cuadradoLlamas a la función antes de que el compilador la conozcaDefine antes o escribe un prototipo
error: conflicting declarationEl prototipo y la definición no coinciden en tipos o parámetrosCopia la cabecera exacta en ambos sitios
warning: no return statement in function returning non-voidUna función con retorno termina sin return en algún caminoDevuelve un valor en todas las ramas
error: too few arguments to functionLa llamada pasa menos argumentos de los que no tienen valor por defectoRevisa la firma o añade el argumento que falta

Un aviso de retorno faltante es más grave de lo que parece: si el flujo llega al final sin return, el valor es indeterminado. El compilador suele avisar con -Wall, y ese aviso debe tratarse como un error. También confunde declarar el parámetro int a, b esperando dos enteros: el segundo se declara sin tipo y el compilador lo rechaza.

Buenas prácticas

  • Una función, una responsabilidad: si el nombre necesita «y», probablemente sean dos funciones.
  • Usa verbos para las acciones (calcularMedia) y preguntas para los booleanos (esValido).
  • Devuelve un valor en lugar de imprimir cuando la función pueda reutilizarse en otro contexto.
  • Declara los prototipos en cabeceras cuando el programa crezca; así todos los archivos ven el mismo contrato.
  • Evita variables globales: pasan información oculta entre funciones y complican las pruebas.
  • Marca inline solo funciones muy pequeñas y en cabeceras; es una pista para el compilador, no una obligación.
Definir una función que suma
Python
PYTHON sumar.py
def sumar(a, b):
    return a + b

print(sumar(2, 3))
C++
CPP sumar.cpp
#include <iostream>

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

int main() {
    std::cout << sumar(2, 3) << '\n';
    return 0;
}

Python deduce los tipos al ejecutar y devuelve `None` si falta el retorno. C++ exige declarar el tipo de cada parámetro y el del retorno; una función que promete `int` debe devolver un entero en todos sus caminos. La ventaja es que el error aparece al compilar.

Ejercicios

Ejercicio 1

Conversión de temperatura

Escribe una función double celsiusAFahrenheit(double celsius) que aplique la fórmula F = C * 9 / 5 + 32. Llámala desde main para convertir 0, 25 y 100 grados y muestra los tres resultados. Verifica que 100 grados dan 212.
Pista

Usa literales double en la fórmula (9.0 / 5.0) para no perder la parte decimal.

Ejercicio 2

Número perfecto

Un número es perfecto si es igual a la suma de sus divisores propios (sin contarse a sí mismo). Escribe dos funciones: int sumaDivisores(int n) y bool esPerfecto(int n). Prueba con 6, 28 y 12 y muestra una línea por número indicando si es perfecto.
Pista

Recorre los posibles divisores de 1 a n - 1 con un for y acumula los que dejan resto cero. esPerfecto puede ser una sola línea que compare.

Completar código Completa la función
int  (int a, int b) {
     a + b;
}

int main() {
    std::cout << sumar(2, 3) << '\n';
}

El nombre debe coincidir con la llamada de `main`; la función promete devolver un entero.

Soluciones explicadas

En la conversión de temperatura, la función aísla una fórmula que podrías necesitar en otro programa. La decisión importante es usar 9.0 / 5.0: con enteros, 9 / 5 vale 1 y el resultado sería C + 32. La función no imprime nada, así que puedes probarla con cualquier valor sin llenar la pantalla.

En el número perfecto, la división del trabajo es el punto: sumaDivisores responde una pregunta numérica y esPerfecto responde una pregunta lógica. Podrías haber escrito todo dentro de main, pero entonces no podrías comprobar esPerfecto(28) de forma aislada. El operador ternario del main solo elige el texto; la lógica vive en las funciones.

En el ejercicio de completar, el nombre y el return son el contrato mínimo. Si el nombre no coincide, el enlazador no encuentra sumar; si falta el return, el valor de retorno es indeterminado y -Wall lo advierte. Una función sin ambas piezas es una promesa sin cumplir.

Resumen

  • Una función tiene tipo de retorno, nombre, parámetros y cuerpo.
  • Declaración (prototipo) anuncia; definición aporta el cuerpo; la llamada ejecuta.
  • Los parámetros se pasan por valor: la función recibe copias.
  • void indica que no hay valor de retorno; return termina la función de inmediato.
  • Los argumentos por defecto van al final de la lista y se declaran una sola vez.
  • Cada llamada crea un marco en la pila de llamadas y lo destruye al volver.
  • Una función debe devolver un valor en todos sus caminos cuando su tipo no es void.

Evaluación

Evaluación 1

¿Qué diferencia hay entre una declaración y una definición de función?

Evaluación 2

¿Qué imprime void f(int x) { x = 99; } si se llama con int a = 5; f(a); y después se imprime a?

Evaluación 3

¿Cuál de estas firmas es válida para un argumento por defecto?

Tus notas

¿Ya lo entendiste?

Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.