Funciones

Capítulo 8: Funciones

Las funciones son los bloques de construcción fundamentales de cualquier programa. En este capítulo aprenderás no solo cómo usarlas, sino cómo funcionan a nivel de memoria: el call stack, stack frames, paso de parámetros, y qué hace el CPU cuando llamas una función. Entender esto te permitirá escribir código más eficiente y debuggear problemas complejos.

Objetivos

Declaración vs definición

#include <iostream>

// Declaración (prototipo)
int suma(int a, int b);

int main() {
    int resultado = suma(5, 3);
    std::cout << "Resultado: " << resultado << "
";  // 8
    return 0;
}

// Definición
int suma(int a, int b) { return a + b; }
Declaración vs Definición

Declaración (prototipo): Le dice al compilador que la función existe.

int suma(int a, int b);  // Solo firma, no implementación

Definición: Implementación real de la función.

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

Puedes declarar múltiples veces, pero solo definir UNA vez (One Definition Rule).

Paso de parámetros: por valor

#include <iostream>

void modify_value(int x) {
    x = 100;                               // Modifica la COPIA local
    std::cout << "Dentro: " << x << "
";  // 100
}

int main() {
    int num = 42;
    modify_value(num);
    std::cout << "Fuera: " << num << "
";  // 42 (sin cambios)

    return 0;
}

Pass by Value: Copia

flowchart TB
    subgraph "main()"
    NUM["num = 42<br/>0x1000"]
    end

    COPY["📋 COPIA"]

    subgraph "modify_value()"
    X["x = 42<br/>0x2000"]
    X_MOD["x = 100<br/>(copia independiente)"]
    X --> X_MOD
    end

    NUM --> |"pasa por valor"| COPY
    COPY --> X

    RESULT["Al terminar:<br/>x se destruye<br/>num sigue siendo 42"]

    style NUM fill:#3498db,color:#fff
    style COPY fill:#e67e22,color:#fff
    style X fill:#9b59b6,color:#fff
    style X_MOD fill:#9b59b6,color:#fff
    style RESULT fill:#27ae60,color:#fff
void modify_value(int x) {
    x = 100;
}
Costo de copiar Básico

Para tipos primitivos (int, double, punteros), copiar es barato (4-8 bytes).

Para objetos grandes (std::vector, std::string), copiar puede ser costoso:

void process(std::vector<int> vec) {  // ❌ Copia todo el vector
    // ...
}

void process(const std::vector<int>& vec) {  // ✅ No copia (8 bytes)
    // ...
}

Paso de parámetros: por referencia

#include <iostream>

void modify_reference(int& x) {
    x = 100;                               // Modifica el ORIGINAL
    std::cout << "Dentro: " << x << "
";  // 100
}

int main() {
    int num = 42;
    modify_reference(num);
    std::cout << "Fuera: " << num << "
";  // 100 (modificado!)

    return 0;
}

Pass by Reference: Alias

flowchart TB
    subgraph "main()"
    NUM["num = 42<br/>0x1000"]
    end

    ALIAS["🔗 ALIAS<br/>(misma dirección)"]

    subgraph "modify_reference()"
    X["x → 0x1000<br/>(referencia a num)"]
    MOD["x = 100<br/>✓ modifica num"]
    X --> MOD
    end

    NUM <--> |"pasa por referencia"| ALIAS
    ALIAS <--> X

    RESULT["Al terminar:<br/>num = 100<br/>(modificado directamente)"]

    style NUM fill:#3498db,color:#fff
    style ALIAS fill:#e67e22,color:#fff
    style X fill:#9b59b6,color:#fff
    style MOD fill:#9b59b6,color:#fff
    style RESULT fill:#27ae60,color:#fff
void modify_reference(int& x) {
    x = 100;
}
Cuándo usar const&

Usa const& para parámetros de solo lectura de objetos grandes:

#include <iostream>
#include <string>

// Evita copia innecesaria (eficiente) pero no permite modificar
void print_string(const std::string& str) {
    std::cout << str << "
";
    // str = "Hola";  // ❌ Error: const reference no permite modificación
}

int main() {
    std::string text = "Hello World";
    print_string(text);  // No copia el string

    return 0;
}

Ventajas:

  • No copia el objeto (eficiente)
  • const garantiza que no se modifica
  • Permite pasar temporales: print_string("Hello")

Regla de oro: Para objetos grandes, usa const& a menos que necesites modificar.

Paso de parámetros: por puntero

#include <iostream>

void modify_pointer(int* ptr) {
    if (ptr != nullptr) {
        *ptr = 100;  // Modifica el valor apuntado
    }
}

