Parámetros y argumentos
Dos palabras que se usan indistintamente pero vale la pena separar:
- Un parámetro es la variable de la definición de la función:
int nenint square(int n). - Un argumento es el valor que suministras en el punto de llamada: el
4desquare(4).
Llamar a una función crea sus parámetros como variables locales nuevas y copia los argumentos dentro de ellas. Esa copia es todo el tema de esta página.
C pasa por valor, siempre
Esta es la regla, y C no tiene ninguna excepción a ella: una función recibe copias de sus argumentos. Asignar a un parámetro cambia solo la copia.
value sigue siendo 42. La función obtuvo una copia llamada x, puso la copia en cero, y la copia desapareció cuando la función retornó. Renombrar el parámetro a value no cambia nada: el nombre no las conecta; son dos variables distintas.
Lo mismo vale para todos los tipos: int, double, char, un puntero, incluso una estructura entera (copiada campo por campo). Si vienes de Python, Java o JavaScript, fíjate en que esos lenguajes pasan referencias a objetos por valor, así que mutar un objeto dentro de una función se ve desde fuera. C no hace eso: copia el objeto mismo.
Pasar por valor es una virtud, no una limitación. Una función no puede corromper los datos de quien la llama por accidente, lo que hace mucho más fácil razonar sobre el código.
El fracaso clásico: swap
La demostración de manual es una función que intenta intercambiar dos valores:
La lógica es correcta: de verdad intercambia a y b. Pero a y b son copias, así que el trabajo se tira a la basura en el instante en que la función retorna.
Simular el paso por referencia con punteros
Para dejar que una función cambie una variable de quien llama, dale la dirección de la variable en lugar de su valor. Esa dirección se pasa a su vez por valor —el puntero se copia— pero la copia sigue apuntando al objeto original, así que escribir a través de ella alcanza la memoria de quien llama.
Tres piezas de sintaxis cargan con toda la idea:
int *aen la lista de parámetros declaraacomo "un puntero a unint".&xen el punto de llamada produce la dirección dex.*adentro de la función significa "elintal que apuntaa", que se puede leer y asignar.
Esto es lo que los programadores de C quieren decir con "paso por referencia", y vale la pena ser preciso: sigue siendo paso por valor. Lo que se copia es el puntero. Asignar a a en sí (a = NULL;) cambiaría solo la copia; asignar a *a cambia la variable de quien llama. Punteros cubre el modelo completo.
El costo es que el punto de llamada ya no te dice si una variable puede cambiar, que es exactamente por lo que el & es obligatorio. swap(x, y) no compila; tienes que escribir swap(&x, &y), y ese & es la marca visible de que esta llamada puede modificar x.
Devolver varios resultados
return produce un valor, así que las salidas extra viajan por parámetros de tipo puntero. La forma habitual es un código de estado como valor de retorno y los resultados reales escritos a través de punteros:
Fíjate en que q y r conservan sus valores anteriores cuando la llamada falla: la función retornó antes de escribir nada. Ese es el contrato que quien llama tiene que respetar, y es por eso que la comprobación del estado va antes de usar las salidas.
La alternativa es devolver una estructura que contenga ambos campos, lo que evita los punteros por completo y suele ser el mejor diseño cuando los valores de verdad van juntos.
Los arrays son distintos
Los arrays no siguen la regla de la copia, y esto hace tropezar a casi todo el mundo. Cuando pasas un array, decae a un puntero a su primer elemento, así que la función recibe una dirección, no una copia de los datos.
El array de quien llama cambió de verdad, sin un & por ninguna parte, porque en este contexto data ya es una dirección. Tres consecuencias:
int a[] e int *a significan exactamente lo mismo en una lista de parámetros. Incluso int a[100]; el tamaño se ignora. Escribir int a[] documenta la intención, pero no lo leas como "se está copiando un array".
sizeof dentro de la función está mal. Este es el error:
En main, sizeof(data) son 20 bytes y la longitud sale 5. Dentro de la función sizeof(a) es el tamaño de un puntero —8 en un sistema de 64 bits— así que la "longitud" da 2. El tamaño del array sencillamente no está disponible ahí. Pasa siempre la longitud como un parámetro aparte. Los compiladores modernos avisan de esto con -Wall.
Usa const cuando no vayas a escribir. const int a[] dice que la función solo lee, lo que el compilador hace cumplir y un lector puede dar por bueno:
int sum(const int a[], int n); /* promete no modificar el array */
Las cadenas también son arrays, así que aplica lo mismo: void greet(const char *name) es la firma estándar de una función que lee una cadena sin cambiarla.
¿Y el costo?
Pasar una estructura grande por valor copia cada uno de sus bytes en cada llamada. Para una estructura con un puñado de campos eso está bien y suele ser más rápido que la indirección que añade un puntero. Para una grande, pasa un puntero, y márcalo const si la función solo lee:
struct Config { char name[64]; int flags[32]; double weights[128]; };
void applyConfig(const struct Config *cfg); /* sin copia de 512 bytes por llamada */
const struct Config *cfg significa "un puntero a un Config que no voy a modificar", lo que da la eficiencia de un puntero con la seguridad de una copia.
Errores comunes
- Esperar que un parámetro corriente cambie la variable de quien llama. No puede. Pasa un puntero.
- Olvidar el
&en el punto de llamada.swap(x, y)cuandoswapespera punteros es un error de tipos: el compilador lo detecta, pero solo si hay un prototipo en ámbito. - Olvidar el
*dentro de la función.a = b;sobre dos parámetros de tipo puntero intercambia las copias locales, no los valores. Lo que quieres es*a = *b;. - Usar
sizeofsobre un parámetro de tipo array. Siempre está mal. Pasa la longitud. - Devolver un puntero a una variable local. La local muere cuando la función retorna, así que quien llama se queda con un puntero colgante, que es comportamiento indefinido. Devuelve por valor, escribe en un búfer que suministre quien llama, o reserva con
malloc. - No comprobar si un parámetro de tipo puntero es
NULL. Una función que desreferencia cualquier cosa que le pasen va a reventar con un argumento nulo.
Preguntas frecuentes
¿C tiene paso por referencia?
No. C tiene exactamente un modo: paso por valor. Una función siempre recibe una copia de cada argumento, así que asignar a un parámetro nunca toca la variable de quien llama. Lo que parece paso por referencia es pasar un puntero por valor: el puntero se copia, pero sigue apuntando al objeto de quien llama, así que la función puede modificarlo a través del puntero.
¿Cómo se pasa un array a una función en C?
Escribe el parámetro como int a[] o int *a —significan lo mismo— y pasa la longitud como un parámetro aparte: int sum(const int a[], int n). El array no se copia; la función recibe un puntero a su primer elemento, así que los cambios en a[i] son visibles para quien llama.
¿Por qué sizeof no funciona sobre un parámetro de tipo array en C?
Porque el parámetro en realidad es un puntero. sizeof(a) dentro de la función da el tamaño de un puntero (normalmente 8 bytes), no el del array, así que sizeof(a)/sizeof(a[0]) da algo como 2 en lugar de la cantidad de elementos. Pasa siempre la longitud de forma explícita.
¿Cómo puede una función de C devolver más de un valor?
Pasa punteros para los resultados extra y escribe a través de ellos: int divide(int a, int b, int *quotient, int *remainder) devuelve un indicador de éxito y llena ambas salidas. La alternativa es devolver una estructura que agrupe los valores.