2011-05-05 22 views
42

He leído detenidamente los posibles duplicados, sin embargo ninguna de las respuestas no están hundiendo en¿Cómo funcionan los archivos de encabezado y fuente en C?

tl; dr:. ¿Cómo están los archivos de origen y de cabecera relacionados de C? ¿Los proyectos clasifican las dependencias de declaración/definición implícitamente en el tiempo de compilación?

Estoy tratando de entender cómo el compilador entiende la relación entre .c y .h archivos.

Dados estos archivos:

header.h:

int returnSeven(void); 

source.c:

int returnSeven(void){ 
    return 7; 
} 

main.c:

#include <stdio.h> 
#include <stdlib.h> 
#include "header.h" 
int main(void){ 
    printf("%d", returnSeven()); 
    return 0; 
} 

¿Compilará este desastre? Actualmente estoy haciendo mi trabajo en NetBeans 7.0 con gcc de Cygwin que automatiza gran parte de la tarea de compilación. Cuando se compila un proyecto, ¿los archivos de proyecto involucrados resolverán esta inclusión implícita de source.c según las declaraciones en header.h?

+1

Sí, esto va a compilar (y por qué pensar que es un "desastre "?). Los conceptos para aprender son ** unidades de compilación ** y ** vinculación **. – Jesper

+0

Gracias ** Jesper **; Jaja, no es un desastre, supongo que esa palabra es mejor reservada para describir mi cerebro, leyendo entre 3 libros de "C" de nivel para principiantes. Ciertamente examinaré * unidades de compilación * y * vinculación *, sin embargo, para enfocarme en la sintaxis de aprendizaje, dejaré que ** NetBeans ** + ** gcc ** resuelva esto por mí.Dado que, cada vez que un archivo de encabezado dado tiene declaraciones para las cuales existen definiciones en otras partes del proyecto, la inclusión de ese archivo de encabezado es suficiente para proporcionar acceso a la funcionalidad definida, y el compilador clasificará los detalles. – Dan

+1

'header.h' necesita incluir guardias;) – alternative

Respuesta

58

la conversión de archivos de código fuente C a un programa ejecutable se hace normalmente en dos pasos: compilar y une.

En primer lugar, el compilador convierte el código fuente en archivos de objetos (*.o). Luego, el enlazador toma estos archivos de objetos, junto con las bibliotecas vinculadas estáticamente y crea un programa ejecutable.