int main() {
    int num = 42;
    modify_pointer(&num);
    std::cout << num << "
";  // 100

    // También se puede pasar nullptr
    modify_pointer(nullptr);  // Seguro si verificamos

    return 0;
}
Referencia vs Puntero Intermedio
Aspecto Referencia Puntero
Puede ser null No Sí (nullptr)
Reasignable No
Sintaxis func(obj) func(&obj)
Verificación requerida No (siempre válida) Sí (verificar != nullptr)

Regla: Prefiere referencias. Usa punteros solo cuando necesites null o reasignación.

Sobrecarga de funciones

#include <iostream>

// Sobrecarga: misma función, diferentes parámetros
int suma(int a, int b) { return a + b; }

double suma(double a, double b) { return a + b; }

int suma(int a, int b, int c) { return a + b + c; }

int main() {
    std::cout << suma(5, 3) << "
";      // Llama a suma(int, int)
    std::cout << suma(5.5, 3.2) << "
";  // Llama a suma(double, double)
    std::cout << suma(1, 2, 3) << "
";   // Llama a suma(int, int, int)

    return 0;
}
Name Mangling

El compilador distingue funciones sobrecargadas mediante name mangling: codifica los tipos de parámetros en el nombre de la función.

int suma(int, int);
→ _Z4sumaii double suma(double, double);
→ _Z4sumadd int suma(int, int, int);
→ _Z4sumaiii

    // Puedes ver los símbolos con: nm programa | c++filt

Por eso C++ soporta sobrecarga pero C no: C no tiene name mangling.

Argumentos por defecto

#include <iostream>

void print_message(std::string msg, int times = 1) {
    for (int i = 0; i < times; i++) {
        std::cout << msg << "
";
    }
}

int main() {
    print_message("Hello");     // times = 1 (por defecto)
    print_message("World", 3);  // times = 3

    return 0;
}

// ⚠️ Reglas:
// - Parámetros con valor por defecto deben estar AL FINAL
// void foo(int a = 1, int b);  // ❌ Error
// void foo(int a, int b = 1);  // ✅ OK
Reglas de Argumentos por Defecto
  • Deben estar al final de la lista de parámetros
  • Se especifican en la declaración, no en la definición
  • No puedes tener "huecos": void foo(int a = 1, int b, int c = 3)
// header.h
void func(int a, int b = 10);

// source.cpp
void func(int a, int b) {  // Sin valores por defecto aquí
    // ...
}

Inline functions

#include <iostream>

// inline sugiere al compilador expandir la función en el call site
inline int square(int x) { return x * x; }

int main() {
    int result = square(5);
    // El compilador PUEDE expandir esto a:
    // int result = 5 * 5;

    return 0;
}
¿Qué hace inline? Avanzado

inline sugiere al compilador que expanda la función en el call site (copia el código directamente) en lugar de hacer una llamada.

Ventajas:

  • Elimina overhead de llamada a función (push/pop stack, jump)
  • Permite optimizaciones adicionales (inlining permite constant folding, etc.)

Desventajas:

  • Aumenta el tamaño del binario (code bloat)
  • Puede empeorar instruction cache

Realidad: Los compiladores modernos ignoran inline y deciden automáticamente qué inlinear basándose en heurísticas (tamaño, frecuencia de llamada, etc.).

En C++ moderno, inline se usa más para evitar errores de "multiple definition" en headers que para optimización.

Recursión

#include <iostream>

int factorial(int n) {
    // Caso base: detiene la recursión
    if (n <= 1) {
        return 1;
    }
    // Caso recursivo
    return n * factorial(n - 1);
}

int main() {
    std::cout << "5! = " << factorial(5) << "
";  // 120

    return 0;
}

// Llamadas:
// factorial(5) = 5 * factorial(4)
//              = 5 * 4 * factorial(3)
//              = 5 * 4 * 3 * factorial(2)
//              = 5 * 4 * 3 * 2 * factorial(1)
//              = 5 * 4 * 3 * 2 * 1
//              = 120

Stack Frames de Recursión

flowchart TB
    subgraph "STACK (crece hacia abajo)"
    F1["factorial(1)<br/>n=1<br/>return=1"]
    F2["factorial(2)<br/>n=2<br/>return=2 * 1 = 2"]
    F3["factorial(3)<br/>n=3<br/>return=3 * 2 = 6"]
    F4["factorial(4)<br/>n=4<br/>return=4 * 6 = 24"]
    F5["factorial(5)<br/>n=5<br/>return=5 * 24 = 120"]
    MAIN["main()"]

    F1 --> F2
    F2 --> F3
    F3 --> F4
    F4 --> F5
    F5 --> MAIN
    end

    style F1 fill:#e74c3c,color:#fff
    style F2 fill:#e67e22,color:#fff
    style F3 fill:#f39c12,color:#fff
    style F4 fill:#3498db,color:#fff
    style F5 fill:#9b59b6,color:#fff
    style MAIN fill:#27ae60,color:#fff

