C++
Funciones: parámetros, retorno y referencias
Cómo declarar funciones, qué pasa con los argumentos al pasarlos, y por qué existen las referencias.
De dónde venimos
En Control de flujo el main() ya empezaba a acumular responsabilidades: pedir datos, validarlos, decidir y mostrar el resultado, todo en el mismo bloque. Esta página parte de ese problema concreto.
También retoma dos ideas anteriores que aquí se vuelven centrales:
- El ámbito de las variables que viste en Bloques y ámbito: el cuerpo de una función es un bloque, y sus variables locales mueren al terminar la función.
- El
constde Variables y tipos, que combinado con referencias se convierte en la forma estándar de pasar datos grandes en C++.
Y, sobre todo, esta página es el puente hacia punteros: el operador & que vas a ver aquí es el mismo símbolo (con otro significado) que aparecerá en Punteros.
Qué es una función
Una función es un bloque de código con nombre que recibe datos de entrada (parámetros), hace algo con ellos y opcionalmente devuelve un resultado. Ya vienes usando una desde el primer programa: main() es una función, y el sistema operativo la llama por ti al ejecutar el programa.
La forma general es:
tipo_de_retorno nombre(tipo parametro1, tipo parametro2) {
// cuerpo
return valor;
}#include <iostream>
double areaRectangulo(double ancho, double alto) {
return ancho * alto;
}
int main() {
double a = 0.0;
double b = 0.0;
std::cout << "Ancho y alto: ";
std::cin >> a >> b;
std::cout << "Área: " << areaRectangulo(a, b) << "\n";
return 0;
}g++ -std=c++17 -Wall -o area area.cpp
./areaAncho y alto: 3 4.5
Área: 13.5Tres cosas que este ejemplo muestra de paso:
std::cin >> a >> b;encadena dos lecturas, igual questd::coutencadena escrituras (misma idea de la página de la Standard Library).- Los parámetros
anchoyaltoson variables locales de la función: existen solo mientras ella se ejecuta. - Los nombres de los argumentos en la llamada (
a,b) no tienen por qué coincidir con los de los parámetros. Lo que importa es el orden y el tipo.
Funciones que no devuelven nada: void
Si una función solo hace algo (imprimir, modificar) y no tiene un resultado que entregar, su tipo de retorno es void y no lleva return (o lleva un return; seco para salir antes de tiempo).
void mostrarSeparador() {
std::cout << "--------------------\n";
}Declaración y definición
El compilador de C++ lee el archivo de arriba a abajo y necesita conocer una función antes de la línea donde la llamas. Por eso, si prefieres poner main() primero, debes anticipar la función con un prototipo (la firma seguida de ;, sin cuerpo):
#include <iostream>
double areaRectangulo(double ancho, double alto); // declaración (prototipo)
int main() {
std::cout << areaRectangulo(3.0, 4.5) << "\n";
return 0;
}
double areaRectangulo(double ancho, double alto) { // definición
return ancho * alto;
}
En un programa de un solo archivo como este, el prototipo es opcional: basta con definir la función antes de usarla. En proyectos reales, en cambio, los prototipos son la norma y viven en archivos de cabecera (.h / .hpp) mientras las definiciones van en .cpp. Esa separación es justamente lo que hace #include <iostream>: te trae las declaraciones de std::cout, no su código. Este ejemplo de un archivo no es representativo de cómo se organiza un proyecto de verdad.
Paso por valor vs. paso por referencia
Aquí está la parte que de verdad importa, porque define cómo se comporta C++ frente a otros lenguajes.
Por defecto, C++ pasa los argumentos por valor: la función recibe una copia del dato. Modificar el parámetro dentro de la función no afecta a la variable original de quien llamó.
#include <iostream>
void intentarDuplicar(int numero) {
numero = numero * 2; // modifica solo la copia local
}
int main() {
int x = 10;
intentarDuplicar(x);
std::cout << x << "\n"; // imprime 10, no 20
return 0;
}Para que la función trabaje sobre la variable original, se declara el parámetro como referencia, añadiendo & al tipo. Una referencia es un alias: otro nombre para la misma variable, no una copia.
#include <iostream>
void duplicar(int& numero) { // el & convierte el parámetro en referencia
numero = numero * 2; // modifica la variable original
}
int main() {
int x = 10;
duplicar(x);
std::cout << x << "\n"; // imprime 20
return 0;
}g++ -std=c++17 -Wall -o referencias referencias.cpp
./referencias20La diferencia entre los dos programas es un único carácter: &. Por eso conviene detenerse aquí y no seguir hasta tenerlo claro.
| Forma | Sintaxis | ¿Copia el dato? | ¿Puede modificar el original? |
|---|---|---|---|
| Por valor | void f(int n) | Sí | No |
| Por referencia | void f(int& n) | No | Sí |
| Referencia const | void f(const std::string& s) | No | No (el compilador lo impide) |
La tercera fila es la combinación que más vas a usar en C++ real: pasar objetos grandes por referencia constante. Copiar un int es gratis, pero copiar un std::string con miles de caracteres o un vector con un millón de elementos no lo es. La referencia evita la copia, y el const deja claro (al compilador y al lector) que la función solo va a leer el dato.
#include <iostream>
#include <string>
void saludar(const std::string& nombre) { // sin copia, sin permiso de modificar
std::cout << "Hola, " << nombre << "\n";
}
int main() {
std::string usuario = "Camilo";
saludar(usuario);
return 0;
}
Nunca devuelvas una referencia a una variable local de la función. La variable muere cuando la función termina (por el ámbito que viste en Bloques y ámbito), y quien reciba esa referencia estará apuntando a memoria que ya no le pertenece a nadie. El código compila —a veces con una advertencia si usas -Wall— y puede parecer que funciona en pruebas pequeñas, pero es comportamiento indefinido. La versión con punteros de este mismo error se llama puntero colgante y la verás en Punteros.
int& mal() {
int local = 42;
return local; // ERROR conceptual: local deja de existir aquí
}Que el paso por valor haga una copia significa que la referencia es la única forma que tiene una función de modificar algo de quien la llamó (junto con los punteros, que veremos después). Si vienes de Python o JavaScript, donde las listas y objetos se comportan como si se pasaran por referencia automáticamente, este es uno de los cambios mentales más importantes: en C++ decides tú, explícitamente, en cada parámetro.
Sobrecarga de funciones
C++ permite tener varias funciones con el mismo nombre siempre que se diferencien en el número o el tipo de sus parámetros. El compilador elige cuál llamar según los argumentos. Esto se llama sobrecarga (overloading), y es una de las diferencias con C que ya se mencionaba en la tabla comparativa C vs C++.
#include <iostream>
double area(double lado) { // cuadrado
return lado * lado;
}
double area(double ancho, double alto) { // rectángulo
return ancho * alto;
}
double area(int radio) { // círculo (radio entero)
return 3.14159 * radio * radio;
}
int main() {
std::cout << area(4.0) << "\n"; // 16 -> cuadrado
std::cout << area(3.0, 4.0) << "\n"; // 12 -> rectángulo
std::cout << area(2) << "\n"; // 12.5664 -> círculo
return 0;
}g++ -std=c++17 -Wall -o sobrecarga sobrecarga.cpp
./sobrecarga16
12
12.5664
Este ejemplo es didáctico, no un buen diseño. Distinguir “cuadrado” de “círculo” solo porque uno recibe double y el otro int es frágil y confuso: area(2) da el área de un círculo, pero area(2.0) da la de un cuadrado. En código real, la sobrecarga se usa cuando las versiones hacen conceptualmente lo mismo con tipos distintos (por ejemplo, imprimir(int) e imprimir(const std::string&)). Si las operaciones son distintas, dales nombres distintos: areaCuadrado, areaCirculo.
El tipo de retorno no participa en la sobrecarga. Estas dos no pueden coexistir, porque en la llamada f(1) el compilador no tendría forma de saber cuál quieres:
int f(int x);
double f(int x); // ERROR: redefinición, difiere solo en el retornoSiguiente paso
Con funciones ya puedes organizar el código, pero sigues manejando valores sueltos de uno en uno. Para trabajar con colecciones de datos del mismo tipo, continúa con Arrays y cadenas de estilo C — que además es el escalón directo hacia los punteros.