Jak działają pliki nagłówkowe i źródłowe w C?
Przejrzałem możliwe duplikaty, jednak żadna z odpowiedzi nie tonie.
Tl; dr: w jaki sposób pliki źródłowe i nagłówkowe są powiązane w C
? Czy projekty domyślnie sortują zależności deklaracji/definicji w czasie budowania?
Próbuję zrozumieć, w jaki sposób kompilator rozumie związek pomiędzy .c
i .h
plikami.
Biorąc pod uwagę te pliki:
Nagłówek.h :
int returnSeven(void);
Źródło.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;
}
Czy ten bałagan się skompiluje? Obecnie pracuję w NetBeans 7.0 z gcc z Cygwina, który automatyzuje większość zadań budowania. Kiedy projekt jest kompilowany, czy zaangażowane pliki projektu uporządkują to Ukryte włączenie source.c
na podstawie deklaracji w header.h
?
5 answers
Konwersja plików kodu źródłowego C do programu wykonywalnego odbywa się zwykle w dwóch krokach: kompilowanie i łączenie .
Najpierw kompilator konwertuje kod źródłowy na pliki obiektowe (*.o
). Następnie linker pobiera te pliki obiektowe wraz ze statycznie powiązanymi bibliotekami i tworzy program wykonywalny.
W pierwszym kroku kompilator pobiera jednostkę kompilacji, która zwykle jest wstępnie przetworzonym plikiem źródłowym (czyli plikiem źródłowym z zawartość wszystkich nagłówków, które #include
s) i konwertuje je do pliku obiektowego.
W każdej jednostce kompilacji wszystkie użyte funkcje muszą być zadeklarowane , aby kompilator wiedział, że funkcja istnieje i jakie są jej argumenty. W twoim przykładzie deklaracja funkcji returnSeven
znajduje się w pliku nagłówkowym header.h
. Kiedy kompilujesz main.c
, dołączasz nagłówek z deklaracją, aby kompilator wiedział, że returnSeven
istnieje, gdy kompiluje main.c
.
Kiedy linker wykonuje swoje zadanie, musi znaleźć definicję każdej funkcji. Każda funkcja musi być zdefiniowana dokładnie raz w jednym z plików obiektowych - jeśli istnieje wiele plików obiektowych, które zawierają definicję tej samej funkcji, linker zatrzyma się z błędem.
Twoja funkcja returnSeven
jest zdefiniowana w source.c
(a main
funkcja jest zdefiniowana w main.c
).
Podsumowując, masz dwie jednostki kompilacji: source.c
i main.c
(z pliki nagłówkowe, które zawiera). Kompilujesz je do dwóch plików obiektowych: source.o
i main.o
. Pierwsza z nich będzie zawierać definicję returnSeven
, druga definicję main
. Następnie linker sklejy te dwa Razem w programie wykonywalnym.
O linkowaniu:
Istnieje zewnętrzne połączenie i wewnętrzne połączenie . Domyślnie funkcje mają zewnętrzne powiązania, co oznacza, że kompilator sprawia, że funkcje te są widoczne dla linkera. Jeśli zrobisz funkcja static
, posiada wewnętrzne powiązanie - jest widoczna tylko wewnątrz jednostki kompilacji, w której jest zdefiniowana(linker nie będzie wiedział, że istnieje). Może to być przydatne dla funkcji, które robią coś wewnętrznie w pliku źródłowym i które chcesz ukryć przed resztą programu.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-05 22:16:34
Język C nie ma pojęcia o plikach źródłowych i plikach nagłówkowych (podobnie jak kompilator). Jest to tylko konwencja; pamiętaj, że plik nagłówkowy jest zawsze #include
d w pliku źródłowym; preprocesor dosłownie po prostu kopiuje-wkleja zawartość, zanim rozpocznie się właściwa kompilacja.
Twój przykład powinien skompilować (bez względu na głupie błędy składniowe). Na przykład używając GCC, możesz najpierw wykonać:
gcc -c -o source.o source.c
gcc -c -o main.o main.c
Kompiluje każdy plik źródłowy osobno, tworzenie niezależnych plików obiektowych. Na tym etapie returnSeven()
nie został rozwiązany wewnątrz main.c
; kompilator tylko oznaczył plik obiektowy w sposób, który stwierdza, że musi zostać rozwiązany w przyszłości. Więc na tym etapie nie jest problemem, że main.c
nie widzi definicji z returnSeven()
. (Uwaga: jest to odmienne od faktu, że main.c
musi być w stanie zobaczyć deklarację z returnSeven()
w celu kompilacji; musi wiedzieć, że rzeczywiście jest to funkcja i jaki jest jej prototyp. Że dlatego musisz #include "source.h"
W main.c
.)
Potem robisz:
gcc -o my_prog source.o main.o
To łączy dwa pliki obiektowe razem w wykonywalny plik binarny i wykonuje rozdzielczość symboli. W naszym przykładzie jest to możliwe, ponieważ main.o
wymaga returnSeven()
, a to jest ujawnione przez source.o
. W przypadkach, gdy wszystko się nie zgadza, spowodowałby błąd łącznika.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-05 22:08:15
Nie ma nic magicznego w kompilacji. Ani automatyczne!
Pliki nagłówkowe w zasadzie dostarczają informacji kompilatorowi, prawie nigdy nie kodują.
Sama ta informacja zwykle nie wystarcza do stworzenia pełnego programu.
Rozważ program "hello world" (z prostszą funkcją puts
):
#include <stdio.h>
int main(void) {
puts("Hello, World!");
return 0;
}
Bez nagłówka kompilator nie wie, jak radzić sobie z puts()
(nie jest to słowo kluczowe C). Nagłówek pozwala kompilatorowi wiedzieć, jak zarządzać argumentami i zwracana wartość.
Sposób działania funkcji nie jest jednak nigdzie określony w tym prostym kodzie. Ktoś inny napisał kod puts()
i włączył skompilowany kod do biblioteki. Kod w tej bibliotece jest dołączany do skompilowanego kodu źródłowego w ramach procesu kompilacji.
Teraz Uznaj, że chciałeś mieć własną wersję puts()
int main(void) {
myputs("Hello, World!");
return 0;
}
Kompilowanie tylko tego kodu powoduje błąd, ponieważ kompilator nie ma informacji o funkcji. Ty może dostarczyć te informacje
int myputs(const char *line);
int main(void) {
myputs("Hello, World!");
return 0;
}
A kod teraz kompiluje - - - ale nie linkuje, ie nie tworzy pliku wykonywalnego, ponieważ nie ma kodu dla myputs()
. Więc piszesz kod dla myputs()
w pliku o nazwie " myputs.c "
#include <stdio.h>
int myputs(const char *line) {
while (*line) putchar(*line++);
return 0;
}
I musisz pamiętać, aby skompilować zarówno swój pierwszy plik źródłowy, jak i " myputs.c " razem.
Po chwili twoje " myputs.c " plik rozszerzył się do ręki pełnej funkcji i trzeba zawierać informacje o wszystkich funkcje (ich prototypy) w plikach źródłowych, które chcą z nich korzystać.
Wygodniej jest zapisać wszystkie prototypy w jednym pliku i #include
tego pliku. Dzięki włączeniu nie ryzykujesz popełnienia błędu podczas pisania prototypu.
Nadal musisz skompilować i połączyć wszystkie pliki kodu razem.
Gdy rosną jeszcze bardziej, umieszczasz cały skompilowany kod w bibliotece ... i to już inna historia:)
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-05 22:25:17
Pliki nagłówkowe są używane do oddzielania deklaracji interfejsu, które odpowiadają implementacjom w plikach źródłowych. Są maltretowani w inny sposób, ale jest to powszechny przypadek. To nie jest dla kompilatora, tylko dla ludzi piszących kod.
Większość kompilatorów nie widzi tych dwóch plików osobno, są one połączone przez preprocesor.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-05 22:01:08
Sam kompilator nie ma specyficznej "wiedzy" o relacjach między plikami źródłowymi a plikami nagłówkowymi. Te typy relacji są zazwyczaj definiowane przez pliki projektu (np. makefile, solution, itp.).
Podany przykład wygląda tak, jakby skompilował się poprawnie. Trzeba by skompilować oba pliki źródłowe, a następnie linker będzie potrzebował obu plików obiektowych do wytworzenia pliku wykonywalnego.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-05 21:58:52