Cada llamada crea un nuevo stack frame con sus propias variables. La recursión profunda puede causar stack overflow.

Recursión vs Iteración

Recursión:

  • ✅ Más elegante para problemas naturalmente recursivos (árboles, grafos)
  • ❌ Usa más memoria (cada llamada = stack frame)
  • ❌ Más lenta (overhead de llamadas)
  • ❌ Riesgo de stack overflow si la recursión es muy profunda

Iteración:

  • ✅ Más eficiente (menos overhead)
  • ✅ Usa memoria constante
  • ❌ A veces más difícil de leer

Call stack y stack frames

El call stack (pila de llamadas) es una estructura de datos LIFO (Last In, First Out) que el sistema operativo utiliza para gestionar las llamadas a funciones. Cada vez que llamas una función, se crea un nuevo stack frame (marco de pila) que contiene toda la información necesaria para esa llamada específica.

¿Qué contiene un stack frame? Cada frame incluye:

¿Cómo funciona? Cuando llamas una función, el procesador "empuja" (push) un nuevo frame al stack. Cuando la función retorna, ese frame se "extrae" (pop) del stack, y el control vuelve a la función que hizo la llamada. Esta estructura en pila permite recursión y llamadas anidadas arbitrariamente profundas (limitadas solo por el tamaño del stack).

#include <iostream>

void func_c() { std::cout << "En func_c
"; }

void func_b() {
    std::cout << "En func_b
";
    func_c();
}

void func_a() {
    std::cout << "En func_a
";
    func_b();
}

int main() {
    std::cout << "En main
";
    func_a();
    std::cout << "De vuelta en main
";
    return 0;
}

Call Stack en Acción

flowchart TB
    subgraph "1. main() llama func_a()"
    S1_A["func_a() ← SP"]
    S1_M["main()"]
    S1_A --- S1_M
    end

    subgraph "2. func_a() llama func_b()"
    S2_B["func_b() ← SP"]
    S2_A["func_a()"]
    S2_M["main()"]
    S2_B --- S2_A --- S2_M
    end

    subgraph "3. func_b() llama func_c()"
    S3_C["func_c() ← SP"]
    S3_B["func_b()"]
    S3_A["func_a()"]
    S3_M["main()"]
    S3_C --- S3_B --- S3_A --- S3_M
    end

    subgraph "4. func_c() retorna"
    S4_B["func_b() ← SP"]
    S4_A["func_a()"]
    S4_M["main()"]
    S4_B --- S4_A --- S4_M
    end

    style S1_A fill:#e74c3c,color:#fff
    style S2_B fill:#e74c3c,color:#fff
    style S3_C fill:#e74c3c,color:#fff
    style S4_B fill:#e74c3c,color:#fff

Cada stack frame contiene:

  • Parámetros de la función
  • Variables locales
  • Dirección de retorno (return address)
  • Frame pointer (para debugging)

Stack overflow

#include <iostream>

// ❌ Recursión infinita: sin caso base
void infinite_recursion() {
    infinite_recursion();  // Stack overflow!
}

// ❌ Arrays muy grandes en el stack
void stack_overflow() {
    int huge_array[10000000];  // ~40 MB en stack → ¡CRASH!
}

// ✅ Correcto: usar heap
void correct_version() {
    int* large_array = new int[10000000];
    // ...
    delete[] large_array;
}
¿Qué es Stack Overflow? Intermedio

El stack tiene un tamaño limitado (típicamente 1-8 MB en sistemas modernos). Si lo excedes, obtienes un stack overflow y el programa crashea.

Causas comunes:

  • Recursión infinita o muy profunda
  • Arrays muy grandes en el stack: int arr[1000000];
  • Muchos niveles de llamadas anidadas

Soluciones:

  • Usa heap en lugar de stack: int* arr = new int[1000000];
  • Reduce la profundidad de recursión
  • Usa iteración en lugar de recursión
  • Aumenta el tamaño del stack (compilador/linker flags)

Return by value vs reference

Return by value

#include <iostream>
#include <vector>

// Return by value (copia o move)
std::vector<int> create_vector() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    return vec;  // Move (C++11), no copia
}

int main() {
    std::vector<int> v = create_vector();
    return 0;
}

Return by reference

#include <iostream>

// ❌ PELIGRO: retorna referencia a variable local
int& dangerous_function() {
    int x = 42;
    return x;  // ¡x se destruye al salir!
}  // Retorna referencia a memoria inválida (dangling)

// ✅ OK: retorna referencia a parámetro o miembro
int& get_element(std::vector<int>& vec, int index) {
    return vec[index];  // vec vive fuera de la función
}

