Declaración, definición y paso de argumentos en C++

Para trabajar con funciones, primero debes entender que no es lo mismo decir que una función existe a decir qué hace. La declaración (también llamada prototipo) es solo la firma: le dice al compilador el nombre de la función, qué tipos recibe y qué devuelve. Esto permite que uses la función en otras partes del código antes de haber escrito su lógica. La definición, en cambio, es el cuerpo de la función, donde reside el código real. El compilador exige la One Definition Rule (ODR): puedes declarar una función tantas veces como quieras (por ejemplo, en diferentes archivos mediante un header), pero solo puedes definirla una vez en todo tu programa para evitar ambigüedades.

Cuando pasas datos a una función, tienes cuatro caminos principales. Si pasas un argumento por valor, el compilador crea una copia local; los cambios dentro de la función no afectan a la variable original, lo cual es seguro pero costoso si el objeto es pesado como un std::vector o un std::string. Si usas referencias (T&), la función trabaja directamente sobre el objeto original (es un alias), por lo que cualquier cambio se refleja fuera. Sin embargo, si no quieres modificar el original pero quieres evitar la copia por rendimiento, la opción estándar es por referencia constante (const T&): es eficiente y segura. Finalmente, puedes usar punteros (T*), que son útiles si el argumento puede ser nulo (nullptr), aunque requieren más cuidado al desreferenciarlos.

En cuanto al retorno, lo normal es devolver por valor. Gracias a la elisión de copia (RVO – Return Value Optimization), esto suele ser extremadamente eficiente. No obstante, si intentas devolver una referencia a una variable que va a morir al salir de la función, romperás el programa. Para funciones que necesitan devolver múltiples datos, no te limites a un solo valor; puedes usar std::pair o std::tuple junto con structured bindings [C++17] para recibir esos valores de forma limpia y legible.

#include <iostream>
#include <string>
#include <utility> // Para std::pair
#include <tuple>   // Para std::tuple

// --- DECLARACIONES (Prototipos) ---
// Le dicen al compilador que estas funciones existen.

// Paso por valor: copia el string.
void mostrarBienvenida(std::string nombre);

// Paso por referencia: modifica el nombre original.
void cambiarNombre(std::string& nombre);

// Paso por referencia constante: eficiente y de solo lectura.
void imprimirPerfil(const std::string& nombre, int edad);

// Retorno de múltiples valores usando std::pair.
std::pair<std::string, int> obtenerDatosDePrueba();

int main() {
    // Datos iniciales
    std::string nombreUsuario = "Candidato";
    int edadUsuario = 20;

    // 1. Uso de paso por valor
    mostrarBienvenida(nombreUsuario);

    // 2. Uso de paso por referencia (modifica nombreUsuario directamente)
    cambiarNombre(nombreUsuario);

    // 3. Uso de const T& (no podemos cambiar el nombre aquí)
    imprimirPerfil(nombreUsuario, edadUsuario);

    // 4. Uso de Structured Bindings [C++17] para desempaquetar un std::pair
    auto [nuevoNombre, nuevaEdad] = obtenerDatosDePrueba();

    std::cout << "Datos recuperados: " << nuevoNombre 
              << " con " << nuevaEdad << " años." << std::endl;

    return 0;
}

// --- DEFINICIONES (Implementación) ---

void mostrarBienvenida(std::string nombre) {
    // 'nombre' es una copia local. Cambiarla aquí no afecta a la original.
    std::cout << "Bienvenido al sistema, " << nombre << "!" << std::endl;
}

void cambiarNombre(std::string& nombre) {
    // 'nombre' es un alias de la variable que pasamos.
    nombre = "Experto_C++";
}

void imprimirPerfil(const std::string& nombre, int edad) {
    // 'nombre' es una referencia a la original, pero 'const' impide modificarla.
    // Es la forma más eficiente de pasar objetos grandes.
    std::cout << "[Perfil] Nombre: " << nombre << " | Edad: " << edad << std::endl;
}

std::pair<std::string, int> obtenerDatosDePrueba() {
    // Devolvemos un par de valores.
    return {"Alice", 30};
}

Desglose del código

Fíjate en cómo main interactúa con las funciones. Cuando llamamos a mostrarBienvenida(nombreUsuario), el tipo std::string se copia; si dentro de la función hiciéramos cambios, nombreUsuario en main permanecería intacto. Sin embargo, al llamar a cambiarNombre(nombreUsuario), estamos pasando la referencia real. Dentro de la definición, la variable nombre no es una copia, sino un alias de nombreUsuario, por lo que la asignación nombre = "Experto_C++" modifica directamente la memoria de la variable en main.

En imprimirPerfil, el uso de const std::string& es clave para el rendimiento. Si pasáramos el nombre “por valor”, el programa tendría que reservar memoria y copiar todo el texto cada vez que llamamos a la función. Con la referencia constante, solo pasamos una dirección de memoria, y el calificador const garantiza que la función no pueda corromper los datos originales.

Por último, la función obtenerDatosDePrueba devuelve un objeto std::pair. En lugar de acceder a él mediante resultado.first o resultado.second, hemos usado structured bindings en main (auto [nuevoNombre, nuevaEdad] = ...). Esta sintaxis de [C++17] descompone el par en dos variables locales nuevas, haciendo que el código sea mucho más intuitivo y menos propenso a errores de lectura.

El error frecuente
Un error clásico y peligroso es intentar devolver una referencia a una variable local. Mira este ejemplo:

std::string& errorFatal() {
    std::string local = "Soy una variable temporal";
    return local; // ¡ERROR! La variable 'local' muere al terminar la función.
}

Si intentas compilar esto, el compilador suele lanzar un warning muy importante: “reference to stack memory associated with local variable ‘local’ might be Dangling”. Si ignoras este aviso, tendrás un Undefined Behavior (UB). Al intentar usar el resultado, estarás accediendo a una posición de la pila (stack) que ya ha sido liberada o reutilizada, lo que provocará un crash aleatorio o datos basura. Siempre devuelve por valor a menos que sea estrictamente necesario y tengas la certeza absoluta de que la vida del objeto supera a la de la función.

Para compilar el ejemplo principal:
g++ -std=c++20 -Wall -Wextra -Wpedantic -o ejemplo ejemplo.cpp

32

Dejar un comentario

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

Scroll al inicio