En C, cuando llamas a una función, siempre estás pasando una copia de los datos. Este mecanismo se conoce como paso por valor. Esto significa que la función recibe su propia versión local de la variable en un espacio de memoria dedicado llamado pila (stack). Debido a este diseño, si modificas el parámetro dentro de la función, solo estás alterando esa copia local, dejando la variable original del llamador totalmente intacta.
Esta arquitectura existe para garantizar el aislamiento y la seguridad: una función no debería poder corromper la memoria de la función que la invocó de forma accidental. Sin embargo, este comportamiento plantea un reto: ¿cómo modificamos algo en la función original? La respuesta es que en C no existe el paso por referencia como tal; lo que hacemos es simularlo pasando el valor de una dirección de memoria (un puntero). En lugar de copiar el número, copiamos la “dirección de su casa”, permitiendo que la función viaje a esa dirección y modifique el contenido original.
Debes usar el paso por valor para tipos básicos (como int o float) y para estructuras pequeñas que no suelan cambiar, ya que es seguro y eficiente. Debes usar punteros (la simulación de referencia) cuando necesites que la función devuelva más de un valor (“parámetros de salida”) o cuando trabajes con objetos grandes donde copiar toda la estructura sería un desperdicio de recursos. Si lo haces mal, el error más común es intentar usar sizeof con un array dentro de una función, obteniendo el tamaño del puntero y no el del array, o bien, modificar una copia pensando que estás alterando el original.
#include <stdio.h>
/* Una estructura para observar el comportamiento de las copias */
typedef struct {
int id;
double valor_pesado;
} Configuracion;
/* 1. Paso por valor: la variable 'n' es una copia local */
void intentar_cambiar_valor(int n) {
n = 42;
// Al salir de aquí, la 'n' original en main() sigue valiendo 10
}
/* 2. Simulación de paso por referencia: usamos un puntero */
void cambiar_original(int *p) {
if (p != NULL) {
*p = 42; // Accedemos al contenido de la dirección de memoria
}
}
/* 3. El problema del array y el decaimiento (decay) */
void analizar_array(int arr[]) {
/*
* IMPORTANTE: Aquí 'arr' no es un array, ha "decaído" a un puntero.
* sizeof(arr) devolverá el tamaño del puntero (ej. 8 en 64-bit),
* NO el tamaño del array original.
*/
printf(" [DEBUG] Tamaño de 'arr' en la función: %zu bytes\n", sizeof(arr));
}
void modificar_array(int arr[], int tamano) {
// Para trabajar con arrays, siempre debemos pasar el tamaño por separado
for (int i = 0; i < tamano; i++) {
arr[i] *= 2;
}
}
int main(void) {
int original = 10;
int copia = original;
printf("--- Paso por Valor ---\n");
intentar_cambiar_valor(copia);
printf("Original: %d, Copia: %d\n", original, copia);
printf("\n--- Simulación de Referencia ---\n");
cambiar_original(&original);
printf("Original tras puntero: %d\n", original);
printf("\n--- Arrays y Decaimiento ---\n");
int lista[] = {1, 2, 3, 4, 5};
analizar_array(lista);
modificar_array(lista, 5);
printf("Lista tras duplicar: ");
for (int i = 0; i < 5; i++) {
printf("%d ", lista[i]);
}
printf("\n");
printf("\n--- Structs y Eficiencia ---\n");
ConfiguraC config = {1, 3.14159};
// Al pasar 'config' por valor, se copia todo el contenido (id y double)
// Para structs grandes, es mejor pasar un puntero: &config
printf("Config ID: %d\n", config.id);
return 0;
}
Desglose del código
Fíjate en cómo intentar_cambiar_valor recibe un int. En la pila de ejecución, el compilador reserva un nuevo espacio para n y le asigna el valor de copia. Cuando la función termina, ese espacio se libera y el cambio se pierde.
En el caso de cambiar_original, pasamos &original. El operador & extrae la dirección de memoria de la variable. La función recibe esa dirección como un puntero (int *p). Al usar el operador de desreferencia *p, le estamos diciendo a la CPU: “no cambies el valor del puntero, cambia el valor que está guardado en la dirección que el puntero contiene”.
Cuando llamamos a analizar_array(lista), ocurre un fenómeno crítico llamado decaimiento a puntero (pointer decay). Aunque en la definición de la función parezca un array int arr[], para el compilador es simplemente un puntero int *arr. Por eso, sizeof(arr) no sabe cuántos elementos tiene la lista original; solo sabe cuánto mide un punero en la arquitectura actual. Por esta razón, en C siempre debes pasar el tamaño del array como un argumento adicional, como hicimos en modificar_array.
Finalmente, con la estructura Configuracion, si hiciéramos void func(Configuracion c), el sistema copiaría todos sus bytes en la pila. Para tipos pequeños como un int, esto es extremadamente rápido porque el compilador suele pasar estos valores directamente en los registros de la CPU, evitando la memoria lenta. Pero para estructuras grandes, la copia puede ser costosa y es preferible pasar su dirección.
El error frecuente
void error_con_sizeof(int arr[]) {
// ERROR: Intentar calcular el tamaño de un array recibido por parámetro
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
printf("%d ", arr[i]);
}
}
Este código es un error clásico. Como arr ha decaído a un puntero, sizeof(arr) devolverá el tamaño del puntero (por ejemplo, 8 bytes en sistemas de 64 bits). La expresión 8 / 4 (asumiendo un int de 4 bytes) dará 2, por lo que el bucle solo procesará los dos primeros elementos, sin importar si el array original tenía 100. Herramientas como UBSan (Undefined Behavior Sanitizer) o un buen linter te avisarán de que estás usando un tamaño de tipo erróneo.
N° 36