En el primer paso, el compilador toma una unidad compilación, que normalmente es un archivo fuente preprocesado (así, un archivo fuente con el contenido de todas las cabeceras que #include s) y convierte eso a un archivo de objeto .

En cada unidad de compilación, todas las funciones que se utilizan deben ser declaradas, para que el compilador sepa que la función existe y cuáles son sus argumentos. En su ejemplo, la declaración de la función returnSeven se encuentra en el archivo de encabezado header.h. Cuando compila main.c, incluye el encabezado con la declaración para que el compilador sepa que returnSeven existe cuando compila main.c.

Cuando el vinculador hace su trabajo, necesita encontrar la definición de cada función. Cada función debe definirse exactamente una vez en uno de los archivos de objetos: si hay varios archivos de objeto que contienen la definición de la misma función, el vinculador se detendrá con un error.

Su función returnSeven se define en source.c (y la función main se define en main.c).

Por lo tanto, para resumir, tiene dos unidades de compilación: source.c y main.c (con los archivos de encabezado que incluye). Compile estos en dos archivos de objetos: source.o y main.o. El primero contendrá la definición de returnSeven, el segundo la definición de main. Entonces el enlazador pegará esos dos en un programa ejecutable.

Acerca de vinculación:

No es enlazado externo y enlace interno. Por defecto, las funciones tienen un enlace externo, lo que significa que el compilador hace que estas funciones sean visibles para el vinculador.Si realiza una función static, tiene un enlace interno: solo está visible dentro de la unidad de compilación en la que está definida (el vinculador no sabrá que existe). Esto puede ser útil para funciones que hacen algo internamente en un archivo fuente y que desea ocultar del resto del programa.

+0

Gracias ** Jesper **; Su respuesta toca casi todos los puntos con los que estaba confundido. Gracias por la respuesta completa. – Dan

+0

4 años después, estoy revisando esta pregunta. Estoy un poco horrorizado. Aunque esta respuesta está bien escrita e informativa, casi no responde la pregunta real. Apenas menciona los archivos de encabezado –

+1

'casi no responde la pregunta real. Apenas menciona los archivos de encabezado. Esta es una de las mejores respuestas para explicar todo el proceso de compilación. –

23

El lenguaje C no tiene ningún concepto de archivos fuente y archivos de encabezado (y tampoco lo hace el compilador). Esto es meramente una convención; recuerde que un archivo de encabezado siempre es #include d en un archivo fuente; el preprocesador simplemente copia y pega los contenidos, antes de que comience la compilación adecuada.

Su ejemplo debería compilar (no obstante los errores de sintaxis estúpidos). Usando GCC, por ejemplo, primero puede hacer:

gcc -c -o source.o source.c 
gcc -c -o main.o main.c 

Esto compila cada archivo fuente por separado, creando archivos de objetos independientes. En esta etapa, returnSeven() no se ha resuelto dentro de main.c; el compilador simplemente marcó el archivo objeto de una manera que indica que debe resolverse en el futuro. Entonces en esta etapa, no es un problema que main.c no pueda ver una definición de returnSeven(). (Nota: esto es distinto del hecho de que main.c debe poder ver una declaración de returnSeven() para compilar; debe saber que de hecho es una función, y cuál es su prototipo. Es por eso que debe #include "source.h" en . main.c)

a continuación, hacer: archivos

gcc -o my_prog source.o main.o 

Este enlaces los dos juntos en un objeto binario ejecutable, y realiza la resolución de símbolos. En nuestro ejemplo, esto es posible, porque main.o requiere returnSeven(), y esto está expuesto por source.o. En los casos en que todo no coincida, se produciría un error del enlazador.

+1

(Nota: esto es distinto del hecho de que main.c debe poder ver una declaración de returnSeven(): estoy siendo pedante, pero esto no es del todo correcto. El compilador compilará felizmente (con una advertencia en C99) este código, y el enlazador lo resuelve, por lo general con malos efectos. Por ejemplo, en el archivo ac, llame a 'x = bob (1,2,3,4)' y en el archivo bc, 'void bob (char * a) {} 'compilará, vinculará y ejecutará – mattnz

+0

Absolutamente de primera clase. Amo las instrucciones minimalistas del compilador GCC ejemplos –

2

El compilador en sí no tiene un "conocimiento" específico de las relaciones entre los archivos fuente y los archivos de encabezado. Esos tipos de relaciones normalmente se definen mediante archivos de proyecto (por ejemplo, archivo MAKE, solución, etc.).

El ejemplo dado parece como si se compilara correctamente. Necesitaría compilar ambos archivos fuente y luego el enlazador necesitaría ambos archivos de objeto para producir el ejecutable.

4

Los archivos de encabezado se utilizan para separar las declaraciones de interfaz que corresponden a las implementaciones en los archivos de origen. Son abusados ​​de otras maneras, pero este es el caso común. Esto no es para el compilador, es para los humanos que escriben el código.

La mayoría de los compiladores en realidad no ven los dos archivos por separado, son combinados por el preprocesador.

10

No hay nada mágico acerca de la compilación. ¡Ni automático!

Los archivos de encabezado básicamente proporcionan información al compilador, casi nunca código.
Esa información sola, por lo general, no es suficiente para crear un programa completo.

considerar el programa "hola mundo" (con la simple función puts):

#include <stdio.h> 
int main(void) { 
    puts("Hello, World!"); 
    return 0; 
} 

sin la cabecera, el compilador no sabe cómo tratar con puts() (no es una palabra clave C). El encabezado le permite al compilador saber cómo administrar los argumentos y devolver el valor.

Cómo funciona la función, sin embargo, no se especifica en ningún lugar de este código simple. Alguien más ha escrito el código para puts() e incluido el código compilado en una biblioteca. El código en esa biblioteca se incluye con el código compilado para su fuente como parte del proceso de compilación.

Ahora consideran que quería su propia versión de puts()

int main(void) { 
    myputs("Hello, World!"); 
    return 0; 
} 

Compilación simplemente este código da un error porque el compilador no tiene información acerca de la función. Puede proporcionar esa información

int myputs(const char *line); 
int main(void) { 
    myputs("Hello, World!"); 
    return 0; 
} 

y el código ahora compila --- pero no se vincula, es decir, no produce un ejecutable, porque no hay ningún código para myputs(). Por lo que escribir el código para myputs() en un archivo llamado "myputs.c"

#include <stdio.h> 
int myputs(const char *line) { 
    while (*line) putchar(*line++); 
    return 0; 
} 

y hay que recordar que compilar tanto su primer archivo de origen y "myputs.c" juntos.

Después de un tiempo, su archivo "myputs.c" se ha expandido a una mano llena de funciones y necesita incluir la información sobre todas las funciones (sus prototipos) en los archivos fuente que desean usarlas.
Es más conveniente escribir todos los prototipos en un solo archivo y #include ese archivo. Con la inclusión no corre riesgo de cometer un error al escribir el prototipo.

Sin embargo, todavía tiene que compilar y vincular todos los archivos de código.


Cuando crecen aún más, se pone todo el código ya compilado en una biblioteca ... y eso es otra historia :)

Cuestiones relacionadas