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
- Dominar declaración vs definición de funciones
- Entender paso por valor, referencia y puntero
- Saber cuándo usar
const¶ eficiencia - Usar sobrecarga y argumentos por defecto
- Comprender el call stack y stack frames
- Identificar y prevenir stack overflow
- Conocer inline functions y su propósito
- Entender recursión y sus tradeoffs
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 (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;
} 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)
constgarantiza 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 | Sí |
| 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;
} 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 - 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:
- ✅ 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:
- Parámetros de la función: Los valores pasados a la función
- Variables locales: Todas las variables declaradas dentro de la función
- Dirección de retorno: Dónde continuar la ejecución cuando la función termine
- Registro base del frame anterior: Para restaurar el contexto al retornar
¿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;
} 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;
} 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
- Entiendo la diferencia entre declaración y definición
- Sé cuándo usar paso por valor, referencia y puntero
- Uso
const¶ parámetros de objetos grandes de solo lectura - Conozco las reglas de sobrecarga de funciones
- Uso argumentos por defecto correctamente (al final)
- Entiendo qué es el call stack y cómo se crean stack frames
- Sé qué causa stack overflow y cómo prevenirlo
- Nunca retorno referencias a variables locales
Preguntas para reflexionar
¿Cuál es la diferencia entre pasar por referencia y por puntero?
| Aspecto | Referencia | Puntero |
|---|---|---|
| Null | No puede ser null | Puede ser nullptr |
| Sintaxis | func(obj) | func(&obj) |
| Reasignación | No | Sí |
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?
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?
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?
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?
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:
newostd::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);
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).