El compilador lee de arriba abajo
Un compilador de C procesa un archivo fuente en una pasada, de la primera línea a la última. Cuando llega a una llamada a una función, necesita saber ya tres cosas: qué devuelve la función, cuántos argumentos toma y de qué tipos son. Sin eso no puede generar código correcto ni comprobar la llamada.
Así que esto es un problema:
#include <stdio.h>
int main(void) {
printf("%d\n", add(2, 3)); /* add todavia no se ha visto */
return 0;
}
int add(int a, int b) {
return a + b;
}
La solución es un prototipo: la firma de la función, escrita antes de la llamada, con un punto y coma donde iría el cuerpo.
Ahora main puede ir primero, que es como se organizan la mayoría de los archivos de C: los prototipos de arriba dicen qué ofrece el archivo, main se lee como el esquema general y los detalles vienen debajo.
Declaración frente a definición
Dos palabras fáciles de confundir y que vale la pena mantener separadas:
- Una declaración dice que una función existe y da su tipo. Termina en punto y coma y no tiene cuerpo. Puedes escribirla tantas veces como quieras.
- Una definición da el cuerpo. Debe aparecer exactamente una vez en todo el programa: dos definiciones de la misma función son un error del enlazador ("multiple definition of").
int add(int a, int b); /* declaracion (prototipo) */
int add(int a, int b) { return a + b; } /* definicion, tambien una declaracion */
Los nombres de los parámetros son opcionales en un prototipo; al compilador solo le importan los tipos:
int add(int, int); /* legal y equivalente */
int add(int a, int b); /* mejor: los nombres documentan el orden */
Usa los nombres. void drawRect(int, int, int, int); no le dice nada a quien lee, mientras que void drawRect(int x, int y, int width, int height); se lo dice todo.
Qué te da el prototipo
No solo la posibilidad de reordenar definiciones: es lo que hace que el compilador compruebe tus llamadas.
Con el prototipo en ámbito, el 3 y el 2 se convierten a double antes de la llamada. Sin él se empujarían como int y scale los leería como doubles: basura, y sin ningún diagnóstico en los dialectos antiguos de C.
El prototipo también detecta cantidades de argumentos equivocadas y tipos incompatibles en tiempo de compilación:
scale(3.0); /* error: faltan argumentos */
scale(3.0, "dos"); /* error: se pasa char * donde se espera double */
Esos son los errores que quieres: ruidosos, en tiempo de compilación y señalando la línea.
Errores de declaración implícita
Llama a una función de la que el compilador nunca ha oído hablar y obtienes uno de los mensajes más comunes de C:
warning: implicit declaration of function 'add' [-Wimplicit-function-declaration]
En C89, el compilador adivinaba: suponía que la función devolvía int y aceptaba los argumentos que le pasaras. Esa suposición está equivocada más veces que no, y cuando la función real devuelve un double o un puntero el resultado es un disparate. C99 eliminó las declaraciones implícitas del lenguaje, así que esto es un error en C99 y posteriores: GCC y clang modernos lo rechazan por defecto en sus versiones recientes.
Dos causas, dos soluciones:
Tu propia función, aún no declarada. Agrega el prototipo encima de la llamada, o mueve la definición más arriba.
Una función de biblioteca cuya cabecera olvidaste. Los prototipos de la biblioteca viven en su cabecera, así que necesitas el #include:
printf, scanf, fopen -> #include <stdio.h>
malloc, free, exit, atoi -> #include <stdlib.h>
strlen, strcpy, strcmp -> #include <string.h>
sqrt, pow, sin, fabs -> #include <math.h>
isdigit, toupper -> #include <ctype.h>
bool, true, false -> #include <stdbool.h>
Un mensaje emparentado, conflicting types for 'add', significa que el prototipo y la definición no coinciden: difiere el tipo de un parámetro, o el de retorno. Arregla el que esté mal; tienen que coincidir exactamente.
La recursión mutua necesita un prototipo
A veces reordenar no lo resuelve: dos funciones que se llaman entre sí no pueden ir ambas primero.
Un solo prototipo rompe el círculo. Este es el mismo mecanismo del que depende una función recursiva corriente: el nombre de una función está en ámbito dentro de su propio cuerpo, así que la recursión directa no necesita prototipo, pero la mutua sí.
Prototipos en archivos de cabecera
Una vez que un programa abarca varios archivos .c, los prototipos se mudan a una cabecera para que todos los archivos vean las mismas declaraciones desde un solo lugar.
mathutils.h, la interfaz:
#ifndef MATHUTILS_H
#define MATHUTILS_H
int add(int a, int b);
int multiply(int a, int b);
double average(const int values[], int count);
#endif
mathutils.c, la implementación:
#include "mathutils.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
double average(const int values[], int count) {
if (count == 0) return 0.0;
int total = 0;
for (int i = 0; i < count; i++) total += values[i];
return (double) total / count;
}
main.c, un usuario:
#include <stdio.h>
#include "mathutils.h"
int main(void) {
printf("%d\n", add(2, 3));
return 0;
}
Y entonces:
gcc main.c mathutils.c -o program
Aquí hay dos hábitos que vale la pena adoptar. La envoltura #ifndef / #define / #endif es un guardián de inclusión: impide que las declaraciones se procesen dos veces si la cabecera llega por dos caminos. Y mathutils.c incluye su propia cabecera, lo que parece redundante pero no lo es, porque hace que el compilador contraste cada definición con la declaración que usarán los demás archivos. Si alguna vez se desincronizan, te enteras de inmediato en lugar de al enlazar. Archivos de cabecera cubre el resto.
Los ángulos (<stdio.h>) buscan en las rutas de inclusión del sistema; las comillas ("mathutils.h") buscan primero en el directorio de tu proyecto. Usa comillas para tus propias cabeceras.
f(void) frente a f()
Este punto sorprende de verdad, y es la razón por la que todos los ejemplos de esta documentación escriben int main(void).
void ping(void); /* NO toma argumentos: las llamadas con argumentos se rechazan */
void pong(); /* no dice NADA sobre los parametros */
void ping(void); es un prototipo: declara que la función no toma parámetros, así que ping(1, 2, 3) es un error de compilación.
void pong(); es una declaración al viejo estilo heredada del C preestándar. Declara el tipo de retorno y nada en absoluto sobre los parámetros, así que el compilador no puede comprobar las llamadas: pong(1, 2, 3) compila sin ruido y hace algo indefinido.
int main(void) { /* correcto: main no toma argumentos */
int main() { /* legal, pero sin comprobacion de argumentos */
Escribe siempre (void) para una función sin parámetros. C23 cambia () para que signifique lo mismo que (void), lo que por fin elimina la trampa, pero mucho código y muchos compiladores no han llegado ahí, y (void) es correcto en todos los estándares de C.
La misma distinción se aplica a la definición. void ping(void) { } es una definición con estilo de prototipo; void ping() { } no lo es, y no habilita la comprobación de las llamadas que aparezcan antes.
Errores comunes
- Punto y coma en la definición.
int add(int a, int b); { return a + b; }declaraaddy luego deja un bloque suelto. El mensaje de error confunde; la causa es un solo carácter. - Falta el punto y coma del prototipo. El compilador sigue leyendo lo que venga después y reporta algo desconcertante varias líneas más abajo.
- El prototipo y la definición no coinciden.
conflicting types for .... Hazlos idénticos o, mejor, incluye la cabecera en el archivo de implementación para que la comprobación sea automática. - Declarar una función dentro de otra. Es legal (
int add(int, int);dentro demain), pero la declaración queda limitada al ámbito de esa función. Pon los prototipos en el ámbito del archivo. - Definir una función en una cabecera. Inclúyela desde dos archivos
.cy el enlazador reporta una definición duplicada. Las cabeceras llevan declaraciones; las definiciones van en un archivo.c. - Confiar en
()para la comprobación. No comprueba nada. Escribe(void).
Preguntas frecuentes
¿Qué es un prototipo de función en C?
Una declaración de la firma de una función —tipo de retorno, nombre y tipos de los parámetros— que termina en punto y coma en vez de un cuerpo: int add(int a, int b);. Le dice al compilador cómo se llama a la función para que las llamadas puedan comprobarse, sin decir qué hace.
¿Cuál es la diferencia entre una declaración y una definición en C?
Una declaración introduce el nombre y el tipo (int add(int, int);) y puede aparecer muchas veces. Una definición aporta el cuerpo (int add(int a, int b) { return a + b; }) y debe aparecer exactamente una vez en todo el programa. Toda definición es además una declaración.
¿Cómo arreglo "implicit declaration of function" en C?
Agrega un prototipo antes de la llamada. Para tus propias funciones, pon tipoDeRetorno nombre(tiposDeParametros); cerca del inicio del archivo o en una cabecera; para funciones de biblioteca, incluye la cabecera correcta: #include <stdio.h> para printf, <stdlib.h> para malloc, <string.h> para strlen, <math.h> para sqrt.
¿Cuál es la diferencia entre f(void) y f() en C?
void f(void); declara una función que no toma argumentos, y el compilador rechaza cualquier llamada que le pase alguno. void f(); es una declaración al viejo estilo que no dice nada sobre los parámetros, así que la comprobación de argumentos queda desactivada. Escribe siempre (void); C23 por fin hace que ambas signifiquen lo mismo, pero el código y los compiladores antiguos todavía difieren.