int main() {
    std::vector<int> numbers = {10, 20, 30};
    get_element(numbers, 1) = 99;     // Modifica numbers[1]
    std::cout << numbers[1] << "
";  // 99

    return 0;
}
NUNCA retornes referencia a variable local
int& bad_function() {
    int x = 42;
    return x;  // ❌ ¡x se destruye al retornar!
}  // Retorna referencia a memoria inválida (dangling)

El compilador puede dar un warning, pero es Undefined Behavior.

Assembly de una llamada a función

// C++ code:
int suma(int a, int b) { return a + b; }

int main() {
    int result = suma(5, 3);
    return 0;
}

// Assembly (x86-64, simplificado):
// main:
//     push rbp
//     mov rbp, rsp
//
//     mov edi, 5         ; Primer argumento (a)
//     mov esi, 3         ; Segundo argumento (b)
//     call suma          ; Llamada a función
//
//     mov [rbp-4], eax   ; Guardar resultado
//     pop rbp
//     ret
//
// suma:
//     push rbp
//     mov rbp, rsp
//
//     mov eax, edi       ; a
//     add eax, esi       ; a + b
//
//     pop rbp
//     ret                ; Resultado en eax
Calling Conventions Avanzado

El calling convention define cómo se pasan parámetros y retornan valores:

x86-64 System V (Linux/macOS):

  • Primeros 6 enteros: rdi, rsi, rdx, rcx, r8, r9
  • Primeros 8 floats: xmm0-xmm7
  • Resto: stack
  • Valor de retorno: rax (enteros), xmm0 (floats)

x64 Windows:

  • Primeros 4 parámetros: rcx, rdx, r8, r9
  • Resto: stack

El compilador se encarga de esto automáticamente.

🆕 Lambdas (Introducción)

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> nums = {5, 2, 8, 1, 9};

    // Lambda: función anónima
    std::sort(nums.begin(), nums.end(), [](int a, int b) {
        return a < b;  // Orden ascendente
    });

    for (int n : nums) {
        std::cout << n << " ";  // 1 2 5 8 9
    }

    return 0;
}
Lambdas en pocas palabras

Un lambda es una función anónima definida inline:

[captura](parámetros) { cuerpo }

Útil para callbacks, algoritmos de la STL (std::sort, std::for_each), etc.

Las veremos en detalle más adelante.

Checklist

Preguntas para reflexionar

¿Cuál es la diferencia entre pasar por referencia y por puntero?
Respuesta:
AspectoReferenciaPuntero
NullNo puede ser nullPuede ser nullptr
Sintaxisfunc(obj)func(&obj)
ReasignaciónNo

Regla: Prefiere referencias para parámetros. Usa punteros cuando necesites null o reasignación.

¿Por qué const& es mejor que pasar por valor para objetos grandes?
Respuesta:

Eficiencia: Evita copia innecesaria.

void process(std::vector<int> v) { // ❌ Copia TODO el vector
}

void process(const std::vector<int>& v) { // ✅ Solo pasa 8 bytes (referencia)
}

Para un vector de 1 millón de elementos:

  • Por valor: copia 4 MB
  • Por const&: pasa 8 bytes

const garantiza que no modificarás el objeto.

¿Qué es el call stack y cómo funciona?
Respuesta:

El call stack es una región de memoria (stack) que almacena información sobre funciones activas.

Cada llamada a función crea un stack frame con:

  • Parámetros
  • Variables locales
  • Dirección de retorno

Cuando la función retorna, su stack frame se destruye (pop). El stack crece al llamar y decrece al retornar.

Es LIFO (Last In, First Out): la última función llamada es la primera en retornar.

¿Por qué no puedo retornar una referencia a una variable local?
Respuesta:

Las variables locales viven en el stack frame de la función. Al retornar, el stack frame se destruye:

int& bad() {
  int x = 42;
  return x; // ❌
} // x destruido aquí

La referencia apunta a memoria que ya no es válida (dangling reference). Usarla causa Undefined Behavior.

Solución: Retorna por valor o retorna referencia a algo que vive fuera de la función (parámetro, miembro de clase, variable global).

¿Qué causa un stack overflow y cómo prevenirlo?
Respuesta:

Causas:

  • Recursión infinita o muy profunda
  • Arrays muy grandes en el stack
  • Demasiadas llamadas anidadas

Prevención:

  • Usa iteración en lugar de recursión profunda
  • Aloca objetos grandes en el heap: new o std::vector
  • Limita la profundidad de recursión
  • Aumenta el tamaño del stack (linker flags)
// ❌ Stack overflow
int huge[10000000];

// ✅ Heap allocation
std::vector huge(10000000);
Próximo Capítulo

En el Capítulo 9 aprenderás sobre structs y clases: la base de la programación orientada a objetos en C++. Verás constructores, destructores, member functions, access specifiers, el puntero this, y cómo se organizan los objetos en memoria (memory layout, vtables).

Siguiente capítulo: Structs y Clases →